Skip to content

Commit 93d1f5d

Browse files
committed
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: #67391 Assisted-by: Claude, Codex, Mistral Signed-off-by: Ulises Gascon <ulisesgascongonzalez@gmail.com>
1 parent c67c30c commit 93d1f5d

8 files changed

Lines changed: 380 additions & 17 deletions

File tree

‎Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1572,7 +1572,7 @@ jslint-ci: lint-js-ci
15721572
LINT_CPP_ADDON_DOC_FILES_GLOB = test/addons/??_*/*.cc test/addons/??_*/*.h
15731573
LINT_CPP_ADDON_DOC_FILES = $(wildcard $(LINT_CPP_ADDON_DOC_FILES_GLOB))
15741574
LINT_CPP_EXCLUDE ?=
1575-
LINT_CPP_EXCLUDE += src/node_root_certs.h
1575+
LINT_CPP_EXCLUDE += src/node_root_certs.h src/node_root_certs_distrust.h
15761576
LINT_CPP_EXCLUDE += $(LINT_CPP_ADDON_DOC_FILES)
15771577
# These files were copied more or less verbatim from V8.
15781578
LINT_CPP_EXCLUDE += src/tracing/trace_event_legacy.h src/tracing/trace_event_legacy_inl.h

‎doc/contributing/maintaining/maintaining-root-certs.md‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ The certificates come from Mozilla, specifically NSS's `certdata.txt` file.
88
The PEM encodings of the certificates are converted to C strings, and committed
99
in `src/node_root_certs.h`.
1010

11+
Mozilla keeps some roots in its store but distrusts TLS server certificates
12+
they issue after a given date (`CKA_NSS_SERVER_DISTRUST_AFTER` in
13+
`certdata.txt`). Those dates are committed in `src/node_root_certs_distrust.h`,
14+
generated by `tools/mk-root-certs-distrust.mjs`, and enforced by the
15+
certificate verify callback in `src/crypto/crypto_context.cc`. A date Mozilla
16+
publishes between NSS releases can be added by hand to the `kDistrustAfter`
17+
table in that file until the next root certificate update brings it into the
18+
generated header.
19+
1120
## When to update
1221

1322
Root certificates should be updated sometime after Mozilla makes an NSS release,
@@ -21,8 +30,15 @@ the root certificates, including:
2130
* Downloading `certdata.txt` from Mozilla's source control repository.
2231
* Running `tools/mk-ca-bundle.pl` to convert the certificates and generate
2332
`src/node_root_certs.h`.
24-
* Using `git diff-files` to determine which certificate have been added and/or
25-
removed.
33+
* Running `tools/mk-root-certs-distrust.mjs` to generate
34+
`src/node_root_certs_distrust.h` from the same `certdata.txt`.
35+
* Using `git diff-files` to determine which certificates and distrust-after
36+
dates have been added and/or removed.
37+
38+
The `root-certificates` job in `.github/workflows/tools.yml` runs this script
39+
weekly and opens a pull request with everything it changed, including the
40+
regenerated `src/node_root_certs_distrust.h`, so no manual step is needed to
41+
keep the distrust-after dates in sync.
2642

2743
Manual instructions are included in the following collapsed section.
2844

