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 7c3f9cbdf8d8..e418b75f4b8e 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -1438,32 +1438,149 @@ 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. 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; +}; + +// 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 + // 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 + // 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; + 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 + // 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; + }; + 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; +} + +// 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 +1602,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 +1649,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 +1715,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/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/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-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/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'); + }); +}); 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}`); +}