💡 Esempio completo funzionante disponibile su GitHub: vault-transit-pdf-signing-python

Perché la firma automatizzata fallisce una revisione della custodia delle chiavi

I team di conformità e operazioni finanziarie firmano migliaia di documenti: contratti di prestito, estratti conto, pacchetti di policy, lettere di audit. Quasi nessuno di questi è firmato da una persona che clicca un pulsante. Un servizio lo fa, secondo un programma, e il certificato allegato a quei documenti porta il nome dell’organizzazione. Questo rende la chiave di firma una delle cose più sensibili che l’azienda possiede, e di solito risiede in un file PFX accanto all’applicazione che lo utilizza.

La firma esterna è una funzionalità di GroupDocs.Signature per Python tramite .NET che passa il digest del documento al codice che fornisci invece di firmare con una chiave privata che l’applicazione detiene. Esiste perché la configurazione PFX non resiste a un esame approfondito. Un file è una copia, e le copie si diffondono: nell’immagine del contenitore, nella pipeline di distribuzione, nel backup, nel laptop dello sviluppatore che ha debugato un problema di produzione la scorsa primavera. Chiunque possieda una di quelle copie e la password può firmare a tuo nome finché qualcuno non se ne accorge e revoca il certificato.

Ciò di cui questi team hanno bisogno non è un formato di file diverso. Hanno bisogno che la chiave esista in un unico luogo, con una traccia di audit di ogni utilizzo, e un servizio di firma che possa richiedere una firma senza mai essere affidato alla chiave. È per questo che esiste un gestore di chiavi come HashiCorp Vault, e funziona solo se la libreria di documenti può essere configurata per delegare la crittografia.

Perché un file certificato protetto da password non è sufficiente

L’istinto è quello di bloccare il PFX: limitare i permessi del file, memorizzare la password in un gestore di segreti, crittografare il volume. Ogni passo aiuta e nessuno di essi cambia il fatto fondamentale. Al momento della firma, la chiave privata è caricata nel processo, quindi un dump di memoria, una fuga dal contenitore o una dichiarazione di log imprudente possono comunque esporla. Il controllo che vuoi è una chiave che non possa essere letta affatto, non una chiave più difficile da leggere.

Il motore Transit di Vault fornisce questo. Esegue la crittografia come servizio: invii un valore, lo firma o lo cripta con una chiave che detiene, e restituisce il risultato. Le chiavi create al suo interno possono essere contrassegnate come non esportabili, e non memorizza mai ciò che riceve. GroupDocs.Signature copre l’altra metà, perché DigitalSignOptions.custom_sign_hash ti permette di collegare il tuo firmatario al flusso di firma PDF.

La configurazione: GroupDocs.Signature per Python con Vault Transit

GroupDocs.Signature gestisce tutto fino alla crittografia. Apre il PDF, decide quale intervallo di byte copre la firma, calcola l’hash di quell’intervallo e chiama il tuo firmatario con il digest. Il tuo firmatario passa il digest a Vault e restituisce i byte prodotti da Vault. La libreria li incorpora insieme al certificato e scrive un PDF firmato normale.

Hai bisogno di Python 3.11 o 3.13, dei pacchetti groupdocs-signature-net e asn1crypto, e di Docker per un contenitore Vault in modalità sviluppo. Una licenza GroupDocs è opzionale; senza di essa, la modalità di valutazione accetta documenti fino a 2 pagine. Il repository di esempio funziona end‑to‑end con due comandi, e lo stesso codice punta a un Vault di produzione modificando due variabili d’ambiente.

Costruirlo passo dopo passo

Passo 1 – Leggere la chiave di firma da Vault

L’applicazione richiede a Vault la chiave che è autorizzata a usare, e riceve indietro un numero di versione e una chiave pubblica. La metà privata non fa mai parte della risposta.

key = vault.read_key(KEY_NAME)
if key is None:
    # Prima esecuzione: abilita Transit e crea una chiave RSA-2048.
    vault.enable_transit()
    vault.create_key(KEY_NAME, "rsa-2048")
    key = vault.read_key(KEY_NAME)
print(f"Signing key: '{KEY_NAME}', version {key.version}")

Leggere la versione una sola volta è lo scopo di questo passo. Un certificato contiene una sola chiave pubblica, quindi il certificato e ogni firma devono provenire dalla stessa versione.

Passo 2 – Costruire un certificato per una chiave che nessuno può esportare

Una firma PDF contiene un certificato X.509 così che un verificatore possa trovare la chiave pubblica. Con una chiave non esportabile, il certificato è assemblato attorno alla metà pubblica e firmato da Vault.

tbs_certificate = x509.TbsCertificate({...})   # costruito attorno alla chiave pubblica di Vault

# Hash locale, poi chiedi a Vault di firmare il digest del certificato.
digest = hashlib.sha256(tbs_certificate.dump()).digest()
signature = vault.sign_hash(key_name, key.version, digest, "sha2-256")

certificate = x509.Certificate({
    "tbs_certificate": tbs_certificate,
    "signature_algorithm": algorithm,
    "signature_value": signature,
})

