Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
crypto: allow adjusting key type on import
  • Loading branch information
tniessen committed Jan 25, 2021
commit a52008c0e9b5d57bc16759feaa78e73333b95478
18 changes: 18 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -2360,6 +2360,9 @@ input.on('readable', () => {
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: ???
description: Add `asymmetricKeyType` option.
- version: v15.0.0
pr-url: https://github.com/nodejs/node/pull/35093
description: The key can also be an ArrayBuffer. The encoding option was
Expand All @@ -2375,6 +2378,7 @@ changes:
required only if the `format` is `'der'` and ignored if it is `'pem'`.
* `passphrase`: {string | Buffer} The passphrase to use for decryption.
* `encoding`: {string} The string encoding to use when `key` is a string.
* `asymmetricKeyType` {string} The requested asymmetric key type.
Copy link
Copy Markdown
Member

@jasnell jasnell Jan 25, 2021

Choose a reason for hiding this comment

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

It would be good to document the expected values here (or link to where they are documented)

(I know there's a paragraph added below but listing the values here would be helpful)

* Returns: {KeyObject}
<!--lint enable maximum-line-length remark-lint-->

Expand All @@ -2385,10 +2389,18 @@ must be an object with the properties described above.
If the private key is encrypted, a `passphrase` must be specified. The length
of the passphrase is limited to 1024 bytes.

If the `asymmetricKeyType` is specified, Node.js will attempt to assign the
given type to the key. This can be used, for example, to distinguish between
EC keys on the SM2 curve (`'ec'`) and SM2 keys (`'sm2'`). If the given type
cannot be assigned to the key, the function fails.

### `crypto.createPublicKey(key)`
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: ???
description: Add `asymmetricKeyType` option.
- version: v15.0.0
pr-url: https://github.com/nodejs/node/pull/35093
description: The key can also be an ArrayBuffer. The encoding option was
Expand All @@ -2409,6 +2421,7 @@ changes:
* `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required
only if the `format` is `'der'`.
* `encoding` {string} The string encoding to use when `key` is a string.
* `asymmetricKeyType` {string} The requested asymmetric key type.
* Returns: {KeyObject}
<!--lint enable maximum-line-length remark-lint-->

Expand All @@ -2419,6 +2432,11 @@ otherwise, `key` must be an object with the properties described above.

If the format is `'pem'`, the `'key'` may also be an X.509 certificate.

If the `asymmetricKeyType` is specified, Node.js will attempt to assign the
given type to the key. This can be used, for example, to distinguish between
EC keys on the SM2 curve (`'ec'`) and SM2 keys (`'sm2'`). If the given type
cannot be assigned to the key, the function fails.

Because public keys can be derived from private keys, a private key may be
passed instead of a public key. In that case, this function behaves as if
[`crypto.createPrivateKey()`][] had been called, except that the type of the
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/crypto/cipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ let StringDecoder;

function rsaFunctionFor(method, defaultPadding, keyType) {
return (options, buffer) => {
const { format, type, data, passphrase } =
const { format, type, data, passphrase, asymmetricKeyType } =
keyType === 'private' ?
preparePrivateKey(options) :
preparePublicOrPrivateKey(options);
Expand All @@ -77,8 +77,8 @@ function rsaFunctionFor(method, defaultPadding, keyType) {
if (oaepLabel !== undefined)
oaepLabel = getArrayBufferOrView(oaepLabel, 'key.oaepLabel', encoding);
buffer = getArrayBufferOrView(buffer, 'buffer', encoding);
return method(data, format, type, passphrase, buffer, padding, oaepHash,
oaepLabel);
return method(data, format, type, passphrase, asymmetricKeyType, buffer,
padding, oaepHash, oaepLabel);
};
}

Expand Down
55 changes: 48 additions & 7 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ const {
kKeyEncodingPKCS8,
kKeyEncodingSPKI,
kKeyEncodingSEC1,
EVP_PKEY_DH,
EVP_PKEY_DSA,
EVP_PKEY_EC,
EVP_PKEY_ED25519,
EVP_PKEY_ED448,
EVP_PKEY_RSA,
EVP_PKEY_RSA_PSS,
EVP_PKEY_SM2,
EVP_PKEY_X25519,
EVP_PKEY_X448
} = internalBinding('crypto');

const {
Expand Down Expand Up @@ -359,13 +369,41 @@ 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, asymmetricKeyType: typeStr } = key;

let asymmetricKeyType;
if (typeStr == null)
asymmetricKeyType = undefined;
else if (typeStr === 'dh')
asymmetricKeyType = EVP_PKEY_DH;
else if (typeStr === 'dsa')
asymmetricKeyType = EVP_PKEY_DSA;
else if (typeStr === 'ec')
asymmetricKeyType = EVP_PKEY_EC;
else if (typeStr === 'ed25519')
asymmetricKeyType = EVP_PKEY_ED25519;
else if (typeStr === 'ed448')
asymmetricKeyType = EVP_PKEY_ED448;
else if (typeStr === 'rsa')
asymmetricKeyType = EVP_PKEY_RSA;
else if (typeStr === 'rsa-pss')
asymmetricKeyType = EVP_PKEY_RSA_PSS;
else if (typeStr === 'sm2')
asymmetricKeyType = EVP_PKEY_SM2;
else if (typeStr === 'x25519')
asymmetricKeyType = EVP_PKEY_X25519;
else if (typeStr === 'x448')
asymmetricKeyType = EVP_PKEY_X448;
else
throw new ERR_INVALID_ARG_VALUE('options.asymmetricKeyType', typeStr);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A switch statement would be nicer here from a code readability point of view.

Copy link
Copy Markdown

@Mifrill Mifrill Nov 17, 2023

Choose a reason for hiding this comment

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

I suggest key-map object instead:

const keyTypeMap = {
  'dh': EVP_PKEY_DH,
  'dsa': EVP_PKEY_DSA,
  'ec': EVP_PKEY_EC,
  'ed25519': EVP_PKEY_ED25519,
  'ed448': EVP_PKEY_ED448,
  'rsa': EVP_PKEY_RSA,
  'rsa-pss': EVP_PKEY_RSA_PSS,
  'sm2': EVP_PKEY_SM2,
  'x25519': EVP_PKEY_X25519,
  'x448': EVP_PKEY_X448,
  null: undefined // Map null to undefined
};
const asymmetricKeyType = keyTypeMap[typeStr];
if (asymmetricKeyType === undefined) { // if asymmetricKeyType null or not in keyTypeMap
  throw new ERR_INVALID_ARG_VALUE('options.asymmetricKeyType', typeStr);
}


// The 'key' property can be a KeyObject as well to allow specifying
// additional options such as padding along with the key.
const common = { asymmetricKeyType };
if (isKeyObject(data))
return { data: getKeyObjectHandle(data, ctx) };
return { data: getKeyObjectHandle(data, ctx), ...common };
else if (isCryptoKey(data))
return { data: getKeyObjectHandle(data[kKeyObject], ctx) };
return { data: getKeyObjectHandle(data[kKeyObject], ctx), ...common };
// Either PEM or DER using PKCS#1 or SPKI.
if (!isStringOrBuffer(data)) {
throw new ERR_INVALID_ARG_TYPE(
Expand All @@ -378,6 +416,7 @@ function prepareAsymmetricKey(key, ctx) {
(ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined;
return {
data: getArrayBufferOrView(data, 'key', encoding),
...common,
...parseKeyEncoding(key, undefined, isPublic)
};
}
Expand Down Expand Up @@ -428,17 +467,19 @@ function createSecretKey(key, encoding) {
}

function createPublicKey(key) {
const { format, type, data } = prepareAsymmetricKey(key, kCreatePublic);
const { format, type, data, passphrase, asymmetricKeyType } =
prepareAsymmetricKey(key, kCreatePublic);
const handle = new KeyObjectHandle();
handle.init(kKeyTypePublic, data, format, type);
handle.init(kKeyTypePublic, data, format, type, passphrase, asymmetricKeyType);
return new PublicKeyObject(handle);
}

function createPrivateKey(key) {
const { format, type, data, passphrase } =
const { format, type, data, passphrase, asymmetricKeyType } =
prepareAsymmetricKey(key, kCreatePrivate);
const handle = new KeyObjectHandle();
handle.init(kKeyTypePrivate, data, format, type, passphrase);
handle.init(kKeyTypePrivate, data, format, type, passphrase,
asymmetricKeyType);
return new PrivateKeyObject(handle);
}

Expand Down
33 changes: 19 additions & 14 deletions lib/internal/crypto/sig.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ Sign.prototype.sign = function sign(options, encoding) {
if (!options)
throw new ERR_CRYPTO_SIGN_KEY_REQUIRED();

const { data, format, type, passphrase } = preparePrivateKey(options, true);
const { data, format, type, passphrase, asymmetricKeyType } =
preparePrivateKey(options, true);

// Options specific to RSA
const rsaPadding = getPadding(options);
Expand All @@ -131,8 +132,9 @@ Sign.prototype.sign = function sign(options, encoding) {
// Options specific to (EC)DSA
const dsaSigEnc = getDSASignatureEncoding(options);

const ret = this[kHandle].sign(data, format, type, passphrase, rsaPadding,
pssSaltLength, dsaSigEnc);
const ret = this[kHandle].sign(data, format, type, passphrase,
asymmetricKeyType, rsaPadding, pssSaltLength,
dsaSigEnc);

encoding = encoding || getDefaultEncoding();
if (encoding && encoding !== 'buffer')
Expand All @@ -154,7 +156,8 @@ function signOneShot(algorithm, data, key) {
data: keyData,
format: keyFormat,
type: keyType,
passphrase: keyPassphrase
passphrase: keyPassphrase,
asymmetricKeyType: keyAsymmetricKeyType
} = preparePrivateKey(key);

// Options specific to RSA
Expand All @@ -167,9 +170,9 @@ function signOneShot(algorithm, data, key) {
// Option specific to SM2.
const sm2Identifier = getSm2Identifier(key);

return _signOneShot(keyData, keyFormat, keyType, keyPassphrase, data,
algorithm, rsaPadding, pssSaltLength, dsaSigEnc,
sm2Identifier);
return _signOneShot(keyData, keyFormat, keyType, keyPassphrase,
keyAsymmetricKeyType, data, algorithm, rsaPadding,
pssSaltLength, dsaSigEnc, sm2Identifier);
}

function Verify(algorithm, options) {
Expand All @@ -193,7 +196,8 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) {
data,
format,
type,
passphrase
passphrase,
asymmetricKeyType
} = preparePublicOrPrivateKey(options, true);

sigEncoding = sigEncoding || getDefaultEncoding();
Expand All @@ -207,8 +211,8 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) {

signature = getArrayBufferOrView(signature, 'signature', sigEncoding);

return this[kHandle].verify(data, format, type, passphrase, signature,
rsaPadding, pssSaltLength, dsaSigEnc);
return this[kHandle].verify(data, format, type, passphrase, asymmetricKeyType,
signature, rsaPadding, pssSaltLength, dsaSigEnc);
};

function verifyOneShot(algorithm, data, key, signature) {
Expand All @@ -229,7 +233,8 @@ function verifyOneShot(algorithm, data, key, signature) {
data: keyData,
format: keyFormat,
type: keyType,
passphrase: keyPassphrase
passphrase: keyPassphrase,
asymmetricKeyType: keyAsymmetricKeyType
} = preparePublicOrPrivateKey(key);

// Options specific to RSA
Expand All @@ -250,9 +255,9 @@ function verifyOneShot(algorithm, data, key, signature) {
);
}

return _verifyOneShot(keyData, keyFormat, keyType, keyPassphrase, signature,
data, algorithm, rsaPadding, pssSaltLength, dsaSigEnc,
sm2Identifier);
return _verifyOneShot(keyData, keyFormat, keyType, keyPassphrase,
keyAsymmetricKeyType, signature, data, algorithm,
rsaPadding, pssSaltLength, dsaSigEnc, sm2Identifier);
}

module.exports = {
Expand Down
Loading