Um PDF criptografado por certificado carrega um único valor /P no dicionário de criptografia, não importa para quem o arquivo foi escrito; nos arquivos construídos mais adiante neste artigo é -64. Três recipients com três conjuntos de permissões diferentes compartilham esse mesmo número. Código que responde "este usuário pode imprimir?" a partir do /P está lendo um placeholder, porque sob o security handler de chave pública a palavra de permissão real viaja dentro do envelope criptografado de cada recipient e só aparece quando a chave privada desse recipient o abre
Esse desenho é o motivo de a criptografia por certificado valer o trabalho. Um pacote de auditoria trimestral vai para três revisores externos; o jurídico quer que todos leiam, só um pode imprimir, e ninguém quer uma senha parada no thread de e-mail ao lado do anexo. Uma senha não expressa isso. Certificados expressam: cada recipient destrava o arquivo com uma chave que já tem, e cada envelope carrega a própria palavra de permissão
Como a criptografia de PDF por certificado difere de uma senha?
Um PDF criptografado por chave pública deriva a file key dele de uma seed aleatória mais os bytes exatos de todo envelope de recipient, e nada que uma pessoa digita entra no cálculo. O handler é especificado na ISO 32000-1 §7.6.4 (§7.6.5 na ISO 32000-2), e os envelopes são estruturas CMS EnvelopedData da RFC 5652. O HotPDF grava /Filter /Adobe.PubSec com /SubFilter /adbe.pkcs7.s5. Para AES-256 isso significa /V 5, uma entrada /DefaultCryptFilter sob /CF com /CFM /AESV3, e o array /Recipients colocado dentro desse crypt filter em vez do dicionário de nível superior
Cada envelope criptografa 24 bytes: a seed de 20 bytes seguida da palavra de permissão de 32 bits daquele recipient. Na carga, um leitor abre um envelope, recupera a seed e faz o hash da seed junto com todo envelope na ordem do /Recipients (SHA-256 para AES-256, SHA-1 para as cifras mais antigas) para reconstruir a file key. O revisor B nunca fica sabendo o que o revisor A pode fazer, mas o leitor de B mesmo assim hasseia os bytes do envelope de A, então o array inteiro precisa sobreviver byte a byte. Se você ainda está escolhendo entre esse modelo e senhas comuns, o guia de criptografia por senha AES-256 e flags de permissão cobre o outro lado dessa troca
Escrevendo recipients RSA com EnablePubKeyEncryption
Para certificados RSA a sequência é curta: EnablePubKeyEncryption com aes256, depois um AddPubKeyRecipientCertificate por certificado codificado em DER, tudo antes do BeginDoc. O helper monta um envelope RSAES-OAEP in-process. Os dois últimos parâmetros escolhem o digest OAEP e o digest MGF1 de THPDFRSAOAEPHash (rohSHA256, rohSHA384 ou rohSHA512), ambos com padrão rohSHA256, e o conteúdo do envelope é criptografado com 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); // exatamente 20 bytes, mesmo para AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // o key type default é aes128
// O revisor A pode imprimir; o revisor B só pode ler e extrair
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;
Três detalhes dessa listagem sustentam tudo. A seed tem 20 bytes para todo key type, AES-256 incluído, e o EnablePubKeyEncryption levanta erro com qualquer outro comprimento. O key type tem padrão aes128, enquanto os dois helpers de certificado insistem em aes256, então deixar cair o segundo argumento faz a primeira chamada de AddPubKeyRecipientCertificate levantar "certificate envelopes require aes256". As cifras legadas (k40, k128, aes128) continuam disponíveis por meio do AddPubKeyRecipient, que recebe um envelope DER completo montado por outra ferramenta
O terceiro detalhe é a versão. AES-256 sob o handler de chave pública é um recurso do PDF 2.0, e o HotPDF sobe a versão do documento para 2.0 por conta própria. Com StrictVersionLock definido em uma versão menor, esse avanço não é permitido: o EnablePubKeyEncryption retorna sem ligar nada, e o erro aparece uma linha depois quando o helper de recipient reporta "call EnablePubKeyEncryption first". Essa mensagem manda as pessoas caçar um erro de ordem de chamada. Verifique o version lock em vez disso. Trocar a criptografia durante um update incremental é recusado de cara com EInvalidOpException
Adicionando recipients ECDH em P-256, P-384, P-521, X25519 e X448
Certificados de curva elíptica passam pelo AddPubKeyAgreementRecipientWithSecret, que escreve um recipient de key agreement CMS (KeyAgreeRecipientInfo da RFC 5753, com o perfil X25519 e X448 da RFC 8418) e calcula o segredo compartilhado ECDH in-process. Um valor THPDFPubKeyAgreementScheme nomeia a curva: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 ou pkasX448. O scheme precisa casar com o algoritmo da chave do certificado, a curva e o comprimento da chave, ou a chamada levanta "Certificate key does not match the requested agreement scheme"
Cada envelope ganha um UKM aleatório fresco de 32 bytes, uma key-encryption key do KDF stdDH (SHA-256 para P-256 e X25519, SHA-384 para P-384, SHA-512 para P-521 e X448) e um key wrap AES-256 conforme a RFC 3394. A aritmética de curvas é Pascal puro sem provedor de criptografia de plataforma por baixo; o artigo sobre aritmética de curvas NIST em Pascal puro percorre como essa camada é montada. Nas curvas de Montgomery o par de chaves efêmero inteiro pode ser gerado localmente:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Scalar efêmero fresco por envelope; o clamping acontece dentro da 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: só tem significado para as curvas NIST
finally
HPDFSecureClearBytes(Scalar);
end;
end;
As curvas NIST pedem mais de quem chama. O HotPDF deriva chaves públicas de originator apenas para X25519 e X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), então para P-256, P-384 e P-521 o par efêmero vem da sua própria toolchain. Passe um scalar big-endian de exatamente o tamanho do campo (32, 48 ou 66 bytes) e o ponto não comprimido 0x04||X||Y correspondente (65, 97 ou 133 bytes) como OriginatorPublicKey, e entregue o mesmo ponto ao OwnPublicPoint, o parâmetro reservado para os schemes NIST. Comprimentos errados levantam erro imediatamente. O ponto do recipient tirado do certificado é checado contra a equação da curva antes de qualquer multiplicação rodar
O que nenhuma biblioteca consegue checar é se a sua chave pública de originator pertence ao seu scalar. Metades que não combinam ainda produzem um envelope perfeitamente bem formado. O escritor deriva a chave dele de scalar vezes ponto do recipient, o recipient deriva a chave dele da chave privada dele vezes o ponto de originator errado, e a checagem de integridade da RFC 3394 no lado do recipient é a primeira coisa a perceber, momento em que o arquivo já foi distribuído. Um load de round trip contra uma chave de teste na mesma curva pertence ao seu build; um EndDoc bem-sucedido e um tamanho de arquivo plausível não provam nada aqui
Esse passo de pareamento é o motivo de a escolha da curva importar na prática. Se você controla a emissão de certificados, X25519 é a curva mais fácil de acertar a partir do Delphi: um scalar aleatório de 32 bytes, uma chamada de helper, nenhum ponto para manter em sincronia à mão. As curvas NIST merecem o lugar delas quando os certificados dos recipients vêm de uma PKI que só emite P-256 ou P-384, e nesse caso o load de round trip deixa de ser opcional
Por que a ordem do /Recipients importa?
A ordem do /Recipients importa porque a file key é um digest sobre a seed seguida de todo envelope na ordem do array, então escritor e leitor precisam hassear bytes idênticos em uma sequência idêntica. O HotPDF mantém os envelopes na ordem em que você os adiciona e os grava inalterados, o que deixa você livre para escolher qualquer ordem. O que acontece com o array depois é a parte arriscada. Ferramentas de pós-processamento, pipelines de assinatura e builders de envelope caseiros são onde isso quebra, e as falhas compartilham uma forma — dois lados hasseando bytes ligeiramente diferentes:
- O
TListsem tipo do Delphi guarda um ponteiro cru. Coletar envelopes comList.Add(Pointer(Bytes))deixa o reference count com a variável local, então o próximoSetLengthnessa variável libera ou reutiliza o buffer e todo slot pode acabar fazendo aliasing do último envelope. UmTList<TBytes>ou umTArray<TBytes>simples mantém as referências contadas - Parsers DER que trabalham no lugar modificam o buffer que interpretam. Hasseie uma cópia pristine de cada envelope, tirada antes de qualquer tentativa de unwrap tocá-lo
- Um
TStringListUnicode não é lugar para DER binário: bytes em$80ou acima são re-encodados pela code page na entrada. Guarde envelopes emTBytes, ou hex-encode-os primeiro - Ciphertext pertence a hex strings. Dentro de uma literal string, CR, LF e CRLF são todos lidos de volta como um único LF (ISO 32000-1 §7.3.4.2), então um envelope que por acaso contenha
$0D $0Aperde um byte. O HotPDF grava toda entrada de/Recipientscomo hex string e a deixa fora da criptografia de strings, já que um leitor precisa dos envelopes antes de ter qualquer chave - O primeiro byte de conteúdo de uma
BIT STRINGDER conta bits não usados e precisa ser$00para chaves alinhadas a byte. DER escrito à mão que descarta esse byte e põe o ponto0x04||X||Ydireto no conteúdo desloca a chave inteira em um byte, e um parser estrito rejeita a chave de originator de cara - Quando uma chave que deveria funcionar ainda não consegue decifrar, compare camada por camada, nesta ordem: file key, o IV no início do ciphertext, chave por objeto, plaintext. A falha mora logo depois da primeira camada que discorda
Carregando um PDF criptografado por certificado com chaves privadas
Chaves privadas precisam estar registradas antes de o LoadFromFile rodar, porque o HotPDF recupera a file key durante a passada estrutural. Identidades RSA interpretadas com HPDFParsePFX vão para o PubSecKeyMaterial, e outras chaves RSA para o AddPubSecKeyMaterial, que recusa material sem modulus e sem expoente privado. Os dois slots de chave RSA são casados pelo modulus RSA e pelo expoente privado, então chaves EC, inclusive as baseadas em PFX, seguem uma terceira rota: AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), com uma das constantes HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 ou HPDFOIDECP521. As curvas NIST exigem ali o ponto público não comprimido do próprio recipient; as curvas de Montgomery o ignoram
Uma identidade EC que chega como arquivo PFX cabe na mesma chamada. O HPDFParsePFX a reporta com KeyAlgorithm = pfxkaECDSA (identidades X25519 e X448 incluídas) e preenche CurveOID, o scalar privado em PrivateKey e o ponto do certificado em PublicKey, que mapeiam direto nos três parâmetros
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;
// identidade RSA vira a chave primária
Reader.PubSecKeyMaterial :=
HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
// identidade EC de um PFX: curva, scalar e ponto vão para a lista de agreement
EcKey := HPDFParsePFX(TFile.ReadAllBytes('reviewer-c-p256.pfx'), 'pfx-password');
if EcKey.KeyAlgorithm = pfxkaECDSA then
Reader.AddPubSecAgreementKeyMaterial(EcKey.CurveOID,
EcKey.PrivateKey, EcKey.PublicKey);
// scalar X25519 cru guardado pelo time jurídico; nenhum ponto é preciso
Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Deixado sozinho, o HotPDF tenta todo envelope contra toda chave registrada: a chave RSA primária, depois cada chave RSA adicional, depois o material EC. O PubSecRecipientQuery estreita isso para um único envelope. O callback recebe a contagem de envelopes e precisa devolver um índice zero-based dentro do array; qualquer outro valor, -1 incluído, levanta erro durante a carga. Para tentar todo envelope, deixe a propriedade sem atribuição. Sob Delphi o tipo é uma reference to a function, então um anonymous method funciona, enquanto sob FPC é um ponteiro de função simples cujo argumento Context vem de PubSecRecipientQueryContext
Um comportamento merece um teste próprio. Quando nenhuma chave registrada abre nenhum envelope, a recuperação de chave termina sem file key e não levanta erro por isso. Faça assert sobre conteúdo que você sabe que o documento guarda, como uma string de texto ou um valor de metadados; uma chamada de LoadFromFile que retorna normalmente só mostra que a estrutura do arquivo foi interpretada
O que o HotPDF não garante
O escritor e o leitor do HotPDF concordam byte a byte, e os envelopes seguem as estruturas CMS citadas acima. Viewers de terceiros são outra pergunta: o suporte a key transport RSA-OAEP, e ainda mais a recipients X25519 ou X448, varia entre leitores e entre versões do mesmo leitor. Se o documento precisa abrir em um viewer específico, criptografe um arquivo de teste para um certificado de teste do mesmo key type e abra-o lá antes de se comprometer com um scheme. Quando o viewer está fora do seu controle, a lista de suporte dele escolhe o scheme, e isso se sobrepõe ao argumento de conveniência do X25519 feito antes
As permissões do envelope continuam sendo política que software em conformidade honra, exatamente como sob criptografia por senha. A qualidade da seed é responsabilidade sua: o AESGenerateRandomBytes existe para esse trabalho, e o HotPDF apaga a própria cópia da seed assim que a file key é derivada. Se uma string, um stream ou um anexo precisa de um crypt filter diferente, o guia de políticas de crypt filter para StmF, StrF e EFF mostra como atribuir um; o handler de chave pública aceita DefaultCryptFilter ou Identity
Envelopes por recipient para certificados RSA e de curva elíptica, junto com a carga de chaves no caminho de volta, fazem parte do HotPDF PDF component para Delphi e C++Builder, ao lado da criptografia por senha e das assinaturas digitais