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(""); + }); });