Articolo tecnico

Firme digitali PDF e PAdES in Delphi con HotPDF

Una firma PDF è per gran parte contabilità di byte, ed è nella contabilità dei byte che le cose vanno storte. La crittografia gira su codice sottoposto a revisione da vent'anni, e quella parte non fallisce quasi mai. Ciò che fallisce in produzione è più umile: un segnaposto riservato troppo piccolo per la firma reale, un hash calcolato sul tratto sbagliato del file, oppure un salvataggio dopo la firma che ha silenziosamente riscritto byte che la firma aveva già congelato. Disponete i byte correttamente e il segno di spunta verde arriva da sé

HotPDF copre la firma per Delphi e C++Builder su tre livelli, e scegliete fra essi rispondendo a una sola domanda: dove risiede la chiave privata? Un file PFX su disco richiede una sola chiamata di funzione. Una chiave chiusa in un HSM o in un servizio di firma remoto richiede la sequenza riserva-hash-inserimento, perché nessuna libreria può infilare la mano in un token ed estrarne la chiave. Una firma che deve soddisfare la normativa europea richiede in aggiunta le strutture della baseline PAdES. Le sezioni seguenti seguono questa progressione

Diagramma decisionale che sceglie fra la firma PFX in una sola chiamata di HotPDF, il percorso riserva-hash-inserimento quando la chiave risiede in un HSM o in un servizio remoto, e le strutture della baseline PAdES per la firma regolamentata europea
Scegliete il livello di firma chiedendovi dove risiede la chiave privata; un file PFX leggibile riduce la firma a una sola chiamata, mentre le chiavi custodite in un token impongono la deviazione a livello di byte e la normativa europea aggiunge lo strato PAdES

Come /ByteRange inchioda i byte firmati

Una firma deve vivere dentro il file che firma, e non può firmare sé stessa. Il PDF aggira il paradosso lasciando un buco. Prima della firma, lo scrittore riserva una voce /Contents di dimensione fissa piena di zeri e registra un array /ByteRange per i due tratti ai lati di essa: tutto ciò che precede il buco, tutto ciò che lo segue. Il firmatario calcola l'hash di quei due tratti e scrive il blob CMS risultante dentro il buco in esadecimale. La trappola sta nella parola fissa. Vi impegnate sulla dimensione di quel buco prima di sapere quanto sarà grande la firma finita, quindi la riserva deve essere una sovrastima sicura. Ottomila byte contengono comodamente una firma CMS separata con una catena di certificati breve

HotPDF separa i due casi in due chiamate, e confonderle è un errore comune agli inizi. AddSignatureField posa un campo vuoto e visibile perché una persona lo firmi in seguito in un visualizzatore. AddSignedSignatureField crea il campo e riserva il buco /Contents, ed è quella che volete ogni volta che a completare la firma sarà del codice anziché una persona. Consegnate a un firmatario esterno un campo vuoto e non avrà nulla da riempire

La via a chiamata singola: firmare da un PFX

Quando il certificato e la sua chiave privata risiedono in un file PFX/PKCS#12 leggibile dal vostro processo, l'intera catena si riduce a una funzione di classe:

if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
    'company-cert.pfx', 'pfx-password') then
  Writeln('Signed: invoice-signed.pdf')
else
  raise Exception.Create('PFX signing failed');

Quando questa fallisce, il PDF è raramente il problema. Lo è il PFX. HotPDF legge contenitori protetti con PBES2, cioè derivazione di chiave PBKDF2 su AES-256-CBC. Un PFX esportato da una vecchia procedura guidata dei certificati di Windows, o da OpenSSL prima della 3.0, di solito è invece avvolto in RC2 o 3DES legacy, e semplicemente non viene interpretato. La soluzione è riesportare una volta il contenitore con protezione moderna; l'OpenSSL di oggi lo fa in modo predefinito, e non è una modifica al codice. Perciò quando la firma muore all'istante su un certificato che «funziona ovunque», guardate come è stato creato il PFX prima di sospettare del vostro codice

La via riserva-hash-inserimento per HSM e token

La via a chiamata singola presuppone che il vostro processo possa leggere la chiave come file. Sempre più spesso non può. La chiave risiede in un HSM, su un token USB o dietro l'API di un servizio di firma, e non c'è modo per una libreria di raggiungerla direttamente. HotPDF gestisce quel caso spezzando la firma in passi a livello di byte: scrivere un documento segnaposto, chiedere alla libreria gli intervalli da sottoporre ad hash, passare l'ingresso dell'hash a chi custodisce la chiave, poi innestare il CMS restituito dentro il buco

