diff --git a/scripts/install-apt.sh b/scripts/install-apt.sh index a64e185..a7ee06e 100755 --- a/scripts/install-apt.sh +++ b/scripts/install-apt.sh @@ -104,9 +104,19 @@ fi # Newer apt verifies with sqv (Sequoia), which rejects the signature Forgejo # currently produces for its Debian registry (malformed Ed25519 MPI encoding # in the upstream signing library). Try the properly signed source first so -# this heals automatically once the forge is fixed; otherwise fall back to -# [trusted=yes] — package integrity then relies on HTTPS to our own forge. -if ! update_only_source "$LIST"; then +# this heals automatically once the forge is fixed. Only that signature-error +# class permits the compatibility fallback; auth, network, and other failures +# must leave verification enabled and retain apt's original diagnostic. +if update_output="$(update_only_source "$LIST" 2>&1)"; then + printf '%s\n' "$update_output" +else + update_status=$? + if ! grep -Eiq \ + 'NO_PUBKEY|EXPKEYSIG|BADSIG|signatures? (could not|couldn.t) be verified|signature (verification )?(failed|failure|error|invalid)|repository .*not signed|is not signed' \ + <<<"$update_output"; then + printf '%s\n' "$update_output" >&2 + exit "$update_status" + fi echo echo "WARNING: signature verification failed (known Forgejo registry issue" >&2 echo "with sqv-based apt). Falling back to [trusted=yes]; transport" >&2 diff --git a/test/install-apt.test.js b/test/install-apt.test.js index f411c39..aea7648 100644 --- a/test/install-apt.test.js +++ b/test/install-apt.test.js @@ -13,12 +13,13 @@ const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh'); // candAfterUpdate Candidate after any `apt-get update` // candAfterNodesource Candidate after an update once nodesource.list exists // releaseStatus HTTP status curl reports for the registry Release file +// sourceUpdateError stderr and exit 100 for the first signed stoke update // The apt-cache stub localizes the "Candidate:" label unless LC_ALL=C is set, // so every scenario doubles as a regression test for locale-safe parsing. const cleanups = []; process.on('exit', () => { for (const dir of cleanups) fs.rmSync(dir, { recursive: true, force: true }); }); -function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus }) { +function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError }) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-')); cleanups.push(root); const bin = path.join(root, 'bin'); @@ -58,8 +59,16 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi ].join('\n')); stub('apt-get', [ 'echo "apt-get $*" >> "$STATE_DIR/apt-get.log"', + 'source_list=""', + 'for a in "$@"; do', + ' case "$a" in Dir::Etc::sourcelist=*) source_list="${a#*=}";; esac', + 'done', 'for a in "$@"; do', ' if [ "$a" = update ]; then', + ' if [ -n "$source_list" ] && grep -q "signed-by=" "$source_list" && [ -n "${SOURCE_UPDATE_ERROR:-}" ]; then', + ' printf "%s\\n" "$SOURCE_UPDATE_ERROR" >&2', + ' exit 100', + ' fi', ' if [ -e "$STOKE_APT_ETC/sources.list.d/nodesource.list" ] && [ -n "${CAND_AFTER_NODESOURCE:-}" ]; then', ' echo "$CAND_AFTER_NODESOURCE" > "$STATE_DIR/candidate"', ' elif [ -n "${CAND_AFTER_UPDATE:-}" ]; then', @@ -82,6 +91,7 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi CAND_AFTER_UPDATE: candAfterUpdate || '', CAND_AFTER_NODESOURCE: candAfterNodesource || '', RELEASE_STATUS: releaseStatus || '', + SOURCE_UPDATE_ERROR: sourceUpdateError || '', LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C }, }); @@ -96,6 +106,7 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi nodesourceKey: read(path.join(aptEtc, 'keyrings', 'nodesource.asc')), nodesourceKeyMode: mode(path.join(aptEtc, 'keyrings', 'nodesource.asc')), forgeKeyMode: mode(path.join(aptEtc, 'keyrings', 'forgejo-heavy-duty.asc')), + forgeList: read(path.join(aptEtc, 'sources.list.d', 'forgejo-heavy-duty.list')), aptGetLog: read(path.join(state, 'apt-get.log')) || '', }; // Drop the throwaway tree after we have read everything we need. @@ -171,3 +182,26 @@ test('registry Release file present: proceeds with the install', () => { assert.equal(s.res.status, 0, s.res.stderr); assert.match(s.aptGetLog, /install -y stoke/); }); + +test('signature verification failure alone may use the trusted compatibility fallback', () => { + const s = runScenario({ + candInitial: '22.23.1-1nodesource1', + sourceUpdateError: 'W: GPG error: signatures could not be verified: NO_PUBKEY DEADBEEF\nE: The repository is not signed.', + }); + assert.equal(s.res.status, 0, s.res.stderr); + assert.match(s.forgeList, /\[trusted=yes\]/); + assert.match(s.aptGetLog, /install -y stoke/); +}); + +test('network update failure stays fatal and never disables signature verification', () => { + const failure = 'Temporary failure resolving forgejo.heavyduty.builders'; + const s = runScenario({ + candInitial: '22.23.1-1nodesource1', + sourceUpdateError: failure, + }); + assert.notEqual(s.res.status, 0); + assert.match(s.res.stderr, new RegExp(failure)); + assert.match(s.forgeList, /\[signed-by=/); + assert.doesNotMatch(s.forgeList, /trusted=yes/); + assert.doesNotMatch(s.aptGetLog, /install -y stoke/); +});