diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts
index a5d308b..eae579f 100644
--- a/test/c14n-non-exclusive-unit-tests.spec.ts
+++ b/test/c14n-non-exclusive-unit-tests.spec.ts
@@ -250,6 +250,70 @@ describe("C14N non-exclusive canonicalization tests", function () {
});
for (const Canonicalization of [C14nCanonicalization, C14nCanonicalizationWithComments]) {
+ describe(`${Canonicalization.name}: hoisted ancestor namespaces`, function () {
+ // A hoisted ancestor default namespace becomes the default the subset's descendants are
+ // canonicalized against. The rationale is spelled out in §4.7: the empty default is kept on
+ // output "so that e3 does not take on the default namespace qualification of e1".
+ // https://www.w3.org/TR/xml-c14n/#ProcessingModel
+ // https://www.w3.org/TR/xml-c14n/#PropagateDefaultNSDecl
+ it('renders xmlns="" on a descendant when the apex hoists an ancestor default namespace', function () {
+ test_C14nCanonicalization(
+ '',
+ "//*[local-name()='x']",
+ '',
+ new Canonicalization(),
+ );
+ });
+
+ it("omits a descendant declaration the hoisted ancestor default namespace makes redundant", function () {
+ test_C14nCanonicalization(
+ '',
+ "//*[local-name()='x']",
+ '',
+ new Canonicalization(),
+ );
+ });
+
+ it("renders the hoisted ancestor default namespace once when the apex inherits it", function () {
+ test_C14nCanonicalization(
+ '',
+ "//*[local-name()='x']",
+ '',
+ new Canonicalization(),
+ );
+ });
+
+ it("prefers the apex's own default namespace over the hoisted ancestor one", function () {
+ test_C14nCanonicalization(
+ '',
+ "//*[local-name()='x']",
+ '',
+ new Canonicalization(),
+ );
+ });
+
+ // A declaration on the apex shadows the ancestor one binding the same prefix, so the
+ // element must not resolve against the outer binding — in its descendants or its own name.
+ // https://www.w3.org/TR/REC-xml-names/#scoping
+ it("renders the apex's redeclaration of an ancestor prefix, not the ancestor binding", function () {
+ test_C14nCanonicalization(
+ '',
+ "//*[local-name()='x']",
+ '',
+ new Canonicalization(),
+ );
+ });
+
+ it("renders the apex's redeclaration of the prefix in its own name", function () {
+ test_C14nCanonicalization(
+ '',
+ "//*[local-name()='x']",
+ '',
+ new Canonicalization(),
+ );
+ });
+ });
+
describe(`${Canonicalization.name}: subset namespace declarations`, function () {
it("does not duplicate the default namespace when the subset declares a prefixed namespace", function () {
// Render the inherited default namespace exactly once on the subset root.
diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts
index effc4e0..578e2bf 100644
--- a/test/signature-integration-tests.spec.ts
+++ b/test/signature-integration-tests.spec.ts
@@ -341,4 +341,42 @@ describe("Signature integration tests", function () {
});
});
}
+
+ // A signed reference must hand back the element in the namespace it was signed in. If a
+ // hoisted ancestor default namespace leaks into a descendant, the signature still verifies
+ // while `getSignedReferences()` reports an identity the sender never signed — and a caller
+ // that dispatches on element namespace acts on it.
+ // https://www.w3.org/TR/xml-c14n/#ProcessingModel
+ it("does not move an element into a namespace it was not signed in", function () {
+ const c14n = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
+ const xml = '';
+
+ const sig = new SignedXml();
+ sig.privateKey = fs.readFileSync("./test/static/client.pem");
+ sig.canonicalizationAlgorithm = c14n;
+ sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
+ sig.addReference({
+ xpath: "//*[local-name(.)='x']",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ transforms: [c14n],
+ });
+ sig.computeSignature(xml);
+ const signed = sig.getSignedXml();
+
+ const signature = xpath.select1(
+ "//*[local-name(.)='Signature']",
+ new xmldom.DOMParser().parseFromString(signed),
+ );
+ isDomNode.assertIsNodeLike(signature);
+
+ const verify = new SignedXml();
+ verify.publicCert = fs.readFileSync("./test/static/client_public.pem");
+ verify.loadSignature(signature);
+ expect(verify.checkSignature(signed)).to.be.true;
+
+ const trusted = new xmldom.DOMParser().parseFromString(verify.getSignedReferences()[0]);
+ const y = xpath.select1("//*[local-name(.)='y']", trusted);
+ isDomNode.assertIsElementNode(y);
+ expect(y.namespaceURI ?? "", " must stay in no namespace").to.equal("");
+ });
});