Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: add KeyObject.from and keyObject.export JWK format support #36203

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,22 @@ passing keys as strings or `Buffer`s due to improved security features.
The receiver obtains a cloned `KeyObject`, and the `KeyObject` does not need to
be listed in the `transferList` argument.

### `keyObject.asymmetricKeyDetails`
<!-- YAML
added: REPLACEME
-->

* {Object}
* `modulusLength`: {number} Key size in bits (RSA, DSA).
* `publicExponent`: {number} Public exponent (RSA).
* `divisorLength`: {number} Size of `q` in bits (DSA).
* `namedCurve`: {string} Name of the curve (EC).

This property exists only on asymmetric keys. Depending on the type of the key,
this object contains information about the key. None of the information obtained
through this property can be used to uniquely identify a key or to compromise
the security of the key.

### `keyObject.asymmetricKeyType`
<!-- YAML
added: v11.6.0
Expand Down Expand Up @@ -1342,20 +1358,22 @@ format.

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

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

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

* `format`: {string} Must be `'pem'`, `'der'`, or `'jwk'`.
* `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or
`'sec1'` (EC only).
* `format`: {string} Must be `'pem'` or `'der'`.
* `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 JWK format was selected, all other options are ignored.

When PEM encoding was selected, the result will be a string, otherwise it will
be a buffer containing the data encoded as DER.

Expand Down
94 changes: 94 additions & 0 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
ObjectDefineProperty,
ObjectSetPrototypeOf,
Symbol,
Uint8Array,
} = primordials;

const {
Expand Down Expand Up @@ -36,6 +37,7 @@ const {
kHandle,
kKeyObject,
getArrayBufferOrView,
bigIntArrayToUnsignedInt,
} = require('internal/crypto/util');

const {
Expand Down Expand Up @@ -111,6 +113,27 @@ const [
throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key);
return key[kKeyObject];
}

// TODO: Can we repurpose KeyObject.from? I don't see it neither
// used or documented.
static fromJwk(jwk) {
// TODO: validate jwk?.kty && typeof jwk.kty === 'string'
if (typeof jwk?.kty !== 'string') {
throw new TypeError('TODO')
}

const handle = new KeyObjectHandle();
const type = handle.initJwk(jwk);

switch (type) {
case kKeyTypeSecret:
return new SecretKeyObject(handle)
case kKeyTypePublic:
return new PublicKeyObject(handle)
case kKeyTypePrivate:
return new PrivateKeyObject(handle)
}
}
}

class SecretKeyObject extends KeyObject {
Expand All @@ -128,12 +151,73 @@ const [
}

const kAsymmetricKeyType = Symbol('kAsymmetricKeyType');
const kAsymmetricKeyDetails = Symbol('kAsymmetricKeyDetails');

function normalizeKeyDetails(details = {}) {
if (details.publicExponent !== undefined) {
return {
...details,
publicExponent:
bigIntArrayToUnsignedInt(new Uint8Array(details.publicExponent))
};
}
return details;
}

function mapEcCrv(keyObject) {
switch (keyObject.asymmetricKeyDetails.namedCurve) {
case 'prime256v1':
return 'P-256'
case 'secp256k1':
return 'secp256k1'
case 'secp384r1':
return 'P-384'
case 'secp521r1':
return 'P-521'
default:
throw new TypeError('Unsupported JWK EC curve value');
}
}

function mapOkpCrv(keyObject) {
switch (keyObject.asymmetricKeyType) {
case 'ed25519':
return 'Ed25519'
case 'ed448':
return 'Ed448'
case 'x25519':
return 'X25519'
case 'x448':
return 'X448'
default:
throw new TypeError('Unsupported JWK OKP sub type value');
}
}

const jwkExport = Symbol('jwkExport');

class AsymmetricKeyObject extends KeyObject {
get asymmetricKeyType() {
return this[kAsymmetricKeyType] ||
(this[kAsymmetricKeyType] = this[kHandle].getAsymmetricKeyType());
}

get asymmetricKeyDetails() {
return this[kAsymmetricKeyDetails] ||
(this[kAsymmetricKeyDetails] = normalizeKeyDetails(
this[kHandle].keyDetail({})
));
}

[jwkExport]() {
const jwk = this[kHandle].exportJwk({});
if (jwk.kty === 'EC') {
jwk.crv = mapEcCrv(this)
} else if (jwk.kty === 'OKP') {
jwk.crv = mapOkpCrv(this)
}
return jwk;
}
}

class PublicKeyObject extends AsymmetricKeyObject {
Expand All @@ -146,6 +230,8 @@ const [
format,
type
} = parsePublicKeyEncoding(encoding, this.asymmetricKeyType);
if (format === 'jwk')
return this[jwkExport]();
return this[kHandle].export(format, type);
}
}
Expand All @@ -162,6 +248,8 @@ const [
cipher,
passphrase
} = parsePrivateKeyEncoding(encoding, this.asymmetricKeyType);
if (format === 'jwk')
return this[jwkExport]();
return this[kHandle].export(format, type, cipher, passphrase);
}
}
Expand All @@ -176,6 +264,8 @@ function parseKeyFormat(formatStr, defaultFormat, optionName) {
return kKeyFormatPEM;
else if (formatStr === 'der')
return kKeyFormatDER;
else if (formatStr === 'jwk')
return 'jwk';
throw new ERR_INVALID_ARG_VALUE(optionName, formatStr);
}

Expand Down Expand Up @@ -216,6 +306,10 @@ function parseKeyFormatAndType(enc, keyType, isPublic, objName) {
isInput ? kKeyFormatPEM : undefined,
option('format', objName));

if (format === 'jwk') {
return { format }
}

