기술 문서

Delphi PDFium의 ISO/TS 32002 ECDSA·EdDSA 정책

Delphi용 PDFium Component는 모든 ECDSA와 EdDSA PDF 서명을 ISO/TS 32002 알고리즘 프로파일에 대조해 검사하고 판정을 TPadesSignatureValidation.AlgorithmPolicyStatus로 보고합니다. 통과할 수 있는 것은 P-256, P-384, P-521, 세 Brainpool r1 곡선, Ed25519, Ed448뿐이고 각각 맞는 다이제스트가 필요하며, 어긋나면 ppeiSignatureAlgorithmMismatch를 일으킵니다. 이 정책은 들리는 것보다 중요합니다. brainpoolP160r1 위의 서명이나 SHA-512 다이제스트에 서명하는 P-256 키는 수학 수준에서는 완벽히 검증될 수 있습니다. 그래서 Windows CryptoAPI는 서명 값을 양호하다고 보고하는 동안 엄격한 PDF 2.0 검증기는 파일을 거부합니다. 정책 검사가 그 간극을 메우고, 서명 바이트가 암호학적으로 올바른지와는 일부러 분리되어 있습니다

ISO/TS 32002는 타원 곡선 서명에 실제로 무엇을 허용할까요?

ISO/TS 32002는 PDF 서명에 정확히 여섯 ECDSA 곡선과 두 EdDSA 방식을 허용하고, 각 곡선을 실을 수 있는 다이제스트 크기에 묶어 둡니다. PDFium Component는 그 표를 PadesCurveDigestAllowed에, 서명자 인증서의 곡선 OID를 키로 인코딩해 둡니다. NIST 곡선은 엄격합니다. 다이제스트는 SHA-2든 SHA-3든 곡선과 같은 비트 폭이어야 합니다. Brainpool 곡선은 더 느슨해서 자기 폭이나 더 넓은 폭을 받아들입니다:

  • P-256 (1.2.840.10045.3.1.7): SHA-256 또는 SHA3-256만
  • P-384 (1.3.132.0.34): SHA-384 또는 SHA3-384만
  • P-521 (1.3.132.0.35): SHA-512 또는 SHA3-512만
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): 256부터 512비트까지의 임의 SHA-2 또는 SHA-3 다이제스트
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): 384 또는 512비트 SHA-2 / SHA-3
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): SHA-512 또는 SHA3-512만
PDFium Component의 ISO TS 32002 알고리즘 프로파일 매트릭스: PadesCurveDigestAllowed에서 P-256, P-384, P-521은 맞는 다이제스트 폭만 받고, brainpoolP256r1은 256부터 512비트까지, brainpoolP384r1은 384와 512를 받으며, Ed25519와 Ed448은 길이 512의 SHA-512와 SHAKE256을 선언합니다. 어긋나면 ppeiSignatureAlgorithmMismatch를 일으키고 프로파일 밖 곡선은 pcsUnsupported를 반환합니다
여섯 ECDSA 곡선과 두 EdDSA 방식만 통과할 수 있고 각 곡선은 실을 수 있는 다이제스트 폭에 묶여 있습니다. 나머지는 전부 invalid 또는 unsupported이며 조용히 받아들여지는 일은 없습니다

EdDSA에는 곡선 선택도 다이제스트 선택도 없습니다. 그래서 규칙이 강도가 아니라 인코딩에 관한 것입니다. RFC 8419에 따르면 Ed25519 SignerInfo는 매개변수 없이 SHA-512를 digestAlgorithm으로 선언해야 하고, PAdES가 항상 쓰는 signed-attributes 경로의 Ed448 SignerInfo는 정확히 512인 INTEGER 매개변수와 함께 id-shake256-len(2.16.840.1.101.3.4.2.18)을 선언해야 합니다. 두 방식 모두 서명의 AlgorithmIdentifier와 인증서 공개 키의 AlgorithmIdentifier는 매개변수를 전혀 실어서는 안 됩니다. 그 자리에 NULL을 쓰는 생산자, 즉 RSA 인코더들이 많은 ASN.1 라이브러리에 길들여 놓은 습관은 키와 서명 값이 멀쩡해도 비준수 서명을 만들어 냅니다

PDFium Component가 CMS에서 알고리즘 삼인조를 추출하는 방법

