XAdESjs
XAdES is short for "XML Advanced Electronic Signatures", it is a superset of XMLDSIG. This library aims to provide an implementation of XAdES in Typescript/Javascript that is built on XMLDSIGjs.
Since it is based on XMLDSIGjs and that library uses Web Crypto for cryptographic operations it can be used both in browsers and in Node.js (when used with a polyfill like node-webcrypto-ossl or node-webcrypto-p11).
There are seven different profiles of XAdES, they are:
- Basic Electronic Signature (XAdES-BES)
- XAdES with Timestamp (XAdES-T)
- XAdES with Complete Validation Data (XAdES-C)
- XAdES with Extended Validation Data (XAdES-X)
- XAdES with Extended Long Term Validation Data (XAdES-X-L)
- XAdES with Archiving Validation Data (XAdES-A)
- XAdES with Explicit policy electronic signatures (XAdES-EPES)
They differ slightly based on what is included in the signature:
| Provides Digital Signature | Includes Cryptographic Timestamp | Includes Revocation References | Includes Revocation Data | Allows Secure Timestamp Countersignature |
---|
XAdES-BES | Yes | No | No | No | No |
XAdES-EPES | Yes | No | No | No | No |
XAdES-T | Yes | Yes | No | No | No |
XAdES-C | Yes | Yes | Yes | No | No |
XAdES-X | Yes | Yes | Yes | No | No |
XAdES-X-L | Yes | Yes | Yes | Yes | No |
XAdES-A | Yes | Yes | Yes | Yes | Yes |
- Only XAdES-BES (in BOLD) is fully supported by XAdESjs. For the other variants can be created, decoded and verified but the caller must do the construction and policy to ensure compliant messages on their own.
INSTALLING
npm install xadesjs
The npm module has a dist
folder with the following files:
Name | Size | Description |
---|
index.js | 105 Kb | UMD module with external modules. Has comments |
xades.js | 803 Kb | UMD bundle module. Has comments |
xades.min.js | 296 Kb | minified UMD bundle module |
There is also a lib
folder with an ES2015 JS file which you can use with rollup
bundler.
COMPATABILITY
CRYPTOGRAPHIC ALGORITHM SUPPORT
Name | SHA1 | SHA2-256 | SHA2-384 | SHA2-512 |
---|
RSASSA-PKCS1-v1_5 | X | X | X | X |
RSA-PSS | X | X | X | X |
ECDSA | X | X | X | X |
HMAC | X | X | X | X |
CANONICALIZATION ALGORITHM SUPPORT
- XmlDsigC14NTransform
- XmlDsigC14NWithCommentsTransform
- XmlDsigExcC14NTransform
- XmlDsigExcC14NWithCommentsTransform
- XmlDsigEnvelopedSignatureTransform
- XmlDsigBase64Transform
PLATFORM SUPPORT
XAdESjs works with any browser that suppports Web Crypto. Since node does not have Web Crypto you will need a polyfill on this platform, for this reason the npm package includes node-webcrypto-ossl; browsers do not need this dependency and in those cases though it will be installed it will be ignored.
If you need to use a Hardware Security Module we have also created a polyfill for Web Crypto that supports PKCS #11. Our polyfill for this is node-webcrypto-p11.
To use node-webcrypto-ossl you need to specify you want to use it, that looks like this:
var xadesjs = require("./built/xades.js");
var WebCrypto = require("node-webcrypto-ossl");
xadesjs.Application.setEngine("OpenSSL", new WebCrypto());
The node-webcrypto-p11 polyfill will work the same way. The only difference is that you have to specify the details about your PKCS #11 device when you instansiate it:
var xadesjs = require("./built/xades.js");
var WebCrypto = require("node-webcrypto-p11").WebCrypto;
xadesjs.Application.setEngine("PKCS11", new WebCrypto({
library: "/path/to/pkcs11.so",
name: "Name of PKCS11 lib",
slot: 0,
sessionFlags: 2 | 4,
pin: "token pin"
}));
WARNING
Using XMLDSIG is a bit like running with scissors, that said it is needed for interoperability with a number of systems, for this reason, we have done this implementation.
Usage
Sign
SignedXml.Sign(algorithm: Algorithm, key: CryptoKey, data: Document, options?: OptionsXAdES): PromiseLike<Signature>;
Parameters
Name | Description |
---|
algorithm | Signing Algorithm |
key | Signing Key |
data | XML document which must be signed |
options | Additional options |
Options
interface OptionsXAdES {
keyValue?: CryptoKey;
x509?: string[];
references?: OptionsSignReference[];
signingCertificate?: string;
policy?: OptionsPolicyIdentifier;
productionPlace?: OptionsProductionPlace;
signerRole?: OptionsSignerRole;
}
interface OptionsSignReference {
id?: string;
uri?: string;
hash: AlgorithmIdentifier;
transforms?: OptionsSignTransform[];
}
type OptionsSignTransform = "enveloped" | "c14n" | "exc-c14n" | "c14n-com" | "exc-c14n-com" | "base64";
interface OptionsSignerRole {
claimed?: string[];
certified?: string[];
}
interface OptionsProductionPlace {
city?: string;
state?: string;
code?: string;
country?: string;
}
interface OptionsPolicyIdentifier {
}
Verify
Verify(key?: CryptoKey): PromiseLike<boolean>;
Parameters
Name | Description |
---|
key | Verifying Key. Optional. If key not set it looks for keys in KeyInfo element of Signature. |
EXAMPLES
Create XAdES-BES Signature
In Node
var xadesjs = require("xadesjs");
var WebCrypto = require("node-webcrypto-ossl");
xadesjs.Application.setEngine("OpenSSL", new WebCrypto());
var privateKey, publicKey;
xadesjs.Application.crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 1024,
publicExponent: new Uint8Array([1, 0, 1]),
hash: { name: "SHA-1" },
},
false,
["sign", "verify"]
)
.then(function (keyPair) {
privateKey = keyPair.privateKey;
publicKey = keyPair.publicKey;
var xmlString = '<player bats="left" id="10012" throws="right">\n\t<!-- Here\'s a comment -->\n\t<name>Alfonso Soriano</name>\n\t<position>2B</position>\n\t<team>New York Yankees</team>\n</player>';
return SignXml(xmlString, keyPair, { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-1" } });
})
.then(function (signedDocument) {
console.log("Signed document:\n\n", signedDocument);
})
.catch(function (e) {
console.error(e);
});
function SignXml(xmlString, keys, algorithm) {
return Promise.resolve()
.then(() => {
var xmlDoc = xadesjs.Parse(xmlString);
var signedXml = new xadesjs.SignedXml();
return signedXml.Sign(
algorithm,
keys.privateKey,
xmlDoc,
{
keyValue: keys.publicKey,
references: [
{ hash: "SHA-256", transforms: ["enveloped"] }
],
productionPlace: {
country: "Country",
state: "State",
city: "City",
code: "Code",
},
signingCertificate: "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"
})
})
.then(signature => signature.toString());
}
In the browser
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>XADESJS Signature Sample</title>
</head>
<body>
<pre id="signature"><code></code></pre>
<script src="https://peculiarventures.github.io/pv-webcrypto-tests/src/promise.js"></script>
<script src="https://peculiarventures.github.io/pv-webcrypto-tests/src/webcrypto-liner.min.js"></script>
<script src="https://peculiarventures.github.io/pv-webcrypto-tests/src/asmcrypto.js"></script>
<script src="https://peculiarventures.github.io/pv-webcrypto-tests/src/elliptic.js"></script>
<script type="text/javascript" src="dist/xades.js"></script>
<script type="text/javascript">
var privateKey, publicKey;
window.crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256"
},
false,
["sign", "verify"]
)
.then(function (keyPair) {
privateKey = keyPair.privateKey;
publicKey = keyPair.publicKey;
var xmlString = '<player bats="left" id="10012" throws="right">\n\t<!-- Here\'s a comment -->\n\t<name>Alfonso Soriano</name>\n\t<position>2B</position>\n\t<team>New York Yankees</team>\n</player>';
return SignXml(xmlString, keyPair, { name: "ECDSA", hash: { name: "SHA-1" } });
})
.then(function (signedDocument) {
document.getElementById("signature").textContent = signedDocument;
console.log("Signed document:\n\n", signedDocument);
})
.catch(function (e) {
console.error(e);
});
function SignXml(xmlString, keys, algorithm) {
var signedXml;
return Promise.resolve()
.then(() => {
var xmlDoc = XAdES.Parse(xmlString);
signedXml = new XAdES.SignedXml();
return signedXml.Sign(
algorithm,
keys.privateKey,
xmlDoc,
{
keyValue: keys.publicKey,
references: [
{ hash: "SHA-256", transforms: ["enveloped"] }
],
productionPlace: {
country: "Country",
state: "State",
city: "City",
code: "Code",
},
signerRoles: {
claimed: ["Some role"]
}
})
})
.then(() => signedXml.toString());
}
</script>
</body>
</html>
Check XAdES-BES Signature
In Node
var XAdES = require("xadesjs");
var WebCrypto = require("node-webcrypto-ossl").default;
XAdES.Application.setEngine("OpenSSL", new WebCrypto());
var fs = require("fs");
var xmlString = fs.readFileSync("some.xml","utf8");
var signedDocument = XAdES.Parse(xmlString, "application/xml");
var xmlSignature = signedDocument.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature");
var signedXml = new xadesjs.SignedXml(signedDocument);
signedXml.LoadXml(xmlSignature[0]);
signedXml.Verify()
.then(res => {
console.log((res ? "Valid" : "Invalid") + " signature");
})
.catch(function (e) {
console.error(e);
});
In the browser
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>XADESJS Signature Sample</title>
</head>
<body>
<pre id="signature"><code></code></pre>
<script src="https://peculiarventures.github.io/pv-webcrypto-tests/src/promise.js"></script>
<script src="https://peculiarventures.github.io/pv-webcrypto-tests/src/webcrypto-liner.min.js"></script>
<script src="https://peculiarventures.github.io/pv-webcrypto-tests/src/asmcrypto.js"></script>
<script src="https://peculiarventures.github.io/pv-webcrypto-tests/src/elliptic.js"></script>
<script type="text/javascript" src="dist/xades.js"></script>
<script type="text/javascript">
"use strict";
fetch("https://cdn.rawgit.com/PeculiarVentures/xadesjs/master/test/static/valid_signature.xml")
.then(response => response.text())
.then(body => {
var xmlString = body;
var signedDocument = XAdES.Parse(xmlString);
var xmlSignature = signedDocument.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature");
var signedXml = new xadesjs.SignedXml(signedDocument);
signedXml.LoadXml(xmlSignature[0]);
signedXml.Verify()
.then(function (signedDocument) {
alert((res ? "Valid" : "Invalid") + " signature");
})
.catch(function (e) {
alert(e.message);
});
})
</script>
</body>
</html>
TESTING
In NodeJS:
npm test
THANKS AND ACKNOWLEDGEMENT
This project takes inspiration (style, approach, design and code) from both the Mono System.Security.Cryptography.Xml implementation as well as xml-crypto.
RELATED