HotPDF cifra un PDF per specifici portatori di certificato attraverso il security handler a chiave pubblica di ISO 32000: EnablePubKeyEncryption prende un seed casuale di 20 byte, e ogni destinatario riceve la sua busta CMS, costruita da AddPubKeyRecipientCertificate per chiavi RSA (trasporto di chiave RSA-OAEP) o da AddPubKeyAgreementRecipientWithSecret per chiavi a curva ellittica (ECDH su P-256, P-384, P-521, X25519 o X448). Nessuno condivide una password; chi possiede la chiave privata corrispondente apre il file
Il caso d'uso è sempre qualche versione della stessa storia. Un pacchetto di audit trimestrale va a tre revisori esterni, la direzione legale vuole che ognuno lo legga, solo uno di loro può stamparlo, e nessuno vuole una password che gironzola in un thread di email accanto all'allegato. La cifratura a password non sa esprimere questo. Quella a certificato sì, perché ogni destinatario sblocca il documento con una chiave che già possiede, e ogni destinatario può portare con sé un set di permessi diverso dentro la propria busta
In che cosa la cifratura PDF a certificato differisce da una password?
Un PDF cifrato a chiave pubblica deriva la sua chiave di file da un seed casuale più i byte esatti di ogni busta dei destinatari, non da nulla che una persona digiti. Il handler è descritto in ISO 32000-1 §7.6.4 (§7.6.5 in ISO 32000-2), e le buste sono strutture CMS EnvelopedData come definite in RFC 5652. HotPDF scrive /Filter /Adobe.PubSec con /SubFilter /adbe.pkcs7.s5; per AES-256 ciò significa /V 5 e una voce /DefaultCryptFilter sotto /CF con /CFM /AESV3, e l'array /Recipients vive dentro quel crypt filter. Ogni busta cifra 24 byte: il seed di 20 byte seguito dalla permission word a 32 bit di quel destinatario. Il valore /P nel dizionario di cifratura è solo un segnaposto, perché i permessi veri viaggiano dentro ogni busta. Al caricamento un reader apre una busta, recupera il seed, e fa l'hash del seed insieme a ogni busta in ordine di /Recipients (SHA-256 per AES-256, SHA-1 per i cifrari più vecchi) per ricostruire la chiave di file. Se stai ancora decidendo tra questo modello e le password ordinarie, la guida sulla cifratura a password AES-256 e sui flag di permesso copre l'altro lato di quel compromesso
Scrivere destinatari RSA con EnablePubKeyEncryption
Per i certificati RSA, chiama EnablePubKeyEncryption con aes256, poi chiama AddPubKeyRecipientCertificate una volta per certificato codificato DER prima di BeginDoc. L'helper costruisce una busta RSAES-OAEP in-process con valori THPDFRSAOAEPHash per il digest OAEP e il digest MGF1 (rohSHA256, rohSHA384 o rohSHA512), e cifra il contenuto della busta con AES-256-CBC
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFRSA;
procedure WriteAuditPack(const OutFile: string);
var
Pdf: THotPDF;
Seed: AnsiString;
begin
SetLength(Seed, 20); // esattamente 20 byte, anche per AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // il tipo di chiave predefinito è aes128
// Il revisore A può stampare; il revisore B può solo leggere ed estrarre
Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-a.cer'),
[prPrint, prPrint12bit, prExtractContent], rohSHA256, rohSHA256);
Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-b.cer'),
[prExtractContent]);
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 720, 0, 'Q3 audit pack');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Tre dettagli di quel listato reggono il resto. Primo, la lunghezza del seed è fissata a 20 byte per ogni tipo di chiave, AES-256 inclusa; EnablePubKeyEncryption alza un'eccezione su qualunque altra lunghezza. Secondo, EnablePubKeyEncryption ha default aes128, ed entrambi gli helper a certificato si rifiutano di girare se il tipo di chiave non è aes256, quindi dimenticare il secondo argomento ti regala l'eccezione "certificate envelopes require aes256". I cifrari legacy (k40, k128, aes128) funzionano ancora, ma solo attraverso AddPubKeyRecipient con una busta costruita altrove. Terzo, la cifratura a chiave pubblica AES-256 è una feature del PDF 2.0, quindi HotPDF alza automaticamente la versione del documento a 2.0. Con StrictVersionLock impostato su una versione più bassa, EnablePubKeyEncryption restituisce senza attivare nulla, e il fallimento appare solo alla riga successiva come "call EnablePubKeyEncryption first". Cambiare cifratura durante un aggiornamento incrementale alza EInvalidOpException all'istante
Aggiungere destinatari ECDH: P-256, P-384, P-521, X25519 e X448
Per i certificati a curva ellittica, AddPubKeyAgreementRecipientWithSecret scrive un destinatario CMS key-agreement (KeyAgreeRecipientInfo, la struttura KARI di RFC 5753, con il profilo X25519 e X448 di RFC 8418) e calcola il segreto condiviso ECDH in-process. Scegli la curva con un valore THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 o pkasX448. Lo schema deve combaciare con la chiave nel certificato, o la chiamata alza "Certificate key does not match the requested agreement scheme". Sotto il cofano, ogni busta riceve un UKM casuale fresco di 32 byte, una key-encryption key derivata con il KDF stdDH (SHA-256 per P-256 e X25519, SHA-384 per P-384, SHA-512 per P-521 e X448), e un AES-256 key wrap come definito in RFC 3394. Il segreto condiviso in sé viene da codice di curve in Pascal puro, senza alcun provider crittografico di piattaforma coinvolto; l'articolo sull'aritmetica NIST in Pascal puro spiega come quel layer è stato costruito e verificato. Per le curve di Montgomery l'intera coppia di chiavi effimera può essere generata localmente:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Scalare effimero fresco per busta; il clamping avviene dentro la ladder
SetLength(Scalar, 32);
AESGenerateRandomBytes(@Scalar[0], Length(Scalar));
try
OriginatorPublic := HPDFX25519PublicFromScalar(Scalar);
Pdf.AddPubKeyAgreementRecipientWithSecret(
TFile.ReadAllBytes('legal-x25519.cer'),
[prPrint, prExtractContent], pkasX25519,
OriginatorPublic, Scalar,
[]); // OwnPublicPoint: significativo solo per le curve NIST
finally
HPDFSecureClearBytes(Scalar);
end;
end;
Le curve NIST chiedono di più a chi chiama. HotPDF spedisce helper a chiave pubblica solo per X25519 e X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), quindi per P-256, P-384 e P-521 generi la coppia di chiavi effimera con i tuoi strumenti e passi uno scalare big-endian di esattamente la dimensione del campo (32, 48 o 66 byte) più il punto non compresso corrispondente 0x04||X||Y come OriginatorPublicKey. HotPDF valida il punto del destinatario contro l'equazione della curva, ma non può controllare che la tua chiave pubblica di origine appartenga davvero al tuo scalare. Metà disallineate producono comunque una busta perfettamente ben formata che nessun destinatario può aprire, ecco perché un load di andata e ritorno appartiene alla tua suite di test, non solo un controllo sulla dimensione del file
Perché l'ordine di /Recipients conta?
L'ordine di /Recipients conta perché la chiave di file è un digest sul seed e su ogni busta in ordine di array, quindi writer e reader devono fare l'hash degli stessi byte nella stessa sequenza. HotPDF mantiene le buste nell'ordine in cui le aggiungi e le scrive invariate, il che significa che puoi aggiungere i destinatari nell'ordine che preferisci, ma nulla a valle può riordinare, ricodificare o «ripulire» quell'array. La maggior parte dei bug veri in quest'area era una variazione sul tema, con due lati che facevano l'hash di byte leggermente diversi:
- Memorizzare array dinamici in una
TListviaAddconserva solo un puntatore grezzo mentre il reference count resta con la variabile locale. Il prossimoSetLengthlibera il buffer e può riutilizzarlo, così ogni slot finiva per aliasare l'ultima busta e i file multi-destinatario derivavano la chiave sbagliata. La correzione è memorizzare una copia di proprietà conList.Add(Pointer(System.Copy(Bytes))) - L'apertura delle buste parsifica il DER sul posto, e la passata di recupero chiave originariamente faceva l'hash di quegli stessi array vivi. Il reader ora scatta istantanee pristine di ogni busta prima che qualunque apertura la tocchi, e il digest gira sulle istantanee
- DER binario passato attraverso una
TStringListUnicode fa ricodificare i byte a$80o sopra dal code page, quindi HotPDF memorizza le buste internamente come testo esadecimale - Le stringhe cifrate e binarie vanno scritte come hex string. Una stringa letterale è soggetta alla normalizzazione di fine riga, dove CR, LF e CRLF diventano tutti un singolo LF (ISO 32000-1 §7.3.4.2), e quello riscrive il ciphertext in silenzio. HotPDF emette ogni voce
/Recipientscome hex string e la esenta dalla cifratura delle stringhe, dato che ogni reader ha bisogno delle buste prima di possedere qualunque chiave - Il primo byte di una
BIT STRINGDER conta i bit non usati e deve essere zero per chiavi allineate al byte. Lasciarlo non inizializzato dopoSetLengthscriveva ciò che era sullo stack, e un unwrapper severo rifiutava la chiave di origine, così un file poteva ogni tanto fallire l'apertura proprio con la chiave per cui era stato scritto - Quando la stessa chiave continua a non decifrare, confronta layer per layer: la chiave di file, poi il prefisso del ciphertext (l'IV), poi la chiave di oggetto, poi il plaintext. Il bug sta subito dopo il primo layer che discorda
Come si apre un PDF cifrato a certificato con una chiave privata?
Per aprire un PDF cifrato a certificato, registra il materiale della chiave privata prima di chiamare LoadFromFile, perché HotPDF recupera la chiave di file durante la passata strutturale. Assegna una chiave RSA o EC parsificata con HPDFParsePFX a PubSecKeyMaterial, aggiungi altre chiavi RSA con AddPubSecKeyMaterial, e registra scalari ECDH grezzi con AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), usando le costanti HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 o HPDFOIDECP521. Le curve NIST richiedono il punto pubblico non compresso del destinatario stesso; le curve di Montgomery lo ignorano
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;
procedure OpenAuditPack(const LegalScalar: TBytes);
var
Reader: THotPDF;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
Reader.PubSecKeyMaterial :=
HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
// Opzionale: scegli la busta direttamente invece di provarle tutte
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = prova ogni busta in ordine
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Senza callback, HotPDF prova ogni busta contro ogni chiave registrata: prima la chiave primaria, poi ogni chiave RSA addizionale, poi il materiale EC. PubSecRecipientQuery riceve il numero di buste e restituisce un indice 0-based o -1, e un indice fuori dall'array alza un'eccezione anziché essere troncato. Nota che AddPubSecKeyMaterial accetta solo materiale RSA (pretende un modulo e un esponente privato), quindi le chiavi EC stanno in PubSecKeyMaterial o in AddPubSecAgreementKeyMaterial. Quando nessuna chiave apre alcuna busta, il passo di recupero restituisce senza chiave di file anziché alzare un'eccezione, quindi verifica che il contenuto che ti aspetti sia davvero stato decifrato anziché fidarti del fatto che la chiamata di load sia tornata
Che cosa HotPDF non garantisce
HotPDF garantisce che il suo writer e il suo reader concordano byte per byte, e costruisce buste che seguono le strutture CMS citate qui sopra. Non garantisce che ogni viewer PDF apra ogni combinazione. Il supporto al trasporto di chiave RSA-OAEP e ai destinatari X25519 o X448 varia tra reader e versioni, e non abbiamo pubblicato risultati di compatibilità per quelle combinazioni. Se un documento deve aprirsi in uno specifico viewer, cifra un file di test per un certificato di test dello stesso tipo di chiave e aprilo lì prima di impegnarti su uno schema. I permessi portati nella busta restano policy che il software conforme onora, esattamente come sotto cifratura a password. La qualità del seed è responsabilità tua: AESGenerateRandomBytes esiste proprio per quel lavoro, e HotPDF cancella la sua copia del seed una volta derivata la chiave di file. Se ti serve anche che una stringa, uno stream o un allegato usino un crypt filter diverso, la guida alle policy dei crypt filter per StmF, StrF e EFF mostra quali nomi di filtro il handler a chiave pubblica accetta
La cifratura a certificato, le buste dei destinatari RSA-OAEP ed ECDH, e il caricamento delle chiavi private sono tutti nella PDF component HotPDF Delphi, accanto alla cifratura a password, alle firme digitali e al resto del toolset ISO 32000 per Delphi e C++Builder