PDFlibPas, la bibliothèque PDF pour Delphi et C++Builder, implémente la suite de cryptographie commerciale chinoise en Object Pascal pur : hachage SM3 (GB/T 32905), chiffrement de blocs SM4 (GB/T 32907) et signatures sur courbes elliptiques SM2 (GB/T 32918). Avec elles, il signe des PDF par signatures CMS SM2-with-SM3 via SetSignProcessSM2Identity et chiffre des documents avec un filtre cryptographique SM4 via Encrypt en force 6, sans fournisseur du système ni DLL externe
La raison habituelle de vouloir cela est une exigence dachat, pas la curiosité. Les systèmes vendus aux secteurs gouvernementaux, financiers et sanitaires chinois sont de plus en plus tenus demployer les algorithmes nationaux, et Windows CryptoAPI et CNG ne signent ni en SM2 ni ne hachent en SM3. Si le chemin de signature passe par la plateforme, la réponse est simplement non. Une implémentation en Pascal pur donne la même réponse sur Win32, Win64 et les cibles FPC, et la donne sans une dépendance native mouvante sous le pipeline documentaire
Pourquoi bâtir la suite GB/T en Object Pascal pur ?
PDFlibPas construit SM2, SM3 et SM4 en Pascal parce quaucun fournisseur de plateforme quil cible déjà ne peut faire le travail. CryptoAPI et CNG nont aucun support de signature SM3, si bien quavant lexistence de lidentité SM2, la bibliothèque refusait délibérément de signer quand le digest était réglé sur SM3 : lalternative était un SignerInfo qui déclare un OID de digest et porte une signature sur autre chose, soit une signature fausse silencieuse plutôt quune erreur visible
Les trois algorithmes ont aussi un ordre de construction naturel, et la bibliothèque la suivi. SM3 est venu dabord, parce que SM2 en a besoin pour le hash didentité ZA et pour le digest du message. SM4 est venu en second comme unité indépendante. SM2 est venu en dernier, dabord comme primitives brutes vérifiées contre la courbe exemple publiée en annexe de la spécification, puis comme identité de signature branchée dans le même constructeur CMS qui gère déjà Ed25519, Ed448 et ML-DSA. La même approche est décrite pour les courbes ECDSA Ed448 et Brainpool en Pascal pur et le signataire post-quantique FIPS 204 ML-DSA, et SM2 se branche dans ce cadre comme un type de clé de plus
Les primitives sont des fonctions publiques, donc vous pouvez les employer hors signature PDF. SM3String et SM3ByteArray de PDFlibDigest renvoient le digest brut de 32 octets (un octet par caractère, pas hex), et PDFlibSM4 exporte des helpers ECB et CBC en deux saveurs : des versions alignées aux blocs qui renvoient une chaîne vide sur une longueur fausse, et des versions PKCS#7 telles que SM4EncryptCBCPKCS7 pour des données de longueur arbitraire
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
// exemple GB/T 32905 : SM3("abc")
Digest := SM3String('abc');
Assert(LowerCase(RawToHex(Digest)) =
'66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');
// SM4 exige une clé de 16 octets ; CBC exige un IV de 16 octets
Key := 'SixteenByteKey!!';
IV := 'InitVector16byte';
Cipher := SM4EncryptCBCPKCS7(Key, IV, 'Invoice 2026-0042 approved');
Plain := SM4DecryptCBCPKCS7(Key, IV, Cipher);
Assert(Plain = 'Invoice 2026-0042 approved');
end;
Comptez ces chaînes de 16 octets à la main à vos risques et périls. Deux chaînes de test de la suite de tests SM4 censées faire 16 octets en faisaient 17, et comme les helpers alignés aux blocs renvoient une chaîne vide pour une longueur fausse par conception, léchec est apparu comme une assertion déroutante plutôt que comme une exception
Signer un PDF avec une identité SM2
Un processus de signature PDFlibPas emploie une clé SM2 dès que SetSignProcessSM2Identity ou SetSignProcessSM2IdentityFromFile y attache un certificat et une clé privée. Le certificat est en DER, et la clé privée peut être un scalaire brut de 32 octets, un conteneur PKCS#8 non chiffré ou un ECPrivateKey SEC 1. Attacher une identité SM2 efface toute identité Ed25519, Ed448, ML-DSA ou Brainpool du même processus et coupe la signature par magasin de certificats et CNG, si bien quun processus de signature ne porte jamais deux clés concurrentes
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;
Notez ce que la valeur de retour de SetSignProcessSM2IdentityFromFile dit et ne dit pas. Un retour de 1 signifie que les deux fichiers ont été lus et stockés sur le processus de signature. La vraie vérification, dériver la clé publique du scalaire privé et la comparer au certificat, tourne quand la signature est appliquée, si bien quune paire de clés inadaptée apparaît comme un GetSignProcessResult différent de 1, pas à lappel didentité
Le code 8 de SetSignProcessDigestAlgorithm sélectionne SM3, et avec une identité SM2 il est en pratique obligatoire : le constructeur CMS force SM3 pour une clé SM2 quoi que le processus ait demandé, de la même façon quune clé Ed25519 est forcée sur SHA-512. La combinaison inverse est bloquée aussi. Avec adbe.pkcs7.detached ou ETSI.CAdES.detached, un processus de signature réglé sur SM3 qui ne détient pas didentité SM2 sort sans signer, parce quaucun autre signataire de la bibliothèque ne peut produire une signature à base de SM3 correspondant à lOID déclaré
Que contient un SignerInfo SM2-with-SM3 ?
Une signature SM2 dans PDFlibPas est calculée sur les attributs signés encodés en DER, la construction CMS standard, mais SM2 ajoute une étape que la ECDSA na pas. GB/T 32918.2 hache le message avec ZA, un digest didentité défini comme ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA), où chaque partie de courbe est une valeur big-endian de 32 octets et ID est lidentifiant distinctif du signataire. La signature est ensuite r = (e + x1) mod n et s = ((1 + d)-1(k - r·d)) mod n, avec un nonce k frais issu du CSPRNG de la bibliothèque et une relance sur les tours dégénérés
PDFlibPas emploie lidentifiant distinctif par défaut de la spécification, la chaîne de 16 caractères 1234567812345678, côté signature comme côté vérification. Il ny a pas dAPI pour le changer. Cela correspond à lécrasante majorité des certificats SM2 déployés, mais si une contrepartie signe avec un ID personnalisé, les valeurs ZA diffèrent et la signature ne vérifiera pas, même si tous les autres octets sont corrects
La paire (r, s) entre dans le CMS comme une SEQUENCE DER de deux valeurs INTEGER, la disposition quemploie tout SignerInfo de la famille DSA. PDFlibPas écrit les identifiants dalgorithmes enregistrés dans GM/T 0006 : 1.2.156.10197.1.301 pour la clé SM2, 1.2.156.10197.1.401 pour SM3 et 1.2.156.10197.1.501 pour SM2-with-SM3. Les demandes dhorodatage RFC 3161 créées avec SM3 (digest 8 de SetTimeStampOptions) portent le même OID SM3. Les versions antérieures à 3.589.1 écrivaient un arc non enregistré 1.2.156.101.1 ; la vérification laccepte toujours, si bien que les documents signés par ces builds continuent de valider dans PDFlibPas, mais les validateurs tiers ne les reconnaîtront pas, de quoi re-signer tout ce qui quitte votre propre pipeline
Où la vérification SM2 a cassé : octets de signe DER et une retenue perdue
Lessentiel de leffort dingénierie du code SM2 est allé dans deux défauts qui produisaient une sortie plausible. Tous deux sont typiques de la cryptographie à grands entiers écrite à la main, et tous deux sont bon marché à éviter une fois connus
Le premier était un octet de signe DER. Les valeurs INTEGER ASN.1 sont signées, donc lencodeur préfixe correctement 0x00 à tout r ou s dont le bit de poids fort est posé, donnant une valeur de 33 octets. Le vérificateur convertissait chaque entier à une largeur fixe de 32 octets sans retirer dabord cet octet. Environ la moitié des valeurs r et s ont le bit de poids fort posé, si bien quune grande fraction des documents fraîchement signés échouait à la vérification. La première passe de sonde est tombée par hasard sur des valeurs courtes et a passé ; la suite Delphi Win32, signant avec des nonces aléatoires, a exposé le problème immédiatement. Le vérificateur supprime désormais un zéro dentête de tout entier de 33 octets avant de compléter à 32 octets, et rejette tout ce qui est plus long
Le second était une retenue perdue dans une addition 256 bits. Les éléments de corps sont huit membres UInt32 little-endian, et laddition modulaire était écrite "additionner, puis soustraire p si le résultat nest pas en dessous de p". Pour des entrées réduites la somme est sous 2p mais peut encore dépasser 2256, et quand cest arrivé, la retenue perdue repliait la valeur dans les 256 bits bas, où la comparaison contre p disait que tout allait bien. Doubler un point avec une grande coordonnée tournait mal ; les petits multiples non. Le symptôme visible était que 1G à 8G correspondaient à la référence alors que 9G, 10G et 11G non, et la correction a été trouvée en comparant 1280 entrées et sorties daddition à des valeurs calculées indépendamment puis en creusant la première paire divergente
Laddition corrigée de PDFlibSM2 garde la retenue renvoyée par laddition brute des membres et soustrait p une fois quand la retenue est posée ou que le résultat nest pas en dessous de p ; une seule soustraction suffit parce que p dépasse 2255. Le schéma sapplique à tout corps premier juste au-dessus dune puissance de deux. Si votre helper daddition jette sa retenue finale, testez des multiples bien au-delà du point où le doublement franchit pour la première fois la frontière de mot, pas seulement 1G, 2G et 3G
Pourquoi SM3 a été faux deux fois avant dêtre juste
La première implémentation SM3 échouait sur chaque vecteur de test standard, et une implémentation de référence indépendante écrite à côté aussi. Deux implémentations qui sentendent entre elles et divergent de la norme pointent un malentendu partagé de la spécification, pas une erreur de transcription. Comparer les formules ligne à ligne au texte SM3 publié a trouvé trois endroits où le code avait été écrit de mémoire
- Lexpansion du message applique la rotation de 7 bits à W[j-13] ; W[j-6] est un simple terme XOR
- SS1 combine rotl(A, 12), E et la constante de tour tournée avec une addition modulo 232, pas un XOR
- Pour les tours 16 à 63, FF est la fonction majorité (X AND Y) OR (X AND Z) OR (Y AND Z) ; la fonction de choix style SHA appartient à GG
Après ces trois corrections, la chaîne vide, abc et le vecteur à deux blocs de 64 octets correspondaient tous. SM4 est ensuite entré du premier coup, parce que la S-box et les vecteurs de test avaient été pris du texte de la spécification avant décriture du moindre code : clé de tour rk0 = F12186F9, le vecteur à bloc unique 681EDF34D206965E86B3E94F536E4246, et le vecteur à un million ditérations 595298C7C6FD271F0402F804C33D3F66. Le travail SM2 a ajouté une règle voisine : les constantes hex longues sont générées par groupement et vérifiées en longueur, jamais tapées à la main, après quune coordonnée de point de base copiée à la main et privée dun chiffre a envoyé une session de débogage après des bugs dimplémentation qui nexistent pas
Chiffrer un PDF avec le filtre cryptographique SM4
TPDFlib.Encrypt en force 6 chiffre le document sélectionné avec SM4-128 via le cadre de filtres cryptographiques dISO 32000-1 §7.6.5. Le dictionnaire de chiffrement emploie le gestionnaire de sécurité standard avec /V 4, /R 4 et /Length 128, et le filtre cryptographique /StdCF porte /CFM /SM4. Sauvegarder en force 6 élève le fichier au moins à PDF 1.6, GetEncryptionLevel rapporte 7 pour un tel fichier, et le chiffrement à clé publique obtient la même option via EncryptPublicKey en force 4
var
Pdf: TPDFlib;
Perm: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report.pdf', '') <> 1 then
raise Exception.Create('cannot load PDF');
// impression, copie et remplissage de formulaires autorisés ; pas dédition ni dassemblage
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;
Chaque chaîne et flux est chiffré comme un IV aléatoire de 16 octets suivi dun chiffré SM4-CBC avec remplissage PKCS#7, la même disposition quemploie AESV2. La clé par objet suit lAlgorithme 1 dISO 32000-1 §7.6.2 : la clé de fichier, les trois octets bas du numéro dobjets et deux octets du numéro de génération, plus le suffixe sAlT, hachés en MD5 et tronqués à 16 octets. La dérivation de clé depuis mot de passe est celle de la révision 4, si bien que la force 6 hérite de ses limites : les mots de passe ne sont pas traités SASLprep (seules les forces AES-256 3, 4 et 5 le sont), et la dérivation de clé nest pas plus forte que celle dAESV2
Limites dinteropérabilité à connaître avant de déployer
Le résumé honnête est que la signature SM2 interopère dans les limites des OIDs et de lID par défaut décrits ci-dessus, tandis que le chiffrement SM4 ninteropère quavec les logiciels qui connaissent la même convention. /SM4 ne figure pas parmi les valeurs /CFM définies dans la Table 25 dISO 32000-1 ni dans ISO 32000-2, qui définissent None, V2, AESV2 et AESV3. Un lecteur PDF grand public qui rencontre /CFM /SM4 ne déchiffrera pas le fichier. PDFlibPas lui-même lit /SM4 et la variante /SM4V1 via son gestionnaire de sécurité, si bien quun pipeline fermé où PDFlibPas écrit et lit fonctionne ; un fichier pour le grand public devrait employer AES-256 à la place
Pour les signatures, la frontière est la disposition du certificat et le validateur de lautre côté. PLValidateSM2Identity, la vérification derrière le processus de signature, accepte la disposition GM/T 0015, où le SubjectPublicKeyInfo porte id-ecPublicKey avec la courbe SM2 1.2.156.10197.1.301 comme paramètre, ainsi que les certificats qui placent lOID SM2 dans le champ dalgorithme lui-même, et elle rejette tout ce dont le point public dérivé ne correspond pas au scalaire privé. Avant de compter sur des signatures SM2 en production, signez un document avec un certificat de lAC que vous utiliserez réellement et vérifiez-le avec le validateur que votre contrepartie fait tourner. Vous pouvez lancer la vérification didentité vous-même avant de démarrer un processus de signature :
uses
SysUtils, PDFlibModernSign;
var
CertDER, KeyDER, Scalar: AnsiString;
begin
// CertDER et KeyDER chargés depuis le disque ou un export HSM
// Gardez le paramètre Out dans sa propre variable : passer KeyDER
// à la fois en entrée Const et en sortie Out viderait lentrée dabord
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;
Ce commentaire enregistre un vrai défaut du travail dintégration. Quand la même variable AnsiString était passée comme entrée Const et sortie Out, le compilateur finalisait la variable Out à lentrée, lentrée devenait vide avant que la routine ne la lise, et la vérification didentité échouait sur une clé parfaitement bonne. Toute API qui prend une clé en Const et renvoie une clé dérivée en Out exige des variables séparées côté appelant
FAQ
PDFlibPas a-t-il besoin de Windows CNG ou dune DLL externe pour SM2, SM3 et SM4 ?
Non. PDFlibPas implémente les trois algorithmes en Object Pascal dans PDFlibDigest, PDFlibSM4 et PDFlibSM2, et les signatures SM2 sont construites et vérifiées par le code CMS de la bibliothèque. Windows CryptoAPI et CNG ne participent pas à la production de la signature SM2
Peut-on employer SM2 avec SHA-256 au lieu de SM3 ?
Non. Quand un processus de signature détient une identité SM2, le constructeur CMS de PDFlibPas emploie toujours SM3, parce que GB/T 32918.2 définit les signatures SM2 sur un digest SM3 qui inclut le hash didentité ZA. Régler le digest sur SHA-256 dans un processus de signature SM2 ne change pas le résultat
Adobe Acrobat ouvrira-t-il un PDF chiffré en SM4 par PDFlibPas ?
Ne comptez pas dessus. Le filtre cryptographique /CFM /SM4 est hors des valeurs définies par ISO 32000, donc les lecteurs grand public ne le reconnaissent pas. Employez le chiffrement SM4 pour des systèmes fermés qui lisent les fichiers avec PDFlibPas ou une implémentation compatible, et AES-256 pour des documents destinés à une diffusion générale
Signature SM2, hachage SM3, chiffrement SM4 et lensemble complet des API de signature PAdES sont livrés avec PDFlibPas, la Delphi PDF Library, qui compile avec Delphi, C++Builder et FPC/Lazarus sur des cibles Windows et Unix