From 70988ea2f60c37d71adaa5e9400d77c377b032b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Sun, 24 Jan 2021 21:51:26 +0100 Subject: [PATCH 1/4] crypto: add 'sm2' key type and key pair generation --- doc/api/crypto.md | 18 ++++++++++++---- lib/internal/crypto/keygen.js | 12 +++++++++++ src/crypto/crypto_ec.cc | 10 ++++++++- src/crypto/crypto_keygen.h | 10 +++++++++ src/crypto/crypto_keys.cc | 4 ++++ src/env.h | 1 + test/parallel/test-crypto-sm2.js | 36 ++++++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-crypto-sm2.js diff --git a/doc/api/crypto.md b/doc/api/crypto.md index e43f3fa5dafc43..76620ec40d8cc9 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1307,6 +1307,9 @@ API using additional attributes. * `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`. @@ -2592,7 +2599,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, @@ -2630,6 +2637,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties. * `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`. @@ -2663,7 +2673,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, diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 56f06f7a79fe2a..91a7bba17c0884 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -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, @@ -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': diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index c764124bdd0d6f..9dcce215454888 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -572,6 +572,7 @@ Maybe EcKeyGenTraits::AdditionalConfig( Environment* env = Environment::GetCurrent(args); CHECK(args[*offset]->IsString()); // curve name CHECK(args[*offset + 1]->IsInt32()); // param encoding + CHECK(args[*offset + 2]->IsInt32()); // target key type Utf8Value curve_name(env->isolate(), args[*offset]); params->params.curve_nid = GetCurveFromName(*curve_name); @@ -587,7 +588,14 @@ Maybe EcKeyGenTraits::AdditionalConfig( return Nothing(); } - *offset += 2; + params->alias_key_type = args[*offset + 2].As()->Value(); + if (params->alias_key_type != EVP_PKEY_EC && + params->alias_key_type != EVP_PKEY_SM2) { + THROW_ERR_OUT_OF_RANGE(env, "Invalid alias_key_type specified"); + return Nothing(); + } + + *offset += 3; return Just(true); } diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h index 58e3b8211d6bd7..b7e497f540c1d5 100644 --- a/src/crypto/crypto_keygen.h +++ b/src/crypto/crypto_keygen.h @@ -130,6 +130,9 @@ struct KeyPairGenTraits final { const v8::FunctionCallbackInfo& args, unsigned int* offset, AdditionalParameters* params) { + // This can be overwritten by the next call to AdditionalConfig. + params->alias_key_type = EVP_PKEY_NONE; + // Notice that offset is a pointer. Each of the AdditionalConfig, // GetPublicKeyEncodingFromJs, and GetPrivateKeyEncodingFromJs // functions will update the value of the offset as they successfully @@ -170,6 +173,11 @@ struct KeyPairGenTraits final { if (!EVP_PKEY_keygen(ctx.get(), &pkey)) return KeyGenJobStatus::FAILED; + if (params->alias_key_type != EVP_PKEY_NONE) { + if (EVP_PKEY_set_alias_type(pkey, params->alias_key_type) != 1) + return KeyGenJobStatus::FAILED; + } + params->key = ManagedEVPPKey(EVPKeyPointer(pkey)); return KeyGenJobStatus::OK; } @@ -231,6 +239,7 @@ template struct KeyPairGenConfig final : public MemoryRetainer { PublicKeyEncodingConfig public_key_encoding; PrivateKeyEncodingConfig private_key_encoding; + int alias_key_type; ManagedEVPPKey key; AlgorithmParams params; @@ -241,6 +250,7 @@ struct KeyPairGenConfig final : public MemoryRetainer { private_key_encoding( std::forward( other.private_key_encoding)), + alias_key_type(other.alias_key_type), key(std::move(other.key)), params(std::move(other.params)) {} diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index cb548d10cf45b7..655b4d9bc25945 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1126,6 +1126,8 @@ Local KeyObjectHandle::GetAsymmetricKeyType() const { return env()->crypto_ed25519_string(); case EVP_PKEY_ED448: return env()->crypto_ed448_string(); + case EVP_PKEY_SM2: + return env()->crypto_sm2_string(); case EVP_PKEY_X25519: return env()->crypto_x25519_string(); case EVP_PKEY_X448: @@ -1361,8 +1363,10 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatSPKI); NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatJWK); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_EC); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED448); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_SM2); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X448); NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1); diff --git a/src/env.h b/src/env.h index 514a3871b4ee05..57330e92519320 100644 --- a/src/env.h +++ b/src/env.h @@ -204,6 +204,7 @@ constexpr size_t kFsStatsBufferLength = V(crypto_ec_string, "ec") \ V(crypto_ed25519_string, "ed25519") \ V(crypto_ed448_string, "ed448") \ + V(crypto_sm2_string, "sm2") \ V(crypto_x25519_string, "x25519") \ V(crypto_x448_string, "x448") \ V(crypto_rsa_string, "rsa") \ diff --git a/test/parallel/test-crypto-sm2.js b/test/parallel/test-crypto-sm2.js new file mode 100644 index 00000000000000..fbb802da3d89fb --- /dev/null +++ b/test/parallel/test-crypto-sm2.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPairSync +}= require('crypto'); + + +// Generate an SM2 key pair. +const { + publicKey: sm2PublicKey, + privateKey: sm2PrivateKey +} = generateKeyPairSync('sm2'); + +// While this key pair also uses the SM2 curve, it produces ECDSA signatures. +const { + publicKey: ecdsaPublicKey, + privateKey: ecdsaPrivateKey +} = generateKeyPairSync('ec', { + namedCurve: 'SM2' +}); + + +{ + // Even though the key structures and curves are exactly the same, their key + // type should be different. + + assert.strictEqual(sm2PublicKey.asymmetricKeyType, 'sm2'); + assert.strictEqual(sm2PrivateKey.asymmetricKeyType, 'sm2'); + + assert.strictEqual(ecdsaPublicKey.asymmetricKeyType, 'ec'); + assert.strictEqual(ecdsaPrivateKey.asymmetricKeyType, 'ec'); +} From 4282617daf5ab9c3aa71b88ca40f6a7e64b05db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Sun, 24 Jan 2021 22:09:48 +0100 Subject: [PATCH 2/4] crypto: add support for SM2 one-shot signatures --- doc/api/crypto.md | 15 ++++- lib/internal/crypto/sig.js | 22 ++++++- src/crypto/crypto_sig.cc | 67 +++++++++++++++---- src/crypto/crypto_sig.h | 2 +- src/node_errors.h | 1 + test/parallel/test-crypto-sm2.js | 110 ++++++++++++++++++++++++++++++- 6 files changed, 197 insertions(+), 20 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 76620ec40d8cc9..16cdf8cda58d2a 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -3637,6 +3637,9 @@ Throws an error if FIPS mode is not available. @@ -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)` @@ -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 diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index 1b6ad0e7ea601c..8130772d0ea895 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -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); @@ -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); }; } diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 9d6f86d32b0b78..d5afb5bd8858ac 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -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 { @@ -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); + // 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( @@ -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) }; } @@ -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); } diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index 4a63852bffff62..5fe607381f9523 100644 --- a/lib/internal/crypto/sig.js +++ b/lib/internal/crypto/sig.js @@ -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); @@ -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') @@ -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 @@ -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) { @@ -193,7 +196,8 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) { data, format, type, - passphrase + passphrase, + asymmetricKeyType } = preparePublicOrPrivateKey(options, true); sigEncoding = sigEncoding || getDefaultEncoding(); @@ -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) { @@ -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 @@ -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 = { diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 655b4d9bc25945..08aab08c778029 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -686,10 +686,62 @@ PublicKeyEncodingConfig ManagedEVPPKey::GetPublicKeyEncodingFromJs( return result; } +static bool CanAssignAliasTypeForNewKey(EVP_PKEY* pkey, int requested_type) { + // Currently, the only possible conversion is EC -> SM2, and only if the EC + // curve is the (named) SM2 curve. + if (EVP_PKEY_id(pkey) == EVP_PKEY_EC && requested_type == EVP_PKEY_SM2) { + const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(pkey); + const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key); + if (EC_GROUP_get_curve_name(ec_group) == NID_sm2) { + return true; + } + } + return false; +} + +static bool AdjustAsymmetricKeyType(const FunctionCallbackInfo& args, + unsigned int* offset, EVP_PKEY* pkey, + bool is_new_key) { + Environment* env = Environment::GetCurrent(args); + + // The caller can request a specific key type, either to ensure that the + // key has the expected type, or to convert between key types with an + // indistinguishable representation during import. + Local arg = args[(*offset)++]; + if (!arg->IsUndefined()) { + CHECK(arg->IsInt32()); + int requested_type = arg.As()->Value(); + if (requested_type != EVP_PKEY_id(pkey)) { + // At this point, the caller requested a type that is not the actual key + // type. If we cannot assign the requested type as an alias type, we fail. + // If the key is not "new", meaning that we are reusing an EVP_PKEY that + // we obtained from another KeyObject, we cannot change the alias type. + // Changing the alias type would affect all other KeyObjects that + // reference the same EVP_PKEY. OpenSSL also does not provide a good way + // of cloning an EVP_PKEY, so we just disable the behavior in this case. + if (is_new_key && CanAssignAliasTypeForNewKey(pkey, requested_type)) { + if (EVP_PKEY_set_alias_type(pkey, requested_type) != 1) { + ThrowCryptoError(env, ERR_get_error(), + "Failed to apply requested asymmetric key type"); + return false; + } + } else { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY(env); + return false; + } + } + } + + return true; +} + ManagedEVPPKey ManagedEVPPKey::GetPrivateKeyFromJs( const FunctionCallbackInfo& args, unsigned int* offset, bool allow_key_object) { + ManagedEVPPKey result; + bool is_new_key; + if (args[*offset]->IsString() || IsAnyByteSource(args[*offset])) { Environment* env = Environment::GetCurrent(args); ByteSource key = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); @@ -701,21 +753,35 @@ ManagedEVPPKey ManagedEVPPKey::GetPrivateKeyFromJs( EVPKeyPointer pkey; ParseKeyResult ret = ParsePrivateKey(&pkey, config.Release(), key.get(), key.size()); - return GetParsedKey(env, std::move(pkey), ret, - "Failed to read private key"); + + result = GetParsedKey(env, std::move(pkey), ret, + "Failed to read private key"); + is_new_key = true; } else { CHECK(args[*offset]->IsObject() && allow_key_object); KeyObjectHandle* key; ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), ManagedEVPPKey()); CHECK_EQ(key->Data()->GetKeyType(), kKeyTypePrivate); (*offset) += 4; - return key->Data()->GetAsymmetricKey(); + + result = key->Data()->GetAsymmetricKey(); + is_new_key = false; + } + + if (result && + !AdjustAsymmetricKeyType(args, offset, result.get(), is_new_key)) { + return ManagedEVPPKey(); } + + return result; } ManagedEVPPKey ManagedEVPPKey::GetPublicOrPrivateKeyFromJs( const FunctionCallbackInfo& args, unsigned int* offset) { + ManagedEVPPKey result; + bool is_new_key; + if (IsAnyByteSource(args[*offset])) { Environment* env = Environment::GetCurrent(args); ArrayBufferOrViewContents data(args[(*offset)++]); @@ -765,16 +831,27 @@ ManagedEVPPKey ManagedEVPPKey::GetPublicOrPrivateKeyFromJs( } } - return ManagedEVPPKey::GetParsedKey( - env, std::move(pkey), ret, "Failed to read asymmetric key"); + result = ManagedEVPPKey::GetParsedKey(env, std::move(pkey), ret, + "Failed to read asymmetric key"); + is_new_key = true; } else { CHECK(args[*offset]->IsObject()); KeyObjectHandle* key = Unwrap(args[*offset].As()); CHECK_NOT_NULL(key); CHECK_NE(key->Data()->GetKeyType(), kKeyTypeSecret); + (*offset) += 4; - return key->Data()->GetAsymmetricKey(); + + result = key->Data()->GetAsymmetricKey(); + is_new_key = false; + } + + if (result && + !AdjustAsymmetricKeyType(args, offset, result.get(), is_new_key)) { + return ManagedEVPPKey(); } + + return result; } ManagedEVPPKey ManagedEVPPKey::GetParsedKey(Environment* env, @@ -939,7 +1016,7 @@ void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { break; } case kKeyTypePublic: { - CHECK_EQ(args.Length(), 4); + CHECK_EQ(args.Length(), 6); offset = 1; pkey = ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(args, &offset); @@ -949,7 +1026,7 @@ void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { break; } case kKeyTypePrivate: { - CHECK_EQ(args.Length(), 5); + CHECK_EQ(args.Length(), 6); offset = 1; pkey = ManagedEVPPKey::GetPrivateKeyFromJs(args, &offset, false); @@ -1363,12 +1440,17 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatSPKI); NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatJWK); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_DH); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_DSA); NODE_DEFINE_CONSTANT(target, EVP_PKEY_EC); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED448); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_RSA); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_RSA_PSS); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SM2); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X448); + NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1); NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS8); NODE_DEFINE_CONSTANT(target, kKeyEncodingSPKI); diff --git a/src/node_errors.h b/src/node_errors.h index 01cac5f6d5e697..32ab2bcd4005a1 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -33,6 +33,7 @@ void OnFatalError(const char* location, const char* message); V(ERR_BUFFER_TOO_LARGE, Error) \ V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \ V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \ + V(ERR_CRYPTO_INCOMPATIBLE_KEY, Error) \ V(ERR_CRYPTO_INITIALIZATION_FAILED, Error) \ V(ERR_CRYPTO_INVALID_AUTH_TAG, TypeError) \ V(ERR_CRYPTO_INVALID_COUNTER, TypeError) \ @@ -110,6 +111,8 @@ void OnFatalError(const char* location, const char* message); "Buffer is not available for the current Context") \ V(ERR_CONSTRUCT_CALL_INVALID, "Constructor cannot be called") \ V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \ + V(ERR_CRYPTO_INCOMPATIBLE_KEY, \ + "The requested key type does not match the actual key type") \ V(ERR_CRYPTO_INITIALIZATION_FAILED, "Initialization failed") \ V(ERR_CRYPTO_INVALID_AUTH_TAG, "Invalid authentication tag") \ V(ERR_CRYPTO_INVALID_COUNTER, "Invalid counter") \ diff --git a/test/parallel/test-crypto-sm2.js b/test/parallel/test-crypto-sm2.js index f9269f27d29019..b987bee5189891 100644 --- a/test/parallel/test-crypto-sm2.js +++ b/test/parallel/test-crypto-sm2.js @@ -5,7 +5,10 @@ if (!common.hasCrypto) const assert = require('assert'); const { + createPrivateKey, + createPublicKey, generateKeyPairSync, + getCurves, getHashes, randomBytes, sign, @@ -142,3 +145,108 @@ const { } } } + +{ + // The type of existing KeyObjects cannot be changed. + + assert.throws(() => { + createPublicKey({ key: sm2PrivateKey, asymmetricKeyType: 'ec' }); + }, { + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY', + name: 'Error' + }); +} + +{ + // Only EC keys with the SM2 curve can be imported as SM2 keys. + + const otherKeyArgs = [ + ['dh', { group: 'modp5' }], + ['dsa', { modulusLength: 1024 }], + ['ec', { namedCurve: 'P-256' }], + ['ed25519'], + ['ed448'], + ['rsa', { modulusLength: 512, divisorLength: 256 }], + ['x25519'], + ['x448'] + ]; + + for (const [type, opts] of otherKeyArgs) { + const { publicKey } = generateKeyPairSync(type, { + publicKeyEncoding: { + format: 'pem', + type: 'spki' + }, + ...opts + }); + + assert.throws(() => { + createPublicKey({ + key: publicKey, + asymmetricKeyType: 'sm2' + }) + }, { + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY', + name: 'Error' + }); + } +} + +{ + // When importing key material, it must be possible to specify the + // asymmetricKeyType since the ASN.1 key material itself does not distinguish + // between EC using the SM2 curve and SM2. + + const ecPrivateExport = ecdsaPrivateKey.export({ + format: 'pem', + type: 'pkcs8' + }); + + const ecPublicExport = ecdsaPublicKey.export({ + format: 'pem', + type: 'spki' + }); + + const ecPrivateAsSm2Private = createPrivateKey({ + key: ecPrivateExport, + asymmetricKeyType: 'sm2' + }); + + const ecPrivateAsSm2Public = createPublicKey({ + key: ecPrivateExport, + asymmetricKeyType: 'sm2' + }); + + const ecPublicAsSm2Public = createPublicKey({ + key: ecPublicExport, + asymmetricKeyType: 'sm2' + }); + + + assert.strictEqual(ecdsaPrivateKey.asymmetricKeyType, 'ec'); + assert.strictEqual(ecPrivateAsSm2Private.asymmetricKeyType, 'sm2'); + assert.strictEqual(ecPrivateAsSm2Public.asymmetricKeyType, 'sm2'); + assert.strictEqual(ecPublicAsSm2Public.asymmetricKeyType, 'sm2'); + + + const ecPrivateAsSm2PrivateExport = ecPrivateAsSm2Private.export({ + format: 'pem', + type: 'pkcs8' + }); + + const ecPrivateAsSm2PublicExport = ecPrivateAsSm2Public.export({ + format: 'pem', + type: 'spki' + }); + + const ecPublicAsSm2PublicExport = ecPublicAsSm2Public.export({ + format: 'pem', + type: 'spki' + }); + + // The exported PEM string is exactly the same for EC and SM2. + assert.strictEqual(ecPrivateAsSm2PrivateExport, ecPrivateExport); + for (const e of [ecPrivateAsSm2PublicExport, ecPublicAsSm2PublicExport]) { + assert.strictEqual(e, ecPublicExport); + } +} From a76c7e4e77a6634e829137e68e34a5c5d9c78a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Mon, 25 Jan 2021 20:12:45 +0100 Subject: [PATCH 4/4] test: ensure EC keys on the SM2 curve work --- test/parallel/test-crypto-sm2.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/parallel/test-crypto-sm2.js b/test/parallel/test-crypto-sm2.js index b987bee5189891..9e51de9546aa75 100644 --- a/test/parallel/test-crypto-sm2.js +++ b/test/parallel/test-crypto-sm2.js @@ -250,3 +250,15 @@ const { assert.strictEqual(e, ecPublicExport); } } + +{ + // EC keys on the SM2 curve should still allow normal operation. + + // TODO(tniessen): Later versions of OpenSSL allow SM3 here. Test that. + + const data = randomBytes(100); + + const signature = sign('sha256', data, ecdsaPrivateKey); + const isValid = verify('sha256', data, ecdsaPublicKey, signature); + assert.strictEqual(isValid, true); +}