Skip to content

Commit a8d7de1

Browse files
committed
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` PR-URL: #37081 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Tobias Nießen <[email protected]>
1 parent 211574b commit a8d7de1

14 files changed

+402
-25
lines changed

doc/api/crypto.md

+16-9
Original file line numberDiff line numberDiff line change
@@ -1348,35 +1348,41 @@ keys.
13481348
### `keyObject.export([options])`
13491349
<!-- YAML
13501350
added: v11.6.0
1351+
changes:
1352+
- version: REPLACEME
1353+
pr-url: https://github.com/nodejs/node/pull/37081
1354+
description: Added support for `'jwk'` format.
13511355
-->
13521356

13531357
* `options`: {Object}
1354-
* Returns: {string | Buffer}
1358+
* Returns: {string | Buffer | Object}
13551359

1356-
For symmetric keys, this function allocates a `Buffer` containing the key
1357-
material and ignores any options.
1360+
For symmetric keys, the following encoding options can be used:
13581361

1359-
For asymmetric keys, the `options` parameter is used to determine the export
1360-
format.
1362+
* `format`: {string} Must be `'buffer'` (default) or `'jwk'`.
13611363

13621364
For public keys, the following encoding options can be used:
13631365

13641366
* `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`.
1365-
* `format`: {string} Must be `'pem'` or `'der'`.
1367+
* `format`: {string} Must be `'pem'`, `'der'`, or `'jwk'`.
13661368

13671369
For private keys, the following encoding options can be used:
13681370

13691371
* `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or
13701372
`'sec1'` (EC only).
1371-
* `format`: {string} Must be `'pem'` or `'der'`.
1373+
* `format`: {string} Must be `'pem'`, `'der'`, or `'jwk'`.
13721374
* `cipher`: {string} If specified, the private key will be encrypted with
13731375
the given `cipher` and `passphrase` using PKCS#5 v2.0 password based
13741376
encryption.
13751377
* `passphrase`: {string | Buffer} The passphrase to use for encryption, see
13761378
`cipher`.
13771379

1378-
When PEM encoding was selected, the result will be a string, otherwise it will
1379-
be a buffer containing the data encoded as DER.
1380+
The result type depends on the selected encoding format, when PEM the
1381+
result is a string, when DER it will be a buffer containing the data
1382+
encoded as DER, when [JWK][] it will be an object.
1383+
1384+
When [JWK][] encoding format was selected, all other encoding options are
1385+
ignored.
13801386

