Skip to content

Commit 5cc36c3

Browse files
bnoordhuisRafaelGSS
authored andcommitted
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: nodejs#35093
1 parent 30ff0ff commit 5cc36c3

9 files changed

+58
-83
lines changed

node.gyp

+2
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,8 @@
681681
'openssl_default_cipher_list%': '',
682682
},
683683

684+
'cflags': ['-Werror=unused-result'],
685+
684686
'defines': [
685687
'NODE_ARCH="<(target_arch)"',
686688
'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
}
@@ -1241,11 +1241,14 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl,
12411241

12421242
if (enc) {
12431243
memcpy(name, sc->ticket_key_name_, sizeof(sc->ticket_key_name_));
1244-
if (RAND_bytes(iv, 16) <= 0 ||
1245-
EVP_EncryptInit_ex(ectx, EVP_aes_128_cbc(), nullptr,
1246-
sc->ticket_key_aes_, iv) <= 0 ||
1247-
HMAC_Init_ex(hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_),
1248-
EVP_sha256(), nullptr) <= 0) {
1244+
if (CSPRNG(iv, 16).is_err() ||
1245+
EVP_EncryptInit_ex(
1246+
ectx, EVP_aes_128_cbc(), nullptr, sc->ticket_key_aes_, iv) <= 0 ||
1247+
HMAC_Init_ex(hctx,
1248+
sc->ticket_key_hmac_,
1249+
sizeof(sc->ticket_key_hmac_),
1250+
EVP_sha256(),
1251+
nullptr) <= 0) {
12491252
return -1;
12501253
}
12511254
return 1;

src/crypto/crypto_keygen.cc

+2-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ KeyGenJobStatus SecretKeyGenTraits::DoKeyGen(
8080
SecretKeyGenConfig* params) {
8181
CHECK_LE(params->length, INT_MAX);
8282
ByteSource::Builder bytes(params->length);
83-
EntropySource(bytes.data<unsigned char>(), params->length);
83+
if (CSPRNG(bytes.data<unsigned char>(), params->length).is_err())
84+
return KeyGenJobStatus::FAILED;
8485
params->out = std::move(bytes).release();
8586
return KeyGenJobStatus::OK;
8687
}

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-26
Original file line numberDiff line numberDiff line change
@@ -60,32 +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-
// If RAND_bytes() returns 0 or -1, the data might not be random at all. In
80-
// that case, return false, which causes V8 to use its own entropy source. The
81-
// quality of V8's entropy source depends on multiple factors and we should
82-
// not assume that it is cryptographically secure (even though it often is).
83-
// However, even if RAND_bytes() fails and V8 resorts to its potentially weak
84-
// entropy source, it really does not matter much: V8 only uses the entropy
85-
// source to seed its own PRNG, which itself is not cryptographically secure.
86-
// In other words, even a cryptographically secure entropy source would not
87-
// guarantee cryptographically secure random numbers in V8.
88-
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};
8971
}
9072

9173
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
@@ -111,31 +111,18 @@ struct MarkPopErrorOnReturn {
111111
~MarkPopErrorOnReturn() { ERR_pop_to_mark(); }
112112
};
113113

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

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

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

+15-10
Original file line numberDiff line numberDiff line change
@@ -913,11 +913,10 @@ std::unique_ptr<InitializationResult> InitializeOncePerProcess(
913913
// fipsinstall command. If the path to this file is incorrect no error
914914
// will be reported.
915915
//
916-
// For Node.js this will mean that EntropySource will be called by V8 as
917-
// part of its initialization process, and EntropySource will in turn call
918-
// CheckEntropy. CheckEntropy will call RAND_status which will now always
919-
// return 0, leading to an endless loop and the node process will appear to
920-
// hang/freeze.
916+
// For Node.js this will mean that CSPRNG() will be called by V8 as
917+
// part of its initialization process, and CSPRNG() will in turn call
918+
// call RAND_status which will now always return 0, leading to an endless
919+
// loop and the node process will appear to hang/freeze.
921920

922921
// Passing NULL as the config file will allow the default openssl.cnf file
923922
// to be loaded, but the default section in that file will not be used,
@@ -975,11 +974,17 @@ std::unique_ptr<InitializationResult> InitializeOncePerProcess(
975974
return result;
976975
}
977976

978-
// Seed V8's PRNG from OpenSSL's pool. This is recommended by V8 and a
979-
// potentially better source of randomness than what V8 uses by default, but
980-
// it does not guarantee that pseudo-random values produced by V8 will be
981-
// cryptograhically secure.
982-
V8::SetEntropySource(crypto::EntropySource);
977+
// Ensure CSPRNG is properly seeded.
978+
CHECK(crypto::CSPRNG(nullptr, 0).is_ok());
979+
980+
V8::SetEntropySource([](unsigned char* buffer, size_t length) {
981+
// V8 falls back to very weak entropy when this function fails
982+
// and /dev/urandom isn't available. That wouldn't be so bad if
983+
// the entropy was only used for Math.random() but it's also used for
984+
// hash table and address space layout randomization. Better to abort.
985+
CHECK(crypto::CSPRNG(buffer, length).is_ok());
986+
return true;
987+
});
983988
#endif // HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL)
984989
}
985990

0 commit comments

Comments
 (0)