Article technique

Certificats de test auto-signés en Delphi avec CryptoAPI

La fonction PLCreateSelfSignedCertificate de PDFlibPas construit un certificat RSA/SHA-256 auto-signé et l'exporte, clé privée incluse, directement dans un fichier PFX protégé par mot de passe, en n'utilisant que le CryptoAPI Win32 déjà installé sur chaque machine Windows. Aucun outil externe, aucune autorité de certification, aucune étape manuelle makecert ou OpenSSL : un seul appel de fonction, un certificat assez bon pour piloter un test de signature

Le scénario qui rend cette fonction utile est presque toujours un pipeline CI. Un test de fumée de signature a besoin d'un véritable PFX avec une véritable clé privée derrière lui, et en committer un dans le dépôt est son propre problème de sécurité, puisqu'une clé privée committée est une clé privée divulguée dès l'instant où ce commit atterrit. Invoquer makecert.exe ou une invocation OpenSSL depuis un script de build fonctionne aussi, mais alors le pipeline dépend d'un outil qui doit être installé, trouvé sur le PATH, et gardé cohérent en version sur chaque agent de build. Générer le certificat à l'intérieur du même processus qui exécute le test, avec les mêmes appels CryptoAPI Win32 que Windows fournit déjà, retire entièrement cette dépendance

Que produit réellement PLCreateSelfSignedCertificate ?

PLCreateSelfSignedCertificate produit un fichier PFX protégé par mot de passe portant un certificat RSA auto-signé et sa clé privée, signé avec sha256RSA, piloté par cinq paramètres : SubjectName, PFXFileName, PFXPassword, ValidDays, et KeyBits, et elle renvoie un simple drapeau de succès Boolean. SubjectName accepte une chaîne X.500 complète telle que 'CN=Alice, O=Example', et un nom nu sans signe = dedans se voit automatiquement préfixé par CN=. ValidDays en dessous de 1 retombe sur 365, et KeyBits en dehors de la plage 1024 à 16384 retombe sur 2048. PDFlibPas fournit cette fonction depuis la v3.224.0, accessible non seulement depuis l'unité Delphi mais aussi via les surfaces DLL et ActiveX, et son propre commentaire de documentation est direct sur où elle cesse d'être utile : chaque visionneuse grand public marque un certificat auto-signé comme non fiable à moins que quelqu'un ne l'installe explicitement, donc traitez ce qu'elle produit comme un certificat pour exercer un chemin de code, pas une signature à laquelle quiconque en dehors de votre équipe devrait être invité à faire confiance

var
  Success: Boolean;
begin
  Success := PLCreateSelfSignedCertificate(
    'CN=PDFlibPas CI Test, O=Example Corp',
    'ci-test-signer.pfx',
    'a-strong-throwaway-password',
    365,     // ValidDays
    2048);   // KeyBits
  if not Success then
    raise Exception.Create('Self-signed certificate generation failed');
end;

Pourquoi CryptGenKey encode-t-il la longueur de clé dans le paramètre flags ?

CryptGenKey empile deux réglages sans rapport dans un seul paramètre dwFlags. Le mot de poids faible porte des drapeaux de comportement, CRYPT_EXPORTABLE parmi eux, tandis que le mot de poids fort, pour une clé d'échange de clé RSA, porte la longueur de clé demandée en bits. Transmettre 2048 comme s'il s'agissait juste d'un autre drapeau le fait atterrir dans le mot de poids faible à la place, où il ne correspond à aucun drapeau de comportement que CryptoAPI définit, si bien que l'appel génère une clé à quelle que soit la longueur par défaut sur laquelle le fournisseur retombe plutôt que la longueur que l'appelant pensait avoir demandée. Obtenir une véritable clé RSA de 2048 bits signifie décaler d'abord le nombre dans le mot de poids fort

// Key length lives in the upper 16 bits of the CryptGenKey flags;
// the low word carries behavior flags such as CRYPT_EXPORTABLE.
if not CryptGenKey(hProv, AT_KEYEXCHANGE,
    (Cardinal(KeyBits) shl 16) or CRYPT_EXPORTABLE, hKey) then
  Exit;

Que se passe-t-il si vous oubliez CRYPT_EXPORTABLE ?

Retirez CRYPT_EXPORTABLE de cette même valeur de drapeaux et CryptGenKey réussit tout de même, mais il marque la clé privée générée comme non exportable au niveau du CSP. Tout ce qui suit en aval continue aussi de signaler un succès : CertCreateSelfSignCertificate renvoie un contexte de certificat valide, et PFXExportCertStoreEx, même appelée avec EXPORT_PRIVATE_KEYS, réussit tout de même et écrit un fichier PFX qui s'ouvre, s'analyse, et a l'air complètement ordinaire. Ce qu'il ne contient pas est la clé privée, car le CSP a refusé de la laisser sortir du conteneur de clé, et PFXExportCertStoreEx ne traite jamais ce refus comme une raison de faire échouer tout l'export