13811387
PKCS#1, SEC1, and PKCS#8 type keys can be encrypted by using a combination of
13821388
the `cipher` and `format` options. The PKCS#8 `type` can be used with any
@@ -4355,6 +4361,7 @@ See the [list of SSL OP Flags][] for details.
43554361
[Crypto constants]: #crypto_crypto_constants_1
43564362
[HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed
43574363
[HTML5's `keygen` element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/keygen
4364+
[JWK]: https://tools.ietf.org/html/rfc7517
43584365
[NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf
43594366
[NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf
43604367
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf

doc/api/errors.md

+14
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,18 @@ added: v15.0.0
919919

920920
Initialization of an asynchronous crypto operation failed.
921921

922+
<a id="ERR_CRYPTO_JWK_UNSUPPORTED_CURVE"></a>
923+
### `ERR_CRYPTO_JWK_UNSUPPORTED_CURVE`
924+
925+
Key's Elliptic Curve is not registered for use in the
926+
[JSON Web Key Elliptic Curve Registry][].
927+
928+
<a id="ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE"></a>
929+
### `ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE`
930+
931+
Key's Asymmetric Key Type is not registered for use in the
932+
[JSON Web Key Types Registry][].
933+
922934
<a id="ERR_CRYPTO_OPERATION_FAILED"></a>
923935
### `ERR_CRYPTO_OPERATION_FAILED`
924936
<!-- YAML
@@ -2716,6 +2728,8 @@ The native call from `process.cpuUsage` could not be processed.
27162728

27172729
[ES Module]: esm.md
27182730
[ICU]: intl.md#intl_internationalization_support
2731+
[JSON Web Key Elliptic Curve Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve
2732+
[JSON Web Key Types Registry]: https://www.iana.org/assignments/jose/jose.xhtml#web-key-types
27192733
[Node.js error codes]: #nodejs-error-codes
27202734
[RFC 7230 Section 3]: https://tools.ietf.org/html/rfc7230#section-3
27212735
[Subresource Integrity specification]: https://www.w3.org/TR/SRI/#the-integrity-attribute

lib/internal/crypto/keys.js

+55-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const {
2222
kKeyEncodingSEC1,
2323
} = internalBinding('crypto');
2424

25+
const {
26+
validateObject,
27+
validateOneOf,
28+
} = require('internal/validators');
29+
2530
const {
2631
codes: {
2732
ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
@@ -30,6 +35,8 @@ const {
3035
ERR_INVALID_ARG_VALUE,
3136
ERR_OUT_OF_RANGE,
3237
ERR_OPERATION_FAILED,
38+
ERR_CRYPTO_JWK_UNSUPPORTED_CURVE,
39+
ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE,
3340
}
3441
} = require('internal/errors');
3542

@@ -124,13 +131,22 @@ const [
124131
return this[kHandle].getSymmetricKeySize();
125132
}
126133

127-
export() {
134+
export(options) {
135+
if (options !== undefined) {
136+
validateObject(options, 'options');
137+
validateOneOf(
138+
options.format, 'options.format', [undefined, 'buffer', 'jwk']);
139+
if (options.format === 'jwk') {
140+
return this[kHandle].exportJwk({});
141+
}
142+
}
128143
return this[kHandle].export();
129144
}
130145
}
131146

132147
const kAsymmetricKeyType = Symbol('kAsymmetricKeyType');
133148
const kAsymmetricKeyDetails = Symbol('kAsymmetricKeyDetails');
149+
const kAsymmetricKeyJWKProperties = Symbol('kAsymmetricKeyJWKProperties');
134150

135151
function normalizeKeyDetails(details = {}) {
136152
if (details.publicExponent !== undefined) {
@@ -163,18 +179,44 @@ const [
163179
return {};
164180
}
165181
}
182+
183+
[kAsymmetricKeyJWKProperties]() {
184+
switch (this.asymmetricKeyType) {
185+
case 'rsa': return {};
186+
case 'ec':
187+
switch (this.asymmetricKeyDetails.namedCurve) {
188+
case 'prime256v1': return { crv: 'P-256' };
189+
case 'secp256k1': return { crv: 'secp256k1' };
190+
case 'secp384r1': return { crv: 'P-384' };
191+
case 'secp521r1': return { crv: 'P-521' };
192+
default:
193+
throw new ERR_CRYPTO_JWK_UNSUPPORTED_CURVE(
194+
this.asymmetricKeyDetails.namedCurve);
195+
}
196+
case 'ed25519': return { crv: 'Ed25519' };
197+
case 'ed448': return { crv: 'Ed448' };
198+
case 'x25519': return { crv: 'X25519' };
199+
case 'x448': return { crv: 'X448' };
200+
default:
201+
throw new ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE();
202+
}
203+
}
166204
}
167205

168206
class PublicKeyObject extends AsymmetricKeyObject {
169207
constructor(handle) {
170208
super('public', handle);
171209
}
172210

173-
export(encoding) {
211+
export(options) {
212+
if (options && options.format === 'jwk') {
213+
const properties = this[kAsymmetricKeyJWKProperties]();
214+
return this[kHandle].exportJwk(properties);
215+
}
174216
const {
175217
format,
176218
type
177-
} = parsePublicKeyEncoding(encoding, this.asymmetricKeyType);
219+
} = parsePublicKeyEncoding(options, this.asymmetricKeyType);
178220
return this[kHandle].export(format, type);
179221
}
180222
}
@@ -184,13 +226,21 @@ const [
184226
super('private', handle);
185227
}
186228

187-
export(encoding) {
229+
export(options) {
230+
if (options && options.format === 'jwk') {
231+
if (options.passphrase !== undefined) {
232+
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
233+
'jwk', 'does not support encryption');
234+
}
235+
const properties = this[kAsymmetricKeyJWKProperties]();
236+
return this[kHandle].exportJwk(properties);
237+
}
188238
const {
189239
format,
190240
type,
191241
cipher,
192242
passphrase
193-
} = parsePrivateKeyEncoding(encoding, this.asymmetricKeyType);
243+
} = parsePrivateKeyEncoding(options, this.asymmetricKeyType);
194244
return this[kHandle].export(format, type, cipher, passphrase);
195245
}
196246
}

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,8 @@ E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);
839839
E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
840840
'Invalid key object type %s, expected %s.', TypeError);
841841
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
842+
E('ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', 'Unsupported JWK EC curve: %s.', Error);
843+
E('ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', 'Unsupported JWK Key Type.', Error);
842844
E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error);
843845
E('ERR_CRYPTO_SCRYPT_INVALID_PARAMETER', 'Invalid scrypt parameter', Error);
844846
E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);

