From de6dd67790a8bedbf90a8103a0ca874e224e287c Mon Sep 17 00:00:00 2001
From: Richard Lau <rlau@redhat.com>
Date: Thu, 19 Jan 2023 17:31:08 +0000
Subject: [PATCH] crypto: avoid hang when no algorithm available

Avoid an endless loop if no algorithm is available to seed the
cryptographically secure pseudorandom number generator (CSPRNG).

Co-authored-by: Anna Henningsen <anna@addaleax.net>
PR-URL: https://github.com/nodejs/node/pull/46237
Fixes: https://github.com/nodejs/node/issues/46200
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
---
 src/crypto/crypto_util.cc                 | 14 +++++++++
 test/fixtures/openssl3-conf/base_only.cnf | 12 +++++++
 test/parallel/test-crypto-no-algorithm.js | 38 +++++++++++++++++++++++
 3 files changed, 64 insertions(+)
 create mode 100644 test/fixtures/openssl3-conf/base_only.cnf
 create mode 100644 test/parallel/test-crypto-no-algorithm.js

diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc
index 33d0baa7cd86e2..59ae7f8be981a2 100644
--- a/src/crypto/crypto_util.cc
+++ b/src/crypto/crypto_util.cc
@@ -65,6 +65,20 @@ MUST_USE_RESULT CSPRNGResult CSPRNG(void* buffer, size_t length) {
     if (1 == RAND_status())
       if (1 == RAND_bytes(static_cast<unsigned char*>(buffer), length))
         return {true};
+#if OPENSSL_VERSION_MAJOR >= 3
+    const auto code = ERR_peek_last_error();
+    // A misconfigured OpenSSL 3 installation may report 1 from RAND_poll()
+    // and RAND_status() but fail in RAND_bytes() if it cannot look up
+    // a matching algorithm for the CSPRNG.
+    if (ERR_GET_LIB(code) == ERR_LIB_RAND) {
+      const auto reason = ERR_GET_REASON(code);
+      if (reason == RAND_R_ERROR_INSTANTIATING_DRBG ||
+          reason == RAND_R_UNABLE_TO_FETCH_DRBG ||
+          reason == RAND_R_UNABLE_TO_CREATE_DRBG) {
+        return {false};
+      }
+    }
+#endif
   } while (1 == RAND_poll());
 
   return {false};
diff --git a/test/fixtures/openssl3-conf/base_only.cnf b/test/fixtures/openssl3-conf/base_only.cnf
new file mode 100644
index 00000000000000..0a2e1bb4c15fbd
--- /dev/null
+++ b/test/fixtures/openssl3-conf/base_only.cnf
@@ -0,0 +1,12 @@
+nodejs_conf = nodejs_init
+
+[nodejs_init]
+providers = provider_sect
+
+# List of providers to load
+[provider_sect]
+base = base_sect
+
+[base_sect]
+activate = 1
+
diff --git a/test/parallel/test-crypto-no-algorithm.js b/test/parallel/test-crypto-no-algorithm.js
new file mode 100644
index 00000000000000..37db38cf613b65
--- /dev/null
+++ b/test/parallel/test-crypto-no-algorithm.js
@@ -0,0 +1,38 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+if (!common.hasOpenSSL3)
+  common.skip('this test requires OpenSSL 3.x');
+
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+
+if (common.isMainThread) {
+  // TODO(richardlau): Decide if `crypto.setFips` should error if the
+  // provider named "fips" is not available.
+  crypto.setFips(1);
+  crypto.randomBytes(20, common.mustCall((err) => {
+    // crypto.randomBytes should either succeed or fail but not hang.
+    if (err) {
+      assert.match(err.message, /digital envelope routines::unsupported/);
+      const expected = /random number generator::unable to fetch drbg/;
+      assert(err.opensslErrorStack.some((msg) => expected.test(msg)),
+             `did not find ${expected} in ${err.opensslErrorStack}`);
+    }
+  }));
+}
+
+{
+  // Startup test. Should not hang.
+  const { path } = require('../common/fixtures');
+  const { spawnSync } = require('node:child_process');
+  const baseConf = path('openssl3-conf', 'base_only.cnf');
+  const cp = spawnSync(process.execPath,
+                       [ `--openssl-config=${baseConf}`, '-p', '"hello"' ],
+                       { encoding: 'utf8' });
+  assert(common.nodeProcessAborted(cp.status, cp.signal),
+         `process did not abort, code:${cp.status} signal:${cp.signal}`);
+}