Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,21 @@ The `SignedXml` constructor provides an abstraction for sign and verify xml docu
- `keyInfoAttributes` - object - default `{}` - a hash of attributes and values `attrName: value` to add to the KeyInfo node
- `getKeyInfoContent` - function - default `noop` - a function that returns the content of the KeyInfo node
- `getCertFromKeyInfo` - function - default `SignedXml.getCertFromKeyInfo` - a function that returns the certificate from the `<KeyInfo />` node
- `getObjectContent` - function - default `noop` - a function that returns the content of the `<Object/>` nodes

#### API

A `SignedXml` object provides the following methods:

To sign xml documents:

- `addReference(xpath, transforms, digestAlgorithm)` - adds a reference to a xml element where:
- `addReference({ xpath, transforms, digestAlgorithm, isSignatureReference, id, type })` - adds a reference to a xml element where:
- `xpath` - a string containing a XPath expression referencing a xml element
- `transforms` - an array of [transform algorithms](#canonicalization-and-transformation-algorithms), the referenced element will be transformed for each value in the array
- `digestAlgorithm` - one of the supported [hashing algorithms](#hashing-algorithms)
- `isSignatureReference` - boolean - default `false` - indicates whether the target of this reference is located inside the `<Signature>` element (e.g. an `<Object>`)
- `id` - an optional `Id` attribute to add to the reference element
- `type` - the optional `Type` attribute to add to the reference element (represented as a URI)
- `computeSignature(xml, [options])` - compute the signature of the given xml where:
- `xml` - a string containing a xml document
- `options` - an object with the following properties:
Expand Down Expand Up @@ -534,6 +538,44 @@ sig.computeSignature(xml, {
});
```

### how to add custom Objects to the signature

Use the `getObjectContent` option when creating a SignedXml instance to add custom Objects to the signature. You can also reference these Objects in your signature by setting `isSignatureReference` to `true` when adding a reference.

```javascript
var SignedXml = require("xml-crypto").SignedXml,
fs = require("fs");

var xml = "<library>" + "<book>" + "<name>Harry Potter</name>" + "</book>" + "</library>";

const sig = new SignedXml({
privateKey: fs.readFileSync("client.pem"),
canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
getObjectContent: () => [
{
content: "<TestObject>Test data in Object</TestObject>",
attributes: {
Id: "Object1",
MimeType: "text/xml",
},
},
],
});

// Add a reference to the Object element
sig.addReference({
xpath: "//*[@Id='Object1']",
digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
// IMPORTANT: Set isSignatureReference to true to indicate this is a reference to an element inside the Signature
isSignatureReference: true,
});

sig.computeSignature(xml);
fs.writeFileSync("signed.xml", sig.getSignedXml());
```

### more examples (_coming soon_)

## Development
Expand Down
196 changes: 194 additions & 2 deletions src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class SignedXml {
keyInfoAttributes: { [attrName: string]: string } = {};
getKeyInfoContent = SignedXml.getKeyInfoContent;
getCertFromKeyInfo = SignedXml.getCertFromKeyInfo;
getObjectContent = SignedXml.getObjectContent;

// Internal state
private id = 0;
Expand Down Expand Up @@ -126,6 +127,16 @@ export class SignedXml {

static noop = () => null;

/**
* Default implementation for getObjectContent that returns null (no Objects)
*/
static getObjectContent(): Array<{
content: string;
attributes?: Record<string, string | undefined>;
}> | null {
return null;
}

/**
* The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using
* @param options {@link SignedXmlOptions}
Expand All @@ -143,6 +154,7 @@ export class SignedXml {
keyInfoAttributes,
getKeyInfoContent,
getCertFromKeyInfo,
getObjectContent,
} = options;

// Options
Expand All @@ -164,6 +176,7 @@ export class SignedXml {
this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes;
this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent;
this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop;
this.getObjectContent = getObjectContent ?? this.getObjectContent;
this.CanonicalizationAlgorithms;
this.HashAlgorithms;
this.SignatureAlgorithms;
Expand Down Expand Up @@ -796,6 +809,9 @@ export class SignedXml {
* @param digestValue The expected digest value for the reference.
* @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization.
* @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`.
* @param isSignatureReference Indicates whether this reference points to an element in the signature itself (like an Object element).
* @param id An optional `Id` attribute for the reference.
* @param type An optional `Type` attribute for the reference.
*/
addReference({
xpath,
Expand All @@ -805,6 +821,9 @@ export class SignedXml {
digestValue,
inclusiveNamespacesPrefixList = [],
isEmptyUri = false,
isSignatureReference = false,
id = undefined,
type = undefined,
}: Partial<Reference> & Pick<Reference, "xpath">): void {
if (digestAlgorithm == null) {
throw new Error("digestAlgorithm is required");
Expand All @@ -822,6 +841,9 @@ export class SignedXml {
digestValue,
inclusiveNamespacesPrefixList,
isEmptyUri,
isSignatureReference,
id,
type,
getValidatedNode: () => {
throw new Error(
"Reference has not been validated yet; Did you call `sig.checkSignature()`?",
Expand Down Expand Up @@ -965,6 +987,7 @@ export class SignedXml {

signatureXml += this.createSignedInfo(doc, prefix);
signatureXml += this.getKeyInfo(prefix);
signatureXml += this.getObjects(prefix);
signatureXml += `</${currentPrefix}Signature>`;

this.originalXmlWithIds = doc.toString();
Expand All @@ -979,6 +1002,9 @@ export class SignedXml {
const dummySignatureWrapper = `<Dummy ${existingPrefixesString}>${signatureXml}</Dummy>`;
const nodeXml = new xmldom.DOMParser().parseFromString(dummySignatureWrapper);

// Process any signature references after the signature has been created
this.processSignatureReferences(nodeXml, prefix);

// Because we are using a dummy wrapper hack described above, we know there will be a `firstChild`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const signatureDoc = nodeXml.documentElement.firstChild!;
Expand Down Expand Up @@ -1070,6 +1096,39 @@ export class SignedXml {
return "";
}

/**
* Creates XML for Object elements to be included in the signature
*
* @param prefix Optional namespace prefix
* @returns XML string with Object elements or empty string if none
*/
private getObjects(prefix?: string) {
const currentPrefix = prefix ? `${prefix}:` : "";
const objects = this.getObjectContent?.();

if (!objects || objects.length === 0) {
return "";
}

let result = "";

for (const obj of objects) {
let objectAttrs = "";
if (obj.attributes) {
Object.keys(obj.attributes).forEach((name) => {
const value = obj.attributes?.[name];
if (value !== undefined) {
objectAttrs += ` ${name}="${value}"`;
}
});
}

result += `<${currentPrefix}Object${objectAttrs}>${obj.content}</${currentPrefix}Object>`;
}

return result;
}

/**
* Generate the Reference nodes (as part of the signature process)
*
Expand All @@ -1082,6 +1141,10 @@ export class SignedXml {

/* eslint-disable-next-line deprecation/deprecation */
for (const ref of this.getReferences()) {
if (ref.isSignatureReference) {
// For signature references, we'll handle them separately after the signature is created
continue;
}
const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver);

if (!utils.isArrayHasLength(nodes)) {
Expand All @@ -1091,13 +1154,25 @@ export class SignedXml {
}

for (const node of nodes) {
let referenceAttrs = "";

if (ref.isEmptyUri) {
res += `<${prefix}Reference URI="">`;
referenceAttrs = 'URI=""';
} else {
const id = this.ensureHasId(node);
ref.uri = id;
res += `<${prefix}Reference URI="#${id}">`;
referenceAttrs = `URI="#${id}"`;
}

if (ref.id) {
referenceAttrs += ` Id="${ref.id}"`;
}

if (ref.type) {
referenceAttrs += ` Type="${ref.type}"`;
}

res += `<${prefix}Reference ${referenceAttrs}>`;
res += `<${prefix}Transforms>`;
for (const trans of ref.transforms || []) {
const transform = this.findCanonicalizationAlgorithm(trans);
Expand Down Expand Up @@ -1262,6 +1337,123 @@ export class SignedXml {
return doc.documentElement.firstChild!;
}

/**
* Process references that point to elements within the Signature element
* This is called after the initial signature has been created
*/
private processSignatureReferences(signatureDoc: Document, prefix?: string) {
// Get signature references
const signatureReferences = this.references.filter((ref) => ref.isSignatureReference);
if (signatureReferences.length === 0) {
return;
}

prefix = prefix || "";
prefix = prefix ? `${prefix}:` : prefix;
const signatureNamespace = "http://www.w3.org/2000/09/xmldsig#";

// Find the SignedInfo element to append to
const signedInfoNode = xpath.select1(
`.//*[local-name(.)='SignedInfo']`,
signatureDoc,
) as Element;
if (!signedInfoNode) {
throw new Error("Could not find SignedInfo element in signature");
}

// Process each signature reference
for (const ref of signatureReferences) {
const nodes = xpath.selectWithResolver(ref.xpath ?? "", signatureDoc, this.namespaceResolver);

if (!utils.isArrayHasLength(nodes)) {
throw new Error(
`the following xpath cannot be signed because it was not found: ${ref.xpath}`,
);
}

// Process the reference
for (const node of nodes) {
// Create the reference element directly using DOM methods to avoid namespace issues
const referenceElem = signatureDoc.createElementNS(
signatureNamespace,
`${prefix}Reference`,
);
if (ref.isEmptyUri) {
referenceElem.setAttribute("URI", "");
} else {
const id = this.ensureHasId(node);
ref.uri = id;
referenceElem.setAttribute("URI", `#${id}`);
}

if (ref.id) {
referenceElem.setAttribute("Id", ref.id);
}

if (ref.type) {
referenceElem.setAttribute("Type", ref.type);
}

const transformsElem = signatureDoc.createElementNS(
signatureNamespace,
`${prefix}Transforms`,
);

for (const trans of ref.transforms || []) {
const transform = this.findCanonicalizationAlgorithm(trans);
const transformElem = signatureDoc.createElementNS(
signatureNamespace,
`${prefix}Transform`,
);
transformElem.setAttribute("Algorithm", transform.getAlgorithmName());

if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) {
const inclusiveNamespacesElem = signatureDoc.createElementNS(
transform.getAlgorithmName(),
"InclusiveNamespaces",
);
inclusiveNamespacesElem.setAttribute(
"PrefixList",
ref.inclusiveNamespacesPrefixList.join(" "),
);
transformElem.appendChild(inclusiveNamespacesElem);
}

transformsElem.appendChild(transformElem);
}

// Get the canonicalized XML
const canonXml = this.getCanonReferenceXml(signatureDoc, ref, node);

// Calculate the digest
const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm);
const digestValue = digestAlgorithm.getHash(canonXml);

// Store the digest value for later validation
ref.digestValue = digestValue;

const digestMethodElem = signatureDoc.createElementNS(
signatureNamespace,
`${prefix}DigestMethod`,
);
digestMethodElem.setAttribute("Algorithm", digestAlgorithm.getAlgorithmName());

const digestValueElem = signatureDoc.createElementNS(
signatureNamespace,
`${prefix}DigestValue`,
);
digestValueElem.textContent = digestValue;

referenceElem.appendChild(transformsElem);
referenceElem.appendChild(digestMethodElem);
referenceElem.appendChild(digestValueElem);

// Append the reference element to SignedInfo
signedInfoNode.appendChild(referenceElem);
}
}
}

/**
* Returns just the signature part, must be called only after {@link computeSignature}
*
Expand Down
Loading