Skip to content

Commit c57dabe

Browse files
richardlauMoLow
authored andcommitted
tools: add root certificate update script
Automates the steps from `doc/contributing/maintaining-root-certs.md`. Extend "Tools and deps update" workflow to use the new script to update the root certificates. PR-URL: #47425 Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
1 parent 6f0f61a commit c57dabe

3 files changed

Lines changed: 259 additions & 1 deletion

File tree

‎.github/workflows/tools.yml‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,22 @@ jobs:
169169
cat temp-output
170170
tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true
171171
rm temp-output
172+
- id: root-certificates
173+
subsystem: crypto
174+
label: crypto, notable-change
175+
run: |
176+
node ./tools/dep_updaters/update-root-certs.mjs -v -f "$GITHUB_ENV"
172177
steps:
173178
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
174179
with:
175180
persist-credentials: false
176181
- run: ${{ matrix.run }}
177182
env:
178183
GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }}
184+
- name: Generate commit message if not set
185+
if: ${{ env.COMMIT_MSG == '' }}
186+
run: |
187+
echo "COMMIT_MSG=${{ matrix.subsystem }}: update ${{ matrix.id }} to ${{ env.NEW_VERSION }}" >> "$GITHUB_ENV"
179188
- uses: gr2m/create-or-update-pull-request-action@77596e3166f328b24613f7082ab30bf2d93079d5
180189
# Creates a PR or update the Action's existing PR, or
181190
# no-op if the base branch is already up-to-date.
@@ -185,6 +194,6 @@ jobs:
185194
author: Node.js GitHub Bot <github-bot@iojs.org>
186195
body: This is an automated update of ${{ matrix.id }} to ${{ env.NEW_VERSION }}.
187196
branch: actions/tools-update-${{ matrix.id }} # Custom branch *just* for this Action.
188-
commit-message: '${{ matrix.subsystem }}: update ${{ matrix.id }} to ${{ env.NEW_VERSION }}'
197+
commit-message: ${{ env.COMMIT_MSG }}
189198
labels: ${{ matrix.label }}
190199
title: '${{ matrix.subsystem }}: update ${{ matrix.id }} to ${{ env.NEW_VERSION }}'

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ check the [NSS release schedule][].
1515

1616
## Process
1717

18+
The `tools/dep_updaters/update-root-certs.mjs` script automates the update of
19+
the root certificates, including:
20+
21+
* Downloading `certdata.txt` from Mozilla's source control repository.
22+
* Running `tools/mk-ca-bundle.pl` to convert the certificates and generate
23+
`src/node_root_certs.h`.
24+
* Using `git diff-files` to determine which certificate have been added and/or
25+
removed.
26+
27+
Manual instructions are included in the following collapsed section.
28+
29+
<details>
30+
1831
Commands assume that the current working directory is the root of a checkout of
1932
the nodejs/node repository.
2033

