Articolo tecnico

Policy ECDSA e EdDSA di ISO/TS 32002 con PDFium in Delphi

Il PDFium Component per Delphi verifica ogni firma PDF ECDSA e EdDSA contro il profilo algoritmico di ISO/TS 32002 e riferisce il verdetto in TPadesSignatureValidation.AlgorithmPolicyStatus. Solo P-256, P-384, P-521, le tre curve Brainpool r1, Ed25519 e Ed448 possono passare, ciascuna con un digest corrispondente, e uno scarto solleva ppeiSignatureAlgorithmMismatch. Quella policy conta più di quanto sembri. Una firma su brainpoolP160r1, o una chiave P-256 che firma un digest SHA-512, può verificare benissimo a livello matematico, quindi Windows CryptoAPI dichiara buona la firma mentre un validator severo PDF 2.0 rifiuta il file. Il controllo di policy chiude quel divario, ed è volutamente separato dalla domanda se i byte della firma siano criptograficamente corretti

Che cosa consente davvero ISO/TS 32002 per le firme a curva ellittica?

ISO/TS 32002 consente esattamente sei curve ECDSA e due schemi EdDSA nelle firme PDF, e lega ogni curva alle dimensioni di digest che può portare. Il PDFium Component codifica quella tabella in PadesCurveDigestAllowed, indicizzata per l'OID della curva dal certificato del firmatario. Le curve NIST sono rigide: il digest deve avere la stessa larghezza in bit della curva, SHA-2 oppure SHA-3. Le curve Brainpool sono più larghe e accettano la loro larghezza o qualsiasi cosa più larga:

  • P-256 (1.2.840.10045.3.1.7): solo SHA-256 o SHA3-256
  • P-384 (1.3.132.0.34): solo SHA-384 o SHA3-384
  • P-521 (1.3.132.0.35): solo SHA-512 o SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): qualsiasi digest SHA-2 o SHA-3 da 256 a 512 bit
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 da 384 o 512 bit
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): solo SHA-512 o SHA3-512
Matrice del profilo algoritmico ISO TS 32002 in PDFium Component: P-256, P-384 e P-521 accettano solo larghezze di digest corrispondenti in PadesCurveDigestAllowed, brainpoolP256r1 accetta da 256 a 512 bit, brainpoolP384r1 accetta 384 e 512, Ed25519 e Ed448 dichiarano SHA-512 e SHAKE256 con lunghezza 512, e le incongruenze sollevano ppeiSignatureAlgorithmMismatch mentre le curve fuori profilo restituiscono pcsUnsupported
Sei curve ECDSA e due schemi EdDSA possono passare, e ogni curva è legata alle larghezze di digest che può portare; tutto il resto è invalido o non supportato, mai accettato in silenzio

EdDSA non ha scelta di curva né scelta di digest, ed è esattamente per questo che le sue regole riguardano la codifica più che la robustezza. Per RFC 8419, un SignerInfo Ed25519 deve dichiarare SHA-512 come suo digestAlgorithm senza parametri, e un SignerInfo Ed448, sul percorso dei signed-attributes che PAdES usa sempre, deve dichiarare id-shake256-len (2.16.840.1.101.3.4.2.18) con un parametro INTEGER di esattamente 512. Per entrambi gli schemi l'AlgorithmIdentifier della firma e l'AlgorithmIdentifier della chiave pubblica del certificato non devono portare alcun parametro. Un produttore che scrive lì un NULL, l'abitudine che i codificatori RSA hanno inoculato in molte librerie ASN.1, produce una firma non conforme anche se chiave e valore della firma stanno bene

Come il PDFium Component estrae la tripla algoritmica dal CMS

PDFium da sé non può rispondere a questa domanda, perché la sua API pubblica delle firme legge il dizionario della firma ma né verifica il CMS né espone la curva del certificato del firmatario. Il livello di ispezione PAdES costruito su PDFium analizza quindi da sé il CMS SignedData (RFC 5652). InspectPadesSignatureAlgorithm legge digestAlgorithm e signatureAlgorithm del primo SignerInfo, poi trova il certificato del firmatario e legge il suo SubjectPublicKeyInfo per ottenere l'algoritmo della chiave e la curva. La ricerca del certificato è volutamente limitata: al massimo 64 certificati nell'insieme certificates del CMS vengono esaminati, la corrispondenza è un confronto esatto a byte di issuer e numero di serie da issuerAndSerialNumber, e il codice ricade su "l'unico certificato che c'è" solo quando l'insieme contiene esattamente un certificato analizzabile. Prendere il primo certificato EC da un insieme non ordinato sarebbe facile, e lascerebbe a un certificato di CA decidere quale curva il firmatario avrebbe usato