const type = parseKeyType(typeStr,
!isInput || format === kKeyFormatDER,
keyType,
Expand Down
6 changes: 3 additions & 3 deletions src/crypto/crypto_dsa.cc
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ std::shared_ptr<KeyObjectData> ImportJWKDsaKey(
!q_value->IsString() ||
!q_value->IsString() ||
(!x_value->IsUndefined() && !x_value->IsString())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK DSA key");
return std::shared_ptr<KeyObjectData>();
}

Expand All @@ -210,14 +210,14 @@ std::shared_ptr<KeyObjectData> ImportJWKDsaKey(
p.ToBN().release(),
q.ToBN().release(),
g.ToBN().release())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK DSA key");
return std::shared_ptr<KeyObjectData>();
}

if (type == kKeyTypePrivate) {
ByteSource x = ByteSource::FromEncodedString(env, x_value.As<String>());
if (!DSA_set0_key(dsa.get(), nullptr, x.ToBN().release())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK DSA key");
return std::shared_ptr<KeyObjectData>();
}
}
Expand Down
60 changes: 56 additions & 4 deletions src/crypto/crypto_ecdh.cc
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,58 @@ WebCryptoKeyExportStatus ECKeyExportTraits::DoExport(
}
}

// TODO: this needs a new home
Maybe<bool> ExportJWKOkpKey(
Environment* env,
std::shared_ptr<KeyObjectData> key,
Local<Object> target) {
ManagedEVPPKey pkey = key->GetAsymmetricKey();
// TODO: CHECK EVP_PKEY_id(pkey.get()) is one of
// EVP_PKEY_X448, EVP_PKEY_ED448, EVP_PKEY_X25519, EVP_PKEY_ED25519

size_t len = 0;
EVP_PKEY_get_raw_public_key(pkey.get(), nullptr, &len);

uint8_t* rawX = new uint8_t[len];
EVP_PKEY_get_raw_public_key(pkey.get(), rawX, &len);

if (target->Set(
env->context(),
env->jwk_kty_string(),
env->jwk_okp_string()).IsNothing()) {
return Nothing<bool>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaks rawX – is there any reason not to use a standard container here instead, i.e. std::vector<uint8_t> raw_x(len);? ditto below for rawD (/raw_d)

}

BignumPointer x(BN_new());
BN_bin2bn(rawX, len, x.get());

if (SetEncodedValue(
env,
target,
env->jwk_x_string(),
x.get(),
len).IsNothing()) {
return Nothing<bool>();
}

if (key->GetKeyType() == kKeyTypePrivate) {
uint8_t* rawD = new uint8_t[len];
EVP_PKEY_get_raw_private_key(pkey.get(), rawD, &len);

BignumPointer d(BN_new());
BN_bin2bn(rawD, len, d.get());

return SetEncodedValue(
env,
target,
env->jwk_d_string(),
d.get(),
len);
}

return Just(true);
}

Maybe<bool> ExportJWKEcKey(
Environment* env,
std::shared_ptr<KeyObjectData> key,
Expand Down Expand Up @@ -680,15 +732,15 @@ std::shared_ptr<KeyObjectData> ImportJWKEcKey(
if (!x_value->IsString() ||
!y_value->IsString() ||
(!d_value->IsUndefined() && !d_value->IsString())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key");
return std::shared_ptr<KeyObjectData>();
}

KeyType type = d_value->IsString() ? kKeyTypePrivate : kKeyTypePublic;

ECKeyPointer ec(EC_KEY_new_by_curve_name(nid));
if (!ec) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key");
return std::shared_ptr<KeyObjectData>();
}

Expand All @@ -699,14 +751,14 @@ std::shared_ptr<KeyObjectData> ImportJWKEcKey(
ec.get(),
x.ToBN().get(),
y.ToBN().get())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key");
return std::shared_ptr<KeyObjectData>();
}

if (type == kKeyTypePrivate) {
ByteSource d = ByteSource::FromEncodedString(env, d_value.As<String>());
if (!EC_KEY_set_private_key(ec.get(), d.ToBN().get())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key");
return std::shared_ptr<KeyObjectData>();
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/crypto/crypto_ecdh.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ struct ECKeyExportTraits final {

using ECKeyExportJob = KeyExportJob<ECKeyExportTraits>;

v8::Maybe<bool> ExportJWKOkpKey(
Environment* env,
std::shared_ptr<KeyObjectData> key,
v8::Local<v8::Object> target);

v8::Maybe<bool> ExportJWKEcKey(
Environment* env,
std::shared_ptr<KeyObjectData> key,
Expand Down
10 changes: 8 additions & 2 deletions src/crypto/crypto_keys.cc
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,13 @@ Maybe<bool> ExportJWKAsymmetricKey(
case EVP_PKEY_RSA_PSS: return ExportJWKRsaKey(env, key, target);
case EVP_PKEY_DSA: return ExportJWKDsaKey(env, key, target);
case EVP_PKEY_EC: return ExportJWKEcKey(env, key, target);
case EVP_PKEY_X448:
// Fall through
case EVP_PKEY_ED448:
// Fall through
case EVP_PKEY_X25519:
// Fall through
case EVP_PKEY_ED25519: return ExportJWKOkpKey(env, key, target);
}
THROW_ERR_CRYPTO_INVALID_KEYTYPE(env);
return Just(false);
Expand Down Expand Up @@ -543,8 +550,7 @@ Maybe<bool> GetAsymmetricKeyDetail(
case EVP_PKEY_EC: return GetEcKeyDetail(env, key, target);
case EVP_PKEY_DH: return GetDhKeyDetail(env, key, target);
}
THROW_ERR_CRYPTO_INVALID_KEYTYPE(env);
return Nothing<bool>();
return Just(false);
}
} // namespace

Expand Down
Loading