기술 문서

HotPDF의 PAdES LTV 증거와 시드 값

방금 서명한 PDF는 B-B 서명일 뿐 그 이상이 아닙니다. 누가 서명했는지와 바이트가 움직이지 않았음을 증명하지만, 서명자 인증서가 서명 시점에 유효했다는 증거는 실어 가지 않으므로, 몇 년 뒤의 검증자는 더 이상 존재하지 않을 수도 있는 폐지 데이터를 찾아 나서야 합니다. 이 간극을 메운다는 것은 OCSP 응답과 CRL을 문서 수준 Document Security Store에 기록하는 것이고, HotPDF에서는 호출 하나입니다. PopulatePAdESLTVEvidence가 로드된 모든 서명을 훑고, 인증서 집합에서 폐지 요청을 도출하고, 여러분이 공급한 전송 계층으로 실행하며, 가져온 자료와 CMS 체인을 DSS에 기록합니다. 증거가 도착한 서명의 개수를 돌려주고, 문서에 서명 필드가 전혀 없으면 마이너스 일을 돌려줍니다

사용하기 전에 이해할 가치가 있는 설계 결정은 라이브러리가 소켓을 절대 열지 않는다는 것입니다. 네트워크에서 도착하는 모든 바이트는 여러분이 작성한 콜백을 통해 도착합니다. 이는 신중함 자체를 위한 것이 아니라, 실제로 장기 검증을 요구하는 환경 안에서 이 기능이 동작할 수 있는 유일한 방법입니다

라이브러리가 자체 HTTP를 거부하는 이유

B-LT 서명을 요구하는 장소가 바로 라이브러리에 네트워크를 맡길 수 없는 장소이기 때문입니다. 서명 서비스는 기업 루트를 갖춘 인증 프록시 뒤에서 돕니다. 에어갭(air-gapped) 서명 계층은 응답기로 가는 경로가 없어 캐시된 증거를 먹여야 합니다. 감사 체계는 모든 아웃바운드 요청이 의존성 속에 묻히는 것이 아니라 애플리케이션에 의해 기록되기를 요구합니다. 그리고 테스트 스위트는 결정론적 응답이 필요한데, 라이브러리가 제멋대로 외부에 전화를 거는 한 불가능합니다

전송 계층은 고정된 모양의 평범한 함수 참조이므로 정책은 여러분 몫으로 남습니다. HotPDF는 콘텐츠 타입과 응답 크기 상한을 포함해 무엇을 가져올지 정확히 기술하는 요청 레코드를 건네주고, 여러분은 바이트와 상태를 돌려줍니다

HotPDF PopulatePAdESLTVEvidence 흐름: 호출자가 공급한 FetchEvidence 전송 계층, 요청 레코드 필드, 서명별 상태 결과
모든 네트워크 바이트는 여러분의 FetchEvidence 콜백을 통과하며, 각 서명은 자신의 상태를 받아 한 번의 타임아웃이 패스 전체를 중단시키지 않습니다
function FetchEvidence(const Request: THPDFSignatureEvidenceRequest;
  Attempt: Integer; CancellationToken: THPDFCancellationToken;
  out Response: TBytes; out RetryAfterMS: Cardinal;
  out ErrorMessage: UnicodeString): THPDFSignatureEvidenceTransportStatus;
begin
  RetryAfterMS := 0;
  try
    // Request.Kind가 OCSP POST인지 CRL GET인지를 말해 줍니다.
    // Request.ContentType과 Request.Body는 이미 준비되어 있고,
    // Request.MaxResponseBytes는 반드시 지켜야 할 상한입니다
    Response := HttpExchange(Request.URI, Request.ContentType,
      Request.Body, Request.MaxResponseBytes);
    Result := setsSucceeded;
  except
    on E: Exception do
    begin
      ErrorMessage := E.Message;
      // setsRetry는 재시도 정책에게 백오프를 허용합니다.
      // 404나 잘못된 URL에는 setsPermanentFailure를 씁니다
      Result := setsRetry;
    end;
  end;
end;

