11cb0ef41Sopenharmony_ci// Script to update certdata.txt from NSS. 21cb0ef41Sopenharmony_ciimport { execFileSync } from 'node:child_process'; 31cb0ef41Sopenharmony_ciimport { randomUUID } from 'node:crypto'; 41cb0ef41Sopenharmony_ciimport { createWriteStream } from 'node:fs'; 51cb0ef41Sopenharmony_ciimport { basename, join, relative } from 'node:path'; 61cb0ef41Sopenharmony_ciimport { Readable } from 'node:stream'; 71cb0ef41Sopenharmony_ciimport { pipeline } from 'node:stream/promises'; 81cb0ef41Sopenharmony_ciimport { fileURLToPath } from 'node:url'; 91cb0ef41Sopenharmony_ciimport { parseArgs } from 'node:util'; 101cb0ef41Sopenharmony_ci 111cb0ef41Sopenharmony_ci// Constants for NSS release metadata. 121cb0ef41Sopenharmony_ciconst kNSSVersion = 'version'; 131cb0ef41Sopenharmony_ciconst kNSSDate = 'date'; 141cb0ef41Sopenharmony_ciconst kFirefoxVersion = 'firefoxVersion'; 151cb0ef41Sopenharmony_ciconst kFirefoxDate = 'firefoxDate'; 161cb0ef41Sopenharmony_ci 171cb0ef41Sopenharmony_ciconst __filename = fileURLToPath(import.meta.url); 181cb0ef41Sopenharmony_ciconst now = new Date(); 191cb0ef41Sopenharmony_ci 201cb0ef41Sopenharmony_ciconst formatDate = (d) => { 211cb0ef41Sopenharmony_ci const iso = d.toISOString(); 221cb0ef41Sopenharmony_ci return iso.substring(0, iso.indexOf('T')); 231cb0ef41Sopenharmony_ci}; 241cb0ef41Sopenharmony_ci 251cb0ef41Sopenharmony_ciconst getCertdataURL = (version) => { 261cb0ef41Sopenharmony_ci const tag = `NSS_${version.replaceAll('.', '_')}_RTM`; 271cb0ef41Sopenharmony_ci const certdataURL = `https://hg.mozilla.org/projects/nss/raw-file/${tag}/lib/ckfw/builtins/certdata.txt`; 281cb0ef41Sopenharmony_ci return certdataURL; 291cb0ef41Sopenharmony_ci}; 301cb0ef41Sopenharmony_ci 311cb0ef41Sopenharmony_ciconst normalizeTD = (text) => { 321cb0ef41Sopenharmony_ci // Remove whitespace and any HTML tags. 331cb0ef41Sopenharmony_ci return text?.trim().replace(/<.*?>/g, ''); 341cb0ef41Sopenharmony_ci}; 351cb0ef41Sopenharmony_ciconst getReleases = (text) => { 361cb0ef41Sopenharmony_ci const releases = []; 371cb0ef41Sopenharmony_ci const tableRE = /<table [^>]+>([\S\s]*?)<\/table>/g; 381cb0ef41Sopenharmony_ci const tableRowRE = /<tr ?[^>]*>([\S\s]*?)<\/tr>/g; 391cb0ef41Sopenharmony_ci const tableHeaderRE = /<th ?[^>]*>([\S\s]*?)<\/th>/g; 401cb0ef41Sopenharmony_ci const tableDataRE = /<td ?[^>]*>([\S\s]*?)<\/td>/g; 411cb0ef41Sopenharmony_ci for (const table of text.matchAll(tableRE)) { 421cb0ef41Sopenharmony_ci const columns = {}; 431cb0ef41Sopenharmony_ci const matches = table[1].matchAll(tableRowRE); 441cb0ef41Sopenharmony_ci // First row has the table header. 451cb0ef41Sopenharmony_ci let row = matches.next(); 461cb0ef41Sopenharmony_ci if (row.done) { 471cb0ef41Sopenharmony_ci continue; 481cb0ef41Sopenharmony_ci } 491cb0ef41Sopenharmony_ci const headers = Array.from(row.value[1].matchAll(tableHeaderRE), (m) => m[1]); 501cb0ef41Sopenharmony_ci if (headers.length > 0) { 511cb0ef41Sopenharmony_ci for (let i = 0; i < headers.length; i++) { 521cb0ef41Sopenharmony_ci if (/NSS version/i.test(headers[i])) { 531cb0ef41Sopenharmony_ci columns[kNSSVersion] = i; 541cb0ef41Sopenharmony_ci } else if (/Release.*from branch/i.test(headers[i])) { 551cb0ef41Sopenharmony_ci columns[kNSSDate] = i; 561cb0ef41Sopenharmony_ci } else if (/Firefox version/i.test(headers[i])) { 571cb0ef41Sopenharmony_ci columns[kFirefoxVersion] = i; 581cb0ef41Sopenharmony_ci } else if (/Firefox release date/i.test(headers[i])) { 591cb0ef41Sopenharmony_ci columns[kFirefoxDate] = i; 601cb0ef41Sopenharmony_ci } 611cb0ef41Sopenharmony_ci } 621cb0ef41Sopenharmony_ci } 631cb0ef41Sopenharmony_ci // Filter out "NSS Certificate bugs" table. 641cb0ef41Sopenharmony_ci if (columns[kNSSDate] === undefined) { 651cb0ef41Sopenharmony_ci continue; 661cb0ef41Sopenharmony_ci } 671cb0ef41Sopenharmony_ci // Scrape releases. 681cb0ef41Sopenharmony_ci row = matches.next(); 691cb0ef41Sopenharmony_ci while (!row.done) { 701cb0ef41Sopenharmony_ci const cells = Array.from(row.value[1].matchAll(tableDataRE), (m) => m[1]); 711cb0ef41Sopenharmony_ci const release = {}; 721cb0ef41Sopenharmony_ci release[kNSSVersion] = normalizeTD(cells[columns[kNSSVersion]]); 731cb0ef41Sopenharmony_ci release[kNSSDate] = new Date(normalizeTD(cells[columns[kNSSDate]])); 741cb0ef41Sopenharmony_ci release[kFirefoxVersion] = normalizeTD(cells[columns[kFirefoxVersion]]); 751cb0ef41Sopenharmony_ci release[kFirefoxDate] = new Date(normalizeTD(cells[columns[kFirefoxDate]])); 761cb0ef41Sopenharmony_ci releases.push(release); 771cb0ef41Sopenharmony_ci row = matches.next(); 781cb0ef41Sopenharmony_ci } 791cb0ef41Sopenharmony_ci } 801cb0ef41Sopenharmony_ci return releases; 811cb0ef41Sopenharmony_ci}; 821cb0ef41Sopenharmony_ci 831cb0ef41Sopenharmony_ciconst getLatestVersion = async (releases) => { 841cb0ef41Sopenharmony_ci const arrayNumberSortDescending = (x, y, i) => { 851cb0ef41Sopenharmony_ci if (x[i] === undefined && y[i] === undefined) { 861cb0ef41Sopenharmony_ci return 0; 871cb0ef41Sopenharmony_ci } else if (x[i] === y[i]) { 881cb0ef41Sopenharmony_ci return arrayNumberSortDescending(x, y, i + 1); 891cb0ef41Sopenharmony_ci } 901cb0ef41Sopenharmony_ci return (y[i] ?? 0) - (x[i] ?? 0); 911cb0ef41Sopenharmony_ci }; 921cb0ef41Sopenharmony_ci const extractVersion = (t) => { 931cb0ef41Sopenharmony_ci return t[kNSSVersion].split('.').map((n) => parseInt(n)); 941cb0ef41Sopenharmony_ci }; 951cb0ef41Sopenharmony_ci const releaseSorter = (x, y) => { 961cb0ef41Sopenharmony_ci return arrayNumberSortDescending(extractVersion(x), extractVersion(y), 0); 971cb0ef41Sopenharmony_ci }; 981cb0ef41Sopenharmony_ci // Return the most recent certadata.txt that exists on the server. 991cb0ef41Sopenharmony_ci const sortedReleases = releases.sort(releaseSorter).filter(pastRelease); 1001cb0ef41Sopenharmony_ci for (const candidate of sortedReleases) { 1011cb0ef41Sopenharmony_ci const candidateURL = getCertdataURL(candidate[kNSSVersion]); 1021cb0ef41Sopenharmony_ci if (values.verbose) { 1031cb0ef41Sopenharmony_ci console.log(`Trying ${candidateURL}`); 1041cb0ef41Sopenharmony_ci } 1051cb0ef41Sopenharmony_ci const response = await fetch(candidateURL, { method: 'HEAD' }); 1061cb0ef41Sopenharmony_ci if (response.ok) { 1071cb0ef41Sopenharmony_ci return candidate[kNSSVersion]; 1081cb0ef41Sopenharmony_ci } 1091cb0ef41Sopenharmony_ci } 1101cb0ef41Sopenharmony_ci}; 1111cb0ef41Sopenharmony_ci 1121cb0ef41Sopenharmony_ciconst pastRelease = (r) => { 1131cb0ef41Sopenharmony_ci return r[kNSSDate] < now; 1141cb0ef41Sopenharmony_ci}; 1151cb0ef41Sopenharmony_ci 1161cb0ef41Sopenharmony_ciconst options = { 1171cb0ef41Sopenharmony_ci help: { 1181cb0ef41Sopenharmony_ci type: 'boolean', 1191cb0ef41Sopenharmony_ci }, 1201cb0ef41Sopenharmony_ci file: { 1211cb0ef41Sopenharmony_ci short: 'f', 1221cb0ef41Sopenharmony_ci type: 'string', 1231cb0ef41Sopenharmony_ci }, 1241cb0ef41Sopenharmony_ci verbose: { 1251cb0ef41Sopenharmony_ci short: 'v', 1261cb0ef41Sopenharmony_ci type: 'boolean', 1271cb0ef41Sopenharmony_ci }, 1281cb0ef41Sopenharmony_ci}; 1291cb0ef41Sopenharmony_ciconst { 1301cb0ef41Sopenharmony_ci positionals, 1311cb0ef41Sopenharmony_ci values, 1321cb0ef41Sopenharmony_ci} = parseArgs({ 1331cb0ef41Sopenharmony_ci allowPositionals: true, 1341cb0ef41Sopenharmony_ci options, 1351cb0ef41Sopenharmony_ci}); 1361cb0ef41Sopenharmony_ci 1371cb0ef41Sopenharmony_ciif (values.help) { 1381cb0ef41Sopenharmony_ci console.log(`Usage: ${basename(__filename)} [OPTION]... [VERSION]...`); 1391cb0ef41Sopenharmony_ci console.log(); 1401cb0ef41Sopenharmony_ci console.log('Updates certdata.txt to NSS VERSION (most recent release by default).'); 1411cb0ef41Sopenharmony_ci console.log(''); 1421cb0ef41Sopenharmony_ci console.log(' -f, --file=FILE writes a commit message reflecting the change to the'); 1431cb0ef41Sopenharmony_ci console.log(' specified FILE'); 1441cb0ef41Sopenharmony_ci console.log(' -v, --verbose writes progress to stdout'); 1451cb0ef41Sopenharmony_ci console.log(' --help display this help and exit'); 1461cb0ef41Sopenharmony_ci process.exit(0); 1471cb0ef41Sopenharmony_ci} 1481cb0ef41Sopenharmony_ci 1491cb0ef41Sopenharmony_ciconst scheduleURL = 'https://wiki.mozilla.org/NSS:Release_Versions'; 1501cb0ef41Sopenharmony_ciif (values.verbose) { 1511cb0ef41Sopenharmony_ci console.log(`Fetching NSS release schedule from ${scheduleURL}`); 1521cb0ef41Sopenharmony_ci} 1531cb0ef41Sopenharmony_ciconst schedule = await fetch(scheduleURL); 1541cb0ef41Sopenharmony_ciif (!schedule.ok) { 1551cb0ef41Sopenharmony_ci console.error(`Failed to fetch ${scheduleURL}: ${schedule.status}: ${schedule.statusText}`); 1561cb0ef41Sopenharmony_ci process.exit(-1); 1571cb0ef41Sopenharmony_ci} 1581cb0ef41Sopenharmony_ciconst scheduleText = await schedule.text(); 1591cb0ef41Sopenharmony_ciconst nssReleases = getReleases(scheduleText); 1601cb0ef41Sopenharmony_ci 1611cb0ef41Sopenharmony_ci// Retrieve metadata for the NSS release being updated to. 1621cb0ef41Sopenharmony_ciconst version = positionals[0] ?? await getLatestVersion(nssReleases); 1631cb0ef41Sopenharmony_ciconst release = nssReleases.find((r) => { 1641cb0ef41Sopenharmony_ci return new RegExp(`^${version.replace('.', '\\.')}\\b`).test(r[kNSSVersion]); 1651cb0ef41Sopenharmony_ci}); 1661cb0ef41Sopenharmony_ciif (!pastRelease(release)) { 1671cb0ef41Sopenharmony_ci console.warn(`Warning: NSS ${version} is not due to be released until ${formatDate(release[kNSSDate])}`); 1681cb0ef41Sopenharmony_ci} 1691cb0ef41Sopenharmony_ciif (values.verbose) { 1701cb0ef41Sopenharmony_ci console.log('Found NSS version:'); 1711cb0ef41Sopenharmony_ci console.log(release); 1721cb0ef41Sopenharmony_ci} 1731cb0ef41Sopenharmony_ci 1741cb0ef41Sopenharmony_ci// Fetch certdata.txt and overwrite the local copy. 1751cb0ef41Sopenharmony_ciconst certdataURL = getCertdataURL(version); 1761cb0ef41Sopenharmony_ciif (values.verbose) { 1771cb0ef41Sopenharmony_ci console.log(`Fetching ${certdataURL}`); 1781cb0ef41Sopenharmony_ci} 1791cb0ef41Sopenharmony_ciconst checkoutDir = join(__filename, '..', '..', '..'); 1801cb0ef41Sopenharmony_ciconst certdata = await fetch(certdataURL); 1811cb0ef41Sopenharmony_ciconst certdataFile = join(checkoutDir, 'tools', 'certdata.txt'); 1821cb0ef41Sopenharmony_ciif (!certdata.ok) { 1831cb0ef41Sopenharmony_ci console.error(`Failed to fetch ${certdataURL}: ${certdata.status}: ${certdata.statusText}`); 1841cb0ef41Sopenharmony_ci process.exit(-1); 1851cb0ef41Sopenharmony_ci} 1861cb0ef41Sopenharmony_ciif (values.verbose) { 1871cb0ef41Sopenharmony_ci console.log(`Writing ${certdataFile}`); 1881cb0ef41Sopenharmony_ci} 1891cb0ef41Sopenharmony_ciawait pipeline(certdata.body, createWriteStream(certdataFile)); 1901cb0ef41Sopenharmony_ci 1911cb0ef41Sopenharmony_ci// Run tools/mk-ca-bundle.pl to generate src/node_root_certs.h. 1921cb0ef41Sopenharmony_ciif (values.verbose) { 1931cb0ef41Sopenharmony_ci console.log('Running tools/mk-ca-bundle.pl'); 1941cb0ef41Sopenharmony_ci} 1951cb0ef41Sopenharmony_ciconst opts = { encoding: 'utf8' }; 1961cb0ef41Sopenharmony_ciconst mkCABundleTool = join(checkoutDir, 'tools', 'mk-ca-bundle.pl'); 1971cb0ef41Sopenharmony_ciconst mkCABundleOut = execFileSync(mkCABundleTool, 1981cb0ef41Sopenharmony_ci values.verbose ? [ '-v' ] : [], 1991cb0ef41Sopenharmony_ci opts); 2001cb0ef41Sopenharmony_ciif (values.verbose) { 2011cb0ef41Sopenharmony_ci console.log(mkCABundleOut); 2021cb0ef41Sopenharmony_ci} 2031cb0ef41Sopenharmony_ci 2041cb0ef41Sopenharmony_ci// Determine certificates added and/or removed. 2051cb0ef41Sopenharmony_ciconst certHeaderFile = relative(process.cwd(), join(checkoutDir, 'src', 'node_root_certs.h')); 2061cb0ef41Sopenharmony_ciconst diff = execFileSync('git', [ 'diff-files', '-u', '--', certHeaderFile ], opts); 2071cb0ef41Sopenharmony_ciif (values.verbose) { 2081cb0ef41Sopenharmony_ci console.log(diff); 2091cb0ef41Sopenharmony_ci} 2101cb0ef41Sopenharmony_ciconst certsAddedRE = /^\+\/\* (.*) \*\//gm; 2111cb0ef41Sopenharmony_ciconst certsRemovedRE = /^-\/\* (.*) \*\//gm; 2121cb0ef41Sopenharmony_ciconst added = [ ...diff.matchAll(certsAddedRE) ].map((m) => m[1]); 2131cb0ef41Sopenharmony_ciconst removed = [ ...diff.matchAll(certsRemovedRE) ].map((m) => m[1]); 2141cb0ef41Sopenharmony_ci 2151cb0ef41Sopenharmony_ciconst commitMsg = [ 2161cb0ef41Sopenharmony_ci `crypto: update root certificates to NSS ${release[kNSSVersion]}`, 2171cb0ef41Sopenharmony_ci '', 2181cb0ef41Sopenharmony_ci `This is the certdata.txt[0] from NSS ${release[kNSSVersion]}, released on ${formatDate(release[kNSSDate])}.`, 2191cb0ef41Sopenharmony_ci '', 2201cb0ef41Sopenharmony_ci `This is the version of NSS that ${release[kFirefoxDate] < now ? 'shipped' : 'will ship'} in Firefox ${release[kFirefoxVersion]} on`, 2211cb0ef41Sopenharmony_ci `${formatDate(release[kFirefoxDate])}.`, 2221cb0ef41Sopenharmony_ci '', 2231cb0ef41Sopenharmony_ci]; 2241cb0ef41Sopenharmony_ciif (added.length > 0) { 2251cb0ef41Sopenharmony_ci commitMsg.push('Certificates added:'); 2261cb0ef41Sopenharmony_ci commitMsg.push(...added.map((cert) => `- ${cert}`)); 2271cb0ef41Sopenharmony_ci commitMsg.push(''); 2281cb0ef41Sopenharmony_ci} 2291cb0ef41Sopenharmony_ciif (removed.length > 0) { 2301cb0ef41Sopenharmony_ci commitMsg.push('Certificates removed:'); 2311cb0ef41Sopenharmony_ci commitMsg.push(...removed.map((cert) => `- ${cert}`)); 2321cb0ef41Sopenharmony_ci commitMsg.push(''); 2331cb0ef41Sopenharmony_ci} 2341cb0ef41Sopenharmony_cicommitMsg.push(`[0] ${certdataURL}`); 2351cb0ef41Sopenharmony_ciconst delimiter = randomUUID(); 2361cb0ef41Sopenharmony_ciconst properties = [ 2371cb0ef41Sopenharmony_ci `NEW_VERSION=${release[kNSSVersion]}`, 2381cb0ef41Sopenharmony_ci `COMMIT_MSG<<${delimiter}`, 2391cb0ef41Sopenharmony_ci ...commitMsg, 2401cb0ef41Sopenharmony_ci delimiter, 2411cb0ef41Sopenharmony_ci '', 2421cb0ef41Sopenharmony_ci].join('\n'); 2431cb0ef41Sopenharmony_ciif (values.verbose) { 2441cb0ef41Sopenharmony_ci console.log(properties); 2451cb0ef41Sopenharmony_ci} 2461cb0ef41Sopenharmony_ciconst propertyFile = values.file; 2471cb0ef41Sopenharmony_ciif (propertyFile !== undefined) { 2481cb0ef41Sopenharmony_ci console.log(`Writing to ${propertyFile}`); 2491cb0ef41Sopenharmony_ci await pipeline(Readable.from(properties), createWriteStream(propertyFile)); 2501cb0ef41Sopenharmony_ci} 251