서명 검증

문서에 들어 있는 전자서명들을 검증합니다. 라이브러리는 값 추출과 결과 종합만 담당하고, 실제 암호 검증(위변조 판정, 인증서 상태)은 콜백으로 위임됩니다. 어느 인증서 툴킷을 쓸지는 서버가 결정합니다. 정적 Verifier.verifyAll()로 검증합니다.

1.결과 모델: 서명과 인증서는 다른 축

서명 상태(Verdict)는 “원문이 바뀌었는가”, 인증서 상태(CertStatus)는 “서명한 인증서가 유효한가”입니다. “서명은 정상인데 인증서는 만료”가 실제로 존재하므로 두 축을 섞지 않고 따로 보고합니다.

축값의미
Verdict(서명 상태)VALID 정상서명이 원문과 일치
TAMPERED 변조서명은 읽었으나 원문과 불일치, 내용이 바뀜
UNVERIFIABLE 검증불가서명 형식/벤더를 읽지 못함 (변조 아님, 미지원)
TIMESTAMP 타임스탬프사람 서명이 아니라 시각증명(RFC 3161)
CertStatus(인증서 상태)VALID 유효현재 시각 기준 유효, 폐기 안 됨
EXPIRED 만료유효기간 지남
REVOKED 폐기발급기관이 폐기 (OCSP/CRL)
UNKNOWN 불명인증서를 못 읽거나 폐기조회 실패
• 왜 「변조」와 「검증불가」를 나누나

검증 실패를 false 하나로만 표기하면 변조인지 검증불가인지 구분할 수 없어 정상 문서를 위조로 오인합니다. 그래서 둘을 나눠 표기합니다. 예: 타사 ETSI.CAdES.detached 서명을 국내 툴킷으로 검증하면 파싱조차 안 되지만, 변조는 아닙니다.

2.사용 (Java)

JAVA검증 엔드포인트
import kr.co.inogard.coredocs.pdf.tools.Verifier;
import kr.co.inogard.coredocs.pdf.tools.Verifier.Outcome;
import kr.co.inogard.coredocs.pdf.tools.Verifier.Result;
import kr.co.inogard.coredocs.pdf.tools.Verifier.Verdict;
import kr.co.inogard.coredocs.pdf.tools.Verifier.CertStatus;

@GetMapping("/document/{name}/verify")
public List<Result> verify(@PathVariable String name) throws IOException {
  byte[] pdfBytes = Files.readAllBytes(pdfPath.resolve(name));

  // 문서의 모든 서명을 순차 검증. 콜백이 실제 판정(인증서 툴킷) 담당
  return Verifier.verifyAll(pdfBytes, (input) -> {
    // input.signedContent : 원문 (ByteRange로 잘라낸 서명 대상 바이트)
    // input.signature     : 서명값 (PKCS#7 / 타임스탬프 토큰)
    // input.subFilter     : /SubFilter, 서명/타임스탬프 판단 근거

    // 타임스탬프(RFC 3161)는 사람 서명이 아니라 시각증명 → SubFilter로 먼저 분류
    if ("ETSI.RFC3161".equals(input.subFilter)) {
      return new Outcome(Verdict.TIMESTAMP, CertStatus.VALID, "(TSA)", 발급자DN);
    }

    boolean ok = 인증서툴킷.verifyDetached(input.signature, input.signedContent);
    Verdict verdict = ok ? Verdict.VALID : Verdict.TAMPERED;

    CertStatus certStatus = 인증서툴킷.isValidCertificate(인증서)
        ? CertStatus.VALID : CertStatus.REVOKED;

    return new Outcome(verdict, certStatus, 서명자CN, 발급자DN);
  });
}

3.API

메서드 / 타입설명
verifyAll(pdfBytes, 콜백)모든 서명 검증 → List<Result> (서명 없으면 빈 목록)
verify(pdfBytes, index, 콜백)N번째 서명만 검증 (0-based, 범위 밖이면 IndexOutOfBoundsException)
VerificationInput콜백 입력: signedContent(원문), signature(서명값), subFilter
Outcome(integrity, certStatus, signerCn, issuerDn)콜백 반환(integrity=서명 상태 Verdict). null 반환 시 검증불가/불명 처리
Result서명별 결과: index, verdict, certStatus, signerName, issuerDn, signedTime, reason
• 주의

인증서 상태 확인(폐기 조회)은 인증서 툴킷의 전체 의존성이 서버 classpath에 있어야 정상 동작합니다. 국내 인증서는 확장 알고리즘(HAS-160 등) 구현 라이브러리가 빠지면 유효한 인증서가 「불명/폐기」로 잘못 보고될 수 있습니다. 또한 검증은 편집용 모델 구성 없이 PDF 바이트만으로 동작하므로, 타사 서명 문서도 로딩 단계에서 실패하지 않습니다.

4.결과 예시

검증은 서버 전용 API라 뷰어 실행 예제가 없습니다. 아래는 서명 2개 + 타임스탬프 1개가 든 문서의 verifyAll 결과 예시입니다.

#서명자서명 상태인증서 상태서명 시각
0홍길동VALID 정상VALID 유효2026-08-18 10:12
1김철수VALID 정상EXPIRED 만료2026-08-18 10:15
2(TSA)TIMESTAMP 타임스탬프VALID 유효2026-08-18 10:16

1번처럼 「서명은 정상인데 인증서는 만료」가 두 축을 나누는 이유입니다. 문서 위변조 여부와 인증서 유효성은 별개로 판단해 사용자에게 보여주십시오.