O PDFlibPas, a PDF library para Delphi e C++Builder, implementa a suíte de criptografia comercial chinesa em Object Pascal puro: hashing SM3 (GB/T 32905), criptografia de blocos SM4 (GB/T 32907) e assinaturas em curva elíptica SM2 (GB/T 32918). Com elas, ele assina PDFs com assinaturas CMS SM2-with-SM3 por meio de SetSignProcessSM2Identity e criptografa documentos com um crypt filter SM4 por meio de Encrypt com strength 6, sem provider do sistema operacional ou DLL externa envolvidos
O motivo habitual para querer isso é um requisito de compra, não curiosidade. Sistemas vendidos para governo, finanças e saúde na China são cada vez mais esperados de usar os algoritmos nacionais, e a Windows CryptoAPI e a CNG não assinam com SM2 nem fazem hash com SM3. Se o caminho de assinatura passa pela plataforma, a resposta é simplesmente não. Uma implementação em Pascal puro dá a mesma resposta em Win32, Win64 e nos alvos FPC, e a dá sem uma dependência nativa móvel embaixo do pipeline de documentos
Por que construir a suíte GB/T em Object Pascal puro?
O PDFlibPas constrói SM2, SM3 e SM4 em Pascal porque nenhum provider de plataforma a que ele já dá suporte consegue fazer o trabalho. CryptoAPI e CNG não têm suporte a assinatura SM3, então antes de a identidade SM2 existir, a biblioteca deliberadamente recusava assinar quando o digest era definido como SM3: a alternativa era um SignerInfo que declara um OID de digest e carrega uma assinatura sobre outra coisa, o que é uma assinatura errada silenciosa em vez de um erro visível
Os três algoritmos também têm uma ordem natural de construção, e a biblioteca a seguiu. O SM3 veio primeiro, porque o SM2 precisa dele para o hash de identidade ZA e para o message digest. O SM4 veio depois como uma unit independente. O SM2 veio por último, primeiro como primitivas brutas verificadas contra a curva de exemplo publicada no apêndice da especificação, depois como uma identidade de assinatura ligada ao mesmo construtor de CMS que já trata Ed25519, Ed448 e ML-DSA. A mesma abordagem está descrita para as curvas Ed448 e Brainpool ECDSA em Pascal puro e o assinante pós-quântico FIPS 204 ML-DSA, e o SM2 se encaixa nesse framework como mais um tipo de chave
As primitivas são funções públicas, então você pode usá-las fora da assinatura de PDF. SM3String e SM3ByteArray em PDFlibDigest devolvem o digest bruto de 32 bytes (um byte por caractere, não hex), e a PDFlibSM4 exporta helpers ECB e CBC em dois sabores: versões alinhadas a bloco que devolvem string vazia em comprimento errado, e versões PKCS#7 como SM4EncryptCBCPKCS7 para dados de comprimento arbitrário
uses
SysUtils, PDFlibDigest, PDFlibSM4;
function RawToHex(const S: AnsiString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + IntToHex(Byte(S[I]), 2);
end;
var
Digest, Key, IV, Cipher, Plain: AnsiString;
begin
// amostra GB/T 32905: SM3("abc")
Digest := SM3String('abc');
Assert(LowerCase(RawToHex(Digest)) =
'66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');
// SM4 precisa de chave de 16 bytes; CBC precisa de IV de 16 bytes
Key := 'SixteenByteKey!!';
IV := 'InitVector16byte';
Cipher := SM4EncryptCBCPKCS7(Key, IV, 'Invoice 2026-0042 approved');
Plain := SM4DecryptCBCPKCS7(Key, IV, Cipher);
Assert(Plain = 'Invoice 2026-0042 approved');
end;
Conte aquelas strings de 16 bytes à mão por sua conta e risco. Duas strings de teste na suíte de testes do SM4 que deveriam ter 16 bytes tinham na verdade 17, e como os helpers alinhados a bloco devolvem string vazia em comprimento errado de propósito, a falha apareceu como um assertion confuso em vez de uma exceção
Assinando um PDF com uma identidade SM2
Um processo de assinatura do PDFlibPas usa uma chave SM2 assim que SetSignProcessSM2Identity ou SetSignProcessSM2IdentityFromFile anexa um certificado e uma chave privada a ele. O certificado é DER, e a chave privada pode ser um escalar bruto de 32 bytes, um contêiner PKCS#8 sem criptografia ou um ECPrivateKey SEC 1. Anexar uma identidade SM2 limpa qualquer identidade Ed25519, Ed448, ML-DSA ou Brainpool no mesmo processo e desliga assinatura por certificate store e CNG, então um processo de assinatura nunca carrega duas chaves competindo
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('contract.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open source PDF');
Pdf.SetSignProcessField(SignId, 'Signature1');
if Pdf.SetSignProcessSM2IdentityFromFile(SignId,
'signer-sm2.cer', 'signer-sm2.key') <> 1 then
raise Exception.Create('SM2 certificate or key file missing');
Pdf.SetSignProcessInfo(SignId, 'Approved', 'Shanghai', '[email protected]');
Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
Pdf.SetSignProcessDigestAlgorithm(SignId, 8); // 8 = SM3
Pdf.EndSignProcessToFile(SignId, 'contract-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('SM2 signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
Pdf.ReleaseSignProcess(SignId);
finally
Pdf.Free;
end;
end;
Note o que o valor de retorno de SetSignProcessSM2IdentityFromFile diz e não diz. Um retorno de 1 significa que ambos os arquivos foram lidos e armazenados no processo de assinatura. A checagem real, derivar a chave pública do escalar privado e compará-la com o certificado, roda quando a assinatura é aplicada, então um par de chaves destoante aparece como um GetSignProcessResult diferente de 1, não na chamada de identidade
O código 8 em SetSignProcessDigestAlgorithm seleciona SM3, e com uma identidade SM2 ele é efetivamente obrigatório: o construtor de CMS força SM3 para uma chave SM2 independentemente do que o processo pediu, do mesmo jeito que uma chave Ed25519 é forçada ao SHA-512. A combinação reversa também é bloqueada. Com adbe.pkcs7.detached ou ETSI.CAdES.detached, um processo de assinatura definido para SM3 que não carrega uma identidade SM2 sai sem assinar, porque nenhum outro signatário da biblioteca consegue produzir uma assinatura baseada em SM3 que case com o OID declarado
O que entra em um SignerInfo SM2-with-SM3?
Uma assinatura SM2 no PDFlibPas é computada sobre os atributos assinados codificados em DER, a construção CMS padrão, mas o SM2 acrescenta um passo que a ECDSA não tem. A GB/T 32918.2 faz o hash da mensagem junto com ZA, um digest de identidade definido como ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA), em que cada parte da curva é um valor big-endian de 32 bytes e ID é o identificador distintivo do signatário. A assinatura é então r = (e + x1) mod n e s = ((1 + d)-1(k - r·d)) mod n, com um nonce k fresco do CSPRNG da biblioteca e uma nova tentativa em rodadas degeneradas
O PDFlibPas usa o identificador distintivo padrão da especificação, a string de 16 caracteres 1234567812345678, tanto no lado da assinatura quanto no da verificação. Não há API para mudá-lo. Isso casa com a esmagadora maioria dos certificados SM2 em produção, mas se uma contraparte assinar com um ID customizado, os valores de ZA divergem e a assinatura não vai validar, mesmo que todos os outros bytes estejam corretos
O par (r, s) entra no CMS como uma SEQUENCE DER de dois valores INTEGER, o layout que todo SignerInfo da família DSA usa. O PDFlibPas grava os identificadores de algoritmo registrados no GM/T 0006: 1.2.156.10197.1.301 para a chave SM2, 1.2.156.10197.1.401 para SM3 e 1.2.156.10197.1.501 para SM2-with-SM3. Requisições de timestamp RFC 3161 criadas com SM3 (SetTimeStampOptions digest 8) carregam o mesmo OID SM3. Versões antes da 3.589.1 gravavam um arco não registrado 1.2.156.101.1; a verificação ainda o aceita, então documentos assinados com aquelas versões continuam validando no PDFlibPas, mas validadores de terceiros não os reconhecerão, o que é uma boa razão para reassinar qualquer coisa que saia do seu próprio pipeline
Onde a verificação SM2 quebrou: bytes de sinal DER e um carry perdido
A maior parte do esforço de engenharia no código SM2 foi em dois defeitos que produziam saída plausível. Ambos são típicos de criptografia de inteiro grande escrita à mão, e ambos são baratos de evitar depois que você os conhece
O primeiro era um byte de sinal DER. Valores INTEGER ASN.1 são com sinal, então o encoder corretamente antepõe 0x00 a qualquer r ou s cujo bit mais alto esteja ligado, dando um valor de 33 bytes. O verificador convertia cada inteiro para uma largura fixa de 32 bytes sem remover esse byte antes. Cerca de metade de todos os valores de r e s tem o bit mais alto ligado, então uma fração grande de documentos recém-assinados falhava na verificação. A primeira rodada de teste por acaso sorteou valores curtos e passou; a suíte Delphi Win32, assinando com nonces aleatórios, expôs o problema imediatamente. O verificador agora apaga um zero à esquerda de qualquer inteiro de 33 bytes antes do preenchimento para 32 bytes, e rejeita qualquer coisa mais longa
O segundo era um carry perdido na soma de 256 bits. Elementos de campo são oito limbs UInt32 little-endian, e a soma modular estava escrita como "some, depois subtraia p se o resultado não estiver abaixo de p". Para entradas reduzidas, a soma fica abaixo de 2p mas ainda pode exceder 2256, e quando acontecia, o carry descartado dobrava o valor de volta aos 256 bits baixos, onde a comparação contra p dizia que estava tudo bem. Dobrar um ponto com coordenada grande dava errado; múltiplos pequenos não. O sintoma visível era que 1G até 8G batia com a referência enquanto 9G, 10G e 11G não, e a correção foi achada comparando 1280 entradas e saídas de soma com valores computados independentemente e investigando o primeiro par divergente
A soma corrigida na PDFlibSM2 mantém o carry devolvido pela soma bruta de limbs e subtrai p uma vez quando o carry está ligado ou o resultado não está abaixo de p; uma única subtração basta porque p excede 2255. O padrão vale para qualquer campo primo logo acima de uma potência de dois. Se seu helper de adição descarta o carry final, teste múltiplos bem além do ponto em que a duplicação cruza a fronteira de palavra pela primeira vez, não apenas 1G, 2G e 3G
Por que o SM3 esteve errado duas vezes antes de estar certo
A primeira implementação de SM3 falhava em todo vetor de teste padrão, e uma implementação de referência independente escrita em paralelo também. Duas implementações que concordam entre si e divergem do padrão apontam para um mal-entendido compartilhado da especificação, não para um erro de transcrição. Comparar as fórmulas linha a linha com o texto publicado do SM3 achou três lugares em que o código tinha sido escrito de memória
- A expansão de mensagem aplica a rotação de 7 bits a W[j-13]; W[j-6] é um termo XOR simples
- SS1 combina rotl(A, 12), E e a constante de rodada rotacionada com adição módulo 232, não XOR
- Para as rodadas 16 a 63, FF é a função de maioria (X AND Y) OR (X AND Z) OR (Y AND Z); a função de escolha estilo SHA pertence à GG
Depois dessas três correções, a string vazia, abc e o vetor de dois blocos de 64 bytes todos bateram. O SM4 então entrou de primeira, porque o S-box e os vetores de teste foram tirados do texto da especificação antes de qualquer código ser escrito: a chave de rodada rk0 = F12186F9, o vetor de bloco único 681EDF34D206965E86B3E94F536E4246, e o vetor de um milhão de iterações 595298C7C6FD271F0402F804C33D3F66. O trabalho do SM2 acrescentou uma regra relacionada: constantes hex longas são geradas por agrupamento e checadas por comprimento, nunca digitadas à mão, depois que uma coordenada de ponto base copiada à mão sem um dígito mandou uma sessão de debugging atrás de bugs de implementação que não existiam
Criptografando um PDF com o crypt filter SM4
O TPDFlib.Encrypt com strength 6 criptografa o documento selecionado com SM4-128 pelo framework de crypt filters da ISO 32000-1 §7.6.5. O dicionário de criptografia usa o security handler padrão com /V 4, /R 4 e /Length 128, e o crypt filter /StdCF carrega /CFM /SM4. Salvar com strength 6 eleva o arquivo a pelo menos PDF 1.6, o GetEncryptionLevel reporta 7 para tal arquivo, e a criptografia por chave pública ganha a mesma opção por meio de EncryptPublicKey com strength 4
var
Pdf: TPDFlib;
Perm: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report.pdf', '') <> 1 then
raise Exception.Create('cannot load PDF');
// impressão, cópia e preenchimento de formulário permitidos; sem edições ou montagem
Perm := Pdf.EncodePermissions(1, 1, 0, 0, 1, 1, 0, 1);
if Pdf.Encrypt('owner-secret', 'reader-pass', 6, Perm) <> 1 then
raise Exception.Create('SM4 encryption failed');
Pdf.SaveToFile('report-sm4.pdf');
finally
Pdf.Free;
end;
end;
Cada string e stream é criptografado como um IV aleatório de 16 bytes seguido de ciphertext SM4-CBC com padding PKCS#7, o mesmo layout que o AESV2 usa. A chave por objeto segue o Algoritmo 1 da ISO 32000-1 §7.6.2: a chave do arquivo, três bytes baixos do número do objeto e dois bytes do número de geração, mais o sufixo sAlT, com hash MD5 e truncado a 16 bytes. A derivação de senha para chave é a da revisão 4, então o strength 6 herda seus limites: senhas não passam por processamento SASLprep (apenas os strengths AES-256 3, 4 e 5 passam), e a derivação de chave não é mais forte que a do AESV2
Limites de interoperabilidade que você deve conhecer antes de colocar em produção
O resumo honesto é que a assinatura SM2 interopera dentro dos limites dos OIDs e do ID padrão descritos acima, enquanto a criptografia SM4 só interopera com software que conhece a mesma convenção. /SM4 não é um dos valores /CFM definidos na ISO 32000-1 Tabela 25 nem na ISO 32000-2, que definem None, V2, AESV2 e AESV3. Um leitor PDF mainstream que encontrar /CFM /SM4 não vai decifrar o arquivo. O próprio PDFlibPas lê /SM4 e a variante /SM4V1 por seu security handler, então um pipeline fechado em que o PDFlibPas grava e lê funciona; um arquivo para o público geral deveria usar AES-256
Para assinaturas, o limite é o layout do certificado e o validador do outro lado. O PLValidateSM2Identity, a checagem por trás do processo de assinatura, aceita o layout GM/T 0015, em que o SubjectPublicKeyInfo carrega id-ecPublicKey com a curva SM2 1.2.156.10197.1.301 como parâmetro, assim como certificados que põem o OID SM2 no campo de algoritmo em si, e rejeita qualquer coisa cujo ponto público derivado não bata com o escalar privado. Antes de confiar em assinaturas SM2 em produção, assine um documento com um certificado da CA que você vai realmente usar e verifique-o com o validador que sua contraparte roda. Você pode rodar a checagem de identidade você mesmo antes de iniciar um processo de assinatura:
uses
SysUtils, PDFlibModernSign;
var
CertDER, KeyDER, Scalar: AnsiString;
begin
// CertDER e KeyDER carregados do disco ou de um export de HSM
// Mantenha o parâmetro Out em sua própria variável: passar KeyDER como
// entrada Const e saída Out ao mesmo tempo limparia a entrada primeiro
if not PLValidateSM2Identity(CertDER, KeyDER, Scalar) then
raise Exception.Create('certificate and SM2 private key do not match');
FillChar(Pointer(Scalar)^, Length(Scalar), 0);
end;
Aquele comentário registra um defeito real do trabalho de integração. Quando a mesma variável AnsiString era passada como entrada Const e saída Out, o compilador finalizava a variável Out na entrada da rotina, o input ficava vazio antes de a rotina lê-lo, e a checagem de identidade falhava com uma chave perfeitamente boa. Qualquer API que recebe uma chave como Const e devolve uma chave derivada como Out precisa de variáveis separadas no lado do chamador
FAQ
O PDFlibPas precisa de Windows CNG ou de uma DLL externa para SM2, SM3 e SM4?
Não. O PDFlibPas implementa os três algoritmos em Object Pascal em PDFlibDigest, PDFlibSM4 e PDFlibSM2, e as assinaturas SM2 são construídas e verificadas pelo código CMS da biblioteca. A Windows CryptoAPI e a CNG não participam da produção da assinatura SM2
Posso usar SM2 com SHA-256 em vez de SM3?
Não. Quando um processo de assinatura carrega uma identidade SM2, o construtor de CMS do PDFlibPas sempre usa SM3, porque a GB/T 32918.2 define assinaturas SM2 sobre um digest SM3 que inclui o hash de identidade ZA. Definir o digest como SHA-256 em um processo de assinatura SM2 não muda o resultado
O Adobe Acrobat abre um PDF criptografado com SM4 pelo PDFlibPas?
Não espere que abra. O crypt filter /CFM /SM4 está fora dos valores definidos pela ISO 32000, então leitores mainstream não o reconhecem. Use criptografia SM4 para sistemas fechados que leem arquivos com o PDFlibPas ou uma implementação compatível, e AES-256 para documentos destinados à distribuição geral
Assinatura SM2, hashing SM3, criptografia SM4 e o conjunto completo de APIs de assinatura PAdES vêm com o PDFlibPas, a Delphi PDF Library, que compila com Delphi, C++Builder e FPC/Lazarus em alvos Windows e Unix