Technisch artikel

ISO/TS 32002-beleid voor ECDSA en EdDSA in PDFium Delphi

De PDFium Component for Delphi toetst elke ECDSA- en EdDSA-PDF-handtekening aan het ISO/TS 32002-algoritmeprofiel en meldt het oordeel in TPadesSignatureValidation.AlgorithmPolicyStatus. Alleen P-256, P-384, P-521, de drie Brainpool r1-curven, Ed25519 en Ed448 kunnen slagen, elk met een bijpassende digest, en een mismatch werpt ppeiSignatureAlgorithmMismatch op. Dat beleid is belangrijker dan het klinkt. Een handtekening op brainpoolP160r1, of een P-256-sleutel die een SHA-512-digest ondertekent, verifieert op rekenkundig niveau prima, dus Windows CryptoAPI meldt de handtekeningwaarde als goed terwijl een strenge PDF 2.0-validator het bestand verwerpt. De beleidscontrole dicht dat gat, en hij is met opzet gescheiden van de vraag of de handtekeningbytes cryptografisch correct zijn

Wat staat ISO/TS 32002 werkelijk toe voor elliptic curve-handtekeningen?

ISO/TS 32002 staat in PDF-handtekeningen precies zes ECDSA-curven en twee EdDSA-schema's toe, en hij koppelt elke curve aan de digestgroottes die ze mag dragen. De PDFium Component codeert die tabel in PadesCurveDigestAllowed, geïndexeerd op de curve-OID uit het ondertekenaarcertificaat. De NIST-curven zijn streng: de digest moet dezelfde bitbreedte hebben als de curve, SHA-2 of SHA-3. De Brainpool-curven zijn coulanter en accepteren hun eigen breedte of alles wat breder is:

  • P-256 (1.2.840.10045.3.1.7): alleen SHA-256 of SHA3-256
  • P-384 (1.3.132.0.34): alleen SHA-384 of SHA3-384
  • P-521 (1.3.132.0.35): alleen SHA-512 of SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): elke SHA-2- of SHA-3-digest van 256 tot 512 bits
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 van 384 of 512 bits
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): alleen SHA-512 of SHA3-512
Matrix van het ISO TS 32002-algoritmeprofiel in PDFium Component: P-256, P-384 en P-521 accepteren in PadesCurveDigestAllowed alleen matchende digestbreedtes, brainpoolP256r1 accepteert 256 tot 512 bits, brainpoolP384r1 accepteert 384 en 512, Ed25519 en Ed448 declareren SHA-512 en SHAKE256 met lengte 512, en mismatches werpen ppeiSignatureAlgorithmMismatch op terwijl curven buiten het profiel pcsUnsupported teruggeven
Zes ECDSA-curven en twee EdDSA-schema's kunnen slagen, en elke curve is gebonden aan de digestbreedtes die ze mag dragen; al het restant is ongeldig of niet ondersteund, nooit geruisloos geaccepteerd

EdDSA heeft geen curvekeuze en geen digestkeuze, en precies daarom gaan zijn regels over encoding in plaats van sterkte. Volgens RFC 8419 moet een Ed25519-SignerInfo SHA-512 als zijn digestAlgorithm declareren zonder parameters, en een Ed448-SignerInfo, op het signed-attributes-pad dat PAdES altijd gebruikt, moet id-shake256-len (2.16.840.1.101.3.4.2.18) declareren met een INTEGER-parameter van precies 512. Voor beide schema's moeten de AlgorithmIdentifier van de handtekening en de AlgorithmIdentifier van de openbare certificaatsleutel helemaal geen parameters dragen. Een producent die daar een NULL wegschrijft, de gewoonte die RSA-encoders in veel ASN.1-bibliotheken hebben ingetraind, produceert een niet-conforme handtekening ook al zijn de sleutel en de handtekeningwaarde in orde

Hoe de PDFium Component het algoritme-drietal uit de CMS haalt

PDFium zelf kan deze vraag niet beantwoorden, want zijn publieke handtekening-API leest de handtekeningdictionary maar verifieert de CMS niet en legt de curve van het ondertekenaarcertificaat niet bloot. De op PDFium gebouwde PAdES-inspectielaag parseert de CMS-SignedData (RFC 5652) daarom zelf. InspectPadesSignatureAlgorithm leest de digestAlgorithm en signatureAlgorithm van de eerste SignerInfo, zoekt dan het ondertekenaarcertificaat en leest zijn SubjectPublicKeyInfo om de sleutelalgoritme en de curve te bemachtigen. Het certificaatzoeken is bewust begrensd: hooguit 64 certificaten in de certificates-set van de CMS worden bekeken, de match is een exacte bytevergelijking van issuer en serienummer uit issuerAndSerialNumber, en de code valt alleen terug op "het enige certificaat dat er is" wanneer de set precies één parseerbaar certificaat bevat. De eerste EC-certificaat uit een ongeordende set pakken zou makkelijk zijn, en het zou een CA-certificaat laten beslissen welke curve de ondertekenaar zogenaamd gebruikte