// 로드된 파일의 모든 서명을 호출 한 번에 B-B에서 B-LT로 올리기
var
  Pdf: THotPDF;
  Upgraded: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('signed.pdf');
    Upgraded := Pdf.PopulatePAdESLTVEvidence(FetchEvidence,
      THPDFSignatureEvidenceRetryPolicy.Default);
    if Upgraded > 0 then
      // append-only 저장: 기존 서명이 커버하는 바이트는
      // 그대로 보존됩니다
      Pdf.SaveIncrementalUpdate('signed-lt.pdf');
  finally
    Pdf.Free;
  end;
end;

실패는 문서별이 아니라 서명별입니다. 한 서명자에게 타임아웃이 난 응답기는 그 서명자의 자료를 건너뛰고 패스의 나머지를 온전히 남겨 둡니다. 배치에서 원하는 바로 그 동작입니다. 부분 증거가 중단된 실행보다 낫고, 반환값이 실제로 개선된 서명이 몇 개인지 말해 줍니다

CMS가 넣는 것을 잊은 체인

폐지 검사에는 발급자 인증서가 필요한데, 놀랄 만큼 많은 서명 스택이 CMS 컨테이너에서 중간 인증서를 빠뜨립니다. 복구 경로는 Authority Information Access 확장, 즉 액세스 메서드 1.3.6.1.5.5.7.48.2로, 발급자 인증서를 내려받을 수 있는 URL을 알려 줍니다. HPDFFetchAIAIntermediates는 같은 전송 계층으로 그 URL들을 훑고, 각 응답에서 DER을 해석하며, CMS가 이미 갖고 있지 않은 인증서만 DER 해시로 키를 매겨 돌려줍니다. 덕분에 중복과 순환이 헛돌 수 없습니다

실제 인증 기관을 상대로 이것이 통하는지는 두 세부가 결정합니다. 첫째는 인코딩입니다. CA 엔드포인트는 인증서를 PEM 갑옷 형태로 주는 것만큼 맨 DER로 주며, 둘을 구분할 믿을 만한 콘텐츠 타입은 없습니다. 튼튼한 탐지는 텍스트로, 그다음 구조로입니다. -----BEGIN CERTIFICATE----- 마커를 찾고, 있으면 갑옷을 벗겨 base64를 디코딩하며, 두 경로 모두에서 결과의 첫 바이트가 SEQUENCE의 DER 태그인 $30인지 확인합니다. 둘째는 깊이입니다. 가져온 중간 인증서가 자기 발급자의 AIA URL을 스스로 광고할 수 있으므로, 순회는 새 후보를 큐에 덧붙이며 두세 홉 모자란 체인을 완성합니다. 여기에는 상한이 필요하고, 그것이 MaxFetch 매개변수의 역할입니다

HotPDF의 AIA 체인 완성 도식: caIssuers URL 가져오기, PEM 대 DER 탐지, DER 해시 중복 제거, MaxFetch 깊이 상한
HPDFFetchAIAIntermediates는 같은 전송 계층으로 caIssuers URL을 훑으며 PEM 갑옷을 탐지하고 MaxFetch로 큐에 상한을 둡니다

서명 시드 값이란 무엇이며, 왜 조용히 실패하는가

시드 값(seed value)은 문서 저자가 서명 필드에 붙여 서명자에게 어떤 종류의 서명이 받아들여질지를 말하는 제약입니다. 어느 SubFilter, 어느 다이제스트 알고리즘, 어느 사유, 어느 최소 PDF 버전, 폐지 정보를 반드시 내장해야 하는지입니다. 필드의 /SV 사전에 살며 ISO 32000-1 §12.7.5.5에 정의되어 있습니다. HotPDF는 AttachPAdESSeedValue로 기록하고 CheckLoadedSignatureSeedValue로 검사합니다. 이 함수는 필드에 제약이 없거나 존재하는 모든 제약이 통과하면 True를 돌려주고, False라면 첫 번째 실패 제약을 출력 매개변수로 알려 주어 그대로 오류 메시지에 넣을 수 있습니다

