← Back to all articles

Pułapki walidacji podpisów XML w Peppol

SealDoc Team · · 6 min read

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:

  1. Kanonizuje element ServiceInformation (lub cały dokument, zależnie od URI odwołania)
  2. Oblicza SHA-256 nad kanonicznymi bajtami
  3. Przechowuje skrót w Reference/DigestValue
  4. Podpisuje element SignedInfo prywatnym 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:

  1. Promocja przestrzeni nazw: parser XML przenosi deklaracje przestrzeni nazw do korzenia przy ładowaniu, zmieniając wynik C14N dla podpisanego poddrzewa
  2. Deserializacja: konwersja odpowiedzi HTTP do ciągu i z powrotem przed weryfikacją
  3. Dynamiczne prefiksy przestrzeni nazw: serwer SMP używa różnych prefiksów przy podpisywaniu i dostarczaniu
  4. Brak PreserveWhitespace: parser XML .NET normalizuje białe znaki, zmieniając skróty węzłów tekstowych
  5. Ł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.


← Back to all articles