Skip to content

Commit a54283a

Browse files
bnoordhuisruyadorno
authored andcommittedSep 22, 2022
crypto: fix weak randomness in WebCrypto keygen
Commit dae283d from August 2020 introduced a call to EntropySource() in SecretKeyGenTraits::DoKeyGen() in src/crypto/crypto_keygen.cc. There are two problems with that: 1. It does not check the return value, it assumes EntropySource() always succeeds, but it can (and sometimes will) fail. 2. The random data returned byEntropySource() may not be cryptographically strong and therefore not suitable as keying material. An example is a freshly booted system or a system without /dev/random or getrandom(2). EntropySource() calls out to openssl's RAND_poll() and RAND_bytes() in a best-effort attempt to obtain random data. OpenSSL has a built-in CSPRNG but that can fail to initialize, in which case it's possible either: 1. No random data gets written to the output buffer, i.e., the output is unmodified, or 2. Weak random data is written. It's theoretically possible for the output to be fully predictable because the CSPRNG starts from a predictable state. Replace EntropySource() and CheckEntropy() with new function CSPRNG() that enforces checking of the return value. Abort on startup when the entropy pool fails to initialize because that makes it too easy to compromise the security of the process. Refs: https://hackerone.com/bugs?report_id=1690000 Refs: #35093 Backport-PR-URL: nodejs-private/node-private#349 PR-URL: nodejs-private/node-private#346 Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> CVE-ID: CVE-2022-35255
1 parent 77fe2f3 commit a54283a

9 files changed

+64
-75
lines changed
 

‎node.gyp

+2
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,8 @@
627627
'openssl_default_cipher_list%': '',
628628
},
629629

630+
'cflags': ['-Werror=unused-result'],
631+
630632
'defines': [
631633
'NODE_ARCH="<(target_arch)"',
632634
'NODE_PLATFORM="<(OS)"',

‎src/crypto/crypto_context.cc

+11-8
Original file line numberDiff line numberDiff line change
@@ -541,9 +541,9 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
541541
// OpenSSL 1.1.0 changed the ticket key size, but the OpenSSL 1.0.x size was
542542
// exposed in the public API. To retain compatibility, install a callback
543543
// which restores the old algorithm.
544-
if (RAND_bytes(sc->ticket_key_name_, sizeof(sc->ticket_key_name_)) <= 0 ||
545-
RAND_bytes(sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_)) <= 0 ||
546-
RAND_bytes(sc->ticket_key_aes_, sizeof(sc->ticket_key_aes_)) <= 0) {
544+
if (CSPRNG(sc->ticket_key_name_, sizeof(sc->ticket_key_name_)).is_err() ||
545+
CSPRNG(sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_)).is_err() ||
546+
CSPRNG(sc->ticket_key_aes_, sizeof(sc->ticket_key_aes_)).is_err()) {
547547
return THROW_ERR_CRYPTO_OPERATION_FAILED(
548548
env, "Error generating ticket keys");
549549
}
@@ -1244,11 +1244,14 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl,
12441244