@@ -121,5 +134,7 @@ the nodejs/node repository.
121134
- OpenTrust Root CA G3
122135
```
123136

137+
</details>
138+
124139
[NSS release schedule]: https://wiki.mozilla.org/NSS:Release_Versions
125140
[tag list]: https://hg.mozilla.org/projects/nss/tags
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Script to update certdata.txt from NSS.
2+
import{execFileSync}from'node:child_process';
3+
import{randomUUID}from'node:crypto';
4+
import{createWriteStream}from'node:fs';
5+
import{basename,join,relative}from'node:path';
6+
import{Readable}from'node:stream';
7+
import{pipeline}from'node:stream/promises';
8+
import{fileURLToPath}from'node:url';
9+
import{parseArgs}from'node:util';
10+
11+
// Constants for NSS release metadata.
12+
constkNSSVersion='version';
13+
constkNSSDate='date';
14+
constkFirefoxVersion='firefoxVersion';
15+
constkFirefoxDate='firefoxDate';
16+
17+
const__filename=fileURLToPath(import.meta.url);
18+
constnow=newDate();
19+
20+
constformatDate=(d)=>{
21+
constiso=d.toISOString();
22+
returniso.substring(0,iso.indexOf('T'));
23+
};
24+
25+
constnormalizeTD=(text)=>{
26+
// Remove whitespace and any HTML tags.
27+
returntext?.trim().replace(/<.*?>/g,'');
28+
};
29+
constgetReleases=(text)=>{
30+
constreleases=[];
31+
consttableRE=/<table[^>]+>([\S\s]*?)<\/table>/g;
32+
consttableRowRE=/<tr?[^>]*>([\S\s]*?)<\/tr>/g;
33+
consttableHeaderRE=/<th?[^>]*>([\S\s]*?)<\/th>/g;
34+
consttableDataRE=/<td?[^>]*>([\S\s]*?)<\/td>/g;
35+
for(consttableoftext.matchAll(tableRE)){
36+
constcolumns={};
37+
constmatches=table[1].matchAll(tableRowRE);
38+
// First row has the table header.
39+
letrow=matches.next();
40+
if(row.done){
41+
continue;
42+
}
43+
constheaders=Array.from(row.value[1].matchAll(tableHeaderRE),(m)=>m[1]);
44+
if(headers.length>0){
45+
for(leti=0;i<headers.length;i++){
46+
if(/NSSversion/i.test(headers[i])){
47+
columns[kNSSVersion]=i;
48+
}elseif(/Release.*frombranch/i.test(headers[i])){
49+
columns[kNSSDate]=i;
50+
}elseif(/Firefoxversion/i.test(headers[i])){
51+
columns[kFirefoxVersion]=i;
52+
}elseif(/Firefoxreleasedate/i.test(headers[i])){
53+
columns[kFirefoxDate]=i;
54+
}
55+
}
56+
}
57+
// Filter out "NSS Certificate bugs" table.
58+
if(columns[kNSSDate]===undefined){
59+
continue;
60+
}
61+
// Scrape releases.
62+
row=matches.next();
63+
while(!row.done){
64+
constcells=Array.from(row.value[1].matchAll(tableDataRE),(m)=>m[1]);
65+
constrelease={};
66+
release[kNSSVersion]=normalizeTD(cells[columns[kNSSVersion]]);
67+
release[kNSSDate]=newDate(normalizeTD(cells[columns[kNSSDate]]));
68+
release[kFirefoxVersion]=normalizeTD(cells[columns[kFirefoxVersion]]);
69+
release[kFirefoxDate]=newDate(normalizeTD(cells[columns[kFirefoxDate]]));
70+
releases.push(release);
71+
row=matches.next();
72+
}
73+
}
74+
returnreleases;
75+
};
76+
77+
constgetLatestVersion=(releases)=>{
78+
constarrayNumberSort=(x,y,i)=>{
79+
if(x[i]===undefined&&y[i]===undefined){
80+
return0;
81+
}elseif(x[i]===y[i]){
82+
returnarrayNumberSort(x,y,i+1);
83+
}
84+
return(x[i]??0)-(y[i]??0);
85+
};
86+
constextractVersion=(t)=>{
87+
returnt[kNSSVersion].split('.').map((n)=>parseInt(n));
88+
};
89+
constreleaseSorter=(x,y)=>{
90+
returnarrayNumberSort(extractVersion(x),extractVersion(y),0);
91+
};
92+
returnreleases.sort(releaseSorter).filter(pastRelease).at(-1)[kNSSVersion];
93+
};
94+
95+
constpastRelease=(r)=>{
96+
returnr[kNSSDate]<now;
97+
};
98+
99+
constoptions={
100+
help: {
101+
type: 'boolean',
102+
},
103+
file: {
104+
short: 'f',
105+
type: 'string',
106+
},
107+
verbose: {
108+
short: 'v',
109+
type: 'boolean',
110+
},
111+
};
112+
const{
113+
positionals,
114+
values,
115+
}=parseArgs({
116+
allowPositionals: true,
117+
options,
118+
});
119+
120+
if(values.help){
121+
console.log(`Usage: ${basename(__filename)} [OPTION]... [VERSION]...`);
122+
console.log();
123+
console.log('Updates certdata.txt to NSS VERSION (most recent release by default).');
124+
console.log('');
125+
console.log(' -f, --file=FILE writes a commit message reflecting the change to the');
126+
console.log(' specified FILE');
127+
console.log(' -v, --verbose writes progress to stdout');
128+
console.log(' --help display this help and exit');
129+
process.exit(0);
130+
}
131+
132+
if(values.verbose){
133+
console.log('Fetching NSS release schedule');
134+
}
135+
constscheduleURL='https://wiki.mozilla.org/NSS:Release_Versions';
136+
constschedule=awaitfetch(scheduleURL);
137+
if(!schedule.ok){
138+
console.error(`Failed to fetch ${scheduleURL}: ${schedule.status}: ${schedule.statusText}`);
139+
process.exit(-1);
140+
}
141+
constscheduleText=awaitschedule.text();
142+
constnssReleases=getReleases(scheduleText);
143+
144+
// Retrieve metadata for the NSS release being updated to.
145+
constversion=positionals[0]??getLatestVersion(nssReleases);
146+
constrelease=nssReleases.find((r)=>{
147+
returnnewRegExp(`^${version.replace('.','\\.')}\\b`).test(r[kNSSVersion]);
148+
});
149+
if(!pastRelease(release)){
150+
console.warn(`Warning: NSS ${version} is not due to be released until ${formatDate(release[kNSSDate])}`);
151+
}
152+
if(values.verbose){
153+
console.log('Found NSS version:');
154+
console.log(release);
155+
}
156+
157+
// Fetch certdata.txt and overwrite the local copy.
158+
consttag=`NSS_${version.replaceAll('.','_')}_RTM`;
159+
constcertdataURL=`https://hg.mozilla.org/projects/nss/raw-file/${tag}/lib/ckfw/builtins/certdata.txt`;
160+
if(values.verbose){
161+
console.log(`Fetching ${certdataURL}`);
162+
}
163+
constcheckoutDir=join(__filename,'..','..','..');
164+
constcertdata=awaitfetch(certdataURL);
165+
constcertdataFile=join(checkoutDir,'tools','certdata.txt');
166+
if(!certdata.ok){
167+
console.error(`Failed to fetch ${certdataURL}: ${certdata.status}: ${certdata.statusText}`);
168+
process.exit(-1);
169+
}
170+
if(values.verbose){
171+
console.log(`Writing ${certdataFile}`);
172+
}
173+
awaitpipeline(certdata.body,createWriteStream(certdataFile));
174+
175+
// Run tools/mk-ca-bundle.pl to generate src/node_root_certs.h.
176+
if(values.verbose){
177+
console.log('Running tools/mk-ca-bundle.pl');
178+
}
179+
constopts={encoding: 'utf8'};
180+
constmkCABundleTool=join(checkoutDir,'tools','mk-ca-bundle.pl');
181+
constmkCABundleOut=execFileSync(mkCABundleTool,
182+
values.verbose ? ['-v'] : [],
183+
opts);
184+
if(values.verbose){
185+
console.log(mkCABundleOut);
186+
}
187+
188+
// Determine certificates added and/or removed.
189+
constcertHeaderFile=relative(process.cwd(),join(checkoutDir,'src','node_root_certs.h'));
190+
constdiff=execFileSync('git',['diff-files','-u','--',certHeaderFile],opts);
191+
if(values.verbose){
192+
console.log(diff);
193+
}
194+
constcertsAddedRE=/^\+\/\*(.*)\*\//gm;
195+
constcertsRemovedRE=/^-\/\*(.*)\*\//gm;
196+
constadded=[ ...diff.matchAll(certsAddedRE)].map((m)=>m[1]);
197+
constremoved=[ ...diff.matchAll(certsRemovedRE)].map((m)=>m[1]);
198+
199+
constcommitMsg=[
200+
`crypto: update root certificates to NSS ${release[kNSSVersion]}`,
201+
'',
202+
`This is the certdata.txt[0] from NSS ${release[kNSSVersion]}, released on ${formatDate(release[kNSSDate])}.`,
203+
'',
204+
`This is the version of NSS that ${release[kFirefoxDate]<now ? 'shipped' : 'will ship'} in Firefox ${release[kFirefoxVersion]} on`,
205+
`${formatDate(release[kFirefoxDate])}.`,
206+
'',
207+
];
208+
if(added.length>0){
209+
commitMsg.push('Certificates added:');
210+
commitMsg.push(...added.map((cert)=>`- ${cert}`));
211+
commitMsg.push('');
212+
}
213+
if(removed.length>0){
214+
commitMsg.push('Certificates removed:');
215+
commitMsg.push(...removed.map((cert)=>`- ${cert}`));
216+
commitMsg.push('');
217+
}
218+
commitMsg.push(`[0] ${certdataURL}`);
219+
constdelimiter=randomUUID();
220+
constproperties=[
221+
`NEW_VERSION=${release[kNSSVersion]}`,
222+
`COMMIT_MSG<<${delimiter}`,
223+
...commitMsg,
224+
delimiter,
225+
'',
226+
].join('\n');
227+
if(values.verbose){
228+
console.log(properties);
229+
}
230+
constpropertyFile=values.file;
231+
if(propertyFile!==undefined){
232+
console.log(`Writing to ${propertyFile}`);
233+
awaitpipeline(Readable.from(properties),createWriteStream(propertyFile));
234+
}

0 commit comments

Comments
 (0)