From 794cb51112f81281c11a1787194012d2e798c391 Mon Sep 17 00:00:00 2001 From: Richard Lau Date: Fri, 1 Nov 2024 16:36:45 +0000 Subject: [PATCH] tools: fix root certificate updater MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Determine the NSS version from actual Firefox releases, instead of attempting to parse a wiki page (which is sensitive to formatting changes and relies on the page being up to date). PR-URL: https://github.com/nodejs/node/pull/55681 Reviewed-By: Luigi Pinca Reviewed-By: Michaƫl Zasso Reviewed-By: Rafael Gonzaga --- tools/dep_updaters/update-root-certs.mjs | 186 +++++++++-------------- 1 file changed, 69 insertions(+), 117 deletions(-) diff --git a/tools/dep_updaters/update-root-certs.mjs b/tools/dep_updaters/update-root-certs.mjs index 64f3c88b851..a9e0a009f02 100644 --- a/tools/dep_updaters/update-root-certs.mjs +++ b/tools/dep_updaters/update-root-certs.mjs @@ -8,109 +8,78 @@ import { pipeline } from 'node:stream/promises'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; -// Constants for NSS release metadata. -const kNSSVersion = 'version'; -const kNSSDate = 'date'; -const kFirefoxVersion = 'firefoxVersion'; -const kFirefoxDate = 'firefoxDate'; - const __filename = fileURLToPath(import.meta.url); -const now = new Date(); - -const formatDate = (d) => { - const iso = d.toISOString(); - return iso.substring(0, iso.indexOf('T')); -}; const getCertdataURL = (version) => { const tag = `NSS_${version.replaceAll('.', '_')}_RTM`; - const certdataURL = `https://hg.mozilla.org/projects/nss/raw-file/${tag}/lib/ckfw/builtins/certdata.txt`; + const certdataURL = `https://raw.githubusercontent.com/nss-dev/nss/refs/tags/${tag}/lib/ckfw/builtins/certdata.txt`; return certdataURL; }; -const normalizeTD = (text = '') => { - // Remove whitespace and any HTML tags. - return text?.trim().replace(/<.*?>/g, ''); +const getFirefoxReleases = async (everything = false) => { + const releaseDataURL = `https://nucleus.mozilla.org/rna/all-releases.json${everything ? '?all=true' : ''}`; + if (values.verbose) { + console.log(`Fetching Firefox release data from ${releaseDataURL}.`); + } + const releaseData = await fetch(releaseDataURL); + if (!releaseData.ok) { + console.error(`Failed to fetch ${releaseDataURL}: ${releaseData.status}: ${releaseData.statusText}.`); + process.exit(-1); + } + return (await releaseData.json()).filter((release) => { + // We're only interested in public releases of Firefox. + return (release.product === 'Firefox' && release.channel === 'Release' && release.is_public === true); + }).sort((a, b) => { + // Sort results by release date. + return new Date(b.release_date) - new Date(a.release_date); + }); }; -const getReleases = (text) => { - const releases = []; - const tableRE = /]+>([\S\s]*?)<\/table>/g; - const tableRowRE = /]*>([\S\s]*?)<\/tr>/g; - const tableHeaderRE = /
]*>([\S\s]*?)<\/th>/g; - const tableDataRE = /]*>([\S\s]*?)<\/td>/g; - for (const table of text.matchAll(tableRE)) { - const columns = {}; - const matches = table[1].matchAll(tableRowRE); - // First row has the table header. - let row = matches.next(); - if (row.done) { - continue; - } - const headers = Array.from(row.value[1].matchAll(tableHeaderRE), (m) => m[1]); - if (headers.length > 0) { - for (let i = 0; i < headers.length; i++) { - if (/NSS version/i.test(headers[i])) { - columns[kNSSVersion] = i; - } else if (/Release.*from branch/i.test(headers[i])) { - columns[kNSSDate] = i; - } else if (/Firefox version/i.test(headers[i])) { - columns[kFirefoxVersion] = i; - } else if (/Firefox release date/i.test(headers[i])) { - columns[kFirefoxDate] = i; - } + +const getFirefoxRelease = async (version) => { + let releases = await getFirefoxReleases(); + let found; + if (version === undefined) { + // No version specified. Find the most recent. + if (releases.length > 0) { + found = releases[0]; + } else { + if (values.verbose) { + console.log('Unable to find release data for Firefox. Searching full release data.'); } + releases = await getFirefoxReleases(true); + found = releases[0]; } - // Filter out "NSS Certificate bugs" table. - if (columns[kNSSDate] === undefined) { - continue; - } - // Scrape releases. - row = matches.next(); - while (!row.done) { - const cells = Array.from(row.value[1].matchAll(tableDataRE), (m) => m[1]); - const release = {}; - release[kNSSVersion] = normalizeTD(cells[columns[kNSSVersion]]); - release[kNSSDate] = new Date(normalizeTD(cells[columns[kNSSDate]])); - release[kFirefoxVersion] = normalizeTD(cells[columns[kFirefoxVersion]]); - release[kFirefoxDate] = new Date(normalizeTD(cells[columns[kFirefoxDate]])); - releases.push(release); - row = matches.next(); + } else { + // Search for the specified release. + found = releases.find((release) => release.version === version); + if (found === undefined) { + if (values.verbose) { + console.log(`Unable to find release data for Firefox ${version}. Searching full release data.`); + } + releases = await getFirefoxReleases(true); + found = releases.find((release) => release.version === version); } } - return releases; + return found; }; -const getLatestVersion = async (releases) => { - const arrayNumberSortDescending = (x, y, i) => { - if (x[i] === undefined && y[i] === undefined) { - return 0; - } else if (x[i] === y[i]) { - return arrayNumberSortDescending(x, y, i + 1); - } - return (y[i] ?? 0) - (x[i] ?? 0); - }; - const extractVersion = (t) => { - return t[kNSSVersion].split('.').map((n) => parseInt(n)); - }; - const releaseSorter = (x, y) => { - return arrayNumberSortDescending(extractVersion(x), extractVersion(y), 0); - }; - // Return the most recent certadata.txt that exists on the server. - const sortedReleases = releases.sort(releaseSorter).filter(pastRelease); - for (const candidate of sortedReleases) { - const candidateURL = getCertdataURL(candidate[kNSSVersion]); - if (values.verbose) { - console.log(`Trying ${candidateURL}`); - } - const response = await fetch(candidateURL, { method: 'HEAD' }); - if (response.ok) { - return candidate[kNSSVersion]; - } +const getNSSVersion = async (release) => { + const latestFirefox = release.version; + const firefoxTag = `FIREFOX_${latestFirefox.replace('.', '_')}_RELEASE`; + const tagInfoURL = `https://hg.mozilla.org/releases/mozilla-release/raw-file/${firefoxTag}/security/nss/TAG-INFO`; + if (values.verbose) { + console.log(`Fetching NSS tag from ${tagInfoURL}.`); } -}; - -const pastRelease = (r) => { - return r[kNSSDate] < now; + const tagInfo = await fetch(tagInfoURL); + if (!tagInfo.ok) { + console.error(`Failed to fetch ${tagInfoURL}: ${tagInfo.status}: ${tagInfo.statusText}`); + } + const tag = await tagInfo.text(); + if (values.verbose) { + console.log(`Found tag ${tag}.`); + } + // Tag will be of form `NSS_x_y_RTM`. Convert to `x.y`. + return tag.split('_').slice(1, -1).join('.'); }; const options = { @@ -135,9 +104,9 @@ const { }); if (values.help) { - console.log(`Usage: ${basename(__filename)} [OPTION]... [VERSION]...`); + console.log(`Usage: ${basename(__filename)} [OPTION]... [RELEASE]...`); console.log(); - console.log('Updates certdata.txt to NSS VERSION (most recent release by default).'); + console.log('Updates certdata.txt to NSS version contained in Firefox RELEASE (default: most recent release).'); console.log(''); console.log(' -f, --file=FILE writes a commit message reflecting the change to the'); console.log(' specified FILE'); @@ -146,29 +115,11 @@ if (values.help) { process.exit(0); } -const scheduleURL = 'https://wiki.mozilla.org/NSS:Release_Versions'; -if (values.verbose) { - console.log(`Fetching NSS release schedule from ${scheduleURL}`); -} -const schedule = await fetch(scheduleURL); -if (!schedule.ok) { - console.error(`Failed to fetch ${scheduleURL}: ${schedule.status}: ${schedule.statusText}`); - process.exit(-1); -} -const scheduleText = await schedule.text(); -const nssReleases = getReleases(scheduleText); - +const firefoxRelease = await getFirefoxRelease(positionals[0]); // Retrieve metadata for the NSS release being updated to. -const version = positionals[0] ?? await getLatestVersion(nssReleases); -const release = nssReleases.find((r) => { - return new RegExp(`^${version.replace('.', '\\.')}\\b`).test(r[kNSSVersion]); -}); -if (!pastRelease(release)) { - console.warn(`Warning: NSS ${version} is not due to be released until ${formatDate(release[kNSSDate])}`); -} +const version = await getNSSVersion(firefoxRelease); if (values.verbose) { - console.log('Found NSS version:'); - console.log(release); + console.log(`Updating to NSS version ${version}`); } // Fetch certdata.txt and overwrite the local copy. @@ -213,14 +164,15 @@ const added = [ ...diff.matchAll(certsAddedRE) ].map((m) => m[1]); const removed = [ ...diff.matchAll(certsRemovedRE) ].map((m) => m[1]); const commitMsg = [ - `crypto: update root certificates to NSS ${release[kNSSVersion]}`, + `crypto: update root certificates to NSS ${version}`, '', - `This is the certdata.txt[0] from NSS ${release[kNSSVersion]}, released on ${formatDate(release[kNSSDate])}.`, - '', - `This is the version of NSS that ${release[kFirefoxDate] < now ? 'shipped' : 'will ship'} in Firefox ${release[kFirefoxVersion]} on`, - `${formatDate(release[kFirefoxDate])}.`, + `This is the certdata.txt[0] from NSS ${version}.`, '', ]; +if (firefoxRelease) { + commitMsg.push(`This is the version of NSS that shipped in Firefox ${firefoxRelease.version} on ${firefoxRelease.release_date}.`); + commitMsg.push(''); +} if (added.length > 0) { commitMsg.push('Certificates added:'); commitMsg.push(...added.map((cert) => `- ${cert}`)); @@ -234,7 +186,7 @@ if (removed.length > 0) { commitMsg.push(`[0] ${certdataURL}`); const delimiter = randomUUID(); const properties = [ - `NEW_VERSION=${release[kNSSVersion]}`, + `NEW_VERSION=${version}`, `COMMIT_MSG<<${delimiter}`, ...commitMsg, delimiter,