mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
fix: apply the certificate SPKI blocklist to Node and add entries (#55126)
Chromium carries a compiled-in list of SubjectPublicKeyInfo hashes that must never appear in a verified chain. Electron's Node had no equivalent, so the same chain could fail in a renderer and succeed in tls/https/fetch from the main or a utility process. - Node: wrap X509_verify_cert on every SecureContext and report CERT_REVOKED when a chain contains a blocked SPKI. - Chromium: add entries that Chrome distributes via CRLSet, which Electron does not consume, to the compiled-in list; Node's list mirrors them. - Testing binding hooks let the new spec extend Node's list at runtime and exercise the hook against the fixture chain.
This commit is contained in:
1 parent
23c730b8b8
commit
44fdb9fee4
6 files changed
+343
No files matched your search
@@ -139,3 +139,4 @@ build_drop_the_unused_chrome_browser_win_dep_from_color_mixers.patch
|
||||
fix_plumb_the_requesting_frame_through_pointer_lock_requests.patch
|
||||
fix_avoid_misaligned_char16_t_reads_in_crdtp_string_deserialization.patch
|
||||
feat_let_the_host_of_a_remote_nswindow_set_more_of_its_behavior.patch
|
||||
add_entries_to_the_spki_blocklist.patch
|
||||
@@ -0,0 +1,57 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Sam Attard <sattard@anthropic.com>
|
||||
Date: Tue, 20 Oct 2026 00:00:20 +0000
|
||||
Subject: Add entries to the SPKI blocklist
|
||||
|
||||
Adds public key hashes that Chrome ships to its users through the CRLSet
|
||||
component, which Electron does not consume, so they are carried in the
|
||||
compiled-in list instead. The comment above each entry is the SHA-256
|
||||
fingerprint of the corresponding certificate.
|
||||
|
||||
This patch can be removed once the hashes land in the upstream blocklist.
|
||||
|
||||
diff --git a/net/cert/cert_verify_proc_blocklist.inc b/net/cert/cert_verify_proc_blocklist.inc
|
||||
index b00fc5124ba8c70d71d1a0bc535d6db3c3f121cf..fb7920b263780b8e58cdce9018732723e80f73eb 100644
|
||||
--- a/net/cert/cert_verify_proc_blocklist.inc
|
||||
+++ b/net/cert/cert_verify_proc_blocklist.inc
|
||||
@@ -418,7 +418,39 @@ static constexpr uint8_t
|
||||
// 289ed1676f42b679f2b665bb5a3f25610d9d19ac017beafa46787f1d6022986a.pem
|
||||
{0x36, 0xce, 0x75, 0xed, 0xea, 0x80, 0x6f, 0x09, 0x85, 0x14, 0x56,
|
||||
0x83, 0x29, 0x4e, 0x54, 0xdb, 0xdf, 0xf0, 0x2f, 0xc8, 0x21, 0x87,
|
||||
- 0x0c, 0xf5, 0x06, 0x7a, 0x30, 0x8d, 0x15, 0x04, 0xbe, 0x3a}
|
||||
+ 0x0c, 0xf5, 0x06, 0x7a, 0x30, 0x8d, 0x15, 0x04, 0xbe, 0x3a},
|
||||
+ // 0164d7ceae5bb909c272af151ff44b1177ae9b47d9424b94f1d9558c30934d19.pem
|
||||
+ {0x49, 0xdd, 0x35, 0x3d, 0xc2, 0xb9, 0x19, 0xa3, 0x77, 0x06, 0x45,
|
||||
+ 0x11, 0x7d, 0x0e, 0x60, 0xc1, 0x61, 0x15, 0xe7, 0xd1, 0xbc, 0x25,
|
||||
+ 0x9e, 0xef, 0xe7, 0x34, 0xb0, 0x0e, 0xa4, 0x34, 0xfe, 0x5f},
|
||||
+ // 432ec25f0d28de9006d97bf752f17bbb2cd41f669933fcb072d9a1148d0261f3.pem
|
||||
+ {0x5c, 0x06, 0x60, 0xa2, 0xb1, 0x64, 0x91, 0x0d, 0x8f, 0xf5, 0x89,
|
||||
+ 0xc1, 0x8e, 0x14, 0x55, 0x2e, 0x9e, 0x0e, 0xc2, 0x37, 0xdc, 0xef,
|
||||
+ 0xe2, 0xcb, 0x73, 0x6d, 0x41, 0xa4, 0x81, 0x1f, 0x81, 0xa0},
|
||||
+ // cb6e15165ac802e9b3f993f16c25d41f4e81c21ebde71ae1b012ac00bf9c7c3f.pem
|
||||
+ {0x63, 0xec, 0xef, 0xee, 0x43, 0x1d, 0x49, 0x52, 0x3b, 0xc1, 0x13,
|
||||
+ 0x6c, 0x4e, 0x72, 0xd0, 0x20, 0x6a, 0x47, 0x70, 0x86, 0xca, 0xf6,
|
||||
+ 0x06, 0xcd, 0x5c, 0xf0, 0x25, 0x68, 0x6c, 0x10, 0x9c, 0xf8},
|
||||
+ // 4b2460a466f95fd40113bcdcca2ceb29b3b093921de45474da7df48874c630b1.pem
|
||||
+ {0x92, 0xc0, 0x63, 0xd6, 0x7e, 0x8f, 0x14, 0xdb, 0x71, 0xd2, 0x88,
|
||||
+ 0xe1, 0x12, 0x66, 0xff, 0xda, 0xf6, 0x9c, 0xbf, 0x11, 0x5f, 0x7b,
|
||||
+ 0xab, 0x14, 0xf3, 0xbc, 0x2f, 0x68, 0xd2, 0x5e, 0x0e, 0x30},
|
||||
+ // 42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493.pem
|
||||
+ {0xeb, 0xed, 0x64, 0x75, 0xc2, 0xc2, 0xd8, 0x0f, 0xcb, 0x19, 0x9f,
|
||||
+ 0x3f, 0xe8, 0xdd, 0x6e, 0x6b, 0x3c, 0xe6, 0x0b, 0x77, 0xeb, 0x15,
|
||||
+ 0xd4, 0xe4, 0x2c, 0x3d, 0xec, 0x0c, 0x6f, 0x5b, 0x51, 0x05},
|
||||
+ // 9e39de2881273acc225a149c72ba3e0f51424963669ab633ee1dc7df5db067b4.pem
|
||||
+ {0xec, 0x4f, 0xfa, 0x1a, 0xd0, 0xac, 0x88, 0x08, 0x13, 0x14, 0xb7,
|
||||
+ 0xaa, 0xff, 0xef, 0x6a, 0x91, 0xf9, 0x7c, 0xd5, 0x72, 0x91, 0xbb,
|
||||
+ 0x5b, 0xa6, 0x86, 0xc5, 0x41, 0x5f, 0x5e, 0xb4, 0xf6, 0xb6},
|
||||
+ // 98b896625c0c230adeec3eb2cf6aad5001ecf9156a68644e82374ce5833bc25b.pem
|
||||
+ {0x43, 0xbf, 0xfa, 0x19, 0xd6, 0xef, 0x65, 0x14, 0x51, 0xf6, 0x84,
|
||||
+ 0x1e, 0x01, 0x99, 0x31, 0xe9, 0x6f, 0x6f, 0x2e, 0xae, 0xff, 0x28,
|
||||
+ 0xab, 0x4b, 0x6f, 0x1c, 0xe4, 0x57, 0x6d, 0xe2, 0x71, 0x7b},
|
||||
+ // e7568fe6e12331f4fd7de61da4c7559d0f8eb192a218d4741a3ba75184333838.pem
|
||||
+ {0xe0, 0x25, 0x4c, 0x7f, 0xcd, 0xc2, 0x65, 0xe2, 0xf0, 0xf8, 0xa4,
|
||||
+ 0x28, 0xda, 0xbb, 0x75, 0xab, 0x4a, 0xe0, 0x39, 0xb4, 0x87, 0x2d,
|
||||
+ 0x62, 0xf5, 0xee, 0x57, 0xf0, 0x87, 0x7d, 0x5a, 0x8a, 0x41}
|
||||
};
|
||||
|
||||
// Hashes of SubjectPublicKeyInfos known to be used for interception by a
|
||||
@@ -51,3 +51,4 @@ deps_add_missing_standard_library_includes_to_simdjson.patch
|
||||
test_drop_removed_wasm_rab_integration_flag.patch
|
||||
test_skip_rmsync_permission_error_test_when_running_as_root.patch
|
||||
test_skip_openssl_config_test_under_boringssl.patch
|
||||
tls_reject_chains_containing_blocked_public_keys.patch
|
||||
@@ -0,0 +1,167 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Sam Attard <sattard@anthropic.com>
|
||||
Date: Tue, 20 Oct 2026 00:10:09 +0000
|
||||
Subject: tls: reject chains containing blocked public keys
|
||||
|
||||
Node has no equivalent of Chromium's SPKI blocklist, so a chain through a
|
||||
key on that list still verifies in Node as long as it reaches a trusted
|
||||
root. Wrap X509_verify_cert for every SecureContext and report
|
||||
X509_V_ERR_CERT_REVOKED when any certificate in the verified chain (or,
|
||||
if no chain could be built, any certificate the peer sent) has a
|
||||
SubjectPublicKeyInfo hash on a small compiled-in list. The list mirrors
|
||||
the entries Electron adds to Chromium's net blocklist. Test-only hooks
|
||||
allow the spec suite to extend the list at runtime.
|
||||
|
||||
diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc
|
||||
index 4d6d9accd1970eca3ab065884e445d662e9f0c5a..13313c07f02b5846e4a3bc7b2fa32dcc63d73c99 100644
|
||||
--- a/src/crypto/crypto_context.cc
|
||||
+++ b/src/crypto/crypto_context.cc
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <openssl/comp.h>
|
||||
#endif
|
||||
#include <openssl/rand.h>
|
||||
+#include <openssl/sha.h>
|
||||
#include <openssl/x509.h>
|
||||
#ifdef __APPLE__
|
||||
#include <Security/Security.h>
|
||||
@@ -27,7 +28,10 @@
|
||||
#include <wincrypt.h>
|
||||
#endif
|
||||
|
||||
+#include <array>
|
||||
+#include <cstring>
|
||||
#include <set>
|
||||
+#include <vector>
|
||||
|
||||
namespace node {
|
||||
|
||||
@@ -75,6 +79,105 @@ using v8::String;
|
||||
using v8::Value;
|
||||
|
||||
namespace crypto {
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+// SHA-256 hashes of SubjectPublicKeyInfos that must never appear in a
|
||||
+// verified chain, regardless of what the trust store says.
|
||||
+constexpr uint8_t kBlockedSPKIs[][SHA256_DIGEST_LENGTH] = {
|
||||
+ // 0164d7ceae5bb909c272af151ff44b1177ae9b47d9424b94f1d9558c30934d19
|
||||
+ {0x49, 0xdd, 0x35, 0x3d, 0xc2, 0xb9, 0x19, 0xa3, 0x77, 0x06, 0x45,
|
||||
+ 0x11, 0x7d, 0x0e, 0x60, 0xc1, 0x61, 0x15, 0xe7, 0xd1, 0xbc, 0x25,
|
||||
+ 0x9e, 0xef, 0xe7, 0x34, 0xb0, 0x0e, 0xa4, 0x34, 0xfe, 0x5f},
|
||||
+ // 432ec25f0d28de9006d97bf752f17bbb2cd41f669933fcb072d9a1148d0261f3
|
||||
+ {0x5c, 0x06, 0x60, 0xa2, 0xb1, 0x64, 0x91, 0x0d, 0x8f, 0xf5, 0x89,
|
||||
+ 0xc1, 0x8e, 0x14, 0x55, 0x2e, 0x9e, 0x0e, 0xc2, 0x37, 0xdc, 0xef,
|
||||
+ 0xe2, 0xcb, 0x73, 0x6d, 0x41, 0xa4, 0x81, 0x1f, 0x81, 0xa0},
|
||||
+ // cb6e15165ac802e9b3f993f16c25d41f4e81c21ebde71ae1b012ac00bf9c7c3f
|
||||
+ {0x63, 0xec, 0xef, 0xee, 0x43, 0x1d, 0x49, 0x52, 0x3b, 0xc1, 0x13,
|
||||
+ 0x6c, 0x4e, 0x72, 0xd0, 0x20, 0x6a, 0x47, 0x70, 0x86, 0xca, 0xf6,
|
||||
+ 0x06, 0xcd, 0x5c, 0xf0, 0x25, 0x68, 0x6c, 0x10, 0x9c, 0xf8},
|
||||
+ // 4b2460a466f95fd40113bcdcca2ceb29b3b093921de45474da7df48874c630b1
|
||||
+ {0x92, 0xc0, 0x63, 0xd6, 0x7e, 0x8f, 0x14, 0xdb, 0x71, 0xd2, 0x88,
|
||||
+ 0xe1, 0x12, 0x66, 0xff, 0xda, 0xf6, 0x9c, 0xbf, 0x11, 0x5f, 0x7b,
|
||||
+ 0xab, 0x14, 0xf3, 0xbc, 0x2f, 0x68, 0xd2, 0x5e, 0x0e, 0x30},
|
||||
+ // 42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493
|
||||
+ {0xeb, 0xed, 0x64, 0x75, 0xc2, 0xc2, 0xd8, 0x0f, 0xcb, 0x19, 0x9f,
|
||||
+ 0x3f, 0xe8, 0xdd, 0x6e, 0x6b, 0x3c, 0xe6, 0x0b, 0x77, 0xeb, 0x15,
|
||||
+ 0xd4, 0xe4, 0x2c, 0x3d, 0xec, 0x0c, 0x6f, 0x5b, 0x51, 0x05},
|
||||
+ // 9e39de2881273acc225a149c72ba3e0f51424963669ab633ee1dc7df5db067b4
|
||||
+ {0xec, 0x4f, 0xfa, 0x1a, 0xd0, 0xac, 0x88, 0x08, 0x13, 0x14, 0xb7,
|
||||
+ 0xaa, 0xff, 0xef, 0x6a, 0x91, 0xf9, 0x7c, 0xd5, 0x72, 0x91, 0xbb,
|
||||
+ 0x5b, 0xa6, 0x86, 0xc5, 0x41, 0x5f, 0x5e, 0xb4, 0xf6, 0xb6},
|
||||
+ // 98b896625c0c230adeec3eb2cf6aad5001ecf9156a68644e82374ce5833bc25b
|
||||
+ {0x43, 0xbf, 0xfa, 0x19, 0xd6, 0xef, 0x65, 0x14, 0x51, 0xf6, 0x84,
|
||||
+ 0x1e, 0x01, 0x99, 0x31, 0xe9, 0x6f, 0x6f, 0x2e, 0xae, 0xff, 0x28,
|
||||
+ 0xab, 0x4b, 0x6f, 0x1c, 0xe4, 0x57, 0x6d, 0xe2, 0x71, 0x7b},
|
||||
+ // e7568fe6e12331f4fd7de61da4c7559d0f8eb192a218d4741a3ba75184333838
|
||||
+ {0xe0, 0x25, 0x4c, 0x7f, 0xcd, 0xc2, 0x65, 0xe2, 0xf0, 0xf8, 0xa4,
|
||||
+ 0x28, 0xda, 0xbb, 0x75, 0xab, 0x4a, 0xe0, 0x39, 0xb4, 0x87, 0x2d,
|
||||
+ 0x62, 0xf5, 0xee, 0x57, 0xf0, 0x87, 0x7d, 0x5a, 0x8a, 0x41},
|
||||
+};
|
||||
+
|
||||
+std::vector<std::array<uint8_t, SHA256_DIGEST_LENGTH>>&
|
||||
+BlockedSPKIsForTesting() {
|
||||
+ static std::vector<std::array<uint8_t, SHA256_DIGEST_LENGTH>> list;
|
||||
+ return list;
|
||||
+}
|
||||
+
|
||||
+bool IsBlockedSPKI(const X509* cert) {
|
||||
+ if (cert == nullptr) return false;
|
||||
+ uint8_t* spki = nullptr;
|
||||
+ int spki_len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), &spki);
|
||||
+ if (spki_len <= 0) return false;
|
||||
+ uint8_t digest[SHA256_DIGEST_LENGTH];
|
||||
+ SHA256(spki, spki_len, digest);
|
||||
+ OPENSSL_free(spki);
|
||||
+ for (const auto& blocked : kBlockedSPKIs) {
|
||||
+ if (memcmp(digest, blocked, sizeof(digest)) == 0) return true;
|
||||
+ }
|
||||
+ for (const auto& blocked : BlockedSPKIsForTesting()) {
|
||||
+ if (memcmp(digest, blocked.data(), sizeof(digest)) == 0) return true;
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+bool ContainsBlockedSPKI(const STACK_OF(X509)* certs) {
|
||||
+ if (certs == nullptr) return false;
|
||||
+ for (size_t i = 0; i < sk_X509_num(certs); i++) {
|
||||
+ if (IsBlockedSPKI(sk_X509_value(certs, i))) return true;
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+// Wraps X509_verify_cert so that a chain through a blocked key is reported
|
||||
+// as revoked even when the trust store would otherwise accept it. When no
|
||||
+// verified chain is available every certificate the peer sent is checked.
|
||||
+int CertVerifyCallback(X509_STORE_CTX* ctx, void* arg) {
|
||||
+ int ok = X509_verify_cert(ctx);
|
||||
+ bool blocked = ok == 1
|
||||
+ ? ContainsBlockedSPKI(X509_STORE_CTX_get0_chain(ctx))
|
||||
+ : IsBlockedSPKI(X509_STORE_CTX_get0_cert(ctx)) ||
|
||||
+ ContainsBlockedSPKI(X509_STORE_CTX_get0_untrusted(ctx));
|
||||
+ if (blocked) {
|
||||
+ X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_REVOKED);
|
||||
+ return 0;
|
||||
+ }
|
||||
+ return ok;
|
||||
+}
|
||||
+
|
||||
+} // namespace
|
||||
+
|
||||
+void AddBlockedSPKIForTesting(const uint8_t* sha256_hash) {
|
||||
+ std::array<uint8_t, SHA256_DIGEST_LENGTH> entry;
|
||||
+ memcpy(entry.data(), sha256_hash, entry.size());
|
||||
+ BlockedSPKIsForTesting().push_back(entry);
|
||||
+}
|
||||
+
|
||||
+void ClearBlockedSPKIsForTesting() {
|
||||
+ BlockedSPKIsForTesting().clear();
|
||||
+}
|
||||
+
|
||||
static const char* const root_certs[] = {
|
||||
#include "node_root_certs.h" // NOLINT(build/include_order)
|
||||
};
|
||||
@@ -1631,6 +1734,7 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
|
||||
|
||||
env->external_memory_accounter()->Increase(env->isolate(), kExternalSize);
|
||||
SSL_CTX_set_app_data(sc->ctx_.get(), sc);
|
||||
+ SSL_CTX_set_cert_verify_callback(sc->ctx_.get(), CertVerifyCallback, nullptr);
|
||||
|
||||
// OpenSSL populates cert_comp_prefs with all available algorithms by
|
||||
// default when compression libraries are linked. Clear them so that
|
||||
diff --git a/src/crypto/crypto_context.h b/src/crypto/crypto_context.h
|
||||
index c65f553e40fd88e8fc7102820355aa1b2ad09436..aa9b98ec614f37813c79a63094c3f515be86408b 100644
|
||||
--- a/src/crypto/crypto_context.h
|
||||
+++ b/src/crypto/crypto_context.h
|
||||
@@ -29,6 +29,11 @@ X509_STORE* GetOrCreateRootCertStore(Environment* env);
|
||||
|
||||
ncrypto::BIOPointer LoadBIO(Environment* env, v8::Local<v8::Value> v);
|
||||
|
||||
+// Test-only hooks for the SPKI blocklist applied to every SecureContext. The
|
||||
+// hash is the SHA-256 of a DER-encoded SubjectPublicKeyInfo.
|
||||
+NODE_EXTERN void AddBlockedSPKIForTesting(const uint8_t* sha256_hash);
|
||||
+NODE_EXTERN void ClearBlockedSPKIsForTesting();
|
||||
+
|
||||
class SecureContext final : public BaseObject {
|
||||
public:
|
||||
using GetSessionCb = SSL_SESSION* (*)(SSL*, const unsigned char*, int, int*);
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "base/memory/ptr_util.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/power_monitor/power_monitor_source.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/task/sequenced_task_runner.h"
|
||||
#include "build/build_config.h"
|
||||
#include "chrome/browser/browser_process.h"
|
||||
@@ -57,6 +58,11 @@
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace node::crypto {
|
||||
NODE_EXTERN void AddBlockedSPKIForTesting(const uint8_t* sha256_hash);
|
||||
NODE_EXTERN void ClearBlockedSPKIsForTesting();
|
||||
} // namespace node::crypto
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<int> live_callback_holder_probes{0};
|
||||
@@ -292,6 +298,20 @@ v8::Local<v8::Promise> SimulateNetworkServiceCrash(v8::Isolate* isolate) {
|
||||
return handle;
|
||||
}
|
||||
|
||||
void AddNodeBlockedSPKIForTesting(gin_helper::ErrorThrower thrower,
|
||||
const std::string& hex) {
|
||||
std::vector<uint8_t> hash;
|
||||
if (!base::HexStringToBytes(hex, &hash) || hash.size() != 32) {
|
||||
thrower.ThrowTypeError("expected a 64 character hex SHA-256");
|
||||
return;
|
||||
}
|
||||
node::crypto::AddBlockedSPKIForTesting(hash.data());
|
||||
}
|
||||
|
||||
void ClearNodeBlockedSPKIsForTesting() {
|
||||
node::crypto::ClearBlockedSPKIsForTesting();
|
||||
}
|
||||
|
||||
void HoldRepeatingCallbackForTesting(const base::RepeatingClosure& callback) {
|
||||
GetCallbackTestingHelper().HoldRepeatingCallback(callback);
|
||||
}
|
||||
@@ -560,6 +580,8 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||
dict.SetMethod<&IsPlatformCaretBrowsingEnabled>(
|
||||
"isPlatformCaretBrowsingEnabled");
|
||||
dict.SetMethod<&SimulateNetworkServiceCrash>("simulateNetworkServiceCrash");
|
||||
dict.SetMethod<&AddNodeBlockedSPKIForTesting>("addNodeBlockedSPKI");
|
||||
dict.SetMethod<&ClearNodeBlockedSPKIsForTesting>("clearNodeBlockedSPKIs");
|
||||
dict.SetMethod<&SimulatePowerEvent>("simulatePowerEvent");
|
||||
dict.SetMethod<&SimulateWebAuthnUvLockedPinSecurityKey>(
|
||||
"simulateWebAuthnUvLockedPinSecurityKey");
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
import * as crypto from 'node:crypto';
|
||||
import { once } from 'node:events';
|
||||
import * as fs from 'node:fs';
|
||||
import * as https from 'node:https';
|
||||
import * as path from 'node:path';
|
||||
import * as tls from 'node:tls';
|
||||
|
||||
import { ifdescribe, isTestingBindingAvailable, listen } from './lib/spec-helpers.ts';
|
||||
|
||||
const certPath = path.resolve(import.meta.dirname, 'fixtures', 'certificates');
|
||||
const rootCA = fs.readFileSync(path.join(certPath, 'rootCA.pem'), 'utf8');
|
||||
const intermediateCA = fs.readFileSync(path.join(certPath, 'intermediateCA.pem'), 'utf8');
|
||||
const serverCert = fs.readFileSync(path.join(certPath, 'server.pem'), 'utf8');
|
||||
const serverKey = fs.readFileSync(path.join(certPath, 'server.key'), 'utf8');
|
||||
|
||||
const spkiHash = (pem: string) =>
|
||||
crypto
|
||||
.createHash('sha256')
|
||||
.update(new crypto.X509Certificate(pem).publicKey.export({ type: 'spki', format: 'der' }))
|
||||
.digest();
|
||||
|
||||
ifdescribe(isTestingBindingAvailable())('certificate revocation', () => {
|
||||
const testing = process._linkedBinding('electron_common_testing');
|
||||
let server: https.Server;
|
||||
let port: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = https.createServer({ key: serverKey, cert: serverCert + intermediateCA }, (req, res) => {
|
||||
res.setHeader('Connection', 'close');
|
||||
res.end('ok');
|
||||
});
|
||||
port = (await listen(server)).port;
|
||||
});
|
||||
afterAll(() => new Promise<void>((resolve) => server.close(() => resolve())));
|
||||
|
||||
describe('Node.js TLS', () => {
|
||||
afterEach(() => testing.clearNodeBlockedSPKIs());
|
||||
|
||||
const connect = async (options: Partial<tls.ConnectionOptions> = {}) => {
|
||||
const socket = tls.connect({
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
servername: 'localhost',
|
||||
ca: [rootCA],
|
||||
rejectUnauthorized: false,
|
||||
...options
|
||||
});
|
||||
try {
|
||||
await once(socket, 'secureConnect');
|
||||
return socket.authorizationError ? String(socket.authorizationError) : null;
|
||||
} finally {
|
||||
socket.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
it('reports a chain through a blocked intermediate key as revoked', async () => {
|
||||
await expect(connect(), 'before').resolves.not.toBe('CERT_REVOKED');
|
||||
testing.addNodeBlockedSPKI(spkiHash(intermediateCA).toString('hex'));
|
||||
await expect(connect(), 'after').resolves.toBe('CERT_REVOKED');
|
||||
});
|
||||
|
||||
it('reports a blocked leaf key as revoked', async () => {
|
||||
testing.addNodeBlockedSPKI(spkiHash(serverCert).toString('hex'));
|
||||
await expect(connect()).resolves.toBe('CERT_REVOKED');
|
||||
});
|
||||
|
||||
it('fails the handshake when rejectUnauthorized is set', async () => {
|
||||
testing.addNodeBlockedSPKI(spkiHash(intermediateCA).toString('hex'));
|
||||
const socket = tls.connect({
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
servername: 'localhost',
|
||||
ca: [rootCA],
|
||||
rejectUnauthorized: true
|
||||
});
|
||||
const [err] = await once(socket, 'error');
|
||||
socket.destroy();
|
||||
expect((err as NodeJS.ErrnoException).code).toBe('CERT_REVOKED');
|
||||
});
|
||||
|
||||
it('applies to https.request', async () => {
|
||||
testing.addNodeBlockedSPKI(spkiHash(intermediateCA).toString('hex'));
|
||||
const req = https.get({ host: '127.0.0.1', port, servername: 'localhost', ca: [rootCA] });
|
||||
const [err] = await once(req, 'error');
|
||||
expect((err as NodeJS.ErrnoException).code).toBe('CERT_REVOKED');
|
||||
});
|
||||
|
||||
it('does not affect chains without a blocked key', async () => {
|
||||
testing.addNodeBlockedSPKI(crypto.randomBytes(32).toString('hex'));
|
||||
await expect(connect()).resolves.not.toBe('CERT_REVOKED');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user