Delphi용 PDFium Component에서 분석한 결과, 검증기가 PAdES 서명을 거부하는 원인은 거의 대부분 세 가지 이유 중 하나에 해당합니다: /ByteRange 배열이 여전히 0 자리 표시자가 갖고 있거나, /M 서명 시간이 유효한 형식의 PDF 날짜 문자열이 아니거나, 혹은 증분 업데이트(incremental update) 과정에서 암호화된 문서의 /Encrypt 항목이 유실된 경우입니다. 세 가지 경우 모두 파일 열기 및 렌더링은 정상 구동되지만, 표준 검증기가 서명 사전을 읽어 들이는 순간 유효성 검사에 실패하게 됩니다
본 기사의 집필 계기가 된 오류 시나리오는 매우 명확합니다. PAdES B-B 서명 기사에 소개된 워크플로를 사용해 계약서에 서명하고, 자체 검사 코드가 서명이 존재하며 구조적으로 이상 없음을 알리며, 모든 로컬 테스트가 통과(green) 판정을 냅니다 — 그러나 상대방이 파일을 자체 검증 플랫폼에 업로드했을 때 경고(red cross)가 표시되는 현상입니다. 이 오류들은 페이지의 드로잉 내용에는 전혀 영향을 미치지 않기 때문에 일반 PDF 뷰어 화면에서는 감지되지 않습니다. 오류 정보는 뷰어가 아니라 주로 검증기가 집중 분석하는 서명 사전 및 파일 트레일러(trailer) 내에 존재하기 때문입니다
PDF 서명의 ByteRange가 유효하지 않은 이유는 무엇입니까?
ByteRange가 유효하지 않아 반려되는 현상은 대개 범위 설정의 미세한 오차 때문이 아니라 네 개 필드에 데이터가 아예 채워지지 않았음을 의미합니다. /ByteRange [A B C D] 배열은 두 개의 스팬(바이트 A부터 A+B까지, 그리고 바이트 C부터 C+D까지)을 정의하며, EN 319 142-1 §6.3(k항 요건)은 이 두 스팬이 16진수로 인코딩된 /Contents 문자열을 제외한 전체 파일 영역을 커버할 것을 요구합니다. 수치 기준으로 A는 0이고, C >= A+B이며, C+D는 전체 파일의 길이와 정확히 같아야 하고, B와 C 사이의 간격은 정확히 <...> 형태의 16진수 문자열(두 개의 괄호 바이트 및 CMS 데이터의 바이트당 두 개의 16진수 문자)이어야 합니다. 검증기는 /Contents 내부의 CMS 구조가 아무리 완벽하더라도 [0 0 0 0] 배열 데이터를 감지하면 서명이 아무 영역도 커버하지 않는 것으로 판단해 파일을 반려합니다
이 결함의 작동 메커니즘을 알아둘 필요가 있습니다. 모든 서명 모듈이 동일한 방식으로 작동하기 때문입니다. 서명 프로그램은 파일 레이아웃 배치가 완료되기 전에는 최종 오프셋을 알 수 없으므로, 먼저 고정 너비의 0 값들로 자리 표시자를 적어두고 배치가 완료된 후에 이 필드들을 다시 채워 넣습니다. 2.14.1 이전 버전의 PDFium Component는 /Contents 지점부터 시작해 뒤쪽 영역에서 이 자리 표시자 패턴을 검색했습니다 — 그러나 사전 내부에서 /ByteRange는 /Contents보다 앞에 위치하므로 조회가 이루어지지 않아 뒤늦게 수행되는 데이터 기입 작업이 조용히 누락되었습니다. 암호화 서명 값은 정상 영역을 기준으로 올바르게 산출되어 암호학적으로는 완벽했으나, 해당 범위의 선언부가 여전히 0으로 남아 있었기에 규격 준수 검증기들이 이를 반려했던 것입니다. 동일한 사전 레이아웃을 취하는 PAdES B-LTA 문서 타임스탬프도 동일하게 오작동했습니다 — 장기 보존 B-LT 및 B-LTA 서명 문서를 빌드하는 과정에서도 고려할 사항입니다. 버전 2.14.1은 서명 객체의 시작 부분부터 탐색하여 데이터를 다시 채우도록 패치되었으며, 이 수정 사항은 결과물을 파싱하여 수학적 정합성을 검증하는 회귀 테스트 코드를 통해 제어되고 있습니다
function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
Raw: TBytes;
Text: AnsiString;
P, N: Integer;
F: array[0..3] of Int64;
begin
Raw := TFile.ReadAllBytes(FileName);
SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
P := Pos('/ByteRange', Text); // first signature only
Result := P > 0;
if not Result then Exit;
Inc(P, Length('/ByteRange'));
for N := 0 to 3 do
begin
while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
F[N] := 0;
while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
begin
F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
Inc(P);
end;
end;
// EN 319 142-1 §6.3 req k: spans cover everything except /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
RTL 라이브러리 함수 호출 없이 약 20줄의 Pascal 코드로 출력 파일의 오류 여부를 확실하게 감지해 줍니다. 본 기사에서 단 하나의 검증 논리만 남겨두어야 한다면 이를 채택하십시오: 즉 파일이 프로세스를 벗어나기 전에 서명 결과물을 직접 파싱하여 위의 3가지 동등 요건을 체크하는 것입니다
검증기가 /M 서명 시간을 유효하지 않은 형식으로 감지하는 이유는 무엇입니까?
엄격한 검증기들은 완전한 PDF 날짜 문자열 규격을 만족하지 않는 서명 시간을 거부하며, 누락되는 대표적인 두 요소는 D: 접두사 및 UTC 오프셋 마커 정보입니다. ISO 32000-1 §7.9.4는 날짜 형식을 D:YYYYMMDDHHmmSS 뒤에 오프셋(UTC의 경우 Z 또는 서명된 +HH'mm' 오프셋 정보)이 붙는 양식으로 규정합니다. 단순 20260709143000 형태는 호환성이 높은 뷰어에서는 대략 해석되지만, 문법을 엄격히 적용하는 검증기는 이를 규격에 위배된 잘못된 문자열로 판단해 서명 반려 플래그를 설정합니다. v2.14.4 이전 버전의 PDFium Component는 TPdf.SignPades 및 SignPadesBytes의 /M 필드에 이처럼 생략된 형식을 썼으나, v2.14.4부터 D: 접두사 및 Z 마커를 기본 기입하도록 개선되어 서명 시간이 D:20260709143000Z 형태로 안정적으로 선언됩니다
이 필드와 관련해 두 가지 알아두어야 할 사항이 있습니다. 첫째, 가능하면 UTC를 기준으로 표기하고 명시하십시오: 오프셋 마커가 없는 타임스탬프는 검증기가 타임존 정보를 유추하도록 강제하며, §7.9.4 규정은 오프셋을 선택 사항이 아닌 표준 규격 포맷의 일부로 규정합니다. 둘째, /M 값은 서명자 측이 주장하는 시간 정보일 뿐 암호학적인 증명 수단이 아니라는 점을 기억하십시오. 검증기는 이 값의 형식 정합성을 확인할 뿐 내용의 진위 여부를 증명하지 못합니다; 암호학적으로 증명 가능한 시간은 PAdES B-T 이상에서 RFC 3161 타임스탬프를 통해서만 제공됩니다. 즉 필드를 규격에 맞게 적는 것과 그 값을 신뢰하는 것은 별개의 판단이며, 검증기는 전자만을 요구할 뿐입니다
암호화된 PDF에 대해 SignPades가 EPadesCrypto 예외를 일으키는 이유는 무엇입니까?
PDFium Component는 암호화된 문서에 대해 서명 날인을 거부합니다. 그대로 서명할 경우 사용자가 파일을 열었을 때 파일 구조를 파괴해 버리는 비정상 출력물이 생성되기 때문입니다. PAdES 서명은 증분 업데이트(incremental update) 방식으로 파일 뒤에 덧붙여지며, ISO 32000-1 §7.5.6은 업데이트 섹션의 새 트레일러가 이전 트레일러의 모든 엔트리(/Prev 제외)를 보존하여 전달할 것을 명시합니다 — 암호화된 문서의 경우 여기에는 /Encrypt 항목이 포함됩니다. 만약 이 선언을 누락하면 트레일러가 문서를 암호화되지 않은 것으로 취급하여 뷰어가 암호화된 바디 영역을 일반 텍스트로 오해해 파싱 오류를 냅니다. 더욱이 서명자가 덧붙이는 신규 스트림 및 문자열 정보도 규정상 문서 암호 키로 암호화되어 기록되어야 하는데, 일반 텍스트를 다루는 서명 인젝터는 이 처리를 수행할 권한이 없습니다. 따라서 암호화 파일 뒤에 올바른 평문 서명을 덧붙일 수 없으므로, 버전 2.14.2부터 TPdf.SignPades, SignPadesBytes 및 InjectPadesDssMarkers 등은 깨진 결과물을 생성하는 대신 EPadesCrypto 예외를 발생시키도록 설계되었습니다
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// e.g. 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
Writeln('Cannot sign: ', E.Message);
end;
end;
예외를 발생시키는 것이 올바른 처리 동작이므로, 억지로 재시도하기보다는 이 예외 사양에 맞춰 워크플로를 설계하십시오. 소유자 권한으로 암호화를 먼저 해제한 후, 평문 사본에 서명을 진행하십시오. 만약 유통 채널상 암호화 처리가 꼭 필요하다면 암호화 후 서명하는 것과 서명 후 암호화하는 것은 완전히 다른 결과물과 검증 논리를 유발한다는 점을 유념하십시오. 평문 바이트 데이터를 기준으로 계산된 서명 값은 바이트 데이터가 다시 암호화되어 재배치되면 유지될 수 없으므로, 서명된 평문 파일을 보존하거나 관련 정책적 대안을 문서화해 보존하십시오
서로의 결함을 가려주던 두 가지 버그 사례
ByteRange 결함이 꽤 오랫동안 발견되지 않았던 이유는 우리 자체 검증기 모듈에 이를 상쇄해 주는 다른 오류가 존재했기 때문이었으며, 이는 개발 환경에서 곱씹어볼 만한 가치 있는 경험입니다. ValidatePadesCompliance의 범위 검사 루틴은 두 번째 스팬이 정확히 A+B 지점(간격이 0인 상태)에서 시작할 것을 요구했습니다. 이는 간격 영역 내에 /Contents 16진수 문자열을 포함하는 표준 레이아웃 사양을 규격 위반으로 오인하는 결함이었습니다. 결과적으로 내부 검증기는 규격을 준수하는 정상 레이아웃을 에러로 잡고 내부 생성기는 에러가 나는 형태를 고수하여, 생성하고 검증하는 내부 테스트 파이프라인 상에서는 오류가 감지되지 않고 항상 성공(green) 판정이 나왔던 것입니다. 각 결함이 서로의 예외 케이스를 상쇄하여 오류의 발생을 숨겨 주었습니다. 버전 2.14.1은 동일 릴리스에서 이 두 부분을 함께 수정했습니다: 생성기는 4개 필드를 모두 정확히 채우고, 검증기는 C >= A+B 및 C+D가 파일 크기와 같은 사양을 정상 수용합니다
개발 방법론적 해법은 자체 구현하지 않은 다른 외부 구현체를 사용해 교차 검증(cross-validation)을 거치는 것입니다. 생성기와 검증기가 동일한 코드베이스, 개발자, 혹은 사양에 대한 동일한 해석 모델을 공유하면 양쪽 모두 동일한 오해에 기반해 테스트를 통과시킬 수 있습니다; 외부의 독립적인 검증기를 투입해야 비로소 균형이 잡힙니다. 릴리스 배포 전에 서명 출력물을 최소 하나 이상의 외부 적합성 검증 플랫폼에 돌려 보고, 빌드 단계에서 빠르게 1차 검증을 수행하도록 자체 유효성 확인 코드를 탑재하십시오 — 서명 분석 기사에서 설명하는 TPdf.ValidatePades는 이러한 영역 커버리지 에러를 이름이 명시된 이슈 정보로 보고해 줍니다
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange does not cover the file');
if not R.IsCompliant then
Writeln('Structural issues present: do not ship this file');
end;
서명 출력물 반려 관련 체크리스트
유효성 검증 플랫폼이 PAdES 서명을 거부하는 경우, 복잡한 인증서나 신뢰 체인을 의심하기 전에 간단한 구조적 원인들부터 먼저 파악해 보십시오. /ByteRange 배열을 분석하여 3가지 동등 조건을 검토해 보십시오: 첫 필드가 0인지, 두 번째 스팬이 첫 스팬 종료 지점 이후에서 시작하는지, 그리고 두 번째 스팬 종료 시점이 전체 파일 끝과 일치하는지 확인하십시오. /M 엔트리를 읽어 D: 접두사 및 오프셋 마커를 구비한 완전한 §7.9.4 날짜 문자열 포맷인지 확인하십시오. 서명을 덧붙일 때 원본 문서가 암호화 상태가 아니었는지 확인하십시오 — 만약 암호화 문서인데도 라이브러리 모듈이 아무 경고 없이 서명을 마쳤다면 그 동작 자체가 라이브러리 결함일 수 있습니다. 이 세 가지 검사 모두 바이트 데이터 조회를 통해 밀리초 단위로 빠르게 검증할 수 있으며, 실제 필드 상황에서는 복잡한 암호학적 오류보다 이와 같은 단순 규격 오류로 인해 반려되는 사례가 훨씬 더 흔합니다
본문에서 다룬 서명, 분석 및 검증용 호출 루틴들(보정된 ByteRange 연산, 날짜 포맷팅 및 암호화 필터 제어 등이 적용된 SignPades, ValidatePades 및 PAdES 규격 준수 검사 모듈)은 Delphi, C++Builder 및 Lazarus용 PDFium Component의 표준 내장 기능으로 제공됩니다