Problemi di validazione delle firme XML in Peppol
Le risposte SMP Peppol sono documenti XML firmati. La validazione di quelle firme dovrebbe essere semplice: analizzare il documento, individuare l’elemento Signature, verificare il digest del riferimento e il valore della firma. In pratica, fallisce più spesso di quanto dovrebbe, e la modalità di errore è abbastanza sottile da farci trascorrere giorni a diagnosticarla.
La causa principale è quasi sempre il Canonical XML (C14N) che interagisce con la gestione dei prefissi dei namespace. Questo articolo spiega esattamente cosa va storto e come risolverlo.
Perché esiste il Canonical XML
Un documento XML può essere serializzato in più modi che sono semanticamente identici ma byte per byte diversi. Gli attributi possono apparire in qualsiasi ordine. Le dichiarazioni di namespace possono apparire su qualsiasi elemento antenato. Gli spazi bianchi nel contenuto degli elementi possono variare. Ognuno di questi cambiamenti produce una stringa di byte diversa e quindi un hash diverso.
Il Canonical XML (C14N, standard W3C) definisce un algoritmo di normalizzazione che trasforma qualsiasi documento XML semanticamente equivalente in una singola forma canonica. XMLDSig utilizza C14N per calcolare un hash stabile su un documento o una sua porzione.
Se si calcola un digest su XML canonicalizzato, e poi qualcuno ri-serializza il documento prima di verificare, la versione ri-serializzata può produrre una forma canonica diversa, un hash diverso e un errore di verifica, anche se il documento è semanticamente invariato.
Il problema delle firme SMP Peppol
Quando un Access Point firma una risposta SMP, calcola:
- Canonicalizzare l’elemento
ServiceInformation(o l’intero documento, a seconda dell’URI di riferimento) - Calcolare SHA-256 sui byte canonici
- Memorizzare il digest in
Reference/DigestValue - Firmare l’elemento
SignedInfocon la chiave privata dell’Access Point
Quando si recupera la risposta SMP tramite HTTP, potrebbero essere accadute diverse cose dalla firma:
- Il server SMP potrebbe aver caricato il documento da un database e averlo ri-serializzato
- Il livello HTTP potrebbe aver ri-analizzato e ri-prodotto l’XML
- Il framework SOAP/HTTP potrebbe aver normalizzato gli spazi bianchi
Ognuno di questi può cambiare la forma canonica che si calcola durante la verifica. Anche se l’algoritmo del digest e l’URI di riferimento sono corretti, i byte che si canonicalizzano sono diversi dai byte che il firmatario ha canonicalizzato.
Promozione dei prefissi di namespace
La causa più comune è la promozione dei prefissi di namespace.
Un server SMP potrebbe firmare un documento che ha dichiarazioni di namespace su elementi figlio:
<ServiceMetadata>
<ServiceInformation xmlns:bdxr="http://docs.oasis-open.org/bdxr/ns/SMP/2016/05">
...
</ServiceInformation>
<Signature>...</Signature>
</ServiceMetadata>
Quando C14N viene applicato a ServiceInformation, la dichiarazione di namespace è inclusa nella forma canonica perché è nell’ambito di quell’elemento.
Quando il server SMP ri-serializza per la consegna, molte librerie XML spostano le dichiarazioni di namespace all’elemento radice:
<ServiceMetadata xmlns:bdxr="http://docs.oasis-open.org/bdxr/ns/SMP/2016/05">
<ServiceInformation>
...
</ServiceInformation>
<Signature>...</Signature>
</ServiceMetadata>
Ora quando si applica C14N a ServiceInformation, il namespace è dichiarato sulla radice, non su ServiceInformation. Con Exclusive C14N (http://www.w3.org/2001/10/xml-exc-c14n#), le dichiarazioni di namespace che non sono visibilmente utilizzate sull’elemento in fase di canonicalizzazione vengono escluse. Il namespace non è più su ServiceInformation; è su ServiceMetadata. L’output C14N cambia. Il digest non corrisponde più.
Prefissi di namespace dinamici (ns1, ns2, …)
Una seconda classe di problema: alcune implementazioni SMP utilizzano prefissi di namespace generati dinamicamente. Invece di xmlns:bdxr, emettono xmlns:ns1, xmlns:ns2 e così via. Questi prefissi vengono assegnati al momento della serializzazione e possono differire tra il passaggio di firma e il passaggio di consegna.
Con Inclusive C14N, le associazioni dei prefissi di namespace sono incluse nella forma canonica. Se il prefisso cambia da bdxr: a ns1:, la forma canonica cambia e il digest fallisce.
Con Exclusive C14N, le associazioni dei prefissi di namespace per i prefissi non utilizzati nell’elemento in fase di canonicalizzazione sono escluse. Ma se il prefisso stesso cambia (l’associazione stessa è diversa), i byte canonici per gli elementi che utilizzano quel prefisso cambiano.
La correzione è normalizzare i prefissi di namespace prima della verifica. Questo richiede la rilettura del sottoalbero firmato con associazioni di prefissi note e stabili. Non è possibile farlo e verificare anche la firma originale; è necessario verificare rispetto a ciò che è stato effettivamente firmato, il che significa lavorare con i byte grezzi che sono stati firmati.
Il problema di SignedXml in .NET
La classe SignedXml di .NET ha una limitazione specifica: risolve i riferimenti relativi al documento in memoria. Se si carica la risposta SMP in un XmlDocument, le dichiarazioni di namespace potrebbero essere spostate dal parser XML prima che SignedXml le veda.
La correzione in .NET:
var doc = new XmlDocument
{
PreserveWhitespace = true // critical
};
doc.Load(responseStream); // load from raw bytes, not from a re-serialized string
PreserveWhitespace = true impedisce al parser XML di normalizzare i nodi di testo. Ancora più importante, l’utilizzo di Load() dallo stream grezzo (piuttosto che da una stringa già analizzata e ri-emessa) evita il passaggio di ri-serializzazione che promuove le dichiarazioni di namespace.
Non fare questo:
var xml = await response.Content.ReadAsStringAsync();
doc.LoadXml(xml); // string parsing may already have re-serialized
Fare invece questo:
using var stream = await response.Content.ReadAsStreamAsync();
doc.Load(stream); // raw bytes, no intermediate string
Poi verificare con 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 significa che .NET valida anche i digest dei riferimenti, non solo la firma su SignedInfo. Si vogliono entrambi i controlli.
Catena di fiducia del certificato
Verificare che la firma sia matematicamente valida non è sufficiente. È necessario verificare anche che il certificato di firma sia emesso dall’ancora di fiducia della PKI Peppol.
OpenPEPPOL pubblica un trust store (certificato SML, radici dei certificati SMP) nella PKI di produzione e test. Caricare il trust store e verificare la catena di certificati:
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);
}
Una risposta SMP con una firma valida da un certificato non attendibile è un attacco, non una configurazione errata. Fallire in modo rigoroso.
Gestione dell’URI di riferimento
Le firme SMP Peppol tipicamente utilizzano un URI di riferimento vuoto (URI=""), il che significa che il riferimento copre l’intero documento. Con C14N, ciò significa che l’intero documento viene canonicalizzato.
Se l’URI di riferimento è un’espressione XPointer (URI="#ID"), il riferimento copre solo l’elemento con quell’ID. Questo è più resiliente alla promozione dei namespace perché si canonicalizza solo un sottoalbero, non l’intero documento.
Se una firma sta fallendo e si può controllare il server SMP, preferire i riferimenti XPointer rispetto all’URI vuoto. Se non si può controllare il server, lavorare con i byte grezzi.
Approccio ai test
Il modo più affidabile per testare la verifica delle firme SMP è testare rispetto a una risposta SMP nota come corretta, salvata come byte grezzi da un partecipante Peppol di produzione o test. Non costruire risposte di test programmaticamente; la gestione dei namespace differirà da quella dei server SMP reali.
La rete TEST Peppol (acc.edelivery.tech.ec.europa.eu SML) ha partecipanti reali che si possono cercare e da cui recuperare risposte SMP firmate reali. Usarle per i test di integrazione.
Riepilogo
Le modalità di errore, in ordine di frequenza:
- Promozione dei namespace: il parser XML sposta le dichiarazioni di namespace alla radice al caricamento, modificando l’output C14N per il sottoalbero firmato
- Ri-serializzazione: conversione della risposta HTTP in una stringa e ritorno prima della verifica
- Prefissi di namespace dinamici: il server SMP utilizza prefissi diversi nella fase di firma rispetto alla consegna
PreserveWhitespacemancante: il parser XML .NET normalizza gli spazi bianchi, modificando gli hash dei nodi di testo- Catena di certificati non verificata: la firma è valida ma il certificato non proviene dalla PKI Peppol
Tutte queste sono risolvibili senza modificare il server SMP. Caricare dai byte grezzi, impostare PreserveWhitespace = true e validare sempre la catena di fiducia del certificato.
SealDoc gestisce internamente la discovery SMP e la verifica delle firme, inclusi tutti i casi limite sopra descritti. Se si sta costruendo un’integrazione Peppol personalizzata e si riscontrano errori di firma che non si riesce a diagnosticare, l’API SealDoc espone i risultati della ricerca SMP incluso lo stato di validità della firma, che si può confrontare con l’output della propria implementazione.