Pułapki walidacji podpisów XML w Peppol
Odpowiedzi Peppol SMP to podpisane dokumenty XML. Walidacja tych podpisów powinna być prosta: wczytaj dokument, zlokalizuj element Signature, zweryfikuj skrót odwołania i wartość podpisu. W praktyce awaria zdarza się częściej niż powinna, a tryb awarii jest na tyle subtelny, że można spędzić na nim wiele dni.
Główna przyczyna to prawie zawsze Canonical XML (C14N) wchodzący w interakcję z obsługą prefiksów przestrzeni nazw. Ten artykuł wyjaśnia dokładnie, co idzie nie tak i jak to naprawić.
Dlaczego istnieje Canonical XML
Dokument XML może być serializowany na wiele sposobów, które są semantycznie identyczne, ale różnią się bajt po bajcie. Atrybuty mogą pojawiać się w dowolnej kolejności. Deklaracje przestrzeni nazw mogą pojawiać się na dowolnym elemencie przodku. Białe znaki w treści elementów mogą się różnić. Każda z tych zmian produkuje inny ciąg bajtów, a zatem inny skrót.
Canonical XML (C14N, standard W3C) definiuje algorytm normalizacji przekształcający każdy semantycznie równoważny dokument XML do jednej postaci kanonicznej. XMLDSig używa C14N do obliczenia stabilnego skrótu nad dokumentem lub jego częścią.
Jeśli obliczasz skrót nad kanonicznym XML, a potem ktoś deserializuje dokument przed Twoją weryfikacją, deserializowana wersja może generować inną postać kanoniczną, inny skrót i niepowodzenie weryfikacji, nawet jeśli dokument jest semantycznie niezmieniony.
Problem z podpisem Peppol SMP
Gdy punkt dostępowy podpisuje odpowiedź SMP, oblicza:
- Kanonizuje element
ServiceInformation(lub cały dokument, zależnie od URI odwołania) - Oblicza SHA-256 nad kanonicznymi bajtami
- Przechowuje skrót w
Reference/DigestValue - Podpisuje element
SignedInfoprywatnym kluczem punktu dostępowego
Gdy pobierasz odpowiedź SMP przez HTTP, od momentu podpisania mogło zajść kilka rzeczy:
- Serwer SMP mógł załadować dokument z bazy danych i deserializować go
- Warstwa HTTP mogła ponownie parsować i wystawiać XML
- Framework SOAP/HTTP mógł normalizować białe znaki
Każda z tych operacji może zmienić postać kanoniczną, którą obliczasz podczas weryfikacji. Nawet jeśli algorytm skrótu i URI odwołania są poprawne, bajty, które kanonizujesz, różnią się od bajtów kanonizowanych przez podpisującego.
Promocja prefiksów przestrzeni nazw
Najczęstszą przyczyną jest promocja prefiksów przestrzeni nazw.
Serwer SMP może podpisać dokument z deklaracjami przestrzeni nazw na elementach podrzędnych:
<ServiceMetadata>
<ServiceInformation xmlns:bdxr="http://docs.oasis-open.org/bdxr/ns/SMP/2016/05">
...
</ServiceInformation>
<Signature>...</Signature>
</ServiceMetadata>
Gdy C14N jest stosowany do ServiceInformation, deklaracja przestrzeni nazw jest uwzględniana w postaci kanonicznej, bo jest w zakresie tego elementu.
Gdy serwer SMP deserializuje do dostarczenia, wiele bibliotek XML przenosi deklaracje przestrzeni nazw do elementu głównego:
<ServiceMetadata xmlns:bdxr="http://docs.oasis-open.org/bdxr/ns/SMP/2016/05">
<ServiceInformation>
...
</ServiceInformation>
<Signature>...</Signature>
</ServiceMetadata>
Teraz gdy stosujesz C14N do ServiceInformation, przestrzeń nazw jest zadeklarowana na korzeniu, a nie na ServiceInformation. Przy Exclusive C14N (http://www.w3.org/2001/10/xml-exc-c14n#) deklaracje przestrzeni nazw, które nie są widocznie używane na kanonizowanym elemencie, są wykluczane. Przestrzeń nazw nie jest już na ServiceInformation; jest na ServiceMetadata. Wynik C14N się zmienia. Skrót przestaje pasować.
Dynamiczne prefiksy przestrzeni nazw (ns1, ns2, …)
Druga klasa problemu: niektóre implementacje SMP używają dynamicznie generowanych prefiksów przestrzeni nazw. Zamiast xmlns:bdxr emitują xmlns:ns1, xmlns:ns2 i tak dalej. Prefiksy te są przypisywane w czasie serializacji i mogą różnić się między przebiegiem podpisywania a przebiegiem dostarczania.
Przy Inclusive C14N powiązania prefiksów przestrzeni nazw są uwzględniane w postaci kanonicznej. Jeśli prefiks zmieni się z bdxr: na ns1:, postać kanoniczna się zmienia i skrót zawodzi.
Przy Exclusive C14N powiązania przestrzeni nazw dla prefiksów nieużywanych w kanonizowanym elemencie są wykluczane. Ale jeśli sam prefiks się zmienia (powiązanie jest inne), bajty kanoniczne elementów używających tego prefiksu się zmieniają.
Rozwiązaniem jest normalizacja prefiksów przestrzeni nazw przed weryfikacją. Wymaga to ponownego odczytu podpisanego poddrzewa ze znanych, stabilnych powiązań prefiksów. Nie można tego zrobić i jednocześnie weryfikować oryginalnego podpisu; musisz weryfikować względem tego, co faktycznie zostało podpisane, co oznacza pracę z surowymi bajtami, które zostały podpisane.
Problem SignedXml w .NET
Klasa SignedXml w .NET ma konkretne ograniczenie: rozwiązuje odwołania względem dokumentu w pamięci. Jeśli załadujesz odpowiedź SMP do XmlDocument, deklaracje przestrzeni nazw mogą zostać przeniesione przez parser XML zanim SignedXml je w ogóle zobaczy.
Rozwiązanie w .NET:
var doc = new XmlDocument
{
PreserveWhitespace = true // critical
};
doc.Load(responseStream); // load from raw bytes, not from a re-serialized string
PreserveWhitespace = true zapobiega normalizowaniu węzłów tekstowych przez parser XML. Co ważniejsze, użycie Load() z surowego strumienia (zamiast z ciągu, który był już parsowany i ponownie emitowany) unika przebiegu deserializacji, który promuje deklaracje przestrzeni nazw.
Nie rób tego:
var xml = await response.Content.ReadAsStringAsync();
doc.LoadXml(xml); // string parsing may already have re-serialized
Zamiast tego zrób tak:
using var stream = await response.Content.ReadAsStreamAsync();
doc.Load(stream); // raw bytes, no intermediate string
Następnie zweryfikuj za pomocą 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 oznacza, że .NET waliduje również skróty odwołań, a nie tylko podpis nad SignedInfo. Chcesz obu sprawdzeń.
Łańcuch zaufania certyfikatów
Weryfikacja, że podpis jest matematycznie ważny, nie wystarczy. Musisz również zweryfikować, że certyfikat podpisujący jest wystawiony przez kotwicę zaufania PKI Peppol.
OpenPEPPOL publikuje magazyn zaufanych certyfikatów (certyfikat SML, korzenie certyfikatów SMP) w produkcyjnej i testowej infrastrukturze PKI. Załaduj magazyn zaufanych certyfikatów i zweryfikuj łańcuch certyfikatów:
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);
}
Odpowiedź SMP z ważnym podpisem od niezaufanego certyfikatu to atak, a nie błąd konfiguracji. Zakończ z błędem.
Obsługa URI odwołania
Podpisy SMP Peppol zazwyczaj używają pustego URI odwołania (URI=""), co oznacza, że odwołanie obejmuje cały dokument. Przy C14N oznacza to kanonizację całego dokumentu.
Jeśli URI odwołania to wyrażenie XPointer (URI="#ID"), odwołanie obejmuje tylko element o tym ID. Jest to bardziej odporne na promocję przestrzeni nazw, bo kanonizujesz tylko poddrzewo, nie cały dokument.
Jeśli podpis zawodzi i możesz kontrolować serwer SMP, preferuj odwołania XPointer zamiast pustego URI. Jeśli nie możesz kontrolować serwera, pracuj z surowymi bajtami.
Podejście do testowania
Najbardziej niezawodnym sposobem testowania weryfikacji podpisów SMP jest użycie odpowiedzi SMP z produkcji lub środowiska testowego zapisanych jako surowe bajty od prawdziwego uczestnika Peppol. Nie konstruuj odpowiedzi testowych programowo; obsługa przestrzeni nazw będzie się różnić od tego, co generują prawdziwe serwery SMP.
Sieć testowa Peppol (acc.edelivery.tech.ec.europa.eu SML) ma prawdziwych uczestników, których można wyszukać i pobrać od nich prawdziwe podpisane odpowiedzi SMP. Użyj ich do testów integracyjnych.
Podsumowanie
Tryby awarii, od najczęstszego:
- Promocja przestrzeni nazw: parser XML przenosi deklaracje przestrzeni nazw do korzenia przy ładowaniu, zmieniając wynik C14N dla podpisanego poddrzewa
- Deserializacja: konwersja odpowiedzi HTTP do ciągu i z powrotem przed weryfikacją
- Dynamiczne prefiksy przestrzeni nazw: serwer SMP używa różnych prefiksów przy podpisywaniu i dostarczaniu
- Brak
PreserveWhitespace: parser XML .NET normalizuje białe znaki, zmieniając skróty węzłów tekstowych - Łańcuch certyfikatów niezweryfikowany: podpis jest ważny, ale certyfikat nie pochodzi z PKI Peppol
Wszystkie te problemy można naprawić bez modyfikowania serwera SMP. Ładuj z surowych bajtów, ustaw PreserveWhitespace = true i zawsze waliduj łańcuch zaufania certyfikatów.
SealDoc obsługuje odkrywanie SMP i weryfikację podpisów wewnętrznie, w tym wszystkie opisane powyżej przypadki szczególne. Jeśli budujesz własną integrację Peppol i napotykasz niewyjaśnione awarie podpisów, SealDoc API udostępnia wyniki wyszukiwania SMP wraz ze statusem ważności podpisu, który możesz porównać z wynikami własnej implementacji.