← Back to all articles

Pièges de la validation des signatures XML dans Peppol

SealDoc Team · · 8 min read

Les réponses SMP Peppol sont des documents XML signés. Valider ces signatures devrait être simple : analyser le document, localiser l’élément Signature, vérifier le résumé de référence et la valeur de signature. En pratique, cela échoue plus souvent que prévu, et le mode de défaillance est suffisamment subtil pour que vous puissiez y passer des jours.

La cause profonde est presque toujours le XML canonique (C14N) qui interagit avec la gestion des préfixes d’espaces de noms. Cet article explique exactement ce qui se passe et comment y remédier.

Pourquoi le XML canonique existe

Un document XML peut être sérialisé de plusieurs façons qui sont sémantiquement identiques mais différentes octet par octet. Les attributs peuvent apparaître dans n’importe quel ordre. Les déclarations d’espaces de noms peuvent apparaître sur n’importe quel élément ancêtre. Les espaces dans le contenu des éléments peuvent varier. L’un de ces changements produit une chaîne d’octets différente et donc un hachage différent.

Le XML canonique (C14N, norme W3C) définit un algorithme de normalisation qui transforme tout document XML sémantiquement équivalent en une forme canonique unique. XMLDSig utilise C14N pour calculer un hachage stable sur un document ou une partie de celui-ci.

Si vous calculez un résumé sur du XML canonicalisé, puis que quelqu’un re-sérialise le document avant votre vérification, la version re-sérialisée peut produire une forme canonique différente, un hachage différent et un échec de vérification, même si le document est sémantiquement inchangé.

Le problème de signature SMP Peppol

Quand un Point d’Accès signe une réponse SMP, il calcule :

  1. Canonicalise l’élément ServiceInformation (ou le document entier, selon l’URI de référence)
  2. Calcule SHA-256 sur les octets canoniques
  3. Stocke le résumé dans Reference/DigestValue
  4. Signe l’élément SignedInfo avec la clé privée du Point d’Accès

Lorsque vous récupérez la réponse SMP via HTTP, plusieurs choses peuvent s’être passées depuis la signature :

  • Le serveur SMP peut avoir chargé le document depuis une base de données et l’avoir re-sérialisé
  • La couche HTTP peut avoir ré-analysé et re-produit le XML
  • Le framework SOAP/HTTP peut avoir normalisé les espaces

Chacun de ces éléments peut modifier la forme canonique que vous calculez lors de la vérification. Même si l’algorithme de résumé et l’URI de référence sont corrects, les octets que vous canonicalisez sont différents de ceux que le signataire a canonicalisés.

Promotion des préfixes d’espaces de noms

La cause la plus fréquente est la promotion des préfixes d’espaces de noms.

Un serveur SMP peut signer un document dont les déclarations d’espaces de noms se trouvent sur des éléments enfants :

<ServiceMetadata>
  <ServiceInformation xmlns:bdxr="http://docs.oasis-open.org/bdxr/ns/SMP/2016/05">
    ...
  </ServiceInformation>
  <Signature>...</Signature>
</ServiceMetadata>

Quand C14N est appliqué à ServiceInformation, la déclaration d’espace de noms est incluse dans la forme canonique car elle est dans la portée de cet élément.

Quand le serveur SMP re-sérialise pour la livraison, de nombreuses bibliothèques XML déplacent les déclarations d’espaces de noms vers l’élément racine :

<ServiceMetadata xmlns:bdxr="http://docs.oasis-open.org/bdxr/ns/SMP/2016/05">
  <ServiceInformation>
    ...
  </ServiceInformation>
  <Signature>...</Signature>
</ServiceMetadata>

