Articolo tecnico

Firma PAdES remota in PDFium VCL: chiavi HSM e cloud

PDFiumPas divide la firma PAdES in due chiamate così la chiave privata non deve mai trovarsi nel tuo processo. PreparePadesRemoteSignature scrive un incremental update con un placeholder /Contents vuoto a larghezza fissa e restituisce un record di richiesta che porta il digest SHA-256 del documento, il ByteRange esatto e un fingerprint del file preparato. CompletePadesRemoteSignature prende il CMS distaccato che il tuo servizio di firma restituisce e lo colloca in quello slot riservato

Tra queste due chiamate possono passare minuti o ore, il processo può riavviarsi, e il lavoro può spostarsi su un'altra macchina. Quel divario è l'intera ragione per cui l'API è progettata in questo modo

Perché una chiave remota non può usare la chiamata di firma ordinaria?

Perché SignPadesBytes presuppone che l'operazione di firma avvenga dentro la chiamata. Costruisce l'incremental update, calcola il digest sul ByteRange, lo firma, e scrive il risultato, tutto prima di ritornare. Questo è esattamente corretto quando la chiave vive nel certificate store di Windows o in un file PKCS#12 che hai caricato

È impossibile quando la chiave vive in un HSM di rete, in un dispositivo qualificato di creazione della firma gestito da un trust service provider, o in un'API di firma cloud che richiede all'utente di confermare su un telefono. In questi casi la sequenza non è una chiamata di funzione, è una conversazione: invii un digest, qualcos'altro autentica un essere umano, e un CMS torna più tardi. Un'API sincrona non può esprimere «più tardi» senza bloccare un thread su un'operazione che potrebbe richiedere un secondo fattore

Il protocollo a due fasi

La fase uno prepara il documento. PDFiumPas aggiunge il signature field e il value dictionary, riserva ContentsSize byte di spazio codificato in hex dentro /Contents, calcola il ByteRange attorno a quella riserva, e produce un TPadesRemoteSigningRequest contenente FormatVersion, PreparedFingerprint, DocumentDigest, il ByteRange a quattro elementi, ContentsHexOffset e ContentsSize

L'unico valore di cui il tuo servizio di firma ha bisogno è DocumentDigest: lo SHA-256 che il SignedData CAdES restituito deve portare come proprio message digest. Tutto il resto nel record esiste perché la fase due possa dimostrare che il file che sta completando è il file da cui è stato calcolato quel digest

uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // hex bytes reserved for the CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // Persisti la sessione così un'esecuzione successiva - o un'altra macchina - possa completarla
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

Cosa rifiuta Complete, e perché esiste ogni controllo?

Il completamento è il punto in cui un design di firma remota di solito va storto, quindi la validazione è deliberatamente inflessibile. CompletePadesRemoteSignature rifiuta un PDF preparato il cui fingerprint non corrisponde più alla richiesta, un ByteRange che non corrisponde alle coordinate registrate del placeholder, delimitatori /Contents modificati, un placeholder che non è più vuoto, un CMS più grande della riserva, un CMS che non è esattamente un valore DER, una forma SignedData non supportata, un attributo signing-certificate-v2 mancante, e un CMS il cui message digest non è uguale al digest del documento preparato

Ognuno di questi corrisponde a un fallimento reale. I controlli su fingerprint e ByteRange catturano il caso in cui qualcuno ha rigenerato il file preparato tra le due fasi, il che produrrebbe una firma che valida contro byte che nessuno possiede. Il controllo del placeholder vuoto cattura il doppio completamento, dove un secondo CMS viene scritto sopra una firma già esistente. Il controllo del message digest cattura il caso più pericoloso di tutti: un CMS correttamente formato ma firmato su un documento diverso, che è ciò che si ottiene quando una coda confonde due sessioni di firma concorrenti. Senza di esso produrresti un file che sembra firmato e fallisce la validazione ovunque, o peggio, che porta l'approvazione di qualcun altro

Il requisito signing-certificate-v2 è una questione di conformità PAdES più che di integrità. ETSI EN 319 142 richiede che il certificato di firma sia legato agli attributi firmati, e un CMS privo di quell'attributo non è una firma PAdES anche se verifica crittograficamente. Rifiutarlo al completamento significa scoprirlo qui, non in un report di un validatore da parte di un cliente, un argomento approfondito in perché i validatori rifiutano le firme PAdES

var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // returned by the HSM or TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // Ogni rifiuto porta una ragione specifica; registrala testualmente
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

Attraversare i confini di processo e di macchina

SavePadesRemoteSigningRequest e LoadPadesRemoteSigningRequest serializzano la sessione tramite un formato binario versionato e stabile, ed è ciò che rende il design pratico e non solo corretto. Un'applicazione web può preparare un documento in una richiesta, memorizzare il PDF preparato e il blob della sessione, restituire un digest al browser per una firma con smart card, e completare il file in un gestore di richiesta completamente diverso

Il campo FormatVersion è ciò che mantiene tutto questo sicuro attraverso gli upgrade. Una sessione scritta da una build più vecchia e caricata da una più recente viene riconosciuta o rifiutata esplicitamente, invece di essere interpretata erroneamente come un record di forma diversa. Se la tua coda può mantenere sessioni per giorni, tratta la format version come un fatto operativo che vale la pena registrare, non un dettaglio implementativo

Dimensionare il placeholder

ContentsSize è l'unico parametro a cui devi pensare, perché viene fissato prima che il CMS esista. Conta la riserva codificata in hex, quindi un CMS DER di 6 KB richiede almeno 12 KB di spazio, e l'implementazione limita la riserva a 64 MiB

Riserva troppo poco e il completamento fallisce con un errore di CMS sovradimensionato dopo che il tuo servizio di firma ha già svolto il proprio lavoro, il che su un servizio di firma qualificata a consumo significa un'operazione sprecata. Riserva troppo e ogni documento firmato porta per sempre quel padding. L'approccio sensato è misurare: firma un documento con la tua vera catena di certificati, guarda la lunghezza DER, raddoppiala per l'hex, poi aggiungi un margine generoso per il timestamp token se intendi passare a una firma di livello T. Le catene con più intermediari e una risposta OCSP lunga crescono più rapidamente di quanto ci si aspetti

Cosa viene dopo la firma

Una firma remota completata è PAdES B-B. La validazione a lungo termine richiede un timestamp e il materiale di validazione, che è un incremental update separato che aggiunge un DSS e i suoi dictionary VRI per singola firma, descritto in firme a lungo termine con timestamp RFC 3161 e DSS. Quel passo è locale: aggiunge certificati, risposte OCSP e CRL, nessuno dei quali richiede la chiave privata

Prima di rilasciare, verifica ciò che hai prodotto con lo stesso percorso di codice che userebbe una relying party, trattato in ispezionare le firme digitali e i livelli PAdES. Firma e verifica sono codice diverso, e una pipeline di firma remota è esattamente il punto in cui i due possono divergere senza che nessuno se ne accorga finché non lo dice un validatore esterno

PDFiumPas è un componente Delphi e Lazarus attorno al motore PDFium con uno stack PAdES nativo in Pascal, così firma, timestamping e validazione funzionano senza strumenti a riga di comando esterni. La documentazione API completa e una build di prova sono sulla pagina del componente Delphi PDFium