From 117e293c376d54665e3c7d7b5b2ed15ef530ada1 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 6 Feb 2021 22:44:27 +0100 Subject: [PATCH] crypto: support JWK objects in create*Key PR-URL: https://github.com/nodejs/node/pull/37254 Reviewed-By: James M Snell Reviewed-By: Matteo Collina --- doc/api/crypto.md | 25 ++-- lib/internal/crypto/keys.js | 146 ++++++++++++++++++++++- lib/internal/errors.js | 1 + test/parallel/test-crypto-key-objects.js | 130 +++++++++++++------- 4 files changed, 246 insertions(+), 56 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 2e0f18b8a84e74..4c55bcaa9cab81 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2452,6 +2452,9 @@ input.on('readable', () => { * `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView} - * `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView} The key material, - either in PEM or DER format. - * `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`. + * `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView|Object} The key + material, either in PEM, DER, or JWK format. + * `format`: {string} Must be `'pem'`, `'der'`, or '`'jwk'`. + **Default:** `'pem'`. * `type`: {string} Must be `'pkcs1'`, `'pkcs8'` or `'sec1'`. This option is - required only if the `format` is `'der'` and ignored if it is `'pem'`. + required only if the `format` is `'der'` and ignored otherwise. * `passphrase`: {string | Buffer} The passphrase to use for decryption. * `encoding`: {string} The string encoding to use when `key` is a string. * Returns: {KeyObject} @@ -2481,6 +2485,9 @@ of the passphrase is limited to 1024 bytes. * `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView} - * `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView} - * `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`. - * `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required - only if the `format` is `'der'`. + * `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView|Object} The key + material, either in PEM, DER, or JWK format. + * `format`: {string} Must be `'pem'`, `'der'`, or '`'jwk'`. + **Default:** `'pem'`. + * `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is + required only if the `format` is `'der'` and ignored otherwise. * `encoding` {string} The string encoding to use when `key` is a string. * Returns: {KeyObject} diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 9b37cb3a43b8c8..cc5b00a16ee021 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -26,6 +26,7 @@ const { const { validateObject, validateOneOf, + validateString, } = require('internal/validators'); const { @@ -38,6 +39,7 @@ const { ERR_OPERATION_FAILED, ERR_CRYPTO_JWK_UNSUPPORTED_CURVE, ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE, + ERR_CRYPTO_INVALID_JWK, } } = require('internal/errors'); @@ -65,6 +67,8 @@ const { const { inspect } = require('internal/util/inspect'); +const { Buffer } = require('buffer'); + const kAlgorithm = Symbol('kAlgorithm'); const kExtractable = Symbol('kExtractable'); const kKeyType = Symbol('kKeyType'); @@ -413,6 +417,122 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) { return types; } +function getKeyObjectHandleFromJwk(key, ctx) { + validateObject(key, 'key'); + validateOneOf( + key.kty, 'key.kty', ['RSA', 'EC', 'OKP']); + const isPublic = ctx === kConsumePublic || ctx === kCreatePublic; + + if (key.kty === 'OKP') { + validateString(key.crv, 'key.crv'); + validateOneOf( + key.crv, 'key.crv', ['Ed25519', 'Ed448', 'X25519', 'X448']); + validateString(key.x, 'key.x'); + + if (!isPublic) + validateString(key.d, 'key.d'); + + let keyData; + if (isPublic) + keyData = Buffer.from(key.x, 'base64'); + else + keyData = Buffer.from(key.d, 'base64'); + + switch (key.crv) { + case 'Ed25519': + case 'X25519': + if (keyData.byteLength !== 32) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + break; + case 'Ed448': + if (keyData.byteLength !== 57) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + break; + case 'X448': + if (keyData.byteLength !== 56) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + break; + } + + const handle = new KeyObjectHandle(); + if (isPublic) { + handle.initEDRaw( + `NODE-${key.crv.toUpperCase()}`, + keyData, + kKeyTypePublic); + } else { + handle.initEDRaw( + `NODE-${key.crv.toUpperCase()}`, + keyData, + kKeyTypePrivate); + } + + return handle; + } + + if (key.kty === 'EC') { + validateString(key.crv, 'key.crv'); + validateOneOf( + key.crv, 'key.crv', ['P-256', 'secp256k1', 'P-384', 'P-521']); + validateString(key.x, 'key.x'); + validateString(key.y, 'key.y'); + + const jwk = { + kty: key.kty, + crv: key.crv, + x: key.x, + y: key.y + }; + + if (!isPublic) { + validateString(key.d, 'key.d'); + jwk.d = key.d; + } + + const handle = new KeyObjectHandle(); + const type = handle.initJwk(jwk, jwk.crv); + if (type === undefined) + throw new ERR_CRYPTO_INVALID_JWK(); + + return handle; + } + + // RSA + validateString(key.n, 'key.n'); + validateString(key.e, 'key.e'); + + const jwk = { + kty: key.kty, + n: key.n, + e: key.e + }; + + if (!isPublic) { + validateString(key.d, 'key.d'); + validateString(key.p, 'key.p'); + validateString(key.q, 'key.q'); + validateString(key.dp, 'key.dp'); + validateString(key.dq, 'key.dq'); + validateString(key.qi, 'key.qi'); + jwk.d = key.d; + jwk.p = key.p; + jwk.q = key.q; + jwk.dp = key.dp; + jwk.dq = key.dq; + jwk.qi = key.qi; + } + + const handle = new KeyObjectHandle(); + const type = handle.initJwk(jwk); + if (type === undefined) + throw new ERR_CRYPTO_INVALID_JWK(); + + return handle; +} + function prepareAsymmetricKey(key, ctx) { if (isKeyObject(key)) { // Best case: A key object, as simple as that. @@ -423,13 +543,15 @@ function prepareAsymmetricKey(key, ctx) { // Expect PEM by default, mostly for backward compatibility. return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') }; } else if (typeof key === 'object') { - const { key: data, encoding } = key; + const { key: data, encoding, format } = key; // The 'key' property can be a KeyObject as well to allow specifying // additional options such as padding along with the key. if (isKeyObject(data)) return { data: getKeyObjectHandle(data, ctx) }; else if (isCryptoKey(data)) return { data: getKeyObjectHandle(data[kKeyObject], ctx) }; + else if (isJwk(data) && format === 'jwk') + return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' }; // Either PEM or DER using PKCS#1 or SPKI. if (!isStringOrBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( @@ -494,16 +616,26 @@ function createSecretKey(key, encoding) { function createPublicKey(key) { const { format, type, data, passphrase } = prepareAsymmetricKey(key, kCreatePublic); - const handle = new KeyObjectHandle(); - handle.init(kKeyTypePublic, data, format, type, passphrase); + let handle; + if (format === 'jwk') { + handle = data; + } else { + handle = new KeyObjectHandle(); + handle.init(kKeyTypePublic, data, format, type, passphrase); + } return new PublicKeyObject(handle); } function createPrivateKey(key) { const { format, type, data, passphrase } = prepareAsymmetricKey(key, kCreatePrivate); - const handle = new KeyObjectHandle(); - handle.init(kKeyTypePrivate, data, format, type, passphrase); + let handle; + if (format === 'jwk') { + handle = data; + } else { + handle = new KeyObjectHandle(); + handle.init(kKeyTypePrivate, data, format, type, passphrase); + } return new PrivateKeyObject(handle); } @@ -609,6 +741,10 @@ function isCryptoKey(obj) { return obj != null && obj[kKeyObject] !== undefined; } +function isJwk(obj) { + return obj != null && obj.kty !== undefined; +} + module.exports = { // Public API. createSecretKey, diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 194ea055a92e03..d8d565582ca970 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -836,6 +836,7 @@ E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error); E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.', Error); E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError); +E('ERR_CRYPTO_INVALID_JWK', 'Invalid JWK data', TypeError); E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', 'Invalid key object type %s, expected %s.', TypeError); E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 7133836789d864..6ee5334851dace 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -118,6 +118,37 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', } { + const jwk = { + e: 'AQAB', + n: 't9xYiIonscC3vz_A2ceR7KhZZlDu_5bye53nCVTcKnWd2seY6UAdKersX6njr83Dd5OVe' + + '1BW_wJvp5EjWTAGYbFswlNmeD44edEGM939B6Lq-_8iBkrTi8mGN4YCytivE24YI0D4XZ' + + 'MPfkLSpab2y_Hy4DjQKBq1ThZ0UBnK-9IhX37Ju_ZoGYSlTIGIhzyaiYBh7wrZBoPczIE' + + 'u6et_kN2VnnbRUtkYTF97ggcv5h-hDpUQjQW0ZgOMcTc8n-RkGpIt0_iM_bTjI3Tz_gsF' + + 'di6hHcpZgbopPL630296iByyigQCPJVzdusFrQN5DeC-zT_nGypQkZanLb4ZspSx9Q', + d: 'ktnq2LvIMqBj4txP82IEOorIRQGVsw1khbm8A-cEpuEkgM71Yi_0WzupKktucUeevQ5i0' + + 'Yh8w9e1SJiTLDRAlJz66kdky9uejiWWl6zR4dyNZVMFYRM43ijLC-P8rPne9Fz16IqHFW' + + '5VbJqA1xCBhKmuPMsD71RNxZ4Hrsa7Kt_xglQTYsLbdGIwDmcZihId9VGXRzvmCPsDRf2' + + 'fCkAj7HDeRxpUdEiEDpajADc-PWikra3r3b40tVHKWm8wxJLivOIN7GiYXKQIW6RhZgH-' + + 'Rk45JIRNKxNagxdeXUqqyhnwhbTo1Hite0iBDexN9tgoZk0XmdYWBn6ElXHRZ7VCDQ', + p: '8UovlB4nrBm7xH-u7XXBMbqxADQm5vaEZxw9eluc-tP7cIAI4sglMIvL_FMpbd2pEeP_B' + + 'kR76NTDzzDuPAZvUGRavgEjy0O9j2NAs_WPK4tZF-vFdunhnSh4EHAF4Ij9kbsUi90NOp' + + 'bGfVqPdOaHqzgHKoR23Cuusk9wFQ2XTV8', + q: 'wxHdEYT9xrpfrHPqSBQPpO0dWGKJEkrWOb-76rSfuL8wGR4OBNmQdhLuU9zTIh22pog-X' + + 'PnLPAecC-4yu_wtJ2SPCKiKDbJBre0CKPyRfGqzvA3njXwMxXazU4kGs-2Fg-xu_iKbaI' + + 'jxXrclBLhkxhBtySrwAFhxxOk6fFcPLSs', + dp: 'qS_Mdr5CMRGGMH0bKhPUWEtAixUGZhJaunX5wY71Xoc_Gh4cnO-b7BNJ_-5L8WZog0vr' + + '6PgiLhrqBaCYm2wjpyoG2o2wDHm-NAlzN_wp3G2EFhrSxdOux-S1c0kpRcyoiAO2n29rN' + + 'Da-jOzwBBcU8ACEPdLOCQl0IEFFJO33tl8', + dq: 'WAziKpxLKL7LnL4dzDcx8JIPIuwnTxh0plCDdCffyLaT8WJ9lXbXHFTjOvt8WfPrlDP_' + + 'Ylxmfkw5BbGZOP1VLGjZn2DkH9aMiwNmbDXFPdG0G3hzQovx_9fajiRV4DWghLHeT9wzJ' + + 'fZabRRiI0VQR472300AVEeX4vgbrDBn600', + qi: 'k7czBCT9rHn_PNwCa17hlTy88C4vXkwbz83Oa-aX5L4e5gw5lhcR2ZuZHLb2r6oMt9rl' + + 'D7EIDItSs-u21LOXWPTAlazdnpYUyw_CzogM_PN-qNwMRXn5uXFFhmlP2mVg2EdELTahX' + + 'ch8kWqHaCSX53yvqCtRKu_j76V31TfQZGM', + kty: 'RSA', + }; + const publicJwk = { kty: jwk.kty, e: jwk.e, n: jwk.n }; + const publicKey = createPublicKey(publicPem); assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); @@ -134,6 +165,16 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.strictEqual(derivedPublicKey.asymmetricKeyType, 'rsa'); assert.strictEqual(derivedPublicKey.symmetricKeySize, undefined); + const publicKeyFromJwk = createPublicKey({ key: publicJwk, format: 'jwk' }); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(publicKey.symmetricKeySize, undefined); + + const privateKeyFromJwk = createPrivateKey({ key: jwk, format: 'jwk' }); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.strictEqual(privateKey.symmetricKeySize, undefined); + // It should also be possible to import an encrypted private key as a public // key. const decryptedKey = createPublicKey({ @@ -158,46 +199,19 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', }); } - const jwk = { - e: 'AQAB', - n: 't9xYiIonscC3vz_A2ceR7KhZZlDu_5bye53nCVTcKnWd2seY6UAdKersX6njr83Dd5OVe' + - '1BW_wJvp5EjWTAGYbFswlNmeD44edEGM939B6Lq-_8iBkrTi8mGN4YCytivE24YI0D4XZ' + - 'MPfkLSpab2y_Hy4DjQKBq1ThZ0UBnK-9IhX37Ju_ZoGYSlTIGIhzyaiYBh7wrZBoPczIE' + - 'u6et_kN2VnnbRUtkYTF97ggcv5h-hDpUQjQW0ZgOMcTc8n-RkGpIt0_iM_bTjI3Tz_gsF' + - 'di6hHcpZgbopPL630296iByyigQCPJVzdusFrQN5DeC-zT_nGypQkZanLb4ZspSx9Q', - d: 'ktnq2LvIMqBj4txP82IEOorIRQGVsw1khbm8A-cEpuEkgM71Yi_0WzupKktucUeevQ5i0' + - 'Yh8w9e1SJiTLDRAlJz66kdky9uejiWWl6zR4dyNZVMFYRM43ijLC-P8rPne9Fz16IqHFW' + - '5VbJqA1xCBhKmuPMsD71RNxZ4Hrsa7Kt_xglQTYsLbdGIwDmcZihId9VGXRzvmCPsDRf2' + - 'fCkAj7HDeRxpUdEiEDpajADc-PWikra3r3b40tVHKWm8wxJLivOIN7GiYXKQIW6RhZgH-' + - 'Rk45JIRNKxNagxdeXUqqyhnwhbTo1Hite0iBDexN9tgoZk0XmdYWBn6ElXHRZ7VCDQ', - p: '8UovlB4nrBm7xH-u7XXBMbqxADQm5vaEZxw9eluc-tP7cIAI4sglMIvL_FMpbd2pEeP_B' + - 'kR76NTDzzDuPAZvUGRavgEjy0O9j2NAs_WPK4tZF-vFdunhnSh4EHAF4Ij9kbsUi90NOp' + - 'bGfVqPdOaHqzgHKoR23Cuusk9wFQ2XTV8', - q: 'wxHdEYT9xrpfrHPqSBQPpO0dWGKJEkrWOb-76rSfuL8wGR4OBNmQdhLuU9zTIh22pog-X' + - 'PnLPAecC-4yu_wtJ2SPCKiKDbJBre0CKPyRfGqzvA3njXwMxXazU4kGs-2Fg-xu_iKbaI' + - 'jxXrclBLhkxhBtySrwAFhxxOk6fFcPLSs', - dp: 'qS_Mdr5CMRGGMH0bKhPUWEtAixUGZhJaunX5wY71Xoc_Gh4cnO-b7BNJ_-5L8WZog0vr' + - '6PgiLhrqBaCYm2wjpyoG2o2wDHm-NAlzN_wp3G2EFhrSxdOux-S1c0kpRcyoiAO2n29rN' + - 'Da-jOzwBBcU8ACEPdLOCQl0IEFFJO33tl8', - dq: 'WAziKpxLKL7LnL4dzDcx8JIPIuwnTxh0plCDdCffyLaT8WJ9lXbXHFTjOvt8WfPrlDP_' + - 'Ylxmfkw5BbGZOP1VLGjZn2DkH9aMiwNmbDXFPdG0G3hzQovx_9fajiRV4DWghLHeT9wzJ' + - 'fZabRRiI0VQR472300AVEeX4vgbrDBn600', - qi: 'k7czBCT9rHn_PNwCa17hlTy88C4vXkwbz83Oa-aX5L4e5gw5lhcR2ZuZHLb2r6oMt9rl' + - 'D7EIDItSs-u21LOXWPTAlazdnpYUyw_CzogM_PN-qNwMRXn5uXFFhmlP2mVg2EdELTahX' + - 'ch8kWqHaCSX53yvqCtRKu_j76V31TfQZGM', - kty: 'RSA', - }; - - for (const keyObject of [publicKey, derivedPublicKey]) { + for (const keyObject of [publicKey, derivedPublicKey, publicKeyFromJwk]) { assert.deepStrictEqual( keyObject.export({ format: 'jwk' }), { kty: 'RSA', n: jwk.n, e: jwk.e } ); } - assert.deepStrictEqual( - privateKey.export({ format: 'jwk' }), - jwk - ); + + for (const keyObject of [privateKey, privateKeyFromJwk]) { + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + jwk + ); + } // Exporting the key using JWK should not work since this format does not // support key encryption @@ -235,10 +249,12 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', // Encrypt using the public key. publicEncrypt(publicKey, plaintext), publicEncrypt({ key: publicKey }, plaintext), + publicEncrypt({ key: publicJwk, format: 'jwk' }, plaintext), // Encrypt using the private key. publicEncrypt(privateKey, plaintext), publicEncrypt({ key: privateKey }, plaintext), + publicEncrypt({ key: jwk, format: 'jwk' }, plaintext), // Encrypt using a public key derived from the private key. publicEncrypt(derivedPublicKey, plaintext), @@ -251,7 +267,8 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', ], [ privateKey, { format: 'pem', key: privatePem }, - { format: 'der', type: 'pkcs1', key: privateDER } + { format: 'der', type: 'pkcs1', key: privateDER }, + { key: jwk, format: 'jwk' } ]); testDecryption(publicDecrypt, [ @@ -261,11 +278,13 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', publicKey, { format: 'pem', key: publicPem }, { format: 'der', type: 'pkcs1', key: publicDER }, + { key: publicJwk, format: 'jwk' }, // Decrypt using the private key. privateKey, { format: 'pem', key: privatePem }, - { format: 'der', type: 'pkcs1', key: privateDER } + { format: 'der', type: 'pkcs1', key: privateDER }, + { key: jwk, format: 'jwk' } ]); } @@ -359,8 +378,20 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', } { - [info.private, info.public].forEach((pem) => { - const key = createPublicKey(pem); + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + for (const input of [ + info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const key = createPublicKey(input); assert.strictEqual(key.type, 'public'); assert.strictEqual(key.asymmetricKeyType, keyType); assert.strictEqual(key.symmetricKeySize, undefined); @@ -370,7 +401,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', delete jwk.d; assert.deepStrictEqual( key.export({ format: 'jwk' }), jwk); - }); + } } }); @@ -438,8 +469,21 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', } { - [info.private, info.public].forEach((pem) => { - const key = createPublicKey(pem); + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } + + { + for (const input of [ + info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const key = createPublicKey(input); assert.strictEqual(key.type, 'public'); assert.strictEqual(key.asymmetricKeyType, keyType); assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); @@ -450,7 +494,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', delete jwk.d; assert.deepStrictEqual( key.export({ format: 'jwk' }), jwk); - }); + } } });