diff --git a/README.md b/README.md index 2d8d82b..cb27353 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ signature algorithms enabled at same time. When signing a xml document you can pass the following options to the `SignedXml` constructor to customize the signature process: - `privateKey` - **[required]** a `Buffer` or pem encoded `String` containing your private key +- `privateKeyPassphrase` - **[optional]** the passphrase to decrypt the private key - `publicCert` - **[optional]** a `Buffer` or pem encoded `String` containing your public key - `signatureAlgorithm` - **[required]** one of the supported [signature algorithms](#signature-algorithms). Ex: `sign.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"` - `canonicalizationAlgorithm` - **[required]** one of the supported [canonicalization algorithms](#canonicalization-and-transformation-algorithms). Ex: `sign.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"` @@ -132,6 +133,7 @@ When verifying a xml document you can pass the following options to the `SignedX - `publicCert` - **[optional]** your certificate as a string, a string of multiple certs in PEM format, or a Buffer - `privateKey` - **[optional]** your private key as a string or a Buffer - used for verifying symmetrical signatures (HMAC) +- `privateKeyPassphrase` - **[optional]** the passphrase to decrypt the private key The certificate that will be used to check the signature will first be determined by calling `this.getCertFromKeyInfo()`, which function you can customize as you see fit. If that returns `null`, then `publicCert` is used. If that is `null`, then `privateKey` is used (for symmetrical signing applications). @@ -256,8 +258,9 @@ The `SignedXml` constructor provides an abstraction for sign and verify xml docu - `idMode` - default `null` - if the value of `wssecurity` is passed it will create/validate id's with the ws-security namespace. - `idAttribute` - string - default `Id` or `ID` or `id` - the name of the attribute that contains the id of the element -- `privateKey` - string or Buffer - default `null` - the private key to use for signing -- `publicCert` - string or Buffer - default `null` - the public certificate to use for verifying +- `privateKey` - string or Buffer or crypto.KeyObject - the private key to use for signing +- `privateKeyPassphrase` - string - the passphrase to decrypt the private key +- `publicCert` - string or Buffer or crypto.KeyObject - the public certificate to use for verifying - `signatureAlgorithm` - string - the signature algorithm to use - `canonicalizationAlgorithm` - string - default `undefined` - the canonicalization algorithm to use - `inclusiveNamespacesPrefixList` - string - default `null` - a list of namespace prefixes to include during canonicalization diff --git a/src/signed-xml.ts b/src/signed-xml.ts index e5d80af..f38565d 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -32,6 +32,7 @@ export class SignedXml { * A {@link Buffer} or pem encoded {@link String} containing your private key */ privateKey?: crypto.KeyLike; + privateKeyPassphrase?: string; publicCert?: crypto.KeyLike; /** * One of the supported signature algorithms. @@ -264,7 +265,7 @@ export class SignedXml { const signedInfoCanon = this.getCanonSignedInfoXml(doc); const signer = this.findSignatureAlgorithm(this.signatureAlgorithm); - const key = this.getCertFromKeyInfo(this.keyInfo) || this.publicCert || this.privateKey; + const key = this.getCertFromKeyInfo(this.keyInfo) || this.publicCert || this.getPrivateKey(); if (key == null) { throw new Error("KeyInfo or publicCert or privateKey is required to validate signature"); } @@ -339,13 +340,14 @@ export class SignedXml { private calculateSignatureValue(doc: Document, callback?: ErrorFirstCallback) { const signedInfoCanon = this.getCanonSignedInfoXml(doc); const signer = this.findSignatureAlgorithm(this.signatureAlgorithm); - if (this.privateKey == null) { + const privateKey = this.getPrivateKey(); + if (privateKey === undefined) { throw new Error("Private key is required to compute signature"); } if (typeof callback === "function") { - signer.getSignature(signedInfoCanon, this.privateKey, callback); + signer.getSignature(signedInfoCanon, privateKey, callback); } else { - this.signatureValue = signer.getSignature(signedInfoCanon, this.privateKey); + this.signatureValue = signer.getSignature(signedInfoCanon, privateKey); } } @@ -1124,4 +1126,17 @@ export class SignedXml { getSignedXml(): string { return this.signedXml; } + + getPrivateKey(): crypto.KeyLike | undefined { + if ( + this.privateKeyPassphrase && + (this.privateKey instanceof Buffer || typeof this.privateKey === "string") + ) { + return crypto.createPrivateKey({ + key: this.privateKey, + passphrase: this.privateKeyPassphrase, + }); + } + return this.privateKey; + } } diff --git a/src/types.ts b/src/types.ts index 090c944..c45c74d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,6 +50,7 @@ export interface SignedXmlOptions { idMode?: "wssecurity"; idAttribute?: string; privateKey?: crypto.KeyLike; + privateKeyPassphrase?: string; publicCert?: crypto.KeyLike; signatureAlgorithm?: SignatureAlgorithmType; canonicalizationAlgorithm?: CanonicalizationAlgorithmType; diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 0db89a4..853c862 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1265,4 +1265,33 @@ describe("Signature unit tests", function () { "MIIDZ", ); }); + + it("can decrypt the private key when privateKeyPassphrase is set", function () { + const xml = ""; + const sig = new SignedXml(); + sig.privateKey = fs.readFileSync("./test/static/client_encrypted.pem"); + sig.privateKeyPassphrase = "password"; + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + expect(() => sig.computeSignature(xml)).to.not.throw(); + }); + + it("throws when privateKeyPassphrase is wrong", function () { + const xml = ""; + const sig = new SignedXml(); + sig.privateKey = fs.readFileSync("./test/static/client_encrypted.pem"); + sig.privateKeyPassphrase = "wrong password"; + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + expect(() => sig.computeSignature(xml)).to.throw(); + }); + + it("throws when privateKeyPassphrase is not set and private key is encrypted", function () { + const xml = ""; + const sig = new SignedXml(); + sig.privateKey = fs.readFileSync("./test/static/client_encrypted.pem"); + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + expect(() => sig.computeSignature(xml)).to.throw(); + }); }); diff --git a/test/static/client_encrypted.pem b/test/static/client_encrypted.pem new file mode 100644 index 0000000..93a1c7a --- /dev/null +++ b/test/static/client_encrypted.pem @@ -0,0 +1,18 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIC3TBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIVAXXug6NnZkCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBomZwMw4b2v6QQbrOXOqhEBIIC +gHaI/eE3LZ7QkEkXUj0pQqIeI5e7CqbXeqWjoICZmkDinzDVb5eLsFbA7iNjZ/On +tSon2uB5s33vgzC5L07lLUjl4Qvz4SN19xfurNpwKbSGYWqm0FfCjg/TK2L7Y7rV +SsA89f/SlqyTIfolsS9p2VxpAuboCpsHUH+c2YAKBUSK9bXjD8ncDEpVgZZXsUzK +o8Re2qwqwb1CyS0jS5pwlGjFXyLIf3km6apTOIifNwsvsqnZEgJjWSZMlQ8ESQMI +0J1wSPt9iU9B12hdfhr5sX9cIYqelP+Pa++tgWEOFWWOnvkD1vKhx4CoSMsgQs3u +O630F2iGaraAb254SSubVQSk0jjOXsjA85tjU2g8PjE6mjdzQDGJBPwxe0nLIaO6 +lJs4Qf08aclspYklbZ0Tu82xtAJ0MxRch/yXQ6SPVZL6wCAnwpQuzXV/CdB4dZ2M +xnjs3kWL/c/gTaIhjaOuTyeG32B9/j+wo1fc++wzXnMgBl1LIpgXQpOQBkZ3PjgE +/sjILPB9VHzigw0LbrT71E3jIoOHbQXh/F20wBDCLJzNNYBZARcK8wI2vQI7/glQ ++hjoC1PqOrKqejBCGcG3NOe+YDeyKfnXYJNA11psSuKBQ4/X/WaL29MfP8UloMrD +WIZkZVc6D6HdYCuFkeS3e7X5XyChIkBfDWFIbWsq9gVt9IhSJ9W8uDYaqsGQ/uBM +aG1brwk8mIJ0JLzz0FQTUpKVSMCUKyz3NCFxRU7wN7tD4/+pqXctnV8IugmNn/DE +aEgG7O+WW+d0Yqm0SIF++/Za6q/9N0GNvcywRu9tgP/5N2pTIygzJQwxuYmr61nU +YD0SBzeR8PoVot1DHQqiKI8= +-----END ENCRYPTED PRIVATE KEY-----