@@ -74,7 +90,15 @@ the nodejs/node repository.
7490
./mk-ca-bundle.pl -v 2>_after
7591
```
7692

77-
Confirm that `../src/node_root_certs.h` was updated.
93+
Confirm that `../src/node_root_certs.h` was updated. Then regenerate the
94+
distrust-after table from the same `certdata.txt`:
95+
96+
```bash
97+
./mk-root-certs-distrust.mjs -v
98+
```
99+
100+
`test/parallel/test-crypto-root-certs-distrust.js` fails when
101+
`src/node_root_certs_distrust.h` is out of date with `certdata.txt`.
78102

79103
Determine what changes were made by diffing the before and after files:
80104

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@
304304
'src/node_report.h',
305305
'src/node_revert.h',
306306
'src/node_root_certs.h',
307+
'src/node_root_certs_distrust.h',
307308
'src/node_sea.h',
308309
'src/node_shadow_realm.h',
309310
'src/node_snapshotable.h',

‎src/crypto/crypto_context.cc‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,9 +1444,8 @@ constexpr SpkiHash kBlockedSPKIs[] = {
14441444
// server chain through one of these keys and the leaf certificate's
14451445
// notBefore is after the cutoff, the chain is rejected; certificates issued
14461446
// up to the cutoff keep working. Client certificates are not affected, see
1447-
// CertVerifyCallback. tools/mk-ca-bundle.pl does not carry this attribute
1448-
// into node_root_certs.h, so the dates are maintained here. The cutoff is
1449-
// the last valid second, as UTC seconds since the epoch.
1447+
// CertVerifyCallback. The cutoff is the last valid second, as UTC seconds
1448+
// since the epoch.
14501449
//
14511450
// Node.js has no Certificate Transparency checking, so unlike Firefox it
14521451
// cannot detect a backdated notBefore. The same limitation applied to the
@@ -1456,6 +1455,18 @@ struct DistrustAfter {
14561455
time_t cutoff;
14571456
};
14581457

1458+
// Dates from tools/certdata.txt, generated by tools/mk-root-certs-distrust.mjs
1459+
// together with node_root_certs.h on every root certificate update.
1460+
constexpr DistrustAfter kDistrustAfterBundled[] = {
1461+
#include "node_root_certs_distrust.h" // NOLINT(build/include_order)
1462+
// Sentinel so that the array is never empty. A cutoff of 0 is skipped.
1463+
{{}, 0},
1464+
};
1465+
1466+
// Dates maintained by hand: ones Mozilla published after the certdata.txt
1467+
// snapshot in the tree, which move to the generated table with the next root
1468+
// certificate update, and the test fixture. Duplicating a generated entry is
1469+
// harmless.
14591470
constexpr DistrustAfter kDistrustAfter[] = {
14601471
// emSign Root CA - G1, cutoff 2026-11-16 23:59:59 UTC.
14611472
// https://bugzilla.mozilla.org/show_bug.cgi?id=2089115
@@ -1466,14 +1477,6 @@ constexpr DistrustAfter kDistrustAfter[] = {
14661477
0x61, 0xd1, 0x19, 0xe9, 0xab, 0x8d, 0x30, 0xf1, 0x1c, 0xc3, 0x21,
14671478
0xd0, 0xa3, 0x7f, 0xa4, 0x1f, 0x0d, 0xf2, 0x84, 0xe0, 0x1c},
14681479
1794873599}, // $ date -u -d @1794873599 = 2026-11-16 23:59:59 UTC
1469-
// Izenpe.com, cutoff 2026-04-15 23:59:59 UTC.
1470-
// CKA_NSS_SERVER_DISTRUST_AFTER in tools/certdata.txt
1471-
// 2530cc8e98321502bad96f9b1fba1b099e2d299e0f4548bb914f363bc0d4531f
1472-
// C=ES, O=IZENPE S.A., CN=Izenpe.com
1473-
{{0x95, 0x2c, 0x20, 0x39, 0xc0, 0x24, 0x3e, 0xb5, 0x15, 0xdd, 0x73,
1474-
0xd8, 0x3f, 0xc3, 0x64, 0x31, 0x84, 0x87, 0x4f, 0xeb, 0x08, 0x62,
1475-
0xa9, 0x83, 0x77, 0x31, 0xed, 0x9b, 0x47, 0x42, 0xe1, 0x7a},
1476-
1776297599}, // $ date -u -d @1776297599 = 2026-04-15 23:59:59 UTC
14771480
// Autoridad de Certificacion Firmaprofesional CIF A62634068 (2014 root),
14781481
// cutoff 2026-08-06 07:01:00 UTC.
14791482
// CKA_NSS_SERVER_DISTRUST_AFTER in Mozilla's certdata.txt, newer than the
@@ -1536,15 +1539,24 @@ SpkiCheck CheckDistrustAfter(X509* cert, X509* leaf) {
15361539
if (cert == nullptr || leaf == nullptr) return SpkiCheck::kAllowed;
15371540
SpkiHash digest;
15381541
if (!HashSPKI(cert, &digest)) return SpkiCheck::kError;
1539-
for (const auto& entry : kDistrustAfter) {
1540-
if (digest != entry.spki) continue;
1542+
auto check = [&](const DistrustAfter& entry) {
1543+
if (entry.cutoff == 0 || digest != entry.spki) return SpkiCheck::kAllowed;
15411544
time_t cutoff = entry.cutoff;
15421545
// X509_cmp_time returns 1 when notBefore is later than cutoff, -1 when
15431546
// earlier or equal, and 0 when the time could not be parsed. A leaf
15441547
// issued exactly at the cutoff second is therefore still accepted.
15451548
int cmp = X509_cmp_time(X509_get0_notBefore(leaf), &cutoff);
15461549
if (cmp == 0) return SpkiCheck::kError;
15471550
if (cmp > 0) return SpkiCheck::kBlocked;
1551+
return SpkiCheck::kAllowed;
1552+
};
1553+
for (const auto& entry : kDistrustAfterBundled) {
1554+
SpkiCheck result = check(entry);
1555+
if (result != SpkiCheck::kAllowed) return result;
1556+
}
1557+
for (const auto& entry : kDistrustAfter) {
1558+
SpkiCheck result = check(entry);
1559+
if (result != SpkiCheck::kAllowed) return result;
15481560
}
15491561
return SpkiCheck::kAllowed;
15501562
}

‎src/node_root_certs_distrust.h‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// This file is generated by tools/mk-root-certs-distrust.mjs from
2+
// tools/certdata.txt. Do not edit it by hand; rerun the script instead.
3+
//
4+
// Each entry is a root in node_root_certs.h that Mozilla keeps in its store
5+
// but distrusts for TLS server certificates issued after the given date
6+
// (CKA_NSS_SERVER_DISTRUST_AFTER): the SHA-256 of the root's
7+
// SubjectPublicKeyInfo and the last valid second as UTC seconds since the
8+
// epoch. The entries initialize kDistrustAfterBundled in
9+
// src/crypto/crypto_context.cc.
10+
11+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
12+
13+
/* Izenpe.com */
14+
// Distrust after 2026-04-15 23:59:59 UTC
15+
// 2530cc8e98321502bad96f9b1fba1b099e2d299e0f4548bb914f363bc0d4531f
16+
{{0x95, 0x2c, 0x20, 0x39, 0xc0, 0x24, 0x3e, 0xb5, 0x15, 0xdd, 0x73,
17+
0xd8, 0x3f, 0xc3, 0x64, 0x31, 0x84, 0x87, 0x4f, 0xeb, 0x08, 0x62,
18+
0xa9, 0x83, 0x77, 0x31, 0xed, 0x9b, 0x47, 0x42, 0xe1, 0x7a},
19+
1776297599},
20+
21+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
6+
// src/node_root_certs_distrust.h is generated from tools/certdata.txt by
7+
// tools/mk-root-certs-distrust.mjs. This test regenerates it into a temporary
8+
// directory and checks that the committed header is up to date, so that a
9+
// root-certificate update that brings a new CKA_NSS_SERVER_DISTRUST_AFTER
10+
// date cannot land without the matching table entry.
11+
12+
const assert = require('assert');
13+
const { spawnSync } = require('child_process');
14+
const fs = require('fs');
15+
const path = require('path');
16+
const tmpdir = require('../common/tmpdir');
17+
18+
const root = path.resolve(__dirname, '..', '..');
19+
const script = path.join(root, 'tools', 'mk-root-certs-distrust.mjs');
20+
const committed = path.join(root, 'src', 'node_root_certs_distrust.h');
21+
if (!fs.existsSync(script) || !fs.existsSync(committed))
22+
common.skip('source tree not available');
23+
24+
tmpdir.refresh();
25+
const generated = path.join(tmpdir.path, 'node_root_certs_distrust.h');
26+
27+
const { status, stderr } = spawnSync(process.execPath, [
28+
script, '--output', generated,
29+
], { encoding: 'utf8' });
30+
assert.strictEqual(status, 0, stderr);
31+
32+
assert.strictEqual(
33+
fs.readFileSync(generated, 'utf8'),
34+
fs.readFileSync(committed, 'utf8'),
35+
'src/node_root_certs_distrust.h is out of date; ' +
36+
'run tools/mk-root-certs-distrust.mjs');

‎tools/dep_updaters/update-root-certs.mjs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,19 @@ if (values.verbose) {
152152
console.log(mkCABundleOut);
153153
}
154154

155+
// Run tools/mk-root-certs-distrust.mjs to generate
156+
// src/node_root_certs_distrust.h from the same certdata.txt.
157+
if (values.verbose) {
158+
console.log('Running tools/mk-root-certs-distrust.mjs');
159+
}
160+
const mkDistrustTool = join(checkoutDir, 'tools', 'mk-root-certs-distrust.mjs');
161+
const mkDistrustOut = execFileSync(process.execPath,
162+
[ mkDistrustTool, ...(values.verbose ? [ '-v' ] : []) ],
163+
opts);
164+
if (values.verbose) {
165+
console.log(mkDistrustOut);
166+
}
167+
155168
// Determine certificates added and/or removed.
156169
const certHeaderFile = relative(process.cwd(), join(checkoutDir, 'src', 'node_root_certs.h'));
157170
const diff = execFileSync('git', [ 'diff-files', '-u', '--', certHeaderFile ], opts);
@@ -163,6 +176,15 @@ const certsRemovedRE = /^-\/\* (.*) \*\//gm;
163176
const added = [ ...diff.matchAll(certsAddedRE) ].map((m) => m[1]);
164177
const removed = [ ...diff.matchAll(certsRemovedRE) ].map((m) => m[1]);
165178

179+
// Determine distrust-after dates added and/or removed.
180+
const distrustHeaderFile = relative(process.cwd(), join(checkoutDir, 'src', 'node_root_certs_distrust.h'));
181+
const distrustDiff = execFileSync('git', [ 'diff-files', '-u', '--', distrustHeaderFile ], opts);
182+
if (values.verbose) {
183+
console.log(distrustDiff);
184+
}
185+
const distrustAdded = [ ...distrustDiff.matchAll(certsAddedRE) ].map((m) => m[1]);
186+
const distrustRemoved = [ ...distrustDiff.matchAll(certsRemovedRE) ].map((m) => m[1]);
187+
166188
const commitMsg = [
167189
`crypto: update root certificates to NSS ${version}`,
168190
'',
@@ -183,6 +205,16 @@ if (removed.length > 0) {
183205
commitMsg.push(...removed.map((cert) => `- ${cert}`));
184206
commitMsg.push('');
185207
}
208+
if (distrustAdded.length > 0) {
209+
commitMsg.push('Distrust-after dates added:');
210+
commitMsg.push(...distrustAdded.map((cert) => `- ${cert}`));
211+
commitMsg.push('');
212+
}
213+
if (distrustRemoved.length > 0) {
214+
commitMsg.push('Distrust-after dates removed:');
215+
commitMsg.push(...distrustRemoved.map((cert) => `- ${cert}`));
216+
commitMsg.push('');
217+
}
186218
commitMsg.push(`[0] ${certdataURL}`);
187219
const delimiter = randomUUID();
188220
const properties = [

0 commit comments

Comments
 (0)