기술 문서

HotPDF로 Delphi에서 양자내성과 EdDSA PDF 서명하기

HotPDF는 로드된 PDF 문서에서 ML-DSA-44, ML-DSA-65, ML-DSA-87, Ed25519, Ed448 CMS 서명을 검증하며, 플러그형 프로바이더를 통해 서명하므로 개인 키가 Delphi 프로세스 안에 있을 필요가 없습니다. 후반부가 대부분의 팀이 먼저 필요로 하는 부분입니다. 하드웨어 토큰, 원격 서명 서비스, 국가 전자ID 카드는 모두 키를 넘겨주기를 거부하며, 서명 파이프라인이 키 저장소에서 분리되기 전까지는 어느 것도 사용할 수 없습니다

그 분할이 THPDFSignatureProvider의 핵심입니다. HotPDF는 자신이 담당해야 할 부분, 즉 CMS 파싱, SignedData 구성, /ByteRange 배치를 갖고, 볼 수 없는 키로 다이제스트를 서명으로 바꾸는 자신이 담당할 수 없는 한 연산을 위임합니다. 아래의 모든 것은 이 분할에서 비롯됩니다

유효한 ML-DSA 서명이 왜 검증에 실패하는가

HotPDF가 ML-DSA를 위한 확장을 선언하지 않은 로드된 문서에서 ML-DSA를 거부하기 때문입니다. ML-DSA, 즉 FIPS 204로 표준화된 격자 서명 방식이자 사람들이 양자내성 PDF라고 말하는 이유는, 아직 ISO 32000-2 등록이 없습니다. 그것을 담은 PDF는 기본 표준이 이름 짓지 않은 알고리즘을 사용하는 것이며, 조용히 이름 없는 알고리즘을 사용하는 파일은 누구도 판정을 재현할 수 없는 파일입니다

그래서 HotPDF는 주장을 명시적으로 만듭니다. EnsureMLDSAExtensions는 허용되는 경우 문서를 PDF 2.0으로 끌어올리고 카탈로그에 /Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >>를 기록합니다. 읽기 측에서 LoadedDocumentDeclaresMLDSAExtension는 그 선언이 살아남았는지 보고하며, VerifyLoadedSignatureWithOptionsOptions.AllowMLDSA를 존중하기 전에 같은 검사를 적용합니다. 선언되지 않은 문서에서 플래그를 설정하면 그대로 꺼진 상태로 남습니다. 이 옵션은 정책을 느슨하게 할 수는 있어도 구조적 요구사항은 느슨하게 할 수 없습니다

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'contract-pq.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
    Pdf.EnsureMLDSAExtensions;   // declare before the signature is written
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

저장한 뒤가 아니라 저장하기 전에 호출하십시오. 선언은 서명된 바이트 범위의 일부이며, 사후에 패치된 카탈로그는 서명된 파일에 대한 서명되지 않은 변경이거나 검증기가 수정으로 보고할 두 번째 리비전이 됩니다

세 알고리즘 패밀리, 하나의 검증 진입점

세 패밀리 모두 VerifyLoadedSignatureWithOptions를 통해 도달합니다. 이 함수는 서명 인덱스, 소스 스트림, THPDFCMSVerifyOptions 레코드, 서명 상세를 위한 out 매개변수를 받습니다. 레코드는 정확히 세 필드를 가지며, 각각은 과거에 리빌드를 필요로 하던 질문에 답합니다

SignatureProvider는 빌트인 플랫폼 프로바이더를 여러분 자신의 프로바이더로 대체합니다. OpenSSLLibraryPath는 OpenSSL 3 라이브러리를 선택하며, 이것이 Windows CNG가 모든 곳에서 제공하지 않는 순수 모드 Ed25519와 Ed448 검증을 공급합니다. AllowMLDSA는 위의 확장 검사를 조건으로 격자 알고리즘에 옵트인합니다. 인식된 정확한 알고리즘 OID는 THPDFSignatureInfo.SignatureAlgorithmOID로 돌아오므로, 감사 로그는 요청된 것이 아니라 검증된 것을 기록할 수 있습니다

var
  Opts: THPDFCMSVerifyOptions;
  Info: THPDFSignatureInfo;
  Status: THPDFSignatureVerifyStatus;
  Src: TFileStream;
begin
  Opts := THPDFCMSVerifyOptions.Default;
  Opts.OpenSSLLibraryPath := 'C:\openssl3\libcrypto-3-x64.dll';
  Opts.AllowMLDSA := Pdf.LoadedDocumentDeclaresMLDSAExtension;
  Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
    if Status = svValid then
      Memo1.Lines.Add('signed with OID ' + string(Info.SignatureAlgorithmOID));
  finally
    Src.Free;
  end;
end;

Ed25519와 Ed448은 확장 선언이 필요 없으며, ISO 32000-2가 이미 그것들을 인정하기 때문입니다. 다만 그것들을 구현하는 프로바이더는 필요하며, 대부분의 Windows 배포에서는 OpenSSLLibraryPath를 기계에 우연히 있는 것이 아니라 여러분이 배포하고 통제하는 라이브러리로 지정하는 것을 의미합니다

서명 프로바이더는 실제로 무엇을 약속하는가

프로바이더는 한 가지를 약속합니다. 요청이 주어지면 상태를 반환하고 서명 시에는 바이트를 반환합니다. THPDFSignatureProviderRequest는 알고리즘과 그 OID, 다이제스트 OID, PSS 솔트 길이, 입력이 메시지인지 이미 계산된 다이제스트인지 여부, 입력 자체, 공개 키 또는 인증서, 키 식별자, 연산 식별자를 담습니다. 그 레코드에는 HotPDF 특유의 것이 아무것도 없으며, 이는 토큰 드라이버나 서명 서비스가 이미 말하는 어휘입니다

