PDFlibPas, la biblioteca PDF para Delphi y C++Builder, implementa la suite de criptografía comercial china en Object Pascal puro: hashing SM3 (GB/T 32905), cifrado en bloques SM4 (GB/T 32907) y firmas de curva elíptica SM2 (GB/T 32918). Con ellas firma PDFs con firmas CMS SM2-con-SM3 mediante SetSignProcessSM2Identity y cifra documentos con un crypt filter SM4 mediante Encrypt con fuerza 6, sin proveedor del sistema operativo ni DLL externa de por medio
El motivo habitual para querer esto es un requisito de contratación, no la curiosidad. A los sistemas que se venden a gobierno, finanzas y sanidad chinos se les exige cada vez más usar los algoritmos nacionales, y Windows CryptoAPI y CNG no firman con SM2 ni hacen hash con SM3. Si el camino de firma pasa por la plataforma, la respuesta es simplemente no. Una implementación en Pascal puro da la misma respuesta en Win32, Win64 y los objetivos FPC, y la da sin una dependencia nativa movediza debajo del pipeline documental
Por qué construir la suite GB/T en Object Pascal puro
PDFlibPas construye SM2, SM3 y SM4 en Pascal porque ningún proveedor de plataforma al que ya apunta puede hacer el trabajo. CryptoAPI y CNG no tienen soporte de firmas SM3, así que antes de que existiera la identidad SM2, la biblioteca se negaba deliberadamente a firmar cuando el digest era SM3: la alternativa era un SignerInfo que declara un OID de digest y lleva una firma sobre otra cosa, que es una firma errónea silenciosa en vez de un error visible
Los tres algoritmos además tienen un orden de construcción natural, y la biblioteca lo siguió. SM3 llegó primero, porque SM2 lo necesita para el hash de identidad ZA y para el digest del mensaje. SM4 llegó segundo como unidad independiente. SM2 llegó al final, primero como primitivas en crudo verificadas contra la curva de ejemplo publicada en el apéndice de la especificación, y luego como identidad de firma conectada al mismo constructor CMS que ya maneja Ed25519, Ed448 y ML-DSA. El mismo enfoque se describe para las curvas Ed448 y Brainpool ECDSA en Pascal puro y el firmante post-cuántico FIPS 204 ML-DSA, y SM2 se enchufa a ese marco como un tipo de clave más
Las primitivas son funciones públicas, así que podéis usarlas fuera de la firma PDF. SM3String y SM3ByteArray en PDFlibDigest devuelven el digest en crudo de 32 bytes (un byte por carácter, no hex), y PDFlibSM4 exporta helpers ECB y CBC en dos sabores: versiones alineadas a bloque que devuelven un string vacío con una longitud errónea, y versiones PKCS#7 como SM4EncryptCBCPKCS7 para datos de longitud arbitraria
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
// ejemplo GB/T 32905: SM3("abc")
Digest := SM3String('abc');
Assert(LowerCase(RawToHex(Digest)) =
'66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');
// SM4 necesita una clave de 16 bytes; CBC necesita un 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;
Contad esas cadenas de 16 bytes a mano bajo vuestra responsabilidad. Dos cadenas de prueba de la suite de pruebas SM4 que se suponían de 16 bytes resultaron ser de 17, y como los helpers alineados a bloque devuelven un string vacío con una longitud errónea por diseño, el fallo se manifestó como una aserción confusa en lugar de una excepción
Firmar un PDF con una identidad SM2
Un proceso de firma de PDFlibPas usa una clave SM2 una vez que SetSignProcessSM2Identity o SetSignProcessSM2IdentityFromFile le adjuntan un certificado y una clave privada. El certificado es DER, y la clave privada puede ser un escalar en crudo de 32 bytes, un contenedor PKCS#8 sin cifrar o un ECPrivateKey SEC 1. Adjuntar una identidad SM2 limpia cualquier identidad Ed25519, Ed448, ML-DSA o Brainpool en el mismo proceso y apaga la firma por almacén de certificados y CNG, así que un proceso de firma nunca lleva dos claves competidoras
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;
Fijaos en lo que el valor devuelto de SetSignProcessSM2IdentityFromFile dice y no dice. Un 1 devuelto significa que ambos ficheros se leyeron y se guardaron en el proceso de firma. La comprobación real, derivar la clave pública desde el escalar privado y compararla con el certificado, corre cuando se aplica la firma, así que un par de claves no coincidente se muestra como un GetSignProcessResult distinto de 1, no en la llamada de identidad
El código 8 en SetSignProcessDigestAlgorithm selecciona SM3, y con una identidad SM2 es efectivamente obligatorio: el constructor CMS fuerza SM3 para una clave SM2 sin importar lo que el proceso pidió, igual que una clave Ed25519 se fuerza a SHA-512. La combinación inversa también está bloqueada. Con adbe.pkcs7.detached o ETSI.CAdES.detached, un proceso de firma puesto a SM3 que no lleva identidad SM2 sale sin firmar, porque ningún otro firmante de la biblioteca puede producir una firma basada en SM3 que case con el OID declarado
Qué entra en un SignerInfo SM2-con-SM3
Una firma SM2 en PDFlibPas se calcula sobre los atributos firmados codificados en DER, la construcción CMS estándar, pero SM2 añade un paso que ECDSA no tiene. GB/T 32918.2 hace el hash del mensaje junto con ZA, un digest de identidad definido como ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA), donde cada parte de la curva es un valor big-endian de 32 bytes e ID es el identificador distintivo del firmante. La firma es entonces r = (e + x1) mod n y s = ((1 + d)-1(k - r·d)) mod n, con un nonce k fresco del CSPRNG de la biblioteca y un reintento en rondas degeneradas
PDFlibPas usa el identificador distintivo por defecto de la especificación, el string de 16 caracteres 1234567812345678, tanto en el lado de firma como en el de verificación. No hay API para cambiarlo. Eso coincide con la enorme mayoría de certificados SM2 desplegados, pero si la contraparte firma con un ID custom, los valores ZA difieren y la firma no verificará, aunque todos los demás bytes sean correctos
El par (r, s) entra en el CMS como una SEQUENCE DER de dos valores INTEGER, la disposición que todo SignerInfo de la familia DSA usa. PDFlibPas escribe los identificadores de algoritmo registrados en GM/T 0006: 1.2.156.10197.1.301 para la clave SM2, 1.2.156.10197.1.401 para SM3 y 1.2.156.10197.1.501 para SM2-con-SM3. Las peticiones de timestamp RFC 3161 creadas con SM3 (SetTimeStampOptions digest 8) llevan el mismo OID de SM3. Las versiones anteriores a 3.589.1 escribían un arco 1.2.156.101.1 no registrado; la verificación aún lo acepta, así que los documentos firmados con esas builds siguen validando en PDFlibPas, pero los validadores de terceros no los reconocerán, razón de sobra para refirmar cualquier cosa que salga de vuestro propio pipeline
Dónde se rompió la verificación SM2: bytes de signo DER y un acarreo perdido
La mayor parte del esfuerzo de ingeniería del código SM2 fue a parar a dos defectos que producían salida plausible. Ambos son típicos de la criptografía de enteros grandes escrita a mano, y ambos son baratos de evitar una vez que los conocéis
El primero fue un byte de signo DER. Los valores INTEGER de ASN.1 son con signo, así que el codificador antepone correctamente 0x00 a cualquier r o s cuyo bit alto esté activo, dando un valor de 33 bytes. El verificador convertía cada entero a una anchura fija de 32 bytes sin quitar antes ese byte. Cerca de la mitad de todos los valores r y s tienen el bit alto activo, así que una gran fracción de documentos recién firmados fallaba la verificación. La primera pasada de sondeo resultó sacar valores cortos y pasó; la suite Delphi Win32, firmando con nonces aleatorios, expuso el problema de inmediato. El verificador ahora borra un cero inicial de cualquier entero de 33 bytes antes de rellenar a 32 bytes, y rechaza cualquier cosa más larga
El segundo fue un acarreo perdido en una suma de 256 bits. Los elementos de cuerpo son ocho limbs UInt32 little-endian, y la suma modular se escribió como «suma, luego resta p si el resultado no queda por debajo de p». Para entradas reducidas la suma queda por debajo de 2p pero aún puede superar 2256, y cuando lo hacía, el acarreo tirado plegaba el valor de vuelta a los 256 bits bajos, donde la comparación contra p decía que todo estaba bien. Duplicar un punto con una coordenada grande salía mal; los múltiplos pequeños no. El síntoma visible era que 1G a 8G casaban con la referencia mientras 9G, 10G y 11G no, y el arreglo se encontró comparando 1280 entradas y salidas de suma contra valores calculados de forma independiente y profundizando en el primer par divergente
La suma corregida en PDFlibSM2 conserva el acarreo que devuelve la suma en crudo de limbs y resta p una vez cuando el acarreo está activo o el resultado no queda por debajo de p; una sola resta basta porque p supera 2255. El patrón sirve para cualquier cuerpo primo justo por encima de una potencia de dos. Si vuestro helper de suma descarta su acarreo final, probad múltiplos bien pasado el punto donde la duplicación cruza por primera vez la frontera de palabra, no solo 1G, 2G y 3G
Por qué SM3 estuvo mal dos veces antes de estar bien
La primera implementación de SM3 fallaba todos los vectores de prueba estándar, y también lo hacía una implementación de referencia independiente escrita a su lado. Dos implementaciones que están de acuerdo entre sí y en desacuerdo con el estándar apuntan a un malentendido compartido de la especificación, no a un error de transcripción. Comparar las fórmulas línea a línea contra el texto publicado de SM3 encontró tres sitios donde el código se había escrito de memoria
- La expansión del mensaje aplica la rotación de 7 bits a W[j-13]; W[j-6] es un término XOR simple
- SS1 combina rotl(A, 12), E y la constante de ronda rotada con suma módulo 232, no XOR
- Para las rondas 16 a 63, FF es la función de mayoría (X AND Y) OR (X AND Z) OR (Y AND Z); la función de elección estilo SHA pertenece a GG
Tras esas tres correcciones el string vacío, abc y el vector de dos bloques de 64 bytes casaron todos. SM4 entró entonces al primer intento, porque la S-box y los vectores de prueba se tomaron del texto de la especificación antes de escribir código: la round key rk0 = F12186F9, el vector de bloque único 681EDF34D206965E86B3E94F536E4246, y el vector de un millón de iteraciones 595298C7C6FD271F0402F804C33D3F66. El trabajo de SM2 añadió una regla relacionada: las constantes hex largas se generan agrupando y se comprueba su longitud, nunca se teclean a mano, después de que una coordenada del punto base copiada a mano sin un dígito mandara una sesión de depuración tras bugs de implementación que no existían
Cifrar un PDF con el crypt filter SM4
TPDFlib.Encrypt con fuerza 6 cifra el documento seleccionado con SM4-128 a través del marco de crypt filters de ISO 32000-1 §7.6.5. El diccionario de cifrado usa el security handler estándar con /V 4, /R 4 y /Length 128, y el crypt filter /StdCF lleva /CFM /SM4. Guardar con fuerza 6 sube el fichero a al menos PDF 1.6, GetEncryptionLevel informa 7 para tal fichero, y el cifrado por clave pública recibe la misma opción vía EncryptPublicKey con fuerza 4
var
Pdf: TPDFlib;
Perm: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report.pdf', '') <> 1 then
raise Exception.Create('cannot load PDF');
// impresión, copia y relleno de formularios permitidos; sin ediciones ni ensamblado
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 y stream se cifra como un IV aleatorio de 16 bytes seguido de ciphertext SM4-CBC con relleno PKCS#7, la misma disposición que usa AESV2. La clave por objeto sigue el Algoritmo 1 de ISO 32000-1 §7.6.2: la clave del fichero, tres bytes bajos del número de objeto y dos bytes del número de generación, más el sufijo sAlT, con hash MD5 y truncado a 16 bytes. La derivación de contraseña a clave es la de revisión 4, así que la fuerza 6 hereda sus límites: las contraseñas no se procesan con SASLprep (solo las fuerzas 3, 4 y 5 de AES-256 lo hacen), y la derivación de clave no es más fuerte que la de AESV2
Límites de interoperabilidad que conviene conocer antes de desplegar
El resumen honesto es que la firma SM2 interopera dentro de los límites de los OIDs y el ID por defecto descritos arriba, mientras que el cifrado SM4 solo interopera con software que conoce la misma convención. /SM4 no es uno de los valores /CFM definidos en la Tabla 25 de ISO 32000-1 ni en ISO 32000-2, que definen None, V2, AESV2 y AESV3. Un lector PDF mayoritario que se encuentre /CFM /SM4 no descifrará el fichero. PDFlibPas en sí lee /SM4 y la variante /SM4V1 a través de su security handler, así que un pipeline cerrado donde PDFlibPas escribe y lee funciona; un fichero para el público general debería usar AES-256 en su lugar
Para firmas, la frontera es la disposición del certificado y el validador del otro lado. PLValidateSM2Identity, la comprobación detrás del proceso de firma, acepta la disposición GM/T 0015, donde el SubjectPublicKeyInfo lleva id-ecPublicKey con la curva SM2 1.2.156.10197.1.301 como parámetro, así como certificados que ponen el OID SM2 en el propio campo de algoritmo, y rechaza cualquier cosa cuyo punto público derivado no case con el escalar privado. Antes de confiar en firmas SM2 en producción, firmad un documento con un certificado de la CA que vais a usar de verdad y verificadlo con el validador que corre vuestra contraparte. Podéis ejecutar la comprobación de identidad vosotros mismos antes de iniciar un proceso de firma:
uses
SysUtils, PDFlibModernSign;
var
CertDER, KeyDER, Scalar: AnsiString;
begin
// CertDER y KeyDER cargados de disco o de una exportación HSM
// Mantened el parámetro Out en su propia variable: pasar KeyDER como
// entrada Const y salida Out a la vez limpiaría la entrada primero
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;
Ese comentario registra un defecto real del trabajo de integración. Cuando la misma variable AnsiString se pasó como entrada Const y salida Out, el compilador finalizó la variable Out al entrar, la entrada quedó vacía antes de que la rutina la leyera, y la comprobación de identidad falló con una clave perfectamente buena. Toda API que toma una clave como Const y devuelve una clave derivada como Out necesita variables separadas en el lado del llamador
FAQ
¿Necesita PDFlibPas Windows CNG o una DLL externa para SM2, SM3 y SM4?
No. PDFlibPas implementa los tres algoritmos en Object Pascal en PDFlibDigest, PDFlibSM4 y PDFlibSM2, y las firmas SM2 se construyen y verifican con el código CMS de la biblioteca. Windows CryptoAPI y CNG no intervienen en producir la firma SM2
¿Puedo usar SM2 con SHA-256 en lugar de SM3?
No. Cuando un proceso de firma lleva una identidad SM2, el constructor CMS de PDFlibPas usa siempre SM3, porque GB/T 32918.2 define las firmas SM2 sobre un digest SM3 que incluye el hash de identidad ZA. Poner el digest a SHA-256 en un proceso de firma SM2 no cambia el resultado
¿Abrirá Adobe Acrobat un PDF cifrado con SM4 por PDFlibPas?
No esperéis que lo haga. El crypt filter /CFM /SM4 está fuera de los valores que ISO 32000 define, así que los lectores mayoritarios no lo reconocen. Usad el cifrado SM4 para sistemas cerrados que leen ficheros con PDFlibPas o una implementación compatible, y AES-256 para documentos destinados a distribución general
La firma SM2, el hashing SM3, el cifrado SM4 y el conjunto completo de APIs de firma PAdES vienen con PDFlibPas, la Delphi PDF Library, que compila con Delphi, C++Builder y FPC/Lazarus en objetivos Windows y Unix