Article technique

Signature et chiffrement PDF SM2, SM3 et SM4 en Delphi

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

Construction du SignerInfo SM2-with-SM3 dans PDFlibPas pour Delphi : le digest couvre ZA = SM3 de ENTL, ID et des paramètres de courbe avec lID par défaut 1234567812345678, r et s suivent GB/T 32918.2 avec un nonce CSPRNG frais, et le CMS porte les OIDs GM/T 0006 1.2.156.10197.1.301, 1.2.156.10197.1.401 et 1.2.156.10197.1.501
La valeur de signature est une SEQUENCE DER de deux INTEGER sur les attributs signés ; les builds antérieurs à 3.589.1 écrivaient un arc OID non enregistré que les validateurs tiers rejettent, donc re-signez tout ce qui quitte votre 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

Défaut de vérification de signature SM2 dans PDFlibPas pour Delphi et sa correction : lencodage ASN.1 INTEGER préfixe 0x00 quand le bit de poids fort de r ou s est posé, lancien vérificateur complétait la valeur de 33 octets à 32 octets fixes sans retirer loctet de signe, et le vérificateur corrigé supprime le zéro dentête et rejette tout ce qui est plus long
Environ la moitié des valeurs r et s portent le préfixe 0x00, si bien que des sorties dapparence plausible échouaient à la vérification en masse ; des valeurs de sonde courtes ont passé la première passe jusquà ce que la suite Delphi Win32 signe avec des nonces aléatoires

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

Structure du filtre cryptographique SM4 écrite par PDFlibPas Encrypt en force 6 en Delphi : /V 4, /R 4 et /Length 128 avec le filtre /StdCF réglé sur /CFM /SM4, une clé MD5 par objet sur la clé de fichier, les numéros dobjets et de génération plus sAlT tronqués à 16 octets, et chaque flux stocké comme un IV de 16 octets suivi dun chiffré SM4-CBC avec remplissage PKCS#7
Chaînes et flux emploient la disposition doctets dAESV2 avec un cœur SM4, mais /SM4 est hors des valeurs /CFM que ISO 32000 définit, si bien que les lecteurs grand public ne peuvent pas déchiffrer et la diffusion générale devrait rester en AES-256

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