Skip to content

Commit c863c75

Browse files
panvasxa
authored andcommitted
src: add BoringSSL EVP enumeration fallback
BoringSSL declares EVP_CIPHER_do_all_sorted and EVP_MD_do_all_sorted, but stock no-decrepit builds do not provide those symbols. Add a Node build flag that keeps ncrypto and its dependents on a local BoringSSL fallback list when libdecrepit is absent. Keep embedders that provide the EVP enumeration symbols on the normal OpenSSL-compatible path, matching Electron's patched BoringSSL build. Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #63206 Backport-PR-URL: #63563 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 06d308f commit c863c75

9 files changed

Lines changed: 236 additions & 8 deletions

File tree

deps/ncrypto/ncrypto.cc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
#include <openssl/pkcs12.h>
88
#include <openssl/rand.h>
99
#include <openssl/x509v3.h>
10+
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
11+
#include <openssl/bytestring.h>
12+
#include <openssl/cipher.h>
13+
#include <openssl/pem.h>
14+
#endif
1015
#include <algorithm>
1116
#include <array>
1217
#include <cstring>
@@ -67,6 +72,28 @@ using NetscapeSPKIPointer = DeleteFnPtr<NETSCAPE_SPKI, NETSCAPE_SPKI_free>;
6772

6873
static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON =
6974
XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL;
75+
76+
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
77+
struct BoringSSLCipher {
78+
const EVP_CIPHER* (*get)();
79+
const char* name;
80+
};
81+
82+
constexpr BoringSSLCipher kBoringSSLCiphers[] = {
83+
{EVP_aes_128_cbc, "aes-128-cbc"}, {EVP_aes_128_ctr, "aes-128-ctr"},
84+
{EVP_aes_128_ecb, "aes-128-ecb"}, {EVP_aes_128_gcm, "aes-128-gcm"},
85+
{EVP_aes_128_ofb, "aes-128-ofb"}, {EVP_aes_192_cbc, "aes-192-cbc"},
86+
{EVP_aes_192_ctr, "aes-192-ctr"}, {EVP_aes_192_ecb, "aes-192-ecb"},
87+
{EVP_aes_192_gcm, "aes-192-gcm"}, {EVP_aes_192_ofb, "aes-192-ofb"},
88+
{EVP_aes_256_cbc, "aes-256-cbc"}, {EVP_aes_256_ctr, "aes-256-ctr"},
89+
{EVP_aes_256_ecb, "aes-256-ecb"}, {EVP_aes_256_gcm, "aes-256-gcm"},
90+
{EVP_aes_256_ofb, "aes-256-ofb"}, {EVP_des_cbc, "des-cbc"},
91+
{EVP_des_ecb, "des-ecb"}, {EVP_des_ede, "des-ede"},
92+
{EVP_des_ede3_cbc, "des-ede3-cbc"}, {EVP_des_ede_cbc, "des-ede-cbc"},
93+
{EVP_rc2_cbc, "rc2-cbc"}, {EVP_rc4, "rc4"},
94+
};
95+
96+
#endif
7097
} // namespace
7198

7299
// ============================================================================
@@ -4209,6 +4236,12 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) {
42094236
CipherCallbackContext context;
42104237
context.cb = std::move(callback);
42114238

4239+
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
4240+
for (const auto& cipher : kBoringSSLCiphers) {
4241+
static_cast<void>(cipher.get);
4242+
context.cb(cipher.name);
4243+
}
4244+
#else
42124245
EVP_CIPHER_do_all_sorted(
42134246
#if OPENSSL_VERSION_MAJOR >= 3
42144247
array_push_back<EVP_CIPHER,
@@ -4220,6 +4253,7 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) {
42204253
array_push_back<EVP_CIPHER>,
42214254
#endif
42224255
&context);
4256+
#endif
42234257
}
42244258

42254259
// ============================================================================

