Teknisk artikel

ISO/TS 32002-policykontroller för ECDSA och EdDSA i Delphi

PDFium Component för Delphi kontrollerar varje ECDSA- och EdDSA-PDF-signatur mot algoritmsprofilen ISO/TS 32002 och rapporterar domen i TPadesSignatureValidation.AlgorithmPolicyStatus. Bara P-256, P-384, P-521, de tre Brainpool r1-kurvorna, Ed25519 och Ed448 kan klara, var och en med en matchande digest, och en mismatch utlöser ppeiSignatureAlgorithmMismatch. Den policyn spelar större roll än den låter. En signatur på brainpoolP160r1, eller en P-256-nyckel som signerar en SHA-512-digest, kan verifiera utmärkt på matematiknivå, så att Windows CryptoAPI rapporterar signaturvärdet som gott medan en strikt PDF 2.0-validator avvisar filen. Policykontrollen stänger den luckan, och den är medvetet åtskild från frågan om signaturbytena är kryptografiskt korrekta

Vad tillåter ISO/TS 32002 egentligen för signaturer med elliptiska kurvor?

ISO/TS 32002 tillåter exakt sex ECDSA-kurvor och två EdDSA-scheman i PDF-signaturer, och knyter varje kurva till de digeststorlekar den får bära. PDFium Component kodar den tabellen i PadesCurveDigestAllowed, nycklad med kurv-OID:n från signerarens certifikat. NIST-kurvorna är strikta: digesten måste ha samma bitbredd som kurvan, antingen SHA-2 eller SHA-3. Brainpool-kurvorna är lösare och accepterar sin egen bredd eller vad som helst bredare:

  • P-256 (1.2.840.10045.3.1.7): bara SHA-256 eller SHA3-256
  • P-384 (1.3.132.0.34): bara SHA-384 eller SHA3-384
  • P-521 (1.3.132.0.35): bara SHA-512 eller SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): valfri SHA-2- eller SHA-3-digest från 256 till 512 bitar
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): 384- eller 512-bitars SHA-2 / SHA-3
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): bara SHA-512 eller SHA3-512
Matris över ISO TS 32002-algoritmsprofilen i PDFium Component: P-256, P-384 och P-521 accepterar bara matchande digestbredder i PadesCurveDigestAllowed, brainpoolP256r1 accepterar 256 till 512 bitar, brainpoolP384r1 accepterar 384 och 512, Ed25519 och Ed448 deklarerar SHA-512 och SHAKE256 med längden 512, och mismatches utlöser ppeiSignatureAlgorithmMismatch medan kurvor utanför profilen returnerar pcsUnsupported
Sex ECDSA-kurvor och två EdDSA-scheman kan klara, och varje kurva är knuten till de digestbredder den får bära; allt annat är ogiltigt eller stöds ej, aldrig tyst accepterat

EdDSA har inget kurvval och inget digestval, vilket är exakt varför dess regler handlar om kodning snarare än styrka. Enligt RFC 8419 måste en Ed25519-SignerInfo deklarera SHA-512 som sin digestAlgorithm utan parametrar, och en Ed448-SignerInfo, på den signerade attributväg som PAdES alltid använder, måste deklarera id-shake256-len (2.16.840.1.101.3.4.2.18) med en INTEGER-parameter på exakt 512. För båda schemana måste signaturens AlgorithmIdentifier och certifikatets publika nyckels AlgorithmIdentifier bära inga parametrar alls. En producent som skriver en NULL där — vanan som RSA-inkodare har tränat in i många ASN.1-bibliotek — producerar en icke-konform signatur även om nyckeln och signaturvärdet är fina

Hur PDFium Component extraherar algoritmtrippeln ur CMS

PDFium själv kan inte svara på den frågan, eftersom dess publika signatur-API läser signaturordlistan men varken verifierar CMS:en eller exponerar signerarcertifikatets kurva. Därför tolkar PAdES-inspektionslagret byggd på PDFium själv CMS SignedData (RFC 5652). InspectPadesSignatureAlgorithm läser digestAlgorithm och signatureAlgorithm för den första SignerInfo:n, hittar sedan signerarcertifikatet och läser dess SubjectPublicKeyInfo för att få nyckelalgoritmen och kurvan. Certifikatsökningen är medvetet avgränsad: högst 64 certifikat i CMS:ens certificates-mängd granskas, matchningen är en exakt bytejämförelse av utfärdare och serienummer från issuerAndSerialNumber, och koden faller tillbaka på "det enda certifikat som finns" bara när mängden håller exakt ett tolkbart certifikat. Att plocka första EC-certifikatet ur en oordnad mängd vore lätt, och det skulle låta ett CA-certifikat avgöra vilken kurva signeraren påstås ha använt

