Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,26 @@ jobs:
- name: scripts/sync-schema.sh --check
run: ./scripts/sync-schema.sh --check

installer:
timeout-minutes: 10
name: Installer (shell)
# The curl|sh installer is the most privileged code we ship (it places the
# binary on PATH) and had NO automated test until R8. shellcheck it under
# the POSIX sh dialect it actually runs as, parse it with dash, and run the
# functional harness that asserts cosign verification is mandatory / fails
# closed when cosign is absent (RFC-0001 R8, backend#889).
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: shellcheck + dash parse
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck dash
shellcheck --shell=sh --severity=error scripts/install.sh
dash -n scripts/install.sh
bash -n scripts/tests/install-verify.sh
- name: Verification harness (mandatory cosign / fail-closed)
run: bash scripts/tests/install-verify.sh

test:
timeout-minutes: 15
name: Test
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,15 @@ tracebloc dataset push ./my-data \

> **`tracebloc: command not found` after installing?** The binary installs to `~/.local/bin` when `/usr/local/bin` isn't writable, and an already-running shell won't see the new PATH entry until you open a new terminal (or `. ~/.bashrc`). See **[Troubleshooting installation](docs/troubleshooting.md)**.

> **Signature verification is mandatory.** The installer verifies the binary's
> SHA256 **and** its cosign signature before installing. If `cosign` isn't on
> PATH it bootstraps a pinned, checksum-verified copy; if it can't, the install
> **fails closed** rather than trusting the same-channel checksum alone (it no
> longer silently skips the signature). The one escape, for a genuinely
> constrained environment, is to re-run with `TRACEBLOC_ALLOW_UNVERIFIED=1` —
> which prints a loud warning. For the highest trust, pre-install `cosign`
> (`brew install cosign`, your package manager, or the [released binary](https://github.com/sigstore/cosign/releases)) before running the installer. (RFC-0001 R8.)

What that runs under the curtain:

1. Reads kubeconfig, discovers the parent `tracebloc/client` release in the cluster
Expand Down
177 changes: 155 additions & 22 deletions scripts/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,10 @@
# 2. Resolves the latest release tag (or honors --version)
# 3. Downloads tracebloc-<tag>-<os>-<arch> from the GitHub Release
# 4. Verifies SHA256 against the release's SHA256SUMS file
# 5. (Optional) Verifies cosign signature if cosign is on PATH
# 5. Verifies the cosign signature — MANDATORY (RFC-0001 R8). If cosign isn't
# installed it bootstraps a pinned, checksum-verified one; if it can't, it
# FAILS CLOSED (never silently skips, never trusts the same-channel SHA256
# alone). Override only with TRACEBLOC_ALLOW_UNVERIFIED=1.
# 6. Installs to /usr/local/bin/tracebloc (falls back to $HOME/.local/bin
# with PATH advice if /usr/local/bin isn't writable)
#
Expand All@@ -29,6 +32,19 @@ RELEASE_VERSION="${RELEASE_VERSION:-latest}"
GITHUB_REPO="tracebloc/cli"
BINARY_NAME="tracebloc"

# Cosign signature verification is MANDATORY on the default path (RFC-0001 R8,
# backend#889). The previous build silently SKIPPED it when cosign was absent —
# the default on a fresh box — degrading to a SHA256 fetched over the same
# channel as the binary, which an on-path attacker also controls. We now require
# a signature: if cosign isn't present we bootstrap a pinned, checksum-verified
# one; if we can't, we FAIL CLOSED. This explicit opt-out is the only way past,
# for the genuinely-constrained operator, and it shouts.
ALLOW_UNVERIFIED="${TRACEBLOC_ALLOW_UNVERIFIED:-0}"
# Pin kept in lockstep with the release workflow's cosign-installer and the
# client installer's COSIGN_VERSION.
COSIGN_VERSION="${COSIGN_VERSION:-v2.4.1}"
COSIGN_BIN=""

usage() {
cat <<EOF
tracebloc CLI installer
Expand DownExpand Up@@ -109,6 +125,66 @@ detect_arch() {
OS="$(detect_os)"
ARCH="$(detect_arch)"

# --------------------------------------------------------------------
# sha256 helper (coreutils sha256sum on Linux, shasum -a 256 on macOS).
# Echoes the digest, or returns non-zero if neither tool is present.
# --------------------------------------------------------------------
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{print $1}'
else
return 1
fi
}

# --------------------------------------------------------------------
# Resolve a usable cosign into $COSIGN_BIN. Prefer one already on PATH;
# otherwise download the pinned release binary for this OS/arch and verify it
# against cosign's own published checksums before trusting it (a cosign we
# can't vouch for is no better than none). Returns non-zero if cosign can be
# neither found nor safely bootstrapped — the caller then fails closed.
# --------------------------------------------------------------------
ensure_cosign() {
if command -v cosign >/dev/null 2>&1; then
COSIGN_BIN="cosign"
return 0
fi

# cosign publishes assets named cosign-<os>-<arch> (arch in amd64/arm64);
# 386/arm have no official cosign build, so bootstrapping isn't possible there.
cosign_arch=""
case "$ARCH" in
amd64) cosign_arch="amd64" ;;
arm64) cosign_arch="arm64" ;;
*) return 1 ;;
esac

cbase="https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}"
casset="cosign-${OS}-${cosign_arch}"
cbin="$TMP/cosign"
csums="$TMP/cosign_checksums.txt"

echo " cosign not found — bootstrapping pinned ${COSIGN_VERSION} to verify the signature..."
# --tlsv1.2 floor for the cosign bootstrap fetch, matching the client
# installer's curls — never negotiate below TLS 1.2 to pull the verifier we
# then trust to authenticate the release.
if ! curl -fsSL --tlsv1.2 "$cbase/$casset" -o "$cbin" 2>/dev/null; then return 1; fi
if ! curl -fsSL --tlsv1.2 "$cbase/cosign_checksums.txt" -o "$csums" 2>/dev/null; then return 1; fi

cwant="$(grep " ${casset}\$" "$csums" | awk '{print $1}' | head -1)"
[ -n "$cwant" ] || return 1
cgot="$(sha256_of "$cbin")" || return 1
if [ "$cwant" != "$cgot" ]; then
echo "Error: bootstrapped cosign failed its own checksum — not using it." >&2
return 1
fi
chmod +x "$cbin"
COSIGN_BIN="$cbin"
return 0
}

# --------------------------------------------------------------------
# Resolve the release tag if "latest".
# --------------------------------------------------------------------
Expand All@@ -133,7 +209,36 @@ resolve_tag() {
basename "$redirect_url"
}

# --------------------------------------------------------------------
# Validate the resolved tag before it flows into a download URL.
#
# --version / RELEASE_VERSION is returned by resolve_tag verbatim and then
# interpolated into BASE_URL=.../releases/download/${TAG}. An unvalidated value
# such as 'v1.2.3-../../heads/main' would let curl collapse the '..' and fetch
# from a path other than the intended release — a path-traversal lever in the
# most security-sensitive download in the installer. Constrain it to a release
# tag shape and refuse any '/' or '..' (RFC-0001 R8, backend#889). Matches the
# client bootstrap's gate (^v[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.]+)?$).
validate_tag() {
# Path-traversal belt: no separators, no parent-dir tokens.
case "$1" in
*/*|*..*)
echo "Error: release tag '$1' contains a path separator or '..' —" >&2
echo " refusing to build a download URL from it (RFC-0001 R8)." >&2
exit 1
;;
esac
# Shape: vMAJOR.MINOR.PATCH with an optional [.-]alnum/dot suffix. grep -E is
# POSIX and already relied on elsewhere in this script; -q keeps it quiet.
if ! printf '%s\n' "$1" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.]+)?$'; then
echo "Error: '$1' is not a valid release tag (expected vX.Y.Z, e.g. v0.1.0)." >&2
echo " Pass --version with a published release tag." >&2
exit 1
fi
}

TAG="$(resolve_tag)"
validate_tag "$TAG"
echo "Installing tracebloc CLI $TAG ($OS/$ARCH)..."

# --------------------------------------------------------------------
Expand DownExpand Up@@ -167,7 +272,7 @@ if [ -z "$expected" ]; then
echo " — release artifacts may be incomplete." >&2
exit 1
fi
# sha256sum (GNU coreutils) vs shasum -a 256 (macOS): detect which is on PATH.
# sha256sum (GNU coreutils) vs shasum -a 256 (macOS): sha256_of picks one.
# If neither is available, refuse to install — running an unverified
# binary from the internet is exactly what this script exists to
# prevent. Bugbot PR #11 caught the previous "warn + continue + still
Expand All@@ -177,11 +282,7 @@ fi
# base Perl install. A host with neither is unusual enough that
# erroring out is the right call — the customer can install coreutils
# / xcode-select / similar and re-run.
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$TMP/$BINARY_FILE" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
actual="$(shasum -a 256 "$TMP/$BINARY_FILE" | awk '{print $1}')"
else
if ! actual="$(sha256_of "$TMP/$BINARY_FILE")"; then
echo "Error: neither sha256sum nor shasum is on PATH — can't verify the" >&2
echo " downloaded binary's integrity. Install one of:" >&2
echo " apt install coreutils # Debian/Ubuntu" >&2
Expand All@@ -201,31 +302,63 @@ fi
echo " ✓ checksum matches"

# --------------------------------------------------------------------
# Verify cosign signature if cosign is on PATH (optional).
# Verify the cosign signature — MANDATORY on the default path (RFC-0001 R8).
#
# The SHA256 check above proves the binary matches SHA256SUMS, but SHA256SUMS is
# fetched over the SAME channel as the binary — an on-path attacker who can swap
# the binary can swap the sums too. The cosign signature is the independent,
# Sigstore-rooted proof that tracebloc's release workflow produced these bytes.
# So we no longer "skip when cosign is absent": we require a verifier, bootstrap
# a pinned+checksummed cosign if one isn't installed, and FAIL CLOSED otherwise.
# The only escape is an explicit, loud TRACEBLOC_ALLOW_UNVERIFIED=1.
# --------------------------------------------------------------------
if command -v cosign >/dev/null 2>&1; then
verify_cosign_signature() {
if ! ensure_cosign; then
if [ "$ALLOW_UNVERIFIED" = "1" ]; then
echo " WARNING: cosign unavailable and couldn't be bootstrapped —" >&2
echo " signature NOT verified (TRACEBLOC_ALLOW_UNVERIFIED=1). The SHA256" >&2
echo " above is same-channel only; do not use this path in production." >&2
return 0
fi
echo "Error: cosign is required to verify the binary's signature and could" >&2
echo " not be found or bootstrapped — refusing to install on an" >&2
echo " unauthenticated, same-channel checksum alone (RFC-0001 R8)." >&2
echo " Fix: install cosign and re-run —" >&2
echo " https://docs.sigstore.dev/cosign/system_config/installation/" >&2
echo " (brew install cosign / apt / the released binary), or for a" >&2
echo " constrained environment re-run with TRACEBLOC_ALLOW_UNVERIFIED=1." >&2
exit 1
fi

echo "Verifying cosign signature..."
if ! curl -fsSL "$BASE_URL/$BINARY_FILE.sig" -o "$TMP/$BINARY_FILE.sig"; then
echo " ⚠ couldn't download .sig — release may pre-date signing." >&2
elif ! curl -fsSL "$BASE_URL/$BINARY_FILE.cert" -o "$TMP/$BINARY_FILE.cert"; then
echo " ⚠ couldn't download .cert — release may pre-date signing." >&2
elif ! cosign verify-blob \
if ! curl -fsSL "$BASE_URL/$BINARY_FILE.sig" -o "$TMP/$BINARY_FILE.sig" 2>/dev/null \
|| ! curl -fsSL "$BASE_URL/$BINARY_FILE.cert" -o "$TMP/$BINARY_FILE.cert" 2>/dev/null; then
if [ "$ALLOW_UNVERIFIED" = "1" ]; then
echo " WARNING: .sig/.cert not published for $TAG — signature NOT verified" >&2
echo " (TRACEBLOC_ALLOW_UNVERIFIED=1)." >&2
return 0
fi
echo "Error: couldn't download $BINARY_FILE.sig / .cert for $TAG — the" >&2
echo " release is unsigned or incomplete. Every supported release is" >&2
echo " cosign-signed; refusing to install unverified (RFC-0001 R8)." >&2
echo " Pin a signed --version, or re-run with TRACEBLOC_ALLOW_UNVERIFIED=1." >&2
exit 1
fi

if "$COSIGN_BIN" verify-blob \
--certificate-identity-regexp \
"https://github.com/${GITHUB_REPO}/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--certificate "$TMP/$BINARY_FILE.cert" \
--signature "$TMP/$BINARY_FILE.sig" \
"$TMP/$BINARY_FILE" 2>/dev/null; then
"$TMP/$BINARY_FILE" >/dev/null 2>&1; then
echo " ✓ cosign signature valid"
else
echo "Error: cosign signature verification FAILED — refusing to install." >&2
exit 1
else
echo " ✓ cosign signature valid"
fi
else
# Not having cosign isn't fatal — the SHA256 check above is the
# baseline. Recommend cosign for higher-trust installs.
echo " (cosign not installed; SHA256 verified, signature skipped)"
fi
}
verify_cosign_signature

# --------------------------------------------------------------------
# Install to a writable prefix.
Expand Down
Loading
Loading