PDFium 자신은 이 질문에 답할 수 없습니다. 공개 서명 API는 서명 사전을 읽지만 CMS를 검증하지도 않고 서명자 인증서의 곡선을 노출하지도 않기 때문입니다. 그래서 PDFium 위에 세워진 PAdES 검사 계층이 CMS SignedData(RFC 5652)를 직접 파싱합니다. InspectPadesSignatureAlgorithm은 첫 SignerInfo의 digestAlgorithm과 signatureAlgorithm을 읽고, 서명자 인증서를 찾아 SubjectPublicKeyInfo에서 키 알고리즘과 곡선을 읽습니다. 인증서 탐색은 일부러 한계를 둡니다. CMS certificates 집합의 최대 64개 인증서만 검사하고, 매칭은 issuerAndSerialNumber의 발행자와 일련번호를 정확히 바이트 비교하는 것이며, 집합에 파싱 가능한 인증서가 정확히 하나 있을 때만 "거기 있는 유일한 인증서"로 폴백합니다. 순서 없는 집합에서 첫 EC 인증서를 골라 오는 건 쉬운 일이지만, 그러면 CA 인증서가 서명자가 사용했다는 곡선을 결정하게 됩니다

PDFium Component가 PDF 서명 알고리즘 삼인조를 검사하는 방식: InspectPadesSignatureAlgorithm은 CMS의 첫 SignerInfo에서 digestAlgorithm과 signatureAlgorithm을 읽고, 최대 64개 후보 가운데 정확한 issuerAndSerialNumber 매칭으로 서명자 인증서를 고정한 뒤, 곡선을 위해 SubjectPublicKeyInfo를 읽으며, EvaluatePadesSignatureAlgorithm이 AlgorithmPolicyStatus를 반환합니다
PDFium 자신은 CMS를 검증하지도 서명자 곡선을 노출하지도 않으므로, PAdES 계층이 SignedData를 파싱하고 설명 가능한 거부를 위해 모든 raw OID를 레코드에 유지합니다
uses
  PDFium, FPdfPades;

const
  StatusNames: array[TPadesCryptoStatus] of string =
    ('not checked', 'valid', 'invalid', 'unsupported', 'indeterminate');

var
  Pdf: TPdf;
  R: TPadesValidationResult;
  A: TPadesSignatureAlgorithmInfo;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice-ecdsa-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    for I := 0 to Length(R.Signatures) - 1 do
    begin
      A := R.Signatures[I].AlgorithmInfo;
      Writeln('Signature ', I);
      Writeln('  digestAlgorithm    : ', string(A.DigestAlgorithmOid));
      Writeln('  signatureAlgorithm : ', string(A.SignatureAlgorithmOid));
      Writeln('  public key / curve : ', string(A.PublicKeyAlgorithmOid),
        ' / ', string(A.CurveOid));
      Writeln('  policy             : ',
        StatusNames[R.Signatures[I].AlgorithmPolicyStatus]);
    end;
    if ppeiSignatureAlgorithmMismatch in R.Issues then
      Writeln('At least one signature violates the ISO/TS 32002 profile');
  finally
    Pdf.Free;
  end;
end;

TPdf.ValidatePades는 컴플라이언스 패스의 일부로 이 정책을 적용합니다. CMS 안에서 찾은 인증서를 출발점으로 하고, TPdf.ValidatePadesTrust는 Windows CryptoAPI가 실제 검증에 쓴 서명자 인증서로 다시 실행하므로 CryptoAPI가 보고한 인증서가 최종 발언권을 갖습니다. 모든 raw 입력은 TPadesSignatureAlgorithmInfo에 기록되는데 DigestParametersPresent, DigestParameterBits, SignatureParametersPresent, PublicKeyParametersAreNamedCurve도 포함됩니다. 거부는 언제나 로그 한 줄이 아니라 레코드에서 설명될 수 있습니다

SHA3-256을 쓴 P-256 서명이 정책에 떨어지는 이유는?

P-256 서명은 CMS digestAlgorithm과 ECDSA signatureAlgorithm이 암시하는 다이제스트가 어긋날 때마다 PDFium Component 정책에 떨어집니다. 둘 각각이 곡선에 개별적으로 허용되더라도요. EvaluatePadesSignatureAlgorithm은 먼저 ecdsa-with-SHA256, ecdsa-with-SHA3-256과 그 형제들을 다이제스트로 매핑하고, 선언된 digestAlgorithm과 비교한 뒤, 곡선 표를 보기 전에 차이가 나면 pcsInvalid를 반환합니다. 이 케이스는 실제로 일어납니다. 서명 도구가 해시를 SHA3-256으로 바꾸면서 하드코딩된 ecdsa-with-SHA256 식별자를 그대로 두면, 그 결과물은 준수하는 어떤 검증기도 일관되게 해석할 수 없는 파일입니다. 이 함수는 공개되어 있으니 단위 테스트에서 PDF를 만들지 않고도 매트릭스를 고정할 수 있습니다:

var
  Info: TPadesSignatureAlgorithmInfo;
begin
  Info := Default(TPadesSignatureAlgorithmInfo);
  Info.Family := psafEcdsa;
  Info.PublicKeyAlgorithmOid := '1.2.840.10045.2.1';      // id-ecPublicKey
  Info.PublicKeyParametersPresent := True;
  Info.PublicKeyParametersAreNamedCurve := True;
  Info.CurveOid := '1.2.840.10045.3.1.7';                 // P-256
  Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.8';    // SHA3-256
  Info.SignatureAlgorithmOid := '2.16.840.1.101.3.4.3.10'; // ecdsa-with-SHA3-256
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsValid);

  Info.SignatureAlgorithmOid := '1.2.840.10045.4.3.2';    // ecdsa-with-SHA256
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsInvalid); // 다이제스트 불일치

  Info.CurveOid := '1.3.36.3.3.2.8.1.1.1';                // brainpoolP160r1
  Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.1';    // SHA-256, 이제는 일치
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // 프로파일에 없는 곡선
end;

곡선 인코딩도 같은 엄격함을 받습니다. RFC 5480 §2.1.1은 ECParameters가 named curve OID, implicit curve(NULL) 또는 전체 명시적 매개변수 집합일 수 있게 하고, PKIX 프로파일은 named 형태를 요구합니다. PDFium Component는 id-ecPublicKey 인증서가 매개변수 없이, implicit 매개변수로, 또는 명시적 매개변수로 올 때 pcsInvalid를 반환합니다. 명시적 매개변수는 공격자가 표준 곡선과 비슷하게만 생긴 곡선을 묘사할 수 있게 하기 때문입니다. ISO/TS 32002 목록에 단순히 없는 올바르게 named된 곡선, 위의 brainpoolP160r1이나 secp256k1 같은 곡선은 대신 pcsUnsupported를 받습니다

invalid, unsupported, indeterminate: 상태를 정직하게 읽기

AlgorithmPolicyStatus의 세 non-valid 상태는 서로 다른 것을 뜻하며, 하나의 "실패" 바구니로 뭉치면 감사인이 필요로 하는 정보가 사라집니다. pcsInvalid는 인식된 알고리즘 조합이 malformed이거나 어긋났다는 뜻입니다. TPadesValidationResult.Issues에 ppeiSignatureAlgorithmMismatch를 더하고 집계 IntegrityStatus를 pcsInvalid로 밀어 내려 IsCryptographicallyValid가 CMS 서명 값이 맞아떨어져도 False를 반환하게 만듭니다. pcsUnsupported는 곡선이나 다이제스트가 프로파일이 이름을 대는 범위 밖이라는 뜻이지, 변조의 증거가 아닙니다. 능력(capability) 결과입니다. pcsIndeterminate는 서명자 인증서를 특정하지 못했다는 뜻인데, 보통 여러 후보가 있는 CertificateSet에 정확한 issuerAndSerialNumber 매칭이 없는 경우라 코드는 곡선을 추측하기를 거부합니다. v3.124.0부터는 SHA-1이나 SHA-224 같은 112비트 다이제스트 위의 RSA 서명도 표시하는데, 현재 검증에서는 더 이상 합의되지 않았기 때문입니다. 같은 분리는 CryptoAPI가 Ed25519나 Ed448을 검증하지 못하는 머신의 EdDSA에도 적용됩니다. CmsSignatureStatus는 pcsUnsupported로 남는 동안 AlgorithmPolicyStatus는 여전히 pcsValid일 수 있습니다. 인코딩은 올바르고 없던 것은 검증기뿐이기 때문입니다. Adobe나 DSS 기반 검증기의 거부를 쫓고 있다면 검증기가 PAdES 서명을 거부하는 이유 가이드가 다른 흔한 원인들을 다룹니다

