Un tour d'horizon de la spécification XML Signature Syntax and Processing

Introduction

XML Signature Syntax and Processing (XMLDsig) est une spécification élaboré par le W3C en 2002 afin de standardiser la signature de documents. Cette spécification a été modifié en 2008 et est actuellement en version 2.
L’objet de cet article est de présenter XMLDsig ainsi que de détailler son implémentation par la JSR-105. Quelques notions élémentaires sur le chiffrement asymétrique seront utiles à sa bonne compréhension.

XML Signature Syntax and Processing

Un premier aperçu

Les modes d’utilisation de XMLDsig étant variés, nous allons d’abord illustrer son fonctionnement à travers un cas d’utilisation simplifié à l’extrême. Nous verrons ensuite plus en détail chacune des étapes qui mène à la génération d’une signature.
xmldsig-core-schema
Comme on le voit sur ce schéma, XMLDsig est en mesure de générer une signature portant sur un ou plusieurs de documents de formats divers. Au cours de ce processus différentes transformations peuvent être appliquées sur ces documents, mais la principale d’entre elle est l’application d’une fonction de hachage. Cette transformation est obligatoire dans la mesure où elle participe au mécanisme de validation qui sera appliqué par le destinataire du message pour s’assurer que les URI n’ont pas été modifiées. L’algorithme MD5 figure sur ce schéma, mais d’autres fonctions de hachage peuvent être utilisées.
Un document XML est ensuite crée. Il contient les empreintes (hash) des trois documents référencés. Une fonction de hachage est de nouveau appliquée et le résultat est chiffré à l’aide d’une clef privée et intégré au document précédemment crée. C’est ce document qui constitue la signature XML.
L’idée centrale à retenir est que ce ne sont pas les documents d’origine qui sont directement chiffrés, mais l’élément XML qui les référence et dont voici un exemple :

<SignedInfo>
    <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
    <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
    <Reference URI="http://path/doc.csv">
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#md5" />
        <DigestValue>jLPnPblTb48XVTtwhZzOJSRr4mSz3lp8Bnryxj39JC0=</DigestValue>
    </Reference>
    <Reference URI="http://path/doc.xml">
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#md5" />
        <DigestValue>hKj94odLnTCKkONgKV5h89vrnDDU+lHgVnVFMIknMZI=</DigestValue>
    </Reference>
    <Reference URI="http://path/doc.txt">
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#md5" />
        <DigestValue>Rr4mSz3lpXVTtjLPnPblTb48whZzOJSyxj39JC08Bnr=</DigestValue>
    </Reference>
</SignedInfo>

On constate qu’à chaque document correspond un nœud Reference spécifiant les algorithmes de transformation utilisés, en l’occurrence l’algorithme de hachage MD5 (DigestMethod), ainsi que l’empreinte correspondante (DigestValue).

Note : le W3C a défini une liste d’URIs pour identifier de manière standardisée les algorithmes de chiffrement, de hachage et d’encodage les plus courants.

Les documents sont désignés par l’attribut URI du nœud Reference. Nous verrons plus tard qu’il est également possible de les intégrer dans la signature.
Le nœud SignedInfo contient deux autres informations : un algorithme de canonisation (CanonicalizationMethod) et un algorithme de chiffrement (SignatureMethod). La canonisation, ou normalisation, est un processus permettant de présenter de l’information sous une forme standard. Pour un document XML il s’agira d’encoder les données au format UTF-8, de transformer les URIs relatives en URIs absolues etc. L’objectif étant de pouvoir détecter si deux documents sont identiques indépendamment de leur formatage respectif.
Le nom de l’algorithme de chiffrement employé est explicite : il nous apprend qu’une empreinte de SignedInfo sera d’abord générée par l’application de la fonction SHA256 et que cette empreinte sera ensuite chiffrée à l’aide de RSA.

Note: l’algorithme désigné par l’URI http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 a été introduit avec Java 7 et porté sur Java 6.

Nous l’avons vu plus haut, ces deux algorithmes doivent être appliqués sur le nœud SignedInfo et le résultat doit être intégré au sein du même document. Le XML final correspond donc à celui ci :

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
        <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
        <Reference URI="http://path/doc.csv">
            <DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#md5" />
            <DigestValue>jLPnPblTb48XVTtwhZzOJSRr4mSz3lp8Bnryxj39JC0=</DigestValue>
        </Reference>
        <Reference URI="http://path/doc.xml">
            <DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#md5" />
            <DigestValue>hKj94odLnTCKkONgKV5h89vrnDDU+lHgVnVFMIknMZI=</DigestValue>
        </Reference>
        <Reference URI="http://path/doc.txt">
            <DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#md5" />
            <DigestValue>Rr4mSz3lpXVTtjLPnPblTb48whZzOJSyxj39JC08Bnr=</DigestValue>
        </Reference>
    </SignedInfo>
   <SignatureValue>h8TLwi5I7bZTBt5YxoF3MxdnGQlL+YJMJlZul/DoBC909fOv3LlHweSH5jthzZIKwy4M7xx2bKuN
