Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,20 @@ 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
- `objects` - array - default `undefined` - an array of objects defining 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, 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)
- `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 +537,42 @@ sig.computeSignature(xml, {
});
```

### How to add custom Objects to the signature

Use the `objects` option when creating a SignedXml instance to add custom Objects to the signature.

```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",
objects: [
{
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#"],
});

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

### more examples (_coming soon_)

## Development
Expand Down
184 changes: 178 additions & 6 deletions src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
GetKeyInfoContentArgs,
HashAlgorithm,
HashAlgorithmType,
ObjectAttributes,
Reference,
SignatureAlgorithm,
SignatureAlgorithmType,
Expand Down Expand Up @@ -56,6 +57,7 @@ export class SignedXml {
keyInfoAttributes: { [attrName: string]: string } = {};
getKeyInfoContent = SignedXml.getKeyInfoContent;
getCertFromKeyInfo = SignedXml.getCertFromKeyInfo;
objects?: Array<{ content: string; attributes?: ObjectAttributes }>;

// Internal state
private id = 0;
Expand All @@ -70,7 +72,7 @@ export class SignedXml {
* Contains the references that were signed.
* @see {@link Reference}
*/
private references: Reference[] = [];
private references: (Reference & { wasProcessed: boolean })[] = [];

/**
* Contains the canonicalized XML of the references that were validly signed.
Expand Down Expand Up @@ -143,6 +145,7 @@ export class SignedXml {
keyInfoAttributes,
getKeyInfoContent,
getCertFromKeyInfo,
objects,
} = options;

// Options
Expand All @@ -164,6 +167,7 @@ export class SignedXml {
this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes;
this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent;
this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop;
this.objects = objects;
this.CanonicalizationAlgorithms;
this.HashAlgorithms;
this.SignatureAlgorithms;
Expand Down Expand Up @@ -796,6 +800,8 @@ 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 id An optional `Id` attribute for the reference.
* @param type An optional `Type` attribute for the reference.
*/
addReference({
xpath,
Expand All @@ -805,6 +811,8 @@ export class SignedXml {
digestValue,
inclusiveNamespacesPrefixList = [],
isEmptyUri = false,
id = undefined,
type = undefined,
}: Partial<Reference> & Pick<Reference, "xpath">): void {
if (digestAlgorithm == null) {
throw new Error("digestAlgorithm is required");
Expand All @@ -822,6 +830,9 @@ export class SignedXml {
digestValue,
inclusiveNamespacesPrefixList,
isEmptyUri,
id,
type,
wasProcessed: false,
getValidatedNode: () => {
throw new Error(
"Reference has not been validated yet; Did you call `sig.checkSignature()`?",
Expand Down Expand Up @@ -965,6 +976,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 +991,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 +1085,38 @@ 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}:` : "";

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

let result = "";

for (const obj of this.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 @@ -1085,19 +1132,30 @@ export class SignedXml {
const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver);

if (!utils.isArrayHasLength(nodes)) {
throw new Error(
`the following xpath cannot be signed because it was not found: ${ref.xpath}`,
);
// Don't throw here - we'll handle this in processSignatureReferences
continue;
}

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 All @@ -1122,6 +1180,7 @@ export class SignedXml {
`<${prefix}DigestValue>${digestAlgorithm.getHash(canonXml)}</${prefix}DigestValue>` +
`</${prefix}Reference>`;
}
ref.wasProcessed = true;
}

return res;
Expand Down Expand Up @@ -1262,6 +1321,119 @@ export class SignedXml {
return doc.documentElement.firstChild!;
}

/**
* Process references that weren't found in the initial document
* This is called after the initial signature has been created to handle references to signature elements
*/
private processSignatureReferences(signatureDoc: Document, prefix?: string) {
// Get unprocessed references
const unprocessedReferences = this.references.filter((ref) => !ref.wasProcessed);
if (unprocessedReferences.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 unprocessed reference
for (const ref of unprocessedReferences) {
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);

// Get the digest algorithm and compute the digest value
const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm);

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

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

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
22 changes: 22 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ export interface GetKeyInfoContentArgs {
prefix?: string | null;
}

/**
* Object attributes as defined in XMLDSig spec
* @see https://www.w3.org/TR/xmldsig-core/#sec-Object
*/
export interface ObjectAttributes {
/** Optional ID attribute */
Id?: string;
/** Optional MIME type attribute */
MimeType?: string;
/** Optional encoding attribute */
Encoding?: string;
/** Any additional custom attributes */
[key: string]: string | undefined;
}

/**
* Options for the SignedXml constructor.
*/
Expand All @@ -58,6 +73,7 @@ export interface SignedXmlOptions {
keyInfoAttributes?: Record<string, string>;
getKeyInfoContent?(args?: GetKeyInfoContentArgs): string | null;
getCertFromKeyInfo?(keyInfo?: Node | null): string | null;
objects?: Array<{ content: string; attributes?: ObjectAttributes }>;
}

export interface NamespacePrefix {
Expand Down Expand Up @@ -127,6 +143,12 @@ export interface Reference {
// Optional. Indicates whether the URI is empty.
isEmptyUri: boolean;

// Optional. The `Id` attribute of the reference node.
id?: string;

// Optional. The `Type` attribute of the reference node.
type?: string;

// Optional. The type of the reference node.
ancestorNamespaces?: NamespacePrefix[];

Expand Down
Loading