Skip to content

Commit

Permalink
crypto: add keyObject.export() 'jwk' format option
Browse files Browse the repository at this point in the history
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`

PR-URL: #37081
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
  • Loading branch information
panva committed Feb 2, 2021
1 parent 211574b commit a8d7de1
Show file tree
Hide file tree
Showing 14 changed files with 402 additions and 25 deletions.
25 changes: 16 additions & 9 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1348,35 +1348,41 @@ keys.
### `keyObject.export([options])`
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37081
description: Added support for `'jwk'` format.
-->

* `options`: {Object}
* Returns: {string | Buffer}
* Returns: {string | Buffer | Object}

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.
* `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 an object.

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
Expand Down Expand Up @@ -4355,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
Expand Down
14 changes: 14 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,18 @@ added: v15.0.0

Initialization of an asynchronous crypto operation failed.

<a id="ERR_CRYPTO_JWK_UNSUPPORTED_CURVE"></a>
### `ERR_CRYPTO_JWK_UNSUPPORTED_CURVE`

Key's Elliptic Curve is not registered for use in the
[JSON Web Key Elliptic Curve Registry][].

<a id="ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE"></a>
### `ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE`

Key's Asymmetric Key Type is not registered for use in the
[JSON Web Key Types Registry][].

<a id="ERR_CRYPTO_OPERATION_FAILED"></a>
### `ERR_CRYPTO_OPERATION_FAILED`
<!-- YAML
Expand Down Expand Up @@ -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
Expand Down
60 changes: 55 additions & 5 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const {
kKeyEncodingSEC1,
} = internalBinding('crypto');

const {
validateObject,
validateOneOf,
} = require('internal/validators');

const {
codes: {
ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
Expand All @@ -30,6 +35,8 @@ const {
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE,
ERR_OPERATION_FAILED,
ERR_CRYPTO_JWK_UNSUPPORTED_CURVE,
ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE,
}
} = require('internal/errors');

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -163,18 +179,44 @@ 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_CRYPTO_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_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE();
}
}
}

class PublicKeyObject extends AsymmetricKeyObject {
constructor(handle) {
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);
}
}
Expand All @@ -184,13 +226,21 @@ const [
super('private', handle);
}

export(encoding) {
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);
}
const {
format,
type,
cipher,
passphrase
} = parsePrivateKeyEncoding(encoding, this.asymmetricKeyType);
} = parsePrivateKeyEncoding(options, this.asymmetricKeyType);
return this[kHandle].export(format, type, cipher, passphrase);
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,8 @@ E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', 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);
E('ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', 'Unsupported JWK EC curve: %s.', Error);
E('ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', 'Unsupported JWK Key Type.', Error);
E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error);
E('ERR_CRYPTO_SCRYPT_INVALID_PARAMETER', 'Invalid scrypt parameter', Error);
E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
Expand Down
42 changes: 41 additions & 1 deletion test/fixtures/keys/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/keys/ec_p256_private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDxBsPQPIgMuMyQbx
zbb9toew6Ev6e9O6ZhpxLNgmAEqhRANCAARfSYxhH+6V5lIg+M3O0iQBLf+53kuE
2luIgWnp81/Ya1Gybj8tl4tJVu1GEwcTyt8hoA7vRACmCHnI5B1+bNpS
-----END PRIVATE KEY-----
4 changes: 4 additions & 0 deletions test/fixtures/keys/ec_p256_public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEX0mMYR/uleZSIPjNztIkAS3/ud5L
hNpbiIFp6fNf2GtRsm4/LZeLSVbtRhMHE8rfIaAO70QApgh5yOQdfmzaUg==
-----END PUBLIC KEY-----
6 changes: 6 additions & 0 deletions test/fixtures/keys/ec_p384_private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB3B+4e4C1OUxGftkEI
Gb/SCulzUP/iE940CB6+B6WWO4LT76T8sMWiwOAGUsuZmyKhZANiAASE43efMYmC
/7Tx90elDGBEkVnOUr4ZkMZrl/cqe8zfVy++MmayPhR46Ah3LesMCNV+J0eG15w0
IYJ8uqasuMN6drU1LNbNYfW7+hR0woajldJpvHMPv7wlnGOlzyxH1yU=
-----END PRIVATE KEY-----
5 changes: 5 additions & 0 deletions test/fixtures/keys/ec_p384_public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEhON3nzGJgv+08fdHpQxgRJFZzlK+GZDG
a5f3KnvM31cvvjJmsj4UeOgIdy3rDAjVfidHhtecNCGCfLqmrLjDena1NSzWzWH1
u/oUdMKGo5XSabxzD7+8JZxjpc8sR9cl
-----END PUBLIC KEY-----
8 changes: 8 additions & 0 deletions test/fixtures/keys/ec_p521_private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN PRIVATE KEY-----
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAEghuafcab9jXW4gO
QLeDaKOlHEiskQFjiL8klijk6i6DNOXcFfaJ9GW48kxpodw16ttAf9Z1WQstfzpK
GUetHImhgYkDgYYABAGixYI8Gbc5zNze6rH2/OmsFV3unOnY1GDqG9RTfpJZXpL9
ChF1dG8HA4zxkM+X+jMSwm4THh0Wr1Euj9dK7E7QZwHd35XsQXgH13Hjc0QR9dvJ
BWzlg+luNTY8CkaqiBdur5oFv/AjpXRimYxZDkhAEsTwXLwNohSUVMkN8IQtNI9D
aQ==
-----END PRIVATE KEY-----
6 changes: 6 additions & 0 deletions test/fixtures/keys/ec_p521_public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBosWCPBm3Oczc3uqx9vzprBVd7pzp
2NRg6hvUU36SWV6S/QoRdXRvBwOM8ZDPl/ozEsJuEx4dFq9RLo/XSuxO0GcB3d+V
7EF4B9dx43NEEfXbyQVs5YPpbjU2PApGqogXbq+aBb/wI6V0YpmMWQ5IQBLE8Fy8
DaIUlFTJDfCELTSPQ2k=
-----END PUBLIC KEY-----
5 changes: 5 additions & 0 deletions test/fixtures/keys/ec_secp256k1_private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgc34ocwTwpFa9NZZh3l88
qXyrkoYSxvC0FEsU5v1v4IOhRANCAARw7OEVKlbGFqUJtY10/Yf/JSR0LzUL1PZ1
4Ol/ErujAPgNwwGU5PSD6aTfn9NycnYB2hby9XwB2qF3+El+DV8q
-----END PRIVATE KEY-----
4 changes: 4 additions & 0 deletions test/fixtures/keys/ec_secp256k1_public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEcOzhFSpWxhalCbWNdP2H/yUkdC81C9T2
deDpfxK7owD4DcMBlOT0g+mk35/TcnJ2AdoW8vV8Adqhd/hJfg1fKg==
-----END PUBLIC KEY-----
Loading

0 comments on commit a8d7de1

Please sign in to comment.