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