From 59caa21907cb9e5873cee27159ec0bfc3a64da63 Mon Sep 17 00:00:00 2001 From: Johan Wassberg Date: Tue, 10 Nov 2026 12:32:36 +0100 Subject: [PATCH] Don't declare the xml prefix on aggregate roots entitiesdescriptor() copied the whole NS map onto the EntitiesDescriptor root, which emitted an explicit xmlns:xml declaration. The xml prefix is implicitly bound and must never be declared. libxml2 ignores the declaration when canonicalizing, but .NET's SignedXml keeps it, so a signed aggregate failed to verify in ADFSToolkit although xmlsec1 and other libxml2-based verifiers accepted it. Drop the prefix from the nsmap before building the root, and add a regression test. --- src/pyff/samlmd.py | 6 ++++++ src/pyff/test/test_utils.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/pyff/samlmd.py b/src/pyff/samlmd.py index 90f2c7e2..acaea918 100644 --- a/src/pyff/samlmd.py +++ b/src/pyff/samlmd.py @@ -510,6 +510,12 @@ def entitiesdescriptor( for entity in entities: nsmap.update(entity.nsmap) + # The xml prefix is bound by definition and must never be declared. An explicit + # xmlns:xml is legal XML, but .NET's exclusive c14n keeps it in the canonical + # form, so consumers such as ADFSToolkit compute a different digest and reject + # the signature. + nsmap.pop('xml', None) + log.debug("selecting %d entities before validation" % len(entities)) attrs = dict(Name=name, nsmap=nsmap) diff --git a/src/pyff/test/test_utils.py b/src/pyff/test/test_utils.py index eb03c2b0..bd794d34 100644 --- a/src/pyff/test/test_utils.py +++ b/src/pyff/test/test_utils.py @@ -280,6 +280,26 @@ def test_entitiesdescriptor_copies_shared_entities(self): [e.get('entityID') for e in first], ) + def test_entitiesdescriptor_does_not_declare_xml_prefix(self): + """The aggregate root must not carry an explicit xmlns:xml declaration. + + .NET's SignedXml keeps it in the exclusive c14n output, so a signature + over such a root fails to verify in ADFSToolkit although libxml2 based + verifiers accept it. + """ + from pyuppsala import etree + + from pyff.samlmd import entitiesdescriptor + + t = entitiesdescriptor(self._entities(), 'https://md.example.org/first', lookup_fn=None, validate=False) + + self.assertNotIn('xml', t.nsmap) + serialized = etree.tostring(t, encoding='unicode') + self.assertNotIn('xmlns:xml=', serialized) + # the other pyff prefixes are still declared on the root + self.assertEqual(NS['md'], t.nsmap.get('md')) + self.assertEqual(NS['ds'], t.nsmap.get('ds')) + def test_entitiesdescriptor_copy_false_moves(self): """copy=False keeps the historical (lxml) move behaviour for callers that want it.""" from pyff.samlmd import entitiesdescriptor