test/fixtures/keys/Makefile

+41-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ all: \
7575
ed448_public.pem \
7676
x448_private.pem \
7777
x448_public.pem \
78+
ec_p256_private.pem \
79+
ec_p256_public.pem \
80+
ec_p384_private.pem \
81+
ec_p384_public.pem \
82+
ec_p521_private.pem \
83+
ec_p521_public.pem \
84+
ec_secp256k1_private.pem \
85+
ec_secp256k1_public.pem \
7886

7987
#
8088
# Create Certificate Authority: ca1
@@ -663,7 +671,7 @@ rsa_cert_foafssl_b.modulus: rsa_cert_foafssl_b.crt
663671

664672
# Have to parse out the hex exponent
665673
rsa_cert_foafssl_b.exponent: rsa_cert_foafssl_b.crt
666-
openssl x509 -in rsa_cert_foafssl_b.crt -text | grep -o 'Exponent:.*' | sed 's/\(.*(\|).*\)//g' > rsa_cert_foafssl_b.exponent
674+
openssl x509 -in rsa_cert_foafssl_b.crt -text | grep -o 'Exponent:.*' | sed 's/\(.*(\|).*\)//g' > rsa_cert_foafssl_b.exponent
667675

668676
# openssl outputs `SPKAC=[SPKAC]`. That prefix needs to be removed to work with node
669677
rsa_spkac.spkac: rsa_private.pem
@@ -733,6 +741,38 @@ x448_private.pem:
733741
x448_public.pem: x448_private.pem
734742
openssl pkey -in x448_private.pem -pubout -out x448_public.pem
735743

744+
ec_p256_private.pem:
745+
openssl ecparam -name prime256v1 -genkey -noout -out sec1_ec_p256_private.pem
746+
openssl pkcs8 -topk8 -nocrypt -in sec1_ec_p256_private.pem -out ec_p256_private.pem
747+
rm sec1_ec_p256_private.pem
748+
749+
ec_p256_public.pem: ec_p256_private.pem
750+
openssl ec -in ec_p256_private.pem -pubout -out ec_p256_public.pem
751+
752+
ec_p384_private.pem:
753+
openssl ecparam -name secp384r1 -genkey -noout -out sec1_ec_p384_private.pem
754+
openssl pkcs8 -topk8 -nocrypt -in sec1_ec_p384_private.pem -out ec_p384_private.pem
755+
rm sec1_ec_p384_private.pem
756+
757+
ec_p384_public.pem: ec_p384_private.pem
758+
openssl ec -in ec_p384_private.pem -pubout -out ec_p384_public.pem
759+
760+
ec_p521_private.pem:
761+
openssl ecparam -name secp521r1 -genkey -noout -out sec1_ec_p521_private.pem
762+
openssl pkcs8 -topk8 -nocrypt -in sec1_ec_p521_private.pem -out ec_p521_private.pem
763+
rm sec1_ec_p521_private.pem
764+
765+
ec_p521_public.pem: ec_p521_private.pem
766+
openssl ec -in ec_p521_private.pem -pubout -out ec_p521_public.pem
767+
768+
ec_secp256k1_private.pem:
769+
openssl ecparam -name secp256k1 -genkey -noout -out sec1_ec_secp256k1_private.pem
770+
openssl pkcs8 -topk8 -nocrypt -in sec1_ec_secp256k1_private.pem -out ec_secp256k1_private.pem
771+
rm sec1_ec_secp256k1_private.pem
772+
773+
ec_secp256k1_public.pem: ec_secp256k1_private.pem
774+
openssl ec -in ec_secp256k1_private.pem -pubout -out ec_secp256k1_public.pem
775+
736776
clean:
737777
rm -f *.pfx *.pem *.srl ca2-database.txt ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem
738778
@> fake-startcom-root-database.txt
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDxBsPQPIgMuMyQbx
3+
zbb9toew6Ev6e9O6ZhpxLNgmAEqhRANCAARfSYxhH+6V5lIg+M3O0iQBLf+53kuE
4+
2luIgWnp81/Ya1Gybj8tl4tJVu1GEwcTyt8hoA7vRACmCHnI5B1+bNpS
5+
-----END PRIVATE KEY-----

