mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
crypto: fetch ciphers for private-key encoding
Resolve provider ciphers before serializing private keys and retain the fetched implementation across encoding configuration copies and async key generation. Keep format-specific restrictions in the serializers. Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66108 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
1 parent
ca8ee04594
commit
3de05da569
5 files changed
+168
-11
No files matched your search
Vendored
+15
-6
@@ -2658,10 +2658,6 @@ const EVP_MD* getDigestByName(const char* name) {
|
||||
return EVP_get_digestbyname(name);
|
||||
}
|
||||
|
||||
const EVP_CIPHER* getCipherByName(const char* name) {
|
||||
return EVP_get_cipherbyname(name);
|
||||
}
|
||||
|
||||
bool checkHkdfLength(const Digest& md, size_t length) {
|
||||
// HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as
|
||||
// the output of the hash function. 255 is a hard limit because HKDF appends
|
||||
@@ -4384,7 +4380,7 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
|
||||
|
||||
#if NCRYPTO_USE_OPENSSL3_PROVIDER
|
||||
const EVP_CIPHER* cipher =
|
||||
config.format == PKFormatType::PEM ? config.cipher : nullptr;
|
||||
config.format == PKFormatType::PEM ? config.cipher.get() : nullptr;
|
||||
if (cipher != nullptr && passphrase.len == 0) {
|
||||
err =
|
||||
!WriteEncryptedTraditionalPEM(bio.get(), get(), cipher, passphrase);
|
||||
@@ -4466,7 +4462,7 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
|
||||
|
||||
#if NCRYPTO_USE_OPENSSL3_PROVIDER
|
||||
const EVP_CIPHER* cipher =
|
||||
config.format == PKFormatType::PEM ? config.cipher : nullptr;
|
||||
config.format == PKFormatType::PEM ? config.cipher.get() : nullptr;
|
||||
err = !WriteEncodedPKey(bio.get(),
|
||||
get(),
|
||||
OSSL_KEYMGMT_SELECT_ALL,
|
||||
@@ -5258,6 +5254,19 @@ const Cipher Cipher::FromName(const char* name, CipherCache* cache) {
|
||||
#endif
|
||||
}
|
||||
|
||||
const Cipher Cipher::FromNameForKeyEncoding(const char* name) {
|
||||
// Key serializers have their own cipher restrictions. Preserve their policy
|
||||
// instead of applying the filters used by the general cipher operations.
|
||||
#if NCRYPTO_USE_OPENSSL3_PROVIDER
|
||||
MarkPopErrorOnReturn mark_pop_error_on_return;
|
||||
DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> fetched(
|
||||
EVP_CIPHER_fetch(nullptr, name, nullptr));
|
||||
if (fetched) return Cipher(std::move(fetched));
|
||||
#endif
|
||||
// Preserve serializer errors for known ciphers that cannot be fetched.
|
||||
return Cipher(EVP_get_cipherbyname(name));
|
||||
}
|
||||
|
||||
const Cipher Cipher::FromNid(int nid, CipherCache* cache) {
|
||||
const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid);
|
||||
if (cipher != nullptr) {
|
||||
|
||||
Vendored
+2
-2
@@ -542,6 +542,7 @@ class Cipher final {
|
||||
unsigned char* iv) const;
|
||||
|
||||
static const Cipher FromName(const char* name, CipherCache* cache = nullptr);
|
||||
static const Cipher FromNameForKeyEncoding(const char* name);
|
||||
static const Cipher FromNid(int nid, CipherCache* cache = nullptr);
|
||||
static const Cipher FromCtx(const CipherCtxPointer& ctx);
|
||||
|
||||
@@ -1250,7 +1251,7 @@ class EVPKeyPointer final {
|
||||
using PublicKeyEncodingConfig = AsymmetricKeyEncodingConfig;
|
||||
|
||||
struct PrivateKeyEncodingConfig : public AsymmetricKeyEncodingConfig {
|
||||
const EVP_CIPHER* cipher = nullptr;
|
||||
Cipher cipher;
|
||||
std::optional<DataPointer> passphrase = std::nullopt;
|
||||
PrivateKeyEncodingConfig() = default;
|
||||
PrivateKeyEncodingConfig(bool output_key_object,
|
||||
@@ -2194,7 +2195,6 @@ class KDF final {
|
||||
#endif
|
||||
|
||||
const EVP_MD* getDigestByName(const char* name);
|
||||
const EVP_CIPHER* getCipherByName(const char* name);
|
||||
|
||||
// Verify that the specified HKDF output length is valid for the given digest.
|
||||
// The maximum length for HKDF output for a given digest is 255 times the
|
||||
|
||||
@@ -538,8 +538,8 @@ KeyObjectData::GetPrivateKeyEncodingFromJs(
|
||||
if (context != kKeyContextInput) {
|
||||
if (args[*offset]->IsString()) {
|
||||
Utf8Value cipher_name(env->isolate(), args[*offset]);
|
||||
config.cipher = ncrypto::getCipherByName(*cipher_name);
|
||||
if (config.cipher == nullptr) {
|
||||
config.cipher = ncrypto::Cipher::FromNameForKeyEncoding(*cipher_name);
|
||||
if (!config.cipher) {
|
||||
THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env);
|
||||
return Nothing<EVPKeyPointer::PrivateKeyEncodingConfig>();
|
||||
}
|
||||
@@ -552,7 +552,7 @@ KeyObjectData::GetPrivateKeyEncodingFromJs(
|
||||
}
|
||||
|
||||
if (IsAnyBufferSource(args[*offset])) {
|
||||
CHECK_IMPLIES(context != kKeyContextInput, config.cipher != nullptr);
|
||||
CHECK_IMPLIES(context != kKeyContextInput, config.cipher);
|
||||
ArrayBufferOrViewContents<char> passphrase(args[*offset]);
|
||||
if (!passphrase.CheckSizeInt32()) [[unlikely]] {
|
||||
THROW_ERR_OUT_OF_RANGE(env, "passphrase is too big");
|
||||
|
||||
@@ -673,3 +673,38 @@ TEST(NodeCrypto, UnavailableBoringSSLKeyAlgorithms) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if NCRYPTO_USE_OPENSSL3_PROVIDER
|
||||
TEST(NodeCrypto, PrivateKeyEncodingOwnsFetchedCipher) {
|
||||
ncrypto::ClearErrorOnReturn clear_errors;
|
||||
EVPKeyPointer::PrivateKeyEncodingConfig assigned;
|
||||
{
|
||||
EVPKeyPointer::PrivateKeyEncodingConfig original;
|
||||
original.cipher =
|
||||
ncrypto::Cipher::FromNameForKeyEncoding("2.16.840.1.101.3.4.1.42");
|
||||
ASSERT_TRUE(original.cipher);
|
||||
ASSERT_NE(EVP_CIPHER_get0_provider(original.cipher.get()), nullptr);
|
||||
const auto copied = original;
|
||||
assigned = copied;
|
||||
}
|
||||
|
||||
ASSERT_TRUE(assigned.cipher);
|
||||
EXPECT_NE(EVP_CIPHER_get0_provider(assigned.cipher.get()), nullptr);
|
||||
EXPECT_EQ(EVP_CIPHER_is_a(assigned.cipher.get(), "AES-256-CBC"), 1);
|
||||
auto ctx = ncrypto::CipherCtxPointer::New();
|
||||
const unsigned char key[32] = {};
|
||||
const unsigned char iv[16] = {};
|
||||
EXPECT_TRUE(ctx.init(assigned.cipher, true, key, iv));
|
||||
}
|
||||
|
||||
TEST(NodeCrypto, PrivateKeyEncodingProviderOnlyCipher) {
|
||||
ncrypto::ClearErrorOnReturn clear_errors;
|
||||
const auto available = ncrypto::Cipher::FromName("AES-128-CBC-CTS");
|
||||
if (!available) GTEST_SKIP();
|
||||
const auto cipher =
|
||||
ncrypto::Cipher::FromNameForKeyEncoding("AES-128-CBC-CTS");
|
||||
ASSERT_TRUE(cipher);
|
||||
EXPECT_NE(EVP_CIPHER_get0_provider(cipher.get()), nullptr);
|
||||
EXPECT_EQ(EVP_CIPHER_is_a(cipher.get(), "AES-128-CBC-CTS"), 1);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
const { hasFIPS, hasOpenSSL, isBoringSSL } = require('../common/crypto');
|
||||
if (isBoringSSL || !hasOpenSSL(3))
|
||||
common.skip('OpenSSL providers are required');
|
||||
|
||||
const assert = require('assert');
|
||||
const {
|
||||
createPrivateKey,
|
||||
createPublicKey,
|
||||
generateKeyPair,
|
||||
generateKeyPairSync,
|
||||
getCiphers,
|
||||
sign,
|
||||
verify,
|
||||
} = require('crypto');
|
||||
|
||||
// Resolve an AES-256-CBC provider alias and retain the fetched cipher while
|
||||
// encoding private keys and running asynchronous key generation jobs.
|
||||
const cipher = '2.16.840.1.101.3.4.1.42';
|
||||
const passphrase = 'provider cipher passphrase';
|
||||
const keyOptions = { namedCurve: 'prime256v1' };
|
||||
const { publicKey, privateKey } = generateKeyPairSync('ec', keyOptions);
|
||||
const data = Buffer.from('encrypted private key');
|
||||
|
||||
function checkPrivateKey(encoded, format, type, expectedPublicKey) {
|
||||
const decrypted = createPrivateKey({
|
||||
key: encoded,
|
||||
format,
|
||||
type,
|
||||
passphrase,
|
||||
});
|
||||
assert(createPublicKey(decrypted).equals(expectedPublicKey));
|
||||
const signature = sign('sha256', data, decrypted);
|
||||
assert(verify('sha256', data, expectedPublicKey, signature));
|
||||
}
|
||||
|
||||
for (const format of ['pem', 'der']) {
|
||||
const privateKeyEncoding = {
|
||||
format,
|
||||
type: 'pkcs8',
|
||||
cipher,
|
||||
passphrase,
|
||||
};
|
||||
|
||||
const exported = privateKey.export(privateKeyEncoding);
|
||||
if (format === 'pem')
|
||||
assert.match(exported, /^-----BEGIN ENCRYPTED PRIVATE KEY-----/);
|
||||
checkPrivateKey(exported, format, 'pkcs8', publicKey);
|
||||
|
||||
const generated = generateKeyPairSync('ec', {
|
||||
...keyOptions,
|
||||
privateKeyEncoding,
|
||||
});
|
||||
checkPrivateKey(generated.privateKey, format, 'pkcs8', generated.publicKey);
|
||||
|
||||
// Async jobs copy the encoding configuration. Its fetched cipher must remain
|
||||
// valid after the configuration used to create the job has been destroyed.
|
||||
generateKeyPair('ec', {
|
||||
...keyOptions,
|
||||
privateKeyEncoding,
|
||||
}, common.mustSucceed((generatedPublicKey, generatedPrivateKey) => {
|
||||
checkPrivateKey(generatedPrivateKey, format, 'pkcs8', generatedPublicKey);
|
||||
}));
|
||||
}
|
||||
|
||||
// Provider lookup must preserve the restrictions of each key format.
|
||||
const sec1 = { format: 'pem', type: 'sec1', cipher, passphrase };
|
||||
// Traditional PEM encryption uses MD5 to derive its key.
|
||||
if (!hasFIPS())
|
||||
checkPrivateKey(privateKey.export(sec1), 'pem', 'sec1', publicKey);
|
||||
assert.throws(() => privateKey.export({ ...sec1, format: 'der' }), {
|
||||
code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS',
|
||||
});
|
||||
|
||||
const unknownCipher = {
|
||||
format: 'pem',
|
||||
type: 'pkcs8',
|
||||
cipher: 'unknown-private-key-cipher',
|
||||
passphrase,
|
||||
};
|
||||
const unknownCipherError = {
|
||||
code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
|
||||
message: 'Unknown cipher',
|
||||
};
|
||||
assert.throws(() => privateKey.export(unknownCipher), unknownCipherError);
|
||||
assert.throws(() => generateKeyPairSync('ec', {
|
||||
...keyOptions,
|
||||
privateKeyEncoding: unknownCipher,
|
||||
}), unknownCipherError);
|
||||
assert.throws(() => generateKeyPair('ec', {
|
||||
...keyOptions,
|
||||
privateKeyEncoding: unknownCipher,
|
||||
}, common.mustNotCall()), unknownCipherError);
|
||||
|
||||
// Provider-only ciphers must reach the serializer, which still rejects ciphers
|
||||
// without an ASN.1 identifier when writing PKCS8.
|
||||
if (getCiphers().includes('aes-128-cbc-cts')) {
|
||||
for (const format of ['pem', 'der']) {
|
||||
assert.throws(() => privateKey.export({
|
||||
format,
|
||||
type: 'pkcs8',
|
||||
cipher: 'aes-128-cbc-cts',
|
||||
passphrase,
|
||||
}), {
|
||||
code: 'ERR_OSSL_ASN1_CIPHER_HAS_NO_OBJECT_IDENTIFIER',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user