Skip to content
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

Add support for <X509Certificate /> in <KeyInfo />; remove KeyInfoProvider #301

Merged
merged 27 commits into from
Jun 17, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
280075b
Replace `KeyInfoProvider` with plugable methods
cjbarth Jun 14, 2023
74f4a23
Address review comments
cjbarth Jun 14, 2023
8bd88c0
Update readme and apply PR suggestions
cjbarth Jun 14, 2023
c09e373
Update types
cjbarth Jun 14, 2023
62fafe9
better documentation
cjbarth Jun 14, 2023
84c8146
Add test per PR review
cjbarth Jun 14, 2023
49b8e64
private hmac
cjbarth Jun 15, 2023
bb508cd
lint
cjbarth Jun 15, 2023
8bd4856
Merge remote-tracking branch 'upstream/master' into key-info
cjbarth Jun 15, 2023
abcf25e
lint
cjbarth Jun 15, 2023
c7e6a99
Merge remote-tracking branch 'upstream/master' into key-info
cjbarth Jun 15, 2023
3cb410f
fix broken tests
Jun 15, 2023
40f1e8f
fix bug
Jun 15, 2023
530f34e
fix bug
Jun 15, 2023
0a5cff5
add test (failing) to make sure private keys are not added to the Key…
Jun 15, 2023
04a4915
add pem file which containts both public and private key
Jun 15, 2023
2acd2ee
revert var to const change (eslint did it)
Jun 15, 2023
eaceb08
make SignatureAlgorithms, HashAlgorithms and CanonicalizationAlgorith…
Jun 15, 2023
8336454
only put actual x509 certificates in the X509Certificate element
Jun 15, 2023
1669c68
improve test
Jun 15, 2023
c6a29f4
test with two public certificates
Jun 15, 2023
be08a46
Merge remote-tracking branch 'upstream/master' into key-info
cjbarth Jun 15, 2023
32b237a
Merge branch 'master' into key-info
cjbarth Jun 15, 2023
2340320
Merge pull request #1 from shunkica/key-info
cjbarth Jun 15, 2023
f87d4b8
Merge remote-tracking branch 'origin/key-info' into key-info
cjbarth Jun 15, 2023
599ca5a
Update index.d.ts
cjbarth Jun 15, 2023
aa2f62f
remove static
cjbarth Jun 15, 2023
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
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:[email protected])).
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
140 changes: 57 additions & 83 deletions index.d.ts
cjbarth marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ 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: {
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