L'échec n'apparaît que plus tard, et ailleurs entièrement : un appel de signature ouvre ce PFX, trouve un certificat sans clé privée attachée, et rapporte exactement l'erreur que vous obtiendriez d'un PFX corrompu ou faux, pas d'un drapeau manquant trois couches en amont. Quiconque déboguant depuis le seul côté signature peut brûler un après-midi sur le mauvais fichier avant de réaliser que le véritable bogue est un unique bit manquant au moment de la génération de clé, dans un appel de fonction complètement différent, possiblement dans un script de build complètement différent

Pourquoi ProvType doit-il correspondre entre CryptAcquireContextW et le certificat ?

ProvType doit correspondre car CertCreateSelfSignCertificate résout la clé privée du nouveau certificat via un enregistrement CRYPT_KEY_PROV_INFO, et un champ dans cet enregistrement, ProvType, doit nommer exactement la même valeur de type CSP transmise à CryptAcquireContextW lorsque le conteneur de clé a été ouvert, PROV_RSA_AES, numériquement 24, dans l'implémentation de PDFlibPas. Réglez ProvType sur zéro, ou sur toute constante de fournisseur autre que celle à laquelle le conteneur appartient réellement, et le certificat peut tout de même être créé, mais son lien enregistré vers la clé privée ne se résout plus vers le conteneur qui la détient, ce qui fait surface plus tard comme un échec de signature ou d'export qui n'a rien à voir avec le contenu cryptographique réel du certificat

// The provider type used to open the key container must match the
// provider type recorded in the certificate's key-provider info.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, build the subject name blob, then:
KeyProvInfo.ProvType := PROV_RSA_AES;   // same constant, both call sites

Assembler le tout : du conteneur GUID au PFX protégé par mot de passe

La chaîne d'appels à l'intérieur de PLCreateSelfSignedCertificate suit une seule ligne droite, ouvrant un conteneur de clé neuf nommé d'après un GUID nouvellement généré afin que des exécutions CI concurrentes n'entrent jamais en collision sur des noms de conteneur, générant la paire de clés RSA à l'intérieur avec les deux drapeaux couverts ci-dessus, encodant SubjectName en un blob de nom X.500 via CertStrToNameW, et appelant CertCreateSelfSignCertificate avec une fenêtre de validité calculée à partir de ValidDays et transmise sous forme d'une simple structure de type SYSTEMTIME. Le contexte de certificat résultant va dans un magasin de certificats en mémoire ouvert avec CertOpenStore et CERT_STORE_PROV_MEMORY, purement afin que PFXExportCertStoreEx ait un magasin depuis lequel exporter, puisque cette API travaille contre un handle de magasin plutôt qu'un simple contexte de certificat

// Each call opens a throwaway container named after a fresh GUID:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, self-sign the certificate, export the PFX ...
// then delete the container once the PFX holds its own copy of the key:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_DELETEKEYSET);

PFXExportCertStoreEx elle-même suit la convention Win32 ordinaire en deux passes : l'appeler une fois avec un tampon de longueur zéro pour apprendre combien d'octets le PFX nécessite, allouer cette quantité, puis l'appeler à nouveau pour remplir le tampon. Une fois les octets sur disque, PDFlibPas supprime le conteneur de clé jetable avec CRYPT_DELETEKEYSET plutôt que de le laisser derrière, car le PFX porte déjà sa propre copie de chaque octet de matériel de clé que le conteneur détenait. Sautez ce nettoyage et chaque appel à PLCreateSelfSignedCertificate laisse un conteneur de clé orphelin, nommé par GUID, assis dans le profil de l'utilisateur appelant, ce qui est exactement le genre de fuite qu'un agent CI exécutant cette fonction à chaque build accumulera pendant des mois avant que quiconque ne le remarque

Un certificat auto-signé est-il sûr à utiliser pour une signature de production ?

Non : un certificat auto-signé est sûr pour exercer un chemin de code de signature et dangereux pour une signature à laquelle quiconque en dehors de l'équipe est censé faire confiance, car rien ne le rattache à une racine que le logiciel d'une partie de confiance fait déjà confiance. L'étape naturelle suivante pour un PFX construit ainsi est un véritable appel de signature, couvert dans la construction d'un atelier de conformité et de signature en Delphi avec PDFlibPas, où un PFX construit ainsi pilote la moitié signature d'un pipeline qui exécute aussi un préflight PDF/A et des audits ByteRange. La signature n'est cependant que la moitié de ce qui entoure un certificat, et l'autre moitié est précisément là où une feuille auto-signée est censée échouer : la signature et la validation PAdES en Delphi avec PDFlibPas couvre les vérifications de chaîne de confiance qu'un validateur de conformité exécute, et un validateur qui remonte la chaîne jusqu'à une racine de confiance n'a aucune raison de faire confiance à un certificat que cette fonction a inventé il y a cinq minutes à partir de rien

PLCreateSelfSignedCertificate est une fonction parmi les API de certificat et de signature de la bibliothèque PDF PDFlibPas pour Delphi et C++Builder, et elle existe précisément pour l'écart décrit ici : un test de signature ayant besoin d'une véritable paire de clés derrière lui et de rien d'externe pour en générer une