O PDFlibPas, a biblioteca PDF para Delphi e C++Builder, implementa o conjunto de criptografia comercial chinesa em Object Pascal puro: hashing SM3 (GB/T 32905), encriptação de blocos SM4 (GB/T 32907) e assinaturas em curva elíptica SM2 (GB/T 32918). Com elas assina PDFs com assinaturas CMS SM2-com-SM3 através do SetSignProcessSM2Identity e encripta documentos com um crypt filter SM4 através do Encrypt com força 6, sem provedor do sistema operativo nem DLL externa envolvidos
A razão habitual para querer isto é um requisito de contratação pública, não curiosidade. Sistemas vendidos para o governo, finanças e saúde chineses são cada vez mais esperados a usar os algoritmos nacionais, e a Windows CryptoAPI e a CNG não assinam com SM2 nem fazem hashing com SM3. Se o caminho de assinatura passa pela plataforma, a resposta é simplesmente não. Uma implementação Pascal pura dá a mesma resposta em Win32, Win64 e nos alvos FPC, e dá-la sem uma dependência nativa móvel por baixo do pipeline de documentos
Porquê construir o conjunto GB/T em Object Pascal puro?
O PDFlibPas constrói SM2, SM3 e SM4 em Pascal porque nenhum provedor de plataforma a que já dá suporte consegue fazer o trabalho. A CryptoAPI e a CNG não têm suporte de assinatura SM3, pelo que antes de existir a identidade SM2 a biblioteca recusava-se deliberadamente a assinar quando o digest estava definido como SM3: a alternativa era um SignerInfo que declara um OID de digest e transporta 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 de construção natural, e a biblioteca seguiu-a. O SM3 veio primeiro, porque o SM2 precisa dele para o hash de identidade ZA e para o digest da mensagem. O SM4 veio em segundo como unidade 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 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 liga-se a esse quadro como mais um tipo de chave
As primitivas são funções públicas, pelo que as pode usar fora da assinatura PDF. O SM3String e o SM3ByteArray em PDFlibDigest devolvem o digest bruto de 32 bytes (um byte por carácter, não hex), e o PDFlibSM4 exporta auxiliares ECB e CBC em dois sabores: versões alinhadas a blocos que devolvem uma string vazia num comprimento errado, e versões PKCS#7 como o 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
// exemplo do GB/T 32905: SM3("abc")
Digest := SM3String('abc');
Assert(LowerCase(RawToHex(Digest)) =
'66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');
// o SM4 precisa de uma chave de 16 bytes; o CBC precisa de um 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 essas strings de 16 bytes à mão por sua conta e risco. Duas strings de teste no conjunto de testes SM4 que deviam ter 16 bytes tinham afinal 17, e como os auxiliares alinhados a blocos devolvem uma string vazia para um comprimento errado por design, a falha apareceu como uma asserção confusa em vez de uma exceção
Assinar um PDF com uma identidade SM2
Um processo de assinatura do PDFlibPas usa uma chave SM2 assim que o SetSignProcessSM2Identity ou o SetSignProcessSM2IdentityFromFile lhe anexa um certificado e uma chave privada. O certificado é DER, e a chave privada pode ser um escalar bruto de 32 bytes, um contentor PKCS#8 não encriptado ou um ECPrivateKey SEC 1. Anexar uma identidade SM2 limpa qualquer identidade Ed25519, Ed448, ML-DSA ou Brainpool no mesmo processo e desliga a assinatura por arquivo de certificados e CNG, pelo que um processo de assinatura nunca carrega duas chaves concorrentes
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 devolvido pelo SetSignProcessSM2IdentityFromFile lhe diz e não lhe diz. Um devolver 1 significa que ambos os ficheiros foram lidos e guardados no processo de assinatura. A verificação real, derivar a chave pública do escalar privado e compará-la com o certificado, corre quando a assinatura é aplicada, pelo que um par de chaves desemparelhado aparece como um GetSignProcessResult diferente de 1, não na chamada de identidade
O código 8 no SetSignProcessDigestAlgorithm seleciona SM3, e com uma identidade SM2 é efetivamente obrigatório: o construtor CMS força SM3 para uma chave SM2 independentemente do que o processo pediu, da mesma forma que uma chave Ed25519 é forçada a SHA-512. A combinação inversa também é bloqueada. Com adbe.pkcs7.detached ou ETSI.CAdES.detached, um processo de assinatura definido como SM3 que não tenha uma identidade SM2 sai sem assinar, porque nenhum outro assinante da biblioteca consegue produzir uma assinatura baseada em SM3 que corresponda ao OID declarado
O que entra num SignerInfo SM2-com-SM3?
Uma assinatura SM2 no PDFlibPas é calculada 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. O GB/T 32918.2 faz o hash da mensagem juntamente 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 o ID é o identificador distintivo do assinante. 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 repetição em rondas degeneradas
O PDFlibPas usa o identificador distintivo predefinido da especificação, a string de 16 caracteres 1234567812345678, tanto no lado da assinatura como no da verificação. Não há API para o mudar. Isso coincide com a esmagadora maioria dos certificados SM2 implantados, mas se uma contraparte assinar com um ID personalizado, os valores ZA diferem e a assinatura não verificará, mesmo que todos os outros bytes estejam corretos
O par (r, s) entra no CMS como uma SEQUENCE DER de dois valores INTEGER, o esquema que todo o SignerInfo da família DSA usa. O PDFlibPas escreve os identificadores de algoritmo registados 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-com-SM3. Pedidos de selo de tempo RFC 3161 criados com SM3 (digest 8 do SetTimeStampOptions) transportam o mesmo OID SM3. Versões anteriores à 3.589.1 escreviam um arco não registado 1.2.156.101.1; a verificação ainda o aceita, pelo que documentos assinados com essas versões continuam a validar no PDFlibPas, mas validadores de terceiros não os reconhecerão, o que é uma boa razão para reassinar tudo o que sai do seu próprio pipeline
Onde a verificação SM2 rebentou: 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 inteiros grandes escrita à mão, e ambos são baratos de evitar quando se conhecem
O primeiro era um byte de sinal DER. Os valores INTEGER ASN.1 são com sinal, pelo que o codificador antepõe corretamente 0x00 a qualquer r ou s cujo bit mais alto esteja ativo, dando um valor de 33 bytes. O verificador convertia cada inteiro para uma largura fixa de 32 bytes sem retirar primeiro esse byte. Cerca de metade de todos os valores r e s tem o bit mais alto ativo, pelo que uma grande fração dos documentos recém-assinados falhava a verificação. A primeira execução de sonda calhou desenhar valores curtos e passou; a suite 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 de encher até 32 bytes, e rejeita tudo o que seja mais comprido
O segundo era um carry perdido numa adição de 256 bits. Os elementos de corpo são oito membros UInt32 little-endian, e a adição modular estava escrita como "soma, depois subtrai p se o resultado não ficar abaixo de p". Para entradas reduzidas a soma fica abaixo de 2p mas pode ainda exceder 2256, e quando acontecia, o carry largado dobrava o valor de volta para os 256 bits baixos, onde a comparação com p dizia que estava tudo bem. Dobrar um ponto com uma coordenada grande corria mal; múltiplos pequenos não. O sintoma visível era que 1G até 8G batia certo com a referência enquanto 9G, 10G e 11G não, e a correção foi encontrada comparando 1280 entradas e saídas de adição com valores calculados independentemente e mergulhando no primeiro par divergente
A adição corrigida no PDFlibSM2 mantém o carry devolvido pela adição bruta de membros e subtrai p uma vez quando o carry está ativo ou o resultado não fica abaixo de p; uma única subtração chega porque p excede 2255. O padrão aplica-se a qualquer corpo primo imediatamente acima de uma potência de dois. Se o seu auxiliar de adição descarta o carry final, teste múltiplos bem para além do ponto em que dobrar cruza pela primeira vez o limite da palavra, 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 todos os vetores de teste padrão, e o mesmo acontecia a uma implementação de referência independente escrita em paralelo. Duas implementações que concordam entre si e discordam do padrão apontam para um mal-entendido partilhado da especificação, não para um erro de transcrição. Comparar as fórmulas linha a linha com o texto SM3 publicado encontrou três lugares onde o código tinha sido escrito de memória
- A expansão da mensagem aplica a rotação de 7 bits a W[j-13]; W[j-6] é um termo XOR simples
- O SS1 combina rotl(A, 12), E e a constante de ronda rodada com adição módulo 232, não XOR
- Para as rondas 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 ao GG
Depois dessas três correções, a string vazia, abc e o vetor de dois blocos de 64 bytes todos bateram certo. O SM4 entrou depois à primeira, porque a S-box e os vetores de teste foram tirados do texto da especificação antes de qualquer código ser escrito: a chave de ronda rk0 = F12186F9, o vetor de bloco único 681EDF34D206965E86B3E94F536E4246, e o vetor de um milhão de iterações 595298C7C6FD271F0402F804C33D3F66. O trabalho SM2 acrescentou uma regra relacionada: constantes hex longas são geradas por agrupamento e verificadas em comprimento, nunca digitadas à mão, depois de uma coordenada de ponto base copiada à mão a faltar um dígito ter mandado uma sessão de depuração atrás de bugs de implementação que não existiam
Encriptar um PDF com o crypt filter SM4
O TPDFlib.Encrypt com força 6 encripta o documento selecionado com SM4-128 através do quadro de crypt filters da ISO 32000-1 §7.6.5. O dicionário de encriptação usa o gestor de segurança padrão com /V 4, /R 4 e /Length 128, e o crypt filter /StdCF transporta /CFM /SM4. Guardar com força 6 eleva o ficheiro a pelo menos PDF 1.6, o GetEncryptionLevel reporta 7 para tal ficheiro, e a encriptação por chave pública recebe a mesma opção através do EncryptPublicKey com força 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ários permitidos; sem edições nem 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 fluxo é encriptado como um IV aleatório de 16 bytes seguido de texto cifrado SM4-CBC com preenchimento PKCS#7, o mesmo esquema que o AESV2 usa. A chave por objeto segue o Algoritmo 1 da ISO 32000-1 §7.6.2: a chave do ficheiro, 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 palavra-passe para chave é a da revisão 4, pelo que a força 6 herda os seus limites: as palavras-passe não são processadas com SASLprep (só as forças AES-256 3, 4 e 5 o são), e a derivação de chave não é mais forte que a do AESV2
Limites de interoperabilidade que deve conhecer antes de implantar
O resumo honesto é que a assinatura SM2 interopera dentro dos limites dos OIDs e do ID predefinido descritos acima, enquanto a encriptação SM4 só interopera com software que conheça a mesma convenção. O /SM4 não é um dos valores /CFM definidos na Tabela 25 da ISO 32000-1 nem na ISO 32000-2, que definem None, V2, AESV2 e AESV3. Um leitor PDF mainstream que encontre /CFM /SM4 não desencriptará o ficheiro. O próprio PDFlibPas lê /SM4 e a variante /SM4V1 através do seu gestor de segurança, pelo que um pipeline fechado em que o PDFlibPas escreve e lê funciona; um ficheiro para o público geral deve usar AES-256
Para assinaturas, o limite é o esquema do certificado e o validador do outro lado. O PLValidateSM2Identity, a verificação por trás do processo de assinatura, aceita o esquema GM/T 0015, em que o SubjectPublicKeyInfo transporta id-ecPublicKey com a curva SM2 1.2.156.10197.1.301 como parâmetro, bem como certificados que põem o OID SM2 no campo de algoritmo propriamente dito, e rejeita tudo aquilo cujo ponto público derivado não corresponda ao escalar privado. Antes de confiar em assinaturas SM2 em produção, assine um documento com um certificado da CA que vai realmente usar e verifique-o com o validador que a sua contraparte corre. Pode correr a verificação de identidade você mesmo antes de começar um processo de assinatura:
uses
SysUtils, PDFlibModernSign;
var
CertDER, KeyDER, Scalar: AnsiString;
begin
// CertDER e KeyDER carregados do disco ou de uma exportação HSM
// Mantenha o parâmetro Out na sua própria variável: passar KeyDER tanto como
// entrada Const como saída Out 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;
Esse comentário regista 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 à entrada, a entrada ficava vazia antes de a rotina a ler, e a verificação 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 do lado do chamador
FAQ
O PDFlibPas precisa da 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 estão envolvidas em produzir a assinatura SM2
Posso usar SM2 com SHA-256 em vez de SM3?
Não. Quando um processo de assinatura tem uma identidade SM2, o construtor CMS do PDFlibPas usa sempre SM3, porque o GB/T 32918.2 define assinaturas SM2 sobre um digest SM3 que inclui o hash de identidade ZA. Definir o digest como SHA-256 num processo de assinatura SM2 não muda o resultado
O Adobe Acrobat abre um PDF encriptado com SM4 pelo PDFlibPas?
Não espere que o faça. O crypt filter /CFM /SM4 está fora dos valores definidos pela ISO 32000, pelo que os leitores mainstream não o reconhecem. Use encriptação SM4 para sistemas fechados que leem ficheiros com o PDFlibPas ou uma implementação compatível, e AES-256 para documentos destinados a distribuição geral
Assinatura SM2, hashing SM3, encriptação SM4 e o conjunto completo de APIs de assinatura PAdES viajam com o PDFlibPas, a Delphi PDF Library, que compila com Delphi, C++Builder e FPC/Lazarus em alvos Windows e Unix