Un PDF cifrato a certificato porta un unico valore /P nel suo dizionario di cifratura, non importa per chi sia stato scritto il file; nei file costruiti più avanti in questo articolo vale -64. Tre destinatari con tre set di permessi diversi condividono comunque quel numero. Il codice che risponde "questo utente può stampare?" leggendo /P sta leggendo un segnaposto, perché sotto il security handler a chiave pubblica la vera permission word viaggia dentro la busta cifrata di ogni destinatario e compare solo quando la chiave privata di quel destinatario l'ha aperta
Quel design è il motivo per cui la cifratura a certificato vale la fatica. Un pacchetto di audit trimestrale va a tre revisori esterni; il legale vuole che lo leggano tutti, solo uno può stamparlo, e nessuno vuole una password che se ne sta nel thread email accanto all'allegato. Una password non può esprimerlo. I certificati sì: ogni destinatario sblocca il file con una chiave che già possiede, e ogni busta porta la propria permission word
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 destinatario, e nulla di ciò che una persona digita entra nel calcolo. L'handler è specificato in ISO 32000-1 §7.6.4 (§7.6.5 in ISO 32000-2), e le buste sono strutture CMS EnvelopedData da RFC 5652. HotPDF scrive /Filter /Adobe.PubSec con /SubFilter /adbe.pkcs7.s5. Per AES-256 questo significa /V 5, una voce /DefaultCryptFilter sotto /CF con /CFM /AESV3, e l'array /Recipients collocato dentro quel crypt filter invece che nel dizionario di primo livello
Ogni busta cifra 24 byte: il seed da 20 byte seguito dalla permission word a 32 bit di quel destinatario. Al caricamento un reader apre una busta, recupera il seed, e calcola 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. Il revisore B non viene mai a sapere cosa può fare il revisore A, eppure il reader di B calcola comunque l'hash dei byte della busta di A, quindi l'intero array deve sopravvivere byte per byte. Se state ancora scegliendo tra questo modello e le password ordinarie, la guida alla cifratura con password AES-256 e ai flag di permesso copre l'altro lato di quel compromesso
Scrivere destinatari RSA con EnablePubKeyEncryption
Per i certificati RSA la sequenza è breve: EnablePubKeyEncryption con aes256, poi una chiamata AddPubKeyRecipientCertificate per ogni certificato codificato DER, tutto prima di BeginDoc. L'helper costruisce una busta RSAES-OAEP dentro il processo. I suoi ultimi due parametri scelgono il digest OAEP e il digest MGF1 da THPDFRSAOAEPHash (rohSHA256, rohSHA384 o rohSHA512), entrambi con default rohSHA256, e il contenuto della busta è cifrato 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 in quel listato pesano. Il seed è di 20 byte per ogni tipo di chiave, AES-256 compreso, e EnablePubKeyEncryption solleva per qualsiasi altra lunghezza. Il tipo di chiave vale aes128 per default mentre entrambi gli helper a certificato pretendono aes256, quindi omettere il secondo argomento fa sollevare alla prima chiamata AddPubKeyRecipientCertificate "certificate envelopes require aes256". I cifrari legacy (k40, k128, aes128) restano disponibili tramite AddPubKeyRecipient, che prende una busta DER completa costruita da altri strumenti
Il terzo dettaglio è la versione. AES-256 sotto l'handler a chiave pubblica è una funzionalità PDF 2.0, e HotPDF alza da solo la versione del documento a 2.0. Con StrictVersionLock impostato su una versione inferiore quel salto non è permesso: EnablePubKeyEncryption ritorna senza accendere nulla, e l'errore affiora una riga dopo quando l'helper del destinatario riporta "call EnablePubKeyEncryption first". Quel messaggio manda a caccia di un errore nell'ordine delle chiamate. Controllate invece il version lock. Cambiare la cifratura durante un aggiornamento incrementale viene rifiutato in blocco con EInvalidOpException
Aggiungere destinatari ECDH su P-256, P-384, P-521, X25519 e X448
I certificati a curva ellittica passano per AddPubKeyAgreementRecipientWithSecret, che scrive un destinatario CMS a key agreement (KeyAgreeRecipientInfo da RFC 5753, con il profilo X25519 e X448 da RFC 8418) e calcola il segreto condiviso ECDH dentro il processo. Un valore THPDFPubKeyAgreementScheme nomina la curva: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 o pkasX448. Lo schema deve combaciare con l'algoritmo di chiave, la curva e la lunghezza di chiave del certificato, altrimenti la chiamata solleva "Certificate key does not match the requested agreement scheme"
Ogni busta riceve un UKM casuale fresco da 32 byte, una key-encryption key dal 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. L'aritmetica delle curve è Pascal puro senza alcun crypto provider di piattaforma sotto; l'articolo sull'aritmetica delle curve NIST in Pascal puro spiega passo passo come è messo insieme quel livello. Sulle curve di Montgomery l'intera coppia di chiavi effimera può essere generata in locale:
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: ha senso solo per le curve NIST
finally
HPDFSecureClearBytes(Scalar);
end;
end;
Le curve NIST chiedono di più al chiamante. HotPDF deriva le chiavi pubbliche dell'originator solo per X25519 e X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), quindi per P-256, P-384 e P-521 la coppia effimera viene dai vostri strumenti. Passate uno scalare big-endian esattamente della dimensione del campo (32, 48 o 66 byte) e il corrispondente punto non compresso 0x04||X||Y (65, 97 o 133 byte) come OriginatorPublicKey, e consegnate lo stesso punto a OwnPublicPoint, il parametro riservato agli schemi NIST. Lunghezze sbagliate sollevano subito. Il punto del destinatario preso dal certificato viene controllato contro l'equazione della curva prima che giri qualsiasi moltiplicazione
Ciò che nessuna libreria può controllare è se la vostra chiave pubblica dell'originator appartiene al vostro scalare. Metà disallineate producono comunque una busta ben formata. Il writer deriva la sua chiave da scalare per punto del destinatario, il destinatario deriva la propria dalla sua chiave privata per il punto originator sbagliato, e il controllo di integrità RFC 3394 lato destinatario è il primo ad accorgersene, quando ormai il file è partito. Un caricamento round-trip contro una chiave di test sulla stessa curva va nella vostra build; un EndDoc riuscito e una dimensione di file plausibile qui non provano nulla
Quel passo di accoppiamento è il motivo per cui la scelta della curva conta in pratica. Se controllate l'emissione dei certificati, X25519 è la curva più facile da fare bene da Delphi: uno scalare casuale da 32 byte, una chiamata helper, nessun punto da tenere in sincrono a mano. Le curve NIST si guadagnano il posto quando i certificati dei destinatari vengono da una PKI che emette solo P-256 o P-384, e in quel caso il caricamento round-trip smette di essere facoltativo
Perché l'ordine di /Recipients conta?
L'ordine di /Recipients conta perché la chiave di file è un digest sul seed seguito da ogni busta in ordine di array, quindi writer e reader devono hashare byte identici in sequenza identica. HotPDF tiene le buste nell'ordine in cui le aggiungete e le scrive invariate, il che vi lascia liberi di scegliere qualsiasi ordine. Ciò che succede all'array dopo è la parte rischiosa. Strumenti di post-processing, pipeline di firma e builder di buste fatti in casa sono dove si rompe, e i fallimenti condividono una forma — due parti che hashano byte leggermente diversi:
- La
TListnon tipizzata di Delphi memorizza un puntatore grezzo. Raccogliere le buste conList.Add(Pointer(Bytes))lascia il reference count alla variabile locale, quindi laSetLengthsuccessiva su quella variabile libera o riusa il buffer e ogni slot può finire per aliasare l'ultima busta.TList<TBytes>o un sempliceTArray<TBytes>tiene i riferimenti contati - I parser DER che lavorano sul posto modificano il buffer che parsano. Hashate una copia intonsa di ogni busta, presa prima che qualsiasi tentativo di apertura la tocchi
- Una
TStringListUnicode non è posto per DER binario: i byte da$80in su vengono ricodificati dalla code page in ingresso. Tenete le buste inTBytes, o codificatele prima in esadecimale - Il ciphertext va nelle stringhe esadecimali. Dentro una literal string, CR, LF e CRLF si rileggono tutti come un unico LF (ISO 32000-1 §7.3.4.2), quindi una busta che per caso contiene
$0D $0Aperde un byte. HotPDF scrive ogni voce di/Recipientscome stringa esadecimale e la lascia fuori dalla cifratura delle stringhe, visto che un reader ha bisogno delle buste prima di possedere qualsiasi chiave - Il primo byte di contenuto di una
BIT STRINGDER conta i bit inutilizzati e deve essere$00per le chiavi allineate al byte. Un DER scritto a mano che omette questo byte e mette il punto0x04||X||Ydirettamente nel contenuto sposta l'intera chiave di un byte, e un parser rigoroso rifiuta in blocco la chiave dell'originator - Quando una chiave che dovrebbe funzionare continua a non decifrare, confrontate livello per livello, in quest'ordine: chiave di file, l'IV in testa al ciphertext, chiave per oggetto, plaintext. Il guasto sta subito dopo il primo livello in disaccordo
Caricare un PDF cifrato a certificato con le chiavi private
Le chiavi private vanno registrate prima che LoadFromFile giri, perché HotPDF recupera la chiave di file durante il passaggio strutturale. Le identità RSA parsate con HPDFParsePFX vanno in PubSecKeyMaterial, e le ulteriori chiavi RSA in AddPubSecKeyMaterial, che rifiuta materiale senza un modulo e un esponente privato. Entrambi quegli slot di chiave vengono abbinati su modulo RSA ed esponente privato, quindi le chiavi EC, comprese quelle da PFX, prendono una terza strada: AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), con una delle costanti HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 o HPDFOIDECP521. Le curve NIST richiedono lì il punto pubblico non compresso del destinatario stesso; le curve di Montgomery lo ignorano
Un'identità EC che arriva come file PFX sta nella stessa chiamata. HPDFParsePFX la riporta con KeyAlgorithm = pfxkaECDSA (identità X25519 e X448 comprese) e compila CurveOID, lo scalare privato in PrivateKey e il punto del certificato in PublicKey, che mappano dritti sui tre parametri
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;
procedure OpenAuditPack(const LegalScalar: TBytes);
var
Reader: THotPDF;
EcKey: THPDFPFXKeyMaterial;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
// L'identità RSA diventa la chiave primaria
Reader.PubSecKeyMaterial :=
HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
// Identità EC da un PFX: curva, scalare e punto vanno nella lista di agreement
EcKey := HPDFParsePFX(TFile.ReadAllBytes('reviewer-c-p256.pfx'), 'pfx-password');
if EcKey.KeyAlgorithm = pfxkaECDSA then
Reader.AddPubSecAgreementKeyMaterial(EcKey.CurveOID,
EcKey.PrivateKey, EcKey.PublicKey);
// Scalare X25519 grezzo tenuto dal team legale; nessun punto necessario
Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Lasciato a sé, HotPDF prova ogni busta contro ogni chiave registrata: la chiave RSA primaria, poi ogni chiave RSA addizionale, poi il materiale EC. PubSecRecipientQuery restringe il tutto a una singola busta. La callback riceve il conteggio delle buste e deve restituire un indice a base zero dentro l'array; qualsiasi altro valore, -1 compreso, solleva durante il caricamento. Per provare ogni busta, lasciate la proprietà non assegnata. Sotto Delphi il tipo è un reference to function, quindi un metodo anonimo funziona, mentre sotto FPC è un semplice puntatore a funzione il cui argomento Context viene da PubSecRecipientQueryContext
Un comportamento merita un test tutto suo. Quando nessuna chiave registrata apre alcuna busta, il recupero della chiave termina senza chiave di file e non solleva per quel motivo. Fate assert su contenuto che sapete essere nel documento, come una stringa di testo o un valore di metadati; una chiamata LoadFromFile che ritorna normalmente mostra solo che la struttura del file è stata parsata
Cosa controllare nel viewer di destinazione?
Writer e reader di HotPDF concordano byte per byte, e le buste seguono le strutture CMS citate sopra. I viewer di terze parti sono un'altra questione: il supporto per il key transport RSA-OAEP, e ancor più per i destinatari X25519 o X448, varia tra i reader e tra le versioni dello stesso reader. Se il documento deve aprirsi in un viewer particolare, cifrate un file di test per un certificato di test dello stesso tipo di chiave e apritelo lì prima di impegnarvi su uno schema. Quando il viewer è fuori dal vostro controllo, la sua lista di supporto sceglie lo schema, e questo prevale sull'argomento di comodità di X25519 fatto prima
I permessi della busta restano policy che il software conforme onora, esattamente come sotto la cifratura a password. La qualità del seed tocca a voi: AESGenerateRandomBytes esiste per quel lavoro, e HotPDF cancella la propria copia del seed una volta derivata la chiave di file. Se una stringa, uno stream o un allegato ha bisogno di un crypt filter diverso, la guida alle policy dei crypt filter per StmF, StrF ed EFF mostra come assegnarne uno; l'handler a chiave pubblica accetta DefaultCryptFilter o Identity
Le buste per destinatario per certificati RSA e a curva ellittica, insieme al caricamento delle chiavi sulla via del ritorno, fanno parte del componente PDF HotPDF per Delphi e C++Builder, accanto alla cifratura a password e alle firme digitali