From c67c30cfaaeda9ac23dd0d8726717b8721cb9155 Mon Sep 17 00:00:00 2001 From: Ulises Gascon Date: Wed, 11 Nov 2026 10:52:54 +0100 Subject: [PATCH 1/2] tls: honour Mozilla distrust-after dates for bundled roots Mozilla keeps emSign Root CA - G1, Izenpe.com and Firmaprofesional CIF A62634068 in its root store but distrusts TLS server certificates they issued after 2026-11-16, 2026-04-15 and 2026-08-06 respectively (CKA_NSS_SERVER_DISTRUST_AFTER). tools/mk-ca-bundle.pl does not carry that attribute into node_root_certs.h, so Node.js keeps accepting new certificates from these roots that browsers reject. Add a distrust-after table next to the SPKI blocklist in the certificate verify callback. When a client verifies a server chain through a listed key and the leaf's notBefore is after the key's cutoff, the chain fails with CERT_REVOKED; certificates issued up to the cutoff keep working. Client certificates are not affected: the attribute is Mozilla's server authentication policy, the roots stay trusted anchors otherwise, and this matches the scope of the 2016 StartCom/WoSign cutoff. Node.js has no Certificate Transparency checking, so a backdated notBefore is not detected, as with that cutoff. Fixes: https://github.com/nodejs/node/issues/67391 Refs: https://bugzilla.mozilla.org/show_bug.cgi?id=2089115 Assisted-by: Claude, Codex, Mistral Signed-off-by: Ulises Gascon --- src/crypto/crypto_context.cc | 184 ++++++++++++-- test/fixtures/keys/Makefile | 93 ++++++- test/fixtures/keys/ca1-cert.srl | 2 +- test/fixtures/keys/distrust-after-ca-cert.pem | 23 ++ .../keys/distrust-after-ca-database.txt | 2 + .../keys/distrust-after-ca-database.txt.attr | 1 + .../distrust-after-ca-issued-certs/01.pem | 21 ++ .../distrust-after-ca-issued-certs/02.pem | 21 ++ test/fixtures/keys/distrust-after-ca-key.pem | 28 +++ test/fixtures/keys/distrust-after-ca-serial | 1 + test/fixtures/keys/distrust-after-ca.cnf | 49 ++++ .../keys/distrust-after-cross-cert.pem | 26 ++ .../keys/distrust-after-cross-csr.pem | 19 ++ test/fixtures/keys/distrust-after-cross.cnf | 24 ++ .../keys/distrust-after-post-leaf-cert.pem | 21 ++ .../keys/distrust-after-post-leaf-csr.pem | 18 ++ .../keys/distrust-after-post-leaf-key.pem | 28 +++ .../keys/distrust-after-post-leaf.cnf | 18 ++ .../keys/distrust-after-pre-leaf-cert.pem | 21 ++ .../keys/distrust-after-pre-leaf-csr.pem | 18 ++ .../keys/distrust-after-pre-leaf-key.pem | 28 +++ .../fixtures/keys/distrust-after-pre-leaf.cnf | 18 ++ test/parallel/test-tls-distrust-after.js | 229 ++++++++++++++++++ 23 files changed, 867 insertions(+), 26 deletions(-) create mode 100644 test/fixtures/keys/distrust-after-ca-cert.pem create mode 100644 test/fixtures/keys/distrust-after-ca-database.txt create mode 100644 test/fixtures/keys/distrust-after-ca-database.txt.attr create mode 100644 test/fixtures/keys/distrust-after-ca-issued-certs/01.pem create mode 100644 test/fixtures/keys/distrust-after-ca-issued-certs/02.pem create mode 100644 test/fixtures/keys/distrust-after-ca-key.pem create mode 100644 test/fixtures/keys/distrust-after-ca-serial create mode 100644 test/fixtures/keys/distrust-after-ca.cnf create mode 100644 test/fixtures/keys/distrust-after-cross-cert.pem create mode 100644 test/fixtures/keys/distrust-after-cross-csr.pem create mode 100644 test/fixtures/keys/distrust-after-cross.cnf create mode 100644 test/fixtures/keys/distrust-after-post-leaf-cert.pem create mode 100644 test/fixtures/keys/distrust-after-post-leaf-csr.pem create mode 100644 test/fixtures/keys/distrust-after-post-leaf-key.pem create mode 100644 test/fixtures/keys/distrust-after-post-leaf.cnf create mode 100644 test/fixtures/keys/distrust-after-pre-leaf-cert.pem create mode 100644 test/fixtures/keys/distrust-after-pre-leaf-csr.pem create mode 100644 test/fixtures/keys/distrust-after-pre-leaf-key.pem create mode 100644 test/fixtures/keys/distrust-after-pre-leaf.cnf create mode 100644 test/parallel/test-tls-distrust-after.js diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 7c3f9cbdf8d8..bf65bc247579 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -1438,32 +1438,137 @@ constexpr SpkiHash kBlockedSPKIs[] = { 0xfc, 0xd8, 0xdd, 0x55, 0xa8, 0x75, 0xd7, 0x20, 0xaa, 0xc2}, }; +// Roots that stay in the bundle but whose trust for TLS server certificates +// is limited by issuance date, mirroring Mozilla's +// CKA_NSS_SERVER_DISTRUST_AFTER in certdata.txt. When a client verifies a +// server chain through one of these keys and the leaf certificate's +// notBefore is after the cutoff, the chain is rejected; certificates issued +// up to the cutoff keep working. Client certificates are not affected, see +// CertVerifyCallback. tools/mk-ca-bundle.pl does not carry this attribute +// into node_root_certs.h, so the dates are maintained here. The cutoff is +// the last valid second, as UTC seconds since the epoch. +// +// Node.js has no Certificate Transparency checking, so unlike Firefox it +// cannot detect a backdated notBefore. The same limitation applied to the +// StartCom/WoSign cutoff Node.js enforced in 2016. +struct DistrustAfter { + SpkiHash spki; + time_t cutoff; +}; + +constexpr DistrustAfter kDistrustAfter[] = { + // emSign Root CA - G1, cutoff 2026-11-16 23:59:59 UTC. + // https://bugzilla.mozilla.org/show_bug.cgi?id=2089115 + // 40f6af0346a99aa1cd1d555a4e9cce62c7f9634603ee406615833dc8c8d00367 + // C=IN, OU=emSign PKI, O=eMudhra Technologies Limited, + // CN=emSign Root CA - G1 + {{0x37, 0x6a, 0x1a, 0x70, 0x82, 0xa5, 0x93, 0xdc, 0xcc, 0x20, 0xd5, + 0x61, 0xd1, 0x19, 0xe9, 0xab, 0x8d, 0x30, 0xf1, 0x1c, 0xc3, 0x21, + 0xd0, 0xa3, 0x7f, 0xa4, 0x1f, 0x0d, 0xf2, 0x84, 0xe0, 0x1c}, + 1794873599}, // $ date -u -d @1794873599 = 2026-11-16 23:59:59 UTC + // Izenpe.com, cutoff 2026-04-15 23:59:59 UTC. + // CKA_NSS_SERVER_DISTRUST_AFTER in tools/certdata.txt + // 2530cc8e98321502bad96f9b1fba1b099e2d299e0f4548bb914f363bc0d4531f + // C=ES, O=IZENPE S.A., CN=Izenpe.com + {{0x95, 0x2c, 0x20, 0x39, 0xc0, 0x24, 0x3e, 0xb5, 0x15, 0xdd, 0x73, + 0xd8, 0x3f, 0xc3, 0x64, 0x31, 0x84, 0x87, 0x4f, 0xeb, 0x08, 0x62, + 0xa9, 0x83, 0x77, 0x31, 0xed, 0x9b, 0x47, 0x42, 0xe1, 0x7a}, + 1776297599}, // $ date -u -d @1776297599 = 2026-04-15 23:59:59 UTC + // Autoridad de Certificacion Firmaprofesional CIF A62634068 (2014 root), + // cutoff 2026-08-06 07:01:00 UTC. + // CKA_NSS_SERVER_DISTRUST_AFTER in Mozilla's certdata.txt, newer than the + // copy in tools/certdata.txt + // 57de0583efd2b26e0361da99da9df4648def7ee8441c3b728afa9bcde0f9b26a + // C=ES, CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 + {{0x3b, 0x0d, 0x73, 0xb4, 0xbe, 0x4a, 0x85, 0x4a, 0xdc, 0x3e, 0x51, + 0xd7, 0xef, 0x9f, 0xa4, 0x8a, 0xef, 0xbb, 0x2c, 0xdd, 0x82, 0x4d, + 0x67, 0xbd, 0xc7, 0xd7, 0xd0, 0x9a, 0x2a, 0xbc, 0x2d, 0x43}, + 1785999660}, // $ date -u -d @1785999660 = 2026-08-06 07:01:00 UTC + // Test fixture, not a real CA: test/fixtures/keys/distrust-after-ca-key.pem + // with a cutoff of 2019-12-31 23:59:59 UTC. Its private key is public, so + // date-limiting it costs nothing and lets + // test/parallel/test-tls-distrust-after.js exercise the real list. The + // key is pinned in the fixtures Makefile; rotating it means updating this + // entry too. + // C=US, ST=CA, L=SF, O=Joyent, OU=Node.js, CN=distrust-after-ca + {{0x76, 0xf8, 0xf8, 0x4a, 0xd8, 0x4f, 0x02, 0x4a, 0x56, 0xc7, 0x0c, + 0x0f, 0xd5, 0xff, 0x21, 0x72, 0x18, 0x11, 0x3b, 0xca, 0x81, 0x44, + 0x5f, 0xe9, 0x23, 0x8d, 0xc1, 0xad, 0x06, 0xb9, 0xa5, 0xd8}, + 1577836799}, // $ date -u -d @1577836799 = 2019-12-31 23:59:59 UTC +}; + enum class SpkiCheck { kAllowed, kBlocked, kError }; -// Hashes the DER-encoded SubjectPublicKeyInfo of `cert` and looks it up in -// the blocklist. A failure to encode or hash is reported as kError rather -// than kAllowed so that it cannot be used to slip past the check. -SpkiCheck CheckSPKI(X509* cert) { - if (cert == nullptr) return SpkiCheck::kAllowed; +// Hashes the DER-encoded SubjectPublicKeyInfo of `cert` into `*digest`. +// Returns false if the key could not be encoded or hashed. +bool HashSPKI(X509* cert, SpkiHash* digest) { uint8_t* spki = nullptr; int spki_len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), &spki); - if (spki_len <= 0) return SpkiCheck::kError; - SpkiHash digest; + if (spki_len <= 0) return false; unsigned int digest_len = 0; int ok = EVP_Digest(spki, static_cast(spki_len), - digest.data(), + digest->data(), &digest_len, Digest::SHA256, nullptr); OPENSSL_free(spki); - if (ok != 1 || digest_len != digest.size()) return SpkiCheck::kError; + return ok == 1 && digest_len == digest->size(); +} + +// Looks up the SubjectPublicKeyInfo of `cert` in the blocklist. A failure +// to encode or hash is reported as kError rather than kAllowed so that it +// cannot be used to slip past the check. +SpkiCheck CheckSPKI(X509* cert) { + if (cert == nullptr) return SpkiCheck::kAllowed; + SpkiHash digest; + if (!HashSPKI(cert, &digest)) return SpkiCheck::kError; for (const auto& blocked : kBlockedSPKIs) { if (digest == blocked) return SpkiCheck::kBlocked; } return SpkiCheck::kAllowed; } +// Applies kDistrustAfter to `leaf` for a chain containing `cert`: kBlocked +// when `cert` is a date-limited root and `leaf` was issued after its cutoff, +// kError when the key or the date could not be read. +SpkiCheck CheckDistrustAfter(X509* cert, X509* leaf) { + if (cert == nullptr || leaf == nullptr) return SpkiCheck::kAllowed; + SpkiHash digest; + if (!HashSPKI(cert, &digest)) return SpkiCheck::kError; + for (const auto& entry : kDistrustAfter) { + if (digest != entry.spki) continue; + time_t cutoff = entry.cutoff; + // X509_cmp_time returns 1 when notBefore is later than cutoff, -1 when + // earlier or equal, and 0 when the time could not be parsed. A leaf + // issued exactly at the cutoff second is therefore still accepted. + int cmp = X509_cmp_time(X509_get0_notBefore(leaf), &cutoff); + if (cmp == 0) return SpkiCheck::kError; + if (cmp > 0) return SpkiCheck::kBlocked; + } + return SpkiCheck::kAllowed; +} + +// Checks every certificate in `certs` against kDistrustAfter for `leaf`. On +// kBlocked or kError, `*offender` points at the date-limited certificate and +// `*depth` is its index in `certs`. +SpkiCheck CheckDistrustAfterChain(const STACK_OF(X509) * certs, + X509* leaf, + X509** offender, + int* depth) { + if (certs == nullptr) return SpkiCheck::kAllowed; + for (OPENSSL_SIZE_T i = 0; i < sk_X509_num(certs); i++) { + X509* cert = sk_X509_value(certs, i); + SpkiCheck result = CheckDistrustAfter(cert, leaf); + if (result != SpkiCheck::kAllowed) { + *offender = cert; + *depth = i; + return result; + } + } + return SpkiCheck::kAllowed; +} + // Checks every certificate in `certs`. On kBlocked or kError, `*offender` // points at the certificate the result applies to and `*depth` is its index // in `certs`. @@ -1485,12 +1590,13 @@ SpkiCheck CheckSPKIs(const STACK_OF(X509) * certs, // Installed with SSL_CTX_set_cert_verify_callback on every SSL_CTX Node.js // creates. It wraps X509_verify_cert so that a chain through a blocked -// public key is reported as X509_V_ERR_CERT_REVOKED even when the trust -// store would accept it. The built chain is always checked; when -// verification did not fully succeed (TLSWrap's per-certificate callback -// continues past errors so JS can apply rejectUnauthorized, leaving the -// error set on the context), the leaf and every peer-sent certificate are -// checked too, so the blocklist takes precedence over the original error. +// public key, or through a date-limited root with a leaf issued after the +// cutoff, is reported as X509_V_ERR_CERT_REVOKED even when the trust store +// would accept it. The built chain is always checked; when verification did +// not fully succeed (TLSWrap's per-certificate callback continues past +// errors so JS can apply rejectUnauthorized, leaving the error set on the +// context), the leaf and every peer-sent certificate are checked too, so +// these checks take precedence over the original error. // // A hit is reported like any other X509_verify_cert error: the error and // offending certificate are recorded on the context and the per-certificate @@ -1531,22 +1637,54 @@ int CertVerifyCallback(X509_STORE_CTX* ctx, void*) { int ok = X509_verify_cert(ctx) > 0 ? 1 : 0; const bool verified = ok == 1 && X509_STORE_CTX_get_error(ctx) == X509_V_OK; + // The SSL object this verification belongs to. The library installs its + // per-certificate verify callback on the store context before calling this + // hook, and SSL_is_server tells which side of the handshake is verifying. + SSL* ssl = static_cast( + X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); + // The depth is the offender's index in the built chain (0 = leaf), as // X509_verify_cert would report it. For certificates found only in the // peer-sent stack their index there is the closest equivalent. X509* offender = nullptr; int depth = 0; - SpkiCheck result = - CheckSPKIs(X509_STORE_CTX_get0_chain(ctx), &offender, &depth); + X509* leaf = X509_STORE_CTX_get0_cert(ctx); + const STACK_OF(X509)* chain = X509_STORE_CTX_get0_chain(ctx); + const STACK_OF(X509)* untrusted = X509_STORE_CTX_get0_untrusted(ctx); + + // The SPKI blocklist applies to every verification: those keys are not + // trusted for anything. + SpkiCheck result = CheckSPKIs(chain, &offender, &depth); if (result == SpkiCheck::kAllowed && !verified) { - X509* leaf = X509_STORE_CTX_get0_cert(ctx); result = CheckSPKI(leaf); if (result != SpkiCheck::kAllowed) { offender = leaf; depth = 0; } else { - result = - CheckSPKIs(X509_STORE_CTX_get0_untrusted(ctx), &offender, &depth); + result = CheckSPKIs(untrusted, &offender, &depth); + } + } + + // The distrust-after dates apply only when this side is a client verifying + // a server certificate. CKA_NSS_SERVER_DISTRUST_AFTER is Mozilla's TLS + // server authentication policy; Firefox does not validate client + // certificates against its root store at all, and the roots stay fully + // trusted anchors for everything else. Several of the date-limited CAs are + // large issuers of personal certificates used for client authentication, + // so a server validating client certificates against the bundled store + // keeps accepting them. This is the same scope the 2016 StartCom/WoSign + // cutoff had ("Server does not need to check the whitelist", 89217d14509). + const bool verifying_server_cert = ssl == nullptr || !SSL_is_server(ssl); + if (result == SpkiCheck::kAllowed && verifying_server_cert) { + result = CheckDistrustAfterChain(chain, leaf, &offender, &depth); + if (result == SpkiCheck::kAllowed && !verified) { + result = CheckDistrustAfter(leaf, leaf); + if (result != SpkiCheck::kAllowed) { + offender = leaf; + depth = 0; + } else { + result = CheckDistrustAfterChain(untrusted, leaf, &offender, &depth); + } } } if (result == SpkiCheck::kAllowed) return ok; @@ -1565,10 +1703,8 @@ int CertVerifyCallback(X509_STORE_CTX* ctx, void*) { X509_STORE_CTX_set_error_depth(ctx, depth); X509_STORE_CTX_set_current_cert(ctx, offender); #endif - // The per-certificate callback is the one carried by the SSL object; it is - // what the library installs on the store context before calling this hook. - SSL* ssl = static_cast( - X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); + // Hand the error to the per-certificate callback carried by the SSL object + // so that rejectUnauthorized and authorizationError behave as usual. auto verify_cb = ssl != nullptr ? SSL_get_verify_callback(ssl) : nullptr; if (verify_cb == nullptr) return 0; return verify_cb(0, ctx) ? 1 : 0; diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index 480b111463f8..e26f2110c388 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -15,6 +15,10 @@ all: \ blocked-ca-cert.pem \ blocked-ca-leaf-cert.pem \ blocked-key-leaf-cert.pem \ + distrust-after-ca-cert.pem \ + distrust-after-pre-leaf-cert.pem \ + distrust-after-post-leaf-cert.pem \ + distrust-after-cross-cert.pem \ cert-without-key-fips.pfx \ agent1-cert.pem \ agent1-fips.pfx \ @@ -301,6 +305,93 @@ blocked-key-leaf-cert.pem: blocked-key-leaf-csr.pem ca1-cert.pem ca1-key.pem -extfile blocked-key-leaf.cnf \ -out blocked-key-leaf-cert.pem +# +# distrust-after-ca is a self-signed root whose public key is on the +# distrust-after list in src/crypto/crypto_context.cc with a cutoff of +# 2019-12-31 23:59:59 UTC. distrust-after-pre-leaf is a server cert it issued +# with notBefore exactly at the cutoff second (the last accepted instant); +# distrust-after-post-leaf is the same with notBefore one second after it. +# The leaves are issued with `openssl ca` because `openssl x509 -req` cannot +# set notBefore. distrust-after-cross-cert is the same CA key cross-signed by +# ca1 as an intermediate, so a chain leaf -> cross -> ca1 carries the +# date-limited key without the self-signed root. +# +# The SHA-256 of the CA key's SubjectPublicKeyInfo is compiled into +# src/crypto/crypto_context.cc, so the key is pinned: it is kept across +# `make clean` and never regenerated here. Rotating it means updating the +# digest and fingerprint in that file as well. +distrust-after-ca-key.pem: + @echo "$@ is pinned (see src/crypto/crypto_context.cc); restore it with: git checkout -- $@" >&2 + @exit 1 + +distrust-after-ca-cert.pem: distrust-after-ca.cnf distrust-after-ca-key.pem + openssl req -new -x509 -days 99999 \ + -config distrust-after-ca.cnf \ + -key distrust-after-ca-key.pem \ + -out distrust-after-ca-cert.pem + echo '01' > distrust-after-ca-serial + touch distrust-after-ca-database.txt + echo 'unique_subject = no' > distrust-after-ca-database.txt.attr + mkdir -p distrust-after-ca-issued-certs + +distrust-after-pre-leaf-key.pem: + openssl genrsa -out distrust-after-pre-leaf-key.pem 2048 + +distrust-after-pre-leaf-csr.pem: distrust-after-pre-leaf.cnf distrust-after-pre-leaf-key.pem + openssl req -new \ + -config distrust-after-pre-leaf.cnf \ + -key distrust-after-pre-leaf-key.pem \ + -out distrust-after-pre-leaf-csr.pem + +distrust-after-pre-leaf-cert.pem: distrust-after-pre-leaf-csr.pem distrust-after-ca-cert.pem distrust-after-ca-key.pem + openssl ca \ + -config distrust-after-ca.cnf \ + -keyfile distrust-after-ca-key.pem \ + -cert distrust-after-ca-cert.pem \ + -batch \ + -days 99999 \ + -in distrust-after-pre-leaf-csr.pem \ + -startdate 20191231235959Z \ + -notext -out distrust-after-pre-leaf-cert.pem + +distrust-after-post-leaf-key.pem: + openssl genrsa -out distrust-after-post-leaf-key.pem 2048 + +distrust-after-post-leaf-csr.pem: distrust-after-post-leaf.cnf distrust-after-post-leaf-key.pem + openssl req -new \ + -config distrust-after-post-leaf.cnf \ + -key distrust-after-post-leaf-key.pem \ + -out distrust-after-post-leaf-csr.pem + +distrust-after-post-leaf-cert.pem: distrust-after-post-leaf-csr.pem distrust-after-ca-cert.pem distrust-after-ca-key.pem + openssl ca \ + -config distrust-after-ca.cnf \ + -keyfile distrust-after-ca-key.pem \ + -cert distrust-after-ca-cert.pem \ + -batch \ + -days 99999 \ + -in distrust-after-post-leaf-csr.pem \ + -startdate 20200101000000Z \ + -notext -out distrust-after-post-leaf-cert.pem + +distrust-after-cross-csr.pem: distrust-after-cross.cnf distrust-after-ca-key.pem + openssl req -new \ + -config distrust-after-cross.cnf \ + -key distrust-after-ca-key.pem \ + -out distrust-after-cross-csr.pem + +distrust-after-cross-cert.pem: distrust-after-cross-csr.pem distrust-after-cross.cnf ca1-cert.pem ca1-key.pem + openssl x509 -req \ + -extfile distrust-after-cross.cnf \ + -extensions v3_ca \ + -days 99999 \ + -passin "pass:password" \ + -in distrust-after-cross-csr.pem \ + -CA ca1-cert.pem \ + -CAkey ca1-key.pem \ + -CAcreateserial \ + -out distrust-after-cross-cert.pem + # # Create Subordinate Certificate Authority: ca4 issued by ca2 # ('password' is used for the CA password.) @@ -1388,7 +1479,7 @@ non-canonical-ip-dns-key.pem: openssl ecparam -name prime256v1 -genkey -noout -out non-canonical-ip-dns-key.pem clean: - rm -f *.pfx $(filter-out blocked-ca-key.pem,$(wildcard *.pem)) *.srl ca2-database.txt ca2-crl-agent3-database.txt* ca2-crl-agent3.cnf ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem + rm -f *.pfx $(filter-out blocked-ca-key.pem distrust-after-ca-key.pem,$(wildcard *.pem)) *.srl ca2-database.txt ca2-crl-agent3-database.txt* ca2-crl-agent3.cnf ca2-serial fake-startcom-root-serial distrust-after-ca-serial distrust-after-ca-database.txt* *.print *.old fake-startcom-root-issued-certs/*.pem distrust-after-ca-issued-certs/*.pem @> fake-startcom-root-database.txt test: agent1-verify agent2-verify agent3-verify agent4-verify agent5-verify agent6-verify agent7-verify agent8-verify agent10-verify ec10-verify diff --git a/test/fixtures/keys/ca1-cert.srl b/test/fixtures/keys/ca1-cert.srl index 973a463cf816..13ca03a0b0a9 100644 --- a/test/fixtures/keys/ca1-cert.srl +++ b/test/fixtures/keys/ca1-cert.srl @@ -1 +1 @@ -147D36C1C2F74206DE9FAB5F2226D78ADB00A42C +147D36C1C2F74206DE9FAB5F2226D78ADB00A42E diff --git a/test/fixtures/keys/distrust-after-ca-cert.pem b/test/fixtures/keys/distrust-after-ca-cert.pem new file mode 100644 index 000000000000..02223078e7ef --- /dev/null +++ b/test/fixtures/keys/distrust-after-ca-cert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID6TCCAtGgAwIBAgIUZTthWINR9jw2kSHogvUpSk1mjD4wDQYJKoZIhvcNAQEL +BQAwgZQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzAN +BgNVBAoMBkpveWVudDEQMA4GA1UECwwHTm9kZS5qczEaMBgGA1UEAwwRZGlzdHJ1 +c3QtYWZ0ZXItY2ExLDAqBgkqhkiG9w0BCQEWHWRpc3RydXN0LWFmdGVyLWNhQGV4 +YW1wbGUub3JnMCAXDTI2MTExMDIxMjQyNFoYDzIzMDAwODI1MjEyNDI0WjCBlDEL +MAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0GA1UECgwG +Sm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMRowGAYDVQQDDBFkaXN0cnVzdC1hZnRl +ci1jYTEsMCoGCSqGSIb3DQEJARYdZGlzdHJ1c3QtYWZ0ZXItY2FAZXhhbXBsZS5v +cmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfF9UotYQq2U7A5ZX3 +4AgyJlq7A0eMkZOX+crwv523bYzny3dd49cqv7i+u66I17+CsJ5To+Oqqo23PZgT +GYBwQHIxktVRYidR184WsaT0m/xZgRqbAWP69T02fsslUc6/xnN20EICWQX0OSLE +kBLZVHJmM5bs/foCA3v/EW/d0eGFFzQctFW0qJ5wcTpSdp1sqD04rOy7vsCFxqqS +Zgs5qK2Je0GeAKTRjRVCz99Z3O3hY9sEL5yqpqx1sAhIGEIUMWsk9SLhLstXZ/03 +xJjbGmm9BOIbO3bBQMNoi5Ef2fPPrxbX/r0X4SC4tQfgx2d0rMfQx77AlC+RfWa2 +Tkt1AgMBAAGjLzAtMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFDvOXAcrAS908Iti +MOuZwudinzSbMA0GCSqGSIb3DQEBCwUAA4IBAQAS1DgvZYd+97lEkGUI2a/40sdD +mu3ZCU5je0qeEFX2Gr5RJ3lclvNoWYLTJGts/cq69b3xRKjsBdDym7Sk6MrvcLuS +Bppk5Gzd4MonzoLzCV+B4/GGu/iwjunaYAXZMw3voC/Ibl4JV1RvWL58PoyOqkY6 +3K5PGHfkh1lESpvxmmcdtRc/XGawIqBJTZS8lhnk+M91jg4TvRdHX/vkYvJEFozz +YBgIEp6YjMZMN4QGGnt4iXa+9zVZZnniAxPoeavy7OU4KRttv7I9oimfifcXDeFE +DTFCQhGZZbLMo97HVF8rh6TvX9exCbVSyJzeUYvkJvlgFRRWNeJdnySYO1Xi +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/distrust-after-ca-database.txt b/test/fixtures/keys/distrust-after-ca-database.txt new file mode 100644 index 000000000000..1a5fa5b4061d --- /dev/null +++ b/test/fixtures/keys/distrust-after-ca-database.txt @@ -0,0 +1,2 @@ +V 23000825212424Z 01 unknown /C=US/ST=CA/L=SF/O=Joyent/OU=Node.js/CN=localhost +V 23000825212424Z 02 unknown /C=US/ST=CA/L=SF/O=Joyent/OU=Node.js/CN=localhost diff --git a/test/fixtures/keys/distrust-after-ca-database.txt.attr b/test/fixtures/keys/distrust-after-ca-database.txt.attr new file mode 100644 index 000000000000..3a7e39e6ee60 --- /dev/null +++ b/test/fixtures/keys/distrust-after-ca-database.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/test/fixtures/keys/distrust-after-ca-issued-certs/01.pem b/test/fixtures/keys/distrust-after-ca-issued-certs/01.pem new file mode 100644 index 000000000000..b9ca4c652da8 --- /dev/null +++ b/test/fixtures/keys/distrust-after-ca-issued-certs/01.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDaTCCAlECAQEwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVTMQswCQYD +VQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoMBkpveWVudDEQMA4GA1UECwwH +Tm9kZS5qczEaMBgGA1UEAwwRZGlzdHJ1c3QtYWZ0ZXItY2ExLDAqBgkqhkiG9w0B +CQEWHWRpc3RydXN0LWFmdGVyLWNhQGV4YW1wbGUub3JnMCAXDTE5MTIzMTIzNTk1 +OVoYDzIzMDAwODI1MjEyNDI0WjBeMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0Ex +CzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05vZGUuanMx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJz5bPAJmXnZwL7PhXXgFXS3HUKnkPHw5WKf4Fb1xGrrpa2eIbcBkQTQPs/N +lhmzQlVS/29FkCuqmJQ4QtLrHpx/wGIMcj4i98qWPiB7xu/RS22N9KqYYA92h+6V +SssD/aJogQoYlKr8/AT17HxZMwplE9j4dM25P1QbG/Qn8iJ51Sp3UCLMPRSL6wSX +l6ATJlcKI55CIUElMx+RZmXPjsChgn+mzU36Wiv292klPwmeOlWfY5hZ83ARjRVH +U1/wwYd1PfBztTr7grWadQbS5hzvU3Q22lxZaGBVRgmkoQ3NgvzWNpquMWkU99/Y +NmeK2D4OGP+/FS6uZa6bskuzW50CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAHTXf +cU7W5ill+W8z4gSX6EuqsOx5KxvwrEZPgWQBEb3pgLEb3g0F+6Kgv502YUgGzdr6 +n2BC7xi5MZ3DBXHWhdpfahlNKzj2/y4pzT3uapRDk2LzCVZndHlmHdfbeOLnl1cM +ImZ7JPtywISmAzqQlVzSGIksFHYCx9uiaIWFa3endE8wKBSo9n0BrPHhtaI2iUVc +JDHnA9GJoLVazr8LIOboj2m2TClrD0s6MoxH/t+IvjJj4WmWwrbHgeal/PegTm6O +xxgFdK6Bblwo5pTCJh3XWSHG/4RJSwx+FHlf21x9BptH2+0/VpxMO+UTOtxlldZF +4Wz9tmjEtHxebyfYuw== +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/distrust-after-ca-issued-certs/02.pem b/test/fixtures/keys/distrust-after-ca-issued-certs/02.pem new file mode 100644 index 000000000000..9f902fb89057 --- /dev/null +++ b/test/fixtures/keys/distrust-after-ca-issued-certs/02.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDaTCCAlECAQIwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVTMQswCQYD +VQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoMBkpveWVudDEQMA4GA1UECwwH +Tm9kZS5qczEaMBgGA1UEAwwRZGlzdHJ1c3QtYWZ0ZXItY2ExLDAqBgkqhkiG9w0B +CQEWHWRpc3RydXN0LWFmdGVyLWNhQGV4YW1wbGUub3JnMCAXDTIwMDEwMTAwMDAw +MFoYDzIzMDAwODI1MjEyNDI0WjBeMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0Ex +CzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05vZGUuanMx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJ5yQo/+8EQSKlCU8wZEVueAZTPdxfJuLmoxN0U85t+X78CD6y1A/+ABiOsM +YycBSeo51UT3QR+mDuREaEYvEyHl9ny/pmg+UPYHBdTkhRUn5jvTzOFR4SE/oLnX +LS79s5Xl2blhd7PqPNfZZsDE643zwz0bdRwa9DH0CiEj8ZQBKtBlJUcklVYAD5hm +hjZel8++gmH/tyXpzno12cSGIuLN7qDYWtC4AE8Sg24N/Cc4SsP3fg48LnopA67k +KBMPYZ6pSuRow/JnBSyNA/NXQkU+1D1NFmqp2G33uI/vvHgRH6aUTbu6yAvQOvOY +sNIM4FuCJmTg6XZL4a5PQMDqJScCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEADDBu +m8/bx763H3yHx/jgagUaq2gwZDBBE4k9dnmPdN9cId5o9y3ffDvoeVgkOQ4r09pt +I79Wj+BJu4ratiKQUg9W6iWAEOvbBA95E3X7Ojg4BNG4BklY3Pk0e1kFEKvENTdS +/R7Q/cXYJZ2mBvakbL7pWr1UFlrjQv2qRS+NrwyeCyCe1divsZ4LE4bAtmQQc+J1 +IC3sSYO2TfysXGf0NMJ0zqx+Vdb69Cv2PuYRscUvPeeM1xVkPoWrNXAn6tJzlEtA +sZkH9sRIL+t9tMagxr+/SMCJPUOvIlOD9byCa2/DDVq4UQ8eRCJUUi79be39m4Iq +dFFVymoteyTgtgZ/Nw== +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/distrust-after-ca-key.pem b/test/fixtures/keys/distrust-after-ca-key.pem new file mode 100644 index 000000000000..c29d2cf6f455 --- /dev/null +++ b/test/fixtures/keys/distrust-after-ca-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCfF9UotYQq2U7A +5ZX34AgyJlq7A0eMkZOX+crwv523bYzny3dd49cqv7i+u66I17+CsJ5To+Oqqo23 +PZgTGYBwQHIxktVRYidR184WsaT0m/xZgRqbAWP69T02fsslUc6/xnN20EICWQX0 +OSLEkBLZVHJmM5bs/foCA3v/EW/d0eGFFzQctFW0qJ5wcTpSdp1sqD04rOy7vsCF +xqqSZgs5qK2Je0GeAKTRjRVCz99Z3O3hY9sEL5yqpqx1sAhIGEIUMWsk9SLhLstX +Z/03xJjbGmm9BOIbO3bBQMNoi5Ef2fPPrxbX/r0X4SC4tQfgx2d0rMfQx77AlC+R +fWa2Tkt1AgMBAAECggEABDIVfmTZYUAyBZJxmGiM5+0al0+T5tXrJubTMM5XJ9K9 +Ju02+AjZIp1ofPM6BVbb1S0BrH26fY2sWYjZ9Di1l+36B0aE+4f4EjVE2TA5cdAC +c6GKpLm3cso5oWeAxZX5FjqpznO1JoSOp/9nv67lTO6xjxbfWt9i93Ggy3Lp6vvZ +JJ5wTMw8WRVOvvkXXBERnPx6ko7V16+/UC95DWQv7Z7Kst7KM57JYti2r2BZVZRP +Hlins1xjm1GtcSDma3PEFJDBx3KVrc4ZHLr5F5zNeT3ljrF98TWZCE2bosBqpQ15 +KDZFYktweWoLOuqK3I+sXnu5cA+Aun5Nik1mMkkIkQKBgQDO/jeJS6HuiOjB+QCu +rN2s213dpv/ear3/KOqfIwt7yo3ZbduJPwuhr+TumAKPGh/GLKMmDpkXoUqQ9712 +/aD6NGYVKpIHmoBh0lcurH0qBB/rO+1s9vLJe4pqom5AumY6WxuH9jqKsqCsFwjH +pzoJzbWWz/ufbRyROWZV8mNpiQKBgQDEwmpZWUgl6NynLT8LzwHCVg0Qq3u3PcFp ++Qc9FcVI15nS5iJOkJlurUOVy3/Lrw9J6KYJ7GxSUVsQS6c+HO9DAC6bs1e26Tsi +1xl8xUA51r5oyq11m/JEcODa36V94i5n9po9rr6ypQjr/8AnF8wiuyt3qnbWgXP+ +yOax2PgTjQKBgHxKOZ6/Q2Sv4NNYaQh51EHPFvXZKRqsjZmBQCB+Mo8KeVJtnL00 +lg9oRtbdf+vdi+a7NtvLODHwL44MtD+W2+phPErJXRYjt6MFYkQsXpu0DlZ5jX84 +aWCCRgLxtbI4OtofitCBEXXm/INY8XSw/TCVzMvZ5wU7x0OMa95/xSHBAoGAc/A/ +LEDgs33gLx4JLVMgLTv5XN4++v2Hr4MO0d63LjHksRHsGzE3R7EOxwBvYDzaHGnb +FLPcLDFGIHcDqCTykx9TV2m5AM2zi9cFbLDqJGq2LhdnWIq4mb2NsUrhQCj7wUTd +JAWI1slKVY2Q0zhBkCGL/mpSthj3maUV0qwPma0CgYEAkYxR0K43/88QPoj/Kros ++qMFaB0/MFlDZhcaBmSDF2dhu9/PkxsjGnQvMTWinVQ+GCoKgfCYTs8C8bQ812He +LsKzD34CEHMsCRDu70D+2t2ovVi9SWKcY0dZGFJlvCdkW9U+zm3T/AvYWTRjcsGz +MfWK36YeFTcNMnCXhXkiGz0= +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/distrust-after-ca-serial b/test/fixtures/keys/distrust-after-ca-serial new file mode 100644 index 000000000000..75016ea36252 --- /dev/null +++ b/test/fixtures/keys/distrust-after-ca-serial @@ -0,0 +1 @@ +03 diff --git a/test/fixtures/keys/distrust-after-ca.cnf b/test/fixtures/keys/distrust-after-ca.cnf new file mode 100644 index 000000000000..f1a17bfd7e8f --- /dev/null +++ b/test/fixtures/keys/distrust-after-ca.cnf @@ -0,0 +1,49 @@ +[ ca ] +default_ca = CA_default + +[ CA_default ] +dir = . +name_opt = CA_default +cert_opt = CA_default +default_crl_days = 9999 +default_md = sha256 +database = distrust-after-ca-database.txt +serial = distrust-after-ca-serial +private_key = distrust-after-ca-key.pem +certificate = distrust-after-ca-cert.pem +new_certs_dir = distrust-after-ca-issued-certs +email_in_dn = no +unique_subject = no +policy = policy_anything + +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ req ] +default_bits = 2048 +days = 9999 +distinguished_name = req_distinguished_name +attributes = req_attributes +prompt = no +x509_extensions = v3_ca + +[ req_distinguished_name ] +C = US +ST = CA +L = SF +O = Joyent +OU = Node.js +CN = distrust-after-ca +emailAddress = distrust-after-ca@example.org + +[ req_attributes ] +challengePassword = A challenge password + +[ v3_ca ] +basicConstraints = CA:TRUE diff --git a/test/fixtures/keys/distrust-after-cross-cert.pem b/test/fixtures/keys/distrust-after-cross-cert.pem new file mode 100644 index 000000000000..577bdfe001ee --- /dev/null +++ b/test/fixtures/keys/distrust-after-cross-cert.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIUFH02wcL3Qgben6tfIibXitsApC4wDQYJKoZIhvcNAQEL +BQAwejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0G +A1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQwwCgYDVQQDDANjYTExIDAe +BgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMCAXDTI2MTExMTEwMTU0NFoY +DzIzMDAwODI2MTAxNTQ0WjCBlDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJTRjEPMA0GA1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMRow +GAYDVQQDDBFkaXN0cnVzdC1hZnRlci1jYTEsMCoGCSqGSIb3DQEJARYdZGlzdHJ1 +c3QtYWZ0ZXItY2FAZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCfF9UotYQq2U7A5ZX34AgyJlq7A0eMkZOX+crwv523bYzny3dd49cq +v7i+u66I17+CsJ5To+Oqqo23PZgTGYBwQHIxktVRYidR184WsaT0m/xZgRqbAWP6 +9T02fsslUc6/xnN20EICWQX0OSLEkBLZVHJmM5bs/foCA3v/EW/d0eGFFzQctFW0 +qJ5wcTpSdp1sqD04rOy7vsCFxqqSZgs5qK2Je0GeAKTRjRVCz99Z3O3hY9sEL5yq +pqx1sAhIGEIUMWsk9SLhLstXZ/03xJjbGmm9BOIbO3bBQMNoi5Ef2fPPrxbX/r0X +4SC4tQfgx2d0rMfQx77AlC+RfWa2Tkt1AgMBAAGjgdQwgdEwDAYDVR0TBAUwAwEB +/zAdBgNVHQ4EFgQUO85cBysBL3Twi2Iw65nC52KfNJswgaEGA1UdIwSBmTCBlqF+ +pHwwejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0G +A1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQwwCgYDVQQDDANjYTExIDAe +BgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnghRKsWyN/Wp9DS38q9+cSw6S +xq0CKTANBgkqhkiG9w0BAQsFAAOCAQEAQGpO8X0v9ppxhS11Q35DhBd97yWnY0nc +5AVrYoS/LV2sUnT1PXNkRpyOLZFb0clWW9CJ+Kmzgw5xvhbjnb9Pf+l4IN1y37I0 +Za2qkoQYJUmg7QYCyTzJcnChUoqbcBw/N9Msu52YVabgMtSQkXgFFVXTfayolNl5 +bw4KuvPAvxx+U9wsN2zA4JTPb1bD0NeRcBh+wAyFzuaezlEvGHRpER5tkufy9wWV +7JtNJTXc/a8zaUsehJtnvAcFIaqfRlwsvMlwNmgEDbBAN6xGRMGZ+KP6TJKS5tQA +rFrZNdXxvxOZAc69OxZB03ZjYren/xGQRVYzu4pwqLOV84I5UzDNeg== +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/distrust-after-cross-csr.pem b/test/fixtures/keys/distrust-after-cross-csr.pem new file mode 100644 index 000000000000..95a3f8fb58e4 --- /dev/null +++ b/test/fixtures/keys/distrust-after-cross-csr.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC/zCCAecCAQAwgZQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UE +BwwCU0YxDzANBgNVBAoMBkpveWVudDEQMA4GA1UECwwHTm9kZS5qczEaMBgGA1UE +AwwRZGlzdHJ1c3QtYWZ0ZXItY2ExLDAqBgkqhkiG9w0BCQEWHWRpc3RydXN0LWFm +dGVyLWNhQGV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAnxfVKLWEKtlOwOWV9+AIMiZauwNHjJGTl/nK8L+dt22M58t3XePXKr+4vruu +iNe/grCeU6PjqqqNtz2YExmAcEByMZLVUWInUdfOFrGk9Jv8WYEamwFj+vU9Nn7L +JVHOv8ZzdtBCAlkF9DkixJAS2VRyZjOW7P36AgN7/xFv3dHhhRc0HLRVtKiecHE6 +UnadbKg9OKzsu77AhcaqkmYLOaitiXtBngCk0Y0VQs/fWdzt4WPbBC+cqqasdbAI +SBhCFDFrJPUi4S7LV2f9N8SY2xppvQTiGzt2wUDDaIuRH9nzz68W1/69F+EguLUH +4MdndKzH0Me+wJQvkX1mtk5LdQIDAQABoCUwIwYJKoZIhvcNAQkHMRYMFEEgY2hh +bGxlbmdlIHBhc3N3b3JkMA0GCSqGSIb3DQEBCwUAA4IBAQAZmP0MSoQymf28peAO +h+1AVSz+SJDKAL2v8LeCseSHQ/PiaSwXKuKhbT1ihM8gSk0aCyuzKDO7IC8UzDRP +yGs0st7gWqDmcc9KRAnlyojRgFfU3Vtr0oKVcusQbltXyOxL9oWbiPlf+DmcJNtK +fN/WkHeg+AzoNXRzij0Gf58gjYiob7+zwSf8Jqm0ag3DBfN2/QqMW2UrRQLSAU+v +4NuQbwNQBDwJOISsYHo6D5zf4SP+VXXZZU2oy/6fNw++P4xN0tyuoPjzgvzCDl6f +oNEzf60CNYN/fVBgELi0x33wXx5r0f9tFKQJltBI6CT6uCqdewNkwKS07YSWIVo6 +1Mr+ +-----END CERTIFICATE REQUEST----- diff --git a/test/fixtures/keys/distrust-after-cross.cnf b/test/fixtures/keys/distrust-after-cross.cnf new file mode 100644 index 000000000000..a4b75fcbb0e2 --- /dev/null +++ b/test/fixtures/keys/distrust-after-cross.cnf @@ -0,0 +1,24 @@ +[ req ] +default_bits = 2048 +days = 9999 +distinguished_name = req_distinguished_name +attributes = req_attributes +prompt = no +x509_extensions = v3_ca + +[ req_distinguished_name ] +C = US +ST = CA +L = SF +O = Joyent +OU = Node.js +# Same subject as distrust-after-ca-cert.pem so that leaves it issued chain +# to this cross-signed certificate as well. +CN = distrust-after-ca +emailAddress = distrust-after-ca@example.org + +[ req_attributes ] +challengePassword = A challenge password + +[ v3_ca ] +basicConstraints = CA:TRUE diff --git a/test/fixtures/keys/distrust-after-post-leaf-cert.pem b/test/fixtures/keys/distrust-after-post-leaf-cert.pem new file mode 100644 index 000000000000..9f902fb89057 --- /dev/null +++ b/test/fixtures/keys/distrust-after-post-leaf-cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDaTCCAlECAQIwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVTMQswCQYD +VQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoMBkpveWVudDEQMA4GA1UECwwH +Tm9kZS5qczEaMBgGA1UEAwwRZGlzdHJ1c3QtYWZ0ZXItY2ExLDAqBgkqhkiG9w0B +CQEWHWRpc3RydXN0LWFmdGVyLWNhQGV4YW1wbGUub3JnMCAXDTIwMDEwMTAwMDAw +MFoYDzIzMDAwODI1MjEyNDI0WjBeMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0Ex +CzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05vZGUuanMx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJ5yQo/+8EQSKlCU8wZEVueAZTPdxfJuLmoxN0U85t+X78CD6y1A/+ABiOsM +YycBSeo51UT3QR+mDuREaEYvEyHl9ny/pmg+UPYHBdTkhRUn5jvTzOFR4SE/oLnX +LS79s5Xl2blhd7PqPNfZZsDE643zwz0bdRwa9DH0CiEj8ZQBKtBlJUcklVYAD5hm +hjZel8++gmH/tyXpzno12cSGIuLN7qDYWtC4AE8Sg24N/Cc4SsP3fg48LnopA67k +KBMPYZ6pSuRow/JnBSyNA/NXQkU+1D1NFmqp2G33uI/vvHgRH6aUTbu6yAvQOvOY +sNIM4FuCJmTg6XZL4a5PQMDqJScCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEADDBu +m8/bx763H3yHx/jgagUaq2gwZDBBE4k9dnmPdN9cId5o9y3ffDvoeVgkOQ4r09pt +I79Wj+BJu4ratiKQUg9W6iWAEOvbBA95E3X7Ojg4BNG4BklY3Pk0e1kFEKvENTdS +/R7Q/cXYJZ2mBvakbL7pWr1UFlrjQv2qRS+NrwyeCyCe1divsZ4LE4bAtmQQc+J1 +IC3sSYO2TfysXGf0NMJ0zqx+Vdb69Cv2PuYRscUvPeeM1xVkPoWrNXAn6tJzlEtA +sZkH9sRIL+t9tMagxr+/SMCJPUOvIlOD9byCa2/DDVq4UQ8eRCJUUi79be39m4Iq +dFFVymoteyTgtgZ/Nw== +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/distrust-after-post-leaf-csr.pem b/test/fixtures/keys/distrust-after-post-leaf-csr.pem new file mode 100644 index 000000000000..ff6146612d10 --- /dev/null +++ b/test/fixtures/keys/distrust-after-post-leaf-csr.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC9zCCAd8CAQAwgYwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UE +BwwCU0YxDzANBgNVBAoMBkpveWVudDEQMA4GA1UECwwHTm9kZS5qczESMBAGA1UE +AwwJbG9jYWxob3N0MSwwKgYJKoZIhvcNAQkBFh1kaXN0cnVzdC1hZnRlci1jYUBl +eGFtcGxlLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ5yQo/+ +8EQSKlCU8wZEVueAZTPdxfJuLmoxN0U85t+X78CD6y1A/+ABiOsMYycBSeo51UT3 +QR+mDuREaEYvEyHl9ny/pmg+UPYHBdTkhRUn5jvTzOFR4SE/oLnXLS79s5Xl2blh +d7PqPNfZZsDE643zwz0bdRwa9DH0CiEj8ZQBKtBlJUcklVYAD5hmhjZel8++gmH/ +tyXpzno12cSGIuLN7qDYWtC4AE8Sg24N/Cc4SsP3fg48LnopA67kKBMPYZ6pSuRo +w/JnBSyNA/NXQkU+1D1NFmqp2G33uI/vvHgRH6aUTbu6yAvQOvOYsNIM4FuCJmTg +6XZL4a5PQMDqJScCAwEAAaAlMCMGCSqGSIb3DQEJBzEWDBRBIGNoYWxsZW5nZSBw +YXNzd29yZDANBgkqhkiG9w0BAQsFAAOCAQEAbz4nY7ZhCbajqBUxePdv2wEghebW +UZlmmvpDkaa9kpY5KlUH8yKMzsSIS/SJPFHr85SmiZziWFkHf4TJxXDD7ZdSVwsY +kDKfkEeP2lsS5ToV1Qt61YParX+FihNVSbaoUViGtfyGAUm5E4dD3CcIs29uCcfl +5NsPg0MSg3xA8D6Mk2vEYriFKTslVG9DRyelajT+JGD43YNVnZqkJUwUz1vuCb/b +SZasBgNYLcINw0HtdT5j/7PRU9bx3T3Kpug1KnZXPWfb2gajGoOJnYOpRuGscJMg +jppfprRMy9HGyD719bm+/UXDHkUDmMRB6720sdoq58tNQW+KukVqh8ZSoA== +-----END CERTIFICATE REQUEST----- diff --git a/test/fixtures/keys/distrust-after-post-leaf-key.pem b/test/fixtures/keys/distrust-after-post-leaf-key.pem new file mode 100644 index 000000000000..2b005264ed7a --- /dev/null +++ b/test/fixtures/keys/distrust-after-post-leaf-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCeckKP/vBEEipQ +lPMGRFbngGUz3cXybi5qMTdFPObfl+/Ag+stQP/gAYjrDGMnAUnqOdVE90Efpg7k +RGhGLxMh5fZ8v6ZoPlD2BwXU5IUVJ+Y708zhUeEhP6C51y0u/bOV5dm5YXez6jzX +2WbAxOuN88M9G3UcGvQx9AohI/GUASrQZSVHJJVWAA+YZoY2XpfPvoJh/7cl6c56 +NdnEhiLize6g2FrQuABPEoNuDfwnOErD934OPC56KQOu5CgTD2GeqUrkaMPyZwUs +jQPzV0JFPtQ9TRZqqdht97iP77x4ER+mlE27usgL0DrzmLDSDOBbgiZk4Ol2S+Gu +T0DA6iUnAgMBAAECggEAGTf4bjDWjw8RGxhKAgJYjqo58WJoAikVTCOfN271FDt7 +lnmoseA5ma7E2xxyn5+t4xDnZ/2uqNnmXuCgdCND+U4/yCU82DSq8C23+OWSZQaf +t4ADiQ1eNsuIymt8LpGExuP5nm7r/X/XxUc9+oqdL0/QEGbRYfLMUTMVdabh6Xtx +9yWYvGRroGKgbF4YOgiKhpLU5iLIzTES/zOD4SJ0n0sV+gVqjgDIfvyR6R5nzDJU +3BIXFT5zXr6r434/8qtxNGs9MCloQphpdYrHfEqouj5Zf75TZbL/AxRp4r7gC6g1 +utt4DVCWDatuF4L2lk/Qz/W4wZ/BDjj+eOvRwvqzIQKBgQDKrHGuSHGqmpiFlJ4R +QXVnlYyJmXOY2mzwZNADVlrnLsGEp1zXDmOf5/G8XenkJCeIHDKt3OTun+thxp6m +FIkHsBFuOdAsj1zugKUdG8LxwqP/9g26mM1CDoqsupnNUwm/IlDawfuTXwtwdJK4 +j4tQB4xyeziQ3jBBrdHyOgaUUQKBgQDIIsikOf00rrwEpwZ14tp/mBXC3Wp/PL2t +zWDag1DpWINdEA07eNE8NW2rwVQAlXsy2kDe1hRn5o9XsARvfcqOpvGMhrNhkNGX +TGf8qI2KVYTSNugbiwXscMRRucfW+2YgBJ1M6ndJudA1gHIG2wz8MlLlh1Qy2Up1 +hXoGn4Sb9wKBgEQHagkkC/NF/RgpcH0+SZhBXHD7bw/JkXOHg0L7g3vB9/b87m7J +R6tvAXdG1rFbaWGIhYJSeQ2X5DxnnO0GS5CnCtLC2Rt766eqysFghij8Omw6PRV7 +ILqhgu7Dm8iNrOHZNOaY4kbvSjmvB2in1g1A22W58py919jT7Xqu+3JhAoGBAKcU +6v637tzhl9QmgO1AA3wMQvaZFQS+d/g2gspKYO0+WQUDlph05/FlBXcjgnauVNkC +fq/Kd+S6BieFql5N3SI022mfNviYB3JRdmrnaKr5dFZvQl1kNgDLsAoumFixLvRC +BfSltgrV1q4j2o5Ap9mQRcabsT52S4RLd7E50WLHAoGANS+qhjUwVDHIkqDD70IV +UneChT6m5ftxMIK23QCjUzL7NUWSqeDbKplIwvGV3pNtNMrDfeVSHvL0h0PbBObR +19wr4eK7tpi+YQkRpiWiHRLPHBofOinyBC64sL4MklKXhz4CTW9HGJIMORhRemIh ++ZGkEmjj4l9k5iG8plcddKM= +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/distrust-after-post-leaf.cnf b/test/fixtures/keys/distrust-after-post-leaf.cnf new file mode 100644 index 000000000000..b68ec71b3d55 --- /dev/null +++ b/test/fixtures/keys/distrust-after-post-leaf.cnf @@ -0,0 +1,18 @@ +[ req ] +default_bits = 2048 +days = 9999 +distinguished_name = req_distinguished_name +attributes = req_attributes +prompt = no + +[ req_distinguished_name ] +C = US +ST = CA +L = SF +O = Joyent +OU = Node.js +CN = localhost +emailAddress = distrust-after-ca@example.org + +[ req_attributes ] +challengePassword = A challenge password diff --git a/test/fixtures/keys/distrust-after-pre-leaf-cert.pem b/test/fixtures/keys/distrust-after-pre-leaf-cert.pem new file mode 100644 index 000000000000..b9ca4c652da8 --- /dev/null +++ b/test/fixtures/keys/distrust-after-pre-leaf-cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDaTCCAlECAQEwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVTMQswCQYD +VQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoMBkpveWVudDEQMA4GA1UECwwH +Tm9kZS5qczEaMBgGA1UEAwwRZGlzdHJ1c3QtYWZ0ZXItY2ExLDAqBgkqhkiG9w0B +CQEWHWRpc3RydXN0LWFmdGVyLWNhQGV4YW1wbGUub3JnMCAXDTE5MTIzMTIzNTk1 +OVoYDzIzMDAwODI1MjEyNDI0WjBeMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0Ex +CzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05vZGUuanMx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJz5bPAJmXnZwL7PhXXgFXS3HUKnkPHw5WKf4Fb1xGrrpa2eIbcBkQTQPs/N +lhmzQlVS/29FkCuqmJQ4QtLrHpx/wGIMcj4i98qWPiB7xu/RS22N9KqYYA92h+6V +SssD/aJogQoYlKr8/AT17HxZMwplE9j4dM25P1QbG/Qn8iJ51Sp3UCLMPRSL6wSX +l6ATJlcKI55CIUElMx+RZmXPjsChgn+mzU36Wiv292klPwmeOlWfY5hZ83ARjRVH +U1/wwYd1PfBztTr7grWadQbS5hzvU3Q22lxZaGBVRgmkoQ3NgvzWNpquMWkU99/Y +NmeK2D4OGP+/FS6uZa6bskuzW50CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAHTXf +cU7W5ill+W8z4gSX6EuqsOx5KxvwrEZPgWQBEb3pgLEb3g0F+6Kgv502YUgGzdr6 +n2BC7xi5MZ3DBXHWhdpfahlNKzj2/y4pzT3uapRDk2LzCVZndHlmHdfbeOLnl1cM +ImZ7JPtywISmAzqQlVzSGIksFHYCx9uiaIWFa3endE8wKBSo9n0BrPHhtaI2iUVc +JDHnA9GJoLVazr8LIOboj2m2TClrD0s6MoxH/t+IvjJj4WmWwrbHgeal/PegTm6O +xxgFdK6Bblwo5pTCJh3XWSHG/4RJSwx+FHlf21x9BptH2+0/VpxMO+UTOtxlldZF +4Wz9tmjEtHxebyfYuw== +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/distrust-after-pre-leaf-csr.pem b/test/fixtures/keys/distrust-after-pre-leaf-csr.pem new file mode 100644 index 000000000000..a620e90c30db --- /dev/null +++ b/test/fixtures/keys/distrust-after-pre-leaf-csr.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC9zCCAd8CAQAwgYwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UE +BwwCU0YxDzANBgNVBAoMBkpveWVudDEQMA4GA1UECwwHTm9kZS5qczESMBAGA1UE +AwwJbG9jYWxob3N0MSwwKgYJKoZIhvcNAQkBFh1kaXN0cnVzdC1hZnRlci1jYUBl +eGFtcGxlLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJz5bPAJ +mXnZwL7PhXXgFXS3HUKnkPHw5WKf4Fb1xGrrpa2eIbcBkQTQPs/NlhmzQlVS/29F +kCuqmJQ4QtLrHpx/wGIMcj4i98qWPiB7xu/RS22N9KqYYA92h+6VSssD/aJogQoY +lKr8/AT17HxZMwplE9j4dM25P1QbG/Qn8iJ51Sp3UCLMPRSL6wSXl6ATJlcKI55C +IUElMx+RZmXPjsChgn+mzU36Wiv292klPwmeOlWfY5hZ83ARjRVHU1/wwYd1PfBz +tTr7grWadQbS5hzvU3Q22lxZaGBVRgmkoQ3NgvzWNpquMWkU99/YNmeK2D4OGP+/ +FS6uZa6bskuzW50CAwEAAaAlMCMGCSqGSIb3DQEJBzEWDBRBIGNoYWxsZW5nZSBw +YXNzd29yZDANBgkqhkiG9w0BAQsFAAOCAQEAPz59XF7W092teYv+hzanJCE5Z69H +KyFLj18mMCKdtDFLABvvfyHj7jYEvkLD0LRqVEHO5EfEQOxNh3OxaoygRUTUtyxG +4gOdCYiy2sjnmUp52w5Jyh0FBkeuAvi1vQ4Nn22CgLR0J4GhdICjBxfmJizuzLgg +fSuwNhkOBa8QJq7P7wvkd1CSPIA24YgdlFEsaccr4LnmXZdpFU4KWYweMp/kzcvB +HM2g++pGaArmbrzgcDJg5jzF+N4rTmjjy6CC0n+gh8p39eNllggYD2MgzaRJucnu +Ufp7Ydqqd9mOvlZFCoCV+7T7EJx1Ka8XEKqbevd25nVEhGReLUOBsON9Wg== +-----END CERTIFICATE REQUEST----- diff --git a/test/fixtures/keys/distrust-after-pre-leaf-key.pem b/test/fixtures/keys/distrust-after-pre-leaf-key.pem new file mode 100644 index 000000000000..c8a2034b9a8b --- /dev/null +++ b/test/fixtures/keys/distrust-after-pre-leaf-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc+WzwCZl52cC+ +z4V14BV0tx1Cp5Dx8OVin+BW9cRq66WtniG3AZEE0D7PzZYZs0JVUv9vRZArqpiU +OELS6x6cf8BiDHI+IvfKlj4ge8bv0UttjfSqmGAPdofulUrLA/2iaIEKGJSq/PwE +9ex8WTMKZRPY+HTNuT9UGxv0J/IiedUqd1AizD0Ui+sEl5egEyZXCiOeQiFBJTMf +kWZlz47AoYJ/ps1N+lor9vdpJT8JnjpVn2OYWfNwEY0VR1Nf8MGHdT3wc7U6+4K1 +mnUG0uYc71N0NtpcWWhgVUYJpKENzYL81jaarjFpFPff2DZnitg+Dhj/vxUurmWu +m7JLs1udAgMBAAECggEAIDz7mgqmqgdGFgoBo1VLosZOrJdEZePvAEPMa5+a4MiX +pKIC05ZvRZlNJRlYV9vgZiyfCCe1tboaDurFEpM2HLcB+EENH2TIFiOoC7zP/QWz +Y4MReg3EQLi7rwybKTmfGAE1azDkK3+8dHpbQKiDGoHqwxc3yd5fOrM8mPaxPFIN +ohfc8hL+xuFPuXN/TRnDatVhijnEGzJtWC+gOQrl9ExdcJZCbB0jEeljcIezn6S+ +bSmo4jg+z7/uEu8SBf9xP9iyy5lCYdwgw2uuG736rKQ1IW+bg6pCk1j1fzCV+/CV +Z217HiYYcJcQbbJbF+N1JORUBmxLWvgyL0d+dZEFgwKBgQDW/rTKG63maoTA1H0d +CJ8gBp953nWQni1qGBbDxjzAlrRLSEDJpRQBIR2LxehQ+13EhRh9jwP5QiYKi6+z +W6uJL59yWuSF5sik9VfuH2znwk2AsC1vMWukHGFSmeua7k5RMNVyBKLgVJWyrKV9 +QoMnPv22OF2eQc6YGZ2z6149hwKBgQC66dFIR86GGjwkoQpE3iFh3+rD7aGrJEZg +Djjif7ZuDUbB5t0qQPkntojJs+WYtzzaxk1S92gypmDxYRYeWKwh+sVqk5IYZeMa +eLUmjMkNKxgEn8Bm1eAiXW36ElLHSjbr4QqU1XjQNNTD1JFv+r9ga760uMlKIMxE +PSeAFQrGuwKBgCqYG8lO4o0b3WE2t/LgmmYF6eKDKFeWfvjd/OSciby+HsorbmqE +wgnXPbtLYlNo+ZLNq6R0iR0A9fhw5EZsiveK79QAHfZJWscTtHAA2dqwrBoqzVWS +PPkuhjmPNRZk0C9VtXP5Zyt6lo3nloHSg5lq6WkIkcOg1gVcTdovb2RRAoGAJDWX +rYD7qU77x7zK2HyPyv4t5LbsUtM8Lu+SZY6IMNmxk269+amMmvzpkume2O7mgxtX +b3MuZGGG9jvwpQBYmFyUUgk0jDVwwXgQ3972M+7dQEgSZdmZCg8/duqFzsW5zghS +nHKebPdPIIDVi/PvjyZQU4JYvFuONsoCFPX+mHECgYAMFWO/3HFQPwwgnr8dDVJm +c9vg7HNSqKx9yofY/HJPYqzCge0L6dwPtR1bVPtxSUn3n3NcXRQwg05tccY2lViz +A0q/1L9u2pSzxb2Iq8LV9TTI+7VCL9QWwRW7DTHF0Q9fvjBxv/sLQe88sDhZTs8S ++nOBJJukYRDFgcZgVvZ4Sw== +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/distrust-after-pre-leaf.cnf b/test/fixtures/keys/distrust-after-pre-leaf.cnf new file mode 100644 index 000000000000..b68ec71b3d55 --- /dev/null +++ b/test/fixtures/keys/distrust-after-pre-leaf.cnf @@ -0,0 +1,18 @@ +[ req ] +default_bits = 2048 +days = 9999 +distinguished_name = req_distinguished_name +attributes = req_attributes +prompt = no + +[ req_distinguished_name ] +C = US +ST = CA +L = SF +O = Joyent +OU = Node.js +CN = localhost +emailAddress = distrust-after-ca@example.org + +[ req_attributes ] +challengePassword = A challenge password diff --git a/test/parallel/test-tls-distrust-after.js b/test/parallel/test-tls-distrust-after.js new file mode 100644 index 000000000000..7f67843bdb35 --- /dev/null +++ b/test/parallel/test-tls-distrust-after.js @@ -0,0 +1,229 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// A client rejects a server certificate chain through a root on the +// compiled-in distrust-after list when the leaf certificate's notBefore is +// after that root's cutoff, and reports the failure as CERT_REVOKED. +// Certificates issued up to the cutoff keep verifying. Like any other +// verification error, the result is surfaced through socket.authorizationError +// and honours rejectUnauthorized. The list mirrors Mozilla's +// CKA_NSS_SERVER_DISTRUST_AFTER, a TLS server authentication policy, so a +// server verifying client certificates does not apply it (same scope as the +// 2016 StartCom/WoSign cutoff). +// +// The distrust-after-ca fixture is a permanent entry on the list with a +// cutoff of 2019-12-31 23:59:59 UTC. The fixtures provide two leaves it +// issued: distrust-after-pre-leaf with notBefore exactly at the cutoff second +// (the last accepted instant) and distrust-after-post-leaf with notBefore one +// second after it. distrust-after-cross is the same CA key cross-signed by +// ca1, so leaf -> cross -> ca1 is a chain that carries the date-limited key +// as an intermediate rather than as the trust anchor. The agent6 -> ca3 -> ca1 +// chain and agent1 (signed by ca1) are unaffected. + +const assert = require('assert'); +const { once } = require('events'); +const { describe, it, afterEach } = require('node:test'); +const https = require('https'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const rootCert = fixtures.readKey('ca1-cert.pem'); +const distrustedCaKey = fixtures.readKey('distrust-after-ca-key.pem'); +const distrustedCaCert = fixtures.readKey('distrust-after-ca-cert.pem'); +const crossCert = fixtures.readKey('distrust-after-cross-cert.pem'); +const preLeafKey = fixtures.readKey('distrust-after-pre-leaf-key.pem'); +const preLeafCert = fixtures.readKey('distrust-after-pre-leaf-cert.pem'); +const postLeafKey = fixtures.readKey('distrust-after-post-leaf-key.pem'); +const postLeafCert = fixtures.readKey('distrust-after-post-leaf-cert.pem'); +const unaffectedKey = fixtures.readKey('agent6-key.pem'); +const unaffectedChain = fixtures.readKey('agent6-cert.pem'); +const serverKey = fixtures.readKey('agent1-key.pem'); +const serverCert = fixtures.readKey('agent1-cert.pem'); + +describe('distrust-after on the server certificate', function() { + let server; + let client; + + function listen(serverOpts) { + server = tls.createServer(serverOpts, (socket) => socket.resume()); + server.listen(0); + return once(server, 'listening'); + } + + function connect(clientOpts) { + client = tls.connect({ + port: server.address().port, + ca: distrustedCaCert, + checkServerIdentity() {}, + ...clientOpts, + }); + return client; + } + + afterEach(function() { + client?.destroy(); + server?.close(); + }); + + it('accepts a chain that contains no date-limited root', async function() { + await listen({ key: unaffectedKey, cert: unaffectedChain }); + connect({ ca: rootCert }); + await once(client, 'secureConnect'); + assert.strictEqual(client.authorized, true); + }); + + it('accepts a leaf issued exactly at the cutoff', async function() { + await listen({ key: preLeafKey, cert: preLeafCert }); + connect(); + await once(client, 'secureConnect'); + assert.strictEqual(client.authorized, true); + }); + + it('rejects a leaf issued after the cutoff', async function() { + await listen({ key: postLeafKey, cert: postLeafCert }); + await assert.rejects(async () => { + connect(); + await once(client, 'secureConnect'); + }, { code: 'CERT_REVOKED' }); + }); + + it('reports CERT_REVOKED without rejectUnauthorized', async function() { + await listen({ key: postLeafKey, cert: postLeafCert }); + connect({ rejectUnauthorized: false }); + await once(client, 'secureConnect'); + assert.strictEqual(client.authorized, false); + assert.strictEqual(client.authorizationError, 'CERT_REVOKED'); + }); + + it('takes precedence over an untrusted-root error', async function() { + // With the date-limited root sent by the peer but absent from the trust + // store, the chain cannot reach a root, which would normally be reported + // as SELF_SIGNED_CERT_IN_CHAIN. The cutoff must win over that error. + await listen({ key: postLeafKey, cert: `${postLeafCert}\n${distrustedCaCert}` }); + await assert.rejects(async () => { + connect({ ca: [] }); + await once(client, 'secureConnect'); + }, { code: 'CERT_REVOKED' }); + }); + + it('matches the key when it is a cross-signed intermediate', async function() { + // leaf -> cross (date-limited key, signed by ca1) -> ca1. The list is + // keyed by public key, not by certificate, so the cutoff still applies + // even though the self-signed root is nowhere in the chain. + await listen({ key: postLeafKey, cert: `${postLeafCert}\n${crossCert}` }); + await assert.rejects(async () => { + connect({ ca: rootCert }); + await once(client, 'secureConnect'); + }, { code: 'CERT_REVOKED' }); + }); + + it('accepts a pre-cutoff leaf through the cross-signed intermediate', async function() { + await listen({ key: preLeafKey, cert: `${preLeafCert}\n${crossCert}` }); + connect({ ca: rootCert }); + await once(client, 'secureConnect'); + assert.strictEqual(client.authorized, true); + }); + + it('rejects the date-limited certificate itself as the leaf', async function() { + // The CA certificate used directly as a server certificate: the + // date-limited key is at depth 0 and its own notBefore (when the fixture + // was generated) is after the cutoff. Trusted and untrusted alike. + await listen({ key: distrustedCaKey, cert: distrustedCaCert }); + await assert.rejects(async () => { + connect(); + await once(client, 'secureConnect'); + }, { code: 'CERT_REVOKED' }); + client.destroy(); + await assert.rejects(async () => { + connect({ ca: [] }); + await once(client, 'secureConnect'); + }, { code: 'CERT_REVOKED' }); + }); + + it('applies to https', async function() { + server = https.createServer({ key: postLeafKey, cert: postLeafCert }); + server.listen(0); + await once(server, 'listening'); + await assert.rejects(async () => { + const req = https.get({ + port: server.address().port, + ca: distrustedCaCert, + checkServerIdentity() {}, + }); + client = req; + await once(req, 'response'); + }, { code: 'CERT_REVOKED' }); + }); +}); + +describe('distrust-after does not apply to client certificates', function() { + let server; + let client; + + function listen(serverOpts) { + server = tls.createServer({ + key: serverKey, + cert: serverCert, + ca: distrustedCaCert, + requestCert: true, + ...serverOpts, + }, (socket) => socket.resume()); + server.listen(0); + return once(server, 'listening'); + } + + function connect(clientOpts) { + client = tls.connect({ + port: server.address().port, + ca: rootCert, + checkServerIdentity() {}, + ...clientOpts, + }); + return client; + } + + afterEach(function() { + client?.destroy(); + server?.close(); + }); + + it('accepts a client leaf issued exactly at the cutoff', async function() { + await listen({ rejectUnauthorized: true }); + const connection = once(server, 'secureConnection'); + connect({ key: preLeafKey, cert: preLeafCert }); + const [socket] = await connection; + assert.strictEqual(socket.authorized, true); + }); + + it('accepts a client leaf issued after the cutoff', async function() { + // The server-authentication cutoff is not a statement about client + // authentication, so the same leaf a client would reject as a server + // certificate is a valid client certificate. + await listen({ rejectUnauthorized: true }); + server.on('tlsClientError', common.mustNotCall()); + const connection = once(server, 'secureConnection'); + connect({ key: postLeafKey, cert: postLeafCert }); + const [socket] = await connection; + assert.strictEqual(socket.authorized, true); + assert.strictEqual(socket.authorizationError, null); + }); + + it('still blocks a client leaf issued after the cutoff by a blocked key', async function() { + // Sanity check of the scoping: the SPKI blocklist is not date-based and + // keeps applying to client certificates. + await listen({ + ca: fixtures.readKey('blocked-ca-cert.pem'), + rejectUnauthorized: false, + }); + const connection = once(server, 'secureConnection'); + connect({ + key: fixtures.readKey('blocked-ca-leaf-key.pem'), + cert: fixtures.readKey('blocked-ca-leaf-cert.pem'), + }); + const [socket] = await connection; + assert.strictEqual(socket.authorized, false); + assert.strictEqual(socket.authorizationError, 'CERT_REVOKED'); + }); +}); From 93d1f5d46ae3d4752eda85c0360ccd75ca809ac7 Mon Sep 17 00:00:00 2001 From: Ulises Gascon Date: Wed, 11 Nov 2026 12:28:00 +0100 Subject: [PATCH 2/2] tools: generate distrust-after dates from certdata.txt Mozilla's CKA_NSS_SERVER_DISTRUST_AFTER dates are enforced by the distrust-after table in src/crypto/crypto_context.cc, which so far had to be typed by hand: find the root in certdata.txt, hash its public key, convert the date to an epoch. The emSign G1 entry was first written with the wrong year, which is the kind of slip a generator avoids. Add tools/mk-root-certs-distrust.mjs, which reads tools/certdata.txt and src/node_root_certs.h and writes src/node_root_certs_distrust.h with one entry per bundled root that carries a distrust-after date. The verify callback includes that header as kDistrustAfterBundled and keeps the hand-written table for dates Mozilla publishes between NSS releases and for the test fixture. update-root-certs.mjs runs the generator after mk-ca-bundle.pl and reports date changes in the commit message it produces, so the weekly root-certificates job picks new dates up with no extra step. A test regenerates the header and fails when the committed copy is out of date with certdata.txt. With the current certdata.txt snapshot the generated table holds Izenpe.com only; emSign Root CA - G1 and Firmaprofesional stay in the hand-written table until the next root certificate update brings their dates in. Refs: https://github.com/nodejs/node/issues/67391 Assisted-by: Claude, Codex, Mistral Signed-off-by: Ulises Gascon --- Makefile | 2 +- .../maintaining/maintaining-root-certs.md | 30 ++- node.gyp | 1 + src/crypto/crypto_context.cc | 38 ++- src/node_root_certs_distrust.h | 21 ++ .../test-crypto-root-certs-distrust.js | 36 +++ tools/dep_updaters/update-root-certs.mjs | 32 +++ tools/mk-root-certs-distrust.mjs | 237 ++++++++++++++++++ 8 files changed, 380 insertions(+), 17 deletions(-) create mode 100644 src/node_root_certs_distrust.h create mode 100644 test/parallel/test-crypto-root-certs-distrust.js create mode 100755 tools/mk-root-certs-distrust.mjs diff --git a/Makefile b/Makefile index fbf6f3f5323a..e851e1f5a511 100644 --- a/Makefile +++ b/Makefile @@ -1572,7 +1572,7 @@ jslint-ci: lint-js-ci LINT_CPP_ADDON_DOC_FILES_GLOB = test/addons/??_*/*.cc test/addons/??_*/*.h LINT_CPP_ADDON_DOC_FILES = $(wildcard $(LINT_CPP_ADDON_DOC_FILES_GLOB)) LINT_CPP_EXCLUDE ?= -LINT_CPP_EXCLUDE += src/node_root_certs.h +LINT_CPP_EXCLUDE += src/node_root_certs.h src/node_root_certs_distrust.h LINT_CPP_EXCLUDE += $(LINT_CPP_ADDON_DOC_FILES) # These files were copied more or less verbatim from V8. LINT_CPP_EXCLUDE += src/tracing/trace_event_legacy.h src/tracing/trace_event_legacy_inl.h diff --git a/doc/contributing/maintaining/maintaining-root-certs.md b/doc/contributing/maintaining/maintaining-root-certs.md index aff4b1cc20b7..30ccb2609e6b 100644 --- a/doc/contributing/maintaining/maintaining-root-certs.md +++ b/doc/contributing/maintaining/maintaining-root-certs.md @@ -8,6 +8,15 @@ The certificates come from Mozilla, specifically NSS's `certdata.txt` file. The PEM encodings of the certificates are converted to C strings, and committed in `src/node_root_certs.h`. +Mozilla keeps some roots in its store but distrusts TLS server certificates +they issue after a given date (`CKA_NSS_SERVER_DISTRUST_AFTER` in +`certdata.txt`). Those dates are committed in `src/node_root_certs_distrust.h`, +generated by `tools/mk-root-certs-distrust.mjs`, and enforced by the +certificate verify callback in `src/crypto/crypto_context.cc`. A date Mozilla +publishes between NSS releases can be added by hand to the `kDistrustAfter` +table in that file until the next root certificate update brings it into the +generated header. + ## When to update Root certificates should be updated sometime after Mozilla makes an NSS release, @@ -21,8 +30,15 @@ the root certificates, including: * Downloading `certdata.txt` from Mozilla's source control repository. * Running `tools/mk-ca-bundle.pl` to convert the certificates and generate `src/node_root_certs.h`. -* Using `git diff-files` to determine which certificate have been added and/or - removed. +* Running `tools/mk-root-certs-distrust.mjs` to generate + `src/node_root_certs_distrust.h` from the same `certdata.txt`. +* Using `git diff-files` to determine which certificates and distrust-after + dates have been added and/or removed. + +The `root-certificates` job in `.github/workflows/tools.yml` runs this script +weekly and opens a pull request with everything it changed, including the +regenerated `src/node_root_certs_distrust.h`, so no manual step is needed to +keep the distrust-after dates in sync. Manual instructions are included in the following collapsed section. @@ -74,7 +90,15 @@ the nodejs/node repository. ./mk-ca-bundle.pl -v 2>_after ``` - Confirm that `../src/node_root_certs.h` was updated. + Confirm that `../src/node_root_certs.h` was updated. Then regenerate the + distrust-after table from the same `certdata.txt`: + + ```bash + ./mk-root-certs-distrust.mjs -v + ``` + + `test/parallel/test-crypto-root-certs-distrust.js` fails when + `src/node_root_certs_distrust.h` is out of date with `certdata.txt`. Determine what changes were made by diffing the before and after files: diff --git a/node.gyp b/node.gyp index 714609b03e66..4889dff5d878 100644 --- a/node.gyp +++ b/node.gyp @@ -304,6 +304,7 @@ 'src/node_report.h', 'src/node_revert.h', 'src/node_root_certs.h', + 'src/node_root_certs_distrust.h', 'src/node_sea.h', 'src/node_shadow_realm.h', 'src/node_snapshotable.h', diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index bf65bc247579..e418b75f4b8e 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -1444,9 +1444,8 @@ constexpr SpkiHash kBlockedSPKIs[] = { // server chain through one of these keys and the leaf certificate's // notBefore is after the cutoff, the chain is rejected; certificates issued // up to the cutoff keep working. Client certificates are not affected, see -// CertVerifyCallback. tools/mk-ca-bundle.pl does not carry this attribute -// into node_root_certs.h, so the dates are maintained here. The cutoff is -// the last valid second, as UTC seconds since the epoch. +// CertVerifyCallback. The cutoff is the last valid second, as UTC seconds +// since the epoch. // // Node.js has no Certificate Transparency checking, so unlike Firefox it // cannot detect a backdated notBefore. The same limitation applied to the @@ -1456,6 +1455,18 @@ struct DistrustAfter { time_t cutoff; }; +// Dates from tools/certdata.txt, generated by tools/mk-root-certs-distrust.mjs +// together with node_root_certs.h on every root certificate update. +constexpr DistrustAfter kDistrustAfterBundled[] = { +#include "node_root_certs_distrust.h" // NOLINT(build/include_order) + // Sentinel so that the array is never empty. A cutoff of 0 is skipped. + {{}, 0}, +}; + +// Dates maintained by hand: ones Mozilla published after the certdata.txt +// snapshot in the tree, which move to the generated table with the next root +// certificate update, and the test fixture. Duplicating a generated entry is +// harmless. constexpr DistrustAfter kDistrustAfter[] = { // emSign Root CA - G1, cutoff 2026-11-16 23:59:59 UTC. // https://bugzilla.mozilla.org/show_bug.cgi?id=2089115 @@ -1466,14 +1477,6 @@ constexpr DistrustAfter kDistrustAfter[] = { 0x61, 0xd1, 0x19, 0xe9, 0xab, 0x8d, 0x30, 0xf1, 0x1c, 0xc3, 0x21, 0xd0, 0xa3, 0x7f, 0xa4, 0x1f, 0x0d, 0xf2, 0x84, 0xe0, 0x1c}, 1794873599}, // $ date -u -d @1794873599 = 2026-11-16 23:59:59 UTC - // Izenpe.com, cutoff 2026-04-15 23:59:59 UTC. - // CKA_NSS_SERVER_DISTRUST_AFTER in tools/certdata.txt - // 2530cc8e98321502bad96f9b1fba1b099e2d299e0f4548bb914f363bc0d4531f - // C=ES, O=IZENPE S.A., CN=Izenpe.com - {{0x95, 0x2c, 0x20, 0x39, 0xc0, 0x24, 0x3e, 0xb5, 0x15, 0xdd, 0x73, - 0xd8, 0x3f, 0xc3, 0x64, 0x31, 0x84, 0x87, 0x4f, 0xeb, 0x08, 0x62, - 0xa9, 0x83, 0x77, 0x31, 0xed, 0x9b, 0x47, 0x42, 0xe1, 0x7a}, - 1776297599}, // $ date -u -d @1776297599 = 2026-04-15 23:59:59 UTC // Autoridad de Certificacion Firmaprofesional CIF A62634068 (2014 root), // cutoff 2026-08-06 07:01:00 UTC. // CKA_NSS_SERVER_DISTRUST_AFTER in Mozilla's certdata.txt, newer than the @@ -1536,8 +1539,8 @@ SpkiCheck CheckDistrustAfter(X509* cert, X509* leaf) { if (cert == nullptr || leaf == nullptr) return SpkiCheck::kAllowed; SpkiHash digest; if (!HashSPKI(cert, &digest)) return SpkiCheck::kError; - for (const auto& entry : kDistrustAfter) { - if (digest != entry.spki) continue; + auto check = [&](const DistrustAfter& entry) { + if (entry.cutoff == 0 || digest != entry.spki) return SpkiCheck::kAllowed; time_t cutoff = entry.cutoff; // X509_cmp_time returns 1 when notBefore is later than cutoff, -1 when // earlier or equal, and 0 when the time could not be parsed. A leaf @@ -1545,6 +1548,15 @@ SpkiCheck CheckDistrustAfter(X509* cert, X509* leaf) { int cmp = X509_cmp_time(X509_get0_notBefore(leaf), &cutoff); if (cmp == 0) return SpkiCheck::kError; if (cmp > 0) return SpkiCheck::kBlocked; + return SpkiCheck::kAllowed; + }; + for (const auto& entry : kDistrustAfterBundled) { + SpkiCheck result = check(entry); + if (result != SpkiCheck::kAllowed) return result; + } + for (const auto& entry : kDistrustAfter) { + SpkiCheck result = check(entry); + if (result != SpkiCheck::kAllowed) return result; } return SpkiCheck::kAllowed; } diff --git a/src/node_root_certs_distrust.h b/src/node_root_certs_distrust.h new file mode 100644 index 000000000000..fe981c377254 --- /dev/null +++ b/src/node_root_certs_distrust.h @@ -0,0 +1,21 @@ +// This file is generated by tools/mk-root-certs-distrust.mjs from +// tools/certdata.txt. Do not edit it by hand; rerun the script instead. +// +// Each entry is a root in node_root_certs.h that Mozilla keeps in its store +// but distrusts for TLS server certificates issued after the given date +// (CKA_NSS_SERVER_DISTRUST_AFTER): the SHA-256 of the root's +// SubjectPublicKeyInfo and the last valid second as UTC seconds since the +// epoch. The entries initialize kDistrustAfterBundled in +// src/crypto/crypto_context.cc. + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +/* Izenpe.com */ +// Distrust after 2026-04-15 23:59:59 UTC +// 2530cc8e98321502bad96f9b1fba1b099e2d299e0f4548bb914f363bc0d4531f + {{0x95, 0x2c, 0x20, 0x39, 0xc0, 0x24, 0x3e, 0xb5, 0x15, 0xdd, 0x73, + 0xd8, 0x3f, 0xc3, 0x64, 0x31, 0x84, 0x87, 0x4f, 0xeb, 0x08, 0x62, + 0xa9, 0x83, 0x77, 0x31, 0xed, 0x9b, 0x47, 0x42, 0xe1, 0x7a}, + 1776297599}, + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/test/parallel/test-crypto-root-certs-distrust.js b/test/parallel/test-crypto-root-certs-distrust.js new file mode 100644 index 000000000000..19a58a3e4368 --- /dev/null +++ b/test/parallel/test-crypto-root-certs-distrust.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// src/node_root_certs_distrust.h is generated from tools/certdata.txt by +// tools/mk-root-certs-distrust.mjs. This test regenerates it into a temporary +// directory and checks that the committed header is up to date, so that a +// root-certificate update that brings a new CKA_NSS_SERVER_DISTRUST_AFTER +// date cannot land without the matching table entry. + +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const tmpdir = require('../common/tmpdir'); + +const root = path.resolve(__dirname, '..', '..'); +const script = path.join(root, 'tools', 'mk-root-certs-distrust.mjs'); +const committed = path.join(root, 'src', 'node_root_certs_distrust.h'); +if (!fs.existsSync(script) || !fs.existsSync(committed)) + common.skip('source tree not available'); + +tmpdir.refresh(); +const generated = path.join(tmpdir.path, 'node_root_certs_distrust.h'); + +const { status, stderr } = spawnSync(process.execPath, [ + script, '--output', generated, +], { encoding: 'utf8' }); +assert.strictEqual(status, 0, stderr); + +assert.strictEqual( + fs.readFileSync(generated, 'utf8'), + fs.readFileSync(committed, 'utf8'), + 'src/node_root_certs_distrust.h is out of date; ' + + 'run tools/mk-root-certs-distrust.mjs'); diff --git a/tools/dep_updaters/update-root-certs.mjs b/tools/dep_updaters/update-root-certs.mjs index 80b6fb33f74c..fd0ad5832498 100644 --- a/tools/dep_updaters/update-root-certs.mjs +++ b/tools/dep_updaters/update-root-certs.mjs @@ -152,6 +152,19 @@ if (values.verbose) { console.log(mkCABundleOut); } +// Run tools/mk-root-certs-distrust.mjs to generate +// src/node_root_certs_distrust.h from the same certdata.txt. +if (values.verbose) { + console.log('Running tools/mk-root-certs-distrust.mjs'); +} +const mkDistrustTool = join(checkoutDir, 'tools', 'mk-root-certs-distrust.mjs'); +const mkDistrustOut = execFileSync(process.execPath, + [ mkDistrustTool, ...(values.verbose ? [ '-v' ] : []) ], + opts); +if (values.verbose) { + console.log(mkDistrustOut); +} + // Determine certificates added and/or removed. const certHeaderFile = relative(process.cwd(), join(checkoutDir, 'src', 'node_root_certs.h')); const diff = execFileSync('git', [ 'diff-files', '-u', '--', certHeaderFile ], opts); @@ -163,6 +176,15 @@ const certsRemovedRE = /^-\/\* (.*) \*\//gm; const added = [ ...diff.matchAll(certsAddedRE) ].map((m) => m[1]); const removed = [ ...diff.matchAll(certsRemovedRE) ].map((m) => m[1]); +// Determine distrust-after dates added and/or removed. +const distrustHeaderFile = relative(process.cwd(), join(checkoutDir, 'src', 'node_root_certs_distrust.h')); +const distrustDiff = execFileSync('git', [ 'diff-files', '-u', '--', distrustHeaderFile ], opts); +if (values.verbose) { + console.log(distrustDiff); +} +const distrustAdded = [ ...distrustDiff.matchAll(certsAddedRE) ].map((m) => m[1]); +const distrustRemoved = [ ...distrustDiff.matchAll(certsRemovedRE) ].map((m) => m[1]); + const commitMsg = [ `crypto: update root certificates to NSS ${version}`, '', @@ -183,6 +205,16 @@ if (removed.length > 0) { commitMsg.push(...removed.map((cert) => `- ${cert}`)); commitMsg.push(''); } +if (distrustAdded.length > 0) { + commitMsg.push('Distrust-after dates added:'); + commitMsg.push(...distrustAdded.map((cert) => `- ${cert}`)); + commitMsg.push(''); +} +if (distrustRemoved.length > 0) { + commitMsg.push('Distrust-after dates removed:'); + commitMsg.push(...distrustRemoved.map((cert) => `- ${cert}`)); + commitMsg.push(''); +} commitMsg.push(`[0] ${certdataURL}`); const delimiter = randomUUID(); const properties = [ diff --git a/tools/mk-root-certs-distrust.mjs b/tools/mk-root-certs-distrust.mjs new file mode 100755 index 000000000000..68d1cf372e40 --- /dev/null +++ b/tools/mk-root-certs-distrust.mjs @@ -0,0 +1,237 @@ +#!/usr/bin/env node +// Generates src/node_root_certs_distrust.h from tools/certdata.txt. +// +// Mozilla keeps some roots in its store but stops trusting TLS certificates +// they issue after a given date (CKA_NSS_SERVER_DISTRUST_AFTER). tools/ +// mk-ca-bundle.pl does not carry that attribute into src/node_root_certs.h, +// so this script extracts it for every root that is in the bundle and emits +// the entries of the kDistrustAfterBundled table consumed by +// src/crypto/crypto_context.cc: the SHA-256 of the root's +// SubjectPublicKeyInfo and the cutoff as UTC seconds since the epoch. +// +// Run it after tools/mk-ca-bundle.pl, or let +// tools/dep_updaters/update-root-certs.mjs do both. +import { X509Certificate, createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { basename, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseArgs } from 'node:util'; + +const __filename = fileURLToPath(import.meta.url); +const checkoutDir = join(__filename, '..', '..'); + +const { values } = parseArgs({ + options: { + help: { type: 'boolean' }, + certdata: { type: 'string', default: join(checkoutDir, 'tools', 'certdata.txt') }, + bundle: { type: 'string', default: join(checkoutDir, 'src', 'node_root_certs.h') }, + output: { short: 'o', type: 'string', default: join(checkoutDir, 'src', 'node_root_certs_distrust.h') }, + verbose: { short: 'v', type: 'boolean' }, + }, +}); + +if (values.help) { + console.log(`Usage: ${basename(__filename)} [OPTION]...`); + console.log(); + console.log('Generates src/node_root_certs_distrust.h from tools/certdata.txt.'); + console.log(''); + console.log(' --certdata=FILE certdata.txt to read (default: tools/certdata.txt)'); + console.log(' --bundle=FILE node_root_certs.h to match against'); + console.log(' (default: src/node_root_certs.h)'); + console.log(' -o, --output=FILE header to write'); + console.log(' (default: src/node_root_certs_distrust.h)'); + console.log(' -v, --verbose writes progress to stdout'); + console.log(' --help display this help and exit'); + process.exit(0); +} + +// Decodes a MULTILINE_OCTAL value ("\062\064...") into a Buffer. +function decodeOctal(lines) { + const bytes = []; + for (const line of lines) { + for (const m of line.matchAll(/\\([0-7]{3})/g)) { + bytes.push(parseInt(m[1], 8)); + } + } + return Buffer.from(bytes); +} + +// Reads a text file with line endings normalized to LF, so that a checkout +// with core.autocrlf cannot change the parse or the generated output. +function readText(file) { + return readFileSync(file, 'utf8').replaceAll('\r\n', '\n'); +} + +// Parses the PKCS#11 objects in certdata.txt. Returns one { attributes } per +// object, with MULTILINE_OCTAL values decoded into Buffers and everything +// else kept as the raw string after the type. +function parseCertdata(text) { + const objects = []; + let current; + const lines = text.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('#') || line.trim() === '') continue; + const m = line.match(/^(CKA_[A-Z_]+)\s+(\S+)(?:\s+(.*))?$/); + if (m === null) continue; + const [, name, type, rest] = m; + if (name === 'CKA_CLASS') { + current = { attributes: {} }; + objects.push(current); + } + if (current === undefined) continue; + if (type === 'MULTILINE_OCTAL') { + const octal = []; + for (;;) { + i++; + if (i >= lines.length) { + throw new Error(`Unterminated ${name} value at end of certdata.txt`); + } + if (lines[i] === 'END') break; + octal.push(lines[i]); + } + current.attributes[name] = decodeOctal(octal); + } else if (type === 'UTF8') { + current.attributes[name] = + rest.replace(/^"(.*)"$/, '$1').replaceAll(/\\(["\\])/g, '$1'); + } else { + current.attributes[name] = rest; + } + } + return objects; +} + +// Parses an NSS distrust-after value into seconds since the epoch. NSS writes +// it as UTCTime, e.g. "261116235959Z" for 2026-11-16 23:59:59 UTC; a +// GeneralizedTime with a four-digit year is accepted as well. +function parseDistrustAfter(buf) { + const s = buf.toString('latin1'); + const m = s.match(/^(\d{2}|\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/); + if (m === null) throw new Error(`Unrecognized distrust-after value: ${s}`); + const [, y, mo, dd, hh, mi, ss] = m.slice(0, 7).map(Number); + // UTCTime: two-digit years from 50 are 19xx, the rest 20xx (RFC 5280). + const year = m[1].length === 2 ? (y >= 50 ? 1900 + y : 2000 + y) : y; + const date = new Date(Date.UTC(year, mo - 1, dd, hh, mi, ss)); + // Date.UTC normalizes out-of-range fields (a 31st of February becomes the + // 3rd of March) instead of failing, so check that nothing moved. + if (date.getUTCFullYear() !== year || date.getUTCMonth() !== mo - 1 || + date.getUTCDate() !== dd || date.getUTCHours() !== hh || + date.getUTCMinutes() !== mi || date.getUTCSeconds() !== ss) { + throw new Error(`Invalid distrust-after date: ${s}`); + } + return date.getTime() / 1000; +} + +function isoDate(epoch) { + return new Date(epoch * 1000).toISOString().replace('T', ' ').replace(/\.000Z$/, ' UTC'); +} + +// SHA-256 fingerprints of every certificate in node_root_certs.h, so that +// roots Mozilla ships but mk-ca-bundle.pl skips (for example for lack of a +// server-auth trust bit) do not end up in the table. +function bundledFingerprints(headerText) { + const fingerprints = new Set(); + const pemRE = /-----BEGIN CERTIFICATE-----\\n"\n([\s\S]*?)"-----END CERTIFICATE-----/g; + for (const m of headerText.matchAll(pemRE)) { + const base64 = m[1].replaceAll(/\\n"|"|\n/g, ''); + const der = Buffer.from(base64, 'base64'); + fingerprints.add(createHash('sha256').update(der).digest('hex')); + } + return fingerprints; +} + +// Formats a digest the way clang-format lays out the hand-written entries in +// src/crypto/crypto_context.cc: eleven bytes per line. +function hexArray(buf) { + const bytes = [...buf].map((b) => `0x${b.toString(16).padStart(2, '0')}`); + const lines = []; + for (let i = 0; i < bytes.length; i += 11) { + lines.push((i === 0 ? ' {{' : ' ') + bytes.slice(i, i + 11).join(', ') + ','); + } + return lines.join('\n').replace(/,$/, '},'); +} + +const certdataFile = values.certdata; +const bundleFile = values.bundle; +const outputFile = values.output; + +if (values.verbose) { + console.log(`Reading ${certdataFile}`); + console.log(`Reading ${bundleFile}`); +} +const bundled = bundledFingerprints(readText(bundleFile)); +if (bundled.size === 0) { + throw new Error(`No certificates found in ${bundleFile}`); +} +const objects = parseCertdata(readText(certdataFile)); + +const entries = []; +for (const { attributes } of objects) { + if (attributes.CKA_CLASS !== 'CKO_CERTIFICATE') continue; + const distrustAfter = attributes.CKA_NSS_SERVER_DISTRUST_AFTER; + if (!Buffer.isBuffer(distrustAfter)) continue; // Absent or CK_FALSE. + const label = attributes.CKA_LABEL ?? '(unlabeled certificate)'; + const der = attributes.CKA_VALUE; + if (!Buffer.isBuffer(der)) { + throw new Error(`${label} has a distrust-after date but no CKA_VALUE`); + } + const fingerprint = createHash('sha256').update(der).digest('hex'); + if (!bundled.has(fingerprint)) { + if (values.verbose) { + console.log(`Skipping: ${label} is not in ${relative(checkoutDir, bundleFile)}`); + } + continue; + } + const cert = new X509Certificate(der); + const spki = cert.publicKey.export({ type: 'spki', format: 'der' }); + // Only the label and the fingerprint go into the comment. The subject as + // formatted by X509Certificate depends on the OpenSSL version, which would + // make the output differ between Node.js builds for no reason. + const entry = { + label, + fingerprint, + spkiHash: createHash('sha256').update(spki).digest(), + cutoff: parseDistrustAfter(distrustAfter), + }; + entries.push(entry); + if (values.verbose) { + console.log(`Distrust after ${isoDate(entry.cutoff)}: ${label}`); + } +} +// Code point order, so the output does not depend on ICU data. +entries.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); + +const out = [ + '// This file is generated by tools/mk-root-certs-distrust.mjs from', + '// tools/certdata.txt. Do not edit it by hand; rerun the script instead.', + '//', + '// Each entry is a root in node_root_certs.h that Mozilla keeps in its store', + '// but distrusts for TLS server certificates issued after the given date', + '// (CKA_NSS_SERVER_DISTRUST_AFTER): the SHA-256 of the root\'s', + '// SubjectPublicKeyInfo and the last valid second as UTC seconds since the', + '// epoch. The entries initialize kDistrustAfterBundled in', + '// src/crypto/crypto_context.cc.', + '', + '#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS', + '', +]; +for (const entry of entries) { + out.push(`/* ${entry.label.replaceAll('*/', '* /')} */`); + out.push(`// Distrust after ${isoDate(entry.cutoff)}`); + out.push(`// ${entry.fingerprint}`); + out.push(hexArray(entry.spkiHash)); + out.push(` ${entry.cutoff}},`); + out.push(''); +} +if (entries.length === 0) { + out.push('// No bundled root carries a distrust-after date.'); + out.push(''); +} +out.push('#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS'); +out.push(''); + +const content = out.join('\n'); +writeFileSync(outputFile, content); +if (values.verbose) { + console.log(`Wrote ${entries.length} entries to ${outputFile}`); +}