Skip to content

Introduce new .getSignedReferences() function of signature to help prevent signature wrapping attacks #489

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 9, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 6.1.0 (...)

# Changelog

- Introduced new .signedReferences property of signature to help prevent signature wrapping attacks.
- After calling .checkSignature() with your public certificate, obtain .signedReferences to use. Array of signed strings by the certificate


## 6.0.0 (2024-01-26)
=======

## 6.0.1 (2025-03-14)

- Address CVEs: CVE-2025-29774 and CVE-2025-29775
Expand Down
61 changes: 32 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# xml-crypto


# Upgrading

The `.getReferences() AND the .references` API is deprecated.
Please do not attempt to access it. The content in there should be treated as unsigned.

Instead, we strongly encourage users to migrate to the .signedReferences API. See the `Verifying XML document` section
We understand that this may take a lot of efforts to migrate, feel free to ask for help.
This will help prevent future XML signature wrapping attacks in the future.

``


![Build](https://github.com/node-saml/xml-crypto/actions/workflows/ci.yml/badge.svg)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/)

Expand Down Expand Up @@ -159,8 +172,13 @@ var select = require("xml-crypto").xpath,
fs = require("fs");

var xml = fs.readFileSync("signed.xml").toString();

var doc = new dom().parseFromString(xml);

// DO NOT attempt to parse whatever data object you have here
// i.e. BAD: parseAssertion(doc),
// good: see below

var signature = select(
doc,
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
Expand All @@ -172,44 +190,29 @@ try {
} catch (ex) {
console.log(ex);
}


```

In order to protect from some attacks we must check the content we want to use is the one that has been signed:

```javascript
// Roll your own
const elem = xpath.select("/xpath_to_interesting_element", doc);
const uri = sig.getReferences()[0].uri; // might not be 0; it depends on the document
const id = uri[0] === "#" ? uri.substring(1) : uri;
if (
elem.getAttribute("ID") != id &&
elem.getAttribute("Id") != id &&
elem.getAttribute("id") != id
) {
throw new Error("The interesting element was not the one verified by the signature");
if (!res) {
throw "Invalid Signature"
}
// good: The XML Signature has been verified, meaning some subset of XML is verified.
var signedBytes = sig.signedReferences;

// Get the validated element directly from a reference
const elem = sig.references[0].getValidatedElement(); // might not be 0; it depends on the document
const matchingReference = xpath.select1("/xpath_to_interesting_element", elem);
if (!isDomNode.isNodeLike(matchingReference)) {
throw new Error("The interesting element was not the one verified by the signature");
}
var authenticatedDoc = new dom().parseFromString(signedBytes[0]); // take the first of the signed references
// load SAML or whatever from now
// obtain the assertion XML from here
// use only authenticated data
let signedAssertionNode = extractAssertion(authenticatedDoc);
let parsedAssertion = parseAssertion(signedAssertionNode)
return parsedAssertion; // now return the client, the signed Assertion

// Use the built-in method
const elem = xpath.select1("/xpath_to_interesting_element", doc);
try {
const matchingReference = sig.validateElementAgainstReferences(elem, doc);
} catch {
throw new Error("The interesting element was not the one verified by the signature");
}

// Use the built-in method with a an xpath expression
try {
const matchingReference = sig.validateReferenceWithXPath("/xpath_to_interesting_element", doc);
} catch {
throw new Error("The interesting element was not the one verified by the signature");
}
// BAD example: DO not use the .getReferences() API.
```

Note:
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export { SignedXml } from "./signed-xml";
export { C14nCanonicalization, C14nCanonicalizationWithComments } from "./c14n-canonicalization";
export {
ExclusiveCanonicalization,
ExclusiveCanonicalizationWithComments,
} from "./exclusive-canonicalization";
export * from "./utils";
export * from "./types";
51 changes: 44 additions & 7 deletions src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as signatureAlgorithms from "./signature-algorithms";
import * as crypto from "crypto";
import * as isDomNode from "@xmldom/is-dom-node";


export class SignedXml {
idMode?: "wssecurity";
idAttributes: string[];
Expand Down Expand Up @@ -87,6 +88,8 @@ export class SignedXml {
"http://www.w3.org/2000/09/xmldsig#enveloped-signature": envelopedSignatures.EnvelopedSignature,
};

// TODO: In V7.x we may consider deprecating sha1

/**
* To add a new hash algorithm create a new class that implements the {@link HashAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms}
*/
Expand All @@ -96,6 +99,8 @@ export class SignedXml {
"http://www.w3.org/2001/04/xmlenc#sha512": hashAlgorithms.Sha512,
};

// TODO: In V7.x we may consider deprecating sha1

/**
* To add a new signature algorithm create a new class that implements the {@link SignatureAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms}
*/
Expand All @@ -112,6 +117,7 @@ export class SignedXml {
};

static noop = () => null;
public signedReferences: string[] = [];

/**
* The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using
Expand Down Expand Up @@ -154,6 +160,9 @@ export class SignedXml {
this.CanonicalizationAlgorithms;
this.HashAlgorithms;
this.SignatureAlgorithms;
// this populates only after verifying the signature
// array of bytes that are cryptographically authenticated
this.signedReferences = []; // TODO: should we rename this to something better.
}

/**
Expand Down Expand Up @@ -252,6 +261,7 @@ export class SignedXml {
this.signedXml = xml;

const doc = new xmldom.DOMParser().parseFromString(xml);

// Reset the references as only references from our re-parsed signedInfo node can be trusted
this.references = [];

Expand Down Expand Up @@ -307,25 +317,45 @@ export class SignedXml {
return false;
}

// (Stage B authentication step, show that the signedInfoCanon is signed)

// first find the key & signature algorithm, this should match
// Stage B: Take the signature algorithm and key and verify the SignatureValue against the canonicalized SignedInfo
const signer = this.findSignatureAlgorithm(this.signatureAlgorithm);
const key = this.getCertFromKeyInfo(this.keyInfo) || this.publicCert || this.privateKey;
if (key == null) {
throw new Error("KeyInfo or publicCert or privateKey is required to validate signature");
}

if (callback) {
signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue, callback);

// let's clear the callback up a little bit, so we can access it's results,
// and decide whether to reset signature value or not
const sigRes = signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue);
// true case
if (sigRes === true) {
if (callback) {
callback(null, true);
} else {
return true;
}
} else {
const verified = signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue);
// false case
// reset the signedReferences back to empty array
// I would have preferred to start by verifying the signedInfoCanon first, if that's OK
// but that may cause some breaking changes?
this.signedReferences = [];


if (verified === false) {
if (callback) {
callback(new Error(
`invalid signature: the signature value ${this.signatureValue} is incorrect`,
));
return; // return early
} else {
throw new Error(
`invalid signature: the signature value ${this.signatureValue} is incorrect`,
);
}

return true;
}
}

Expand Down Expand Up @@ -525,6 +555,11 @@ export class SignedXml {

return false;
}
// This step can only be done after we have verified the signedInfo
// we verified that they have same hash
// so, the canonXml and only the canonXml can be trusted
// append this to signedReferences
this.signedReferences.push(canonXml);

return true;
}
Expand Down Expand Up @@ -575,6 +610,7 @@ export class SignedXml {

const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo");
if (!utils.isArrayHasLength(signedInfoNodes)) {

throw new Error("no signed info node found");
}
if (signedInfoNodes.length > 1) {
Expand Down Expand Up @@ -655,6 +691,7 @@ export class SignedXml {
if (nodes.length === 0) {
throw new Error(`could not find DigestValue node in reference ${refNode.toString()}`);
}

if (nodes.length > 1) {
throw new Error(
`could not load reference for a node that contains multiple DigestValue nodes: ${refNode.toString()}`,
Expand Down Expand Up @@ -713,7 +750,7 @@ export class SignedXml {
) {
transforms.push("http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
}
const refUri = isDomNode.isElementNode(refNode)
const refUri = isDomNode.isElementNode(refNode)
? refNode.getAttribute("URI") || undefined
: undefined;

Expand Down