서명이 날인된 PDF 문서를 전달받은 상황에서, 뷰어 프로그램 상에 서명자 신원, 서명 실행 시점, 서명이 문서 전체 영역을 올바르게 커버하고 있는지 여부, 그리고 장기 보존 표준 규격을 충족하는지 표시해야 하는 요구 사항이 있을 수 있습니다. Delphi 및 Lazarus용 PDFium 컴포넌트는 FPDF_GetSignatureCount 및 FPDFSignatureObj_* 계열의 읽기 전용 API를 통해 서명 딕셔너리를 노출하며, TPdf.ValidatePades 메서드로 PAdES 표준 규격의 상세 레벨을 검출하여 이 모든 요구 사항을 깔끔히 해결해 줍니다. 이 문서는 PDFium 디지털 서명 연동 기획 3부작 중 첫 번째 가이드이며, 후속 가이드에서는 PAdES B-B 서명 작성 및 장기 검증용 타임스탬프 탑재 프로세스를 안내합니다. 단, 사전 정의할 분명한 한계선이 있습니다: 본 가이드에서 가이드하는 모든 내용은 순수 '구조 분석(Inspection)' 영역이며, 서식에 기록된 서명 주장을 판독하는 연산은 암호학적 신뢰성을 실검증하거나 서명자의 인증 권한 신용도를 평가하는 절차와는 완벽히 구별됩니다
PDF 서명 딕셔너리가 단순한 바이너리 블록이 아닌 이유
ISO 32000-1 §12.8 규격은 서명 딕셔너리를 /ByteRange 및 /Contents 엔트리로 정의합니다. /Contents는 서명자 인증서, 서명 속성 정보 및 실제 디지털 서명 값을 운반하는 암호학적 외포인 16진수 인코딩된 CMS SignedData structure(RFC 5652)를 포함합니다. /ByteRange는 많은 개발자들이 과소평가하기 쉬운 핵심 요소입니다: 서명 문자열(/Contents)이 위치하는 영역만 정확하게 제외하고 파일의 서두부터 끝까지 전 영역을 포함하는 두 개의 오프셋-길이 쌍의 정형 배열입니다. 이 제외된 빈 틈이 서명 바이너리 데이터가 실재하는 슬롯이며, 이 틈의 앞과 뒤에 존재하는 두 영역이 바로 해당 서명이 보증을 선 실 데이터 바이트 범위가 됩니다
이 ByteRange 설계 아키텍처 덕분에 증분 저장(Incremental save)된 문서의 위변조 여부를 정밀 감사할 수 있습니다. 서명자는 계산 시점에 아직 존재하지 않는 서명 데이터 자체를 해싱(Hash)할 수 없기 때문에, 파일 전체를 /Contents 플레이스홀더를 중심으로 물리적으로 쪼개어 그 틈을 뺀 나머지 전 구역을 해싱하여 서명 값을 생성합니다. 만약 ByteRange 범위가 물리 파일의 끝단에 가닿지 않는 서명이 발견된다면 이는 위험 신호입니다: 서명이 커버하는 범위 밖의 파일 후미에 증분 저장을 통해 무단 텍스트를 덧붙여도, 서명 검증 엔진은 이를 정상 서명으로 간주하여 고객 화면의 위변조 데이터를 감지하지 못하게 됩니다. 따라서 숙련된 감사 엔진이 최초로 스캔해야 할 지점은 서명자 신원이 아니라, 서명이 파일의 전체 물리 바이트 영역을 빈틈없이 커버하고 있는지 확인하는 일입니다
PDFium 읽기 전용 API를 사용한 서명 딕셔너리 정밀 판독
PDFium 컴포넌트는 두 가지 읽기 전용 속성인 SignatureCount 및 Signature[Index] 레코드를 통해 서명 딕셔너리를 노출합니다. 내부적으로는 FPDF_GetSignatureCount, FPDF_GetSignatureObject 및 /SubFilter, /ByteRange, /Contents, /Reason, 서명 일시 데이터를 파싱하는 FPDFSignatureObj_* 계열 API들이 긴밀히 조율됩니다. 여기서 명심할 점은 '읽기 전용'이라는 속성입니다: PDFium은 기존 서명을 순회 탐색하고 읽을 수는 있으나 신규 서명을 직접 저작하거나 파일 스트림에 기입하는 API를 자체 제공하지 않습니다. 이 때문에 본 연재물에서 구현할 신규 서명 날인 및 파일 저장 로직은 PDFium API가 아닌 당사 라이브러리 엔진의 고유 구현부를 통해 수행됩니다
var
Pdf: TPdf;
i: Integer;
Sig: TPdfSignature;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
for i := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[i];
Writeln('SubFilter : ', Sig.Encoding); // ETSI.CAdES.detached, adbe.pkcs7.detached 등
Writeln('Signed at : ', Sig.Time); // 서명자가 선언한 날짜 문자열, 예: D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // /Contents에서 발췌한 원시 DER SignedData
Writeln('DocMDP : ', Sig.Permission); // 0 = 인증 서명 없음, 1..3 = MDP 문서 보안 레벨
end;
finally
Pdf.Free;
end;
end;
각 TPdfSignature 레코드는 서명 딕셔너리 구조에 일대일 매핑됩니다. Encoding은 가장 핵심적인 진단 수치가 되는 /SubFilter 정보를 나타냅니다. 이는 서명 핸들러 명칭을 직접 알려주며 현대적인 ETSI.CAdES.detached 규격의 서명과 이미 규격 취소되거나 금지된 구형 포맷을 직관적으로 분류해 줍니다. Time은 서명자 시스템 클럭 기준으로 기록되는 문서 날짜 정보로, 서명자의 일방적 주장 수치일 뿐 신뢰 가능한 공인된 시간 정보로 간주해서는 안 됩니다. Content는 암호 외포의 날것 형태인 원시 CMS SignedData를 의미하며, Permission은 문서 보호 등급을 제어하는 DocMDP 인증 레벨(0은 단순 결재 서명, 1~3은 이후 문서 변경을 원천 금지하는 강력한 잠금 인증 서명)을 표시합니다. 이 레코드 구조에서 ByteRange의 정밀 바이트 크기를 직접 노출하지 않는 이유는, ValidatePades 검증 함수가 이 복잡한 바이트 영역 정산 및 잔여 영역 산출 연산을 백엔드에서 완벽하게 대리 수행해 주기 때문입니다
PAdES B-B, B-T, B-LT, B-LTA 규격 레벨의 차이점
네 가지 PAdES 표준 규격 등급(Baseline level)은 가장 기본적 수준의 무결성 서명부터 수십 년에 달하는 영구 보존(Archival)을 목적으로 다듬어진 고신뢰성 서명에 이르기까지 순차적인 기술 계층을 형성하며, 상위 레벨은 하위 레벨의 모든 스펙을 완전 포함합니다. ETSI EN 319 142-1 표준은 이들을 B-B, B-T, B-LT, B-LTA로 정의합니다. B-B(Basic)는 필수 서명 서식 기입 속성을 구비한 기본 등급입니다. B-T(Timestamp)는 서명 위에 신용 가능한 RFC 3161 공인 타임스탬프를 보강하여, 임의 조작이 가능한 로컬 PC 클럭 대신 공인 시각 인증 기관(TSA)이 보증하는 객관적 서명 일시를 확인(Attested) 처리합니다. B-LT(Long-Term)는 파일 내에 서명 검증을 위한 원시 리소스(인증서 체인 전체 경로 및 OCSP/CRL 실시간 해지 검증 응답값)를 통째로 임베딩(DSS)하여, 수년 뒤 인증서가 만료되거나 발급 기관 시스템이 사라진 먼 미래에도 서명 검증을 독립 완수할 수 있게 돕습니다. B-LTA(Archive timestamp)는 DSS 정보 전체를 감사 스탬프(Document timestamp)로 감싸 보호함으로써, 장기 검증 데이터의 무결성 자체를 반영구 수호하며 암호학적 알고리즘 만료 전에 타임스탬프를 덧씌워 유효 수명을 연장할 수 있는 토대를 마련합니다
실무적인 관점에서 이는 검증 유효 기간(Time horizon)과 직결됩니다. B-B 서명은 '누군가 서명했는가'라는 기초 질문에 답합니다. B-T는 '그것이 정확히 언제 증빙 가능한지'를 보장합니다. B-LT는 '인증서가 사멸한 먼 훗날에도 이 유효성을 판정할 수 있는지'를 확보합니다. B-LTA는 '20년이 지난 뒤에도 이 문서가 위변조되지 않았음을 입증할 수 있는지'를 책임집니다. 국내외의 다양한 조세 법령 및 eIDAS 규격 등은 최소 B-T 레벨 이상을 의무 요구하며, 영구 아카이빙 분야는 B-LT 또는 B-LTA 단계를 필수로 지정합니다. 문서 접수 또는 승인 단계에서 해당 파일이 도달한 규격 단계를 정밀 선별하는 것이 이 분석 단계의 핵심 목표입니다
TPdf.ValidatePades를 사용한 PAdES 등급 식별
PDFium 컴포넌트는 이 복잡한 등급 분석 연산을 단 한 번의 호출로 해소합니다. TPdf.ValidatePades는 분석 결과(TPadesValidationResult 레코드)를 통해 TPadesLevel 구조(plNone, plUnknown, plB_B, plB_T, plB_LT, plB_LTA)를 인계하며, 발견된 상세 구조 에러 목록(Issues)과 서명 수 및 타임스탬프 수를 함께 리턴합니다. 판별 알고리즘은 수학적인 순차 판별법을 추종합니다: B-B 요건을 우선 확인하고, 서명 타임스탬프 또는 문서 타임스탬프가 발견되면 B-T로 등급을 승급시키며, 문서 카탈로그 정보에 인증서 DSS 정보 및 /Extensions /ESIC Level 1 지시자가 구비된 것이 진증되면 B-LT로, 여기에 아카이브 타임스탬프와 ESIC Level 2 규격이 모두 확보된 경우 최종 B-LTA 레벨을 확정합니다. 두 가지 유용한 단축 헬퍼 메서드가 제공됩니다: 최소 B-B 등급 이상이 확립되고 발견된 에러 플래그가 전무한 완벽한 상태인지를 판별해 주는 IsCompliant와, 프로그램 정책상 요구되는 최소 수치인 B-T 정책 한계선 등을 저격 검사해 주는 IsCompliantAt입니다
var
Pdf: TPdf;
R: TPadesValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
case R.Level of
plNone: Writeln('PAdES 서명이 존재하지 않음');
plUnknown: Writeln('서명이 실재하나 규격 레벨 판별 불가');
plB_B: Writeln('PAdES B-B (기본 서명)');
plB_T: Writeln('PAdES B-T (공인 타임스탬프 탑재)');
plB_LT: Writeln('PAdES B-LT (장기 검증 인증서 임베딩)');
plB_LTA: Writeln('PAdES B-LTA (아카이브 타임스탬프 보호)');
end;
Writeln('Signatures : ', R.SignatureCount);
Writeln('DocTimeStamps: ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('B-T 필수 정책 조건을 충족합니다')
else
Writeln('요구되는 B-T 필수 정책 미만입니다');
finally
Pdf.Free;
end;
end;
왜 adbe.pkcs7.sha1 서명 규격이 전면 금지되나요?
보안 강도가 취약해 완전히 용도 폐기된 SHA-1 해시 알고리즘을 adbe.pkcs7.sha1 핸들러가 물리적으로 고정 강제하고 있기 때문입니다. 해당 SubFilter는 문서 내용을 SHA-1 해시로 미리 다진 뒤 PKCS#7 포맷으로 포장하는데, SHA-1은 이미 충돌 공격(Collision-vulnerable)에 취약함이 공인된 알고리즘이므로 EN 319 142-1 규범 6.3조는 PAdES 디지털 서명 생태계에서 이를 원천 사용 금지합니다. ValidatePades는 adbe.pkcs7.sha1 또는 adbe.x509.rsa_sha1이 식별되는 즉시 보안 금지 에러(ppeiForbiddenSubFilter)를 지정하며, 내부 CMS 레코드 단에서 MD5나 SHA-1이 체크되는 경우에도 즉각 검증 에러(ppeiBadDigestAlgorithm)를 활성화시킵니다. 이는 동일한 수학적 취약성을 서로 다른 계층에서 이중으로 정밀 스캔해 예방하기 위한 핵심 제어 조치입니다
에러 플래그 셋은 총 26가지 항목으로 세분화되어 있으며, 실무에서 흔히 마주치는 오류들은 대개 물리적 영역 및 구조 결함 영역에 집중되어 있습니다. 앞서 설명한 서명 커버리지 부족 건은 ppeiByteRangeNotCoveringFile로 정산됩니다. 서명 딕셔너리 하단에 /Cert 엔트리를 잘못 중복 탑재한 오동작(PAdES 규격상 인증서 전체 경로는 반드시 CMS SignedData.certificates 내에 고정 배치되어야 함)은 ppeiForbiddenCertKey로 식별됩니다. 필수적인 서명 속성값들이 누락된 정황들은 ppeiMissingSigningCertificate, ppeiMissingContentType, ppeiMissingMessageDigest 등으로 잡히며, 외부 분리 형태가 아닌 본체 데이터를 무단 병합하고 있는 서명은 ppeiDetachedContentViolation으로 검출됩니다. 에러 집합들을 순회하면 단순한 '검증 실패' 통지를 넘어 정밀한 에러 로그 추적 시스템을 구축할 수 있습니다
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
ValidatePades가 검증하지 않는 한계 영역
ValidatePades는 오직 구조 규격의 정합성만 검사할 뿐 신용(Trust) 자체를 보증하지 않으며, 이 둘의 한계를 혼동하는 것은 보안상 대단히 위험한 오독입니다. 검출 결과가 plB_LTA로 나왔다는 사실은 문서가 규격 지침에 입각한 온전한 속성과 아카이브 타임스탬프 구조를 지니고 있다는 물리 정황을 확인해 줄 뿐이며, 실제 서명 값이 암호학적으로 온전한지, 서명자가 신뢰할 수 있는 루트 인증서 체인에 연결되어 있는지, 혹은 해당 인증서가 실시간으로 해지 취소(Revoked)되지는 않았는지를 입증해 주지는 않습니다. 본 검증 모듈은 네트워크 기동이나 플랫폼 종속적인 암호 모듈 개입을 전면 차단하기 위해 암호 연산(ByteRange 해시 재조합 연산, 해지 OCSP/CRL 실검사 등)을 고의적으로 수행하지 않습니다. 이 엄격한 설계상 분리는 매우 유용하며, 가벼운 구조 정합성 검사만 순수 Pascal 기반의 빠른 속도로 Windows, Linux, macOS 환경에서 완전 동일하게 무의존 기동할 수 있게 해줍니다. 이와 달리 신뢰 체인 검증 연산은 어떤 루트 기관을 신용할 것인지, 해지 조회를 어떻게 캐시 처리할지 등 기업 정책과 밀접히 연결되므로 플랫폼 인증서 저장소에 기대한 후속 보안 단에서 정산 가공해야 합니다. 따라서 ValidatePades 통과 판정은 서명의 구조적 규격 적합성 통과라는 필수 관문으로 정립하고, 이 관문을 통과한 정상 껍데기 파일에 대해서만 실질적인 암호학적 신뢰 검증 로직을 최종 통과시킨 후 실무 연동에 활용해 가십시오
이러한 구조적 정합성 검사는 전체 보안 파이프라인의 훌륭한 시발점이 되며, PDFium 컴포넌트를 사용한 PDF 보안 리스크 감사 가이드 및 규격 Conformance 검사법을 다루는 출력용 PDF/X 표준 규격 적합성 검사 가이드와 훌륭한 시너지를 냅니다. 서명 정보를 완벽히 읽고 분류하는 방법을 습득한 뒤의 다음 단계는 직접 서명을 날인해 저장하는 것입니다: 연재의 두 번째 문서인 PAdES B-B 표준 서명 날인 구현 가이드를 거쳐, 세 번째 문서인 공인 타임스탬프 및 DSS가 탑재되는 장기 아카이빙용 B-LT/B-LTA 서명 저장 프로세스로 완성해 갈 수 있습니다. 여기에 소개된 읽기 전용 서명 분석 도구 및 ValidatePades 등급 판별기는 모두 Delphi, C++Builder 및 Lazarus용 PDFium 컴포넌트 최신 패키지에 기본 내장되어 있습니다