From 27181347bb82d045dc8ee9b92588d8ca41c95e34 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Tue, 27 Oct 2026 15:45:35 +0100 Subject: [PATCH 1/7] Include xmlsec/dl.h explicitly to fix build against xmlsec 1.3.13 main.c calls xmlSecCryptoDLLoadLibrary() and xmlSecCryptoDLUnloadLibrary(), which are declared in , but never included that header directly. The declarations were only pulled in transitively via -> -> when XMLSEC_CRYPTO_DYNAMIC_LOADING is defined. xmlsec 1.3.13 dropped the #include from xmlsec/app.h, so builds with dynamic crypto loading enabled (e.g. MacPorts, Homebrew, distro packages) fail with "call to undeclared function" errors under -Werror=implicit-function-declaration. Static-crypto builds (XMLSEC_NO_CRYPTO_DYNAMIC_LOADING) are unaffected since those calls are compiled out. dl.h has existed since xmlsec 1.2.x and guards its declarations with XMLSEC_NO_CRYPTO_DYNAMIC_LOADING itself, so the include is safe for all supported versions and build modes. Fixes #434 --- src/main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.c b/src/main.c index c7dac2b5..d0a43824 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,7 @@ #include #include +#include #include #include #include From c67c3c68a2cf900d8e6632e1b2366e9452e2426e Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Tue, 27 Oct 2026 16:28:01 +0100 Subject: [PATCH 2/7] Fix runtime incompatibilities with xmlsec 1.3.13 xmlsec 1.3.13 changed three behaviors that broke the test suite: - xmlSecInit() can no longer be called twice without a shutdown: xmlSecTransformXsltInitialize() now returns an error instead of ignoring the failed assertion. Since the module initializes xmlsec on import, an explicit xmlsec.init() failed. Make init() a no-op when the library is already initialized. - xmlSecKeyDataEncryptedKeyXmlWrite() no longer copies the parent key info context preferences into the nested encryption context, so the lax key search enabled by EncryptionContext was lost when encrypting the session key into , failing with "key is not found". Create the nested context upfront (xmlsec reuses it) and enable lax key search on its read context too. - The default allowed URI types changed from Any to Empty | SameDocument, so references to local/remote resources (e.g. cid: URIs served via register_callbacks) are rejected. Keep the secure upstream default and expose an opt-in: SignatureContext.enabled_reference_uris plus TransformUriType* constants. Refs #434 --- doc/source/modules/constants.rst | 24 +++++++++++++++++++ src/constants.c | 7 ++++++ src/ds.c | 41 ++++++++++++++++++++++++++++++++ src/enc.c | 11 +++++++++ src/main.c | 6 +++++ src/xmlsec/__init__.pyi | 1 + src/xmlsec/constants.pyi | 6 +++++ tests/test_ds.py | 17 +++++++++++++ tests/test_main.py | 2 ++ 9 files changed, 115 insertions(+) diff --git a/doc/source/modules/constants.rst b/doc/source/modules/constants.rst index 3df6b50f..a6f3ac51 100644 --- a/doc/source/modules/constants.rst +++ b/doc/source/modules/constants.rst @@ -292,6 +292,30 @@ Transforms Transform could be used for operation. +.. data:: xmlsec.constants.TransformUriTypeNone + + No URI types are allowed. + +.. data:: xmlsec.constants.TransformUriTypeEmpty + + Empty URI (:xml:`URI=""`), i.e. the document containing the signature. + +.. data:: xmlsec.constants.TransformUriTypeSameDocument + + Same-document URI (:xml:`URI="#id"`). + +.. data:: xmlsec.constants.TransformUriTypeLocal + + Local file URI (:xml:`URI="file:///path"` or a URI without a scheme). + +.. data:: xmlsec.constants.TransformUriTypeRemote + + Remote URI (any other scheme, e.g. :xml:`URI="http://..."` or :xml:`URI="cid:..."`). + +.. data:: xmlsec.constants.TransformUriTypeAny + + Any URI type. + .. data:: xmlsec.constants.TransformInclC14N The regular (inclusive) C14N without comments transform klass. diff --git a/src/constants.c b/src/constants.c index bd1fa5e0..e0e20207 100644 --- a/src/constants.c +++ b/src/constants.c @@ -282,6 +282,13 @@ int PyXmlSec_ConstantsModule_Init(PyObject* package) { if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageEncryptionMethod) < 0) goto ON_FAIL; if (PYXMLSEC_ADD_INT_CONSTANT(TransformUsageAny) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUriTypeNone) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUriTypeEmpty) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUriTypeSameDocument) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUriTypeLocal) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUriTypeRemote) < 0) goto ON_FAIL; + if (PYXMLSEC_ADD_INT_CONSTANT(TransformUriTypeAny) < 0) goto ON_FAIL; + #undef PYXMLSEC_ADD_INT_CONSTANT #define PYXMLSEC_DECLARE_NAMESPACE(var, name) \ diff --git a/src/ds.c b/src/ds.c index d0b4bdf9..7623fbcf 100644 --- a/src/ds.c +++ b/src/ds.c @@ -517,6 +517,40 @@ static PyObject* PyXmlSec_SignatureContextSetEnabledKeyData(PyObject* self, PyOb return NULL; } +static const char PyXmlSec_SignatureContextEnabledReferenceUris__doc__[] = \ + "Bitmask of :samp:`TransformUriType{XXX}` constants allowed in :xml:`` URI.\n\n" + ".. note:: since xmlsec1 1.3.13 only empty and same-document URIs are enabled by default. " + "Dereferencing local or remote URIs (e.g. through :func:`~xmlsec.register_callbacks`) requires " + "an explicit opt-in.\n"; +static PyObject* PyXmlSec_SignatureContextEnabledReferenceUrisGet(PyObject* self, void* closure) { + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; + return PyLong_FromUnsignedLong(ctx->handle->enabledReferenceUris); +} + +static int PyXmlSec_SignatureContextEnabledReferenceUrisSet(PyObject* self, PyObject* value, void* closure) { + PyXmlSec_SignatureContext* ctx = (PyXmlSec_SignatureContext*)self; + unsigned long uris; + + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "cannot delete enabled_reference_uris."); + return -1; + } + if (!PyLong_Check(value)) { + PyErr_SetString(PyExc_TypeError, "instance of *int* expected."); + return -1; + } + uris = PyLong_AsUnsignedLong(value); + if (PyErr_Occurred()) { + return -1; + } + if (uris > xmlSecTransformUriTypeAny) { + PyErr_SetString(PyExc_ValueError, "invalid uri type mask."); + return -1; + } + ctx->handle->enabledReferenceUris = (xmlSecTransformUriType)uris; + return 0; +} + static PyGetSetDef PyXmlSec_SignatureContextGetSet[] = { { "key", @@ -525,6 +559,13 @@ static PyGetSetDef PyXmlSec_SignatureContextGetSet[] = { (char*)PyXmlSec_SignatureContextKey__doc__, NULL }, + { + "enabled_reference_uris", + (getter)PyXmlSec_SignatureContextEnabledReferenceUrisGet, + (setter)PyXmlSec_SignatureContextEnabledReferenceUrisSet, + (char*)PyXmlSec_SignatureContextEnabledReferenceUris__doc__, + NULL + }, {NULL} /* Sentinel */ }; diff --git a/src/enc.c b/src/enc.c index 42195dd3..e85e4d0f 100644 --- a/src/enc.c +++ b/src/enc.c @@ -62,6 +62,17 @@ static int PyXmlSec_EncryptionContext__init__(PyObject* self, PyObject* args, Py ctx->handle->keyInfoReadCtx.flags = XMLSEC_KEYINFO_FLAGS_LAX_KEY_SEARCH; ctx->handle->keyInfoWriteCtx.flags = XMLSEC_KEYINFO_FLAGS_LAX_KEY_SEARCH; + // xmlsec 1.3.13 no longer copies these flags to the read context of the nested + // context used to encrypt , so the session key encryption key + // lookup becomes strict. Create the nested context upfront (it is reused by + // xmlsec) and enable lax key search on it as well. + if (xmlSecKeyInfoCtxCreateEncCtx(&(ctx->handle->keyInfoWriteCtx)) < 0) { + // ctx->manager is owned by ctx now and released on dealloc + PyXmlSec_SetLastError("failed to create the nested encryption context"); + return -1; + } + ctx->handle->keyInfoWriteCtx.encCtx->keyInfoReadCtx.flags = XMLSEC_KEYINFO_FLAGS_LAX_KEY_SEARCH; + return 0; ON_FAIL: PYXMLSEC_DEBUGF("%p: init enc context - failed", self); diff --git a/src/main.c b/src/main.c index d0a43824..b91365e0 100644 --- a/src/main.c +++ b/src/main.c @@ -57,6 +57,12 @@ static void PyXmlSec_Free(int what) { } static int PyXmlSec_Init(void) { + // Already initialized (e.g. on module import). Since xmlsec1 1.3.13, + // calling xmlSecInit() again without a shutdown fails. + if (free_mode == _PYXMLSEC_FREE_ALL) { + return 0; + } + if (xmlSecInit() < 0) { PyXmlSec_SetLastError("cannot initialize xmlsec library."); PyXmlSec_Free(_PYXMLSEC_FREE_NONE); diff --git a/src/xmlsec/__init__.pyi b/src/xmlsec/__init__.pyi index 9cfc8cc6..6b7260ea 100644 --- a/src/xmlsec/__init__.pyi +++ b/src/xmlsec/__init__.pyi @@ -68,6 +68,7 @@ class KeysManager: class SignatureContext: key: Key | None + enabled_reference_uris: int def enable_reference_transform(self, transform: Transform) -> None: ... def enable_signature_transform(self, transform: Transform) -> None: ... def register_id(self, node: _Element, id_attr: str = ..., id_ns: str | None = ...) -> None: ... diff --git a/src/xmlsec/constants.pyi b/src/xmlsec/constants.pyi index a9254ddd..fda62320 100644 --- a/src/xmlsec/constants.pyi +++ b/src/xmlsec/constants.pyi @@ -129,6 +129,12 @@ TransformSha224: Final[__Transform] TransformSha256: Final[__Transform] TransformSha384: Final[__Transform] TransformSha512: Final[__Transform] +TransformUriTypeAny: Final[int] +TransformUriTypeEmpty: Final[int] +TransformUriTypeLocal: Final[int] +TransformUriTypeNone: Final[int] +TransformUriTypeRemote: Final[int] +TransformUriTypeSameDocument: Final[int] TransformUsageAny: Final[int] TransformUsageC14NMethod: Final[int] TransformUsageDSigTransform: Final[int] diff --git a/tests/test_ds.py b/tests/test_ds.py index dd0657d3..519c5f22 100644 --- a/tests/test_ds.py +++ b/tests/test_ds.py @@ -39,6 +39,23 @@ def test_set_key_bad_type(self): with self.assertRaisesRegex(TypeError, r'instance of \*xmlsec.Key\* expected.'): ctx.key = '' + def test_set_enabled_reference_uris(self): + ctx = xmlsec.SignatureContext() + uris = consts.TransformUriTypeEmpty | consts.TransformUriTypeSameDocument | consts.TransformUriTypeRemote + ctx.enabled_reference_uris = uris + self.assertEqual(ctx.enabled_reference_uris, uris) + + def test_set_enabled_reference_uris_bad_args(self): + ctx = xmlsec.SignatureContext() + with self.assertRaisesRegex(TypeError, r'instance of \*int\* expected.'): + ctx.enabled_reference_uris = '' + with self.assertRaises(OverflowError): + ctx.enabled_reference_uris = -1 + with self.assertRaisesRegex(ValueError, 'invalid uri type mask'): + ctx.enabled_reference_uris = consts.TransformUriTypeAny + 1 + with self.assertRaisesRegex(TypeError, 'cannot delete'): + del ctx.enabled_reference_uris + def test_set_invalid_key(self): ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) with self.assertRaisesRegex(TypeError, 'empty key.'): diff --git a/tests/test_main.py b/tests/test_main.py index 8f1501f2..a63a4871 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -48,6 +48,8 @@ def _sign_doc(self): xmlsec.template.add_reference(sign, consts.TransformSha1, uri='cid:123456') ctx = xmlsec.SignatureContext() + # xmlsec1 >= 1.3.13 only allows empty and same-document reference URIs by default + ctx.enabled_reference_uris = consts.TransformUriTypeAny ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) ctx.sign(sign) return sign From 2bcd392135e215ce71c437615ac74f0ca8d3f8af Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Tue, 27 Oct 2026 16:28:02 +0100 Subject: [PATCH 3/7] Bump bundled xmlsec1 to 1.3.13 for non-Windows builds --- .github/workflows/cache_libs.yml | 2 +- build_support/lib_xmlsec_dependency_builder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cache_libs.yml b/.github/workflows/cache_libs.yml index 79b76f98..98ed1a4e 100644 --- a/.github/workflows/cache_libs.yml +++ b/.github/workflows/cache_libs.yml @@ -20,7 +20,7 @@ on: required: false type: string XMLSEC1_VERSION: - default: "1.3.12" + default: "1.3.13" required: false type: string ZLIB_VERSION: diff --git a/build_support/lib_xmlsec_dependency_builder.py b/build_support/lib_xmlsec_dependency_builder.py index b50513e1..ca6195dc 100644 --- a/build_support/lib_xmlsec_dependency_builder.py +++ b/build_support/lib_xmlsec_dependency_builder.py @@ -50,7 +50,7 @@ class LibXmlsecDependencyBuilder: 'libxml2_version': '2.14.6', # Make sure it matches with lxml 'libxslt_version': '1.1.43', # 1.1.44+ requires libxml2 >= 2.15.1 'openssl_version': '3.6.5', - 'xmlsec1_version': '1.3.12', + 'xmlsec1_version': '1.3.13', 'zlib_version': '1.3.2', } WINDOWS_DEFAULT_LIB_VERSIONS: ClassVar[dict[str, str]] = { From 782c95a358f6adf005e43e6bdadb3f9e380f97db Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Tue, 27 Oct 2026 18:24:27 +0100 Subject: [PATCH 4/7] Disable xmlsec1 apps in static builds xmlsec 1.3.13 adds OSS-Fuzz harnesses under apps/ whose CFLAGS omit $(LIBXSLT_CFLAGS), while xmlsec/transforms.h includes . In static builds libxslt is installed into a private prefix, so building the fuzzers fails with "libxslt/security.h: No such file or directory". We only need the libraries, not the xmlsec1 CLI, unit tests or fuzzers, so configure with --disable-apps (also supported by older xmlsec1). --- build_support/lib_xmlsec_dependency_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/build_support/lib_xmlsec_dependency_builder.py b/build_support/lib_xmlsec_dependency_builder.py index ca6195dc..93388087 100644 --- a/build_support/lib_xmlsec_dependency_builder.py +++ b/build_support/lib_xmlsec_dependency_builder.py @@ -390,6 +390,7 @@ def _build_xmlsec1(self, env, prefix_arg, host_arg): './configure', prefix_arg, '--disable-shared', + '--disable-apps', '--disable-gost', '--enable-md5', '--enable-ripemd160', From 1dfad0759ff8fa0a9d1ef350d1e17e2bf11090f5 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Wed, 28 Oct 2026 09:28:31 +0100 Subject: [PATCH 5/7] Build static xmlsec1 with --with-pic xmlsec 1.3.13 enables hardening flags by default and appends -fPIE after the user-provided CFLAGS, overriding our -fPIC. The resulting static libxmlsec1.a cannot be linked into the Python extension module ("relocation R_X86_64_PC32 against symbol ... can not be used when making a shared object; recompile with -fPIC"). Pass --with-pic so libtool appends -fPIC last when compiling the static objects, which keeps the other hardening flags intact. --- build_support/lib_xmlsec_dependency_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/build_support/lib_xmlsec_dependency_builder.py b/build_support/lib_xmlsec_dependency_builder.py index 93388087..938356d8 100644 --- a/build_support/lib_xmlsec_dependency_builder.py +++ b/build_support/lib_xmlsec_dependency_builder.py @@ -390,6 +390,7 @@ def _build_xmlsec1(self, env, prefix_arg, host_arg): './configure', prefix_arg, '--disable-shared', + '--with-pic', '--disable-apps', '--disable-gost', '--enable-md5', From a8b66445e0e8453fdba0f6d586ba7710a8d41d8d Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Wed, 28 Oct 2026 12:05:27 +0100 Subject: [PATCH 6/7] Align TransformUriType Local/Remote docs with xmlsec xmlsec classifies only "file://" URIs as Local; every other non-empty, non-"#" URI (including relative paths such as "other.xml") is Remote. Release notes: Wheels now bundle xmlsec1 1.3.13, which by default only allows empty and same-document URIs (URI="", URI="#id") in , and . To sign or verify references to external URIs (e.g. through xmlsec.register_callbacks), set SignatureContext.enabled_reference_uris using the new xmlsec.constants.TransformUriType* constants. --- doc/source/modules/constants.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/modules/constants.rst b/doc/source/modules/constants.rst index a6f3ac51..23fafd9e 100644 --- a/doc/source/modules/constants.rst +++ b/doc/source/modules/constants.rst @@ -306,11 +306,12 @@ Transforms .. data:: xmlsec.constants.TransformUriTypeLocal - Local file URI (:xml:`URI="file:///path"` or a URI without a scheme). + Local file URI (:xml:`URI="file:///path"`). .. data:: xmlsec.constants.TransformUriTypeRemote - Remote URI (any other scheme, e.g. :xml:`URI="http://..."` or :xml:`URI="cid:..."`). + Any other non-empty URI, e.g. :xml:`URI="http://..."`, :xml:`URI="cid:..."` + or a relative path such as :xml:`URI="other.xml"`. .. data:: xmlsec.constants.TransformUriTypeAny From 0b80dba101f372411c225c9a5f718d8a851a5d90 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Wed, 28 Oct 2026 14:12:39 +0100 Subject: [PATCH 7/7] Bump bundled xmlsec1 to 1.3.13 for Windows builds --- build_support/lib_xmlsec_dependency_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_support/lib_xmlsec_dependency_builder.py b/build_support/lib_xmlsec_dependency_builder.py index 938356d8..db624c96 100644 --- a/build_support/lib_xmlsec_dependency_builder.py +++ b/build_support/lib_xmlsec_dependency_builder.py @@ -36,7 +36,7 @@ def triplet(self) -> str: class LibXmlsecDependencyBuilder: - WINDOWS_LIBS_DOWNLOAD_RELEASE_URL = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2026.10.23/' + WINDOWS_LIBS_DOWNLOAD_RELEASE_URL = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2026.10.28/' LIB_VERSION_ENV_VARS: ClassVar[dict[str, str]] = { 'libiconv_version': 'PYXMLSEC_LIBICONV_VERSION', 'libxml2_version': 'PYXMLSEC_LIBXML2_VERSION', @@ -58,7 +58,7 @@ class LibXmlsecDependencyBuilder: 'libxml2_version': '2.11.9-8', # Make sure it matches with lxml 'libxslt_version': '1.1.45', 'openssl_version': '3.5.9', - 'xmlsec1_version': '1.3.12', + 'xmlsec1_version': '1.3.13', 'zlib_version': '1.3.2', }