Skip to content

Commit

Permalink
Add support for <X509Certificate /> in <KeyInfo />; remove `KeyInfoPr…
Browse files Browse the repository at this point in the history
…ovider` (#301)

* Replace `KeyInfoProvider` with plugable methods

Co-authored-by: Ivan <admin@clab.hr>
Co-authored-by: shunkica <ivannovak90@gmail.com>
  • Loading branch information
3 people authored Jun 17, 2023
1 parent 67b3a78 commit c2b83f9
Show file tree
Hide file tree
Showing 20 changed files with 436 additions and 323 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"root": true,
"parserOptions": {
"ecmaVersion": 6
"ecmaVersion": 2020
},
"extends": ["eslint:recommended", "prettier"],
"rules": {
Expand Down
47 changes: 15 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ _Signature Algorithm:_ RSA-SHA1 http://www.w3.org/2000/09/xmldsig#rsa-sha1
When signing a xml document you can specify the following properties on a `SignedXml` instance to customize the signature process:

- `sign.signingKey` - **[required]** a `Buffer` or pem encoded `String` containing your private key
- `sign.keyInfoProvider` - **[optional]** a key info provider instance, see [customizing algorithms](#customizing-algorithms) for an implementation example
- `sign.signatureAlgorithm` - **[optional]** one of the supported [signature algorithms](#signature-algorithms). Ex: `sign.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"`
- `sign.canonicalizationAlgorithm` - **[optional]** one of the supported [canonicalization algorithms](#canonicalization-and-transformation-algorithms). Ex: `sign.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"`

Expand Down Expand Up @@ -119,7 +118,9 @@ To generate a `<X509Data></X509Data>` element in the signature you must provide

When verifying a xml document you must specify the following properties on a ``SignedXml` instance:

- `sign.keyInfoProvider` - **[required]** a key info provider instance containing your certificate, see [customizing algorithms](#customizing-algorithms) for an implementation example
- `sign.signingCert` - **[optional]** your certificate as a string, a string of multiple certs in PEM format, or a Buffer, see [customizing algorithms](#customizing-algorithms) for an implementation example

The certificate that will be used to check the signature will first be determined by calling `.getCertFromKeyInfo()`, which function you can customize as you see fit. If that returns `null`, then `.signingCert` is used. If that is `null`, then `.signingKey` is used (for symmetrical signing applications).

You can use any dom parser you want in your code (or none, depending on your usage). This sample uses [xmldom](https://github.com/jindw/xmldom) so you should install it first:

Expand All @@ -133,7 +134,6 @@ Example:
var select = require("xml-crypto").xpath,
dom = require("@xmldom/xmldom").DOMParser,
SignedXml = require("xml-crypto").SignedXml,
FileKeyInfo = require("xml-crypto").FileKeyInfo,
fs = require("fs");

var xml = fs.readFileSync("signed.xml").toString();
Expand All @@ -144,7 +144,7 @@ var signature = select(
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']"
)[0];
var sig = new SignedXml();
sig.keyInfoProvider = new FileKeyInfo("client_public.pem");
sig.signingCert = new FileKeyInfo("client_public.pem");
sig.loadSignature(signature);
var res = sig.checkSignature(xml);
if (!res) console.log(sig.validationErrors);
Expand Down Expand Up @@ -179,7 +179,7 @@ If you keep failing verification, it is worth trying to guess such a hidden tran
```javascript
var option = { implicitTransforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"] };
var sig = new SignedXml(null, option);
sig.keyInfoProvider = new FileKeyInfo("client_public.pem");
sig.signingCert = new FileKeyInfo("client_public.pem");
sig.loadSignature(signature);
var res = sig.checkSignature(xml);
```
Expand Down Expand Up @@ -232,14 +232,6 @@ To verify xml documents:
- `checkSignature(xml)` - validates the given xml document and returns true if the validation was successful, `sig.validationErrors` will have the validation errors if any, where:
- `xml` - a string containing a xml document

### FileKeyInfo

A basic key info provider implementation using `fs.readFileSync(file)`, is constructed using `new FileKeyInfo([file])` where:

- `file` - a path to a pem encoded certificate

See [verifying xml documents](#verifying-xml-documents) for an example usage

## Customizing Algorithms

The following sample shows how to sign a message using custom algorithms.
Expand All @@ -253,24 +245,15 @@ var SignedXml = require("xml-crypto").SignedXml,

Now define the extension point you want to implement. You can choose one or more.

A key info provider is used to extract and construct the key and the KeyInfo xml section.
Implement it if you want to create a signature with a KeyInfo section, or you want to read your key in a different way then the default file read option.
To determine the inclusion and contents of a `<KeyInfo />` element, the function
`getKeyInfoContent()` is called. There is a default implementation of this. If you wish to change
this implementation, provide your own function assigned to the property `.getKeyInfoContent`. If
there are no attributes and no contents to the `<KeyInfo />` element, it won't be included in the
generated XML.

```javascript
function MyKeyInfo() {
this.getKeyInfo = function (key, prefix) {
prefix = prefix || "";
prefix = prefix ? prefix + ":" : prefix;
return "<" + prefix + "X509Data></" + prefix + "X509Data>";
};
this.getKey = function (keyInfo) {
//you can use the keyInfo parameter to extract the key in any way you want
return fs.readFileSync("key.pem");
};
}
```
To specify custom attributes on `<KeyInfo />`, add the properties to the `.keyInfoAttributes` property.

A custom hash algorithm is used to calculate digests. Implement it if you want a hash other than the default SHA1.
A custom hash algorithm is used to calculate digests. Implement it if you want a hash other than the built-in methods.

```javascript
function MyDigest() {
Expand All @@ -284,7 +267,7 @@ function MyDigest() {
}
```

A custom signing algorithm. The default is RSA-SHA1
A custom signing algorithm. The default is RSA-SHA1.

```javascript
function MySignatureAlgorithm() {
Expand Down Expand Up @@ -350,7 +333,7 @@ function signXml(xml, xpath, key, dest) {

/*configure the signature object to use the custom algorithms*/
sig.signatureAlgorithm = "http://mySignatureAlgorithm";
sig.keyInfoProvider = new MyKeyInfo();
sig.signingCert = fs.readFileSync("my_public_cert.pem", "latin1");
sig.canonicalizationAlgorithm = "http://MyCanonicalization";
sig.addReference(
"//*[local-name(.)='x']",
Expand All @@ -370,7 +353,7 @@ var xml = "<library>" + "<book>" + "<name>Harry Potter</name>" + "</book>";
signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml");
```

You can always look at the actual code as a sample (or drop me a [mail](mailto:yaronn01@gmail.com)).
You can always look at the actual code as a sample.

## Asynchronous signing and verification

Expand Down
3 changes: 1 addition & 2 deletions example/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const select = require("xml-crypto").xpath;
const dom = require("@xmldom/xmldom").DOMParser;
const SignedXml = require("xml-crypto").SignedXml;
const FileKeyInfo = require("xml-crypto").FileKeyInfo;
const fs = require("fs");

function signXml(xml, xpath, key, dest) {
Expand All @@ -21,7 +20,7 @@ function validateXml(xml, key) {
doc
)[0];
const sig = new SignedXml();
sig.keyInfoProvider = new FileKeyInfo(key);
sig.signingCert = key;
sig.loadSignature(signature.toString());
const res = sig.checkSignature(xml);
if (!res) {
Expand Down
146 changes: 60 additions & 86 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,25 @@ export interface TransformAlgorithm {
* - {@link SignedXml#checkSignature}
* - {@link SignedXml#validationErrors}
*/

/**
* @param cert the certificate as a string or array of strings (see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data)
* @param prefix an optional namespace alias to be used for the generated XML
*/
export interface GetKeyInfoContentArgs {
cert: string | string[] | Buffer;
prefix: string;
}

export class SignedXml {
// To add a new transformation algorithm create a new class that implements the {@link TransformationAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms}
static CanonicalizationAlgorithms: {
CanonicalizationAlgorithms: {
[uri in TransformAlgorithmType]: new () => TransformAlgorithm;
};
// 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}
static HashAlgorithms: { [uri in HashAlgorithmType]: new () => HashAlgorithm };
HashAlgorithms: { [uri in HashAlgorithmType]: new () => HashAlgorithm };
// 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}
static SignatureAlgorithms: { [uri in SignatureAlgorithmType]: new () => SignatureAlgorithm };
SignatureAlgorithms: { [uri in SignatureAlgorithmType]: new () => SignatureAlgorithm };
// Rules used to convert an XML document into its canonical form.
canonicalizationAlgorithm: TransformAlgorithmType;
// It specifies a list of namespace prefixes that should be considered "inclusive" during the canonicalization process.
Expand All @@ -149,7 +159,7 @@ export class SignedXml {
// One of the supported signature algorithms. See {@link SignatureAlgorithmType}
signatureAlgorithm: SignatureAlgorithmType;
// A {@link Buffer} or pem encoded {@link String} containing your private key
signingKey: Buffer | string;
privateKey: Buffer | string;
// Contains validation errors (if any) after {@link checkSignature} method is called
validationErrors: string[];

Expand Down Expand Up @@ -278,115 +288,79 @@ export class SignedXml {
* @returns The signed XML.
*/
getSignedXml(): string;
}

/**
* KeyInfoProvider interface represents the structure for managing keys
* and KeyInfo section in XML data when dealing with XML digital signatures.
*/
export interface KeyInfoProvider {
/**
* Method to return the key based on the contents of the specified KeyInfo.
* Builds the contents of a KeyInfo element as an XML string.
*
* @param keyInfo - An optional array of XML Nodes.
* @return A string or Buffer representing the key.
*/
getKey(keyInfo?: Node[]): string | Buffer;

/**
* Method to return an XML string representing the contents of a KeyInfo element.
* For example, if the value of the prefix argument is 'foo', then
* the resultant XML string will be "<foo:X509Data></foo:X509Data>"
*
* @param key - An optional string representing the key.
* @param prefix - An optional string representing the namespace alias.
* @return An XML string representation of the contents of a KeyInfo element.
* @return an XML string representation of the contents of a KeyInfo element, or `null` if no `KeyInfo` element should be included
*/
getKeyInfo(key?: string, prefix?: string): string;
getKeyInfoContent(args: GetKeyInfoContentArgs): string | null;

/**
* An optional dictionary of attributes which will be added to the KeyInfo element.
* Returns the value of the signing certificate based on the contents of the
* specified KeyInfo.
*
* @param keyInfo an array with exactly one KeyInfo element (see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data)
* @return the signing certificate as a string in PEM format
*/
attrs?: { [key: string]: string };
getCertFromKeyInfo(keyInfo: string): string | null;
}

/**
* The FileKeyInfo class loads the certificate from the file provided in the constructor.
*/
export class FileKeyInfo implements KeyInfoProvider {
export interface Utils {
/**
* The path to the file from which the certificate is to be read.
* @param pem The PEM-encoded base64 certificate to strip headers from
*/
file: string;
static pemToDer(pem: string): string;

/**
* Initializes a new instance of the FileKeyInfo class.
*
* @param file - An optional string representing the file path of the certificate.
* @param der The DER-encoded base64 certificate to add PEM headers too
* @param pemLabel The label of the header and footer to add
*/
constructor(file?: string);
static derToPem(
der: string,
pemLabel: ["CERTIFICATE" | "PRIVATE KEY" | "RSA PUBLIC KEY"]
): string;

/**
* Return the loaded certificate. The certificate is read from the file specified in the constructor.
* The keyInfo parameter is ignored. (not implemented)
* -----BEGIN [LABEL]-----
* base64([DATA])
* -----END [LABEL]-----
*
* @param keyInfo - (not used) An optional array of XML Elements.
* @return A Buffer representing the certificate.
*/
getKey(keyInfo?: Node[]): Buffer;

/**
* Builds the contents of a KeyInfo element as an XML string.
* Above is shown what PEM file looks like. As can be seen, base64 data
* can be in single line or multiple lines.
*
* Currently, this returns exactly one empty X509Data element
* (e.g. "<X509Data></X509Data>"). The resultant X509Data element will be
* prefaced with a namespace alias if a value for the prefix argument
* is provided. In example, if the value of the prefix argument is 'foo', then
* the resultant XML string will be "<foo:X509Data></foo:X509Data>"
* This function normalizes PEM presentation to;
* - contain PEM header and footer as they are given
* - normalize line endings to '\n'
* - normalize line length to maximum of 64 characters
* - ensure that 'preeb' has line ending '\n'
*
* @param key (not used) the signing/private key as a string
* @param prefix an optional namespace alias to be used for the generated XML
* @return an XML string representation of the contents of a KeyInfo element
*/
getKeyInfo(key?: string, prefix?: string): string;
}

/**
* The StringKeyInfo class loads the certificate from the string provided in the constructor.
*/
export class StringKeyInfo implements KeyInfoProvider {
/**
* The certificate in string form.
*/
key: string;

/**
* Initializes a new instance of the StringKeyInfo class.
* @param key - An optional string representing the certificate.
*/
constructor(key?: string);

/**
* Returns the certificate loaded in the constructor.
* The keyInfo parameter is ignored. (not implemented)
* With couple of notes:
* - 'eol' is normalized to '\n'
*
* @param keyInfo (not used) an array with exactly one KeyInfo element
* @return the signing certificate as a string
* @param pem The PEM string to normalize to RFC7468 'stricttextualmsg' definition
*/
getKey(keyInfo?: Node[]): string;
static normalizePem(pem: string): string;

/**
* Builds the contents of a KeyInfo element as an XML string.
* PEM format has wide range of usages, but this library
* is enforcing RFC7468 which focuses on PKIX, PKCS and CMS.
*
* Currently, this returns exactly one empty X509Data element
* (e.g. "<X509Data></X509Data>"). The resultant X509Data element will be
* prefaced with a namespace alias if a value for the prefix argument
* is provided. In example, if the value of the prefix argument is 'foo', then
* the resultant XML string will be "<foo:X509Data></foo:X509Data>"
* https://www.rfc-editor.org/rfc/rfc7468
*
* PEM_FORMAT_REGEX is validating given PEM file against RFC7468 'stricttextualmsg' definition.
*
* @param key (not used) the signing/private key as a string
* @param prefix an optional namespace alias to be used for the generated XML
* @return an XML string representation of the contents of a KeyInfo element
* With few exceptions;
* - 'posteb' MAY have 'eol', but it is not mandatory.
* - 'preeb' and 'posteb' lines are limited to 64 characters, but
* should not cause any issues in context of PKIX, PKCS and CMS.
*/
getKeyInfo(key?: string, prefix?: string): string;
PEM_FORMAT_REGEX: RegExp;
EXTRACT_X509_CERTS: RegExp;
BASE64_REGEX: RegExp;
}

/**
Expand Down
17 changes: 0 additions & 17 deletions lib/file-key-info.js

This file was deleted.

Loading

0 comments on commit c2b83f9

Please sign in to comment.