Come il PDFium Component ispeziona una tripla algoritmica di firma PDF: InspectPadesSignatureAlgorithm legge digestAlgorithm e signatureAlgorithm del primo SignerInfo nel CMS, aggancia il certificato del firmatario tramite una corrispondenza esatta issuerAndSerialNumber tra al massimo 64 candidati, legge il SubjectPublicKeyInfo per la curva, e EvaluatePadesSignatureAlgorithm restituisce l'AlgorithmPolicyStatus
PDFium da sé né verifica il CMS né espone la curva del firmatario, quindi il livello PAdES analizza il SignedData e tiene ogni OID grezzo nel record per un rifiuto spiegabile
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 applica la policy come parte del suo passaggio di conformità, partendo dal certificato che individua dentro il CMS, e TPdf.ValidatePadesTrust la riesegue contro il certificato del firmatario che Windows CryptoAPI ha davvero usato per la verifica, quindi il certificato riferito da CryptoAPI ha l'ultima parola. Ogni input grezzo atterra in TPadesSignatureAlgorithmInfo, compresi DigestParametersPresent, DigestParameterBits, SignatureParametersPresent e PublicKeyParametersAreNamedCurve, così un rifiuto è sempre spiegabile dal record anziché da una riga di log

Perché una firma P-256 con SHA3-256 fallisce la policy?

Una firma P-256 fallisce la policy del PDFium Component ogni volta che il digestAlgorithm del CMS e il digest implicato dall'signatureAlgorithm ECDSA non sono d'accordo, anche se entrambi sono singolarmente accettabili per la curva. EvaluatePadesSignatureAlgorithm mappa prima ecdsa-with-SHA256, ecdsa-with-SHA3-256 e i loro fratelli a un digest, confronta quello con il digestAlgorithm dichiarato e restituisce pcsInvalid a qualsiasi differenza prima ancora di consultare la tabella delle curve. Il caso è reale: uno strumento di firma passa il suo hash a SHA3-256 ma mantiene un identificatore ecdsa-with-SHA256 hard-coded, e il risultato è un file che nessun verifier conforme può interpretare in modo coerente. La funzione è pubblica, quindi la matrice si può inchiodare in uno unit test senza costruire un 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 non corrispondente

  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, ora congruente
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // curva fuori dal profilo
end;

La codifica della curva riceve la stessa severità. RFC 5480 §2.1.1 lascia che ECParameters sia un OID di curva nominata, una curva implicita (NULL) o un set completo di parametri espliciti, e i profili PKIX richiedono la forma nominata. Il PDFium Component restituisce pcsInvalid quando un certificato id-ecPublicKey non porta parametri, porta parametri impliciti o parametri espliciti, perché i parametri espliciti lasciano a un attaccante descrivere una curva che somiglia soltanto a una standard. Una curva correttamente nominata che manca semplicemente dalla lista di ISO/TS 32002, come la brainpoolP160r1 sopra o la secp256k1, riceve invece pcsUnsupported

Invalid, unsupported o indeterminate: leggere lo stato con onestà