rHdsNpjk19mjmNV7YqWLTJvzCav0CBwXld0GhNbqODXQUoG5yHqsDuMj8mOcGQbEfqj6/KQUxyYI
J5wgWV69WsnESegvWIU3Hfh6VsUwIPOwxRGPIRDfilmxNVyUGWLiRO0uKRcZZhm6BPbu2iXPb7UP
ZslZOeWYQ/MncQdQHbLcaGFjFfO7fGCSeikLnzyFC0cMnzaGpwCgeJkQeR+R96F6qHXczzulnDhp
AxNjc/Kas1eXy/g0SN4bB8cpILfhESKjqHijgg==
    </SignatureValue>
</Signature>

 
Une dernière chose : les valeurs de DigestValue et SignatureValue sont présentées au format base 64.

Les différents modes de signature.

On distingue les signatures détachées (detached signature), enveloppées (envelopped signature) et enveloppantes (enveloping signature). L’exemple que nous avons vu précédemment est du premier type : la signature XML est complètement détachée des documents qu’elle référence.
Une signature enveloppée est une signature intégrée à un des documents XML qu’elle référence. La sélection de ce mode passe par l’ajout d’une transformation supplémentaire à l’élément Reference concerné.

<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>

Note : Dans le cas d’une signature enveloppée, l’URI de l’élément contenant la signature doit être vide.

Une signature enveloppante est une signature qui contient tout ou partie des données qu’elle référence. Ces données sont présentes dans des nœuds Object au même niveau que SignedInfo et SignatureValue. Object possède un attribut id afin qu’il soit possible de le référencer avec l’attribut URI du nœud Reference.

<SignedInfo>
    …
    <Reference URI="#myId">
        …
    </Reference>
</SignedInfo
<Object Id="myId">
      …
</Object>

Identification du certificat.

Le destinataire doit utiliser le certificat associé à la clef privé qui a permis de générer la signature pour s’assurer de sa validité. Dans le cas où il ne serait pas en mesure de déterminer seul quel certificat doit être employé, il est possible de lui transmettre une indication en ajoutant un nœud KeyInfo dans la signature. Par exemple :

<KeyInfo>
    <KeyName>80F6D4F7FB242A85A853C0188A41633D124A8EDF</KeyName>
</KeyInfo>

Ici nous avons choisi pour nom l’empreinte SHA1 du certificat à utiliser. XMLDsig ne donne aucune indication quant à la valeur de KeyName et il est donc du ressort de l’expéditeur et du destinataire de s’accorder sur sa signification.
Il est également possible de renseigner dans KeyInfo toutes les informations nécessaires pour calculer la clef publiques à utiliser. Le nœud suivant permet ainsi de reconstituer une clef publique RSA :

<KeyInfo>
   <KeyValue>
      <RSAKeyValue>
      <Modulus>kbeoO/Uou6nJ/UALAp1N1eGHLVnFvsEiASALUhzkwHHniF50b0hcRATMdNSMjDgVphBBu0lhv+Kx
Q1vL82EgQwkjSKRoSfT997peQ5CjJmzwM8fMvYcO+JgSW/h1cm0YEAcFgb2X1X/Qy0YfkboEfT1p
zu1AqlP0YZ/YkjOFPFEyoi0olTxhViZqbXBpO9YuFXN7CZntkrxC+gLpCAl4T0CRmudag4XUBkgt
grywq6mEfvTjcEnRvJyj09svdx16V9+XbBKcrFi28ge+P1Xv/GPVUCjyRbLvdUvTTR2fMZYec73x
+X7w1VO+3ZfZEz+ZsugHFJqB3rqrZyAPTzII2w==
         </Modulus>
         <Exponent>AQAB</Exponent>
      </RSAKeyValue>
   </KeyValue>
</KeyInfo>

