From 87f39d55b28e8a88bcf12dc73a62ce7a3ea02c49 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 3 Feb 2021 16:46:38 +0100 Subject: [PATCH] crypto: introduce crypto/promises Refs: https://github.com/nodejs/node/issues/36181 --- doc/api/crypto.md | 128 ++++++++- lib/crypto.js | 12 + lib/crypto/promises.js | 3 + lib/internal/crypto/diffiehellman.js | 42 +++ lib/internal/crypto/promises.js | 266 +++++++++++++++++ node.gyp | 2 + .../test-crypto-promises-diffiehellman.js | 272 ++++++++++++++++++ test/parallel/test-crypto-promises-digest.js | 97 +++++++ test/parallel/test-crypto-promises-sig.js | 114 ++++++++ 9 files changed, 934 insertions(+), 2 deletions(-) create mode 100644 lib/crypto/promises.js create mode 100644 lib/internal/crypto/promises.js create mode 100644 test/parallel/test-crypto-promises-diffiehellman.js create mode 100644 test/parallel/test-crypto-promises-digest.js create mode 100644 test/parallel/test-crypto-promises-sig.js diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 9088b78babdadb..28a766c5ca29ab 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2551,8 +2551,8 @@ added: * Returns: {Buffer} Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`. -Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'` -(for Diffie-Hellman), `'ec'` (for ECDH), `'x448'`, or `'x25519'` (for ECDH-ES). +Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'`, +`'ec'`, `'x448'`, or `'x25519'`. ### `crypto.generateKey(type, options, callback)` + +> Stability: 1 - Experimental + +The `crypto.promises` API provides an alternative set of asynchronous crypto +methods that return `Promise` objects and execute operations in libuv's +threadpool. +The API is accessible via `require('crypto').promises` or `require('crypto/promises')`. + +### `cryptoPromises.diffieHellman(options)` + + +* `options`: {Object} + * `privateKey`: {KeyObject|CryptoKey} + * `publicKey`: {KeyObject|CryptoKey} +* Returns: {Promise} containing {Buffer} + +Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`. +Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'`, +`'ec'`, `'x448'`, or `'x25519'`. + +### `cryptoPromises.digest(algorithm, data[, options])` + + +* `algorithm` {string} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} +* `options` {Object} + * `outputLength` {number} Used to specify the desired output length in bytes + for XOF hash functions such as `'shake256'`. +* Returns: {Promise} containing {Buffer} + +Calculates the digest for the `data` using the given `algorithm`. + +The `algorithm` is dependent on the available algorithms supported by the +version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. +On recent releases of OpenSSL, `openssl list -digest-algorithms` +(`openssl list-message-digest-algorithms` for older versions of OpenSSL) will +display the available digest algorithms. + +### `cryptoPromises.hmac(algorithm, data, key)` + + +* `algorithm` {string} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} +* `key` {KeyObject|CryptoKey} +* Returns: {Promise} containing {Buffer} + +Calculates the HMAC digest for the `data` using the given `algorithm`. + +The `algorithm` is dependent on the available algorithms supported by the +version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. +On recent releases of OpenSSL, `openssl list -digest-algorithms` +(`openssl list-message-digest-algorithms` for older versions of OpenSSL) will +display the available digest algorithms. + +### `cryptoPromises.sign(algorithm, data, key[, options])` + + +* `algorithm` {string|null|undefined} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} +* `key` {KeyObject|CryptoKey} +* `options` {Object} + * `dsaEncoding` {string} For DSA and ECDSA, this option specifies the + format of the generated signature. It can be one of the following: + * `'der'` (default): DER-encoded ASN.1 signature structure encoding `(r, s)`. + * `'ieee-p1363'`: Signature format `r || s` as proposed in IEEE-P1363. + * `padding` {integer} Optional padding value for RSA, one of the following: + * `crypto.constants.RSA_PKCS1_PADDING` (default) + * `crypto.constants.RSA_PKCS1_PSS_PADDING` + * `saltLength` {integer} Salt length for when padding is + `RSA_PKCS1_PSS_PADDING`. The special value + `crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest + size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the + maximum permissible value. +* Returns: {Promise} containing {Buffer} + +Calculates the signature for `data` using the given private key and +algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is +dependent upon the key type (especially Ed25519 and Ed448). + +### `cryptoPromises.verify(algorithm, data, key, signature[, options])` + + +* `algorithm` {string|null|undefined} +* `data` {ArrayBuffer|TypedArray|DataView|Buffer} +* `key` {KeyObject|CryptoKey} +* `signature` {ArrayBuffer|TypedArray|DataView|Buffer} +* `options` {Object} + * `dsaEncoding` {string} For DSA and ECDSA, this option specifies the + format of the generated signature. It can be one of the following: + * `'der'` (default): DER-encoded ASN.1 signature structure encoding `(r, s)`. + * `'ieee-p1363'`: Signature format `r || s` as proposed in IEEE-P1363. + * `padding` {integer} Optional padding value for RSA, one of the following: + * `crypto.constants.RSA_PKCS1_PADDING` (default) + * `crypto.constants.RSA_PKCS1_PSS_PADDING` + * `saltLength` {integer} Salt length for when padding is + `RSA_PKCS1_PSS_PADDING`. The special value + `crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest + size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the + maximum permissible value. +* Returns: {Promise} containing {boolean} + +Verifies the given signature for `data` using the given key and algorithm. If +`algorithm` is `null` or `undefined`, then the algorithm is dependent upon the +key type (especially Ed25519 and Ed448). + +The `signature` argument is the previously calculated signature for the `data`. + +Because public keys can be derived from private keys, a private key or a public +key may be passed for `key`. + ## Notes ### Legacy streams API (prior to Node.js 0.10) diff --git a/lib/crypto.js b/lib/crypto.js index 3f110bef5844f8..98cd74f428373d 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -172,6 +172,9 @@ function createVerify(algorithm, options) { return new Verify(algorithm, options); } +// Lazy loaded +let promises = null; + module.exports = { // Methods checkPrime, @@ -327,5 +330,14 @@ ObjectDefineProperties(module.exports, { value: pendingDeprecation ? deprecate(randomBytes, 'crypto.rng is deprecated.', 'DEP0115') : randomBytes + }, + promises: { + configurable: true, + enumerable: true, + get() { + if (promises === null) + promises = require('internal/crypto/promises').exports; + return promises; + } } }); diff --git a/lib/crypto/promises.js b/lib/crypto/promises.js new file mode 100644 index 00000000000000..29ed875864efa8 --- /dev/null +++ b/lib/crypto/promises.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('internal/crypto/promises').exports; diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index bcf9b1e5090d1a..31716e10fdc6b5 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -346,6 +346,46 @@ function deriveBitsDH(publicKey, privateKey, callback) { job.run(); } +async function asyncDiffieHellman(publicKey, privateKey) { + const { asymmetricKeyType } = privateKey; + + if (asymmetricKeyType === 'dh') { + return new Promise((resolve, reject) => { + deriveBitsDH( + publicKey[kHandle], + privateKey[kHandle], + (err, bits) => { + if (err) reject(err); + else resolve(bits); + }); + }); + } + + if (asymmetricKeyType === 'x25519' || asymmetricKeyType === 'x448') { + return new Promise((resolve, reject) => { + deriveBitsECDH( + `NODE-${asymmetricKeyType.toUpperCase()}`, + publicKey[kHandle], + privateKey[kHandle], + (err, bits) => { + if (err) reject(err); + else resolve(bits); + }); + }); + } + + return new Promise((resolve, reject) => { + deriveBitsECDH( + privateKey.asymmetricKeyDetails.namedCurve, + publicKey[kHandle], + privateKey[kHandle], + (err, bits) => { + if (err) reject(err); + else resolve(bits); + }); + }); +} + function verifyAcceptableDhKeyUse(name, type, usages) { let checkSet; switch (type) { @@ -601,9 +641,11 @@ module.exports = { DiffieHellman, DiffieHellmanGroup, ECDH, + asyncDiffieHellman, diffieHellman, deriveBitsECDH, deriveBitsDH, + dhEnabledKeyTypes, dhGenerateKey, asyncDeriveBitsECDH, asyncDeriveBitsDH, diff --git a/lib/internal/crypto/promises.js b/lib/internal/crypto/promises.js new file mode 100644 index 00000000000000..729f35f1253ed9 --- /dev/null +++ b/lib/internal/crypto/promises.js @@ -0,0 +1,266 @@ +'use strict'; + +const { + ArrayPrototypeIncludes, + ArrayPrototypeJoin, +} = primordials; + +const { + HashJob, + HmacJob, + kCryptoJobAsync, + kSigEncDER, + kSigEncP1363, + kSignJobModeSign, + kSignJobModeVerify, + SignJob, +} = internalBinding('crypto'); + +const { + crypto: constants, +} = internalBinding('constants'); + +const { + asyncDiffieHellman, + dhEnabledKeyTypes, +} = require('internal/crypto/diffiehellman'); + +const { + createPublicKey, + isCryptoKey, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + jobPromise, + kHandle, + kKeyObject, +} = require('internal/crypto/util'); + +const { + hideStackFrames, + codes: { + ERR_CRYPTO_INCOMPATIBLE_KEY, + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + } +} = require('internal/errors'); + +const { + isArrayBufferView, +} = require('internal/util/types'); + +const { + validateObject, + validateString, + validateUint32, +} = require('internal/validators'); + +const { Buffer } = require('buffer'); + +const validateKeyType = hideStackFrames((value, oneOf) => { + if (!ArrayPrototypeIncludes(oneOf, value)) { + const allowed = ArrayPrototypeJoin(oneOf, ' or '); + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(value, allowed); + } +}); + +function getIntOption(name, options) { + if (typeof options === 'object') { + const value = options[name]; + if (value !== undefined) { + if (value === value >> 0) { + return value; + } + throw new ERR_INVALID_ARG_VALUE(`options.${name}`, value); + } + } + return undefined; +} + +function getPadding(options) { + return getIntOption('padding', options); +} + +function getSaltLength(options) { + return getIntOption('saltLength', options); +} + +function getOutputLength(options) { + if (typeof options === 'object') { + if (options.outputLength !== undefined) { + validateUint32(options.outputLength, 'options.outputLength'); + return options.outputLength << 3; + } + } +} + +function getDSASignatureEncoding(options) { + if (typeof options === 'object') { + const { dsaEncoding = 'der' } = options; + if (dsaEncoding === 'der') + return kSigEncDER; + else if (dsaEncoding === 'ieee-p1363') + return kSigEncP1363; + throw new ERR_INVALID_ARG_VALUE('options.dsaEncoding', dsaEncoding); + } + + return kSigEncDER; +} + +function getKeyObject(key, argument, ...types) { + if (isCryptoKey(key)) + key = key[kKeyObject]; + + if (!isKeyObject(key)) + throw new ERR_INVALID_ARG_TYPE(argument, ['KeyObject', 'CryptoKey'], key); + + validateKeyType(key.type, types); + + return key; +} + +function getArrayBufferView(data, argument) { + if (!isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE( + argument, + ['Buffer', 'TypedArray', 'DataView'], + data + ); + } + + return data; +} + +function getData(data) { + return getArrayBufferView(data, 'data'); +} + +function getSignature(data) { + return getArrayBufferView(data, 'signature'); +} + +async function digest(algorithm, data, options) { + validateString(algorithm, 'algorithm'); + + data = getData(data); + + const outputLength = getOutputLength(options); + + const digest = await jobPromise(new HashJob( + kCryptoJobAsync, + algorithm, + data, + outputLength)); + + return Buffer.from(digest); +} + +async function hmac(algorithm, data, key) { + validateString(algorithm, 'algorithm'); + + data = getData(data); + + const keyObject = getKeyObject(key, 'key', 'secret'); + + const digest = await jobPromise(new HmacJob( + kCryptoJobAsync, + kSignJobModeSign, + algorithm, + keyObject[kHandle], + data)); + + return Buffer.from(digest); +} + +async function sign(algorithm, data, key, options) { + if (algorithm != null) + validateString(algorithm, 'algorithm'); + + data = getData(data); + + const keyObject = getKeyObject(key, 'key', 'private'); + + // Options specific to RSA + const rsaPadding = getPadding(options); + const pssSaltLength = getSaltLength(options); + + // Options specific to (EC)DSA + // TODO(@jasnell): add dsaSigEnc to SignJob + const dsaSigEnc = getDSASignatureEncoding(options); + + const signature = await jobPromise(new SignJob( + kCryptoJobAsync, + kSignJobModeSign, + keyObject[kHandle], + data, + algorithm, + pssSaltLength, + rsaPadding)); + + return Buffer.from(signature); +} + +async function verify(algorithm, data, key, signature, options) { + if (algorithm != null) + validateString(algorithm, 'algorithm'); + + data = getData(data); + + let keyObject = getKeyObject(key, 'key', 'public', 'private'); + + if (keyObject.type === 'private') + keyObject = createPublicKey(keyObject); + + signature = getSignature(signature); + + // Options specific to RSA + const rsaPadding = getPadding(options); + const pssSaltLength = getSaltLength(options); + + // Options specific to (EC)DSA + // TODO(@jasnell): add dsaSigEnc to SignJob + const dsaSigEnc = getDSASignatureEncoding(options); + + return jobPromise(new SignJob( + kCryptoJobAsync, + kSignJobModeVerify, + keyObject[kHandle], + data, + algorithm, + pssSaltLength, + rsaPadding, + signature)); +} + +async function diffieHellman(options) { + validateObject(options, 'options'); + + const privateKey = getKeyObject(options.privateKey, + 'options.privateKey', 'private'); + const publicKey = getKeyObject(options.publicKey, + 'options.publicKey', 'public'); + + const privateType = privateKey.asymmetricKeyType; + const publicType = publicKey.asymmetricKeyType; + if (privateType !== publicType || !dhEnabledKeyTypes.has(privateType)) { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY('key types for Diffie-Hellman', + `${privateType} and ${publicType}`); + } + + const bits = await asyncDiffieHellman(publicKey, privateKey); + + return Buffer.from(bits); +} + +module.exports = { + exports: { + diffieHellman, + digest, + hmac, + sign, + verify, + constants, + } +}; diff --git a/node.gyp b/node.gyp index 01ef5e3b6a2f27..8157083c79907e 100644 --- a/node.gyp +++ b/node.gyp @@ -47,6 +47,7 @@ 'lib/console.js', 'lib/constants.js', 'lib/crypto.js', + 'lib/crypto/promises.js', 'lib/cluster.js', 'lib/diagnostics_channel.js', 'lib/dgram.js', @@ -135,6 +136,7 @@ 'lib/internal/crypto/keys.js', 'lib/internal/crypto/mac.js', 'lib/internal/crypto/pbkdf2.js', + 'lib/internal/crypto/promises.js', 'lib/internal/crypto/random.js', 'lib/internal/crypto/rsa.js', 'lib/internal/crypto/scrypt.js', diff --git a/test/parallel/test-crypto-promises-diffiehellman.js b/test/parallel/test-crypto-promises-diffiehellman.js new file mode 100644 index 00000000000000..01caefb837604a --- /dev/null +++ b/test/parallel/test-crypto-promises-diffiehellman.js @@ -0,0 +1,272 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createDiffieHellman, + createPrivateKey, + createPublicKey, + generateKeyPairSync, + getDiffieHellman, +} = require('crypto'); + +const { + diffieHellman, +} = require('crypto/promises'); + +async function testDiffieHellman() { + // Test cryptoPromises.diffieHellman + await assert.rejects(() => diffieHellman(), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. Received undefined' + }); + + await assert.rejects(() => diffieHellman(null), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. Received null' + }); + + await assert.rejects(() => diffieHellman([]), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: + 'The "options" argument must be of type object. ' + + 'Received an instance of Array', + }); + + async function test( + { publicKey: alicePublicKey, privateKey: alicePrivateKey }, + { publicKey: bobPublicKey, privateKey: bobPrivateKey }, + expectedValue) { + const buf1 = await diffieHellman({ + privateKey: alicePrivateKey, + publicKey: bobPublicKey + }); + const buf2 = await diffieHellman({ + privateKey: bobPrivateKey, + publicKey: alicePublicKey + }); + assert.deepStrictEqual(buf1, buf2); + + if (expectedValue !== undefined) + assert.deepStrictEqual(buf1, expectedValue); + } + + const alicePrivateKey = createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + + '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + + 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwEh82IAVnYNf0Kjb\n' + + 'qYSImDFyg9sH6CJ0GzRK05e6hM3dOSClFYi4kbA7Pr7zyfdn2SH6wSlNS14Jyrtt\n' + + 'HePrRSeYl1T+tk0AfrvaLmyM56F+9B3jwt/nzqr5YxmfVdXb2aQV53VS/mm3pB2H\n' + + 'iIt9FmvFaaOVe2DupqSr6xzbf/zyON+WF5B5HNVOWXswgpgdUsCyygs98hKy/Xje\n' + + 'TGzJUoWInW39t0YgMXenJrkS0m6wol8Rhxx81AGgELNV7EHZqg==\n' + + '-----END PRIVATE KEY-----', + format: 'pem' + }); + + const alicePublicKey = createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + + 'MIIBnzCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + + 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + + 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxAACgcBR7+iL5qx7aOb9K+aZ\n' + + 'y2oLt7ST33sDKT+nxpag6cWDDWzPBKFDCJ8fr0v7yW453px8N4qi4R7SYYxFBaYN\n' + + 'Y3JvgDg1ct2JC9sxSuUOLqSFn3hpmAjW7cS0kExIVGfdLlYtIqbhhuo45cTEbVIM\n' + + 'rDEz8mjIlnvbWpKB9+uYmbjfVoc3leFvUBqfG2In2m23Md1swsPxr3n7g68H66JX\n' + + 'iBJKZLQMqNdbY14G9rdKmhhTJrQjC+i7Q/wI8JPhOFzHIGA=\n' + + '-----END PUBLIC KEY-----', + format: 'pem' + }); + + const bobPrivateKey = createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + + '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + + 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwHxnT7Zw2Ehh1vyw\n' + + 'eolzQFHQzyuT0y+3BF+FxK2Ox7VPguTp57wQfGHbORJ2cwCdLx2mFM7gk4tZ6COS\n' + + 'E3Vta85a/PuhKXNLRdP79JgLnNtVtKXB+ePDS5C2GgXH1RHvqEdJh7JYnMy7Zj4P\n' + + 'GagGtIy3dV5f4FA0B/2C97jQ1pO16ah8gSLQRKsNpTCw2rqsZusE0rK6RaYAef7H\n' + + 'y/0tmLIsHxLIn+WK9CANqMbCWoP4I178BQaqhiOBkNyNZ0ndqA==\n' + + '-----END PRIVATE KEY-----', + format: 'pem' + }); + + const bobPublicKey = createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + + 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + + 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + + 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxQACgcEAi26oq8z/GNSBm3zi\n' + + 'gNt7SA7cArUBbTxINa9iLYWp6bxrvCKwDQwISN36/QUw8nUAe8aRyMt0oYn+y6vW\n' + + 'Pw5OlO+TLrUelMVFaADEzoYomH0zVGb0sW4aBN8haC0mbrPt9QshgCvjr1hEPEna\n' + + 'QFKfjzNaJRNMFFd4f2Dn8MSB4yu1xpA1T2i0JSk24vS2H55jx24xhUYtfhT2LJgK\n' + + 'JvnaODey/xtY4Kql10ZKf43Lw6gdQC3G8opC9OxVxt9oNR7Z\n' + + '-----END PUBLIC KEY-----', + format: 'pem' + }); + + await assert.rejects(() => diffieHellman({ privateKey: alicePrivateKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.publicKey" property must be an instance of ' + + 'KeyObject or CryptoKey. Received undefined' + }); + + await assert.rejects(() => diffieHellman({ publicKey: alicePublicKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.privateKey" property must be an instance of ' + + 'KeyObject or CryptoKey. Received undefined' + }); + + const privateKey = Buffer.from( + '487CD880159D835FD0A8DBA9848898317283DB07E822741B344AD397BA84CDDD3920A515' + + '88B891B03B3EBEF3C9F767D921FAC1294D4B5E09CABB6D1DE3EB4527989754FEB64D007E' + + 'BBDA2E6C8CE7A17EF41DE3C2DFE7CEAAF963199F55D5DBD9A415E77552FE69B7A41D8788' + + '8B7D166BC569A3957B60EEA6A4ABEB1CDB7FFCF238DF961790791CD54E597B3082981D52' + + 'C0B2CA0B3DF212B2FD78DE4C6CC95285889D6DFDB746203177A726B912D26EB0A25F1187' + + '1C7CD401A010B355EC41D9AA', 'hex'); + const publicKey = Buffer.from( + '8b6ea8abccff18d4819b7ce280db7b480edc02b5016d3c4835af622d85a9e9bc6bbc22b0' + + '0d0c0848ddfafd0530f275007bc691c8cb74a189fecbabd63f0e4e94ef932eb51e94c545' + + '6800c4ce8628987d335466f4b16e1a04df21682d266eb3edf50b21802be3af58443c49da' + + '40529f8f335a25134c1457787f60e7f0c481e32bb5c690354f68b4252936e2f4b61f9e63' + + 'c76e3185462d7e14f62c980a26f9da3837b2ff1b58e0aaa5d7464a7f8dcbc3a81d402dc6' + + 'f28a42f4ec55c6df68351ed9', 'hex'); + + const group = getDiffieHellman('modp5'); + const dh = createDiffieHellman(group.getPrime(), group.getGenerator()); + dh.setPrivateKey(privateKey); + + // Test simple Diffie-Hellman, no curves involved. + + await test( + { publicKey: alicePublicKey, privateKey: alicePrivateKey }, + { publicKey: bobPublicKey, privateKey: bobPrivateKey }, + dh.computeSecret(publicKey)); + + await test( + generateKeyPairSync('dh', { group: 'modp5' }), + generateKeyPairSync('dh', { group: 'modp5' })); + + await test( + generateKeyPairSync('dh', { group: 'modp5' }), + generateKeyPairSync('dh', { prime: group.getPrime() })); + + // TODO(@jasnell): instead of rejecting this resolves, the underlying + // implementation resolves an empty buffer. + // for (const [params1, params2] of [ + // // Same generator, but different primes. + // [{ group: 'modp5' }, { group: 'modp18' }], + // // Same primes, but different generator. + // [{ group: 'modp5' }, { prime: group.getPrime(), generator: 5 }], + // // Same generator, but different primes. + // [{ primeLength: 1024 }, { primeLength: 1024 }] + // ]) { + // await assert.rejects(() => { + // return test( + // generateKeyPairSync('dh', params1), + // generateKeyPairSync('dh', params2)); + // }, { + // name: 'Error', + // code: 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' + // }); + // } + + { + const privateKey = createPrivateKey({ + key: + '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + + '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + + 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwHu9fpiqrfJJ+tl9\n' + + 'ujFtEWv4afub6A/1/7sgishOYN3YQ+nmWQlmPpveIY34an5dG82CTrixHwUzQTMF\n' + + 'JaiCW3ax9+qk31f2jTNKrQznmKgopVKXF0FEJC6H79W/8Y0U14gsI9sHpovKhfou\n' + + 'RQD0QogW7ejSwMG8hCYibfrvMm0b5PHlwimISyEKh7VtDQ1frYN/Wr9ZbiV+FePJ\n' + + '2j6RUKYNj1Pv+B4zdMgiLLjILAs8WUfbHciU21KSJh1izVQaUQ==\n' + + '-----END PRIVATE KEY-----' + }); + const publicKey = createPublicKey({ + key: + '-----BEGIN PUBLIC KEY-----\n' + + 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + + 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + + 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxQACgcEAmG9LpD8SAA6/W7oK\n' + + 'E4MCuuQtf5E8bqtcEAfYTOOvKyCS+eiX3TtZRsvHJjUBEyeO99PR/KrGVlkSuW52\n' + + 'ZOSXUOFu1L/0tqHrvRVHo+QEq3OvZ3EAyJkdtSEUTztxuUrMOyJXHDc1OUdNSnk0\n' + + 'taGX4mP3247golVx2DS4viDYs7UtaMdx03dWaP6y5StNUZQlgCIUzL7MYpC16V5y\n' + + 'KkFrE+Kp/Z77gEjivaG6YuxVj4GPLxJYbNFVTel42oSVeKuq\n' + + '-----END PUBLIC KEY-----', + format: 'pem' + }); + + // This key combination will result in an unusually short secret, and should + // not cause an assertion failure. + const secret = await diffieHellman({ publicKey, privateKey }); + assert.strictEqual( + secret.toString('hex'), + '0099d0fa242af5db9ea7330e23937a27db041f79c581500fc7f9976' + + '554d59d5b9ced934778d72e19a1fefc81e9d981013198748c0b5c6c' + + '762985eec687dc5bec5c9367b05837daee9d0bcc29024ed7f3abba1' + + '2794b65a745117fb0d87bc5b1b2b68c296c3f686cc29e450e4e1239' + + '21f56a5733fe58aabf71f14582954059c2185d342b9b0fa10c2598a' + + '5426c2baee7f9a686fc1e16cd4757c852bf7225a2732250548efe28' + + 'debc26f1acdec51efe23d20786a6f8a14d360803bbc71972e87fd3'); + } + + // Test w/ ec keys. + + await test( + generateKeyPairSync('ec', { namedCurve: 'secp256k1' }), + generateKeyPairSync('ec', { namedCurve: 'secp256k1' })); + + // TODO(@jasnell): this rejects with an openssl error instead of the + // node `ERR_OSSL_EVP_DIFFERENT_PARAMETERS` code one. + // const not256k1 = getCurves().find((c) => /^sec.*(224|384|512)/.test(c)); + // await assert.rejects(() => { + // return test( + // generateKeyPairSync('ec', { namedCurve: 'secp256k1' }), + // generateKeyPairSync('ec', { namedCurve: not256k1 })); + // }, { + // name: 'Error', + // code: 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' + // }); + + // Test w/ x25519 and x448 keys. + + await test( + generateKeyPairSync('x448'), + generateKeyPairSync('x448')); + + await test( + generateKeyPairSync('x25519'), + generateKeyPairSync('x25519')); + + await assert.rejects( + () => test( + generateKeyPairSync('x448'), + generateKeyPairSync('x25519')), + { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY', + message: 'Incompatible key types for Diffie-Hellman: x448 and x25519' + }); +} + +testDiffieHellman().then(common.mustCall()); diff --git a/test/parallel/test-crypto-promises-digest.js b/test/parallel/test-crypto-promises-digest.js new file mode 100644 index 00000000000000..4a7d05f254705d --- /dev/null +++ b/test/parallel/test-crypto-promises-digest.js @@ -0,0 +1,97 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const { + digest, +} = require('crypto/promises'); + +async function testDigest() { + // Test cryptoPromises.digest + + // Test XOF hash functions and the outputLength option. + { + // Default outputLengths. + assert.deepStrictEqual( + await digest('shake128', Buffer.alloc(0)), + Buffer.from('7f9c2ba4e88f827d616045507605853e', 'hex')); + assert.deepStrictEqual( + await digest('shake256', Buffer.alloc(0)), + Buffer.from('46b9dd2b0ba88d13233b3feb743eeb24' + + '3fcd52ea62b81b82b50c27646ed5762f', 'hex')); + + // Short outputLengths. + assert.deepStrictEqual( + await digest('shake128', Buffer.alloc(0), { outputLength: 0 }), + Buffer.alloc(0)); + assert.deepStrictEqual( + await digest('shake128', Buffer.alloc(0), { outputLength: 5 }), + Buffer.from('7f9c2ba4e8', 'hex')); + assert.deepStrictEqual( + await digest('shake128', Buffer.alloc(0), { outputLength: 15 }), + Buffer.from('7f9c2ba4e88f827d61604550760585', 'hex')); + assert.deepStrictEqual( + await digest('shake256', Buffer.alloc(0), { outputLength: 16 }), + Buffer.from('46b9dd2b0ba88d13233b3feb743eeb24', 'hex')); + + // Large outputLengths. + assert.deepStrictEqual( + await digest('shake128', Buffer.alloc(0), { outputLength: 128 }), + Buffer.from('7f9c2ba4e88f827d616045507605853e' + + 'd73b8093f6efbc88eb1a6eacfa66ef26' + + '3cb1eea988004b93103cfb0aeefd2a68' + + '6e01fa4a58e8a3639ca8a1e3f9ae57e2' + + '35b8cc873c23dc62b8d260169afa2f75' + + 'ab916a58d974918835d25e6a435085b2' + + 'badfd6dfaac359a5efbb7bcc4b59d538' + + 'df9a04302e10c8bc1cbf1a0b3a5120ea', 'hex')); + + const superLongHash = await digest( + 'shake256', + Buffer.from('The message is shorter than the hash!'), + { outputLength: 1024 * 1024 }); + assert.strictEqual(superLongHash.length, 1024 * 1024); + + const end = Buffer.from('193414035ddba77bf7bba97981e656ec', 'hex'); + assert.strictEqual( + superLongHash.indexOf(end), + superLongHash.length - end.length); + assert.strictEqual( + superLongHash.indexOf( + Buffer.from('a2a28dbc49cfd6e5d6ceea3d03e77748', 'hex') + ), + 0); + + // Non-XOF hash functions should accept valid outputLength options as well. + assert.deepStrictEqual( + await digest('sha224', Buffer.alloc(0), { outputLength: 28 }), + Buffer.from('d14a028c2a3a2bc9476102bb288234c4' + + '15a2b01f828ea62ac5b3e42f', 'hex')); + + // Passing invalid sizes should throw during creation. + // TODO(@jasnell): rejects ERR_CRYPTO_INVALID_DIGEST instead of + // ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH + // + // await assert.rejects( + // () => digest('sha256', Buffer.alloc(0), { outputLength: 28 }), + // { code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH' }); + + for (const outputLength of [null, {}, 'foo', false]) { + await assert.rejects( + () => digest('sha256', Buffer.alloc(0), { outputLength }), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + for (const outputLength of [-1, .5, Infinity, 2 ** 90]) { + await assert.rejects( + () => digest('sha256', Buffer.alloc(0), { outputLength }), + { code: 'ERR_OUT_OF_RANGE' }); + } + } +} + +testDigest().then(common.mustCall()); diff --git a/test/parallel/test-crypto-promises-sig.js b/test/parallel/test-crypto-promises-sig.js new file mode 100644 index 00000000000000..8a4471bfa99574 --- /dev/null +++ b/test/parallel/test-crypto-promises-sig.js @@ -0,0 +1,114 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createPrivateKey, + createPublicKey, +} = require('crypto'); + +const { + sign, + verify, +} = require('crypto/promises'); + +const fixtures = require('../common/fixtures'); + +async function testRsaPss() { + // Test cryptoPromises.(sign/verify) w/ RSA-PSS. + { + // This key pair does not restrict the message digest algorithm or salt + // length. + const publicPem = fixtures.readKey('rsa_pss_public_2048.pem'); + const privatePem = fixtures.readKey('rsa_pss_private_2048.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Any algorithm should work. + for (const algo of ['sha1', 'sha256']) { + // Any salt length should work. + for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { + const signature = await sign( + algo, Buffer.from('foo'), privateKey, { saltLength }); + + for (const key of [publicKey, privateKey]) { + const okay = await verify( + algo, Buffer.from('foo'), key, signature, { saltLength }); + + assert.ok(okay); + } + } + } + } + + { + // This key pair enforces sha256 as the message digest and the MGF1 + // message digest and a salt length of at least 16 bytes. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha256_sha256_16.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Signing with anything other than sha256 should fail. + await assert.rejects(() => sign('sha1', Buffer.from('foo'), privateKey), /digest not allowed/); + + // Signing with salt lengths less than 16 bytes should fail. + for (const saltLength of [8, 10, 12]) { + await assert.rejects(() => sign('sha256', Buffer.from('foo'), privateKey, { saltLength }), /pss saltlen too small/); + } + + // Signing with sha256 and appropriate salt lengths should work. + for (const saltLength of [undefined, 16, 18, 20]) { + const signature = await sign( + 'sha256', Buffer.from('foo'), privateKey, { saltLength }); + + for (const key of [publicKey, privateKey]) { + const okay = await verify( + 'sha256', Buffer.from('foo'), key, signature, { saltLength }); + + assert.ok(okay); + } + } + } + + { + // This key enforces sha512 as the message digest and sha256 as the MGF1 + // message digest. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha512_sha256_20.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha512_sha256_20.pem'); + + const publicKey = createPublicKey(publicPem); + const privateKey = createPrivateKey(privatePem); + + // Node.js usually uses the same hash function for the message and for MGF1. + // However, when a different MGF1 message digest algorithm has been + // specified as part of the key, it should automatically switch to that. + // This behavior is required by sections 3.1 and 3.3 of RFC4055. + + // sha256 matches the MGF1 hash function and should be used internally, + // but it should not be permitted as the main message digest algorithm. + for (const algo of ['sha1', 'sha256']) { + await assert.rejects(() => sign(algo, Buffer.from('foo'), privateKey), /digest not allowed/); + } + + // sha512 should produce a valid signature. + const signature = await sign('sha512', Buffer.from('foo'), privateKey); + + for (const pkey of [publicKey, privateKey]) { + const okay = await verify('sha512', Buffer.from('foo'), pkey, signature); + + assert.ok(okay); + } + } +} + +testRsaPss().then(common.mustCall());