I tre stati non validi di AlgorithmPolicyStatus significano cose diverse, e accorparli in un unico secchio "fallito" butta via l'informazione di cui gli auditor hanno bisogno. pcsInvalid significa che una combinazione di algoritmi riconosciuta è malformata o disallineata; aggiunge ppeiSignatureAlgorithmMismatch a TPadesValidationResult.Issues e spinge l'IntegrityStatus aggregato a pcsInvalid, così IsCryptographicallyValid restituisce False anche quando il valore della firma CMS torna. pcsUnsupported significa che curva o digest stanno fuori da ciò che il profilo nomina, che è un risultato di capacità, non prova di manomissione. pcsIndeterminate significa che il certificato del firmatario non è stato agganciato, di solito un CertificateSet con diversi candidati e nessuna corrispondenza esatta di issuerAndSerialNumber, quindi il codice si rifiuta di indovinare la curva; dalla v3.124.0 segnala anche una firma RSA su SHA-1 o un digest a 112 bit come SHA-224, che non è più concordato per la validazione attuale. La stessa ripartizione vale per EdDSA su una macchina il cui CryptoAPI non sa verificare Ed25519 o Ed448: CmsSignatureStatus resta pcsUnsupported mentre AlgorithmPolicyStatus può ancora essere pcsValid, perché la codifica era corretta e mancava solo il verifier. Se state inseguendo un rifiuto da Adobe o da un validator basato su DSS, la guida ai motivi per cui i validator rifiutano le firme PAdES copre le altre cause comuni

Percorso decisionale di EvaluatePadesSignatureAlgorithm in PDFium Component: uno scarto di digest imposta pcsInvalid e ppeiSignatureAlgorithmMismatch, una curva nominata fuori dal profilo ISO TS 32002 come brainpoolP160r1 imposta pcsUnsupported, un certificato firmatario non agganciabile imposta pcsIndeterminate, e dalla v3.124.0 il ramo RSA applica le suite di digest ETSI TS 119 312, con la dimensione della chiave ancora lasciata all'applicazione
I tre stati non validi significano cose diverse: invalid è prova di una combinazione rotta, unsupported è un risultato di capacità, e indeterminate significa che il codice si è rifiutato di indovinare
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, nessuna revoca
    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;

Che cosa pcsValid non garantisce?

AlgorithmPolicyStatus = pcsValid certifica solo che una firma ECDSA o EdDSA usa una curva approvata con un digest congruente e correttamente codificato, e che una firma RSA usa un digest di una suite attuale; non dice nulla su se il valore della firma sia corretto. Prima della v3.124.0 il ramo RSA di EvaluatePadesSignatureAlgorithm era volutamente largo: qualsiasi signatureAlgorithm sotto l'arco PKCS #1 1.2.840.113549.1.1.* restituiva pcsValid, sha1WithRSAEncryption compreso. Da PDFiumPas v3.124.0 il ramo RSA applica le suite di firma ETSI TS 119 312. I digest MD2, MD4 e MD5 sono pcsInvalid. SHA-1 e i digest a 112 bit come SHA-224 sono pcsIndeterminate, quindi una firma SHA-1 conserva il suo risultato di integrità complessivo e viene segnalata per revisione anziché rifiutata. Un digestAlgorithm che differisce dal digest fissato dall'algoritmo di firma, come sha256WithRSAEncryption su un digest SHA-1, o un algoritmo di firma RSA su una chiave firmatario non RSA, è pcsInvalid, riferito come ppeiSignatureAlgorithmMismatch e fallisce l'integrità. Un certificato firmatario introvabile dà pcsIndeterminate, come già faceva per ECDSA, e digest non riconosciuti o OID RSA non di firma danno pcsUnsupported. La lunghezza del modulo non è ancora controllata, i parametri PSS non sono validati qui (l'articolo su RSASSA-PSS-params di RFC 4055 copre come vengono codificati sul lato firma), e i digest SHA-1 o MD5 sollevano in più la questione separata ppeiBadDigestAlgorithm. Allo stesso modo, la matematica della firma, la catena dei certificati e la revoca restano compito di CmsSignatureStatus, CertificateTrustStatus e RevocationStatus, che arrivano da Windows CryptoAPI. Trattate pcsValid come "il profilo algoritmico regge", mai come "questa chiave è abbastanza robusta"

Per un'applicazione Delphi che accetta fatture, contratti o pacchetti archivio PDF firmati, l'assetto pratico è breve: eseguite ValidatePadesTrust, rifiutate su ppeiSignatureAlgorithmMismatch, instradate pcsUnsupported e pcsIndeterminate a un essere umano, e imponete la vostra soglia minima di dimensione chiave RSA perché la policy non lo farà. Il PDFium Component for Delphi and Lazarus spedisce il validator PAdES, il costruttore di report di evidenza e la pipeline di firma, così la stessa libreria può produrre queste firme e verificarle end to end