12451245
if (enc) {
12461246
memcpy(name, sc->ticket_key_name_, sizeof(sc->ticket_key_name_));
1247-
if (RAND_bytes(iv, 16) <= 0 ||
1248-
EVP_EncryptInit_ex(ectx, EVP_aes_128_cbc(), nullptr,
1249-
sc->ticket_key_aes_, iv) <= 0 ||
1250-
HMAC_Init_ex(hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_),
1251-
EVP_sha256(), nullptr) <= 0) {
1247+
if (CSPRNG(iv, 16).is_err() ||
1248+
EVP_EncryptInit_ex(
1249+
ectx, EVP_aes_128_cbc(), nullptr, sc->ticket_key_aes_, iv) <= 0 ||
1250+
HMAC_Init_ex(hctx,
1251+
sc->ticket_key_hmac_,
1252+
sizeof(sc->ticket_key_hmac_),
1253+
EVP_sha256(),
1254+
nullptr) <= 0) {
12521255
return -1;
12531256
}
12541257
return 1;

‎src/crypto/crypto_keygen.cc

+7-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,13 @@ KeyGenJobStatus SecretKeyGenTraits::DoKeyGen(
8484
SecretKeyGenConfig* params) {
8585
CHECK_LE(params->length, INT_MAX);
8686
params->out = MallocOpenSSL<char>(params->length);
87-
EntropySource(reinterpret_cast<unsigned char*>(params->out), params->length);
87+
if (CSPRNG(reinterpret_cast<unsigned char*>(params->out),
88+
params->length).is_err()) {
89+
OPENSSL_clear_free(params->out, params->length);
90+
params->out = nullptr;
91+
params->length = 0;
92+
return KeyGenJobStatus::FAILED;
93+
}
8894
return KeyGenJobStatus::OK;
8995
}
9096

‎src/crypto/crypto_keygen.h

-3
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,6 @@ class KeyGenJob final : public CryptoJob<KeyGenTraits> {
7575
std::move(params)) {}
7676

7777
void DoThreadPoolWork() override {
78-
// Make sure the CSPRNG is properly seeded so the results are secure.
79-
CheckEntropy();
80-
8178
AdditionalParams* params = CryptoJob<KeyGenTraits>::params();
8279

8380
switch (KeyGenTraits::DoKeyGen(AsyncWrap::env(), params)) {

‎src/crypto/crypto_random.cc

+7-8
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ bool RandomBytesTraits::DeriveBits(
6666
Environment* env,
6767
const RandomBytesConfig& params,
6868
ByteSource* unused) {
69-
CheckEntropy(); // Ensure that OpenSSL's PRNG is properly seeded.
70-
return RAND_bytes(params.buffer, params.size) != 0;
69+
return CSPRNG(params.buffer, params.size).is_ok();
7170
}
7271

7372
void RandomPrimeConfig::MemoryInfo(MemoryTracker* tracker) const {
@@ -156,12 +155,12 @@ Maybe<bool> RandomPrimeTraits::AdditionalConfig(
156155
return Just(true);
157156
}
158157

159-
bool RandomPrimeTraits::DeriveBits(
160-
Environment* env,
161-
const RandomPrimeConfig& params,
162-
ByteSource* unused) {
163-
164-
CheckEntropy();
158+
bool RandomPrimeTraits::DeriveBits(Environment* env,
159+
const RandomPrimeConfig& params,
160+
ByteSource* unused) {
161+
// BN_generate_prime_ex() calls RAND_bytes_ex() internally.
162+
// Make sure the CSPRNG is properly seeded.
163+
CHECK(CSPRNG(nullptr, 0).is_ok());
165164

166165
if (BN_generate_prime_ex(
167166
params.prime.get(),

‎src/crypto/crypto_util.cc

+8-20
Original file line numberDiff line numberDiff line change
@@ -60,26 +60,14 @@ int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx) {
6060
return 1;
6161
}
6262

63-
void CheckEntropy() {
64-
for (;;) {
65-
int status = RAND_status();
66-
CHECK_GE(status, 0); // Cannot fail.
67-
if (status != 0)
68-
break;
69-
70-
// Give up, RAND_poll() not supported.
71-
if (RAND_poll() == 0)
72-
break;
73-
}
74-
}
75-
76-
bool EntropySource(unsigned char* buffer, size_t length) {
77-
// Ensure that OpenSSL's PRNG is properly seeded.
78-
CheckEntropy();
79-
// RAND_bytes() can return 0 to indicate that the entropy data is not truly
80-
// random. That's okay, it's still better than V8's stock source of entropy,
81-
// which is /dev/urandom on UNIX platforms and the current time on Windows.
82-
return RAND_bytes(buffer, length) != -1;
63+
MUST_USE_RESULT CSPRNGResult CSPRNG(void* buffer, size_t length) {
64+
do {
65+
if (1 == RAND_status())
66+
if (1 == RAND_bytes(static_cast<unsigned char*>(buffer), length))
67+
return {true};
68+
} while (1 == RAND_poll());
69+
70+
return {false};
8371
}
8472

8573
int PasswordCallback(char* buf, int size, int rwflag, void* u) {

‎src/crypto/crypto_util.h

+12-25
Original file line numberDiff line numberDiff line change
@@ -110,31 +110,18 @@ struct MarkPopErrorOnReturn {
110110
~MarkPopErrorOnReturn() { ERR_pop_to_mark(); }
111111
};
112112

113-
// Ensure that OpenSSL has enough entropy (at least 256 bits) for its PRNG.
114-
// The entropy pool starts out empty and needs to fill up before the PRNG
115-
// can be used securely. Once the pool is filled, it never dries up again;
116-
// its contents is stirred and reused when necessary.
117-
//
118-
// OpenSSL normally fills the pool automatically but not when someone starts
119-
// generating random numbers before the pool is full: in that case OpenSSL
120-
// keeps lowering the entropy estimate to thwart attackers trying to guess
121-
// the initial state of the PRNG.
122-
//
123-
// When that happens, we will have to wait until enough entropy is available.
124-
// That should normally never take longer than a few milliseconds.
125-
//
126-
// OpenSSL draws from /dev/random and /dev/urandom. While /dev/random may
127-
// block pending "true" randomness, /dev/urandom is a CSPRNG that doesn't
128-
// block under normal circumstances.
129-
//
130-
// The only time when /dev/urandom may conceivably block is right after boot,
131-
// when the whole system is still low on entropy. That's not something we can
132-
// do anything about.
133-
void CheckEntropy();
134-
135-
// Generate length bytes of random data. If this returns false, the data
136-
// may not be truly random but it's still generally good enough.
137-
bool EntropySource(unsigned char* buffer, size_t length);
113+
struct CSPRNGResult {
114+
const bool ok;
115+
MUST_USE_RESULT bool is_ok() const { return ok; }
116+
MUST_USE_RESULT bool is_err() const { return !ok; }
117+
};
118+
119+
// Either succeeds with exactly |length| bytes of cryptographically
120+
// strong pseudo-random data, or fails. This function may block.
121+
// Don't assume anything about the contents of |buffer| on error.
122+
// As a special case, |length == 0| can be used to check if the CSPRNG
123+
// is properly seeded without consuming entropy.
124+
MUST_USE_RESULT CSPRNGResult CSPRNG(void* buffer, size_t length);
138125

139126
int PasswordCallback(char* buf, int size, int rwflag, void* u);
140127

‎src/inspector_io.cc

+1-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ std::string ScriptPath(uv_loop_t* loop, const std::string& script_name) {
4646
// Used ver 4 - with numbers
4747
std::string GenerateID() {
4848
uint16_t buffer[8];
49-
CHECK(crypto::EntropySource(reinterpret_cast<unsigned char*>(buffer),
50-
sizeof(buffer)));
49+
CHECK(crypto::CSPRNG(buffer, sizeof(buffer)).is_ok());
5150

5251
char uuid[256];
5352
snprintf(uuid, sizeof(uuid), "%04x%04x-%04x-%04x-%04x-%04x%04x%04x",

‎src/node.cc

+16-8
Original file line numberDiff line numberDiff line change
@@ -1092,11 +1092,10 @@ InitializationResult InitializeOncePerProcess(
10921092
// fipsinstall command. If the path to this file is incorrect no error
10931093
// will be reported.
10941094
//
1095-
// For Node.js this will mean that EntropySource will be called by V8 as
1096-
// part of its initialization process, and EntropySource will in turn call
1097-
// CheckEntropy. CheckEntropy will call RAND_status which will now always
1098-
// return 0, leading to an endless loop and the node process will appear to
1099-
// hang/freeze.
1095+
// For Node.js this will mean that CSPRNG() will be called by V8 as
1096+
// part of its initialization process, and CSPRNG() will in turn call
1097+
// call RAND_status which will now always return 0, leading to an endless
1098+
// loop and the node process will appear to hang/freeze.
11001099

11011100
// Passing NULL as the config file will allow the default openssl.cnf file
11021101
// to be loaded, but the default section in that file will not be used,
@@ -1156,14 +1155,23 @@ InitializationResult InitializeOncePerProcess(
11561155
}
11571156
#endif
11581157

1159-
// V8 on Windows doesn't have a good source of entropy. Seed it from
1160-
// OpenSSL's pool.
1161-
V8::SetEntropySource(crypto::EntropySource);
11621158
if (!crypto::ProcessFipsOptions()) {
11631159
return handle_openssl_error(ERR_GET_REASON(ERR_peek_error()),
11641160
fips_error_msg,
11651161
&result);
11661162
}
1163+
1164+
// Ensure CSPRNG is properly seeded.
1165+
CHECK(crypto::CSPRNG(nullptr, 0).is_ok());
1166+
1167+
V8::SetEntropySource([](unsigned char* buffer, size_t length) {
1168+
// V8 falls back to very weak entropy when this function fails
1169+
// and /dev/urandom isn't available. That wouldn't be so bad if
1170+
// the entropy was only used for Math.random() but it's also used for
1171+
// hash table and address space layout randomization. Better to abort.
1172+
CHECK(crypto::CSPRNG(buffer, length).is_ok());
1173+
return true;
1174+
});
11671175
#endif // HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL)
11681176
}
11691177
per_process::v8_platform.Initialize(

0 commit comments

Comments
 (0)
Please sign in to comment.