Compare commits

..

No commits in common. "fb5cb4746bce564b173e6b741014aa4f0e481dd1" and "25c7267ecd54d6e17c4212f4e59b70aff8e95806" have entirely different histories.

4 changed files with 19 additions and 269 deletions

View file

@ -13,38 +13,13 @@ A command-line interface for [Forgejo](https://forgejo.org/), built with [Comman
### With apt (Debian/Ubuntu — recommended) ### With apt (Debian/Ubuntu — recommended)
The package is published to the public Debian registry of the forge itself. The package is published to the Debian registry of the forge itself. One-time setup:
One-time setup:
```bash ```bash
curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash
``` ```
If a private registry or a `FORGE_URL=` override requires authentication, or manually. First add the forge's registry as an apt source:
download the installer and supply a Forgejo login and package-readable token:
```bash
export FORGE_USER=your-forgejo-login
read -rsp 'Forgejo token: ' FORGE_TOKEN && echo && export FORGE_TOKEN
curl -fsSLo /tmp/stoke-install-apt.sh \
https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh
sudo --preserve-env=FORGE_USER,FORGE_TOKEN bash /tmp/stoke-install-apt.sh
unset FORGE_TOKEN
```
The authenticated path keeps credentials out of the source URL in a
root-readable apt auth file. To configure that file manually before adding the
source:
```bash
sudo install -d -m 0755 /etc/apt/auth.conf.d
sudo install -m 0600 /dev/null /etc/apt/auth.conf.d/forgejo-heavy-duty.conf
printf 'machine forgejo.heavyduty.builders\nlogin %s\npassword %s\n' \
"$FORGE_USER" "$FORGE_TOKEN" \
| sudo tee /etc/apt/auth.conf.d/forgejo-heavy-duty.conf >/dev/null
```
Then add the forge's registry as an apt source:
```bash ```bash
sudo install -d /etc/apt/keyrings sudo install -d /etc/apt/keyrings
@ -71,28 +46,7 @@ sudo apt-get update && sudo apt-get install stoke
Upgrades then arrive through regular `apt-get upgrade`. `install-apt.sh` performs all of the above, adding the NodeSource repository only when no already-configured apt source offers a new-enough nodejs. Upgrades then arrive through regular `apt-get upgrade`. `install-apt.sh` performs all of the above, adding the NodeSource repository only when no already-configured apt source offers a new-enough nodejs.
Note: apt releases that verify OpenPGP with `sqv` (Debian 13+, apt >= 2.9) Note: apt releases that verify OpenPGP with `sqv` (Debian 13+, apt >= 2.9) currently reject the signature Forgejo generates for its Debian registry (an upstream signing bug). `install-apt.sh` detects this and falls back to a `[trusted=yes]` source — integrity then relies on HTTPS to the forge. The script prefers the signed source, so setups heal automatically once the forge is fixed.
may reject signatures produced by affected Forgejo versions. By default,
`install-apt.sh` refuses that signature failure and removes the Forge source;
authentication, network, and all other update failures are also fatal and never
disable verification.
If the installer reports the known `sqv` parsing failure and you deliberately
accept HTTPS-only integrity without OpenPGP verification, opt in on a second
run:
```bash
export STOKE_ALLOW_UNVERIFIED_APT=1
curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash
unset STOKE_ALLOW_UNVERIFIED_APT
```
For a private registry, re-run the downloaded installer with
`sudo --preserve-env=FORGE_USER,FORGE_TOKEN,STOKE_ALLOW_UNVERIFIED_APT` instead.
This exact opt-in is the only path in the installer that writes a
`[trusted=yes]` source. The installer prints the security trade-off again when
it takes that path.
As a fallback, each release also has the `.deb` attached for direct install: `sudo dpkg -i stoke_<version>_all.deb`. As a fallback, each release also has the `.deb` attached for direct install: `sudo dpkg -i stoke_<version>_all.deb`.

View file

@ -1 +0,0 @@
- Private apt installs keep credentials out of source URLs, refuse unverifiable registries by default, and require an explicit HTTPS-only opt-in to disable signature checks. (#1).

View file

@ -9,7 +9,6 @@
# Usage: # Usage:
# ./scripts/install-apt.sh # ./scripts/install-apt.sh
# FORGE_URL=... OWNER=... ./scripts/install-apt.sh # non-default instance # FORGE_URL=... OWNER=... ./scripts/install-apt.sh # non-default instance
# STOKE_ALLOW_UNVERIFIED_APT=1 ./scripts/install-apt.sh # explicit HTTPS-only opt-in
# #
# Run as root or as a user with sudo. # Run as root or as a user with sudo.
@ -19,27 +18,12 @@ FORGE_URL="${FORGE_URL:-https://forgejo.heavyduty.builders}"
OWNER="${OWNER:-heavy-duty}" OWNER="${OWNER:-heavy-duty}"
DISTRIBUTION="${DISTRIBUTION:-stable}" DISTRIBUTION="${DISTRIBUTION:-stable}"
COMPONENT="${COMPONENT:-main}" COMPONENT="${COMPONENT:-main}"
FORGE_USER="${FORGE_USER:-}"
FORGE_TOKEN="${FORGE_TOKEN:-}"
ALLOW_UNVERIFIED="${STOKE_ALLOW_UNVERIFIED_APT:-}"
# Where apt configuration lives; overridable so tests can run against a # Where apt configuration lives; overridable so tests can run against a
# throwaway directory instead of the real /etc/apt. # throwaway directory instead of the real /etc/apt.
APT_ETC="${STOKE_APT_ETC:-/etc/apt}" APT_ETC="${STOKE_APT_ETC:-/etc/apt}"
KEYRING="$APT_ETC/keyrings/forgejo-$OWNER.asc" KEYRING="$APT_ETC/keyrings/forgejo-$OWNER.asc"
LIST="$APT_ETC/sources.list.d/forgejo-$OWNER.list" LIST="$APT_ETC/sources.list.d/forgejo-$OWNER.list"
AUTH="$APT_ETC/auth.conf.d/forgejo-$OWNER.conf"
if { [ -n "$FORGE_USER" ] && [ -z "$FORGE_TOKEN" ]; } \
|| { [ -z "$FORGE_USER" ] && [ -n "$FORGE_TOKEN" ]; }; then
echo "error: FORGE_USER and FORGE_TOKEN must be set together" >&2
exit 1
fi
if [ -n "$ALLOW_UNVERIFIED" ] && [ "$ALLOW_UNVERIFIED" != "1" ]; then
echo "error: STOKE_ALLOW_UNVERIFIED_APT must be unset or exactly 1" >&2
exit 1
fi
SUDO="" SUDO=""
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
@ -47,18 +31,6 @@ if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo" SUDO="sudo"
fi fi
CURL_AUTH=()
if [ -n "$FORGE_USER" ] && [ -n "$FORGE_TOKEN" ]; then
forge_host="${FORGE_URL#*://}"
forge_host="${forge_host%%/*}"
$SUDO install -d -m 0755 "$APT_ETC/auth.conf.d"
$SUDO install -m 0600 /dev/null "$AUTH"
printf 'machine %s\nlogin %s\npassword %s\n' \
"$forge_host" "$FORGE_USER" "$FORGE_TOKEN" \
| $SUDO tee "$AUTH" >/dev/null
CURL_AUTH=(--netrc-file "$AUTH")
fi
update_only_source() { update_only_source() {
$SUDO apt-get update \ $SUDO apt-get update \
-o Dir::Etc::sourcelist="$1" \ -o Dir::Etc::sourcelist="$1" \
@ -108,8 +80,8 @@ ensure_nodejs_source() {
} }
echo "Adding APT source for $FORGE_URL/$OWNER ..." echo "Adding APT source for $FORGE_URL/$OWNER ..."
$SUDO install -d -m 0755 "$APT_ETC/keyrings" "$APT_ETC/sources.list.d" $SUDO install -d -m 0755 "$APT_ETC/keyrings"
$SUDO curl "${CURL_AUTH[@]}" -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null
echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \
| $SUDO tee "$LIST" >/dev/null | $SUDO tee "$LIST" >/dev/null
# tee inherits our umask; apt's unprivileged _apt user must be able to # tee inherits our umask; apt's unprivileged _apt user must be able to
@ -122,7 +94,7 @@ $SUDO chmod 0644 "$KEYRING" "$LIST"
# is fatal; any other curl outcome (e.g. a network hiccup) is left for # is fatal; any other curl outcome (e.g. a network hiccup) is left for
# apt-get update to report. # apt-get update to report.
RELEASE_URL="$FORGE_URL/api/packages/$OWNER/debian/dists/$DISTRIBUTION/Release" RELEASE_URL="$FORGE_URL/api/packages/$OWNER/debian/dists/$DISTRIBUTION/Release"
if [ "$($SUDO curl "${CURL_AUTH[@]}" -sSL -o /dev/null -w '%{http_code}' "$RELEASE_URL" || true)" = "404" ]; then if [ "$(curl -sSL -o /dev/null -w '%{http_code}' "$RELEASE_URL" || true)" = "404" ]; then
echo "error: no stoke package has been published to the $OWNER Debian registry yet" >&2 echo "error: no stoke package has been published to the $OWNER Debian registry yet" >&2
echo "($RELEASE_URL returned 404)." >&2 echo "($RELEASE_URL returned 404)." >&2
echo "Install stoke via npm or manually instead — see the README." >&2 echo "Install stoke via npm or manually instead — see the README." >&2
@ -130,37 +102,16 @@ if [ "$($SUDO curl "${CURL_AUTH[@]}" -sSL -o /dev/null -w '%{http_code}' "$RELEA
fi fi
# Newer apt verifies with sqv (Sequoia), which rejects the signature Forgejo # Newer apt verifies with sqv (Sequoia), which rejects the signature Forgejo
# currently produces for its Debian registry. The cause is recorded on #1; it # currently produces for its Debian registry (malformed Ed25519 MPI encoding
# is not the key algorithm, because the registry serves an RSA-2048 signing # in the upstream signing library). Try the properly signed source first so
# key (`gpg --list-packets` on repository.key reports `algo 1` with a # this heals automatically once the forge is fixed; otherwise fall back to
# 2048-bit pkey[0]). Try the properly signed source first so this heals # [trusted=yes] — package integrity then relies on HTTPS to our own forge.
# automatically once the forge is fixed. Only the exact live sqv failure, plus if ! update_only_source "$LIST"; then
# the user's exact opt-in, permits an unverified source; auth, network, and echo
# other failures must leave verification enabled and retain apt's diagnostic. echo "WARNING: signature verification failed (known Forgejo registry issue" >&2
if update_output="$(update_only_source "$LIST" 2>&1)"; then echo "with sqv-based apt). Falling back to [trusted=yes]; transport" >&2
printf '%s\n' "$update_output" echo "security is provided by HTTPS to $FORGE_URL." >&2
else echo
update_status=$?
if ! grep -Fqi '/usr/bin/sqv' <<<"$update_output" \
|| ! grep -Fqi 'Malformed MPI' <<<"$update_output"; then
printf '%s\n' "$update_output" >&2
exit "$update_status"
fi
if [ "$ALLOW_UNVERIFIED" != "1" ]; then
$SUDO rm -f "$LIST"
echo "error: apt could not verify the Forgejo registry signature." >&2
echo "On sqv-based apt, the known cause is that sqv-based apt cannot parse" >&2
echo "the Forgejo registry signature, although gpgv-based apt accepts it." >&2
echo "No apt source was left behind." >&2
echo "If you knowingly accept HTTPS-only integrity, re-run with" >&2
echo "STOKE_ALLOW_UNVERIFIED_APT=1 to disable OpenPGP verification." >&2
exit "$update_status"
fi
echo >&2
echo "WARNING: OpenPGP signature verification is disabled for the Forgejo" >&2
echo "registry at $FORGE_URL. You explicitly accepted HTTPS-only integrity" >&2
echo "by setting STOKE_ALLOW_UNVERIFIED_APT=1." >&2
echo >&2
echo "deb [trusted=yes] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ echo "deb [trusted=yes] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \
| $SUDO tee "$LIST" >/dev/null | $SUDO tee "$LIST" >/dev/null
$SUDO chmod 0644 "$LIST" $SUDO chmod 0644 "$LIST"

View file

@ -13,16 +13,12 @@ const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh');
// candAfterUpdate Candidate after any `apt-get update` // candAfterUpdate Candidate after any `apt-get update`
// candAfterNodesource Candidate after an update once nodesource.list exists // candAfterNodesource Candidate after an update once nodesource.list exists
// releaseStatus HTTP status curl reports for the registry Release file // releaseStatus HTTP status curl reports for the registry Release file
// sourceUpdateError stderr and exit 100 for the first signed stoke update
// forgeUser/token private-registry credentials
// allowUnverified explicit HTTPS-only integrity opt-in
// precreateSourcesDir whether the throwaway apt root already has sources.list.d
// The apt-cache stub localizes the "Candidate:" label unless LC_ALL=C is set, // 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. // so every scenario doubles as a regression test for locale-safe parsing.
const cleanups = []; const cleanups = [];
process.on('exit', () => { for (const dir of cleanups) fs.rmSync(dir, { recursive: true, force: true }); }); process.on('exit', () => { for (const dir of cleanups) fs.rmSync(dir, { recursive: true, force: true }); });
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError, forgeUser, forgeToken, allowUnverified, precreateSourcesDir = true }) { function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus }) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-')); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-'));
cleanups.push(root); cleanups.push(root);
const bin = path.join(root, 'bin'); const bin = path.join(root, 'bin');
@ -30,7 +26,7 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
const aptEtc = path.join(root, 'etc', 'apt'); const aptEtc = path.join(root, 'etc', 'apt');
fs.mkdirSync(bin, { recursive: true }); fs.mkdirSync(bin, { recursive: true });
fs.mkdirSync(state, { recursive: true }); fs.mkdirSync(state, { recursive: true });
if (precreateSourcesDir) fs.mkdirSync(path.join(aptEtc, 'sources.list.d'), { recursive: true }); fs.mkdirSync(path.join(aptEtc, 'sources.list.d'), { recursive: true });
fs.writeFileSync(path.join(state, 'candidate'), candInitial); fs.writeFileSync(path.join(state, 'candidate'), candInitial);
if (preexistingNodesourceList !== undefined) { if (preexistingNodesourceList !== undefined) {
fs.writeFileSync(path.join(aptEtc, 'sources.list.d', 'nodesource.list'), preexistingNodesourceList); fs.writeFileSync(path.join(aptEtc, 'sources.list.d', 'nodesource.list'), preexistingNodesourceList);
@ -43,30 +39,10 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
// Force the non-root path so every mutation goes through the sudo stub. // Force the non-root path so every mutation goes through the sudo stub.
stub('id', 'echo 1000'); stub('id', 'echo 1000');
stub('sudo', 'SUDO_ACTIVE=1 exec "$@"'); stub('sudo', 'exec "$@"');
stub('tee', [
'for destination in "$@"; do',
' case "$destination" in',
' */auth.conf.d/forgejo-*.conf)',
' mode="$(stat -c %a "$destination" 2>/dev/null || true)"',
' if [ "$mode" != 600 ]; then',
' echo "credential destination was not mode 0600 before write" >&2',
' exit 78',
' fi',
' ;;',
' esac',
'done',
'exec /usr/bin/tee "$@"',
].join('\n'));
// Registry Release-file probes (URLs under /dists/) answer with the // Registry Release-file probes (URLs under /dists/) answer with the
// scenario's HTTP status; everything else is a key fetch. // scenario's HTTP status; everything else is a key fetch.
stub('curl', [ stub('curl', [
'uses_netrc=false',
'for a in "$@"; do [ "$a" = "--netrc-file" ] && uses_netrc=true; done',
'if [ "$uses_netrc" = true ] && [ "${SUDO_ACTIVE:-}" != 1 ]; then',
' echo "curl: root-owned netrc is unreadable without sudo" >&2',
' exit 77',
'fi',
'for a in "$@"; do', 'for a in "$@"; do',
' case "$a" in */dists/*) echo "${RELEASE_STATUS:-200}"; exit 0;; esac', ' case "$a" in */dists/*) echo "${RELEASE_STATUS:-200}"; exit 0;; esac',
'done', 'done',
@ -82,16 +58,8 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
].join('\n')); ].join('\n'));
stub('apt-get', [ stub('apt-get', [
'echo "apt-get $*" >> "$STATE_DIR/apt-get.log"', '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', 'for a in "$@"; do',
' if [ "$a" = update ]; then', ' 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', ' if [ -e "$STOKE_APT_ETC/sources.list.d/nodesource.list" ] && [ -n "${CAND_AFTER_NODESOURCE:-}" ]; then',
' echo "$CAND_AFTER_NODESOURCE" > "$STATE_DIR/candidate"', ' echo "$CAND_AFTER_NODESOURCE" > "$STATE_DIR/candidate"',
' elif [ -n "${CAND_AFTER_UPDATE:-}" ]; then', ' elif [ -n "${CAND_AFTER_UPDATE:-}" ]; then',
@ -114,10 +82,6 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
CAND_AFTER_UPDATE: candAfterUpdate || '', CAND_AFTER_UPDATE: candAfterUpdate || '',
CAND_AFTER_NODESOURCE: candAfterNodesource || '', CAND_AFTER_NODESOURCE: candAfterNodesource || '',
RELEASE_STATUS: releaseStatus || '', RELEASE_STATUS: releaseStatus || '',
SOURCE_UPDATE_ERROR: sourceUpdateError || '',
FORGE_USER: forgeUser || '',
FORGE_TOKEN: forgeToken || '',
STOKE_ALLOW_UNVERIFIED_APT: allowUnverified || '',
LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C
}, },
}); });
@ -132,9 +96,6 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
nodesourceKey: read(path.join(aptEtc, 'keyrings', 'nodesource.asc')), nodesourceKey: read(path.join(aptEtc, 'keyrings', 'nodesource.asc')),
nodesourceKeyMode: mode(path.join(aptEtc, 'keyrings', 'nodesource.asc')), nodesourceKeyMode: mode(path.join(aptEtc, 'keyrings', 'nodesource.asc')),
forgeKeyMode: mode(path.join(aptEtc, 'keyrings', 'forgejo-heavy-duty.asc')), forgeKeyMode: mode(path.join(aptEtc, 'keyrings', 'forgejo-heavy-duty.asc')),
forgeList: read(path.join(aptEtc, 'sources.list.d', 'forgejo-heavy-duty.list')),
forgeAuth: read(path.join(aptEtc, 'auth.conf.d', 'forgejo-heavy-duty.conf')),
forgeAuthMode: mode(path.join(aptEtc, 'auth.conf.d', 'forgejo-heavy-duty.conf')),
aptGetLog: read(path.join(state, 'apt-get.log')) || '', aptGetLog: read(path.join(state, 'apt-get.log')) || '',
}; };
// Drop the throwaway tree after we have read everything we need. // Drop the throwaway tree after we have read everything we need.
@ -210,118 +171,3 @@ test('registry Release file present: proceeds with the install', () => {
assert.equal(s.res.status, 0, s.res.stderr); assert.equal(s.res.status, 0, s.res.stderr);
assert.match(s.aptGetLog, /install -y stoke/); assert.match(s.aptGetLog, /install -y stoke/);
}); });
test('fresh apt root creates sources.list.d before writing the forge source', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
precreateSourcesDir: false,
});
assert.equal(s.res.status, 0, s.res.stderr);
assert.match(s.forgeList, /\[signed-by=/);
assert.match(s.aptGetLog, /install -y stoke/);
});
test('signature verification failure refuses by default and removes the forge source', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
sourceUpdateError: 'W: OpenPGP signature verification failed: Sub-process /usr/bin/sqv returned an error code (1), error message is: Verifying signature: Malformed MPI: leading bit is not set',
});
assert.notEqual(s.res.status, 0);
assert.equal(s.forgeList, null);
assert.match(s.res.stderr, /sqv-based apt cannot parse\s+the Forgejo registry signature/);
assert.match(s.res.stderr, /STOKE_ALLOW_UNVERIFIED_APT=1/);
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
});
test('exact opt-in permits an HTTPS-only forge source after signature failure', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
sourceUpdateError: 'W: OpenPGP signature verification failed: Sub-process /usr/bin/sqv returned an error code (1), error message is: Verifying signature: Malformed MPI: leading bit is not set',
allowUnverified: '1',
});
assert.equal(s.res.status, 0, s.res.stderr);
assert.match(s.forgeList, /\[trusted=yes\]/);
assert.match(s.res.stderr, /OpenPGP signature verification is disabled/);
assert.match(s.res.stderr, /HTTPS-only integrity/);
assert.match(s.aptGetLog, /install -y stoke/);
});
test('opt-in cannot bypass a missing signing key', () => {
const failure = 'W: GPG error: signatures could not be verified: NO_PUBKEY DEADBEEF\nE: The repository is not signed.';
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
sourceUpdateError: failure,
allowUnverified: '1',
});
assert.notEqual(s.res.status, 0);
assert.match(s.res.stderr, new RegExp(failure.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.match(s.forgeList, /\[signed-by=/);
assert.doesNotMatch(s.forgeList, /trusted=yes/);
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
});
test('opt-in cannot bypass sqv output without the known Malformed MPI failure', () => {
const failure = 'W: OpenPGP signature verification failed: Sub-process /usr/bin/sqv returned an error code (1): unexpected packet';
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
sourceUpdateError: failure,
allowUnverified: '1',
});
assert.notEqual(s.res.status, 0);
assert.match(s.res.stderr, new RegExp(failure.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.match(s.forgeList, /\[signed-by=/);
assert.doesNotMatch(s.forgeList, /trusted=yes/);
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
});
test('unrecognized opt-in value is rejected before configuring apt', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
allowUnverified: 'yes',
});
assert.notEqual(s.res.status, 0);
assert.match(s.res.stderr, /STOKE_ALLOW_UNVERIFIED_APT must be unset or exactly 1/);
assert.equal(s.forgeList, null);
assert.equal(s.aptGetLog, '');
});
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/);
});
test('private-registry credentials stay in a root-readable auth file, not the source URL', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
forgeUser: 'apt-user',
forgeToken: 'secret-token',
});
assert.equal(s.res.status, 0, s.res.stderr);
assert.equal(s.forgeAuthMode, 0o600);
assert.equal(s.forgeAuth, [
'machine forgejo.heavyduty.builders',
'login apt-user',
'password secret-token',
'',
].join('\n'));
assert.doesNotMatch(s.forgeList, /apt-user|secret-token/);
});
test('incomplete private-registry credentials fail before configuring apt', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
forgeUser: 'apt-user',
});
assert.notEqual(s.res.status, 0);
assert.match(s.res.stderr, /FORGE_USER and FORGE_TOKEN must be set together/);
assert.equal(s.forgeList, null);
assert.equal(s.aptGetLog, '');
});