Hoe de PDFium Component een PDF-handtekening-algoritmedrietal inspecteert: InspectPadesSignatureAlgorithm leest de digestAlgorithm en signatureAlgorithm van de eerste SignerInfo in de CMS, pint het ondertekenaarcertificaat vast via een exacte issuerAndSerialNumber-match onder hooguit 64 kandidaten, leest de SubjectPublicKeyInfo voor de curve, en EvaluatePadesSignatureAlgorithm geeft de AlgorithmPolicyStatus terug
PDFium zelf verifieert de CMS niet en legt de ondertekenaar-curve niet bloot, dus de PAdES-laag parseert de SignedData en houdt elke ruwe OID in het record vast voor een verklaarbare afwijzing
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 past het beleid toe als deel van zijn conformiteitspas, vertrekkend vanuit het certificaat dat hij in de CMS vindt, en TPdf.ValidatePadesTrust voert het opnieuw uit tegen het ondertekenaarcertificaat dat Windows CryptoAPI werkelijk voor de verificatie gebruikte, dus het door CryptoAPI gemelde certificaat heeft het laatste woord. Elke ruwe invoer belandt in TPadesSignatureAlgorithmInfo, inclusief DigestParametersPresent, DigestParameterBits, SignatureParametersPresent en PublicKeyParametersAreNamedCurve, dus een afwijzing is altijd verklaarbaar vanuit het record in plaats van uit een logregel

Waarom zakt een P-256-handtekening met SHA3-256 door het beleid?

Een P-256-handtekening zakt door het beleid van de PDFium Component telkens wanneer de digestAlgorithm in de CMS en de digest die de ECDSA-signatureAlgorithm impliceert uit elkaar liggen, ook al zijn beide op zich acceptabel voor de curve. EvaluatePadesSignatureAlgorithm mapt eerst ecdsa-with-SHA256, ecdsa-with-SHA3-256 en hun verwanten naar een digest, vergelijkt die met de opgegeven digestAlgorithm, en geeft pcsInvalid terug bij elk verschil voordat de curvetabel wordt geraadpleegd. Het geval is echt: een ondertekeninstrument schakelt zijn hash om naar SHA3-256 maar houdt een hardgecodeerde ecdsa-with-SHA256-identifier aan, en het resultaat is een bestand dat geen conforme verificateur consistent kan interpreteren. De functie is publiek, dus de matrix kan in een unittest worden vastgepind zonder een PDF te bouwen:

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); // digest loopt uit de pas

  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, nu congruent
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // curve zit niet in het profiel
end;

De curve-encoding krijgt dezelfde strengheid. RFC 5480 §2.1.1 laat ECParameters een named-curve-OID, een impliciete curve (NULL) of een volledige expliciete parameterset zijn, en PKIX-profielen eisen de benoemde vorm. De PDFium Component geeft pcsInvalid terug wanneer een id-ecPublicKey-certificaat geen parameters, impliciete parameters of expliciete parameters draagt, want expliciete parameters laten een aanvaller een curve beschrijven die slechts op een standaardcurve lijkt. Een correct benoemde curve die simpelweg in de ISO/TS 32002-lijst ontbreekt, zoals brainpoolP160r1 hierboven of secp256k1, krijgt pcsUnsupported in plaats daarvan

Ongeldig, niet ondersteund of onbepaald: de status eerlijk lezen

De drie niet-geldige statussen van AlgorithmPolicyStatus betekenen verschillende dingen, en ze samenvouwen in één "gefaald"-bak gooit de informatie weg die auditors nodig hebben. pcsInvalid betekent dat een herkende algoritme-combinatie misvormd is of uit de pas loopt; hij voegt ppeiSignatureAlgorithmMismatch toe aan TPadesValidationResult.Issues en duwt de geaggregeerde IntegrityStatus naar pcsInvalid, dus IsCryptographicallyValid geeft False terug zelfs wanneer de CMS-handtekeningwaarde klopt. pcsUnsupported betekent dat de curve of digest buiten ligt wat het profiel noemt, wat een capaciteitsuitspraak is, geen bewijs van manipulatie. pcsIndeterminate betekent dat het ondertekenaarcertificaat niet kon worden vastgepind, meestal een CertificateSet met meerdere kandidaten en geen exacte issuerAndSerialNumber-match, dus de code weigert de curve te gokken; sinds v3.124.0 markeert hij ook een RSA-handtekening over SHA-1 of een 112-bits digest zoals SHA-224, die niet langer is overeengekomen voor huidige validatie. Dezelfde splitsing geldt voor EdDSA op een machine waarvan CryptoAPI Ed25519 of Ed448 niet kan verifiëren: CmsSignatureStatus blijft pcsUnsupported terwijl AlgorithmPolicyStatus nog steeds pcsValid kan zijn, want de encoding was correct en alleen de verificateur ontbrak. Zit u een afwijzing van Adobe of een op DSS gebaseerde validator achterna, dan behandelt de gids waarom validators PAdES-handtekeningen afwijzen de andere veelvoorkomende oorzaken

