Skip to content

Commit c2b83f9

Browse files
cjbarthIvanshunkica
authored
Add support for <X509Certificate /> in <KeyInfo />; remove KeyInfoProvider (#301)
* Replace `KeyInfoProvider` with plugable methods Co-authored-by: Ivan <[email protected]> Co-authored-by: shunkica <[email protected]>
1 parent 67b3a78 commit c2b83f9

20 files changed

+436
-323
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"root": true,
88
"parserOptions": {
9-
"ecmaVersion": 6
9+
"ecmaVersion": 2020
1010
},
1111
"extends": ["eslint:recommended", "prettier"],
1212
"rules": {

README.md

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ _Signature Algorithm:_ RSA-SHA1 http://www.w3.org/2000/09/xmldsig#rsa-sha1
6868
When signing a xml document you can specify the following properties on a `SignedXml` instance to customize the signature process:
6969

7070
- `sign.signingKey` - **[required]** a `Buffer` or pem encoded `String` containing your private key
71-
- `sign.keyInfoProvider` - **[optional]** a key info provider instance, see [customizing algorithms](#customizing-algorithms) for an implementation example
7271
- `sign.signatureAlgorithm` - **[optional]** one of the supported [signature algorithms](#signature-algorithms). Ex: `sign.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"`
7372
- `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"`
7473

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

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

122-
- `sign.keyInfoProvider` - **[required]** a key info provider instance containing your certificate, see [customizing algorithms](#customizing-algorithms) for an implementation example
121+
- `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
122+
123+
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).
123124

124125
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:
125126

@@ -133,7 +134,6 @@ Example:
133134
var select = require("xml-crypto").xpath,
134135
dom = require("@xmldom/xmldom").DOMParser,
135136
SignedXml = require("xml-crypto").SignedXml,
136-
FileKeyInfo = require("xml-crypto").FileKeyInfo,
137137
fs = require("fs");
138138

139139
var xml = fs.readFileSync("signed.xml").toString();
@@ -144,7 +144,7 @@ var signature = select(
144144
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']"
145145
)[0];
146146
var sig = new SignedXml();
147-
sig.keyInfoProvider = new FileKeyInfo("client_public.pem");
147+
sig.signingCert = new FileKeyInfo("client_public.pem");
148148
sig.loadSignature(signature);
149149
var res = sig.checkSignature(xml);
150150
if (!res) console.log(sig.validationErrors);
@@ -179,7 +179,7 @@ If you keep failing verification, it is worth trying to guess such a hidden tran
179179
```javascript
180180
var option = { implicitTransforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"] };
181181
var sig = new SignedXml(null, option);
182-
sig.keyInfoProvider = new FileKeyInfo("client_public.pem");
182+
sig.signingCert = new FileKeyInfo("client_public.pem");
183183
sig.loadSignature(signature);
184184
var res = sig.checkSignature(xml);
185185
```
@@ -232,14 +232,6 @@ To verify xml documents:
232232
- `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:
233233
- `xml` - a string containing a xml document
234234

235-
### FileKeyInfo
236-
237-
A basic key info provider implementation using `fs.readFileSync(file)`, is constructed using `new FileKeyInfo([file])` where:
238-
239-
- `file` - a path to a pem encoded certificate
240-
241-
See [verifying xml documents](#verifying-xml-documents) for an example usage
242-
243235
## Customizing Algorithms
244236

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

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

256-
A key info provider is used to extract and construct the key and the KeyInfo xml section.
257-
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.
248+
To determine the inclusion and contents of a `<KeyInfo />` element, the function
249+
`getKeyInfoContent()` is called. There is a default implementation of this. If you wish to change
250+
this implementation, provide your own function assigned to the property `.getKeyInfoContent`. If
251+
there are no attributes and no contents to the `<KeyInfo />` element, it won't be included in the
252+
generated XML.
258253

259-
```javascript
260-
function MyKeyInfo() {
261-
this.getKeyInfo = function (key, prefix) {
262-
prefix = prefix || "";
263-
prefix = prefix ? prefix + ":" : prefix;
264-
return "<" + prefix + "X509Data></" + prefix + "X509Data>";
265-
};
266-
this.getKey = function (keyInfo) {
267-
//you can use the keyInfo parameter to extract the key in any way you want
268-
return fs.readFileSync("key.pem");
269-
};
270-
}
271-
```
254+
To specify custom attributes on `<KeyInfo />`, add the properties to the `.keyInfoAttributes` property.
272255

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

275258
```javascript
276259
function MyDigest() {
@@ -284,7 +267,7 @@ function MyDigest() {
284267
}
285268
```
286269

287-
A custom signing algorithm. The default is RSA-SHA1
270+
A custom signing algorithm. The default is RSA-SHA1.
288271

289272
```javascript
290273
function MySignatureAlgorithm() {
@@ -350,7 +333,7 @@ function signXml(xml, xpath, key, dest) {
350333

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

373-
You can always look at the actual code as a sample (or drop me a [mail](mailto:[email protected])).
356+
You can always look at the actual code as a sample.
374357

375358
## Asynchronous signing and verification
376359

example/example.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const select = require("xml-crypto").xpath;
44
const dom = require("@xmldom/xmldom").DOMParser;
55
const SignedXml = require("xml-crypto").SignedXml;
6-
const FileKeyInfo = require("xml-crypto").FileKeyInfo;
76
const fs = require("fs");
87

98
function signXml(xml, xpath, key, dest) {
@@ -21,7 +20,7 @@ function validateXml(xml, key) {
2120
doc
2221
)[0];
2322
const sig = new SignedXml();
24-
sig.keyInfoProvider = new FileKeyInfo(key);
23+
sig.signingCert = key;
2524
sig.loadSignature(signature.toString());
2625
const res = sig.checkSignature(xml);
2726
if (!res) {

index.d.ts

Lines changed: 60 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,25 @@ export interface TransformAlgorithm {
129129
* - {@link SignedXml#checkSignature}
130130
* - {@link SignedXml#validationErrors}
131131
*/
132+
133+
/**
134+
* @param cert the certificate as a string or array of strings (see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data)
135+
* @param prefix an optional namespace alias to be used for the generated XML
136+
*/
137+
export interface GetKeyInfoContentArgs {
138+
cert: string | string[] | Buffer;
139+
prefix: string;
140+
}
141+
132142
export class SignedXml {
133143
// 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}
134-
static CanonicalizationAlgorithms: {
144+
CanonicalizationAlgorithms: {
135145
[uri in TransformAlgorithmType]: new () => TransformAlgorithm;
136146
};
137147
// 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}
138-
static HashAlgorithms: { [uri in HashAlgorithmType]: new () => HashAlgorithm };
148+
HashAlgorithms: { [uri in HashAlgorithmType]: new () => HashAlgorithm };
139149
// 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}
140-
static SignatureAlgorithms: { [uri in SignatureAlgorithmType]: new () => SignatureAlgorithm };
150+
SignatureAlgorithms: { [uri in SignatureAlgorithmType]: new () => SignatureAlgorithm };
141151
// Rules used to convert an XML document into its canonical form.
142152
canonicalizationAlgorithm: TransformAlgorithmType;
143153
// It specifies a list of namespace prefixes that should be considered "inclusive" during the canonicalization process.
@@ -149,7 +159,7 @@ export class SignedXml {
149159
// One of the supported signature algorithms. See {@link SignatureAlgorithmType}
150160
signatureAlgorithm: SignatureAlgorithmType;
151161
// A {@link Buffer} or pem encoded {@link String} containing your private key
152-
signingKey: Buffer | string;
162+
privateKey: Buffer | string;
153163
// Contains validation errors (if any) after {@link checkSignature} method is called
154164
validationErrors: string[];
155165

@@ -278,115 +288,79 @@ export class SignedXml {
278288
* @returns The signed XML.
279289
*/
280290
getSignedXml(): string;
281-
}
282291

283-
/**
284-
* KeyInfoProvider interface represents the structure for managing keys
285-
* and KeyInfo section in XML data when dealing with XML digital signatures.
286-
*/
287-
export interface KeyInfoProvider {
288292
/**
289-
* Method to return the key based on the contents of the specified KeyInfo.
293+
* Builds the contents of a KeyInfo element as an XML string.
290294
*
291-
* @param keyInfo - An optional array of XML Nodes.
292-
* @return A string or Buffer representing the key.
293-
*/
294-
getKey(keyInfo?: Node[]): string | Buffer;
295-
296-
/**
297-
* Method to return an XML string representing the contents of a KeyInfo element.
295+
* For example, if the value of the prefix argument is 'foo', then
296+
* the resultant XML string will be "<foo:X509Data></foo:X509Data>"
298297
*
299-
* @param key - An optional string representing the key.
300-
* @param prefix - An optional string representing the namespace alias.
301-
* @return An XML string representation of the contents of a KeyInfo element.
298+
* @return an XML string representation of the contents of a KeyInfo element, or `null` if no `KeyInfo` element should be included
302299
*/
303-
getKeyInfo(key?: string, prefix?: string): string;
300+
getKeyInfoContent(args: GetKeyInfoContentArgs): string | null;
304301

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

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

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

327327
/**
328-
* Return the loaded certificate. The certificate is read from the file specified in the constructor.
329-
* The keyInfo parameter is ignored. (not implemented)
328+
* -----BEGIN [LABEL]-----
329+
* base64([DATA])
330+
* -----END [LABEL]-----
330331
*
331-
* @param keyInfo - (not used) An optional array of XML Elements.
332-
* @return A Buffer representing the certificate.
333-
*/
334-
getKey(keyInfo?: Node[]): Buffer;
335-
336-
/**
337-
* Builds the contents of a KeyInfo element as an XML string.
332+
* Above is shown what PEM file looks like. As can be seen, base64 data
333+
* can be in single line or multiple lines.
338334
*
339-
* Currently, this returns exactly one empty X509Data element
340-
* (e.g. "<X509Data></X509Data>"). The resultant X509Data element will be
341-
* prefaced with a namespace alias if a value for the prefix argument
342-
* is provided. In example, if the value of the prefix argument is 'foo', then
343-
* the resultant XML string will be "<foo:X509Data></foo:X509Data>"
335+
* This function normalizes PEM presentation to;
336+
* - contain PEM header and footer as they are given
337+
* - normalize line endings to '\n'
338+
* - normalize line length to maximum of 64 characters
339+
* - ensure that 'preeb' has line ending '\n'
344340
*
345-
* @param key (not used) the signing/private key as a string
346-
* @param prefix an optional namespace alias to be used for the generated XML
347-
* @return an XML string representation of the contents of a KeyInfo element
348-
*/
349-
getKeyInfo(key?: string, prefix?: string): string;
350-
}
351-
352-
/**
353-
* The StringKeyInfo class loads the certificate from the string provided in the constructor.
354-
*/
355-
export class StringKeyInfo implements KeyInfoProvider {
356-
/**
357-
* The certificate in string form.
358-
*/
359-
key: string;
360-
361-
/**
362-
* Initializes a new instance of the StringKeyInfo class.
363-
* @param key - An optional string representing the certificate.
364-
*/
365-
constructor(key?: string);
366-
367-
/**
368-
* Returns the certificate loaded in the constructor.
369-
* The keyInfo parameter is ignored. (not implemented)
341+
* With couple of notes:
342+
* - 'eol' is normalized to '\n'
370343
*
371-
* @param keyInfo (not used) an array with exactly one KeyInfo element
372-
* @return the signing certificate as a string
344+
* @param pem The PEM string to normalize to RFC7468 'stricttextualmsg' definition
373345
*/
374-
getKey(keyInfo?: Node[]): string;
346+
static normalizePem(pem: string): string;
375347

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

392366
/**

lib/file-key-info.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)