Hur PDFium Component inspekterar en PDF-signaturalgoritmtrippel: InspectPadesSignatureAlgorithm läser digestAlgorithm och signatureAlgorithm för den första SignerInfo:n i CMS:en, fäster signerarcertifikatet genom en exakt issuerAndSerialNumber-match bland högst 64 kandidater, läser SubjectPublicKeyInfo för kurvan, och EvaluatePadesSignatureAlgorithm returnerar AlgorithmPolicyStatus
PDFium själv verifierar varken CMS:en eller exponerar signerarkurvan, så PAdES-lagret tolkar SignedData och behåller varje rå OID i posten för en förklarbar avvisning
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 tillämpar policyn som en del av sitt efterlevnadsvarv, med utgångspunkt i certifikatet den lokaliserar inne i CMS:en, och TPdf.ValidatePadesTrust kör om den mot signerarcertifikatet som Windows CryptoAPI faktiskt använde för verifiering, så att det CryptoAPI-rapporterade certifikatet har sista ordet. Varje rå ingång hamnar i TPadesSignatureAlgorithmInfo, inklusive DigestParametersPresent, DigestParameterBits, SignatureParametersPresent och PublicKeyParametersAreNamedCurve, så att en avvisning alltid kan förklaras från posten i stället för från en loggrad

Varför faller en P-256-signatur med SHA3-256 på policyn?

En P-256-signatur faller på PDFium Components policy närhelst CMS:ens digestAlgorithm och digesten som ECDSA-signatureAlgorithm implicerar tvistar, även om båda var för sig är acceptabla för kurvan. EvaluatePadesSignatureAlgorithm mappar först ecdsa-with-SHA256, ecdsa-with-SHA3-256 och deras syskon till en digest, jämför den med den deklarerade digestAlgorithm och returnerar pcsInvalid vid varje skillnad innan kurvtabellen konsulteras. Fallet är verkligt: ett signeringsverktyg byter sin hash till SHA3-256 men behåller en hårdkodad ecdsa-with-SHA256-identifierare, och resultatet är en fil som ingen konform verifierare kan tolka konsekvent. Funktionen är publik, så matrisen kan låsas i ett unittest utan att bygga en 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); // digest stämmer inte

  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 kongruent
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // kurvan finns inte i profilen
end;

Kurvkodningen får samma strikthet. RFC 5480 §2.1.1 låter ECParameters vara en namngiven kurv-OID, en implicit kurva (NULL) eller en fullständig explicit parametermängd, och PKIX-profiler kräver den namngivna formen. PDFium Component returnerar pcsInvalid när ett id-ecPublicKey-certifikat bär inga parametrar, implicita parametrar eller explicita parametrar, eftersom explicita parametrar låter en angripare beskriva en kurva som bara liknar en standardkurva. En korrekt namngiven kurva som bara saknas i ISO/TS 32002-listan, som brainpoolP160r1 ovan eller secp256k1, får pcsUnsupported i stället

Ogiltigt, stöds ej eller obestämt: att läsa statusen ärligt

De tre icke-giltiga statusarna på AlgorithmPolicyStatus betyder olika saker, och att trycka ihop dem i en enda "underkänd"-hink kastar bort informationen revisorer behöver. pcsInvalid betyder att en igenkänd algoritmkombination är felformad eller mismatchad; den lägger till ppeiSignatureAlgorithmMismatch i TPadesValidationResult.Issues och driver den samlade IntegrityStatus till pcsInvalid, så att IsCryptographicallyValid returnerar False även när CMS-signaturvärdet stämmer. pcsUnsupported betyder att kurvan eller digesten ligger utanför vad profilen namnger, vilket är ett kapacitetsresultat, inte bevis för manipulering. pcsIndeterminate betyder att signerarcertifikatet inte kunde fastställas, vanligen en CertificateSet med flera kandidater och ingen exakt issuerAndSerialNumber-match, så att koden vägrar gissa kurvan; sedan v3.124.0 märker den också en RSA-signatur över SHA-1 eller en 112-bitars digest som SHA-224, som inte längre överenskommits för aktuell validering. Samma tredelning gäller EdDSA på en maskin vars CryptoAPI inte kan verifiera Ed25519 eller Ed448: CmsSignatureStatus stannar på pcsUnsupported medan AlgorithmPolicyStatus fortfarande kan vara pcsValid, eftersom kodningen var korrekt och bara verifieraren saknades. Om du jagar en avvisning från Adobe eller en DSS-baserad validator täcker guiden till varför validatorer avvisar PAdES-signaturer de andra vanliga orsakerna

