HotPDF cifra un PDF para titulares de certificados específicos mediante el manejador de seguridad de clave pública de ISO 32000: EnablePubKeyEncryption toma una semilla aleatoria de 20 bytes, y cada destinatario recibe su propio sobre CMS, armado por AddPubKeyRecipientCertificate para claves RSA (transporte de claves RSA-OAEP) o por AddPubKeyAgreementRecipientWithSecret para claves de curva elíptica (ECDH sobre P-256, P-384, P-521, X25519 o X448). Nadie comparte una contraseña; quien tenga la clave privada correspondiente abre el archivo
El caso de uso siempre es alguna versión de la misma historia. Un paquete de auditoría trimestral va a tres revisores externos, legal quiere que cada uno lo lea, solo uno de ellos puede imprimirlo, y nadie quiere una contraseña alojada en un hilo de correo justo al lado del adjunto. El cifrado con contraseña no puede expresar eso. El cifrado con certificados sí, porque cada destinatario desbloquea el documento con una clave que ya tiene, y cada destinatario puede llevar un conjunto de permisos distinto dentro de su propio sobre
¿En qué se diferencia el cifrado de PDF con certificados de una contraseña?
Un PDF cifrado con clave pública deriva su clave de archivo de una semilla aleatoria más los bytes exactos de cada sobre de destinatario, no de nada que una persona escriba. El manejador está descrito en ISO 32000-1 §7.6.4 (§7.6.5 en ISO 32000-2), y los sobres son estructuras CMS EnvelopedData según define RFC 5652. HotPDF escribe /Filter /Adobe.PubSec con /SubFilter /adbe.pkcs7.s5; para AES-256 eso significa /V 5 y una entrada /DefaultCryptFilter bajo /CF con /CFM /AESV3, y el array /Recipients vive dentro de ese crypt filter. Cada sobre cifra 24 bytes: la semilla de 20 bytes seguida de la palabra de permisos de 32 bits de ese destinatario. El valor /P del diccionario de cifrado es solo un placeholder, porque los permisos reales viajan dentro de cada sobre. Al cargar el archivo, un lector desenvuelve un sobre, recupera la semilla y la hashea junto con cada sobre en el orden de /Recipients (SHA-256 para AES-256, SHA-1 para los cifradores antiguos) para reconstruir la clave de archivo. Si todavía está decidiendo entre este modelo y las contraseñas corrientes, la guía de cifrado AES-256 con contraseña y flags de permisos cubre el otro lado de esa decisión
Escribir destinatarios RSA con EnablePubKeyEncryption
Para certificados RSA, llame a EnablePubKeyEncryption con aes256 y después llame a AddPubKeyRecipientCertificate una vez por cada certificado codificado en DER antes de BeginDoc. El helper arma un sobre RSAES-OAEP en el propio proceso con valores THPDFRSAOAEPHash para el digest de OAEP y el digest de MGF1 (rohSHA256, rohSHA384 o rohSHA512), y cifra el contenido del sobre 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); // exactamente 20 bytes, incluso para AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // el tipo de clave por default es aes128
// El revisor A puede imprimir; el revisor B solo puede leer y extraer
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;
Tres detalles de ese listado sostienen todo. Primero, la longitud de la semilla está fija en 20 bytes para cualquier tipo de clave, AES-256 incluido; EnablePubKeyEncryption lanza una excepción con cualquier otra longitud. Segundo, EnablePubKeyEncryption queda por default en aes128, y los dos helpers de certificados se niegan a correr salvo que el tipo de clave sea aes256, así que olvidar el segundo argumento le cuesta la excepción “certificate envelopes require aes256”. Los cifradores legacy (k40, k128, aes128) siguen funcionando, pero solo vía AddPubKeyRecipient con un sobre armado por su cuenta. Tercero, el cifrado de clave pública AES-256 es una característica de PDF 2.0, así que HotPDF sube la versión del documento a 2.0 automáticamente. Con StrictVersionLock activo sobre una versión menor, EnablePubKeyEncryption regresa sin habilitar nada, y el fallo recién aparece en la línea siguiente como “call EnablePubKeyEncryption first”. Cambiar el cifrado durante una actualización incremental lanza EInvalidOpException de inmediato
Agregar destinatarios ECDH: P-256, P-384, P-521, X25519 y X448
Para certificados de curva elíptica, AddPubKeyAgreementRecipientWithSecret escribe un destinatario CMS de key agreement (KeyAgreeRecipientInfo, la estructura KARI de RFC 5753, con el perfil X25519 y X448 de RFC 8418) y calcula el secreto compartido ECDH en el propio proceso. Usted elige la curva con un valor THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 o pkasX448. El esquema tiene que coincidir con la clave del certificado, o la llamada lanza “Certificate key does not match the requested agreement scheme”. Por debajo, cada sobre recibe un UKM aleatorio fresco de 32 bytes, una key-encryption key derivada con el KDF stdDH (SHA-256 para P-256 y X25519, SHA-384 para P-384, SHA-512 para P-521 y X448), y un key wrap AES-256 según define RFC 3394. El secreto compartido sale de código de curvas en Pascal puro, sin ningún proveedor criptográfico de plataforma de por medio; el artículo sobre aritmética de curvas NIST en Pascal puro explica cómo se construyó y verificó esa capa. Para las curvas de Montgomery, el par de claves efímero completo se puede generar localmente:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Escalar efímero fresco por sobre; el clamping ocurre dentro del Montgomery 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: solo tiene sentido para las curvas NIST
finally
HPDFSecureClearBytes(Scalar);
end;
end;
Las curvas NIST le piden más al que llama. HotPDF trae helpers de clave pública solo para X25519 y X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), así que para P-256, P-384 y P-521 usted genera el par de claves efímero con su propia herramienta y pasa un escalar big-endian de exactamente el tamaño del campo (32, 48 o 66 bytes) más el punto sin comprimir 0x04||X||Y correspondiente como OriginatorPublicKey. HotPDF valida el punto del destinatario contra la ecuación de la curva, pero no puede chequear que su clave pública originadora realmente pertenezca a su escalar. Mitades que no coinciden igual producen un sobre perfectamente bien formado que ningún destinatario puede abrir, y por eso una carga de ida y vuelta pertenece a su suite de pruebas, no a una simple verificación del tamaño del archivo
¿Por qué importa el orden de /Recipients?
El orden de /Recipients importa porque la clave de archivo es un digest sobre la semilla y cada sobre en el orden del array, así que escritor y lector tienen que hashear los mismos bytes en la misma secuencia. HotPDF conserva los sobres en el orden en que usted los agrega y los escribe sin cambios, lo que significa que puede agregar destinatarios en el orden que quiera, pero nada aguas abajo puede reordenar, recodificar o “limpiar” ese array. La mayoría de los bugs reales de esta zona fueron una variación sobre ese tema, con dos lados hasheando bytes ligeramente distintos:
- Guardar arrays dinámicos en un
TListvíaAddconserva solo un puntero crudo mientras el conteo de referencias se queda con la variable local. El siguienteSetLengthlibera el buffer y puede reutilizarlo, así que cada posición terminaba aliasando el último sobre y los archivos multi-destinatario derivaban una clave equivocada. El arreglo es guardar una copia propia conList.Add(Pointer(System.Copy(Bytes))) - Desenvolver sobres parsea el DER in situ, y la pasada de recuperación de claves originalmente hasheaba esos mismos arrays vivos. El lector ahora saca un snapshot prístino de cada sobre antes de que cualquier unwrap los toque, y el digest corre sobre los snapshots
- DER binario pasado por un
TStringListUnicode hace que los bytes en$80o superiores queden recodificados por la code page, así que HotPDF guarda los sobres como texto hex internamente - Los strings cifrados y binarios deben escribirse como hex strings. Un string literal está sujeto a la normalización de fin de línea, donde CR, LF y CRLF se convierten todos en un solo LF (ISO 32000-1 §7.3.4.2), y eso reescribe el ciphertext sin avisar. HotPDF emite cada entrada de
/Recipientscomo hex string y la exime del cifrado de strings, ya que todo lector necesita los sobres antes de tener alguna clave - El primer byte de un
BIT STRINGDER cuenta bits sin usar y debe ser cero para claves alineadas a byte. Dejarlo sin inicializar hizo que después deSetLengthse escribiera lo que hubiera en el stack, y un unwrapper estricto rechazaba la clave originadora, así que un archivo podía fallar al abrirse justamente con la clave para la que fue escrito - Cuando la misma clave sigue sin descifrar, compare capa por capa: la clave de archivo, luego el prefijo del ciphertext (el IV), luego la clave del objeto, luego el plaintext. El bug vive justo después de la primera capa que no coincide
¿Cómo se abre un PDF cifrado con certificados usando una clave privada?
Para abrir un PDF cifrado con certificados, registre el material de claves privadas antes de llamar a LoadFromFile, porque HotPDF recupera la clave de archivo durante la pasada estructural. Asigne una clave RSA o EC parseada con HPDFParsePFX a PubSecKeyMaterial, agregue más claves RSA con AddPubSecKeyMaterial, y registre escalares ECDH crudos con AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), usando las constantes HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 o HPDFOIDECP521. Las curvas NIST requieren el punto público sin comprimir del propio destinatario; las curvas de Montgomery lo ignoran
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);
// Opcional: elija el sobre directamente en vez de probarlos todos
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = probar cada sobre en orden
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Sin callback, HotPDF prueba cada sobre contra cada clave registrada: primero la clave primaria, luego cada clave RSA adicional, luego el material EC. PubSecRecipientQuery recibe el conteo de sobres y devuelve un índice base cero o -1, y un índice fuera del array lanza una excepción en lugar de recortarse. Ojo: AddPubSecKeyMaterial acepta solo material RSA (insiste en un módulo y un exponente privado), así que las claves EC van en PubSecKeyMaterial o en AddPubSecAgreementKeyMaterial. Cuando ninguna clave desenvuelve ningún sobre, el paso de recuperación regresa sin clave de archivo en lugar de lanzar una excepción, así que verifique que el contenido esperado realmente se descifró en vez de fiarse de que la llamada de carga regresó
Lo que HotPDF no garantiza
HotPDF garantiza que su propio escritor y lector concuerdan byte a byte, y arma sobres que siguen las estructuras CMS citadas arriba. No garantiza que cualquier visor de PDF abra cualquier combinación. El soporte de transporte de claves RSA-OAEP y de destinatarios X25519 o X448 varía entre lectores y versiones, y no hemos publicado resultados de compatibilidad para esas combinaciones. Si un documento tiene que abrirse en un visor específico, cifre un archivo de prueba con un certificado de prueba del mismo tipo de clave y ábralo ahí antes de comprometerse con un esquema. Los permisos que viajan en el sobre siguen siendo política que el software conforme respeta, igual que bajo cifrado con contraseña. La calidad de la semilla también es responsabilidad suya: AESGenerateRandomBytes existe para ese trabajo, y HotPDF borra su copia de la semilla una vez derivada la clave de archivo. Si además necesita que un string, un stream o un adjunto use otro crypt filter, la guía de políticas de crypt filter para StmF, StrF y EFF muestra qué nombres de filtro acepta el manejador de clave pública
El cifrado con certificados, los sobres de destinatarios RSA-OAEP y ECDH, y la carga de claves privadas vienen todos en el HotPDF Delphi PDF component, junto con el cifrado con contraseña, las firmas digitales y el resto del toolset ISO 32000 para Delphi y C++Builder