시드 값을 틀리게 만들기 쉽게 하는 장치는 §12.7.5.5.3에서 기술하는 /Ff 플래그 엔트리입니다. 설정된 비트는 자기 제약을 필수로 표시합니다. 불일치는 오류이며 서명자는 거부해야 합니다. 지워진 비트는 같은 제약을 선호로 표시합니다. 그 값은 UI가 무엇을 제공해야 하는지 걸러낼 뿐 그 이상이 아닙니다. 여기서 함정 두 개가 따라옵니다. 첫째, /Ff는 위젯 어노테이션이 아니라 /SV 사전 안에 삽니다. 그러므로 필드 수준의 /Ff를 읽는 코드는 영원히 빈 답을 받고 아무것도 강제되지 않는다고 결론 내립니다. 둘째, 비트 배정은 일, 이, 사, 팔의 단순 나열이 아닙니다. HotPDF의 라이터는 SubFilter에 2, MinVersion에 4, AddRevInfo에 32, DigestMethod에 64를 냅니다. 연속 비트를 가정한 리더는 모든 제약을 선택 사항으로 해석해 중요한 그 하나를 빼고 모든 테스트를 통과합니다

HotPDF PAdES 서명의 시드 값 플래그 비트표로 Ff 비트 2, 4, 32, 64와 필수 대 선호 제약 처리를 보여 줌
/Ff 엔트리는 /SV 안에 살며, 각 비트 위치가 불일치가 강제 거부인지 UI 선호인지를 결정합니다
var
  Violation: AnsiString;
begin
  // 막 서명하려는 프로파일이 허용되는지 필드에게 묻습니다
  if not Pdf.CheckLoadedSignatureSeedValue(0, 'ETSI.CAdES.detached',
       'SHA256', 'Approved for payment', 1, Violation) then
    raise Exception.Create('Signature field rejects this profile: ' +
      String(Violation));
  // 제약 충족: 서명 패스를 진행합니다
end;

원래의 해석 버그를 폭로한 테스트는 긍정 테스트가 아니었습니다. 강제된 불일치는 반드시 거부되어야 한다는 단정이었고, 이 부류의 결함을 잡을 수 있는 유일한 종류의 테스트입니다. 잘못된 사전이나 잘못된 비트 위치를 읽는 디코더는 모든 입력에 대해 "위반된 제약 없음"을 내놓습니다. 일부러 하나를 위반하기 전까지는 정확히 올바른 동작처럼 보입니다

LTV 사다리에서의 위치

네 개의 단이 있고, 각 단은 아래 단을 필요로 합니다. B-B는 맨 서명입니다. B-T는 신뢰된 타임스탬프를 더해 서명 시각을 고정하므로 검증자가 폐지를 어느 시점 기준으로 평가할지 알 수 있습니다. B-LT는 폐지 증거를 DSS에 더하는데, 이것이 PopulatePAdESLTVEvidence가 자동화하는 것입니다. B-LTA는 이전 타임스탬프가 약해지기 전에 갱신하는 문서 타임스탬프를 더해 유효성을 무기한 연장합니다. HotPDF는 이를 RenewPAdESLTATimestamp로 노출하며, 새 타임스탬프를 증분 리비전으로 덧붙이고 이전의 모든 서명, 타임스탬프, DSS 엔트리를 그대로 보존합니다

증분 업데이트 모델이 서명된 문서에 증거를 더하는 유일하게 올바른 방법입니다. 파일을 다시 쓰면 기존 서명이 커버하는 바이트 범위가 깨지기 때문입니다. 리비전 사이에 무엇이 바뀌었는지, 그 변화가 서명이 허용하는 부류인지 따져야 한다면 그 분석은 DocMDP 및 FieldMDP 리비전 분석에서 별도로 다룹니다. 인증서 소스와 바이트 순서 함정을 포함한 서명 파이프라인 자체는 PAdES 서명 워크스루에, 검증 쪽은 로드된 문서의 서명 검증에 있습니다

순서에 관한 실무적 경고 하나. 서명 직후, 이상적으로는 같은 작업 안에서 최대한 빨리 증거를 수집하십시오. 인증서에 답할 수 있는 응답기는 인증서가 현행인 동안에만 온라인이고 몇 년 뒤에는 사라지므로, 파이프라인을 B-B로 떠나는 문서는 다시는 업그레이드되지 못할 수 있습니다. HotPDF는 Delphi와 C++Builder용 네이티브 VCL 컴포넌트로 실행되며, 전체 증거 패스는 여러분의 전송 계층을 제외하면 인프로세스입니다. 지원되는 프로파일은 HotPDF Delphi PDF component 제품 페이지에 정리되어 있습니다