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 support for SM2 #37066

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 45 additions & 6 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,9 @@ API using additional attributes.
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: ???
description: Added support for `'sm2'`.
- version:
- v13.9.0
- v12.17.0
Expand Down Expand Up @@ -1336,6 +1339,7 @@ types are:
* `'rsa-pss'` (OID 1.2.840.113549.1.1.10)
* `'dsa'` (OID 1.2.840.10040.4.1)
* `'ec'` (OID 1.2.840.10045.2.1)
* `'sm2'` (OID 1.2.840.10045.2.1)
* `'x25519'` (OID 1.3.101.110)
* `'x448'` (OID 1.3.101.111)
* `'ed25519'` (OID 1.3.101.112)
Expand Down Expand Up @@ -2356,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 @@ -2371,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
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 @@ -2381,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 @@ -2405,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 @@ -2415,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 Expand Up @@ -2555,6 +2577,9 @@ console.log(key.export().toString('hex')); // e89..........41e
<!-- YAML
added: v10.12.0
changes:
- version: REPLACEME
pr-url: ???
description: Add support for SM2.
- version:
- v13.9.0
- v12.17.0
Expand All @@ -2573,7 +2598,7 @@ changes:
-->

* `type`: {string} Must be `'rsa'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`,
`'x25519'`, `'x448'`, or `'dh'`.
`'x25519'`, `'x448'`, `'dh'`, or `'sm2'`.
* `options`: {Object}
* `modulusLength`: {number} Key size in bits (RSA, DSA).
* `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
Expand All @@ -2592,7 +2617,7 @@ changes:
* `privateKey`: {string | Buffer | KeyObject}

Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519,
Ed448, X25519, X448, and DH are currently supported.
Ed448, X25519, X448, DH, and SM2 are currently supported.

If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function
behaves as if [`keyObject.export()`][] had been called on its result. Otherwise,
Expand Down Expand Up @@ -2630,6 +2655,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties.
<!-- YAML
added: v10.12.0
changes:
- version: REPLACEME
pr-url: ???
description: Add support for SM2.
- version:
- v13.9.0
- v12.17.0
Expand All @@ -2645,7 +2673,7 @@ changes:
-->

* `type`: {string} Must be `'rsa'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`,
`'x25519'`, `'x448'`, or `'dh'`.
`'x25519'`, `'x448'`, `'dh'`, or `'sm2'`.
* `options`: {Object}
* `modulusLength`: {number} Key size in bits (RSA, DSA).
* `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
Expand All @@ -2663,7 +2691,7 @@ changes:
* `privateKey`: {string | Buffer | KeyObject}

Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519,
Ed448, X25519, X448, and DH are currently supported.
Ed448, X25519, X448, DH, and SM2 are currently supported.

If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function
behaves as if [`keyObject.export()`][] had been called on its result. Otherwise,
Expand Down Expand Up @@ -3627,6 +3655,9 @@ Throws an error if FIPS mode is not available.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: ???
description: Add support for SM2.
- version:
- v13.2.0
- v12.16.0
Expand All @@ -3643,7 +3674,7 @@ changes:

Calculates and returns the signature for `data` using the given private key and
algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is
dependent upon the key type (especially Ed25519 and Ed448).
dependent upon the key type (especially Ed25519, Ed448, and SM2).

If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
passed to [`crypto.createPrivateKey()`][]. If it is an object, the following
Expand All @@ -3664,6 +3695,9 @@ additional properties can be passed:
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.
* `sm2Identifier` {ArrayBuffer|Buffer|TypedArray|DataView} For SM2, this option
specifies the SM2 identifier. The same identifier must be specified during
verification.

### `crypto.timingSafeEqual(a, b)`
<!-- YAML
Expand Down Expand Up @@ -3699,6 +3733,9 @@ not introduce timing vulnerabilities.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: ???
description: Add support for SM2.
- version: v15.0.0
pr-url: https://github.com/nodejs/node/pull/35093
description: The data, key, and signature arguments can also be ArrayBuffer.
Expand All @@ -3719,7 +3756,7 @@ changes:

Verifies the given signature for `data` using the given key and algorithm. If
`algorithm` is `null` or `undefined`, then the algorithm is dependent upon the
key type (especially Ed25519 and Ed448).
key type (especially Ed25519, Ed448, and SM2).

If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
passed to [`crypto.createPublicKey()`][]. If it is an object, the following
Expand All @@ -3740,6 +3777,8 @@ additional properties can be passed:
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.
* `sm2Identifier` {ArrayBuffer|Buffer|TypedArray|DataView} For SM2, this option
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer to make this a more generic name, e.g. just identifier perhaps.

specifies the SM2 identifier.

The `signature` argument is the previously calculated signature for the `data`.

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
12 changes: 12 additions & 0 deletions lib/internal/crypto/keygen.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ const {
kCryptoJobSync,
kKeyVariantRSA_PSS,
kKeyVariantRSA_SSA_PKCS1_V1_5,
EVP_PKEY_EC,
EVP_PKEY_ED25519,
EVP_PKEY_ED448,
EVP_PKEY_SM2,
EVP_PKEY_X25519,
EVP_PKEY_X448,
OPENSSL_EC_NAMED_CURVE,
Expand Down Expand Up @@ -244,6 +246,16 @@ function createJob(mode, type, options) {
mode,
namedCurve,
paramEncoding,
EVP_PKEY_EC,
...encoding);
}
case 'sm2':
{
return new EcKeyPairGenJob(
mode,
'SM2',
OPENSSL_EC_NAMED_CURVE,
EVP_PKEY_SM2,
...encoding);
}
case 'ed25519':
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
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

@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