HotPDF: catena in quattro passi riserva-hash-inserimento su placeholder.pdf che mostra il buco /Contents riservato fra i due tratti del ByteRange e un HSM che scambia il digest con il CMS in esadecimale
HotPDF riserva il buco e riporta entrambi i tratti del ByteRange, chi custodisce la chiave li firma all'esterno, e il CMS restituito viene innestato byte per byte senza toccare un solo byte congelato
var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Scrive il documento con un buco /Contents riservato
  Doc := THotPDF.Create(nil);
  try
    Doc.FileName := 'placeholder.pdf';
    Doc.BeginDoc;
    Doc.CurrentPage.AddSignedSignatureField('Sig1',
      Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
      'Contract approval', 'Boston, MA', 'legal@example.com');
    Doc.EndDoc;
  finally
    Doc.Free;
  end;

  // 2. Carica i byte salvati; gli offset restituiti partono da 0
  Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
  try
    SetLength(PdfBytes, Fs.Size);
    Fs.ReadBuffer(PdfBytes[1], Fs.Size);
  finally
    Fs.Free;
  end;
  THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
    CStart, CLen);

  // 3. Calcola l hash dei due tratti e firma esternamente (HSM, token, servizio)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // vostra integrazione: CMS in esadecimale

  // 4. Innesta la firma nel buco riservato
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Due dettagli di questa sequenza causano la maggior parte dei fallimenti intermittenti. Il primo è che PreparePDFForSigning lavora sui byte di un file finito. Il segnaposto va scritto e salvato per intero prima che gli offset significhino qualcosa; calcolateli su uno stream ancora in assemblaggio e non combaceranno con i byte di cui poi calcolate l'hash. Il secondo è di nuovo la dimensione della riserva. Gli 8192 byte che avete chiesto devono contenere il CMS finale, e una firma che porta con sé certificati intermedi, o che un servizio decora con attributi firmati, può sforare. InsertSignatureHex non allargherà il buco per fare spazio. Il segnale è una catena che firma senza problemi con un certificato e fallisce con il successivo; la cura è rigenerare il segnaposto con una riserva misurata su una firma reale prodotta dal firmatario effettivo, non tirata a indovinare

Le baseline PAdES e le marche temporali che tengono viva una firma

Se firmate sotto le regole europee, la norma in gioco è la ETSI EN 319 142-1, che impila quattro livelli di baseline PAdES. B-B è la firma semplice. B-T aggiunge una marca temporale attendibile che prova quando è stata apposta. B-LT incorpora il materiale di validazione, i certificati e i dati di revoca, dentro il documento così da poterlo verificare anche anni dopo. B-LTA sovrappone marche temporali di documento periodiche, così le prove sopravvivono agli algoritmi su cui erano state costruite. HotPDF emette le strutture lato documento per ciascun livello:

HotPDF: livelli di baseline PAdES impilati da B-B a B-T e B-LT fino a B-LTA, con una linea temporale di rinnovo che mostra marche temporali di documento periodiche che mantengono verificabile una firma decenni dopo
Ogni livello impila una nuova protezione sul precedente; B-LTA continua a riapplicare marche temporali di documento così che le prove sopravvivano agli algoritmi con cui erano state costruite
// Campo firma della baseline PAdES (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Marca temporale di documento: riserva più ampia per token TSA e catena
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

La riserva di 16384 byte sulla marca temporale è voluta. Una autorità di marcatura temporale restituisce un token che si trascina dietro la propria catena di certificati, quindi ha normalmente bisogno di più spazio degli 8 KB che bastano a una firma semplice. Quelle marche temporali di documento sono anche il meccanismo dietro B-LTA: rimarcare temporalmente una firma archiviata ogni pochi anni, con algoritmi ancora attuali, è ciò che mantiene verificabile nel 2040 un documento firmato nel 2026

Una parola sulle stringhe di motivo, luogo e contatto che entrambe le chiamate di campo accettano: sono metadati di comodo e nulla più. HotPDF le conserva come semplici voci di dizionario e le disegna nell'aspetto visibile della firma, ma nessun validatore le confronta con alcunché. Compilatele in modo coerente con i dati del vostro flusso di lavoro, dato che i revisori le leggono, e poi non scambiatele mai per prove. L'affermazione crittografica vera vive interamente nel CMS e nella sua catena di certificati, e un verificatore ignora del tutto il testo visibile

Dopo la firma il file può soltanto crescere

Nel momento in cui una firma esiste, i byte dentro i suoi intervalli sono congelati. L'unico modo legittimo di cambiare il file in seguito è un aggiornamento incrementale ISO 32000-1 §7.5.6, che accoda oggetti nuovi e modificati dopo i byte originali e concatena una nuova sezione di riferimenti incrociati che punta a essi. Fatto così, la firma resta valida per la sua revisione e un visualizzatore riporta lo stato onesto: la revisione firmata è integra, il documento è stato esteso in seguito. Riserializzate invece l'intero file e riscrivete i tratti firmati, il che distrugge la firma anche quando nulla di visibile è cambiato. Lo stesso meccanismo di revisione è anche il modo in cui un documento porta più firme: ogni nuova firma finisce nel proprio aggiornamento incrementale, e i suoi intervalli coprono tutto ciò che la precede, comprese le firme precedenti. La meccanica a sola aggiunta, e quando sia sicuro compattarla, è trattata nell'articolo su object stream e aggiornamenti incrementali

Due confini vanno tenuti a mente in fase di progetto. La modalità di uscita PDF/A di HotPDF rifiuta i campi firma di netto, quindi conformità archivistica e firma incorporata devono viaggiare come file separati. E la firma non dice nulla sulla segretezza: prova chi ha prodotto un documento e che non è cambiato da allora, ma chiunque può comunque leggerlo. Nascondere il contenuto è un compito a parte, gestito dalla cifratura AES-256 e dalla politica dei permessi

Qualunque cosa costruiate, collaudatela con qualcosa di diverso dal codice che ha scritto il file. Aprite l'uscita nel pannello firme di Acrobat e confermate tre cose: la firma è valida, l'identità si concatena fino alla radice che vi aspettavate, e il pannello non segnala modifiche dopo la firma. Poi cambiate un solo byte dentro l'intervallo firmato di una copia usa e getta e confermate che ora il pannello dichiari il documento alterato. Una catena di firma che non avete mai visto respingere un file manomesso è una catena la cui verifica non è stata davvero collaudata

Tutti e tre i livelli di firma sono distribuiti con lo HotPDF Delphi Component per Delphi e C++Builder; la pagina di prodotto rimanda al riferimento completo dell'API di firma