Avant-propos▲
Ce document a pour but de présenter le principe de fonctionnement de l'API Windows concernant la cryptographie en prenant l'exemple de la signature de données.
Le code a été écrit et testé avec Borland C++ Builder 6 Enterprise anglais.
Bien entendu, toute remarque constructive est la bienvenue.
Pour une description détaillée des fonctions et valeurs permises pour les paramètres, tout se trouve dans MSDN , rubrique Security / Cryptography. La description y est exhaustive, mais parfois complexe.
Les termes 'clé publique', clé privée', 'Certificat' ,'Autorité de Certification' doivent idéalement vous être, si pas familiers, du moins connus. Si ce n'est pas le cas, je vous invite à vous documenter sur le sujet.
En très résumé, mais de façon à pouvoir comprendre la suite du tutoriel.
- La clé privée est celle que vous gardez précieusement.
- La clé publique est celle que vous pouvez distribuer.
- L'Autorité de Certification est un organisme, une société, une personne, etc. à qui l'on accorde sa confiance pour certifier que la personne, la société est bien qui elle prétend être.
- un Certificat est une clé publique, certifiée par une Autorité de Certification.
La clé que j'ai utilisée pour ces tests provient de TrustCenter. Cette autorité de certification offre des clés valables pour une durée d'un an, gratuitement, à des fins d'évaluation. Il en existe d'autres.
La paire de clés doit être enregistrée dans le magasin de certificats de Windows. Ce magasin est consultable dans Internet Explorer / Outils / Options Internet / Contenu / Certificats. Lorsque vous sélectionnez un élément de la liste 'Personnel', et que vous cliquez sur le bouton Affichage, dans la fenêtre des propriétés qui apparaît, l'information 'Vous avez une clé privée qui correspond à ce certificat' doit être présente. (cf. la zone en rouge dans la capture d'écran ci-dessous.)
I. Principe▲
La signature de données, dans son principe, est très simple : c'est la création d'un 'marquage' à partir des données et de la clé privée. Le moindre changement des données génère un marquage tout à fait différent. Ce qui permet de garantir que les données transmises n'ont pas été altérées. La signature permet aussi d'authentifier la personne qui envoie les données.
Le contrôle de la signature, est, quant à lui effectué grâce à la clé publique. Il permet de vérifier que la signature a bien été faite par la clé privée qui lui correspond, mais ne permet en aucun cas de régénérer cette signature. Seul le possesseur de la clé privée peut le faire.
La signature peut être liée aux données, ou séparée de ces données. On parle dans ce second cas, de signature détachée.
II. Préparation du projet▲
La première chose est de lancer en ligne de commande, dans le répertoire System de Windows la commande suivante IMPLIB crypt32.lib crpt32.dll pour créer une librairie d'importation. Pour une application à mettre en production, le mieux serait de faire un chargement dynamique des fonctions dont on aura besoin, car la version de cette DLL varie entre les différents Windows, mais ceci sort du cadre de ce tutoriel.
Après avoir créé un nouveau répertoire pour le projet, on y recopiera ce fichier crypt32.lib.
Il faut ensuite créer un nouveau projet et lui ajouter la librairie. Ajoutez la ligne suivante, avant le premier include.
#define CRYPT_SIGN_MESSAGE_PARA_HAS_CMS_FIELDS
Les deux defines suivant nous faciliteront la tâche. Le premier correspond aux types d'encodage des certificats reconnus par l'API. Le second est le nom du magasin de certificats dans lequel les clés se trouvent. Il s'agit ici d'un magasin de certificats système, le plus couramment utilisé.
#define CRYPT_TYPE (PKCS_7_ASN_ENCODING | X509_ASN_ENCODING)
#define CERT_STORE_NAME L
"MY"
Le projet de test que l'ai utilisé est téléchargeable ici.
III. Principe▲
Il faut d'abord ouvrir le magasin de certificat.
Ensuite, il faut obtenir un handle sur la clé privée.
Il faut ensuite signer les données et récupérer la signature ainsi créée.
Un point très important est qu'à aucun moment, on ne manipule directement la clé. L'utilisation de la clé se fait toujours à l'aide de 'handle' sur les clés.
III-A. Ouvrir le magasin de certificats▲
HCERTSTORE hCertStore;
hCertStore =
CertOpenStore(CERT_STORE_PROV_SYSTEM, 0
, NULL
, CERT_SYSTEM_STORE_CURRENT_USER, CERT_STORE_NAME);
CertOpenStore renvoie une structure de type HCERTSTORE en cas de succès, NULL sinon.
Les paramètres sont :
- le type de magasin ;
- le type d'encodage des certificats : 0, car inutilisé dans ce cas particulier ;
- un 'cryptography provider' : NULL demande d'en créer un par défaut, qui est l'option recommandée ;
- des flags de configuration : nous précisons ici que nous allons travailler dans le magasin de l' utilisateur courant ;
- le dernier paramètre est le nom du magasin de certificats utilisé ( MY ).
Après la fin de l'utilisation du handle hCertStore, il ne faut pas oublier de le libérer :
CertCloseStore(hCertStore, CERT_CLOSE_STORE_CHECK_FLAG);
III-B. Obtenir un handle sur la clé privée▲
PCCERT_CONTEXT CertSignerContext ;
AnsiString aKey;
aKey =
"LFE@home.be"
;
CertSignerContext =
CertFindCertificateInStore(hCertStore,
CRYPT_TYPE,
0
,
CERT_FIND_SUBJECT_STR,
WideString(aKey.c_str()),
NULL
);
CertFondCertificateInStore renvoie une structure de type PCCERT_CONTEXT en cas de succès, NULL sinon.
Les paramètres sont :
- le handle du magasin de certificats dans lequel la recherche va se faire ;
- le type d'encodage des certificats ;
- 0 dans la plupart des cas, ce paramètre permet de configurer des options en fonction du type de recherche ;
- le type de recherche que l'on va effectuer ( nous allons chercher le certificat sur la base du Nom repris dans le sujet du certificat ;
- le critère de recherche (ici, le nom repris dans le champ certificat) ;
- le dernier paramètre permet d'effectuer la recherche à partir d'un certificat donné.
NULL indique que c'est le début de la recherche. Ce paramètre est utile quand on recherche un certificat sur base d'un critère qui n'est pas un identifiant unique.
Dans un but de simplicité, nous supposerons que le certificat identifié par LFE@home.be est unique et possède une clé privée associée.
Après utilisation de cette structure, il faut la libérer par :
CertFreeCertificateContext(CertSignerContext);
III-C. Signer les données▲
Pour signer des données, il faut :
- initialiser une structure contenant les paramètres de la signature ;
- appeler une première fois la fonction de signature pour déterminer la longueur du buffer qui contiendra les données après signature ;
- appeler une seconde fois la fonction de signature pour effectuer la signature proprement dite.
III-C-1. Signature jointe, certificat joint▲
CRYPT_SIGN_MESSAGE_PARA SignMsgParam;
BYTE*
pbToBeSigned[1
];
DWORD cbToBeSigned[1
];
bool
lOk;
memset(&
SignMsgParam, 0x00
, sizeof
(CRYPT_SIGN_MESSAGE_PARA));
SignMsgParam.cbSize =
sizeof
(CRYPT_SIGN_MESSAGE_PARA);
SignMsgParam.dwMsgEncodingType =
CRYPT_TYPE;
SignMsgParam.HashAlgorithm.pszObjId =
szOID_RSA_SHA1RSA;
SignMsgParam.pSigningCert =
CertSignerContext;
SignMsgParam.cMsgCert =
1
;
SignMsgParam.rgpMsgCert =
&
CertSignerContext;
pbToBeSigned[0
] =
(char
*
) malloc(Edit1->
Text.Length() +
1
);
strcpy(pbToBeSigned[0
], Edit1->
Text.c_str());
cbToBeSigned[0
] =
Edit1->
Text.Length();
// Get The output buffer size
lOk =
CryptSignMessage(&
SignMsgParam,
false
,
1
,
(const
BYTE **
)rgpbToBeSigned,
rgcbToBeSigned,
NULL
,
&
cbSignedMsgBlob);
cSignedMsg =
(char
*
)malloc(cbSignedMsgBlob);
memset(cSignedMsg,0x00
,cbSignedMsgBlob);
// Sign the Message
lOk =
CryptSignMessage(&
SignMsgParam,
false
,
1
,
(const
BYTE **
)rgpbToBeSigned,
rgcbToBeSigned,
cSignedMsg,
&
cbSignedMsgBlob);
La première étape est de déclarer et d'initialiser une structure de type CRYPT_SIGN_MESSAGE.
- cbSize est la taille de la structure.
- dwMsgEncondingType est le type d'encodage des certificats.
- HashAlgortihm est le type de hashage utilisé pour créer la signature.
- pSigningCert est le handle du certificat utilisé pour signer les données.
- cMsgCert est le nombre d'éléments repris dans le paramètre rgpMsgCert.
- rgpMsgCert est un tableau de pointeurs sur des structures de type PCCERT_CONTEXT. Il s'agit du (des) certificat(s) à joindre aux données.
Il existe d'autres éléments dans cette structure, mais ils ne sont pas utilisés dans ce cas-ci.
Dans l'exemple présenté ici, nous ne joindrons que le certificat de la personne qui signe le message.
Pour la seconde étape, il faut savoir que lors de la signature, on peut, en une seule manipulation, signer plusieurs buffers de données. Pour cela, il faut remplir deux tableaux :
- pbToBeSigned, contient les pointeurs sur les buffers de données à signer ;
- cbToBeSigned, contient les longueurs de ces buffers.
CryptSignMessage effectue la signature proprement dite. Cette fonction renvoie true en cas de succès, false, sinon.
Les paramètres de la fonction sont :
- un pointeur sur la structure de type CRYPT_SIGN_MESSAGE ;
- un booléen qui vaut true si la signature est détachée, false sinon ;
- le nombre d'éléments des deux tableaux contenant respectivement les pointeurs sur les buffers à signer, et les longueurs de ces buffers ;
- le tableau pbToBeSigned ;
- le tableau cbToBeSigned ;
- le pointeur sur le buffer recevant la signature ;
- la longueur des données signées.
Les deux appels successifs de cryptSignMessage s'expliquent par le fait qu'il faut connaître la longueur de la signature, pour pouvoir allouer le buffer :
- le premier appel de la fonction avec NULL comme pointeur de retour fait effectuer le calcul de la longueur ;
- le second appel, avec un pointeur non NULL, signe les données.
En cas d'échec de la signature, en plus de la valeur de retour à false, le pointeur de retour pointe sur une chaîne vide, et la longueur des données est 0.
En complément de la valeur de retour, la fonction GetLastError() permet de récupérer un statut d'erreur un tout petit peu plus explicite.
III-C-2. Signature jointe, certificat non joint▲
Pour ne pas joindre de certificat, il faut juste modifier ces deux lignes de code :
SignMsgParam.cMsgCert =
0
;
SignMsgParam.rgpMsgCert =
NULL
;
III-C-3. Signature détachée, certificat joint▲
Pour détacher la signature, l'appel à CryptSignMessage se fait de la façon suivante :
lOk =
CryptSignMessage(&
SignMsgParam,
true
,
1
,
(const
BYTE **
)rgpbToBeSigned,
rgcbToBeSigned,
NULL
,
&
cbSignedMsgBlob);
et
lOk =
CryptSignMessage(&
SignMsgParam,
true
,
1
,
(const
BYTE **
)rgpbToBeSigned,
rgcbToBeSigned,
cSignedMsg,
&
cbSignedMsgBlob);
Il est important que les deux fonctions soient appelées de la même façon, pour que le calcul de longueur soit toujours correct.
III-C-4. Signature détachée, certificat non joint▲
Pour cela, il faut combiner les deux points précédents.
III-D. Récupérer la signature▲
Nous voilà donc avec un buffer contenant les données signées. Cette signature est une suite de données binaires, avec une longueur associée.
On peut soit les écrire telles quelles dans un fichier, soit les encoder en Base64 dans le but de les envoyer par mail, etc.
IV. Vérifier la signature▲
À nouveau, il faut ouvrir le magasin de certificat.
Cette fois, il ne faut pas obtenir de handle sur le certificat.
Il y a deux cas :
- le certificat est joint à la signature, il suffit alors de contrôler que le message est bien conforme ;
- le certificat n'est pas joint. Dans ce cas, il faut spécifier la façon de le retrouver. Pour des raisons de simplicité, nous supposerons que le certificat est déjà dans le magasin de certificat de l'utilisateur.
IV-A. Vérifier une signature jointe, certificat joint▲
CRYPT_VERIFY_MESSAGE_PARA VerifParams;
DWORD cbSignedData, cbData;
char
*
pbSignedData, *
pbData;
// récupérer les données signées et la longueur de ces données
memset(&
VerifParams, 0x00
, sizeof
(CRYPT_VERIFY_MESSAGE_PARA));
VerifParams.cbSize =
sizeof
(CRYPT_VERIFY_MESSAGE_PARA);
VerifParams.dwMsgAndCertEncodingType =
CRYPT_TYPE;
// Get The output buffer size
lOk =
CryptVerifyMessageSignature(&
VerifParams,
0
,
pbSignedData,
cbSignedData,
NULL
,
&
cbData,
NULL
);
pbData =
(char
*
)malloc(cbData);
memset(pbData, 0x00
, cbData);
// Verify the Message Signature
lOk =
CryptVerifyMessageSignature(&
VerifParams,
0
,
pbSignedData,
cbSgnedData,
pbData,
&
cbData,
NULL
);
IV-B. Vérifier une signature jointe, certificat non joint▲
Si le certificat n'est pas joint, les données signées contiennent malgré tout certaines informations permettant de l'identifier : l'émetteur et le n° de série du certificat.
La structure de type CRYPT_VERIFY_MESSAGE_PARA doit avoir deux éléments supplémentaires.
VerifParams.pfnGetSignerCertificate =
CryptGetSignerCertificateCallback;
VerifParams.pvGetArg =
hCertStore;
L'élément pfnGetSignerCertificate est un pointeur sur une fonction de recherche du certificat.
Le prototype de cette fonction est le suivant :
PCCERT_CONTEXT __stdcall CryptGetSignerCertificateCallback(
void
*
pvGetArg,
DWORD dwCertEncodingType,
PCERT_INFO pSignerId,
HCERTSTORE hMsgCertStore);
L'élément pvGetArg de la structure est le premier paramètre de la fonction.
Le paramètre pSignerId contient l'identification de l'émetteur et le n° de série du certificat.
hMsgCertStore correspond au magasin de certificat contenu dans le message signé.
La fonction suivante recherche le certificat dans le magasin de certificat spécifié par pvGetArg.
PCCERT_CONTEXT __stdcall CryptGetSignerCertificateCallback(
void
*
pvGetArg,
DWORD dwCertEncodingType,
PCERT_INFO pSignerId,
HCERTSTORE hMsgCertStore)
{
PCCERT_CONTEXT CertSignerContext =
NULL
;
CertSignerContext =
CertFindCertificateInStore(pvGetArg,
CRYPT_TYPE,
0
,
CERT_FIND_SUBJECT_CERT,
pSignerId,
NULL
);
return
CertSignerContext;
}
IV-C. Vérifier une signature détachée, certificat joint▲
CRYPT_VERIFY_MESSAGE_PARA VerifParams;
BYTE*
pbToBeSigned[1
];
DWORD cbToBeSigned[1
];
bool
lOk;
memset(&
VerifParams, 0x00
, sizeof
(CRYPT_VERIFY_MESSAGE_PARA));
VerifParams.cbSize =
sizeof
(CRYPT_VERIFY_MESSAGE_PARA);
VerifParams.dwMsgAndCertEncodingType =
CRYPT_TYPE;
lOk =
CryptVerifyDetachedMessageSignature(&
VerifParams,
0
,
pbSignature,
cbSignature,
1
,
pbToBeSigned,
cbToBeSigned,
NULL
);
Une signature détachée est uniquement le 'marquage' des données, par opposition au mécanisme de signature jointe, ou les données sont jointes à la signature.
Dans ce cas, il faut fournir les données ayant été signées.
CryptVerifyDetachedSignature prend les paramètres suivants :
- une structure de type CRYPT_VERIFY_MESSAGE_PARA ;
- le n° d'ordre de la signature à vérifier ;
- la signature ;
- la longueur de la signature ;
- le nombre d'éléments des deux paramètres suivants ;
- un premier tableau pbToBeSigned contient les pointeurs sur les buffers de données à signer ;
- un second tableau cbToBeSigned contient les longueurs des buffers de données à signer ;
- un pointeur sur un pointeur de type PCCERT_CONTEXT, si l'on désire utiliser le certificat ayant servi à la signature pour d'autres opérations, NULL sinon.
(Attention, dans ce cas, il ne faudra pas oublier de le libérer par un appel à CertFreeCertificatContext).
La valeur de retour est true si la signature est vérifiée, false sinon.
IV-D. Vérifier une signature détachée, certificat non joint▲
Les modifications sont exactement les mêmes que pour Vérifier une signature jointe, certificat non joint.