deps/ncrypto/ncrypto.gyp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
'variables': {
3+
'ncrypto_bssl_libdecrepit_missing%': 1,
34
'ncrypto_sources': [
45
'engine.cc',
56
'ncrypto.cc',
@@ -11,8 +12,14 @@
1112
'target_name': 'ncrypto',
1213
'type': 'static_library',
1314
'include_dirs': ['.'],
15+
'defines': [
16+
'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)',
17+
],
1418
'direct_dependent_settings': {
1519
'include_dirs': ['.'],
20+
'defines': [
21+
'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)',
22+
],
1623
},
1724
'sources': [ '<@(ncrypto_sources)' ],
1825
'conditions': [

deps/ncrypto/ncrypto.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@
2222
#ifndef OPENSSL_NO_ENGINE
2323
#include <openssl/engine.h>
2424
#endif // !OPENSSL_NO_ENGINE
25+
26+
#ifndef OPENSSL_VERSION_PREREQ
27+
#define OPENSSL_VERSION_PREREQ(maj, min) \
28+
(OPENSSL_VERSION_NUMBER >= (((maj) << 28) | ((min) << 20)))
29+
#endif
30+
31+
// BoringSSL declares the EVP_*_do_all* APIs, but their implementation may
32+
// live in libdecrepit. This matches standalone ncrypto's build flag.
33+
#ifndef NCRYPTO_BSSL_LIBDECREPIT_MISSING
34+
#define NCRYPTO_BSSL_LIBDECREPIT_MISSING 0
35+
#endif
36+
37+
#if defined(OPENSSL_IS_BORINGSSL) && NCRYPTO_BSSL_LIBDECREPIT_MISSING
38+
#define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 1
39+
#else
40+
#define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 0
41+
#endif
42+
2543
// The FIPS-related functions are only available
2644
// when the OpenSSL itself was compiled with FIPS support.
2745
#if defined(OPENSSL_FIPS) && OPENSSL_VERSION_MAJOR < 3

lib/internal/crypto/keys.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,10 @@ function prepareAsymmetricKey(key, ctx, name = 'key') {
682682
return { data, format: kKeyFormatJWK };
683683
} else if (format === 'raw-public' || format === 'raw-private' ||
684684
format === 'raw-seed') {
685+
if ((ctx === kConsumePrivate || ctx === kCreatePrivate) &&
686+
format === 'raw-public') {
687+
throw new ERR_INVALID_ARG_VALUE(`${name}.format`, format);
688+
}
685689
if (!isArrayBufferView(data) && !isAnyArrayBuffer(data)) {
686690
throw new ERR_INVALID_ARG_TYPE(
687691
`${name}.key`,

src/crypto/crypto_hash.cc

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
#include "threadpoolwork-inl.h"
88
#include "v8.h"
99

10+
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
11+
#include <openssl/digest.h>
12+
#endif
13+
1014
#include <cstdio>
1115

1216
namespace node {
@@ -41,6 +45,24 @@ void Hash::MemoryInfo(MemoryTracker* tracker) const {
4145
tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0);
4246
}
4347

48+
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
49+
struct BoringSSLDigest {
50+
const EVP_MD* (*get)();
51+
const char* name;
52+
};
53+
54+
constexpr BoringSSLDigest kBoringSSLDigests[] = {
55+
{EVP_md4, "md4"},
56+
{EVP_md5, "md5"},
57+
{EVP_sha1, "sha1"},
58+
{EVP_sha224, "sha224"},
59+
{EVP_sha256, "sha256"},
60+
{EVP_sha384, "sha384"},
61+
{EVP_sha512, "sha512"},
62+
{EVP_sha512_256, "sha512-256"},
63+
};
64+
#endif
65+
4466
#if OPENSSL_VERSION_MAJOR >= 3
4567
void PushAliases(const char* name, void* data) {
4668
static_cast<std::vector<std::string>*>(data)->push_back(name);
@@ -122,7 +144,12 @@ void SaveSupportedHashAlgorithms(const EVP_MD* md,
122144
const std::vector<std::string>& GetSupportedHashAlgorithms(Environment* env) {
123145
if (env->supported_hash_algorithms.empty()) {
124146
MarkPopErrorOnReturn mark_pop_error_on_return;
125-
#if OPENSSL_VERSION_MAJOR >= 3
147+
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
148+
for (const auto& digest : kBoringSSLDigests) {
149+
static_cast<void>(digest.get);
150+
env->supported_hash_algorithms.emplace_back(digest.name);
151+
}
152+
#elif OPENSSL_VERSION_MAJOR >= 3
126153
// Since we'll fetch the EVP_MD*, cache them along the way to speed up
127154
// later lookups instead of throwing them away immediately.
128155
EVP_MD_do_all_sorted(SaveSupportedHashAlgorithmsAndCacheMD, env);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
if (!process.features.openssl_is_boringssl)
8+
common.skip('BoringSSL-only test');
9+
10+
const assert = require('assert');
11+
const { getCiphers, getHashes } = require('crypto');
12+
13+
const ciphers = getCiphers();
14+
[
15+
'aes-128-cbc',
16+
'aes-256-gcm',
17+
'des-ede',
18+
'des-ede-cbc',
19+
'des-ede3-cbc',
20+
'rc2-cbc',
21+
'rc4',
22+
].forEach((cipher) => assert(ciphers.includes(cipher), cipher));
23+
24+
const hashes = getHashes();
25+
[
26+
'md4',
27+
'md5',
28+
'sha1',
29+
'sha256',
30+
'sha512-256',
31+
].forEach((hash) => assert(hashes.includes(hash), hash));

test/parallel/test-crypto-key-objects-raw.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,47 @@ const { hasOpenSSL } = require('../common/crypto');
5959
}
6060
}
6161

62+
// Raw public keys cannot be imported as private keys.
63+
{
64+
const rawPublicKeys = [
65+
['ec', 'ec_p256_public.pem', { namedCurve: 'P-256' }],
66+
['ed25519', 'ed25519_public.pem'],
67+
['x25519', 'x25519_public.pem'],
68+
];
69+
70+
if (!process.features.openssl_is_boringssl) {
71+
rawPublicKeys.push(
72+
['ed448', 'ed448_public.pem'],
73+
['x448', 'x448_public.pem'],
74+
);
75+
} else {
76+
common.printSkipMessage('Skipping unsupported ed448/x448 test cases');
77+
}
78+
79+
if (hasOpenSSL(3, 5)) {
80+
rawPublicKeys.push(
81+
['ml-dsa-44', 'ml_dsa_44_public.pem'],
82+
['ml-kem-768', 'ml_kem_768_public.pem'],
83+
);
84+
}
85+
86+
if (hasOpenSSL(3, 5)) {
87+
rawPublicKeys.push(
88+
['slh-dsa-sha2-128f', 'slh_dsa_sha2_128f_public.pem'],
89+
);
90+
}
91+
92+
for (const [asymmetricKeyType, fixture, options = {}] of rawPublicKeys) {
93+
const publicKey = crypto.createPublicKey(fixtures.readKey(fixture, 'ascii'));
94+
assert.throws(() => crypto.createPrivateKey({
95+
key: publicKey.export({ format: 'raw-public' }),
96+
format: 'raw-public',
97+
asymmetricKeyType,
98+
...options,
99+
}), { code: 'ERR_INVALID_ARG_VALUE' });
100+
}
101+
}
102+
62103
// Raw seed imports do not support strings.
63104
if (hasOpenSSL(3, 5)) {
64105
const privKeyObj = crypto.createPrivateKey(
@@ -116,7 +157,11 @@ if (hasOpenSSL(3, 5)) {
116157
for (const format of ['raw-public', 'raw-private', 'raw-seed']) {
117158
assert.throws(() => crypto.createPrivateKey({
118159
key: Buffer.alloc(32), format, asymmetricKeyType: 'dh',
119-
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
160+
}), {
161+
code: format === 'raw-public' ?
162+
'ERR_INVALID_ARG_VALUE' :
163+
'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS',
164+
});
120165
}
121166
} else {
122167
common.printSkipMessage('Skipping unsupported dh test case');

test/parallel/test-crypto-pqc-key-objects-ml-kem.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ const common = require('../common');
44
if (!common.hasCrypto)
55
common.skip('missing crypto');
66

7+
if (process.features.openssl_is_boringssl) {
8+
common.skip('Skipping unsupported ML-KEM key tests');
9+
}
10+
711
const { hasOpenSSL } = require('../common/crypto');
812

913
const assert = require('assert');

test/wpt/status/WebCryptoAPI.cjs

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@ const { hasOpenSSL } = require('../../common/crypto.js');
66

77
const s390x = os.arch() === 's390x';
88

9-
const conditionalSkips = {};
9+
const conditionalFileSkips = {};
10+
const conditionalSubtestSkips = {};
1011

1112
function skip(...files) {
1213
for (const file of files) {
13-
conditionalSkips[file] = {
14-
'skip': `Unsupported in OpenSSL ${process.versions.openssl}`,
14+
conditionalFileSkips[file] = {
15+
'skip': 'Unsupported in ' + (process.features.openssl_is_boringssl ? 'BoringSSL' : `OpenSSL ${process.versions.openssl}`),
1516
};
1617
}
1718
}
1819

20+
function skipSubtests(...entries) {
21+
for (const [file, regexp] of entries) {
22+
conditionalSubtestSkips[file] ||= {
23+
'skipTests': [],
24+
};
25+
26+
conditionalSubtestSkips[file].skipTests.push(regexp);
27+
}
28+
}
29+
1930
if (!hasOpenSSL(3, 0)) {
2031
skip(
2132
'encrypt_decrypt/aes_ocb.tentative.https.any.js',
@@ -45,8 +56,54 @@ if (!hasOpenSSL(3, 5)) {
4556
'import_export/ML-DSA_importKey.tentative.https.any.js',
4657
'import_export/ML-KEM_importKey.tentative.https.any.js',
4758
'sign_verify/mldsa.tentative.https.any.js');
59+
60+
skipSubtests(
61+
['supports-modern.tentative.https.any.js', /ml-(?:kem|dsa)/i]);
4862
}
4963

64+
if (process.features.openssl_is_boringssl) {
65+
skip(
66+
'derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js',
67+
'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js',
68+
'digest/cshake.tentative.https.any.js',
69+
'digest/sha3.tentative.https.any.js',
70+
'encrypt_decrypt/chacha20_poly1305.tentative.https.any.js',
71+
'generateKey/failures_AES-KW.https.any.js',
72+
'generateKey/failures_Ed448.tentative.https.any.js',
73+
'generateKey/failures_X448.tentative.https.any.js',
74+
'generateKey/failures_chacha20_poly1305.tentative.https.any.js',
75+
'generateKey/successes_AES-KW.https.any.js',
76+
'generateKey/successes_Ed448.tentative.https.any.js',
77+
'generateKey/successes_X448.tentative.https.any.js',
78+
'generateKey/successes_chacha20_poly1305.tentative.https.any.js',
79+
'import_export/ChaCha20-Poly1305_importKey.tentative.https.any.js',
80+
'import_export/okp_importKey_Ed448.tentative.https.any.js',
81+
'import_export/okp_importKey_failures_Ed448.tentative.https.any.js',
82+
'import_export/okp_importKey_failures_X448.tentative.https.any.js',
83+
'import_export/okp_importKey_X448.tentative.https.any.js',
84+
'sign_verify/eddsa_curve448.tentative.https.any.js');
85+
86+
skipSubtests(
87+
['derive_bits_keys/hkdf.https.any.js', /AES-KW/],
88+
['derive_bits_keys/pbkdf2.https.any.js', /AES-KW/],
89+
['import_export/raw_format_aliases.tentative.https.any.js', /AES-KW/],
90+
['import_export/symmetric_importKey.https.any.js', /AES-KW/],
91+
['supports.tentative.https.any.js', /AES-KW/],
92+
['supports-modern.tentative.https.any.js', /ChaCha20-Poly1305/],
93+
['supports-modern.tentative.https.any.js', /^supports returns true for algorithm objects with valid parameters$/]);
94+
}
95+
96+
function assertNoOverlap(fileSkips, subtestSkips) {
97+
const subtestSkipFiles = new Set(Object.keys(subtestSkips));
98+
const overlap = Object.keys(fileSkips).filter((file) => subtestSkipFiles.has(file));
99+
100+
if (overlap.length !== 0) {
101+
throw new Error(`conditionalFileSkips and conditionalSubtestSkips overlap: ${overlap.join(', ')}`);
102+
}
103+
}
104+
105+
assertNoOverlap(conditionalFileSkips, conditionalSubtestSkips);
106+
50107
const cshakeExpectedFailures = ['cSHAKE128', 'cSHAKE256'].flatMap((algorithm) => {
51108
return [0, 256, 384, 512].flatMap((length) => {
52109
return ['empty', 'short', 'medium'].flatMap((size) => {
@@ -95,7 +152,8 @@ const kmacExpectedFailures = kmacVectorNames.flatMap((name) => {
95152
});
96153

97154
module.exports = {
98-
...conditionalSkips,
155+
...conditionalFileSkips,
156+
...conditionalSubtestSkips,
99157
'algorithm-discards-context.https.window.js': {
100158
'skip': 'Not relevant in Node.js context',
101159
},
@@ -133,13 +191,13 @@ module.exports = {
133191
],
134192
},
135193
},
136-
'digest/cshake.tentative.https.any.js': {
194+
'digest/cshake.tentative.https.any.js': conditionalFileSkips['digest/cshake.tentative.https.any.js'] ?? {
137195
'fail': {
138196
'note': 'WPT still uses CShakeParams.length; implementation moved to CShakeParams.outputLength',
139197
'expected': cshakeExpectedFailures,
140198
},
141199
},
142-
'sign_verify/kmac.tentative.https.any.js': conditionalSkips['sign_verify/kmac.tentative.https.any.js'] ?? {
200+
'sign_verify/kmac.tentative.https.any.js': conditionalFileSkips['sign_verify/kmac.tentative.https.any.js'] ?? {
143201
'fail': {
144202
'note': 'WPT still uses KmacParams.length; implementation moved to KmacParams.outputLength',
145203
'expected': kmacExpectedFailures,

0 commit comments

Comments
 (0)