Il certificato inizia deliberatamente un giorno prima di ora. Un lettore PDF confronta l’ora di firma con quella data di inizio, e su una macchina due ore a est di UTC un certificato creato pochi secondi prima della firma è stato segnalato da Adobe Acrobat come scaduto o non ancora valido.

Passo 3 – Firmare il PDF tramite il firmatario esterno

certificate_object = DigitalVerifyOptions(
    certificate_stream=io.BytesIO(certificate)).certificate
digital_signature = DigitalSignature(certificate=certificate_object)
digital_signature.sign_time = datetime.now().astimezone()

options = DigitalSignOptions()
options.signature = digital_signature
options.custom_sign_hash = VaultSigner(vault, KEY_NAME, key.version)
options.hash_algorithm = HashAlgorithm.SHA256
options.reason = "Document signed with HashiCorp Vault Transit"

with gs.Signature(str(input_pdf)) as signature:
    signature.sign(str(output_pdf), options)

Due righe sono specifiche per Python. Il binding non ha un tipo X.509, quindi l’oggetto certificato ritorna da DigitalVerifyOptions; passare un percorso a DigitalSignOptions invece genera l’errore “Specified certificate doesn’t contain private key”. E sign_time è consapevole del fuso orario di proposito, perché un valore con offset viene scritto nel PDF.

All’interno del firmatario, il digest viene inviato a Vault con prehashed = true e pkcs1v15. Quei due campi decidono se la firma è valida, e il primo fallisce silenziosamente quando manca.

Passo 4 – Verificare prima che qualcosa lasci il servizio

options = DigitalVerifyOptions(certificate_stream=io.BytesIO(certificate))
with gs.Signature(str(signed_pdf)) as signature:
    is_valid = signature.verify(options).is_valid

Firmare e verificare nella stessa esecuzione trasforma la modalità di fallimento silenzioso in una immediata. Per una pipeline di documenti questo è il filtro: un file che non verifica non raggiunge mai l’archivio, il controparte o il portale cliente.

Impatto sul business: cosa cambia per l’audit

Problema PFX sull’host dell’applicazione Firma tramite Vault Transit
Copie della chiave privata una per host, immagine, backup e laptop una, dentro Vault
Diritti richiesti dall’app di firma accesso completo al file della chiave lettura della chiave, chiamata transit/sign
Prova che un documento è stato firmato log dell’applicazione, se qualcuno li ha aggiunti una voce di audit Vault per firma
Risposta a una perdita sospetta revoca, ri‑emissione, ridistribuzione ovunque rotazione della chiave in Vault, ri‑emissione una sola volta
Chi può firmare a tuo nome chiunque abbia il file e la password chiunque possieda un token Vault valido

L’ultima riga è la più sincera. Questo design sposta il segreto anziché eliminarlo: un token Vault rubato può ancora richiedere firme. La differenza è che un token è a breve durata, revocabile in un unico punto, limitato a due operazioni, e ogni suo utilizzo è registrato. È molto più facile da difendere in una revisione rispetto a un file le cui copie nessuno può elencare.

Flusso reale: un contratto di prestito firmato da un servizio

Una piattaforma di prestito genera un PDF di accordo quando una domanda viene approvata, lo firma e lo deposita nella cassaforte documentale del cliente. Prima di questo cambiamento, il servizio di firma deteneva il certificato aziendale e il runbook di distribuzione prevedeva un passo per copiare il PFX su nuovi host. Dopo, il servizio detiene un token AppRole con due permessi, e la chiave privata del certificato non è mai esistita da nessuna parte tranne che in Vault.

Quando gli auditor chiedono quanti documenti sono stati firmati nell’ultimo trimestre e da chi, la risposta proviene dal dispositivo di audit di Vault anziché dal ricostruire i log dell’applicazione. Quando il team ruota la chiave, la ruota in un unico luogo e rilascia un nuovo certificato per la nuova versione.

Cosa dovrei sistemare per primo?

La gestione del digest. Un nome di algoritmo errato fallisce in modo evidente, con Vault che risponde 500 crypto/rsa: input must be hashed message, e costa un minuto. Un flag prehashed mancante non produce alcun errore: Vault hash di nuovo il digest, la firma copre il valore sbagliato, e il PDF sembra completato. Verifica ogni file che firmi e quel fallimento smetterà di essere invisibile.

Altri contesti in cui questo design è adatto

Il gancio non ha idea di cosa ci sia dietro, quindi gli stessi quattro passaggi funzionano contro un modulo di sicurezza hardware, un KMS cloud o una smart card. Ho portato questa build dal suo fratello .NET aspettandomi che il firmatario richiedesse una riscrittura, e l’unica parte che è cambiata realmente è stata la consegna del certificato; il corpo della richiesta a Vault e il pinning della versione erano identici. Team in sanità, governo e assicurazioni lo adottano per lo stesso motivo per cui lo fa la finanza: la chiave deve trovarsi in un luogo che possa dimostrare a cosa è stata usata.

Per iniziare

Risorse