Bien entendu, la transmission de cette information rend impossible l’identification de l’expéditeur puisque la preuve de son identité est normalement liée à la clef publique qu’il est censé avoir préalablement communiqué au destinataire dans un contexte sécurisé.
Quoiqu’il en soit, la procédure de validation, appelée core validation, est une procédure en deux étapes :

  1. validation des références : l’empreinte de chaque document référencé doit être calculée et comparée à la valeur de l’élément DigestValue correspondant ;
  2. validation de la signature : le contenu de SignatureValue doit être déchiffré à l’aide de la clef publique et comparé à l’empreinte de l’élément SignedInfo.

Présentation de la JSR 105.

Cette spécification définie une API permettant de générer et de valider des signatures XML. Nous allons voir comment l’utiliser pour signer un document à l’aide des trois modes de signature présentés plus haut.

Génération de signatures.

Génération d’une signature détachée.

Nous commençons par récupérer une instance de XMLSignatureFactory, c’est l’élément central de l’API dans la mesure où il permet de construire les différents éléments de notre signature.

XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance();

Nous appelons sur cette factory le méthode newSignatureMethod pour indiquer l’algorithme de chiffrement à utiliser ; nous choisissons l’application de l’algorithme SHA256 suivi par un chiffrement RSA. newSignatureMethod possède un deuxième argument permettant de spécifier des paramètres supplémentaires spécifiques à l’algorithme utilisé. Dans notre cas cela n’est pas nécessaire.

SignatureMethod signatureMethod = xmlSignatureFactory.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null);

Nous instancions également une méthode de canonisation :

CanonicalizationMethod canonicalizationMethod = xmlSignatureFactory.newCanonicalizationMethod(
CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null);

Nous construisons maintenant l’élément Reference de notre signature. Pour ce faire nous devons d’abord spécifier l’algorithme de hachage à utiliser (SHA256 dans cet exemple). Comme pour newSignatureMethod, des paramètres supplémentaires spécifiques à l’algorithme utilisé peuvent être transmis.

DigestMethod digestMethod = xmlSignatureFactory.newDigestMethod(DigestMethod.SHA256, null);

Nous pouvons à présent instancier notre objet Reference en transmettant à la factory la fonction de hachage choisie et l’URL du document à signer. Nous aurions égale
ment pu ajouter une liste de transformations à effectuer (canonisation, XSLT…), une valeur d’id et le type d’URI (URL ou URN).

Reference reference = xmlSignatureFactory.newReference("http://www.google-analytics.com/ga.js", digestMethod, null, null, null);

Nous possédons maintenant tous les éléments nécessaires pour créer l’élément SignedInfo :

SignedInfo signedInfo = xmlSignatureFactory.newSignedInfo(canonicalizationMethod, signatureMethod, Lists.newArrayList(reference));olor: #006633;" data-mce-style="color: #006633;">newSignedInfo</span><span style="color: #009900;" data-mce-style="color: #009900;">(</span>canonicalizationMethod, signatureMethod, Lists.<span style="color: #006633;" data-mce-style="color: #006633;">newArrayList</span><span style="color: #009900;" data-mce-style="color: #009900;">(</span>reference<span style="color: #009900;" data-mce-style="color: #009900;">)</span><span style="color: #009900;" data-mce-style="color: #009900;">)</span><span style="color: #339933;" data-mce-style="color: #339933;">;</span>

N’oublions pas de générer un élément KeyInfo pour indiquer le nom du certificat à utiliser pour valider la signature :

KeyInfoFactory keyInfoFactory = xmlSignatureFactory.getKeyInfoFactory();
KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Lists.newArrayList(
keyInfoFactory.newKeyName("certificat1")));

Il ne nous reste que deux étapes à effectuer : récupérer une instance de XMLSignature paramétrée avec signedInfo et keyInfo, puis générer notre signature dans un document XML vierge.

XMLSignature xmlSignature = xmlSignatureFactory.newXMLSignature(signedInfo, keyInfo, null, null, null);
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
Document result = documentBuilder.newDocument();
xmlSignature.sign(new DOMSignContext(privateKey, result));
Signature enveloppée.

La génération d’une signature enveloppée est identique à celle d’une signature détachée, à l’exception du fait que le document transmis au constructeur de DOMSignContext est également le document sur lequel porte la signature. Dans ce contexte, l’attribut uri de Reference doit être vide. Par ailleurs, une transformation supplémentaire doit être effectuée sur le document enveloppant :

List<Transform> transforms = Lists.newArrayList(xmlSignatureFactory.newTransform(Transform.ENVELOPED, null));
xmlSignatureFactory.newReference(uri, digestMethod, transforms, null, null);
Signature enveloppante.

