From 9f6e3d1bb8ccb71221979a9ba8e2260469f35883 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Jan 2021 18:21:14 +0100 Subject: [PATCH 01/10] crypto: add keyObject.export() 'jwk' format option Adds [JWK](https://tools.ietf.org/html/rfc7517) keyObject.export format option. Supported key types: `ec`, `rsa`, `ed25519`, `ed448`, `x25519`, `x448`, and symmetric keys, resulting in JWK `kty` (Key Type) values `EC`, `RSA`, `OKP`, and `oct`. `rsa-pss` is not supported since the JWK format does not support PSS Parameters. `EC` JWK curves supported are `P-256`, `secp256k1`, `P-384`, and `P-521` --- doc/api/crypto.md | 14 +- doc/api/errors.md | 14 ++ lib/internal/crypto/keys.js | 56 ++++- lib/internal/errors.js | 2 + test/fixtures/keys/ec_p256_private.pem | 5 + test/fixtures/keys/ec_p256_public.pem | 4 + test/fixtures/keys/ec_p384_private.pem | 6 + test/fixtures/keys/ec_p384_public.pem | 5 + test/fixtures/keys/ec_p521_private.pem | 8 + test/fixtures/keys/ec_p521_public.pem | 6 + test/fixtures/keys/ec_secp256k1_private.pem | 5 + test/fixtures/keys/ec_secp256k1_public.pem | 4 + test/parallel/test-crypto-key-objects.js | 226 +++++++++++++++++++- 13 files changed, 333 insertions(+), 22 deletions(-) create mode 100644 test/fixtures/keys/ec_p256_private.pem create mode 100644 test/fixtures/keys/ec_p256_public.pem create mode 100644 test/fixtures/keys/ec_p384_private.pem create mode 100644 test/fixtures/keys/ec_p384_public.pem create mode 100644 test/fixtures/keys/ec_p521_private.pem create mode 100644 test/fixtures/keys/ec_p521_public.pem create mode 100644 test/fixtures/keys/ec_secp256k1_private.pem create mode 100644 test/fixtures/keys/ec_secp256k1_public.pem diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 581216d02678f2..6f28f408b56294 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1351,24 +1351,22 @@ added: v11.6.0 --> * `options`: {Object} -* Returns: {string | Buffer} +* Returns: {string | Buffer | JWK} -For symmetric keys, this function allocates a `Buffer` containing the key -material and ignores any options. +For symmetric keys, the following encoding options can be used: -For asymmetric keys, the `options` parameter is used to determine the export -format. +* `format`: {string} Must be `'buffer'` (default) or `'jwk'`. For public keys, the following encoding options can be used: * `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`. -* `format`: {string} Must be `'pem'` or `'der'`. +* `format`: {string} Must be `'pem'`, `'der'`, or `'jwk'`. For private keys, the following encoding options can be used: * `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or `'sec1'` (EC only). -* `format`: {string} Must be `'pem'` or `'der'`. +* `format`: {string} Must be `'pem'`, `'der'`, or `'jwk'`. * `cipher`: {string} If specified, the private key will be encrypted with the given `cipher` and `passphrase` using PKCS#5 v2.0 password based encryption. @@ -1378,6 +1376,8 @@ For private keys, the following encoding options can be used: When PEM encoding was selected, the result will be a string, otherwise it will be a buffer containing the data encoded as DER. +When JWK encoding was selected, all other encoding options are ignored. + PKCS#1, SEC1, and PKCS#8 type keys can be encrypted by using a combination of the `cipher` and `format` options. The PKCS#8 `type` can be used with any `format` to encrypt any key algorithm (RSA, EC, or DH) by specifying a diff --git a/doc/api/errors.md b/doc/api/errors.md index 896a403188cbc2..15ff6593586dfc 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -950,6 +950,18 @@ release binaries but can happen with custom builds, including distro builds. A signing `key` was not provided to the [`sign.sign()`][] method. + +### `ERR_JWK_UNSUPPORTED_CURVE` + +Key's Elliptic Curve is not registered for use in the +[JSON Web Key Elliptic Curve Registry][]. + + +### `ERR_JWK_UNSUPPORTED_KEY_TYPE` + +Key's Asymmetric Key Type is not registered for use in the +[JSON Web Key Types Registry][]. + ### `ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH` @@ -2716,6 +2728,8 @@ The native call from `process.cpuUsage` could not be processed. [ES Module]: esm.md [ICU]: intl.md#intl_internationalization_support +[JSON Web Key Elliptic Curve Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve +[JSON Web Key Types Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-types [Node.js error codes]: #nodejs-error-codes [RFC 7230 Section 3]: https://tools.ietf.org/html/rfc7230#section-3 [Subresource Integrity specification]: https://www.w3.org/TR/SRI/#the-integrity-attribute diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 071a97954cf151..2dd6aeb13c4b03 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -22,6 +22,11 @@ const { kKeyEncodingSEC1, } = internalBinding('crypto'); +const { + validateObject, + validateOneOf, +} = require('internal/validators'); + const { codes: { ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, @@ -30,6 +35,8 @@ const { ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE, ERR_OPERATION_FAILED, + ERR_JWK_UNSUPPORTED_CURVE, + ERR_JWK_UNSUPPORTED_KEY_TYPE, } } = require('internal/errors'); @@ -124,13 +131,22 @@ const [ return this[kHandle].getSymmetricKeySize(); } - export() { + export(options) { + if (options !== undefined) { + validateObject(options, 'options'); + validateOneOf( + options.format, 'options.format', [undefined, 'buffer', 'jwk']); + if (options.format === 'jwk') { + return this[kHandle].exportJwk({}); + } + } return this[kHandle].export(); } } const kAsymmetricKeyType = Symbol('kAsymmetricKeyType'); const kAsymmetricKeyDetails = Symbol('kAsymmetricKeyDetails'); + const kAsymmetricKeyJWKProperties = Symbol('kAsymmetricKeyJWKProperties'); function normalizeKeyDetails(details = {}) { if (details.publicExponent !== undefined) { @@ -163,6 +179,28 @@ const [ return {}; } } + + [kAsymmetricKeyJWKProperties]() { + switch (this.asymmetricKeyType) { + case 'rsa': return {}; + case 'ec': + switch (this.asymmetricKeyDetails.namedCurve) { + case 'prime256v1': return { crv: 'P-256' }; + case 'secp256k1': return { crv: 'secp256k1' }; + case 'secp384r1': return { crv: 'P-384' }; + case 'secp521r1': return { crv: 'P-521' }; + default: + throw new ERR_JWK_UNSUPPORTED_CURVE( + this.asymmetricKeyDetails.namedCurve); + } + case 'ed25519': return { crv: 'Ed25519' }; + case 'ed448': return { crv: 'Ed448' }; + case 'x25519': return { crv: 'X25519' }; + case 'x448': return { crv: 'X448' }; + default: + throw new ERR_JWK_UNSUPPORTED_KEY_TYPE(); + } + } } class PublicKeyObject extends AsymmetricKeyObject { @@ -170,11 +208,15 @@ const [ super('public', handle); } - export(encoding) { + export(options) { + if (options && options.format === 'jwk') { + const properties = this[kAsymmetricKeyJWKProperties](); + return this[kHandle].exportJwk(properties); + } const { format, type - } = parsePublicKeyEncoding(encoding, this.asymmetricKeyType); + } = parsePublicKeyEncoding(options, this.asymmetricKeyType); return this[kHandle].export(format, type); } } @@ -184,13 +226,17 @@ const [ super('private', handle); } - export(encoding) { + export(options) { + if (options && options.format === 'jwk') { + const properties = this[kAsymmetricKeyJWKProperties](); + return this[kHandle].exportJwk(properties); + } const { format, type, cipher, passphrase - } = parsePrivateKeyEncoding(encoding, this.asymmetricKeyType); + } = parsePrivateKeyEncoding(options, this.asymmetricKeyType); return this[kHandle].export(format, type, cipher, passphrase); } } diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 631f31291353eb..76b1f1bcf6d20c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1234,6 +1234,8 @@ E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error); E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error); E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error); E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', Error); +E('ERR_JWK_UNSUPPORTED_CURVE', 'Unsupported JWK EC curve: %s.', Error); +E('ERR_JWK_UNSUPPORTED_KEY_TYPE', 'Unsupported JWK Key Type.', Error); E('ERR_MANIFEST_ASSERT_INTEGRITY', (moduleURL, realIntegrities) => { let msg = `The content of "${ diff --git a/test/fixtures/keys/ec_p256_private.pem b/test/fixtures/keys/ec_p256_private.pem new file mode 100644 index 00000000000000..6bb0bb9cfdfe99 --- /dev/null +++ b/test/fixtures/keys/ec_p256_private.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDxBsPQPIgMuMyQbx +zbb9toew6Ev6e9O6ZhpxLNgmAEqhRANCAARfSYxhH+6V5lIg+M3O0iQBLf+53kuE +2luIgWnp81/Ya1Gybj8tl4tJVu1GEwcTyt8hoA7vRACmCHnI5B1+bNpS +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/ec_p256_public.pem b/test/fixtures/keys/ec_p256_public.pem new file mode 100644 index 00000000000000..08f7bd26d3c5f3 --- /dev/null +++ b/test/fixtures/keys/ec_p256_public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEX0mMYR/uleZSIPjNztIkAS3/ud5L +hNpbiIFp6fNf2GtRsm4/LZeLSVbtRhMHE8rfIaAO70QApgh5yOQdfmzaUg== +-----END PUBLIC KEY----- diff --git a/test/fixtures/keys/ec_p384_private.pem b/test/fixtures/keys/ec_p384_private.pem new file mode 100644 index 00000000000000..06393e263c81d7 --- /dev/null +++ b/test/fixtures/keys/ec_p384_private.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB3B+4e4C1OUxGftkEI +Gb/SCulzUP/iE940CB6+B6WWO4LT76T8sMWiwOAGUsuZmyKhZANiAASE43efMYmC +/7Tx90elDGBEkVnOUr4ZkMZrl/cqe8zfVy++MmayPhR46Ah3LesMCNV+J0eG15w0 +IYJ8uqasuMN6drU1LNbNYfW7+hR0woajldJpvHMPv7wlnGOlzyxH1yU= +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/ec_p384_public.pem b/test/fixtures/keys/ec_p384_public.pem new file mode 100644 index 00000000000000..2b50f3bbc73760 --- /dev/null +++ b/test/fixtures/keys/ec_p384_public.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEhON3nzGJgv+08fdHpQxgRJFZzlK+GZDG +a5f3KnvM31cvvjJmsj4UeOgIdy3rDAjVfidHhtecNCGCfLqmrLjDena1NSzWzWH1 +u/oUdMKGo5XSabxzD7+8JZxjpc8sR9cl +-----END PUBLIC KEY----- diff --git a/test/fixtures/keys/ec_p521_private.pem b/test/fixtures/keys/ec_p521_private.pem new file mode 100644 index 00000000000000..e4a8a655c9ee89 --- /dev/null +++ b/test/fixtures/keys/ec_p521_private.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAEghuafcab9jXW4gO +QLeDaKOlHEiskQFjiL8klijk6i6DNOXcFfaJ9GW48kxpodw16ttAf9Z1WQstfzpK +GUetHImhgYkDgYYABAGixYI8Gbc5zNze6rH2/OmsFV3unOnY1GDqG9RTfpJZXpL9 +ChF1dG8HA4zxkM+X+jMSwm4THh0Wr1Euj9dK7E7QZwHd35XsQXgH13Hjc0QR9dvJ +BWzlg+luNTY8CkaqiBdur5oFv/AjpXRimYxZDkhAEsTwXLwNohSUVMkN8IQtNI9D +aQ== +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/ec_p521_public.pem b/test/fixtures/keys/ec_p521_public.pem new file mode 100644 index 00000000000000..c0ed00f6429dd4 --- /dev/null +++ b/test/fixtures/keys/ec_p521_public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBosWCPBm3Oczc3uqx9vzprBVd7pzp +2NRg6hvUU36SWV6S/QoRdXRvBwOM8ZDPl/ozEsJuEx4dFq9RLo/XSuxO0GcB3d+V +7EF4B9dx43NEEfXbyQVs5YPpbjU2PApGqogXbq+aBb/wI6V0YpmMWQ5IQBLE8Fy8 +DaIUlFTJDfCELTSPQ2k= +-----END PUBLIC KEY----- diff --git a/test/fixtures/keys/ec_secp256k1_private.pem b/test/fixtures/keys/ec_secp256k1_private.pem new file mode 100644 index 00000000000000..f753c751bf720d --- /dev/null +++ b/test/fixtures/keys/ec_secp256k1_private.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgc34ocwTwpFa9NZZh3l88 +qXyrkoYSxvC0FEsU5v1v4IOhRANCAARw7OEVKlbGFqUJtY10/Yf/JSR0LzUL1PZ1 +4Ol/ErujAPgNwwGU5PSD6aTfn9NycnYB2hby9XwB2qF3+El+DV8q +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/ec_secp256k1_public.pem b/test/fixtures/keys/ec_secp256k1_public.pem new file mode 100644 index 00000000000000..e95322efb42dcb --- /dev/null +++ b/test/fixtures/keys/ec_secp256k1_public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEcOzhFSpWxhalCbWNdP2H/yUkdC81C9T2 +deDpfxK7owD4DcMBlOT0g+mk35/TcnJ2AdoW8vV8Adqhd/hJfg1fKg== +-----END PUBLIC KEY----- diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 1d34aa5f6eea6d..32c4dc330cf8f5 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -18,7 +18,8 @@ const { publicDecrypt, publicEncrypt, privateDecrypt, - privateEncrypt + privateEncrypt, + generateKeyPairSync } = require('crypto'); const fixtures = require('../common/fixtures'); @@ -156,6 +157,47 @@ 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]) { + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + { kty: 'RSA', n: jwk.n, e: jwk.e } + ); + } + assert.deepStrictEqual( + privateKey.export({ format: 'jwk' }), + jwk + ); + const publicDER = publicKey.export({ format: 'der', type: 'pkcs1' @@ -254,36 +296,150 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', [ { private: fixtures.readKey('ed25519_private.pem', 'ascii'), public: fixtures.readKey('ed25519_public.pem', 'ascii'), - keyType: 'ed25519' }, + keyType: 'ed25519', + jwk: { + crv: 'Ed25519', + x: 'K1wIouqnuiA04b3WrMa-xKIKIpfHetNZRv3h9fBf768', + d: 'wVK6M3SMhQh3NK-7GRrSV-BVWQx1FO5pW8hhQeu_NdA', + kty: 'OKP' + } }, { private: fixtures.readKey('ed448_private.pem', 'ascii'), public: fixtures.readKey('ed448_public.pem', 'ascii'), - keyType: 'ed448' }, + keyType: 'ed448', + jwk: { + crv: 'Ed448', + x: 'oX_ee5-jlcU53-BbGRsGIzly0V-SZtJ_oGXY0udf84q2hTW2RdstLktvwpkVJOoNb7o' + + 'Dgc2V5ZUA', + d: '060Ke71sN0GpIc01nnGgMDkp0sFNQ09woVo4AM1ffax1-mjnakK0-p-S7-Xf859QewX' + + 'jcR9mxppY', + kty: 'OKP' + } }, { private: fixtures.readKey('x25519_private.pem', 'ascii'), public: fixtures.readKey('x25519_public.pem', 'ascii'), - keyType: 'x25519' }, + keyType: 'x25519', + jwk: { + crv: 'X25519', + x: 'aSb8Q-RndwfNnPeOYGYPDUN3uhAPnMLzXyfi-mqfhig', + d: 'mL_IWm55RrALUGRfJYzw40gEYWMvtRkesP9mj8o8Omc', + kty: 'OKP' + } }, { private: fixtures.readKey('x448_private.pem', 'ascii'), public: fixtures.readKey('x448_public.pem', 'ascii'), - keyType: 'x448' }, + keyType: 'x448', + jwk: { + crv: 'X448', + x: 'ioHSHVpTs6hMvghosEJDIR7ceFiE3-Xccxati64oOVJ7NWjfozE7ae31PXIUFq6cVYg' + + 'vSKsDFPA', + d: 'tMNtrO_q8dlY6Y4NDeSTxNQ5CACkHiPvmukidPnNIuX_EkcryLEXt_7i6j6YZMKsrWy' + + 'S0jlSYJk', + kty: 'OKP' + } }, ].forEach((info) => { const keyType = info.keyType; { - const exportOptions = { type: 'pkcs8', format: 'pem' }; const key = createPrivateKey(info.private); assert.strictEqual(key.type, 'private'); assert.strictEqual(key.asymmetricKeyType, keyType); assert.strictEqual(key.symmetricKeySize, undefined); - assert.strictEqual(key.export(exportOptions), info.private); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); } { - const exportOptions = { type: 'spki', format: 'pem' }; [info.private, info.public].forEach((pem) => { const key = createPublicKey(pem); assert.strictEqual(key.type, 'public'); assert.strictEqual(key.asymmetricKeyType, keyType); assert.strictEqual(key.symmetricKeySize, undefined); - assert.strictEqual(key.export(exportOptions), info.public); + assert.strictEqual( + key.export({ type: 'spki', format: 'pem' }), info.public); + const jwk = { ...info.jwk }; + delete jwk.d; + assert.deepStrictEqual( + key.export({ format: 'jwk' }), jwk); + }); + } +}); + +[ + { private: fixtures.readKey('ec_p256_private.pem', 'ascii'), + public: fixtures.readKey('ec_p256_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'prime256v1', + jwk: { + crv: 'P-256', + d: 'DxBsPQPIgMuMyQbxzbb9toew6Ev6e9O6ZhpxLNgmAEo', + kty: 'EC', + x: 'X0mMYR_uleZSIPjNztIkAS3_ud5LhNpbiIFp6fNf2Gs', + y: 'UbJuPy2Xi0lW7UYTBxPK3yGgDu9EAKYIecjkHX5s2lI' + } }, + { private: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'), + public: fixtures.readKey('ec_secp256k1_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp256k1', + jwk: { + crv: 'secp256k1', + d: 'c34ocwTwpFa9NZZh3l88qXyrkoYSxvC0FEsU5v1v4IM', + kty: 'EC', + x: 'cOzhFSpWxhalCbWNdP2H_yUkdC81C9T2deDpfxK7owA', + y: '-A3DAZTk9IPppN-f03JydgHaFvL1fAHaoXf4SX4NXyo' + } }, + { private: fixtures.readKey('ec_p384_private.pem', 'ascii'), + public: fixtures.readKey('ec_p384_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp384r1', + jwk: { + crv: 'P-384', + d: 'dwfuHuAtTlMRn7ZBCBm_0grpc1D_4hPeNAgevgelljuC0--k_LDFosDgBlLLmZsi', + kty: 'EC', + x: 'hON3nzGJgv-08fdHpQxgRJFZzlK-GZDGa5f3KnvM31cvvjJmsj4UeOgIdy3rDAjV', + y: 'fidHhtecNCGCfLqmrLjDena1NSzWzWH1u_oUdMKGo5XSabxzD7-8JZxjpc8sR9cl' + } }, + { private: fixtures.readKey('ec_p521_private.pem', 'ascii'), + public: fixtures.readKey('ec_p521_public.pem', 'ascii'), + keyType: 'ec', + namedCurve: 'secp521r1', + jwk: { + crv: 'P-521', + d: 'ABIIbmn3Gm_Y11uIDkC3g2ijpRxIrJEBY4i_JJYo5OougzTl3BX2ifRluPJMaaHcNer' + + 'bQH_WdVkLLX86ShlHrRyJ', + kty: 'EC', + x: 'AaLFgjwZtznM3N7qsfb86awVXe6c6djUYOob1FN-kllekv0KEXV0bwcDjPGQz5f6MxL' + + 'CbhMeHRavUS6P10rsTtBn', + y: 'Ad3flexBeAfXceNzRBH128kFbOWD6W41NjwKRqqIF26vmgW_8COldGKZjFkOSEASxPB' + + 'cvA2iFJRUyQ3whC00j0Np' + } }, +].forEach((info) => { + const { keyType, namedCurve } = info; + + { + const key = createPrivateKey(info.private); + 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); + } + + { + [info.private, info.public].forEach((pem) => { + const key = createPublicKey(pem); + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'spki', format: 'pem' }), info.public); + const jwk = { ...info.jwk }; + delete jwk.d; + assert.deepStrictEqual( + key.export({ format: 'jwk' }), jwk); }); } }); @@ -321,6 +477,9 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'dsa'); assert.strictEqual(publicKey.symmetricKeySize, undefined); + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { code: 'ERR_JWK_UNSUPPORTED_KEY_TYPE' }); const privateKey = createPrivateKey({ key: privateDsa, @@ -330,7 +489,9 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'dsa'); assert.strictEqual(privateKey.symmetricKeySize, undefined); - + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { code: 'ERR_JWK_UNSUPPORTED_KEY_TYPE' }); } { @@ -350,6 +511,13 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { code: 'ERR_JWK_UNSUPPORTED_KEY_TYPE' }); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { code: 'ERR_JWK_UNSUPPORTED_KEY_TYPE' }); + for (const key of [privatePem, privateKey]) { // Any algorithm should work. for (const algo of ['sha1', 'sha256']) { @@ -485,3 +653,41 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', message: "The property 'options.cipher' is invalid. Received undefined" }); } + +{ + // SecretKeyObject export buffer format (default) + const buffer = Buffer.from('Hello World'); + const keyObject = createSecretKey(buffer); + assert(buffer.equals(keyObject.export())); + assert(buffer.equals(keyObject.export({}))); + assert(buffer.equals(keyObject.export({ format: 'buffer' }))); + assert(buffer.equals(keyObject.export({ format: undefined }))); +} + +{ + // Exporting an "oct" JWK from a SecretKeyObject + const buffer = Buffer.from('Hello World'); + const keyObject = createSecretKey(buffer); + assert.deepStrictEqual( + keyObject.export({ format: 'jwk' }), + { kty: 'oct', k: 'SGVsbG8gV29ybGQ' } + ); +} + +{ + // Exporting a JWK unsupported curve EC key + const keyPair = generateKeyPairSync('ec', { namedCurve: 'prime192v1' }); + const { publicKey, privateKey } = keyPair; + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { + code: 'ERR_JWK_UNSUPPORTED_CURVE', + message: 'Unsupported JWK EC curve: prime192v1.' + }); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { + code: 'ERR_JWK_UNSUPPORTED_CURVE', + message: 'Unsupported JWK EC curve: prime192v1.' + }); +} From 5ba12fbd2e17954c036a595e9ba001aedbff48f3 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Jan 2021 18:32:26 +0100 Subject: [PATCH 02/10] add custom doc type --- tools/doc/type-parser.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 70a3fede3d51fd..13a606e58560ed 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -72,6 +72,7 @@ const customTypesMap = { 'ECDH': 'crypto.html#crypto_class_ecdh', 'Hash': 'crypto.html#crypto_class_hash', 'Hmac': 'crypto.html#crypto_class_hmac', + 'JWK': 'https://tools.ietf.org/html/rfc7517', 'KeyObject': 'crypto.html#crypto_class_keyobject', 'Sign': 'crypto.html#crypto_class_sign', 'Verify': 'crypto.html#crypto_class_verify', From 1cfd2433f745297d2915f7252a7126316622d300 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Jan 2021 18:36:30 +0100 Subject: [PATCH 03/10] add doc changes entry --- doc/api/crypto.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 6f28f408b56294..02dd84e95fa0d4 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1348,6 +1348,10 @@ keys. ### `keyObject.export([options])` * `options`: {Object} From 25a6c8c4f01cc024f7425e8247bc42fd0a64740c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Jan 2021 20:59:25 +0100 Subject: [PATCH 04/10] address doc comments --- doc/api/crypto.md | 11 +++++++---- tools/doc/type-parser.js | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 02dd84e95fa0d4..665e6f49481cf3 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1355,7 +1355,7 @@ changes: --> * `options`: {Object} -* Returns: {string | Buffer | JWK} +* Returns: {string | Buffer | Object} For symmetric keys, the following encoding options can be used: @@ -1377,10 +1377,12 @@ For private keys, the following encoding options can be used: * `passphrase`: {string | Buffer} The passphrase to use for encryption, see `cipher`. -When PEM encoding was selected, the result will be a string, otherwise it will -be a buffer containing the data encoded as DER. +The result type depends on the selected encoding format, when PEM the +result is a string, when DER it will be a buffer containing the data +encoded as DER, when [JWK][] it will be a an object. -When JWK encoding was selected, all other encoding options are ignored. +When [JWK][] encoding format was selected, all other encoding options are +ignored. PKCS#1, SEC1, and PKCS#8 type keys can be encrypted by using a combination of the `cipher` and `format` options. The PKCS#8 `type` can be used with any @@ -4359,6 +4361,7 @@ See the [list of SSL OP Flags][] for details. [Crypto constants]: #crypto_crypto_constants_1 [HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed [HTML5's `keygen` element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/keygen +[JWK]: https://tools.ietf.org/html/rfc7517 [NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf [NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 13a606e58560ed..70a3fede3d51fd 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -72,7 +72,6 @@ const customTypesMap = { 'ECDH': 'crypto.html#crypto_class_ecdh', 'Hash': 'crypto.html#crypto_class_hash', 'Hmac': 'crypto.html#crypto_class_hmac', - 'JWK': 'https://tools.ietf.org/html/rfc7517', 'KeyObject': 'crypto.html#crypto_class_keyobject', 'Sign': 'crypto.html#crypto_class_sign', 'Verify': 'crypto.html#crypto_class_verify', From 955696f25c0c383dae3f4fb40df8fae671144042 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Jan 2021 22:28:17 +0100 Subject: [PATCH 05/10] add makefile for fixtures --- test/fixtures/keys/Makefile | 42 ++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index 824704c7241b0a..1c6c9c520870ae 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -75,6 +75,14 @@ all: \ ed448_public.pem \ x448_private.pem \ x448_public.pem \ + ec_p256_private.pem \ + ec_p256_public.pem \ + ec_p384_private.pem \ + ec_p384_public.pem \ + ec_p521_private.pem \ + ec_p521_public.pem \ + ec_secp256k1_private.pem \ + ec_secp256k1_public.pem \ # # Create Certificate Authority: ca1 @@ -663,7 +671,7 @@ rsa_cert_foafssl_b.modulus: rsa_cert_foafssl_b.crt # Have to parse out the hex exponent rsa_cert_foafssl_b.exponent: rsa_cert_foafssl_b.crt - openssl x509 -in rsa_cert_foafssl_b.crt -text | grep -o 'Exponent:.*' | sed 's/\(.*(\|).*\)//g' > rsa_cert_foafssl_b.exponent + openssl x509 -in rsa_cert_foafssl_b.crt -text | grep -o 'Exponent:.*' | sed 's/\(.*(\|).*\)//g' > rsa_cert_foafssl_b.exponent # openssl outputs `SPKAC=[SPKAC]`. That prefix needs to be removed to work with node rsa_spkac.spkac: rsa_private.pem @@ -733,6 +741,38 @@ x448_private.pem: x448_public.pem: x448_private.pem openssl pkey -in x448_private.pem -pubout -out x448_public.pem +ec_p256_private.pem: + openssl ecparam -name prime256v1 -genkey -noout -out sec1_ec_p256_private.pem + openssl pkcs8 -topk8 -nocrypt -in sec1_ec_p256_private.pem -out ec_p256_private.pem + rm sec1_ec_p256_private.pem + +ec_p256_public.pem: ec_p256_private.pem + openssl ec -in ec_p256_private.pem -pubout -out ec_p256_public.pem + +ec_p384_private.pem: + openssl ecparam -name secp384r1 -genkey -noout -out sec1_ec_p384_private.pem + openssl pkcs8 -topk8 -nocrypt -in sec1_ec_p384_private.pem -out ec_p384_private.pem + rm sec1_ec_p384_private.pem + +ec_p384_public.pem: ec_p384_private.pem + openssl ec -in ec_p384_private.pem -pubout -out ec_p384_public.pem + +ec_p521_private.pem: + openssl ecparam -name secp521r1 -genkey -noout -out sec1_ec_p521_private.pem + openssl pkcs8 -topk8 -nocrypt -in sec1_ec_p521_private.pem -out ec_p521_private.pem + rm sec1_ec_p521_private.pem + +ec_p521_public.pem: ec_p521_private.pem + openssl ec -in ec_p521_private.pem -pubout -out ec_p521_public.pem + +ec_secp256k1_private.pem: + openssl ecparam -name secp256k1 -genkey -noout -out sec1_ec_secp256k1_private.pem + openssl pkcs8 -topk8 -nocrypt -in sec1_ec_secp256k1_private.pem -out ec_secp256k1_private.pem + rm sec1_ec_secp256k1_private.pem + +ec_secp256k1_public.pem: ec_secp256k1_private.pem + openssl ec -in ec_secp256k1_private.pem -pubout -out ec_secp256k1_public.pem + clean: rm -f *.pfx *.pem *.srl ca2-database.txt ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem @> fake-startcom-root-database.txt From a76db7afba02eae64adf0faf04f325b95bd5e8ef Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Jan 2021 22:37:12 +0100 Subject: [PATCH 06/10] get a curve not supported on openssl111fips --- test/parallel/test-crypto-key-objects.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 32c4dc330cf8f5..5e58fa93abe045 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -19,6 +19,7 @@ const { publicEncrypt, privateDecrypt, privateEncrypt, + getCurves, generateKeyPairSync } = require('crypto'); @@ -676,18 +677,22 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', { // Exporting a JWK unsupported curve EC key - const keyPair = generateKeyPairSync('ec', { namedCurve: 'prime192v1' }); + const supported = ['prime256v1', 'secp256k1', 'secp384r1', 'secp521r1']; + // Find an unsupported curve regardless of whether a FIPS compliant crypto + // provider is currently in use. + const namedCurve = getCurves().find((curve) => !supported.includes(curve)); + const keyPair = generateKeyPairSync('ec', { namedCurve }); const { publicKey, privateKey } = keyPair; assert.throws( () => publicKey.export({ format: 'jwk' }), { code: 'ERR_JWK_UNSUPPORTED_CURVE', - message: 'Unsupported JWK EC curve: prime192v1.' + message: `Unsupported JWK EC curve: ${namedCurve}.` }); assert.throws( () => privateKey.export({ format: 'jwk' }), { code: 'ERR_JWK_UNSUPPORTED_CURVE', - message: 'Unsupported JWK EC curve: prime192v1.' + message: `Unsupported JWK EC curve: ${namedCurve}.` }); } From 5f03cdfc58593170f60359428fb6fd6c17464e6d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 27 Jan 2021 00:11:19 +0100 Subject: [PATCH 07/10] Update doc/api/crypto.md Co-authored-by: James M Snell --- doc/api/crypto.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 665e6f49481cf3..7777e692f9bc0c 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1379,7 +1379,7 @@ For private keys, the following encoding options can be used: The result type depends on the selected encoding format, when PEM the result is a string, when DER it will be a buffer containing the data -encoded as DER, when [JWK][] it will be a an object. +encoded as DER, when [JWK][] it will be an object. When [JWK][] encoding format was selected, all other encoding options are ignored. From 5ca795c89e74d9c2be10c3112f3bb176c482ff6f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 31 Jan 2021 17:03:59 +0100 Subject: [PATCH 08/10] apply review feedback --- doc/api/errors.md | 24 ++++++++++++------------ lib/internal/crypto/keys.js | 8 ++++---- lib/internal/errors.js | 4 ++-- test/parallel/test-crypto-key-objects.js | 12 ++++++------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index 15ff6593586dfc..411e2a4221004e 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -919,6 +919,18 @@ added: v15.0.0 Initialization of an asynchronous crypto operation failed. + +### `ERR_CRYPTO_JWK_UNSUPPORTED_CURVE` + +Key's Elliptic Curve is not registered for use in the +[JSON Web Key Elliptic Curve Registry][]. + + +### `ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE` + +Key's Asymmetric Key Type is not registered for use in the +[JSON Web Key Types Registry][]. + ### `ERR_CRYPTO_OPERATION_FAILED` * `options`: {Object} diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 46a3d688f7932e..32ae162546b5ac 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -659,10 +659,10 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', // SecretKeyObject export buffer format (default) const buffer = Buffer.from('Hello World'); const keyObject = createSecretKey(buffer); - assert(buffer.equals(keyObject.export())); - assert(buffer.equals(keyObject.export({}))); - assert(buffer.equals(keyObject.export({ format: 'buffer' }))); - assert(buffer.equals(keyObject.export({ format: undefined }))); + assert.deepStrictEqual(keyObject.export(), buffer); + assert.deepStrictEqual(keyObject.export({}), buffer); + assert.deepStrictEqual(keyObject.export({ format: 'buffer' }), buffer); + assert.deepStrictEqual(keyObject.export({ format: undefined }), buffer); } { @@ -681,6 +681,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', // Find an unsupported curve regardless of whether a FIPS compliant crypto // provider is currently in use. const namedCurve = getCurves().find((curve) => !supported.includes(curve)); + assert(namedCurve); const keyPair = generateKeyPairSync('ec', { namedCurve }); const { publicKey, privateKey } = keyPair; assert.throws( From a6d1e961774a60f5b99b57de2740f40ccc43e5fe Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 1 Feb 2021 18:49:53 +0100 Subject: [PATCH 10/10] fail on private export with a passphrase --- lib/internal/crypto/keys.js | 4 ++++ test/parallel/test-crypto-key-objects.js | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index b7e811ff4eeeed..c4c60f3d226de7 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -228,6 +228,10 @@ const [ export(options) { if (options && options.format === 'jwk') { + if (options.passphrase !== undefined) { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + 'jwk', 'does not support encryption'); + } const properties = this[kAsymmetricKeyJWKProperties](); return this[kHandle].exportJwk(properties); } diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 32ae162546b5ac..7133836789d864 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -199,6 +199,15 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', jwk ); + // Exporting the key using JWK should not work since this format does not + // support key encryption + assert.throws(() => { + privateKey.export({ format: 'jwk', passphrase: 'secret' }); + }, { + message: 'The selected key encoding jwk does not support encryption.', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' + }); + const publicDER = publicKey.export({ format: 'der', type: 'pkcs1'