diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 55b948aac50db5..ef50293a8b506a 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2743,14 +2743,14 @@ encoding of `'utf8'` is enforced. If `data` is a [`Buffer`][], `TypedArray`, or This can be called many times with new data as it is streamed. -### `verify.verify(object, signature[, signatureEncoding])` +### `verify.verify(key, signature[, signatureEncoding])` -* `object` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} * `dsaEncoding` {string} * `padding` {integer} * `saltLength` {integer} @@ -2780,10 +2780,10 @@ changes: -Verifies the provided data using the given `object` and `signature`. +Verifies the provided data using the given `key` and `signature`. -If `object` is not a [`KeyObject`][], this function behaves as if -`object` had been passed to [`crypto.createPublicKey()`][]. If it is an +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 additional properties can be passed: * `dsaEncoding` {string} For DSA and ECDSA, this option specifies the @@ -4142,23 +4142,32 @@ added: - v13.9.0 - v12.17.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/62527 + description: Accept key data in addition to KeyObject instances. - version: v23.11.0 pr-url: https://github.com/nodejs/node/pull/57274 description: Optional callback argument added. --> * `options` {Object} - * `privateKey` {KeyObject} - * `publicKey` {KeyObject} + * `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} + * `publicKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} * `callback` {Function} * `err` {Error} * `secret` {Buffer} * Returns: {Buffer} if the `callback` function is not provided. Computes the Diffie-Hellman shared secret based on a `privateKey` and a `publicKey`. -Both keys must have the same `asymmetricKeyType` and must support either the DH or +Both keys must represent the same asymmetric key type and must support either the DH or ECDH operation. +If `options.privateKey` is not a [`KeyObject`][], this function behaves as if +`options.privateKey` had been passed to [`crypto.createPrivateKey()`][]. + +If `options.publicKey` is not a [`KeyObject`][], this function behaves as if +`options.publicKey` had been passed to [`crypto.createPublicKey()`][]. + If the `callback` function is provided this function uses libuv's threadpool. ### `crypto.encapsulate(key[, callback])` @@ -6941,7 +6950,7 @@ See the [list of SSL OP Flags][] for details. [`stream.transform` options]: stream.md#new-streamtransformoptions [`util.promisify()`]: util.md#utilpromisifyoriginal [`verify.update()`]: #verifyupdatedata-inputencoding -[`verify.verify()`]: #verifyverifyobject-signature-signatureencoding +[`verify.verify()`]: #verifyverifykey-signature-signatureencoding [`x509.fingerprint256`]: #x509fingerprint256 [`x509.verify(publicKey)`]: #x509verifypublickey [argon2]: https://www.rfc-editor.org/rfc/rfc9106.html diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index c463ea41f763b3..1d397e8a94819c 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -63,14 +63,15 @@ const { normalizeEncoding } = require('internal/util'); const { StringDecoder } = require('string_decoder'); function rsaFunctionFor(method, defaultPadding, keyType) { - return (options, buffer) => { + const keyName = keyType === 'private' ? 'privateKey' : undefined; + return (key, buffer) => { const { format, type, data, passphrase, namedCurve } = keyType === 'private' ? - preparePrivateKey(options) : - preparePublicOrPrivateKey(options); - const padding = options.padding || defaultPadding; - const { oaepHash, encoding } = options; - let { oaepLabel } = options; + preparePrivateKey(key, keyName) : + preparePublicOrPrivateKey(key, keyName); + const padding = key.padding || defaultPadding; + const { oaepHash, encoding } = key; + let { oaepLabel } = key; if (oaepHash !== undefined) validateString(oaepHash, 'key.oaepHash'); if (oaepLabel !== undefined) diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 58e2bc0c91ec3c..8e631ecf7053f5 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -17,7 +17,6 @@ const { DiffieHellman: _DiffieHellman, DiffieHellmanGroup: _DiffieHellmanGroup, ECDH: _ECDH, - ECDHBitsJob, ECDHConvertKey: _ECDHConvertKey, kCryptoJobAsync, kCryptoJobSync, @@ -52,9 +51,11 @@ const { } = require('internal/util'); const { - KeyObject, + isKeyObject, kAlgorithm, kKeyType, + preparePrivateKey, + preparePublicOrPrivateKey, } = require('internal/crypto/keys'); const { @@ -284,31 +285,65 @@ function diffieHellman(options, callback) { validateFunction(callback, 'callback'); const { privateKey, publicKey } = options; - if (!(privateKey instanceof KeyObject)) + + // TODO(@panva): remove these non-semver-major error code preserving measures + // in a semver-major followup, the final state is just preparePublicOrPrivateKey + // and preparePrivateKey + if (privateKey == null) throw new ERR_INVALID_ARG_VALUE('options.privateKey', privateKey); - if (!(publicKey instanceof KeyObject)) + if (publicKey == null) throw new ERR_INVALID_ARG_VALUE('options.publicKey', publicKey); - if (privateKey.type !== 'private') - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, 'private'); + if (isKeyObject(privateKey)) { + 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'); + if (isKeyObject(publicKey)) { + 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}`); + if (isKeyObject(privateKey) && isKeyObject(publicKey)) { + 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}`); + } } + const { + data: pubData, + format: pubFormat, + type: pubType, + passphrase: pubPassphrase, + namedCurve: pubNamedCurve, + } = preparePublicOrPrivateKey(publicKey, 'options.publicKey'); + + const { + data: privData, + format: privFormat, + type: privType, + passphrase: privPassphrase, + namedCurve: privNamedCurve, + } = preparePrivateKey(privateKey, 'options.privateKey'); + const job = new DHBitsJob( callback ? kCryptoJobAsync : kCryptoJobSync, - publicKey[kHandle], - privateKey[kHandle]); + pubData, + pubFormat, + pubType, + pubPassphrase, + pubNamedCurve, + privData, + privFormat, + privType, + privPassphrase, + privNamedCurve); if (!callback) { const { 0: err, 1: secret } = job.run(); @@ -349,10 +384,18 @@ async function ecdhDeriveBits(algorithm, baseKey, length) { throw lazyDOMException('Named curve mismatch', 'InvalidAccessError'); } - const bits = await jobPromise(() => new ECDHBitsJob( + const bits = await jobPromise(() => new DHBitsJob( kCryptoJobAsync, key[kKeyObject][kHandle], - baseKey[kKeyObject][kHandle])); + undefined, + undefined, + undefined, + undefined, + baseKey[kKeyObject][kHandle], + undefined, + undefined, + undefined, + undefined)); // If a length is not specified, return the full derived secret if (length === null) diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index a612430af86db5..d1c14e547fd640 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -148,7 +148,7 @@ function parseKeyEncoding(keyType, options = kEmptyObject) { format: publicFormat, type: publicType, } = parsePublicKeyEncoding(publicKeyEncoding, keyType, - 'publicKeyEncoding')); + 'options.publicKeyEncoding')); } else { throw new ERR_INVALID_ARG_VALUE('options.publicKeyEncoding', publicKeyEncoding); @@ -164,7 +164,7 @@ function parseKeyEncoding(keyType, options = kEmptyObject) { cipher, passphrase, } = parsePrivateKeyEncoding(privateKeyEncoding, keyType, - 'privateKeyEncoding')); + 'options.privateKeyEncoding')); } else { throw new ERR_INVALID_ARG_VALUE('options.privateKeyEncoding', privateKeyEncoding); diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index d66f03a4ebcea7..a1acb782a5f5db 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -466,9 +466,9 @@ function parseKeyType(typeStr, required, keyType, isPublic, optionName) { throw new ERR_INVALID_ARG_VALUE(optionName, typeStr); } -function option(name, objName) { - return objName === undefined ? - `options.${name}` : `options.${objName}.${name}`; +function option(name, prefix) { + return prefix === undefined ? + `options.${name}` : `${prefix}.${name}`; } function parseKeyFormatAndType(enc, keyType, isPublic, objName) { @@ -628,7 +628,7 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) { } -function prepareAsymmetricKey(key, ctx) { +function prepareAsymmetricKey(key, ctx, name = 'key') { if (isKeyObject(key)) { // Best case: A key object, as simple as that. return { data: getKeyObjectHandle(key, ctx) }; @@ -639,7 +639,7 @@ function prepareAsymmetricKey(key, ctx) { } if (isStringOrBuffer(key)) { // Expect PEM by default, mostly for backward compatibility. - return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') }; + return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, name) }; } if (typeof key === 'object') { const { key: data, encoding, format } = key; @@ -654,23 +654,23 @@ function prepareAsymmetricKey(key, ctx) { return { data: getKeyObjectHandle(data[kKeyObject], ctx) }; } if (format === 'jwk') { - validateObject(data, 'key.key'); + validateObject(data, `${name}.key`); return { data, format: kKeyFormatJWK }; } else if (format === 'raw-public' || format === 'raw-private' || format === 'raw-seed') { if (!isStringOrBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( - 'key.key', + `${name}.key`, ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], data); } - validateString(key.asymmetricKeyType, 'key.asymmetricKeyType'); + validateString(key.asymmetricKeyType, `${name}.asymmetricKeyType`); if (key.asymmetricKeyType === 'ec') { - validateString(key.namedCurve, 'key.namedCurve'); + validateString(key.namedCurve, `${name}.namedCurve`); } - const rawFormat = parseKeyFormat(format, undefined, 'options.format'); + const rawFormat = parseKeyFormat(format, undefined, `${name}.format`); return { - data: getArrayBufferOrView(data, 'key.key'), + data: getArrayBufferOrView(data, `${name}.key`), format: rawFormat, type: key.asymmetricKeyType, namedCurve: key.namedCurve ?? null, @@ -680,7 +680,7 @@ function prepareAsymmetricKey(key, ctx) { // Either PEM or DER using PKCS#1 or SPKI. if (!isStringOrBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( - 'key.key', + `${name}.key`, getKeyTypes(ctx !== kCreatePrivate), data); } @@ -688,23 +688,23 @@ function prepareAsymmetricKey(key, ctx) { const isPublic = (ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined; return { - data: getArrayBufferOrView(data, 'key', encoding), - ...parseKeyEncoding(key, undefined, isPublic), + data: getArrayBufferOrView(data, `${name}.key`, encoding), + ...parseKeyEncoding(key, undefined, isPublic, name), }; } throw new ERR_INVALID_ARG_TYPE( - 'key', + name, getKeyTypes(ctx !== kCreatePrivate), key); } -function preparePrivateKey(key) { - return prepareAsymmetricKey(key, kConsumePrivate); +function preparePrivateKey(key, name) { + return prepareAsymmetricKey(key, kConsumePrivate, name); } -function preparePublicOrPrivateKey(key) { - return prepareAsymmetricKey(key, kConsumePublic); +function preparePublicOrPrivateKey(key, name) { + return prepareAsymmetricKey(key, kConsumePublic, name); } function prepareSecretKey(key, encoding, bufferOnly = false) { diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index 324b804a817a3d..a27ce4b190e111 100644 --- a/lib/internal/crypto/sig.js +++ b/lib/internal/crypto/sig.js @@ -130,19 +130,19 @@ function getIntOption(name, options) { return undefined; } -Sign.prototype.sign = function sign(options, encoding) { - if (!options) +Sign.prototype.sign = function sign(privateKey, encoding) { + if (!privateKey) throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); const { data, format, type, passphrase, namedCurve } = - preparePrivateKey(options, true); + preparePrivateKey(privateKey, 'privateKey'); // Options specific to RSA - const rsaPadding = getPadding(options); - const pssSaltLength = getSaltLength(options); + const rsaPadding = getPadding(privateKey); + const pssSaltLength = getSaltLength(privateKey); // Options specific to (EC)DSA - const dsaSigEnc = getDSASignatureEncoding(options); + const dsaSigEnc = getDSASignatureEncoding(privateKey); const ret = this[kHandle].sign(data, format, type, passphrase, namedCurve, @@ -232,21 +232,21 @@ ObjectSetPrototypeOf(Verify, Writable); Verify.prototype._write = Sign.prototype._write; Verify.prototype.update = Sign.prototype.update; -Verify.prototype.verify = function verify(options, signature, sigEncoding) { +Verify.prototype.verify = function verify(key, signature, sigEncoding) { const { data, format, type, passphrase, namedCurve, - } = preparePublicOrPrivateKey(options, true); + } = preparePublicOrPrivateKey(key, 'key'); // Options specific to RSA - const rsaPadding = getPadding(options); - const pssSaltLength = getSaltLength(options); + const rsaPadding = getPadding(key); + const pssSaltLength = getSaltLength(key); // Options specific to (EC)DSA - const dsaSigEnc = getDSASignatureEncoding(options); + const dsaSigEnc = getDSASignatureEncoding(key); signature = getArrayBufferOrView(signature, 'signature', sigEncoding); diff --git a/src/crypto/crypto_dh.cc b/src/crypto/crypto_dh.cc index 0a14ebc9cdcd80..36e38e99f06fe0 100644 --- a/src/crypto/crypto_dh.cc +++ b/src/crypto/crypto_dh.cc @@ -477,20 +477,16 @@ Maybe DHBitsTraits::AdditionalConfig( const FunctionCallbackInfo& args, unsigned int offset, DHBitsConfig* params) { - CHECK(args[offset]->IsObject()); // public key - CHECK(args[offset + 1]->IsObject()); // private key + auto public_key = KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &offset); + if (!public_key) [[unlikely]] + return Nothing(); - KeyObjectHandle* private_key; - KeyObjectHandle* public_key; + auto private_key = KeyObjectData::GetPrivateKeyFromJs(args, &offset, true); + if (!private_key) [[unlikely]] + return Nothing(); - ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset], Nothing()); - ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 1], Nothing()); - - CHECK(private_key->Data().GetKeyType() == kKeyTypePrivate); - CHECK(public_key->Data().GetKeyType() != kKeyTypeSecret); - - params->public_key = public_key->Data().addRef(); - params->private_key = private_key->Data().addRef(); + params->public_key = std::move(public_key); + params->private_key = std::move(private_key); return JustVoid(); } diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 6738edd590c300..9cd50a421f8715 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -39,7 +39,6 @@ using v8::JustVoid; using v8::Local; using v8::LocalVector; using v8::Maybe; -using v8::MaybeLocal; using v8::Nothing; using v8::Object; using v8::String; @@ -68,7 +67,6 @@ void ECDH::Initialize(Environment* env, Local target) { SetMethodNoSideEffect(context, target, "ECDHConvertKey", ECDH::ConvertKey); SetMethodNoSideEffect(context, target, "getCurves", ECDH::GetCurves); - ECDHBitsJob::Initialize(env, target); ECKeyPairGenJob::Initialize(env, target); NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE); @@ -86,7 +84,6 @@ void ECDH::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(ECDH::ConvertKey); registry->Register(ECDH::GetCurves); - ECDHBitsJob::RegisterExternalReferences(registry); ECKeyPairGenJob::RegisterExternalReferences(registry); } @@ -391,106 +388,6 @@ void ECDH::ConvertKey(const FunctionCallbackInfo& args) { } } -void ECDHBitsConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackField("public", public_); - tracker->TrackField("private", private_); -} - -MaybeLocal ECDHBitsTraits::EncodeOutput(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out) { - return out->ToArrayBuffer(env); -} - -Maybe ECDHBitsTraits::AdditionalConfig( - CryptoJobMode mode, - const FunctionCallbackInfo& args, - unsigned int offset, - ECDHBitsConfig* params) { - Environment* env = Environment::GetCurrent(args); - - CHECK(args[offset]->IsObject()); // public key - CHECK(args[offset + 1]->IsObject()); // private key - - KeyObjectHandle* private_key; - KeyObjectHandle* public_key; - - ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset], Nothing()); - ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 1], Nothing()); - - if (private_key->Data().GetKeyType() != kKeyTypePrivate || - public_key->Data().GetKeyType() != kKeyTypePublic) { - THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); - return Nothing(); - } - - params->private_ = private_key->Data().addRef(); - params->public_ = public_key->Data().addRef(); - - return JustVoid(); -} - -bool ECDHBitsTraits::DeriveBits(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore* errors) { - size_t len = 0; - const auto& m_privkey = params.private_.GetAsymmetricKey(); - const auto& m_pubkey = params.public_.GetAsymmetricKey(); - - switch (m_privkey.id()) { - case EVP_PKEY_X25519: - // Fall through - case EVP_PKEY_X448: { - Mutex::ScopedLock pub_lock(params.public_.mutex()); - EVPKeyCtxPointer ctx = m_privkey.newCtx(); - if (!ctx.initForDerive(m_pubkey)) return false; - - auto data = ctx.derive(); - if (!data) return false; - DCHECK(!data.isSecure()); - - *out = ByteSource::Allocated(data.release()); - break; - } - default: { - const EC_KEY* private_key; - { - Mutex::ScopedLock priv_lock(params.private_.mutex()); - private_key = m_privkey; - } - - Mutex::ScopedLock pub_lock(params.public_.mutex()); - const EC_KEY* public_key = m_pubkey; - - const auto group = ECKeyPointer::GetGroup(private_key); - if (group == nullptr) { - errors->Insert(NodeCryptoError::ECDH_FAILED); - return false; - } - - CHECK(ECKeyPointer::Check(private_key)); - CHECK(ECKeyPointer::Check(public_key)); - const auto pub = ECKeyPointer::GetPublicKey(public_key); - int field_size = EC_GROUP_get_degree(group); - len = (field_size + 7) / 8; - auto buf = DataPointer::Alloc(len); - CHECK_NOT_NULL(pub); - CHECK_NOT_NULL(private_key); - if (ECDH_compute_key( - static_cast(buf.get()), len, pub, private_key, nullptr) <= - 0) { - return false; - } - - *out = ByteSource::Allocated(buf.release()); - } - } - - return true; -} - EVPKeyCtxPointer EcKeyGenTraits::Setup(EcKeyPairGenConfig* params) { EVPKeyCtxPointer key_ctx; switch (params->params.curve_nid) { diff --git a/src/crypto/crypto_ec.h b/src/crypto/crypto_ec.h index 5522ac743e3089..9bc817f3d7f5d3 100644 --- a/src/crypto/crypto_ec.h +++ b/src/crypto/crypto_ec.h @@ -55,41 +55,6 @@ class ECDH final : public BaseObject { const EC_GROUP* group_; }; -struct ECDHBitsConfig final : public MemoryRetainer { - int id_; - KeyObjectData private_; - KeyObjectData public_; - - void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(ECDHBitsConfig) - SET_SELF_SIZE(ECDHBitsConfig) -}; - -struct ECDHBitsTraits final { - using AdditionalParameters = ECDHBitsConfig; - static constexpr const char* JobName = "ECDHBitsJob"; - static constexpr AsyncWrap::ProviderType Provider = - AsyncWrap::PROVIDER_DERIVEBITSREQUEST; - - static v8::Maybe AdditionalConfig( - CryptoJobMode mode, - const v8::FunctionCallbackInfo& args, - unsigned int offset, - ECDHBitsConfig* params); - - static bool DeriveBits(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out_, - CryptoJobMode mode, - CryptoErrorStore* errors); - - static v8::MaybeLocal EncodeOutput(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out); -}; - -using ECDHBitsJob = DeriveBitsJob; - struct EcKeyPairParams final : public MemoryRetainer { int curve_nid; int param_encoding; diff --git a/test/parallel/test-crypto-dh-stateless-async.js b/test/parallel/test-crypto-dh-stateless-async.js index 891c9a983e0603..ac3aeed3b23aee 100644 --- a/test/parallel/test-crypto-dh-stateless-async.js +++ b/test/parallel/test-crypto-dh-stateless-async.js @@ -221,3 +221,132 @@ test(crypto.generateKeyPairSync('x25519'), assert.strictEqual(err.code, hasOpenSSL3 ? 'ERR_OSSL_FAILED_DURING_DERIVATION' : 'ERR_CRYPTO_OPERATION_FAILED'); })); } + +// Test all key encoding formats +for (const { privateKey: alicePriv, publicKey: bobPub } of [ + crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), + crypto.generateKeyPairSync('x25519'), +]) { + const expected = crypto.diffieHellman({ + privateKey: alicePriv, + publicKey: bobPub, + }); + + const encodings = [ + // PEM string + { + privateKey: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + publicKey: bobPub.export({ type: 'spki', format: 'pem' }), + }, + // PEM { key, format } object + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + format: 'pem', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'pem' }), + format: 'pem', + }, + }, + // DER PKCS#8 / SPKI + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'der' }), + format: 'der', + type: 'pkcs8', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'der' }), + format: 'der', + type: 'spki', + }, + }, + // JWK + { + privateKey: { key: alicePriv.export({ format: 'jwk' }), format: 'jwk' }, + publicKey: { key: bobPub.export({ format: 'jwk' }), format: 'jwk' }, + }, + // Raw key material + { + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: alicePriv.asymmetricKeyType, + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType: bobPub.asymmetricKeyType, + ...bobPub.asymmetricKeyDetails, + }, + }, + ]; + + // EC-only encodings + if (alicePriv.asymmetricKeyType === 'ec') { + // DER SEC1 private key + encodings.push({ + privateKey: { + key: alicePriv.export({ type: 'sec1', format: 'der' }), + format: 'der', + type: 'sec1', + }, + publicKey: bobPub, + }); + // Raw with compressed public key + encodings.push({ + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: 'ec', + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public', type: 'compressed' }), + format: 'raw-public', + asymmetricKeyType: 'ec', + ...bobPub.asymmetricKeyDetails, + }, + }); + } + + for (const options of encodings) { + crypto.diffieHellman(options, common.mustSucceed((buf) => { + assert.deepStrictEqual(buf, expected); + })); + } +} + +// Test C++ error conditions (delivered via the callback) +{ + const ec256 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); + const ec384 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' }); + const x25519 = crypto.generateKeyPairSync('x25519'); + const ed25519 = crypto.generateKeyPairSync('ed25519'); + + // Mismatching EC curves + crypto.diffieHellman({ + privateKey: ec256.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: ec384.publicKey.export({ type: 'spki', format: 'pem' }), + }, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS'); + })); + + // Incompatible key types (ec + x25519) + crypto.diffieHellman({ + privateKey: ec256.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: x25519.publicKey.export({ type: 'spki', format: 'pem' }), + }, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE'); + })); + + // Unsupported key type (ed25519) + crypto.diffieHellman({ + privateKey: ed25519.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: ed25519.publicKey.export({ type: 'spki', format: 'pem' }), + }, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE'); + })); +} diff --git a/test/parallel/test-crypto-dh-stateless.js b/test/parallel/test-crypto-dh-stateless.js index 84ebbd21aad6a1..eb4a6c9110e0da 100644 --- a/test/parallel/test-crypto-dh-stateless.js +++ b/test/parallel/test-crypto-dh-stateless.js @@ -302,3 +302,167 @@ assert.throws(() => { { name: 'Error', message: /Deriving bits failed/ }, ); } + +// Test all key encoding formats +for (const { privateKey: alicePriv, publicKey: bobPub } of [ + crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), + crypto.generateKeyPairSync('x25519'), +]) { + const expected = crypto.diffieHellman({ + privateKey: alicePriv, + publicKey: bobPub, + }); + + const encodings = [ + // PEM string + { + privateKey: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + publicKey: bobPub.export({ type: 'spki', format: 'pem' }), + }, + // PEM { key, format } object + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + format: 'pem', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'pem' }), + format: 'pem', + }, + }, + // DER PKCS#8 / SPKI + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'der' }), + format: 'der', + type: 'pkcs8', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'der' }), + format: 'der', + type: 'spki', + }, + }, + // JWK + { + privateKey: { key: alicePriv.export({ format: 'jwk' }), format: 'jwk' }, + publicKey: { key: bobPub.export({ format: 'jwk' }), format: 'jwk' }, + }, + // Raw key material + { + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: alicePriv.asymmetricKeyType, + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType: bobPub.asymmetricKeyType, + ...bobPub.asymmetricKeyDetails, + }, + }, + ]; + + // EC-only encodings + if (alicePriv.asymmetricKeyType === 'ec') { + // DER SEC1 private key + encodings.push({ + privateKey: { + key: alicePriv.export({ type: 'sec1', format: 'der' }), + format: 'der', + type: 'sec1', + }, + publicKey: bobPub, + }); + // Raw with compressed public key + encodings.push({ + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: 'ec', + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public', type: 'compressed' }), + format: 'raw-public', + asymmetricKeyType: 'ec', + ...bobPub.asymmetricKeyDetails, + }, + }); + } + + for (const options of encodings) { + assert.deepStrictEqual(crypto.diffieHellman(options), expected); + } +} + +// Test that error messages include the correct property path +{ + const kp = crypto.generateKeyPairSync('x25519'); + const pub = kp.publicKey.export({ type: 'spki', format: 'pem' }); + const priv = kp.privateKey.export({ type: 'pkcs8', format: 'pem' }); + + // Invalid privateKey format + assert.throws(() => crypto.diffieHellman({ + privateKey: { key: Buffer.alloc(0), format: 'banana', type: 'pkcs8' }, + publicKey: pub, + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.privateKey\.format/, + }); + + // Invalid privateKey type + assert.throws(() => crypto.diffieHellman({ + privateKey: { key: Buffer.alloc(0), format: 'der', type: 'banana' }, + publicKey: pub, + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.privateKey\.type/, + }); + + // Invalid publicKey format + assert.throws(() => crypto.diffieHellman({ + publicKey: { key: Buffer.alloc(0), format: 'banana', type: 'spki' }, + privateKey: priv, + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.publicKey\.format/, + }); + + // Invalid publicKey type + assert.throws(() => crypto.diffieHellman({ + publicKey: { key: Buffer.alloc(0), format: 'der', type: 'banana' }, + privateKey: priv, + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.publicKey\.type/, + }); +} + +// Test C++ error conditions +{ + const ec256 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); + const ec384 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' }); + const x25519 = crypto.generateKeyPairSync('x25519'); + const ed25519 = crypto.generateKeyPairSync('ed25519'); + + // Mismatching EC curves + assert.throws(() => crypto.diffieHellman({ + privateKey: ec256.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: ec384.publicKey.export({ type: 'spki', format: 'pem' }), + }), { code: 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' }); + + // Incompatible key types (ec + x25519) + assert.throws(() => crypto.diffieHellman({ + privateKey: ec256.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: x25519.publicKey.export({ type: 'spki', format: 'pem' }), + }), { code: 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' }); + + // Unsupported key type (ed25519) + assert.throws(() => crypto.diffieHellman({ + privateKey: ed25519.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: ed25519.publicKey.export({ type: 'spki', format: 'pem' }), + }), { code: 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' }); +} diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 52fcad0882e1c7..ce93de980946e5 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -343,7 +343,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', createPrivateKey({ key: Buffer.alloc(0), format: 'der', type: 'spki' }); }, { code: 'ERR_INVALID_ARG_VALUE', - message: "The property 'options.type' is invalid. Received 'spki'" + message: "The property 'key.type' is invalid. Received 'spki'" }); // Unlike SPKI, PKCS#1 is a valid encoding for private keys (and public keys), @@ -1074,3 +1074,38 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); } } + +// Test that createPublicKey/createPrivateKey error messages use 'key.' paths +{ + // createPrivateKey with invalid format + assert.throws(() => { + createPrivateKey({ key: Buffer.alloc(0), format: 'banana', type: 'pkcs8' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); + + // createPrivateKey with invalid type + assert.throws(() => { + createPrivateKey({ key: Buffer.alloc(0), format: 'der', type: 'banana' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.type/, + }); + + // createPublicKey with invalid format + assert.throws(() => { + createPublicKey({ key: Buffer.alloc(0), format: 'banana', type: 'spki' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); + + // createPublicKey with invalid type + assert.throws(() => { + createPublicKey({ key: Buffer.alloc(0), format: 'der', type: 'banana' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.type/, + }); +} diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index c67e9bbff83837..ba30cb17dc32df 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -891,7 +891,7 @@ if (hasOpenSSL(3, 2)) { }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); assert.throws(() => { crypto.createSign('sha256').sign({ key, format: 'jwk' }); - }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "privateKey\.key" property must be of type object/ }); } } @@ -932,3 +932,66 @@ if (hasOpenSSL(3, 2)) { }, { code: 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE', message: /operation not supported for this keytype/ }); } } + +// Test that sign/verify error messages use correct property paths +{ + // Sign with invalid format + assert.throws(() => { + crypto.createSign('SHA256').update('test').sign({ + key: Buffer.alloc(0), format: 'banana', type: 'pkcs8', + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /privateKey\.format/, + }); + + // Sign with invalid type + assert.throws(() => { + crypto.createSign('SHA256').update('test').sign({ + key: Buffer.alloc(0), format: 'der', type: 'banana', + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /privateKey\.type/, + }); + + // Verify with invalid format + assert.throws(() => { + crypto.createVerify('SHA256').update('test').verify({ + key: Buffer.alloc(0), format: 'banana', type: 'spki', + }, Buffer.alloc(0)); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); + + // Verify with invalid type + assert.throws(() => { + crypto.createVerify('SHA256').update('test').verify({ + key: Buffer.alloc(0), format: 'der', type: 'banana', + }, Buffer.alloc(0)); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.type/, + }); + + // crypto.sign with invalid format + assert.throws(() => { + crypto.sign('SHA256', Buffer.from('test'), { + key: Buffer.alloc(0), format: 'banana', type: 'pkcs8', + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); + + // crypto.verify with invalid format + assert.throws(() => { + crypto.verify('SHA256', Buffer.from('test'), { + key: Buffer.alloc(0), format: 'banana', type: 'spki', + }, Buffer.alloc(0)); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); +}