Maintenant, quand vous appliquez C14N à ServiceInformation, l’espace de noms est déclaré sur la racine, pas sur ServiceInformation. Sous le C14N exclusif (http://www.w3.org/2001/10/xml-exc-c14n#), les déclarations d’espaces de noms qui ne sont pas visiblement utilisées sur l’élément en cours de canonicalisation sont exclues. L’espace de noms n’est plus sur ServiceInformation ; il est sur ServiceMetadata. La sortie C14N change. Le résumé ne correspond plus.

Préfixes d’espaces de noms dynamiques (ns1, ns2, …)

Une deuxième classe de problème : certaines implémentations SMP utilisent des préfixes d’espaces de noms générés dynamiquement. Au lieu de xmlns:bdxr, ils émettent xmlns:ns1, xmlns:ns2, etc. Ces préfixes sont assignés au moment de la sérialisation et peuvent différer entre le passage de signature et le passage de livraison.

Sous le C14N inclusif, les liaisons de préfixes d’espaces de noms sont incluses dans la forme canonique. Si le préfixe passe de bdxr: à ns1:, la forme canonique change et le résumé échoue.

Sous le C14N exclusif, les liaisons de préfixes d’espaces de noms pour les préfixes non utilisés dans l’élément en cours de canonicalisation sont exclues. Mais si le préfixe lui-même change (la liaison elle-même est différente), les octets canoniques pour les éléments utilisant ce préfixe changent.

La correction consiste à normaliser les préfixes d’espaces de noms avant la vérification. Cela nécessite de relire le sous-arbre signé avec des liaisons de préfixes connues et stables. Il n’est pas possible de faire cela et de vérifier également la signature originale ; vous devez vérifier par rapport à ce qui a réellement été signé, ce qui signifie travailler avec les octets bruts qui ont été signés.

Le problème SignedXml de .NET

La classe SignedXml de .NET a une limitation spécifique : elle résout les références par rapport au document en mémoire. Si vous chargez la réponse SMP dans un XmlDocument, les déclarations d’espaces de noms peuvent être déplacées par l’analyseur XML avant que SignedXml ne les voie.

La correction en .NET :

var doc = new XmlDocument
{
    PreserveWhitespace = true  // critical
};
doc.Load(responseStream);  // load from raw bytes, not from a re-serialized string

PreserveWhitespace = true empêche l’analyseur XML de normaliser les nœuds de texte. Plus important encore, utiliser Load() depuis le flux brut (plutôt que depuis une chaîne déjà analysée et re-émise) évite le passage de re-sérialisation qui promeut les déclarations d’espaces de noms.

Ne faites pas ceci :

var xml = await response.Content.ReadAsStringAsync();
doc.LoadXml(xml);  // string parsing may already have re-serialized

Faites ceci à la place :

using var stream = await response.Content.ReadAsStreamAsync();
doc.Load(stream);  // raw bytes, no intermediate string

Ensuite, vérifiez avec SignedXml :

var signedXml = new SignedXml(doc);
var sigElement = doc.GetElementsByTagName("Signature")[0] as XmlElement
    ?? throw new InvalidOperationException("No Signature element found");
signedXml.LoadXml(sigElement);

// Resolve the signing certificate from the SMP KeyInfo
var cert = GetCertificateFromKeyInfo(signedXml);

if (!signedXml.CheckSignature(cert, verifySignatureOnly: false))
    throw new SecurityException("SMP response signature verification failed");

verifySignatureOnly: false signifie que .NET valide également les résumés de référence, pas seulement la signature sur SignedInfo. Vous voulez les deux vérifications.

Chaîne de confiance des certificats

Vérifier que la signature est mathématiquement valide ne suffit pas. Vous devez également vérifier que le certificat de signature est émis par l’ancre de confiance PKI Peppol.

OpenPEPPOL publie un magasin de confiance (certificat SML, racines de certificats SMP) dans la PKI de production et de test. Chargez le magasin de confiance et vérifiez la chaîne de certificats :

static bool IsInPeppolTrustChain(X509Certificate2 cert)
{
    var chain = new X509Chain();
    chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
    chain.ChainPolicy.CustomTrustStore.Add(peppolRootCert);
    chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
    return chain.Build(cert);
}

Une réponse SMP avec une signature valide provenant d’un certificat non approuvé est une attaque, pas une mauvaise configuration. Échouez fermement.

Gestion de l’URI de référence

Les signatures SMP Peppol utilisent généralement un URI de référence vide (URI=""), ce qui signifie que la référence couvre l’intégralité du document. Sous C14N, cela signifie que l’intégralité du document est canonicalisée.

Si l’URI de référence est une expression XPointer (URI="#ID"), la référence couvre uniquement l’élément avec cet ID. C’est plus résilient à la promotion des espaces de noms car vous ne canonicalisez qu’un sous-arbre, pas le document entier.

Si une signature échoue et que vous pouvez contrôler le serveur SMP, préférez les références XPointer aux URI vides. Si vous ne pouvez pas contrôler le serveur, travaillez avec les octets bruts.

Approche de test

La façon la plus fiable de tester la vérification des signatures SMP est de travailler contre une réponse SMP connue correcte sauvegardée comme octets bruts depuis un participant Peppol de production ou de test. Ne construisez pas de réponses de test de manière programmatique ; la gestion des espaces de noms différera de ce que produisent les vrais serveurs SMP.

Le réseau TEST Peppol (acc.edelivery.tech.ec.europa.eu SML) a de vrais participants que vous pouvez rechercher et depuis lesquels vous pouvez récupérer de vraies réponses SMP signées. Utilisez-les pour les tests d’intégration.

Résumé

Les modes de défaillance, par ordre de fréquence :

  1. Promotion des espaces de noms : l’analyseur XML déplace les déclarations d’espaces de noms vers la racine lors du chargement, modifiant la sortie C14N pour le sous-arbre signé
  2. Re-sérialisation : conversion de la réponse HTTP en chaîne et retour avant la vérification
  3. Préfixes d’espaces de noms dynamiques : le serveur SMP utilise des préfixes différents lors de la signature et de la livraison
  4. PreserveWhitespace manquant : l’analyseur XML .NET normalise les espaces, modifiant les hachages de nœuds de texte
  5. Chaîne de certificats non vérifiée : la signature est valide mais le certificat ne provient pas de la PKI Peppol

Tous ces problèmes sont corrigeables sans patcher le serveur SMP. Chargez depuis les octets bruts, définissez PreserveWhitespace = true et validez toujours la chaîne de confiance des certificats.

SealDoc gère la découverte SMP et la vérification des signatures en interne, y compris tous les cas limites ci-dessus. Si vous construisez une intégration Peppol personnalisée et rencontrez des échecs de signature que vous ne pouvez pas déboguer, l’API SealDoc expose les résultats de recherche SMP, y compris le statut de validité de la signature, que vous pouvez comparer avec la sortie de votre propre implémentation.


← Back to all articles