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