Un hex dump di una signatureAlgorithm RSASSA-PSS che finisce con A0 0F … A1 1C … 02 01 20 02 01 01 sembra completo: SHA-256, MGF1, un salt di 32 byte e un trailer di 1. Un decoder severo respinge quell'insieme di parametri in blocco, e uno permissivo legge la lunghezza del salt come 20. In entrambi i casi, una firma la cui aritmetica RSA è corretta torna invalida. RFC 4055 §3.1 spiega perché: ogni campo di RSASSA-PSS-params porta un tag di contesto esplicito, quindi il salt appartiene dentro A2, e sotto DER un trailerField pari al suo default non deve comparire affatto
Questo dà a una firma CMS due metà che possono fallire indipendentemente. Una è l'operazione crittografica; l'altra è l'ASN.1 che dice al verificatore come è stata eseguita. Beccate la prima giusta e la seconda sbagliata, e un verificatore che segue la specifica non ha alcun modo di distinguere il vostro documento da un falso. La metà ASN.1 è quella di cui si occupa questo articolo
Perché un verificatore rifiuta una firma RSASSA-PSS i cui byte sono corretti?
Perché RSASSA-PSS è l'unico schema RSA in cui il verificatore non può ricavare i parametri dall'OID dell'algoritmo. PKCS#1 v1.5 è completamente conficcato da sha256WithRSAEncryption, quindi i suoi parametri sono un NULL nudo. PSS è parametrizzato da un hash del messaggio, una funzione di generazione della maschera con il suo hash, e una lunghezza del salt, e RFC 8017 §A.2.3 lascia tutti e tre al firmatario. L'AlgorithmIdentifier porta le scelte del firmatario, e il verificatore deve riprodurle esattamente prima che EMSA-PSS-VERIFY possa girare
Un blocco che il verificatore non sa analizzare chiude la verifica prima che accada qualsiasi elevamento a potenza modulare. Un blocco che analizza diversamente è peggio. RFC 4055 §3.1 dà a saltLength un default di 20, quindi un decoder che non trova mai il campo del salt fa girare EMSA-PSS-VERIFY con un salt da 20 byte contro una firma calcolata con 32. Il risultato viene riferito come firma sbagliata, senza nulla che punti ai metadati. Quanto è severo il verificatore decide quale dei due fallimenti vedete, e nessuno dei due messaggi d'errore menziona l'AlgorithmIdentifier
I quattro campi di RSASSA-PSS-params e i loro tag
RFC 4055 §3.1 definisce RSASSA-PSS-params come una SEQUENCE di quattro campi, ciascuno con un tag di contesto esplicito e un valore DEFAULT. In DER, il tagging esplicito avvolge ogni campo in un TLV di contesto costruito — da A0 a A3 — con la codifica universale del campo stesso annidata dentro:
| Campo | Tag (byte DER) | DEFAULT | Valore ETSI TS 119 312 §7 | Codificato? |
|---|---|---|---|---|
hashAlgorithm | [0] (A0) | sha1 | SHA-256 | Sì |
maskGenAlgorithm | [1] (A1) | mgf1SHA1 | MGF1 con SHA-256 | Sì |
saltLength | [2] (A2) | 20 | 32 (la lunghezza del digest) | Sì |
trailerField | [3] (A3) | trailerFieldBC (1) | 1 | No, è pari al default |
I tag ci sono perché ogni campo è opzionale attraverso il suo default. hashAlgorithm e maskGenAlgorithm sono entrambe SEQUENCE AlgorithmIdentifier; senza un tag, un decoder davanti a un blocco di parametri che contiene una SEQUENCE sola non saprebbe dire quale delle due sia. Con i tag, il numero di tag nomina il campo a prescindere da quali vicini il codificatore abbia lasciato fuori
Scegliete un salt pari alla lunghezza del digest e tenetelo lì. Alcune API offrono "salt massimo" o "salt length auto", che dà un valore dipendente dalla taglia della chiave; è legale sotto RFC 8017, ma l'AlgorithmIdentifier allora deve dichiarare un numero che cambia da chiave a chiave, e un salt da 32 byte per SHA-256 è ciò che ETSI TS 119 312 §7 chiede comunque. I backend di firma incorporati di PDFium Component lo fissano tutti a 32 e passano lo stesso numero alla chiamata di piattaforma: cbSalt nella BCRYPT_PSS_PADDING_INFO consegnata a NCryptSignHash su Windows, sLen in CK_RSA_PKCS_PSS_PARAMS per un token PKCS#11, EVP_PKEY_CTX_set_rsa_pss_saltlen sotto OpenSSL, e il salt della lunghezza del digest che kSecKeyAlgorithmRSASignatureDigestPSSSHA256 implica su macOS
Scrivere GetSignatureAlgorithmParams per il vostro ICmsSigner
In PDFium Component, ogni backend di firma implementa l'interfaccia ICmsSigner di FPdfCms.pas, e il blocco dei parametri viene da un metodo, GetSignatureAlgorithmParams. Il costruttore CMS abbina il suo risultato all'OID di GetSignatureAlgorithmOid tramite TDerWriter.AlgIdWithParams. Incontrate questo metodo quando innestate un firmatario vostro tramite ConfigurePadesSignerProvider — un HSM di rete, un servizio di firma, un driver di smart card che i backend incorporati non coprono. La codifica che la maggior parte della gente scrive per prima etichetta i due AlgorithmIdentifier, perché ovviamente richiedono un involucro, e lascia i due interi nudi:
// Un primo tentativo comune: [0] e [1] etichettati, [2] e [3] no
Result := W.Sequence(Concat4(
W.ContextSpecific(0, W.AlgId('2.16.840.1.101.3.4.2.1'), True),
W.ContextSpecific(1, W.Sequence(ConcatBytes(
W.OID('1.2.840.113549.1.1.8'),
W.AlgId('2.16.840.1.101.3.4.2.1'))), True),
W.IntegerOf(32), // INTEGER nudo dove è richiesto [2] EXPLICIT
W.IntegerOf(1))); // trailerField pari al DEFAULT: deve essere assente
Percorretelo come farebbe un decoder. Vede A0 e legge l'hash, vede A1 e legge la funzione di generazione della maschera, poi incontra 02 01 20, un INTEGER universale. RSASSA-PSS-params non ha nessun membro INTEGER senza tag da nessuna parte, quindi un decoder severo si ferma. Uno permissivo salta l'elemento sconosciuto, non trova mai un A2, assegna il salt di default di 20, e poi inciampa in un secondo INTEGER smarrito, 02 01 01. Nessuna delle due strade arriva a 32
La versione conforme usa tre chiamate a TDerWriter.ContextSpecific da FPdfAsn1.pas, una per campo non predefinito, ciascuna con Constructed impostato a True così il risultato è un involucro esplicito. Non c'è nessuna riga per il campo trailer. È lo stesso blocco che emette i backend Windows CNG, macOS Keychain, PKCS#11 e OpenSSL, scritto qui come lo porterebbe un firmatario personalizzato:
uses
System.SysUtils, FPdfSha256, FPdfAsn1, FPdfCms;
type
TMyHsmSigner = class(TInterfacedObject, ICmsSigner)
private
FUsePss: Boolean;
public
function GetDigestAlgorithmOid: AnsiString;
function GetSignatureAlgorithmOid: AnsiString;
function GetSignatureAlgorithmParams: TBytes;
function GetSignerId: TCmsSignerId;
function SignSignedAttrsDigest(const Digest: TSHA256Digest): TBytes;
end;
// La vostra classe; gli altri quattro metodi parlano con il vostro HSM e non sono mostrati
function TMyHsmSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if not FUsePss then
Exit(nil); // v1.5 ed ECDSA: AlgIdWithParams scrive NULL per nil
W := TDerWriter.Create;
try
// RFC 4055 3.1: [0] hash, [1] MGF1 con il suo hash, [2] salt.
// [3] trailerField = 1 è pari al suo DEFAULT, quindi X.690 11.5 lo omette
Result := W.Sequence(Concat3(
W.ContextSpecific(0, W.AlgId('2.16.840.1.101.3.4.2.1'), True),
W.ContextSpecific(1, W.Sequence(ConcatBytes(
W.OID('1.2.840.113549.1.1.8'), // id-mgf1
W.AlgId('2.16.840.1.101.3.4.2.1'))), True),
W.ContextSpecific(2, W.IntegerOf(32), True)));
finally
W.Free;
end;
end;
TDerWriter.AlgId scrive un AlgorithmIdentifier con parametri NULL, che è ciò che RFC 4055 §2.1 dice ai codificatori di generare per gli identificatori SHA-256 annidati. I byte risultanti sono corti abbastanza da controllarsi a occhio: un 30 34 esterno, poi A0 0F attorno all'AlgorithmIdentifier SHA-256 di 15 byte, A1 1C attorno all'AlgorithmIdentifier MGF1 di 28 byte, e A2 03 02 01 20 per il salt. Il salt che scrivete in GetSignatureAlgorithmParams deve combaciare con quello che il vostro HSM usa davvero; se il servizio espone solo "PSS con SHA-256" e non vi dice la lunghezza del salt, scopritela prima di spedire, perché l'AlgorithmIdentifier è un'affermazione su di esso
Perché trailerField viene omesso anziché etichettato come [3]?
X.690 §11.5 dice che un codificatore DER non deve codificare un componente il cui valore è pari al suo DEFAULT. trailerField ha DEFAULT trailerFieldBC, l'intero 1, e PSS così come RFC 8017 lo definisce usa solo quel trailer. Quindi la riparazione ovvia al primo tentativo — sostituire W.IntegerOf(1) con W.ContextSpecific(3, W.IntegerOf(1), True) — produce A3 03 02 01 01, che un decoder BER permissivo accetta e un decoder DER severo è nel diritto di respingere. Il valore è giusto. La sua presenza no
La stessa regola spiega perché gli altri tre campi compaiono: SHA-256 non è sha1, MGF1 con SHA-256 non è mgf1SHA1, e 32 non è 20. Un firmatario che usa SHA-1 con un salt da 20 byte collasserebbe l'intero blocco in una SEQUENCE vuota, 30 00. Quella forma fa inciampare chiunque presupponga che "nessun parametro" si scriva sempre 05 00; per id-RSASSA-PSS in CMS il campo dei parametri è obbligatorio, una SEQUENCE vuota significa "tutti i default", e NULL non è un valore valido di RSASSA-PSS-params
BER consente a un codificatore di includere un componente con valore predefinito; DER lo vieta, perché DER esiste perché un valore abbia esattamente una codifica, e una firma su una struttura con due codifiche legali può essere contesa. Il blocco dei parametri non può sfuggire a DER qui. Siede nella signatureAlgorithm esterna del SignerInfo, e viene copiato negli attributi firmati tramite l'attributo cmsAlgorithmProtection di RFC 6211, dove ogni byte è coperto dalla firma
Controllare un CMS che non avete costruito voi
Quella copia dentro cmsAlgorithmProtection non becca un blocco mal taggato. L'attributo e la signatureAlgorithm esterna vengono dalla stessa chiamata, quindi combaciano sempre, e un confronto della struttura con se stessa passa. Ciò che becca il problema è un decoder che legge il campo dei parametri campo per campo. L'InspectPadesSignatureAlgorithm di PDFium Component in FPdfPades.pas lo fa: accetta solo da A0 a A3, ciascuno al massimo una volta e in ordine crescente, riempie i default di RFC 4055 per tutto ciò che manca, e riferisce i valori effettivi in TPadesRsaPssParameters. Un INTEGER nudo nel blocco lascia SignaturePss.Valid a False, e EvaluatePadesSignatureAlgorithm imposta allora PolicyStatus a pcsInvalid. Respinge anche un hash PSS che differisca dall'algoritmo di digest del SignerInfo, che è esattamente ciò che produrrebbe un fallback permissivo a sha1
Questo conta di più con la firma remota, dove un servizio esterno restituisce un CMS finito e CompletePadesRemoteSignature lo incorpora nel PDF preparato. Controllate il blob prima di incorporarlo. Respingerlo in quel punto costa una chiamata API; scoprirla dal validatore di un cliente costa un giro di ri-firma con chi gestisce il servizio:
uses
System.SysUtils, FPdfPades;
// Controllo suggerito che aggiungete voi prima di CompletePadesRemoteSignature
function CheckRemotePssCms(const CmsDer: TBytes; out Why: string): Boolean;
const
OID_RSASSA_PSS = '1.2.840.113549.1.1.10';
var
Info: TPadesSignatureAlgorithmInfo;
Values: string;
begin
Result := False;
Why := '';
// nil: lasciate che l'ispettore localizzi il certificato del firmatario dentro il CMS
if not InspectPadesSignatureAlgorithm(CmsDer, nil, Info) then
begin
Why := 'CMS SignerInfo could not be parsed';
Exit;
end;
if Info.SignatureAlgorithmOid = OID_RSASSA_PSS then
begin
if not Info.SignaturePss.Valid then
begin
Why := 'RSASSA-PSS-params does not decode as RFC 4055 3.1';
Exit;
end;
// Valori effettivi, default inclusi: un A2 mancante appare qui come 20
Why := Format('hash %s, MGF hash %s, salt %d', [
string(Info.SignaturePss.HashAlgorithmOid),
string(Info.SignaturePss.MaskGenHashAlgorithmOid),
Info.SignaturePss.SaltLength]);
end;
if Info.PolicyStatus = pcsInvalid then
Exit; // Why conserva ancora i valori decodificati per il log
Values := Why;
Result := VerifyCmsAlgorithmProtection(CmsDer, Why);
if Result then
Why := Values; // il controllo di protezione azzera Why in caso di successo
end;
Registrate la stringa Why anche in caso di successo. Una lunghezza del salt di 20 accanto a un hash SHA-256 è l'impronta digitale di un blocco di parametri il cui A2 è sparito, a prescindere da ciò che dicono i log del servizio. Per un'occhiata veloce senza codice, openssl asn1parse -inform DER -in signature.p7s stampa l'albero; sotto l'OID id-RSASSA-PSS volete vedere cont [ 2 ] che avvolge un INTEGER di 20 esadecimale, e nessun cont [ 3 ] affatto. Un INTEGER al livello più alto dentro quella SEQUENCE è la codifica mal taggata del primo blocco di codice
Dove questo si colloca nel resto della storia PSS
La codifica dei parametri è una di tre strade separate per cui PSS può andare storto in una firma PAdES, e tenerle separate accorcia il debugging. Su macOS una particolare chiave o un sistema più vecchio può non offrire PSS, e il backend allora firma con PKCS#1 v1.5 e dichiara sha256WithRSAEncryption per combaciare; quella domanda di capacità è trattata in firmare PAdES con un'identità Keychain macOS. Un token PKCS#11 può leggere CK_RSA_PKCS_PSS_PARAMS con una larghezza intera diversa da quella scritta dal chiamante, una questione ABI trattata in CK_ULONG e la trappola di packing del PKCS#11. La terza è quella qui sopra: la chiave è disponibile, il token calcola i byte giusti, e il DER che li descrive non segue RFC 4055 §3.1
Il SET degli attributi firmati ha la sua versione del punto cieco dell'auto-coerenza, dove un SET con hash in un ordine ed emesso in un altro sembra a posto finché un verificatore esterno non ricalcola l'hash; vedete signedAttrs CMS e l'ordinamento DER SET OF. Per il ciclo prepara, firma da remoto, completa in cui il controllo qui sopra si innesta, la firma PAdES remota con un HSM copre il protocollo a due fasi e ciò che il passo di completamento rifiuta
Il componente PDFium per Delphi spedisce i suoi backend di firma come sorgente, così i corpi di GetSignatureAlgorithmParams nelle unità Windows, macOS, PKCS#11 e OpenSSL sono lì da leggere e scaricare accanto all'output del vostro firmatario, byte per byte