diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 7d65fc78cf2999..d924c8643cf050 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1232,6 +1232,9 @@ passing keys as strings or `Buffer`s due to improved security features. + +* `options`: {Object} + * `privateKey`: {KeyObject} + * `publicKey`: {KeyObject} +* Returns: {Buffer} + +Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`. +Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'` +(for Diffie-Hellman), `'ec'` (for ECDH), `'x448'`, or `'x25519'` (for ECDH-ES). + ### `crypto.generateKeyPair(type, options, callback)` * `type`: {string} Must be `'rsa'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`, - `'x25519'`, or `'x448'`. + `'x25519'`, `'x448'`, or `'dh'`. * `options`: {Object} * `modulusLength`: {number} Key size in bits (RSA, DSA). * `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`. * `divisorLength`: {number} Size of `q` in bits (DSA). * `namedCurve`: {string} Name of the curve to use (EC). + * `prime`: {Buffer} The prime parameter (DH). + * `primeLength`: {number} Prime length in bits (DH). + * `generator`: {number} Custom generator (DH). **Default:** `2`. + * `groupName`: {string} Diffie-Hellman group name (DH). See + [`crypto.getDiffieHellman()`][]. * `publicKeyEncoding`: {Object} See [`keyObject.export()`][]. * `privateKeyEncoding`: {Object} See [`keyObject.export()`][]. * `callback`: {Function} @@ -2115,8 +2141,8 @@ changes: * `publicKey`: {string | Buffer | KeyObject} * `privateKey`: {string | Buffer | KeyObject} -Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519 -and Ed448 are currently supported. +Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519, +Ed448, X25519, X448, and DH are currently supported. If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function behaves as if [`keyObject.export()`][] had been called on its result. Otherwise, @@ -2154,6 +2180,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties. -* `type`: {string} Must be `'rsa'`, `'dsa'`, `'ec'`, `'ed25519'`, or `'ed448'`. +* `type`: {string} Must be `'rsa'`, `'dsa'`, `'ec'`, `'ed25519'`, `'ed448'`, + `'x25519'`, `'x448'`, or `'dh'`. * `options`: {Object} * `modulusLength`: {number} Key size in bits (RSA, DSA). * `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`. * `divisorLength`: {number} Size of `q` in bits (DSA). * `namedCurve`: {string} Name of the curve to use (EC). + * `prime`: {Buffer} The prime parameter (DH). + * `primeLength`: {number} Prime length in bits (DH). + * `generator`: {number} Custom generator (DH). **Default:** `2`. + * `groupName`: {string} Diffie-Hellman group name (DH). See + [`crypto.getDiffieHellman()`][]. * `publicKeyEncoding`: {Object} See [`keyObject.export()`][]. * `privateKeyEncoding`: {Object} See [`keyObject.export()`][]. * Returns: {Object} * `publicKey`: {string | Buffer | KeyObject} * `privateKey`: {string | Buffer | KeyObject} -Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519 -and Ed448 are currently supported. +Generates a new asymmetric key pair of the given `type`. RSA, DSA, EC, Ed25519, +Ed448, X25519, X448, and DH 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/doc/api/errors.md b/doc/api/errors.md index 63d2e1c20d5346..524daa21c515e0 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -776,6 +776,11 @@ be called no more than one time per instance of a `Hash` object. [`hash.update()`][] failed for any reason. This should rarely, if ever, happen. + +### `ERR_CRYPTO_INCOMPATIBLE_KEY` + +The given crypto keys are incompatible with the attempted operation. + ### `ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS` @@ -826,6 +831,12 @@ A signing `key` was not provided to the [`sign.sign()`][] method. [`crypto.timingSafeEqual()`][] was called with `Buffer`, `TypedArray`, or `DataView` arguments of different lengths. + +### `ERR_CRYPTO_UNKNOWN_DH_GROUP` + +An unknown Diffie-Hellman group name was given. See +[`crypto.getDiffieHellman()`][] for a list of valid group names. + ### `ERR_DIR_CLOSED` @@ -1514,6 +1525,12 @@ strict compliance with the API specification (which in some cases may accept An [ES Module][] loader hook specified `format: 'dynamic'` but did not provide a `dynamicInstantiate` hook. + +### `ERR_MISSING_OPTION` + +For APIs that accept options objects, some options might be mandatory. This code +is thrown if a required option is missing. + ### `ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST` @@ -2423,6 +2440,7 @@ such as `process.stdout.on('data')`. [`Writable`]: stream.html#stream_class_stream_writable [`child_process`]: child_process.html [`cipher.getAuthTag()`]: crypto.html#crypto_cipher_getauthtag +[`crypto.getDiffieHellman()`]: crypto.html#crypto_crypto_getdiffiehellman_groupname [`crypto.scrypt()`]: crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback [`crypto.scryptSync()`]: crypto.html#crypto_crypto_scryptsync_password_salt_keylen_options [`crypto.timingSafeEqual()`]: crypto.html#crypto_crypto_timingsafeequal_a_b diff --git a/lib/crypto.js b/lib/crypto.js index e2d1875d8d0c8b..cec0b2c094fa70 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -70,7 +70,8 @@ const { const { DiffieHellman, DiffieHellmanGroup, - ECDH + ECDH, + diffieHellman } = require('internal/crypto/diffiehellman'); const { Cipher, @@ -163,6 +164,7 @@ module.exports = { createSecretKey, createSign, createVerify, + diffieHellman, getCiphers, getCurves, getDiffieHellman: createDiffieHellmanGroup, diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index da8f87bf16d4f6..ae6b68b73b5767 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -2,16 +2,21 @@ const { ObjectDefineProperty, + Set } = primordials; const { Buffer } = require('buffer'); const { ERR_CRYPTO_ECDH_INVALID_FORMAT, ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY, - ERR_INVALID_ARG_TYPE + ERR_CRYPTO_INCOMPATIBLE_KEY, + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_OPT_VALUE } = require('internal/errors').codes; const { validateString } = require('internal/validators'); const { isArrayBufferView } = require('internal/util/types'); +const { KeyObject } = require('internal/crypto/keys'); const { getDefaultEncoding, kHandle, @@ -21,7 +26,8 @@ const { DiffieHellman: _DiffieHellman, DiffieHellmanGroup: _DiffieHellmanGroup, ECDH: _ECDH, - ECDHConvertKey: _ECDHConvertKey + ECDHConvertKey: _ECDHConvertKey, + statelessDH } = internalBinding('crypto'); const { POINT_CONVERSION_COMPRESSED, @@ -232,8 +238,40 @@ function getFormat(format) { return POINT_CONVERSION_UNCOMPRESSED; } +const dhEnabledKeyTypes = new Set(['dh', 'ec', 'x448', 'x25519']); + +function diffieHellman(options) { + if (typeof options !== 'object') + throw new ERR_INVALID_ARG_TYPE('options', 'object', options); + + const { privateKey, publicKey } = options; + if (!(privateKey instanceof KeyObject)) + throw new ERR_INVALID_OPT_VALUE('privateKey', privateKey); + + if (!(publicKey instanceof KeyObject)) + throw new ERR_INVALID_OPT_VALUE('publicKey', publicKey); + + if (privateKey.type !== 'private') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, 'private'); + + if (publicKey.type !== 'public' && publicKey.type !== 'private') { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(publicKey.type, + 'private or public'); + } + + const privateType = privateKey.asymmetricKeyType; + const publicType = publicKey.asymmetricKeyType; + if (privateType !== publicType || !dhEnabledKeyTypes.has(privateType)) { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY('key types for Diffie-Hellman', + `${privateType} and ${publicType}`); + } + + return statelessDH(privateKey[kHandle], publicKey[kHandle]); +} + module.exports = { DiffieHellman, DiffieHellmanGroup, - ECDH + ECDH, + diffieHellman }; diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 88d2822fa6fad0..ced1a0608fa4aa 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -11,6 +11,7 @@ const { generateKeyPairDSA, generateKeyPairEC, generateKeyPairNid, + generateKeyPairDH, EVP_PKEY_ED25519, EVP_PKEY_ED448, EVP_PKEY_X25519, @@ -28,10 +29,12 @@ const { const { customPromisifyArgs } = require('internal/util'); const { isUint32, validateString } = require('internal/validators'); const { + ERR_INCOMPATIBLE_OPTION_PAIR, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_CALLBACK, - ERR_INVALID_OPT_VALUE + ERR_INVALID_OPT_VALUE, + ERR_MISSING_OPTION } = require('internal/errors').codes; const { isArrayBufferView } = require('internal/util/types'); @@ -245,6 +248,49 @@ function check(type, options, callback) { cipher, passphrase, wrap); } break; + case 'dh': + { + const { group, primeLength, prime, generator } = needOptions(); + let args; + if (group != null) { + if (prime != null) + throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'prime'); + if (primeLength != null) + throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'primeLength'); + if (generator != null) + throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'generator'); + if (typeof group !== 'string') + throw new ERR_INVALID_OPT_VALUE('group', group); + args = [group]; + } else { + if (prime != null) { + if (primeLength != null) + throw new ERR_INCOMPATIBLE_OPTION_PAIR('prime', 'primeLength'); + if (!isArrayBufferView(prime)) + throw new ERR_INVALID_OPT_VALUE('prime', prime); + } else if (primeLength != null) { + if (!isUint32(primeLength)) + throw new ERR_INVALID_OPT_VALUE('primeLength', primeLength); + } else { + throw new ERR_MISSING_OPTION( + 'At least one of the group, prime, or primeLength options'); + } + + if (generator != null) { + if (!isUint32(generator)) + throw new ERR_INVALID_OPT_VALUE('generator', generator); + } + + args = [prime != null ? prime : primeLength, + generator == null ? 2 : generator]; + } + + impl = (wrap) => generateKeyPairDH(...args, + publicFormat, publicType, + privateFormat, privateType, + cipher, passphrase, wrap); + } + break; default: throw new ERR_INVALID_ARG_VALUE('type', type, 'must be a supported key type'); diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 206724eacb6f52..edd286a20afee4 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -767,6 +767,7 @@ E('ERR_CRYPTO_FIPS_UNAVAILABLE', 'Cannot set FIPS mode in a non-FIPS build.', Error); E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error); E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error); +E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error); E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.', Error); E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError); @@ -1187,6 +1188,7 @@ E('ERR_MISSING_ARGS', E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', 'The ES Module loader may not return a format of \'dynamic\' when no ' + 'dynamicInstantiate function was provided', Error); +E('ERR_MISSING_OPTION', '%s is required', TypeError); E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); E('ERR_NAPI_INVALID_DATAVIEW_ARGS', diff --git a/src/env.h b/src/env.h index 558ef5cbde8cbf..fcba4dcde67f53 100644 --- a/src/env.h +++ b/src/env.h @@ -192,6 +192,7 @@ constexpr size_t kFsStatsBufferLength = V(commonjs_string, "commonjs") \ V(config_string, "config") \ V(constants_string, "constants") \ + V(crypto_dh_string, "dh") \ V(crypto_dsa_string, "dsa") \ V(crypto_ec_string, "ec") \ V(crypto_ed25519_string, "ed25519") \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 9165e9171e6e6f..f03c1e31b994f8 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -3900,6 +3900,8 @@ Local KeyObject::GetAsymmetricKeyType() const { return env()->crypto_rsa_pss_string(); case EVP_PKEY_DSA: return env()->crypto_dsa_string(); + case EVP_PKEY_DH: + return env()->crypto_dh_string(); case EVP_PKEY_EC: return env()->crypto_ec_string(); case EVP_PKEY_ED25519: @@ -5682,6 +5684,13 @@ bool DiffieHellman::Init(const char* p, int p_len, const char* g, int g_len) { return VerifyContext(); } +inline const modp_group* FindDiffieHellmanGroup(const char* name) { + for (const modp_group& group : modp_groups) { + if (StringEqualNoCase(name, group.name)) + return &group; + } + return nullptr; +} void DiffieHellman::DiffieHellmanGroup( const FunctionCallbackInfo& args) { @@ -5697,22 +5706,15 @@ void DiffieHellman::DiffieHellmanGroup( bool initialized = false; const node::Utf8Value group_name(env->isolate(), args[0]); - for (size_t i = 0; i < arraysize(modp_groups); ++i) { - const modp_group* it = modp_groups + i; - - if (!StringEqualNoCase(*group_name, it->name)) - continue; - - initialized = diffieHellman->Init(it->prime, - it->prime_size, - it->gen, - it->gen_size); - if (!initialized) - env->ThrowError("Initialization failed"); - return; - } + const modp_group* group = FindDiffieHellmanGroup(*group_name); + if (group == nullptr) + return env->ThrowError("Unknown group"); - env->ThrowError("Unknown group"); + initialized = diffieHellman->Init(group->prime, + group->prime_size, + group->gen); + if (!initialized) + env->ThrowError("Initialization failed"); } @@ -5825,6 +5827,20 @@ void DiffieHellman::GetPrivateKey(const FunctionCallbackInfo& args) { }, "No private key - did you forget to generate one?"); } +static void ZeroPadDiffieHellmanSecret(size_t remainder_size, + AllocatedBuffer* ret) { + // DH_size returns number of bytes in a prime number. + // DH_compute_key returns number of bytes in a remainder of exponent, which + // may have less bytes than a prime number. Therefore add 0-padding to the + // allocated buffer. + const size_t prime_size = ret->size(); + if (remainder_size != prime_size) { + CHECK_LT(remainder_size, prime_size); + const size_t padding = prime_size - remainder_size; + memmove(ret->data() + padding, ret->data(), remainder_size); + memset(ret->data(), 0, padding); + } +} void DiffieHellman::ComputeSecret(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -5875,16 +5891,7 @@ void DiffieHellman::ComputeSecret(const FunctionCallbackInfo& args) { } CHECK_GE(size, 0); - - // DH_size returns number of bytes in a prime number - // DH_compute_key returns number of bytes in a remainder of exponent, which - // may have less bytes than a prime number. Therefore add 0-padding to the - // allocated buffer. - if (static_cast(size) != ret.size()) { - CHECK_GT(ret.size(), static_cast(size)); - memmove(ret.data() + ret.size() - size, ret.data(), size); - memset(ret.data(), 0, ret.size() - size); - } + ZeroPadDiffieHellmanSecret(static_cast(size), &ret); args.GetReturnValue().Set(ret.ToBuffer().ToLocalChecked()); } @@ -6623,6 +6630,71 @@ class NidKeyPairGenerationConfig : public KeyPairGenerationConfig { const int id_; }; +// TODO(tniessen): Use std::variant instead. +// Diffie-Hellman can either generate keys using a fixed prime, or by first +// generating a random prime of a given size (in bits). Only one of both options +// may be specified. +struct PrimeInfo { + BignumPointer fixed_value_; + unsigned int prime_size_; +}; + +class DHKeyPairGenerationConfig : public KeyPairGenerationConfig { + public: + explicit DHKeyPairGenerationConfig(PrimeInfo&& prime_info, + unsigned int generator) + : prime_info_(std::move(prime_info)), + generator_(generator) {} + + EVPKeyCtxPointer Setup() override { + EVPKeyPointer params; + if (prime_info_.fixed_value_) { + DHPointer dh(DH_new()); + if (!dh) + return nullptr; + + BIGNUM* prime = prime_info_.fixed_value_.get(); + BignumPointer bn_g(BN_new()); + if (!BN_set_word(bn_g.get(), generator_) || + !DH_set0_pqg(dh.get(), prime, nullptr, bn_g.get())) + return nullptr; + + prime_info_.fixed_value_.release(); + bn_g.release(); + + params = EVPKeyPointer(EVP_PKEY_new()); + CHECK(params); + EVP_PKEY_assign_DH(params.get(), dh.release()); + } else { + EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DH, nullptr)); + if (!param_ctx) + return nullptr; + + if (EVP_PKEY_paramgen_init(param_ctx.get()) <= 0) + return nullptr; + + if (EVP_PKEY_CTX_set_dh_paramgen_prime_len(param_ctx.get(), + prime_info_.prime_size_) <= 0) + return nullptr; + + if (EVP_PKEY_CTX_set_dh_paramgen_generator(param_ctx.get(), + generator_) <= 0) + return nullptr; + + EVP_PKEY* raw_params = nullptr; + if (EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) + return nullptr; + params = EVPKeyPointer(raw_params); + } + + return EVPKeyCtxPointer(EVP_PKEY_CTX_new(params.get(), nullptr)); + } + + private: + PrimeInfo prime_info_; + unsigned int generator_; +}; + class GenerateKeyPairJob : public CryptoJob { public: GenerateKeyPairJob(Environment* env, @@ -6842,6 +6914,39 @@ void GenerateKeyPairNid(const FunctionCallbackInfo& args) { GenerateKeyPair(args, 1, std::move(config)); } +void GenerateKeyPairDH(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + PrimeInfo prime_info = {}; + unsigned int generator; + if (args[0]->IsString()) { + String::Utf8Value group_name(args.GetIsolate(), args[0].As()); + const modp_group* group = FindDiffieHellmanGroup(*group_name); + if (group == nullptr) + return THROW_ERR_CRYPTO_UNKNOWN_DH_GROUP(env); + + prime_info.fixed_value_ = BignumPointer( + BN_bin2bn(reinterpret_cast(group->prime), + group->prime_size, nullptr)); + generator = group->gen; + } else { + if (args[0]->IsInt32()) { + prime_info.prime_size_ = args[0].As()->Value(); + } else { + ArrayBufferViewContents input(args[0]); + prime_info.fixed_value_ = BignumPointer( + BN_bin2bn(input.data(), input.length(), nullptr)); + } + + CHECK(args[1]->IsInt32()); + generator = args[1].As()->Value(); + } + + std::unique_ptr config( + new DHKeyPairGenerationConfig(std::move(prime_info), generator)); + GenerateKeyPair(args, 2, std::move(config)); +} + void GetSSLCiphers(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -7097,6 +7202,49 @@ void ConvertKey(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(buf); } +AllocatedBuffer StatelessDiffieHellman(Environment* env, ManagedEVPPKey our_key, + ManagedEVPPKey their_key) { + size_t out_size; + + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr)); + if (!ctx || + EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) <= 0 || + EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0) + return AllocatedBuffer(); + + AllocatedBuffer result = env->AllocateManaged(out_size); + CHECK_NOT_NULL(result.data()); + + unsigned char* data = reinterpret_cast(result.data()); + if (EVP_PKEY_derive(ctx.get(), data, &out_size) <= 0) + return AllocatedBuffer(); + + ZeroPadDiffieHellmanSecret(out_size, &result); + return result; +} + +void StatelessDiffieHellman(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args[0]->IsObject() && args[1]->IsObject()); + KeyObject* our_key_object; + ASSIGN_OR_RETURN_UNWRAP(&our_key_object, args[0].As()); + CHECK_EQ(our_key_object->GetKeyType(), kKeyTypePrivate); + KeyObject* their_key_object; + ASSIGN_OR_RETURN_UNWRAP(&their_key_object, args[1].As()); + CHECK_NE(their_key_object->GetKeyType(), kKeyTypeSecret); + + ManagedEVPPKey our_key = our_key_object->GetAsymmetricKey(); + ManagedEVPPKey their_key = their_key_object->GetAsymmetricKey(); + + AllocatedBuffer out = StatelessDiffieHellman(env, our_key, their_key); + if (out.size() == 0) + return ThrowCryptoError(env, ERR_get_error(), "diffieHellman failed"); + + args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); +} + void TimingSafeEqual(const FunctionCallbackInfo& args) { ArrayBufferViewContents buf1(args[0]); @@ -7248,6 +7396,7 @@ void Initialize(Local target, env->SetMethod(target, "generateKeyPairDSA", GenerateKeyPairDSA); env->SetMethod(target, "generateKeyPairEC", GenerateKeyPairEC); env->SetMethod(target, "generateKeyPairNid", GenerateKeyPairNid); + env->SetMethod(target, "generateKeyPairDH", GenerateKeyPairDH); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED448); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); @@ -7265,6 +7414,7 @@ void Initialize(Local target, NODE_DEFINE_CONSTANT(target, kKeyTypePrivate); NODE_DEFINE_CONSTANT(target, kSigEncDER); NODE_DEFINE_CONSTANT(target, kSigEncP1363); + env->SetMethodNoSideEffect(target, "statelessDH", StatelessDiffieHellman); env->SetMethod(target, "randomBytes", RandomBytes); env->SetMethod(target, "signOneShot", SignOneShot); env->SetMethod(target, "verifyOneShot", VerifyOneShot); diff --git a/src/node_crypto_groups.h b/src/node_crypto_groups.h index d22fdc7f966f9a..108edda01bb4eb 100644 --- a/src/node_crypto_groups.h +++ b/src/node_crypto_groups.h @@ -32,7 +32,7 @@ */ -static const unsigned char two_generator[] = { 2 }; +static const unsigned int two_generator = 2; static const unsigned char group_modp1[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, @@ -394,20 +394,19 @@ typedef struct { const char* name; const char* prime; unsigned int prime_size; - const char* gen; - unsigned int gen_size; + unsigned int gen; } modp_group; static const modp_group modp_groups[] = { #define V(var) reinterpret_cast(var) - { "modp1", V(group_modp1), sizeof(group_modp1), V(two_generator), 1 }, - { "modp2", V(group_modp2), sizeof(group_modp2), V(two_generator), 1 }, - { "modp5", V(group_modp5), sizeof(group_modp5), V(two_generator), 1 }, - { "modp14", V(group_modp14), sizeof(group_modp14), V(two_generator), 1 }, - { "modp15", V(group_modp15), sizeof(group_modp15), V(two_generator), 1 }, - { "modp16", V(group_modp16), sizeof(group_modp16), V(two_generator), 1 }, - { "modp17", V(group_modp17), sizeof(group_modp17), V(two_generator), 1 }, - { "modp18", V(group_modp18), sizeof(group_modp18), V(two_generator), 1 } + { "modp1", V(group_modp1), sizeof(group_modp1), two_generator }, + { "modp2", V(group_modp2), sizeof(group_modp2), two_generator }, + { "modp5", V(group_modp5), sizeof(group_modp5), two_generator }, + { "modp14", V(group_modp14), sizeof(group_modp14), two_generator }, + { "modp15", V(group_modp15), sizeof(group_modp15), two_generator }, + { "modp16", V(group_modp16), sizeof(group_modp16), two_generator }, + { "modp17", V(group_modp17), sizeof(group_modp17), two_generator }, + { "modp18", V(group_modp18), sizeof(group_modp18), two_generator } #undef V }; diff --git a/src/node_errors.h b/src/node_errors.h index e554d5fd9250a3..96ea94cb563f07 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -39,6 +39,7 @@ void PrintErrorString(const char* format, ...); V(ERR_BUFFER_TOO_LARGE, Error) \ V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \ V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \ + V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \ V(ERR_INVALID_ARG_TYPE, TypeError) \ @@ -89,6 +90,7 @@ void PrintErrorString(const char* format, ...); "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_UNKNOWN_DH_GROUP, "Unknown DH group") \ V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \ V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \ V(ERR_OSSL_EVP_INVALID_DIGEST, "Invalid digest used") \ diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status index 846277b3e96edc..90914610ce8221 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status @@ -33,6 +33,9 @@ test-worker-message-port-transfer-terminate: PASS,FLAKY [$arch==arm || $arch==arm64] # https://github.com/nodejs/node/issues/26610 test-async-hooks-http-parser-destroy: PASS,FLAKY +# https://github.com/nodejs/node/pull/31178 +test-crypto-dh-stateless: SKIP +test-crypto-keygen: SKIP [$system==solaris] # Also applies to SmartOS diff --git a/test/parallel/test-crypto-dh-stateless.js b/test/parallel/test-crypto-dh-stateless.js new file mode 100644 index 00000000000000..f00ee997cfcfdf --- /dev/null +++ b/test/parallel/test-crypto-dh-stateless.js @@ -0,0 +1,222 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +assert.throws(() => crypto.diffieHellman(), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. Received undefined' +}); + +function test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, + { publicKey: bobPublicKey, privateKey: bobPrivateKey }, + expectedValue) { + const buf1 = crypto.diffieHellman({ + privateKey: alicePrivateKey, + publicKey: bobPublicKey + }); + const buf2 = crypto.diffieHellman({ + privateKey: bobPrivateKey, + publicKey: alicePublicKey + }); + assert.deepStrictEqual(buf1, buf2); + + if (expectedValue !== undefined) + assert.deepStrictEqual(buf1, expectedValue); +} + +const alicePrivateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + + '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + + 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwEh82IAVnYNf0Kjb\n' + + 'qYSImDFyg9sH6CJ0GzRK05e6hM3dOSClFYi4kbA7Pr7zyfdn2SH6wSlNS14Jyrtt\n' + + 'HePrRSeYl1T+tk0AfrvaLmyM56F+9B3jwt/nzqr5YxmfVdXb2aQV53VS/mm3pB2H\n' + + 'iIt9FmvFaaOVe2DupqSr6xzbf/zyON+WF5B5HNVOWXswgpgdUsCyygs98hKy/Xje\n' + + 'TGzJUoWInW39t0YgMXenJrkS0m6wol8Rhxx81AGgELNV7EHZqg==\n' + + '-----END PRIVATE KEY-----', + format: 'pem' +}); + +const alicePublicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + + 'MIIBnzCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + + 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + + 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxAACgcBR7+iL5qx7aOb9K+aZ\n' + + 'y2oLt7ST33sDKT+nxpag6cWDDWzPBKFDCJ8fr0v7yW453px8N4qi4R7SYYxFBaYN\n' + + 'Y3JvgDg1ct2JC9sxSuUOLqSFn3hpmAjW7cS0kExIVGfdLlYtIqbhhuo45cTEbVIM\n' + + 'rDEz8mjIlnvbWpKB9+uYmbjfVoc3leFvUBqfG2In2m23Md1swsPxr3n7g68H66JX\n' + + 'iBJKZLQMqNdbY14G9rdKmhhTJrQjC+i7Q/wI8JPhOFzHIGA=\n' + + '-----END PUBLIC KEY-----', + format: 'pem' +}); + +const bobPrivateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + + '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + + 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwHxnT7Zw2Ehh1vyw\n' + + 'eolzQFHQzyuT0y+3BF+FxK2Ox7VPguTp57wQfGHbORJ2cwCdLx2mFM7gk4tZ6COS\n' + + 'E3Vta85a/PuhKXNLRdP79JgLnNtVtKXB+ePDS5C2GgXH1RHvqEdJh7JYnMy7Zj4P\n' + + 'GagGtIy3dV5f4FA0B/2C97jQ1pO16ah8gSLQRKsNpTCw2rqsZusE0rK6RaYAef7H\n' + + 'y/0tmLIsHxLIn+WK9CANqMbCWoP4I178BQaqhiOBkNyNZ0ndqA==\n' + + '-----END PRIVATE KEY-----', + format: 'pem' +}); + +const bobPublicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + + 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + + 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + + 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxQACgcEAi26oq8z/GNSBm3zi\n' + + 'gNt7SA7cArUBbTxINa9iLYWp6bxrvCKwDQwISN36/QUw8nUAe8aRyMt0oYn+y6vW\n' + + 'Pw5OlO+TLrUelMVFaADEzoYomH0zVGb0sW4aBN8haC0mbrPt9QshgCvjr1hEPEna\n' + + 'QFKfjzNaJRNMFFd4f2Dn8MSB4yu1xpA1T2i0JSk24vS2H55jx24xhUYtfhT2LJgK\n' + + 'JvnaODey/xtY4Kql10ZKf43Lw6gdQC3G8opC9OxVxt9oNR7Z\n' + + '-----END PUBLIC KEY-----', + format: 'pem' +}); + +assert.throws(() => crypto.diffieHellman({ privateKey: alicePrivateKey }), { + name: 'TypeError', + code: 'ERR_INVALID_OPT_VALUE', + message: 'The value "undefined" is invalid for option "publicKey"' +}); + +assert.throws(() => crypto.diffieHellman({ publicKey: alicePublicKey }), { + name: 'TypeError', + code: 'ERR_INVALID_OPT_VALUE', + message: 'The value "undefined" is invalid for option "privateKey"' +}); + +const privateKey = Buffer.from( + '487CD880159D835FD0A8DBA9848898317283DB07E822741B344AD397BA84CDDD3920A51588' + + 'B891B03B3EBEF3C9F767D921FAC1294D4B5E09CABB6D1DE3EB4527989754FEB64D007EBBDA' + + '2E6C8CE7A17EF41DE3C2DFE7CEAAF963199F55D5DBD9A415E77552FE69B7A41D87888B7D16' + + '6BC569A3957B60EEA6A4ABEB1CDB7FFCF238DF961790791CD54E597B3082981D52C0B2CA0B' + + '3DF212B2FD78DE4C6CC95285889D6DFDB746203177A726B912D26EB0A25F11871C7CD401A0' + + '10B355EC41D9AA', 'hex'); +const publicKey = Buffer.from( + '8b6ea8abccff18d4819b7ce280db7b480edc02b5016d3c4835af622d85a9e9bc6bbc22b00d' + + '0c0848ddfafd0530f275007bc691c8cb74a189fecbabd63f0e4e94ef932eb51e94c5456800' + + 'c4ce8628987d335466f4b16e1a04df21682d266eb3edf50b21802be3af58443c49da40529f' + + '8f335a25134c1457787f60e7f0c481e32bb5c690354f68b4252936e2f4b61f9e63c76e3185' + + '462d7e14f62c980a26f9da3837b2ff1b58e0aaa5d7464a7f8dcbc3a81d402dc6f28a42f4ec' + + '55c6df68351ed9', 'hex'); + +const group = crypto.getDiffieHellman('modp5'); +const dh = crypto.createDiffieHellman(group.getPrime(), group.getGenerator()); +dh.setPrivateKey(privateKey); + +// Test simple Diffie-Hellman, no curves involved. + +test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, + { publicKey: bobPublicKey, privateKey: bobPrivateKey }, + dh.computeSecret(publicKey)); + +test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), + crypto.generateKeyPairSync('dh', { group: 'modp5' })); + +test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), + crypto.generateKeyPairSync('dh', { prime: group.getPrime() })); + +for (const [params1, params2] of [ + // Same generator, but different primes. + [{ group: 'modp5' }, { group: 'modp18' }], + // Same primes, but different generator. + [{ group: 'modp5' }, { prime: group.getPrime(), generator: 5 }], + // Same generator, but different primes. + [{ primeLength: 1024 }, { primeLength: 1024 }] +]) { + assert.throws(() => { + test(crypto.generateKeyPairSync('dh', params1), + crypto.generateKeyPairSync('dh', params2)); + }, { + name: 'Error', + code: 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' + }); +} + +{ + const privateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + + '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + + 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwHu9fpiqrfJJ+tl9\n' + + 'ujFtEWv4afub6A/1/7sgishOYN3YQ+nmWQlmPpveIY34an5dG82CTrixHwUzQTMF\n' + + 'JaiCW3ax9+qk31f2jTNKrQznmKgopVKXF0FEJC6H79W/8Y0U14gsI9sHpovKhfou\n' + + 'RQD0QogW7ejSwMG8hCYibfrvMm0b5PHlwimISyEKh7VtDQ1frYN/Wr9ZbiV+FePJ\n' + + '2j6RUKYNj1Pv+B4zdMgiLLjILAs8WUfbHciU21KSJh1izVQaUQ==\n' + + '-----END PRIVATE KEY-----' + }); + const publicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + + 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + + 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + + 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxQACgcEAmG9LpD8SAA6/W7oK\n' + + 'E4MCuuQtf5E8bqtcEAfYTOOvKyCS+eiX3TtZRsvHJjUBEyeO99PR/KrGVlkSuW52\n' + + 'ZOSXUOFu1L/0tqHrvRVHo+QEq3OvZ3EAyJkdtSEUTztxuUrMOyJXHDc1OUdNSnk0\n' + + 'taGX4mP3247golVx2DS4viDYs7UtaMdx03dWaP6y5StNUZQlgCIUzL7MYpC16V5y\n' + + 'KkFrE+Kp/Z77gEjivaG6YuxVj4GPLxJYbNFVTel42oSVeKuq\n' + + '-----END PUBLIC KEY-----', + format: 'pem' + }); + + // This key combination will result in an unusually short secret, and should + // not cause an assertion failure. + const secret = crypto.diffieHellman({ publicKey, privateKey }); + assert.strictEqual(secret.toString('hex'), + '0099d0fa242af5db9ea7330e23937a27db041f79c581500fc7f9976' + + '554d59d5b9ced934778d72e19a1fefc81e9d981013198748c0b5c6c' + + '762985eec687dc5bec5c9367b05837daee9d0bcc29024ed7f3abba1' + + '2794b65a745117fb0d87bc5b1b2b68c296c3f686cc29e450e4e1239' + + '21f56a5733fe58aabf71f14582954059c2185d342b9b0fa10c2598a' + + '5426c2baee7f9a686fc1e16cd4757c852bf7225a2732250548efe28' + + 'debc26f1acdec51efe23d20786a6f8a14d360803bbc71972e87fd3'); +} + +// Test ECDH. + +test(crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' }), + crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' })); + +assert.throws(() => { + test(crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' }), + crypto.generateKeyPairSync('ec', { namedCurve: 'secp224k1' })); +}, { + name: 'Error', + code: 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' +}); + +// Test ECDH-ES. + +test(crypto.generateKeyPairSync('x448'), + crypto.generateKeyPairSync('x448')); + +test(crypto.generateKeyPairSync('x25519'), + crypto.generateKeyPairSync('x25519')); + +assert.throws(() => { + test(crypto.generateKeyPairSync('x448'), + crypto.generateKeyPairSync('x25519')); +}, { + name: 'Error', + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY', + message: 'Incompatible key types for Diffie-Hellman: x448 and x25519' +}); diff --git a/test/parallel/test-crypto-keygen.js b/test/parallel/test-crypto-keygen.js index 30b04e34d6ec36..e052c9a16ab312 100644 --- a/test/parallel/test-crypto-keygen.js +++ b/test/parallel/test-crypto-keygen.js @@ -981,6 +981,75 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); } } +// Test classic Diffie-Hellman key generation. +{ + generateKeyPair('dh', { + primeLength: 1024 + }, common.mustCall((err, publicKey, privateKey) => { + assert.ifError(err); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'dh'); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'dh'); + })); + + assert.throws(() => { + generateKeyPair('dh', common.mustNotCall()); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options" argument must be of type object. Received undefined' + }); + + assert.throws(() => { + generateKeyPair('dh', {}, common.mustNotCall()); + }, { + name: 'TypeError', + code: 'ERR_MISSING_OPTION', + message: 'At least one of the group, prime, or primeLength options is ' + + 'required' + }); + + assert.throws(() => { + generateKeyPair('dh', { + group: 'modp0' + }, common.mustNotCall()); + }, { + name: 'Error', + code: 'ERR_CRYPTO_UNKNOWN_DH_GROUP', + message: 'Unknown DH group' + }); + + // Test incompatible options. + const allOpts = { + group: 'modp5', + prime: Buffer.alloc(0), + primeLength: 1024, + generator: 2 + }; + const incompatible = [ + ['group', 'prime'], + ['group', 'primeLength'], + ['group', 'generator'], + ['prime', 'primeLength'] + ]; + for (const [opt1, opt2] of incompatible) { + assert.throws(() => { + generateKeyPairSync('dh', { + [opt1]: allOpts[opt1], + [opt2]: allOpts[opt2] + }); + }, { + name: 'TypeError', + code: 'ERR_INCOMPATIBLE_OPTION_PAIR', + message: `Option "${opt1}" can not be used in combination with option ` + + `"${opt2}"` + }); + } +} + // Test invalid key encoding types. { // Invalid public key type.