Beslutsvägen i EvaluatePadesSignatureAlgorithm i PDFium Component: en digest-mismatch sätter pcsInvalid och ppeiSignatureAlgorithmMismatch, en namngiven kurva utanför ISO TS 32002-profilen som brainpoolP160r1 sätter pcsUnsupported, ett signerarcertifikat som inte kan fastställas sätter pcsIndeterminate, och sedan v3.124.0 tillämpar RSA-grenen digestsviterna i ETSI TS 119 312, med nyckelstorleken fortfarande lämnad åt programmet
De tre icke-giltiga statusarna betyder olika saker: invalid är bevis för en söndrig kombination, unsupported är ett kapacitetsresultat, och indeterminate betyder att koden vägrade gissa
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, ingen spärrning
    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;

Vad garanterar pcsValid inte?

AlgorithmPolicyStatus = pcsValid intygar bara att en ECDSA- eller EdDSA-signatur använder en godkänd kurva med en kongruent, korrekt kodad digest, och att en RSA-signatur använder en digest från en aktuell svit; den säger ingenting om signaturvärdet är korrekt. Före v3.124.0 var RSA-grenen i EvaluatePadesSignatureAlgorithm medvetet vid: en signatureAlgorithm under PKCS #1-bågen 1.2.840.113549.1.1.* returnerade pcsValid, äldre sha1WithRSAEncryption inbegripen. Sedan PDFiumPas v3.124.0 tillämpar RSA-grenen signatursviterna i ETSI TS 119 312. MD2-, MD4- och MD5-digests är pcsInvalid. SHA-1 och 112-bitars digests som SHA-224 är pcsIndeterminate, så att en SHA-1-signatur behåller sitt samlade integritetsresultat och märks för granskning i stället för att avvisas. En digestAlgorithm som skiljer sig från digesten som signaturalgoritmen fixerat, som sha256WithRSAEncryption över en SHA-1-digest, eller en RSA-signaturalgoritm på en icke-RSA-signerarnyckel, är pcsInvalid, rapporteras som ppeiSignatureAlgorithmMismatch och faller på integriteten. Ett signerarcertifikat som inte kan hittas ger pcsIndeterminate, som det redan gjorde för ECDSA, och okända digests eller icke-signatur-RSA-OID:er ger pcsUnsupported. Moduluslängden kontrolleras fortfarande inte, PSS-parametrar valideras inte här (artikeln om RFC 4055 RSASSA-PSS-parametrar täcker hur de kodas på signeringssidan), och SHA-1- eller MD5-digests utlöser dessutom det separata problemet ppeiBadDigestAlgorithm. På samma sätt är signaturmatematiken, certifikatkedjan och spärrningen fortfarande uppgiften för CmsSignatureStatus, CertificateTrustStatus och RevocationStatus, som kommer från Windows CryptoAPI. Behandla pcsValid som "algoritmsprofilen håller", aldrig som "den här nyckeln är stark nog"

För en Delphi-applikation som tar emot signerade PDF-fakturor, avtal eller arkivpaket är den praktiska uppställningen kort: kör ValidatePadesTrust, avvisa vid ppeiSignatureAlgorithmMismatch, dirigera pcsUnsupported och pcsIndeterminate till en människa, och kräv din egen undre gräns för RSA-nyckelstorlek, för policyn gör det inte. PDFium Component för Delphi och Lazarus skeppar PAdES-validatoren, byggaren av bevisrapporter och signeringspipelinen, så att samma bibliotek kan producera dessa signaturer och kontrollera dem hela vägen