세 구현이 라이브러리와 함께 배포됩니다. THPDFCallbackSignatureProvider는 익명 메서드를 감싸며, 기존 사내 서명 루틴에서 작동하는 PDF 서명까지 가는 가장 짧은 경로입니다. THPDFRemoteSignatureProvider는 재시도 한도, 취소 레지스트리, 입력과 서명 크기의 상한을 가진 전송 콜백을 감싸서, 멈춰버린 HSM이 멈춰버린 애플리케이션이 되는 일을 막습니다. THPDFPKCS11SignatureProvider는 호출자가 소유하고 이미 인증된 PKCS#11 세션과 개인 키 핸들에 대해 RSA 연산을 직렬화합니다. HotPDF는 로그인하지 않고, PIN을 보지 않으며, 자신이 열지 않은 세션을 닫지 않습니다

var
  Provider: THPDFRemoteSignatureProvider;
begin
  Provider := THPDFRemoteSignatureProvider.Create(
    function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
      out Signature: TBytes): THPDFSignatureProviderStatus
    begin
      // POST Req.Input to the signing service; Req.KeyIdentifier selects the key
      if PostToSigningService(Req.KeyIdentifier, Req.Input, Signature) then
        Result := spsValid
      else
        Result := spsProviderError;
    end,
    3,          // RetryLimit
    1048576,    // MaxInputBytes
    65536);     // MaxSignatureBytes
  try
    // hand Provider to the signing call
  finally
    Provider.Free;
  end;
end;

상태 열거가 불리언 대신 여섯 값을 가지는 이유

THPDFSignatureProviderStatusspsValid, spsInvalid, spsUnsupported, spsMalformed, spsProviderError, spsCancelled를 구분하며, 이것들을 합치면 올바르게 대응하는 능력을 잃습니다. 암호학적으로 틀린 서명(spsInvalid)은 보안 이벤트입니다. 프로바이더가 구현하지 않는 알고리즘(spsUnsupported)은 배포 갭입니다. 전송 실패(spsProviderError)는 재시도할 가치가 있으며, 사용자가 취소한 토큰 프롬프트(spsCancelled)는 재시도할 가치가 전혀 없습니다

서명을 위한 규칙은 좁습니다. 서명 프로바이더는 비어 있지 않은 서명과 함께만 spsValid를 반환합니다. 검증 프로바이더는 spsValid 또는 spsInvalid를 반환하며, 나머지 넷은 양쪽 경로 모두에서 구분된 채로 남습니다. 프로바이더를 작성할 때 인식하지 못하는 모든 것을 spsInvalid로 매핑하려는 유혹을 거부하십시오. 그것은 누락된 DLL을 고객의 서명이 위조되었다는 보고로 바꿉니다

서명이 파일에 실제로 내려앉는 곳

두 함수가 프로바이더를 실제 PDF 바이트에 연결합니다. HPDFCMSBuildSignedDataWithProvider는 문서 SHA-256 다이제스트에서 분리형 CMS를 구성하며, 워크플로가 다이제스트를 다른 곳에서 계산할 때 올바른 진입점입니다. HPDFCMSSignPDFStreamWithProvider는 PDF 스트림의 기존 서명 자리표시자에 서명하고 표준 /ByteRange 파이프라인을 보존하며, HotPDF가 자리표시자를 스스로 배치했을 때 올바른 진입점입니다

그 파이프라인을 보존하는 것은 들리는 것보다 중요합니다. /ByteRange 관행, 즉 16진 서명 창을 건너뛰는 두 개의 범위는 모든 검증기가 가장 먼저 검사하는 것이며, 그것을 다시 쓴 프로바이더 기반 경로는 암호학이 얼마나 건전하든 PAdES 적합성을 깨뜨릴 것입니다. HotPDF는 레이아웃을 빌트인 서명 경로와 동일하게 유지하므로, PKCS#11 토큰을 통해 서명된 문서는 PFX 파일에서 서명된 것과 동일한 서명 검증 코드로 검증됩니다. 알고리즘 선택 위에 놓인 프로필 규칙은 Delphi에서 PAdES 베이스라인 서명 산책문을 보고, 이 프로바이더 모델에 앞서는 ECDSA 특유의 인코딩 함정은 ECDSA CMS 검증과 P1363 서명 형식에 관한 글을 보십시오

문서를 고립시키지 않는 마이그레이션 순서

양자내성 대비는 스위치가 아니라 일정 문제입니다. 오늘 배포된 PDF 뷰어 중 ML-DSA를 검증하는 것은 거의 없으므로, 그것만으로 서명된 문서는 독자 관점에서 검증할 수 없는 서명을 가진 문서입니다. 실제 아카이브와 접촉해 살아남는 순서는 다음과 같습니다. 검증기가 판정할 서명으로 RSA 또는 ECDSA를 유지하고, 정책이 양자내성 증거를 요구하는 곳에 확장 선언과 두 번째 ML-DSA 서명을 추가하며, 소비 시스템이 따라잡았을 때만 주 서명을 옮기는 것입니다

오늘 HotPDF가 주는 것은 동일한 코드에서 둘 다 작성하고 검증하는 능력이며, 알고리즘은 파일과 검증 결과에 정직하게 기록됩니다. HotPDF는 외부 PDF 런타임 없이 Delphi와 C++Builder를 위한 네이티브 VCL PDF 컴포넌트이므로 서명과 검증 경로는 실행 파일 옆이 아니라 그 안에서 배포됩니다. 전체 기능 목록과 평가판 다운로드는 HotPDF Delphi PDF 컴포넌트 페이지를 보십시오