test/fixtures/keys/ec_p256_public.pem

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEX0mMYR/uleZSIPjNztIkAS3/ud5L
3+
hNpbiIFp6fNf2GtRsm4/LZeLSVbtRhMHE8rfIaAO70QApgh5yOQdfmzaUg==
4+
-----END PUBLIC KEY-----
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB3B+4e4C1OUxGftkEI
3+
Gb/SCulzUP/iE940CB6+B6WWO4LT76T8sMWiwOAGUsuZmyKhZANiAASE43efMYmC
4+
/7Tx90elDGBEkVnOUr4ZkMZrl/cqe8zfVy++MmayPhR46Ah3LesMCNV+J0eG15w0
5+
IYJ8uqasuMN6drU1LNbNYfW7+hR0woajldJpvHMPv7wlnGOlzyxH1yU=
6+
-----END PRIVATE KEY-----

test/fixtures/keys/ec_p384_public.pem

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEhON3nzGJgv+08fdHpQxgRJFZzlK+GZDG
3+
a5f3KnvM31cvvjJmsj4UeOgIdy3rDAjVfidHhtecNCGCfLqmrLjDena1NSzWzWH1
4+
u/oUdMKGo5XSabxzD7+8JZxjpc8sR9cl
5+
-----END PUBLIC KEY-----
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAEghuafcab9jXW4gO
3+
QLeDaKOlHEiskQFjiL8klijk6i6DNOXcFfaJ9GW48kxpodw16ttAf9Z1WQstfzpK
4+
GUetHImhgYkDgYYABAGixYI8Gbc5zNze6rH2/OmsFV3unOnY1GDqG9RTfpJZXpL9
5+
ChF1dG8HA4zxkM+X+jMSwm4THh0Wr1Euj9dK7E7QZwHd35XsQXgH13Hjc0QR9dvJ
6+
BWzlg+luNTY8CkaqiBdur5oFv/AjpXRimYxZDkhAEsTwXLwNohSUVMkN8IQtNI9D
7+
aQ==
8+
-----END PRIVATE KEY-----

test/fixtures/keys/ec_p521_public.pem

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBosWCPBm3Oczc3uqx9vzprBVd7pzp
3+
2NRg6hvUU36SWV6S/QoRdXRvBwOM8ZDPl/ozEsJuEx4dFq9RLo/XSuxO0GcB3d+V
4+
7EF4B9dx43NEEfXbyQVs5YPpbjU2PApGqogXbq+aBb/wI6V0YpmMWQ5IQBLE8Fy8
5+
DaIUlFTJDfCELTSPQ2k=
6+
-----END PUBLIC KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgc34ocwTwpFa9NZZh3l88
3+
qXyrkoYSxvC0FEsU5v1v4IOhRANCAARw7OEVKlbGFqUJtY10/Yf/JSR0LzUL1PZ1
4+
4Ol/ErujAPgNwwGU5PSD6aTfn9NycnYB2hby9XwB2qF3+El+DV8q
5+
-----END PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEcOzhFSpWxhalCbWNdP2H/yUkdC81C9T2
3+
deDpfxK7owD4DcMBlOT0g+mk35/TcnJ2AdoW8vV8Adqhd/hJfg1fKg==
4+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)