PDFlibPas, la biblioteca PDF para Delphi y C++Builder, implementa la suite china de criptografía comercial 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 a través de SetSignProcessSM2Identity y cifra documentos con un crypt filter SM4 a través de Encrypt con fuerza 6, sin proveedor del sistema operativo ni DLL externa de por medio
La razón habitual para querer esto es un requisito de adquisición, no curiosidad. De los sistemas que se venden al gobierno, las finanzas y la salud de China se espera cada vez más que usen los algoritmos nacionales, y Windows CryptoAPI y CNG no firman con SM2 ni hacen hashing con SM3. Si la ruta 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 destinos FPC, y la da sin una dependencia nativa movediza debajo del pipeline de documentos
¿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 a los que ya apunta puede hacer el trabajo. CryptoAPI y CNG no tienen soporte de firma SM3, así que antes de que existiera la identidad SM2, la biblioteca se negaba deliberadamente a firmar cuando el digest estaba fijado a SM3: la alternativa era un SignerInfo que declara un OID de digest y lleva una firma sobre otra cosa, que es una firma incorrecta silenciosa en lugar de un error visible
Los tres algoritmos también tienen un orden natural de construcción, y la biblioteca lo siguió. SM3 vino primero, porque SM2 lo necesita para el hash de identidad ZA y para el digest del mensaje. SM4 vino segundo como unidad independiente. SM2 vino al final, primero como primitivas crudas verificadas contra la curva de ejemplo publicada en el apéndice de la especificación, luego como identidad de firma conectada al mismo constructor de CMS que ya maneja Ed25519, Ed448 y ML-DSA. El mismo enfoque se describe para las curvas ECDSA Ed448 y Brainpool en Pascal puro y el firmante post-cuántico ML-DSA de FIPS 204, y SM2 se enchufa a ese marco como un tipo de clave más
Las primitivas son funciones públicas, así que puede usarlas fuera de la firma de PDFs. SM3String y SM3ByteArray en PDFlibDigest devuelven el digest 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 una cadena vacía ante un largo incorrecto, y versiones PKCS#7 como SM4EncryptCBCPKCS7 para datos de largo arbitrario
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
// muestra 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;
Contar esas cadenas de 16 bytes a mano lo pone en riesgo. Dos cadenas de prueba en la suite de pruebas de SM4 que debían ser de 16 bytes resultaron ser de 17, y como los helpers alineados a bloque devuelven una cadena vacía ante un largo incorrecto por diseño, la falla apareció como un assertion confuso 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 adjunta un certificado y una clave privada. El certificado es DER, y la clave privada puede ser un escalar 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 del mismo proceso y apaga la firma por almacén de certificados y CNG, así que un proceso de firma nunca lleva dos claves en competencia
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 lo que el valor de retorno de SetSignProcessSM2IdentityFromFile le dice y lo que no. Un retorno de 1 significa que ambos archivos fueron leídos y guardados en el proceso de firma. La verificación real, derivar la clave pública desde el escalar privado y compararla con el certificado, corre cuando la firma se aplica, así que un par de claves que no coincide aparece 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 de 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 fijado a SM3 que no lleve una identidad SM2 sale sin firmar, porque ningún otro firmante de la biblioteca puede producir una firma basada en SM3 que coincida 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 agrega un paso que ECDSA no tiene. GB/T 32918.2 aplica hash al 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 y 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, la cadena 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 abrumadora mayoría de los certificados SM2 desplegados, pero si una contraparte firma con un ID personalizado, los valores de ZA difieren y la firma no verificará, aunque todos los demás bytes estén correctos
El par (r, s) entra al CMS como una SEQUENCE DER de dos valores INTEGER, el diseño 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 solicitudes de marca de tiempo 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 no registrado 1.2.156.101.1; la verificación aún lo acepta, así que los documentos firmados con esas compilaciones siguen validando en PDFlibPas, pero los validadores de terceros no los reconocerán, lo cual es una buena razón para volver a firmar cualquier cosa que salga de su 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 en el 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 conoce
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 superior esté activo, dando un valor de 33 bytes. El verificador convertía cada entero a un ancho fijo de 32 bytes sin quitar ese byte primero. Cerca de la mitad de todos los valores r y s tienen el bit superior activo, así que una gran fracción de los documentos recién firmados fallaba la verificación. La primera corrida de sondeo por casualidad sacó valores cortos y pasó; la suite de Delphi Win32, firmando con nonces aleatorios, expuso el problema de inmediato. El verificador ahora elimina 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 campo son ocho limbs UInt32 little-endian, y la suma modular estaba escrita como "suma, y 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 exceder 2256, y cuando lo hacía, el acarreo descartado doblaba el valor de vuelta a los 256 bits bajos, donde la comparación contra p decía que todo estaba bien. Doblar un punto con una coordenada grande salía mal; los múltiplos pequeños no. El síntoma visible era que 1G a 8G coincidían con la referencia mientras que 9G, 10G y 11G no, y la corrección 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 cruda 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 excede 2255. El patrón aplica a cualquier campo primo justo encima de una potencia de dos. Si su helper de suma descarta el acarreo final, pruebe múltiplos bien más allá del punto donde el doblado cruza por primera vez el límite 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 en paralelo. Dos implementaciones que coinciden entre sí y discrepan del estándar apuntan a un malentendido compartido de la especificación, no a un error de transcripción. Comparar las fórmulas línea por línea contra el texto publicado de SM3 encontró tres lugares 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
Después de esas tres correcciones la cadena vacía, abc y el vector de dos bloques de 64 bytes coincidieron todos. SM4 entró 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 alguno: la clave de ronda rk0 = F12186F9, el vector de un bloque 681EDF34D206965E86B3E94F536E4246, y el vector de un millón de iteraciones 595298C7C6FD271F0402F804C33D3F66. El trabajo de SM2 agregó una regla relacionada: las constantes hex largas se generan agrupando y se verifica su largo, nunca se teclean a mano, después de que una coordenada del punto base copiada a mano a la que le faltaba un dígito mandó 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 eleva el archivo a al menos PDF 1.6, GetEncryptionLevel reporta 7 para tal archivo, y el cifrado de clave pública obtiene la misma opción a través de 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 llenado 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 cadena y flujo se cifra como un IV aleatorio de 16 bytes seguido de texto cifrado SM4-CBC con relleno PKCS#7, el mismo diseño que usa AESV2. La clave por objeto sigue el Algoritmo 1 de ISO 32000-1 §7.6.2: la clave del archivo, los tres bytes bajos del número de objeto y dos bytes del número de generación, más el sufijo sAlT, todo con hash MD5 y truncado a 16 bytes. La derivación de contraseña a clave es la de la revisión 4, así que la fuerza 6 hereda sus límites: las contraseñas no se procesan con SASLprep (solo las fuerzas AES-256 3, 4 y 5 lo hacen), y la derivación de clave no es más fuerte que la de AESV2
Límites de interoperabilidad que debe conocer antes de desplegar
El resumen honesto es que la firma SM2 interoperar dentro de los límites de los OIDs y el ID por defecto descritos arriba, mientras que el cifrado SM4 solo interoperar con software que conozca 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 convencional que se encuentre con /CFM /SM4 no descifrará el archivo. PDFlibPas mismo 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 archivo para el público general debería usar AES-256 en su lugar
Para firmas, el límite es el diseño del certificado y el validador del otro lado. PLValidateSM2Identity, la verificación detrás del proceso de firma, acepta el diseño 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 de SM2 en el campo de algoritmo mismo, y rechaza cualquier cosa cuyo punto público derivado no coincida con el escalar privado. Antes de confiar en firmas SM2 en producción, firme un documento con un certificado de la CA que realmente vaya a usar y verifíquelo con el validador que corre su contraparte. Puede ejecutar la verificación de identidad usted mismo antes de iniciar un proceso de firma:
uses
SysUtils, PDFlibModernSign;
var
CertDER, KeyDER, Scalar: AnsiString;
begin
// CertDER y KeyDER cargados desde disco o una exportación de HSM
// Mantenga el parámetro Out en su propia variable: pasar KeyDER como
// entrada Const y salida Out a la vez borrarí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 pasaba como entrada Const y salida Out, el compilador finalizaba la variable Out al entrar, la entrada quedaba vacía antes de que la rutina la leyera, y la verificación de identidad fallaba con una clave perfectamente buena. Toda API que tome una clave como Const y devuelva una clave derivada como Out necesita variables separadas del 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 de CMS de PDFlibPas siempre usa SM3, porque GB/T 32918.2 define las firmas SM2 sobre un digest SM3 que incluye el hash de identidad ZA. Fijar 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 espere que lo haga. El crypt filter /CFM /SM4 está fuera de los valores definidos por ISO 32000, así que los lectores convencionales no lo reconocen. Use el cifrado SM4 para sistemas cerrados que leen archivos 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 destinos Windows y Unix