La génération d’une signature enveloppante nécessite d’ajouter un nœud Object contenant le document à signer, ainsi qu’à donner pour valeur à l’attribut uri de Reference l’identifiant de cet Object :

XMLObject object = xmlSignatureFactory.newXMLObject(Collections.singletonList(new DOMStructure(document.getDocumentElement())), "myId", null, null);
DigestMethod digestMethod = xmlSignatureFactory.newDigestMethod(DigestMethod.SHA256, null);
Reference reference = xmlSignatureFactory.newReference("#myId", digestMethod, null, null, null);
xmlSignature  = xmlSignatureFactory.newXMLSignature(signedInfo, keyInfo, Lists.newArrayList(object), null, null);

Les deux autre arguments pouvant être transmis à newXMLObject sont le type MIME et l’encodage de l’objet inséré.

validation de signatures.

Une signature peut être validée de deux manière : soit en donnant explicitement la clef publique à utiliser, comme ci dessous, soit avec une implémentation de KeySelector qui sera chargée de sélectionner le bonne clef, par exemple à l’intérieur d’un keystore par exemple.

NodeList nodes = document.getElementsByTagName("Signature");
Node signatureNode = nodes.item(0);
DOMValidateContext validateContext = new DOMValidateContext(publicKey, signatureNode);
XMLSignature xmlSignature = XMLSignatureFactory.getInstance().unmarshalXMLSignature(
validateContext);
xmlSignature.validate(validateContext); // true ou false

Vers plus de simplicité.

L’API de la JSR105 est relativement complexe. Nous pouvons contourner ce problème en localisant cette complexité à l’intérieur de builders : l’utilisation de builders permet de réaliser des appels de méthode nécessitant moins de paramètres que l’API d’origine et donc plus explicites quand à leur finalité. xmldsig-diag1
Les principaux éléments d’une signature XML sont les nœuds Signature, SignedInfo, Reference et KeyInfo. Nous proposons donc la construction de builders dédiés : XmlSignatureBuilder, SignedInfoBuilder, ReferenceBuilder et KeyInfoBuilder. En organisant notre code de façon à ce que chaque builder de niveau N puisse lui même instancier les builders de niveau inférieur N-1, nous obtenons une logique d’appel qui épouse parfaitement la structure d’une signature XML :

Document result = new XmlSignatureBuilder().
                withKeyInfo().
                    withKeyName(certificate, keySelector.getKeyNameFunction()).
                    buildAndAttach().
                withSignedInfo().
                    withExclusiveCanonicalization().
                    withRsaSha256Signature().
                    withReference().
                        withURI("#doc1").
                        withSHA256Digest().
                        withExclusiveCanonicalization().
                        buildAndAttach().
                    withReference().
                        withURI("#doc2").
                        withSHA1Digest().
                        withExclusiveCanonicalization().
                        buildAndAttach().
                    buildAndAttach().
                withObject(document1.getDocumentElement(), "doc1").
                withObject(document2.getDocumentElement(), "doc2").
                buildAndSign(privateKey);

Ce code permet de générer une signature enveloppante sur deux documents. La méthode withKeyName attend en paramètre le certificat contenant la clef publique qui devra être utilisée pour valider la signature ainsi qu’une instance de Function de la librairie Guava (en attendant la généralisation de Java 8). Le rôle de cette fonction est de convertir un certificat en une String représentant son nom.
La raison pour laquelle nous passons par une fonction pour réaliser cette opération est que nous avons réalisé une implémentation de KeySelector qui en fait déjà usage : HashKeySelector. Ce KeySelector pourra être utilisé a
u moment de la validation pour sélectionner à l’intérieur d’un keystore la clef publique dont l’empreinte correspond à la valeur de l’élément KeyName :

boolean validate = XmlSignatureValidationFactory.
                fromKeySelector(keySelector).
                validate(document) ;

XmlSignatureValidationFactory est une factory qui retourne une instance de XmlSignatureValidation. Il en existe deux implémentations : PublicKeyXmlSignatureValidation qui effectue une validation à partir d’une clef publique donnée et KeySelectorXmlSignatureValidation qui effectue une validation en utilisant l’implémentation de KeySelector que nous venons de présenter.
xmldsig-diag2
Cette surcouche ne couvre sans doute pas tous les cas de figure prévus par XMLDsig. Elle remplacera néanmoins avantageusement une utilisation directe de la JSR-105 pour les cas d’utilisation les plus simples. Les sources sont disponibles sur le repo GitHub de Zenika.

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :