Merge pull request 'fix: preserve apt signature verification' (#38) from build/1-apt-signed-install into main
All checks were successful
ci / test (push) Successful in 16s
All checks were successful
ci / test (push) Successful in 16s
Reviewed-on: #38 Reviewed-by: kimi-bot-andresmgsl <andres+4@heavyduty.builders> Reviewed-by: claude-bot-andresmgsl <andres+1@heavyduty.builders> Reviewed-by: glm-bot-andresmgsl <andres+5@heavyduty.builders>
This commit is contained in:
commit
fb5cb4746b
4 changed files with 269 additions and 19 deletions
52
README.md
52
README.md
|
|
@ -13,13 +13,38 @@ 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 Debian registry of the forge itself. One-time setup:
|
The package is published to the public Debian registry of the forge itself.
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
or manually. First add the forge's registry as an apt source:
|
If a private registry or a `FORGE_URL=` override requires authentication,
|
||||||
|
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
|
||||||
|
|
@ -46,7 +71,28 @@ 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) 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.
|
Note: apt releases that verify OpenPGP with `sqv` (Debian 13+, apt >= 2.9)
|
||||||
|
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`.
|
||||||
|
|
||||||
|
|
|
||||||
1
changelog.d/1.md
Normal file
1
changelog.d/1.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
- 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).
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
# 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.
|
||||||
|
|
||||||
|
|
@ -18,12 +19,27 @@ 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
|
||||||
|
|
@ -31,6 +47,18 @@ 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" \
|
||||||
|
|
@ -80,8 +108,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"
|
$SUDO install -d -m 0755 "$APT_ETC/keyrings" "$APT_ETC/sources.list.d"
|
||||||
curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null
|
$SUDO curl "${CURL_AUTH[@]}" -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
|
||||||
|
|
@ -94,7 +122,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 [ "$(curl -sSL -o /dev/null -w '%{http_code}' "$RELEASE_URL" || true)" = "404" ]; then
|
if [ "$($SUDO curl "${CURL_AUTH[@]}" -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
|
||||||
|
|
@ -102,16 +130,37 @@ if [ "$(curl -sSL -o /dev/null -w '%{http_code}' "$RELEASE_URL" || true)" = "404
|
||||||
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 (malformed Ed25519 MPI encoding
|
# currently produces for its Debian registry. The cause is recorded on #1; it
|
||||||
# in the upstream signing library). Try the properly signed source first so
|
# is not the key algorithm, because the registry serves an RSA-2048 signing
|
||||||
# this heals automatically once the forge is fixed; otherwise fall back to
|
# key (`gpg --list-packets` on repository.key reports `algo 1` with a
|
||||||
# [trusted=yes] — package integrity then relies on HTTPS to our own forge.
|
# 2048-bit pkey[0]). Try the properly signed source first so this heals
|
||||||
if ! update_only_source "$LIST"; then
|
# automatically once the forge is fixed. Only the exact live sqv failure, plus
|
||||||
echo
|
# the user's exact opt-in, permits an unverified source; auth, network, and
|
||||||
echo "WARNING: signature verification failed (known Forgejo registry issue" >&2
|
# other failures must leave verification enabled and retain apt's diagnostic.
|
||||||
echo "with sqv-based apt). Falling back to [trusted=yes]; transport" >&2
|
if update_output="$(update_only_source "$LIST" 2>&1)"; then
|
||||||
echo "security is provided by HTTPS to $FORGE_URL." >&2
|
printf '%s\n' "$update_output"
|
||||||
echo
|
else
|
||||||
|
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"
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,16 @@ 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 }) {
|
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError, forgeUser, forgeToken, allowUnverified, precreateSourcesDir = true }) {
|
||||||
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');
|
||||||
|
|
@ -26,7 +30,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 });
|
||||||
fs.mkdirSync(path.join(aptEtc, 'sources.list.d'), { recursive: true });
|
if (precreateSourcesDir) 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);
|
||||||
|
|
@ -39,10 +43,30 @@ 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', 'exec "$@"');
|
stub('sudo', 'SUDO_ACTIVE=1 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',
|
||||||
|
|
@ -58,8 +82,16 @@ 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',
|
||||||
|
|
@ -82,6 +114,10 @@ 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
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -96,6 +132,9 @@ 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.
|
||||||
|
|
@ -171,3 +210,118 @@ 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, '');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue