Spostate una routine di firma Delphi su SHA3-512 e OpenSSL respinge il risultato: il CMS dichiara SHA3-512, eppure il suo attributo messageDigest contiene 32 byte dove quell'algoritmo ne produce 64. Sì, il codice Delphi può firmare con SHA-3: SignPadesForDigest di PDFium Component accetta SHA3-256, SHA3-384 o SHA3-512 e scrive ogni campo CMS che dipende dal digest per quella sola scelta, come si attende ISO/TS 32001
Cosa aggiunge ISO/TS 32001 a una firma PDF 2.0
ISO/TS 32001 è una breve estensione di ISO 32000-2. Allarga l'insieme delle funzioni hash che una firma PDF può dichiarare. La lista di base siede nella Tabella 260 di ISO 32000-2 §12.8.3.1, e per i subfilter basati su CMS si ferma a SHA-256, SHA-384, SHA-512 e RIPEMD-160, con SHA-1 deprecata. La specifica tecnica aggiunge la famiglia SHA-3 di FIPS 202. SHA-3 è una costruzione a spugna che non condivide nessuna struttura interna con SHA-2, ed è tutto il fascino per una politica di firma che vuole una seconda famiglia di hash sullo scaffale nel caso SHA-2 si indebolisca un giorno. In CMS ogni variante è un object identifier sotto l'arco NIST. Gli identificatori degli algoritmi di firma combinati siedono proprio accanto:
TCmsDigestAlgorithm | OID del digest | Output | RSA PKCS#1 v1.5 | ECDSA |
|---|---|---|---|---|
cdaSha256 | 2.16.840.1.101.3.4.2.1 | 32 byte | 1.2.840.113549.1.1.11 | 1.2.840.10045.4.3.2 |
cdaSha3_256 | 2.16.840.1.101.3.4.2.8 | 32 byte | 2.16.840.1.101.3.4.3.14 | 2.16.840.1.101.3.4.3.10 |
cdaSha3_384 | 2.16.840.1.101.3.4.2.9 | 48 byte | 2.16.840.1.101.3.4.3.15 | 2.16.840.1.101.3.4.3.11 |
cdaSha3_512 | 2.16.840.1.101.3.4.2.10 | 64 byte | 2.16.840.1.101.3.4.3.16 | 2.16.840.1.101.3.4.3.12 |
In PDFium Component l'intera scelta è una enumerazione, TCmsDigestAlgorithm dall'unit FPdfCms. Lasciate che sia la chiave a scegliere il membro. Una curva NIST si attende un digest della sua stessa larghezza, così P-256 si abbina a SHA3-256, P-384 a SHA3-384 e P-521 a SHA3-512, e un validatore che applica la politica di curve e digest di ISO/TS 32002 marca ogni altro abbinamento come invalido. RSA non ha una regola del genere. L'argomento della robustezza è comunque breve: una chiave RSA-2048 eroga circa 112 bit di sicurezza e una RSA-3072 circa 128, così SHA3-256 corre già più veloce della chiave, mentre SHA3-512 su una chiave RSA-2048 aggiunge 32 byte al CMS e nulla alla sicurezza. Raccomandiamo SHA3-256 per RSA, a meno che una politica scritta non nomini una variante più grande
Cinque campi CMS che cambiano con il digest
Sostituire la funzione hash è la parte più piccola del lavoro. Una firma CAdES detached nomina il suo digest in cinque posti, e un verificatore severo li ricontrolla a incrocio. Se anche uno solo dice ancora SHA-256, la firma fallisce anche se l'aritmetica RSA o ECDSA è corretta:
digestAlgorithmnel SignerInfo, riflesso nell'insiemedigestAlgorithmsdel SignedData, porta l'OID del digest, per esempio 2.16.840.1.101.3.4.2.9 per SHA3-384- L'attributo firmato
messageDigest(RFC 5652) contiene l'hash dei due intervalli/ByteRange, così il suo OCTET STRING è lungo 32, 48 o 64 byte signatureAlgorithmnomina lo schema combinato della tabella qui sopra. RSASSA-PSS è l'eccezione: il suo OID resta id-RSASSA-PSS (1.2.840.113549.1.1.10) e il digest si sposta nei parametri- I parametri RSASSA-PSS ripetono il digest due volte, come
hashAlgorithme come hash MGF1, e portanosaltLengthalla lunghezza del digest. Se li omettete, un verificatore presuppone SHA-1, MGF1 con SHA-1 e un salt di 20 byte, che è un altro algoritmo; anche le regole di tagging dei RSASSA-PSS-params di RFC 4055 contano qui - L'attributo firmato CMSAlgorithmProtection (RFC 6211) copia gli identificatori del digest e dell'algoritmo di firma negli attributi firmati, dove nessuno può scambiarli senza che si noti. Deve coincidere coi campi 1 e 3 byte per byte
Un campo non cambia affatto. L'ESSCertIDv2 dentro l'attributo signing-certificate-v2 (RFC 5035) hasha il certificato del firmatario con il suo hashAlgorithm, che per impostazione predefinita vale SHA-256 e è indipendente dal digest del SignedData, così PDFium Component lo mantiene su SHA-256 nella codifica predefinita. Lo sbaglio che compare più spesso nel CMS scritto a mano è la lunghezza del messageDigest. Il codice scritto per SHA-256 tende a tenere l'hash in un record fisso di 32 byte e a copiare quel record nell'attributo. Passateci dentro un risultato SHA3-512 e l'attributo riceve i primi 32 byte mentre digestAlgorithm dice .10: è il rifiuto di OpenSSL dell'apertura. Lo stesso sbaglio vive anche uno strato più in basso, dove l'EVP_PKEY_sign di OpenSSL prende la lunghezza del digest come tbslen e la controlla contro il message digest impostato sul contesto, così un firmatario che passa sempre 32 si prende un errore di lunghezza che non nomina mai SHA-3. PDFium Component calcola il digest del documento dall'algoritmo che il firmatario risolto dichiara e deriva gli attributi, i parametri PSS e l'attributo di protezione dell'algoritmo dalla stessa dichiarazione, così nessun campo può allontanarsi dagli altri
Firmare un PDF locale con SignPadesForDigest
Sul componente TPdf i punti d'ingresso SHA-3 sono SignPadesForDigest(const FileName: string; const Options: TPadesSignOptions; DigestAlgorithm: TCmsDigestAlgorithm): Boolean e il suo gemello su stream SignPadesToStreamForDigest. Entrambi prendono lo stesso record TPadesSignOptions di SignPades, e cdaSha256 produce la stessa firma dell'overload esistente, così un solo percorso di codice serve entrambe le famiglie. Su Windows un thumbprint SHA-1 seleziona un certificato nello store Current User MY. Il backend OpenSSL sulle altre piattaforme firma con chiave e certificato dati a ConfigureSslSigningCredentials e rifiuta un thumbprint. Controllate anche la versione del documento. ISO/TS 32001 estende il PDF 2.0, e la firma entra come aggiornamento incrementale che conserva l'intestazione originale, così una relying party che valida secondo le regole PDF 2.0 vuole un file 2.0 prima che firmiate
uses
System.SysUtils, System.Classes, System.IOUtils,
PDFium, FPdfCms, FPdfCrypto, FPdfPades
{$IFDEF MSWINDOWS}, FPdfCryptoWin{$ELSE}, FPdfCryptoSsl{$ENDIF};
function Sha3SigningAvailable: Boolean;
begin
{$IFDEF MSWINDOWS}
Result := WinSha3DigestAvailable; // provider SHA-3 di CNG, Windows 11 24H2 e oltre
{$ELSE}
Result := SslSha3SigningAvailable; // False finché le credenziali non sono configurate
{$ENDIF}
end;
procedure SignWithSha3(const SourceFile, SignedFile, Thumbprint: string;
const KeyPassword: AnsiString; const Log: TProc<string>);
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Signed: TMemoryStream;
begin
{$IFNDEF MSWINDOWS}
// OpenSSL firma con materiale configurato esplicitamente; nessuna catena in questo abbozzo
ConfigureSslSigningCredentials(
TFile.ReadAllBytes('/etc/signing/signer-key.pem'),
TFile.ReadAllBytes('/etc/signing/signer.der'), nil, KeyPassword);
{$ENDIF}
if not Sha3SigningAvailable then
raise Exception.Create('No SHA-3 capable signing backend on this machine');
Opts := TPadesSignOptions.Default; // B-B, RSASSA-PSS
{$IFDEF MSWINDOWS}
Opts.CertificateThumbprint := Thumbprint; // certificato in Current User MY
{$ENDIF}
Opts.Reason := 'Approved for release';
Pdf := TPdf.Create(nil);
Signed := TMemoryStream.Create;
try
Pdf.FileName := SourceFile;
Pdf.Active := True;
if Pdf.PdfVersion <> pv20 then
Log(SourceFile + ' is not a PDF 2.0 file');
try
if Pdf.SignPadesToStreamForDigest(Signed, Opts, cdaSha3_256) then
Signed.SaveToFile(SignedFile);
except
on E: EPadesCryptoUnavailable do
Log('SHA-3 signing unavailable: ' + E.Message);
end;
finally
Signed.Free;
Pdf.Free;
end;
end;
Quando il backend non sa produrre SHA-3, la chiamata solleva EPadesCryptoUnavailable invece di ripiegare su SHA-256. È il comportamento giusto da volere. Qualcuno ha chiesto SHA3-256 per un motivo, di solito un documento di politica della relying party, e una firma SHA-256 prodotta al posto suo passerebbe ogni controllo dal vostro lato e verrebbe respinta dal loro, settimane dopo, quando nessuno nel vostro team ricorda quale macchina abbia firmato. Catturate l'eccezione dove potete ancora dire all'operatore quale host non ha l'algoritmo
Dove SHA-3 è disponibile: CNG di Windows e OpenSSL
La disponibilità appartiene alla macchina. Su Windows il backend predefinito è CNG, e CNG spedisce provider di algoritmi SHA-3 da Windows 11 24H2 in avanti; sulle build precedenti BCryptOpenAlgorithmProvider per "SHA3-256" fallisce con STATUS_NOT_FOUND. WinSha3DigestAvailable apre una volta sola i provider SHA3-256, SHA3-384 e SHA3-512 e mette in cache la risposta, visto che non può cambiare mentre il processo gira. Su tutte le altre piattaforme firma il backend OpenSSL, e cosa sa fare dipende dalla libcrypto che trova:
- OpenSSL 3.x esporta sempre
EVP_sha3_256,EVP_sha3_384edEVP_sha3_512, così la firma SHA-3 funziona su ogni piattaforma dove il backend si carica - Il loader accetta anche una libreria di serie 1.1 (
libcrypto-1_1.dll,libcrypto.so.1.1). SHA-3 è arrivato in OpenSSL 1.1.1 e manca nella 1.1.0, così sulle immagini Linux più vecchie decideSslSha3SigningAvailable SslSha3SigningAvailableesige anche credenziali di firma configurate. Chiesto prima diConfigureSslSigningCredentials, risponde False anche quandoSslLibraryVersionsegnala una libreria 3.x perfettamente in forma. Sembra un backend mancante. È solo un problema di ordine- Un eseguibile a 32 bit non può caricare
libcrypto-3-x64.dll. Quando questo backend è stato messo in piedi, una build Win32 puntata sulla DLL x64 riportavaSslAvailable = FalsementreSslMissingSymbolsrestituiva nient'altro che il percorso della libreria, un sintomo che sembra un binding di simboli rotto; la causa era la bitness, e una DLL compatibile con l'eseguibile l'ha sistemata
Un parco macchine Windows che include ancora macchine pre-24H2 può tenere CNG dove ha SHA-3 e instradare il resto attraverso OpenSSL con un signer provider. IPadesSignerProvider ha due metodi. ResolveSslSignerForDigest in FPdfCryptoSsl restituisce un signer il cui digest dichiarato, algoritmo di firma, lunghezza del digest e salt PSS seguono tutti l'algoritmo richiesto, e con un provider configurato ResolvePadesSignerForDigest controlla che il signer restituito dichiari il digest chiesto dalla chiamata, sollevando EPadesCrypto in caso negativo. Un provider costruito per SHA3-256 non può timbrare una richiesta SHA3-384. Il provider però vale per l'intero processo, così un server che firma in concorrenza dovrebbe configurarlo una volta sola all'avvio anziché per chiamata come fa questo abbozzo; ConfigureSslLibraryPath punta il loader sulla libcrypto quando la DLL non è nel percorso di ricerca
uses
System.SysUtils, System.Classes, System.IOUtils,
PDFium, FPdfCms, FPdfCrypto, FPdfCryptoWin, FPdfCryptoSsl, FPdfPades;
type
TOpenSslSha3Provider = class(TInterfacedObject, IPadesSignerProvider)
private
FDigest: TCmsDigestAlgorithm;
public
constructor Create(ADigest: TCmsDigestAlgorithm);
function BackendName: string;
function Resolve(const Options: TPadesSignerOptions): TPadesResolvedSigner;
end;
constructor TOpenSslSha3Provider.Create(ADigest: TCmsDigestAlgorithm);
begin
inherited Create;
FDigest := ADigest;
end;
function TOpenSslSha3Provider.BackendName: string;
begin
Result := 'OpenSSL';
end;
function TOpenSslSha3Provider.Resolve(
const Options: TPadesSignerOptions): TPadesResolvedSigner;
begin
Result := ResolveSslSignerForDigest(Options, FDigest);
end;
procedure SignSha3OnAnyWindows(Pdf: TPdf; Dest: TStream;
const Opts: TPadesSignOptions; const KeyPassword: AnsiString);
var
Provider: IPadesSignerProvider;
Chain: TPadesChainCertArray;
SslOpts: TPadesSignOptions;
begin
if WinSha3DigestAvailable then
begin
Pdf.SignPadesToStreamForDigest(Dest, Opts, cdaSha3_256); // percorso CNG
Exit;
end;
Chain := nil;
ConfigureSslSigningCredentials(
TFile.ReadAllBytes('C:\Signing\signer-key.pem'),
TFile.ReadAllBytes('C:\Signing\signer.der'), Chain, KeyPassword);
if not SslSha3SigningAvailable then
raise Exception.Create('No SHA-3 in loaded OpenSSL ' + SslLibraryVersion);
SslOpts := Opts;
SslOpts.CertificateThumbprint := ''; // OpenSSL firma con il certificato configurato
Provider := TOpenSslSha3Provider.Create(cdaSha3_256);
ConfigurePadesSignerProvider(Provider);
try
Pdf.SignPadesToStreamForDigest(Dest, SslOpts, cdaSha3_256);
finally
ConfigurePadesSignerProvider(nil);
end;
end;
Come scopre il digest un signer remoto o HSM?
La firma remota divide il lavoro in due. PreparePadesRemoteSignature scrive un PDF con uno slot /Contents vuoto più un TPadesRemoteSigningRequest, e il detentore della chiave restituisce più tardi un CMS detached, spesso da un altro processo o macchina, attraverso la sessione di prepare e complete usata per chiavi HSM e cloud. Sul lato prepare impostate TPadesRemoteSignOptions.DigestAlgorithm, diciamo a cdaSha3_384, e la richiesta registra la scelta in TPadesRemoteSigningRequest.DigestAlgorithm accanto a DocumentDigest, il valore che il messageDigest del CMS deve portare. Da lì in poi la richiesta è l'autorità. CompletePadesRemoteSignature ricalcola il digest /ByteRange con l'algoritmo conservato nella richiesta e non consulta più mai le opzioni; altrimenti un cambio di configurazione tra prepare e complete giudicherebbe una richiesta vecchia con un algoritmo nuovo. Un servizio di firma costruito sul componente dovrebbe prendere l'algoritmo dallo stesso posto:
uses
System.SysUtils, System.Classes,
FPdfCms, FPdfCrypto, FPdfCryptoSsl, FPdfPades,
FPdfPadesRemote; // collega il modulo Enterprise
function SignRemoteRequest(const RequestFile: string;
Padding: TPadesPaddingScheme): TBytes;
var
Stream: TFileStream;
Request: TPadesRemoteSigningRequest;
SignerOpts: TPadesSignerOptions;
Resolved: TPadesResolvedSigner;
begin
Stream := TFileStream.Create(RequestFile, fmOpenRead or fmShareDenyWrite);
try
Request := LoadPadesRemoteSigningRequest(Stream);
finally
Stream.Free;
end;
SignerOpts := TPadesSignerOptions.Default; // thumbprint vuoto per OpenSSL
SignerOpts.PaddingScheme := Padding;
// Il digest lo decide la richiesta, non la configurazione di questo servizio
Resolved := ResolveSslSignerForDigest(SignerOpts, Request.DigestAlgorithm);
Result := BuildSignedDataEx(Request.DocumentDigest, Resolved.Signer,
Resolved.SignerCertDer, Resolved.ChainCertDer, TCmsBuildOptions.Default);
end;
Dentro la richiesta, la lunghezza del digest è l'unica prova dell'algoritmo. Fa anche da impronta digitale. Prima di leggere qualunque stream, CompletePadesRemoteSignature confronta Length(DocumentDigest) con CmsDigestByteLength(DigestAlgorithm), così un digest troncato o un algoritmo rinominato fallisce in quel punto e lo stream di destinazione non viene mai toccato. La lunghezza da sola non distingue SHA3-256 da SHA-256, entrambi 32 byte, ed è per questo che il valore del digest viene confrontato anche con il messageDigest dentro il CMS restituito. Il PDF preparato, al contrario, è byte-identico per SHA-256 e SHA3-384: il segnaposto contiene solo numeri /ByteRange e nessun digest. I file identici si lasciano leggere facilmente come segno che l'impostazione venga ignorata. La firma remota viaggia nell'edizione Enterprise attraverso l'unit FPdfPadesRemote
La variante è raramente la decisione difficile; per una chiave RSA di solito è cdaSha3_256, e ciò che fallisce in produzione è un host il cui CNG o la cui libcrypto non sanno produrla, ed è per questo che il controllo di disponibilità sta all'avvio dell'applicazione in qualunque deployment del PDFium Component per Delphi