Beslispad van EvaluatePadesSignatureAlgorithm in PDFium Component: een digest-mismatch zet pcsInvalid en ppeiSignatureAlgorithmMismatch, een benoemde curve buiten het ISO TS 32002-profiel zoals brainpoolP160r1 zet pcsUnsupported, een ondertekenaarcertificaat dat niet vast te pinnen is zet pcsIndeterminate, en sinds v3.124.0 past de RSA-tak de digestsuites uit ETSI TS 119 312 toe, met de sleutelgrootte nog steeds aan de applicatie
De drie niet-geldige statussen betekenen verschillende dingen: invalid is bewijs van een kapotte combinatie, unsupported is een capaciteitsuitspraak, en indeterminate betekent dat de code weigerde te gokken
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; // offline, no revocation
    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;

Wat garandeert pcsValid niet?

AlgorithmPolicyStatus = pcsValid certificeert alleen dat een ECDSA- of EdDSA-handtekening een goedgekeurde curve gebruikt met een congruente, correct gecodeerde digest, en dat een RSA-handtekening een digest uit een actuele suite gebruikt; over de vraag of de handtekeningwaarde klopt zegt hij niets. Vóór v3.124.0 was de RSA-tak van EvaluatePadesSignatureAlgorithm opzettelijk breed: elke signatureAlgorithm onder de PKCS #1-boog 1.2.840.113549.1.1.* gaf pcsValid terug, legacy-sha1WithRSAEncryption incluis. Sinds PDFiumPas v3.124.0 past de RSA-tak de ETSI TS 119 312-handtekeningsuites toe. MD2-, MD4- en MD5-digests zijn pcsInvalid. SHA-1 en 112-bits digests zoals SHA-224 zijn pcsIndeterminate, dus een SHA-1-handtekening houdt zijn algehele integriteitsuitslag en wordt gemarkeerd voor review in plaats van afgewezen. Een digestAlgorithm die afwijkt van de digest die het handtekeningalgoritme vastlegt, zoals sha256WithRSAEncryption over een SHA-1-digest, of een RSA-handtekeningalgoritme op een niet-RSA-ondertekenaarsleutel, is pcsInvalid, gemeld als ppeiSignatureAlgorithmMismatch, en laat de integriteit zakken. Een ondertekenaarcertificaat dat niet wordt gevonden geeft pcsIndeterminate, zoals het al voor ECDSA deed, en onbekende digests of niet-handtekening-RSA-OIDs geven pcsUnsupported. Moduluslengte wordt nog steeds niet gecontroleerd, PSS-parameters worden hier niet gevalideerd (het RFC 4055-artikel over RSASSA-PSS-params behandelt hoe ze aan de ondertekenkant zijn gecodeerd), en SHA-1- of MD5-digests werpen daarnaast de aparte ppeiBadDigestAlgorithm-kwestie op. De handtekeningrekenkunde, de certificaatketen en de revocatie blijven zoals het hoort de taak van CmsSignatureStatus, CertificateTrustStatus en RevocationStatus, die uit Windows CryptoAPI komen. Beschouw pcsValid als "het algoritmeprofiel houdt stand", nooit als "deze sleutel is sterk genoeg"

Voor een Delphi-applicatie die ondertekende PDF-facturen, contracten of archiefpakketten accepteert, is de praktische opzet kort: draai ValidatePadesTrust, wijs af op ppeiSignatureAlgorithmMismatch, routeer pcsUnsupported en pcsIndeterminate naar een mens, en handhaaf uw eigen RSA-sleutelgrootte-ondergrens want het beleid doet dat niet. De PDFium Component for Delphi and Lazarus verscheept de PAdES-validator, de bouwer van bewijsrapporten en de ondertekenpipeline, dus dezelfde library kan deze handtekeningen produceren en ze end-to-end controleren