PDFium Component의 EvaluatePadesSignatureAlgorithm 판정 경로: 다이제스트 불일치는 pcsInvalid와 ppeiSignatureAlgorithmMismatch를 set하고, brainpoolP160r1 같은 ISO TS 32002 프로파일 밖 named curve는 pcsUnsupported를 set하며, 특정하지 못한 서명자 인증서는 pcsIndeterminate를 set합니다. v3.124.0부터 RSA 분기는 ETSI TS 119 312 다이제스트 스위트를 적용하고, 키 크기는 여전히 애플리케이션에 맡겨집니다
세 non-valid 상태는 각각 다른 것을 뜻합니다. invalid는 깨진 조합의 증거이고, unsupported는 능력 결과이며, indeterminate는 코드가 추측하기를 거부했다는 뜻입니다
var
  Pdf: TPdf;
  Options: TPadesTrustValidationOptions;
  R: TPadesValidationResult;
  S: TPadesSignatureValidation;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice-ecdsa-signed.pdf';
    Pdf.Active := True;
    Options := TPadesTrustValidationOptions.Default; // 오프라인, 폐기 확인 없음
    R := Pdf.ValidatePadesTrust(Options);
    for I := 0 to Length(R.Signatures) - 1 do
    begin
      S := R.Signatures[I];
      if S.IsDocumentTimeStamp then
        Continue;
      case S.AlgorithmPolicyStatus of
        pcsInvalid:       Writeln(I, ': reject, algorithm combination is invalid');
        pcsUnsupported:   Writeln(I, ': manual review, curve or digest outside the profile');
        pcsIndeterminate: Writeln(I, ': manual review, signer not identified or digest no longer agreed');
        pcsValid:
          if S.AlgorithmInfo.Family = psafRsa then
            Writeln(I, ': RSA digest suite accepted, check key size yourself')
          else
            Writeln(I, ': EC/EdDSA profile satisfied');
      end;
    end;
    Writeln('Cryptographically valid: ', R.IsCryptographicallyValid);
  finally
    Pdf.Free;
  end;
end;

pcsValid가 보장하지 않는 것은?

AlgorithmPolicyStatus = pcsValid는 ECDSA 또는 EdDSA 서명이 승인된 곡선과 일치하며 올바르게 인코딩된 다이제스트를 쓴다는 것, 그리고 RSA 서명이 현행 스위트의 다이제스트를 쓴다는 것만 인증합니다. 서명 값이 올바른지에 관해서는 아무 말도 하지 않습니다. v3.124.0 이전 EvaluatePadesSignatureAlgorithm의 RSA 분기는 일부러 넓었습니다. PKCS #1 arc 1.2.840.113549.1.1.* 아래의 어떤 signatureAlgorithm이든, 레거시 sha1WithRSAEncryption을 포함해 pcsValid를 반환했습니다. PDFiumPas v3.124.0부터 RSA 분기는 ETSI TS 119 312 서명 스위트를 적용합니다. MD2, MD4, MD5 다이제스트는 pcsInvalid입니다. SHA-1과 SHA-224 같은 112비트 다이제스트는 pcsIndeterminate라서 SHA-1 서명은 전체 무결성 결과를 유지한 채 거부 대신 검토 대상으로 표시됩니다. 서명 알고리즘이 고정하는 다이제스트와 다른 digestAlgorithm, 예컨대 SHA-1 다이제스트 위의 sha256WithRSAEncryption이나 비 RSA 서명자 키 위의 RSA 서명 알고리즘은 pcsInvalid로, ppeiSignatureAlgorithmMismatch로 보고되며 무결성에 떨어집니다. 찾을 수 없는 서명자 인증서는 ECDSA에서 그랬던 것처럼 pcsIndeterminate를 주고, 인식되지 않는 다이제스트나 비서명 RSA OID는 pcsUnsupported를 줍니다. 모듈러스 길이는 여전히 검사하지 않고, PSS 매개변수는 여기서 검증하지 않습니다(RFC 4055 RSASSA-PSS-params 글이 서명 쪽 인코딩을 다룹니다). SHA-1이나 MD5 다이제스트는 별도의 ppeiBadDigestAlgorithm 이슈도 추가로 일으킵니다. 마찬가지로 서명 수학, 인증서 체인, 폐기는 Windows CryptoAPI가 돌려주는 CmsSignatureStatus, CertificateTrustStatus, RevocationStatus의 몫입니다. pcsValid는 "알고리즘 프로파일이 유지된다"로 취급하세요. "이 키는 충분히 강하다"로 절대 취급하지 마세요

서명된 PDF 송장, 계약서나 아카이브 패키지를 받아들이는 Delphi 애플리케이션의 실용적인 셋업은 짧습니다. ValidatePadesTrust를 돌리고, ppeiSignatureAlgorithmMismatch에서 거부하고, pcsUnsupported와 pcsIndeterminate는 사람에게 돌리고, RSA 키 크기 하한은 정책이 해주지 않으니 직접 강제하세요. Delphi 및 Lazarus용 PDFium Component는 PAdES 검증기, 증거 리포트 빌더와 서명 파이프라인을 실어 나르므로, 같은 라이브러리가 이 서명들을 만들고 끝까지 검사할 수 있습니다