fix: close security audit findings
ober
8b0043021ded338595d08b9e0802a9c517dd6042
--- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,24 +13,29 @@ jobs: verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false - name: Install build tools run: sudo apt-get update && sudo apt-get install -y build-essential curl ca-certificates git ripgrep pkg-config - - name: Install jerbuild - run: | - set -eux - curl -fsSL "https://github.com/jerboa-lang/jerboa/releases/download/${JERBOA_VERSION}/jerbuild-linux-x86_64" -o /usr/local/bin/jerbuild - chmod +x /usr/local/bin/jerbuild - env: - JERBOA_VERSION: v0.2.3 + - name: Fetch locked source dependencies + run: sh support/fetch-locked-deps.sh - - name: Checkout jerboa-sinatra - run: git clone --depth 1 https://git.sr.ht/~lisp/jerboa-sinatra ../jerboa-sinatra + - name: Build locked Jerboa toolchain + run: make -C .deps/jerboa jerboa - name: Verify - run: JERBUILD=/usr/local/bin/jerbuild make verify + run: make verify + env: + JERBUILD: ${{ github.workspace }}/.deps/jerboa/dist/jerbuild + JERBOA_HOME: ${{ github.workspace }}/.deps/jerboa + JERBOA_SINATRA: ${{ github.workspace }}/.deps/jerboa-sinatra - name: Release evidence - run: JERBUILD=/usr/local/bin/jerbuild make release-evidence + run: make release-evidence + env: + JERBUILD: ${{ github.workspace }}/.deps/jerboa/dist/jerbuild + JERBOA_HOME: ${{ github.workspace }}/.deps/jerboa + JERBOA_SINATRA: ${{ github.workspace }}/.deps/jerboa-sinatra --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist/ *.o *.a *.boot +.deps/ --- a/.jerboa/security.json +++ b/.jerboa/security.json @@ -21,7 +21,13 @@ "reviewProof": "required-for-promotion" }, "secrets": { "tlsMaterialRuntimeOnly": true }, - "supplyChain": { "sbom": "required", "reproducibilityReport": "required", "reproducibleBinary": "release-required" }, + "supplyChain": { + "sbom": "required", + "reproducibilityReport": "required", + "reproducibleBinary": "release-required", + "dependencyLock": "required", + "installer": "blocked-until-pinned-signed-manifest-and-release-signer" + }, "eval": { "stringEval": "deny", "bareRead": "deny" } }, "runtimeTargets": ["linux"], --- a/Makefile +++ b/Makefile @@ -1,11 +1,13 @@ JERBOA ?= jerboa -JERBUILD ?= $(JERBOA) jerbuild +LOCAL_JERBUILD := $(abspath ../jerboa/dist/jerbuild) +JERBUILD ?= $(if $(wildcard $(LOCAL_JERBUILD)),$(LOCAL_JERBUILD),$(JERBOA) jerbuild) JERBOA_HOME ?= $(shell $(JERBUILD) --jerboa-home 2>/dev/null) ifeq ($(JERBOA_HOME),) JERBOA_HOME := $(abspath ../jerboa) endif JERBOA_SINATRA ?= $(abspath ../jerboa-sinatra) UNAME_S := $(shell uname -s) +export PATH := $(abspath support):$(PATH) ENTRY ?= secure-site.ss BINARY_OUTPUT ?= dist/jerboa-site @@ -25,7 +27,7 @@ REPRODUCIBILITY_DIR ?= dist/reproducibility DEPLOYMENT_EVIDENCE_DIR ?= dist/deployment-evidence JERBOA_SITE_DEPLOYMENT_URL ?= https://jerboa.sh/ -.PHONY: deps test binary static-binary sync-jerboa security audit sbom reproducibility-report deployment-evidence verify release-evidence clean distclean +.PHONY: deps test binary binary-smoke static-binary sync-jerboa security audit sbom reproducibility-report deployment-evidence verify release-evidence clean distclean deps: @test -d "$(JERBOA_SINATRA)" || { echo "missing jerboa-sinatra checkout at $(JERBOA_SINATRA); set JERBOA_SINATRA=/path/to/checkout" >&2; exit 1; } @@ -59,7 +61,10 @@ reproducibility-report: deployment-evidence: JERBOA_SITE_DEPLOYMENT_URL="$(JERBOA_SITE_DEPLOYMENT_URL)" DEPLOYMENT_EVIDENCE_DIR="$(DEPLOYMENT_EVIDENCE_DIR)" bash scripts/deployment-evidence.sh -verify: security test binary audit sbom reproducibility-report +binary-smoke: binary + BINARY_OUTPUT="$(abspath $(BINARY_OUTPUT))" bash scripts/binary-smoke.sh + +verify: security test binary-smoke audit sbom reproducibility-report release-evidence: rm -rf $(EVIDENCE_DIR) @@ -78,6 +83,9 @@ release-evidence: @if command -v otool >/dev/null 2>&1 && [ -f "$(BINARY_OUTPUT)" ]; then otool -L "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-linkage.txt; elif command -v ldd >/dev/null 2>&1 && [ -f "$(BINARY_OUTPUT)" ]; then ldd "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-linkage.txt; fi @if [ -f "$(BINARY_OUTPUT)" ]; then shasum -a 256 "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-sha256.txt; fi { find . \( -path './.git' -o -path './build' -o -path './dist' \) -prune -o -type f -print | LC_ALL=C sort | while IFS= read -r f; do shasum -a 256 "$$f"; done; } > $(EVIDENCE_DIR)/source-sha256.txt + cp support/installer-release.lock $(EVIDENCE_DIR)/installer-release.lock + shasum -a 256 support/verified-install.sh dependency-lock.tsv > $(EVIDENCE_DIR)/installer-and-dependency-sha256.txt + { echo 'installer_endpoint_status=blocked-http-503'; echo 'installer_signature_key_status=unprovisioned'; echo 'installer_mutable_download_status=absent'; } > $(EVIDENCE_DIR)/installer-status.txt rm -rf $(EVIDENCE_DIR)/sbom $(EVIDENCE_DIR)/reproducibility $(EVIDENCE_DIR)/deployment-evidence cp -R $(SBOM_DIR) $(EVIDENCE_DIR)/sbom cp -R $(REPRODUCIBILITY_DIR) $(EVIDENCE_DIR)/reproducibility --- a/README.md +++ b/README.md @@ -17,16 +17,18 @@ make sync-jerboa See [`docs/site-sync.md`](docs/site-sync.md) for the update workflow and LLM-assisted documentation guardrails. -## Public Install Endpoint - -The deployed site serves a curlable POSIX shell bootstrap: - -```sh -curl -fsSL https://jerboa.sh/install.sh | sh -``` - -That endpoint fetches the canonical installer from the public SourceHut -`jerboa` repository and forwards any arguments to it. +## Install Endpoint + +Network installation is fail-closed. `/install.sh` returns HTTP 503 and a +nonzero shell program until the project publishes a version-pinned archive, +signed SHA-256 manifest, detached signature, and independently protected +signer identity. It never downloads mutable branch content. + +[`support/verified-install.sh`](support/verified-install.sh) is the reviewed +reference flow. It pins the origin and release, refuses redirects, verifies an +SSH-signed manifest before trusting its archive digest, and validates the +archive layout. Its signer key is deliberately `UNPROVISIONED`, so it cannot be +enabled accidentally. See [`docs/installer-trust.md`](docs/installer-trust.md). ## Build --- a/SECURITY.md +++ b/SECURITY.md @@ -31,9 +31,15 @@ No public production-support commitment exists yet. Security-sensitive releases - Keep generated metadata sanitized by `tools/sync-jerboa`; do not publish local absolute paths or dirty-tree indicators. - Keep the site script static-content only unless new dynamic routes get a threat model and tests. - Use TLS key material only through runtime environment variables; never commit certificate private keys. +- Keep `/install.sh` fail-closed until the signed-release requirements in + `docs/installer-trust.md` are complete. Never restore branch downloads, + same-origin-only checksum trust, redirects, or `curl | sh` advertising. ## Remaining Blockers +- The release signer key and signed immutable release set are not provisioned; + the network installer therefore remains HTTP 503 by policy. + - Repeated clean binary builds must keep reporting `status=match` in `dist/release-evidence/reproducibility/result.txt`; any regression requires a documented release exception before promotion. new file mode 100644 --- /dev/null +++ b/dependency-lock.tsv @@ -0,0 +1,2 @@ +jerboa https://git.sr.ht/~lisp/jerboa 23ad83d28ac65cf6f0d2d702fd32934f207c26bf 6cbc7dd433a88801de82540e74433fc04d2474cd +jerboa-sinatra https://git.sr.ht/~lisp/jerboa-sinatra ed479a07007b3c840861c6316a6bfe35e1209fd4 7ae5f99a41cb41b9f726caa082454969f244dd63 new file mode 100644 --- /dev/null +++ b/docs/installer-trust.md @@ -0,0 +1,24 @@ +# Installer trust policy + +The public installer is disabled. This is an intentional availability loss to +remove the former mutable-code execution path. + +Enabling it requires one reviewed change that: + +1. publishes an immutable `vX.Y.Z` archive plus `release-manifest.sha256` and + its detached SSH signature; +2. provisions the release-only public key in `support/verified-install.sh` and + records its SHA-256 fingerprint through an independent project channel; +3. changes `support/installer-release.lock` from + `blocked-awaiting-signed-release` only after clean reproducibility, SBOM, + signing, and target-install evidence exists; +4. embeds the reviewed verifier in `/install.sh`, preserving its pinned HTTPS + origin, zero-redirect policy, unique manifest entry check, signature-before- + checksum order, private temporary directory, and archive path validation; +5. adds deployment evidence proving the endpoint body matches the reviewed + source and that unsigned, wrong-key, redirected, malformed-manifest, and + digest-mismatch inputs all fail before extraction or execution. + +The checksum and signature must not be fetched from a mutable branch. A digest +served beside an archive is useful only after the manifest containing that +digest is authenticated by the pinned signer. --- a/docs/release-evidence.md +++ b/docs/release-evidence.md @@ -30,6 +30,9 @@ The target writes `dist/release-evidence/` with: `JERBOA_SITE_DEPLOYMENT_URL` (default `https://jerboa.sh/`). - `git-commit.txt`, `git-status.txt`, `diff-stat.txt`, `build-env.txt`, and `source-sha256.txt`. +- installer-policy evidence proving the endpoint is HTTP 503/fail-closed while + `support/installer-release.lock` remains blocked, plus shell syntax and + unprovisioned-signer negative tests for the future verifier. `make deployment-evidence` is networked evidence. It records explicit `blocked-*` status fields when the target cannot be checked, while default local new file mode 100755 --- /dev/null +++ b/scripts/binary-smoke.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +binary=${BINARY_OUTPUT:-$repo_root/dist/jerboa-site} +port=${JERBOA_SITE_SMOKE_PORT:-$((40000 + $$ % 20000))} +work=$(mktemp -d "${TMPDIR:-/tmp}/jerboa-site-smoke.XXXXXX") +pid= + +cleanup() { + if [[ -n "$pid" ]]; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi + rm -rf -- "$work" +} +trap cleanup EXIT HUP INT TERM + +[[ -x "$binary" ]] || { echo "site binary is missing: $binary" >&2; exit 1; } +command -v openssl >/dev/null 2>&1 || { echo "openssl is required" >&2; exit 1; } +command -v curl >/dev/null 2>&1 || { echo "curl is required" >&2; exit 1; } + +openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ + -subj /CN=localhost \ + -addext subjectAltName=DNS:localhost,IP:127.0.0.1 \ + -addext basicConstraints=critical,CA:FALSE \ + -keyout "$work/key.pem" -out "$work/cert.pem" >/dev/null 2>&1 + +TLS_CERT="$work/cert.pem" TLS_KEY="$work/key.pem" PORT="$port" \ + "$binary" >"$work/server.log" 2>&1 & +pid=$! + +ready=0 +for _ in {1..50}; do + if curl -ksS --connect-timeout 1 "https://127.0.0.1:$port/healthz" \ + >"$work/health" 2>/dev/null; then + ready=1 + break + fi + if ! kill -0 "$pid" 2>/dev/null; then + break + fi + sleep 0.1 +done +if [[ "$ready" -ne 1 ]]; then + sed -n '1,120p' "$work/server.log" >&2 + echo "site binary did not become ready" >&2 + exit 1 +fi +[[ "$(<"$work/health")" == ok ]] || { echo "health response mismatch" >&2; exit 1; } + +status=$(curl -ksS -D "$work/headers" -o "$work/installer" \ + -w '%{http_code}' "https://127.0.0.1:$port/install.sh") +[[ "$status" == 503 ]] || { echo "installer endpoint returned $status" >&2; exit 1; } +grep -Eiq '^Cache-Control:[[:space:]]*no-store' "$work/headers" || { + echo "installer response lacks Cache-Control: no-store" >&2 + exit 1 +} +grep -Fq 'exit 1' "$work/installer" || { echo "installer body does not fail closed" >&2; exit 1; } +if grep -Eiq 'https?://|curl|wget|fetch' "$work/installer"; then + echo "installer body contains a network download path" >&2 + exit 1 +fi + +echo "binary-smoke: health ok; installer blocked with HTTP 503" --- a/scripts/security-check.sh +++ b/scripts/security-check.sh @@ -19,6 +19,13 @@ required=( docs/threat-model.md docs/content-security.md docs/release-evidence.md + docs/installer-trust.md + dependency-lock.tsv + support/fetch-locked-deps.sh + support/installer-release.lock + support/nm + support/verified-install.sh + scripts/binary-smoke.sh scripts/security-check.sh scripts/sbom.sh scripts/reproducibility-report.sh @@ -34,7 +41,7 @@ if [[ "$content_only" -eq 0 ]]; then done fi -content_files=(README.md SECURITY.md docs/site-sync.md docs/threat-model.md docs/content-security.md docs/release-evidence.md secure-site.ss data/jerboa-source.json Makefile tools/sync-jerboa) +content_files=(README.md SECURITY.md docs/site-sync.md docs/threat-model.md docs/content-security.md docs/release-evidence.md docs/installer-trust.md secure-site.ss data/jerboa-source.json dependency-lock.tsv support/installer-release.lock Makefile tools/sync-jerboa) present_files=() for path in "${content_files[@]}"; do [[ -f "$path" ]] && present_files+=("$path") @@ -68,6 +75,44 @@ if grep -n -E '<script|[[:space:]<]on[a-z]+=' secure-site.ss; then exit 1 fi +grep -q '^status=blocked-awaiting-signed-release$' support/installer-release.lock +grep -q '^signer_public_key=UNPROVISIONED$' support/installer-release.lock +grep -q 'SIGNER_PUBLIC_KEY=UNPROVISIONED' support/verified-install.sh +grep -q -- '--max-redirs 0' support/verified-install.sh +grep -q 'ssh-keygen -Y verify' support/verified-install.sh +grep -q 'release-manifest.sha256' support/verified-install.sh +grep -q '(status! 503)' secure-site.ss +grep -q '(cache-control! "no-store")' secure-site.ss +if grep -n -E 'blob/(master|main)/support/install\.sh|installer_url=|curl -fsSL https://jerboa\.sh/install\.sh[[:space:]]*\|[[:space:]]*sh' \ + secure-site.ss README.md docs/*.md; then + echo "mutable or unauthenticated installer path is forbidden" >&2 + exit 1 +fi +sh -n support/verified-install.sh +sh -n support/fetch-locked-deps.sh +sh -n support/nm +bash -n scripts/binary-smoke.sh +grep -q 'export PATH :=.*support' Makefile +grep -q 'llvm-nm' support/nm +if support/verified-install.sh >/dev/null 2>&1; then + echo "unprovisioned installer must fail closed" >&2 + exit 1 +fi +awk -F '\t' ' + NF != 4 { exit 1 } + $1 !~ /^jerboa(-sinatra)?$/ { exit 1 } + $2 !~ /^https:\/\/git\.sr\.ht\/~lisp\/jerboa(-sinatra)?$/ { exit 1 } + $3 !~ /^[0-9a-f]{40}$/ || $4 !~ /^[0-9a-f]{40}$/ { exit 1 } +' dependency-lock.tsv || { + echo "dependency lock is malformed" >&2 + exit 1 +} +grep -Eq 'actions/checkout@[0-9a-f]{40}' .github/workflows/ci.yml +if grep -n -E 'uses:.*@(v[0-9]+|main|master)|git clone|curl .*jerbuild' .github/workflows/ci.yml; then + echo "CI contains a mutable action, clone, or unverified toolchain download" >&2 + exit 1 +fi + if [[ "$content_only" -eq 0 ]]; then unignored_generated="$( git ls-files --others --exclude-standard -- \ --- a/secure-site.ss +++ b/secure-site.ss @@ -27,28 +27,11 @@ (define install-script (string-append "#!/bin/sh\n" - "# Install the base Jerboa toolchain from SourceHut release artifacts.\n" - "# Public entry point: https://jerboa.sh/install.sh\n" - "# Canonical installer source: https://git.sr.ht/~lisp/jerboa/blob/master/support/install.sh\n" - "\n" "set -eu\n" - "\n" - "installer_url=${JERBOA_INSTALLER_URL:-https://git.sr.ht/~lisp/jerboa/blob/master/support/install.sh}\n" - "tmp=$(mktemp \"${TMPDIR:-/tmp}/jerboa-install.XXXXXX\")\n" - "trap 'rm -f \"$tmp\"' EXIT HUP INT TERM\n" - "\n" - "if command -v curl >/dev/null 2>&1; then\n" - " curl -fsSL \"$installer_url\" -o \"$tmp\"\n" - "elif command -v fetch >/dev/null 2>&1; then\n" - " fetch -q -o \"$tmp\" \"$installer_url\"\n" - "elif command -v wget >/dev/null 2>&1; then\n" - " wget -q -O \"$tmp\" \"$installer_url\"\n" - "else\n" - " echo \"ERROR: need curl, fetch, or wget to download Jerboa\" >&2\n" - " exit 1\n" - "fi\n" - "\n" - "sh \"$tmp\" \"$@\"\n")) + "echo 'ERROR: Jerboa network installation is temporarily disabled.' >&2\n" + "echo 'No release signer identity has been provisioned, so the site refuses to download or execute code.' >&2\n" + "echo 'Use a reviewed local source build until a signed immutable release is published.' >&2\n" + "exit 1\n")) (define (nav-link key href label active) (string-append "<a class=\"nav-link" @@ -102,7 +85,8 @@ (string-append "<section class=\"content\"><h1>Get Started</h1>" "<p class=\"lead\">Jerboa uses one command for the language runtime, builder, packages, LSP, and service tooling.</p>" - "<h2>Install</h2><div class=\"code\"><pre>curl -fsSL https://jerboa.sh/install.sh | sh</pre></div>" + "<h2>Install</h2><div class=\"code\"><pre>Network installer disabled pending a signed immutable release.</pre></div>" + "<p>Until the project provisions an independently protected release signer, build from a reviewed local checkout. The install endpoint fails closed and never fetches mutable source.</p>" "<h2>Run a file</h2><div class=\"code\"><pre>jerboa app.ss</pre></div>" "<h2>Build a project</h2><div class=\"code\"><pre>jerboa jerbuild transpile src lib --force\njerboa jerbuild exec --libdirs lib app.ss\njerboa jerbuild binary --libdirs lib app.ss dist/app</pre></div>" "<h2>Serve HTTPS</h2><div class=\"code\"><pre>(import (std net httpsd))\n\n(httpsd-start 8443 handler \"fullchain.pem\" \"privkey.pem\")</pre></div>" @@ -176,8 +160,9 @@ site-css) (GET "/install.sh" + (status! 503) (content-type! "text/plain; charset=utf-8") - (cache-control! "no-cache") + (cache-control! "no-store") install-script) (GET "/" new file mode 100755 --- /dev/null +++ b/support/fetch-locked-deps.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -eu + +lock_file=${1:-dependency-lock.tsv} +destination_root=${2:-.deps} + +[ "$destination_root" = .deps ] || { + echo "ERROR: dependency destination must be .deps" >&2 + exit 1 +} +[ -f "$lock_file" ] || { + echo "ERROR: missing dependency lock: $lock_file" >&2 + exit 1 +} + +mkdir -p "$destination_root" +tab=$(printf '\t') +while IFS="$tab" read -r name origin commit tree extra; do + [ -n "$name" ] || continue + [ -z "${extra:-}" ] || { + echo "ERROR: malformed lock row for $name" >&2 + exit 1 + } + case "$name:$origin" in + jerboa:https://git.sr.ht/~lisp/jerboa|\ + jerboa-sinatra:https://git.sr.ht/~lisp/jerboa-sinatra) ;; + *) echo "ERROR: unapproved locked dependency: $name $origin" >&2; exit 1 ;; + esac + printf '%s\n' "$commit" | grep -Eq '^[0-9a-f]{40}$' || { + echo "ERROR: invalid commit for $name" >&2 + exit 1 + } + printf '%s\n' "$tree" | grep -Eq '^[0-9a-f]{40}$' || { + echo "ERROR: invalid tree for $name" >&2 + exit 1 + } + + destination=$destination_root/$name + rm -rf "$destination" + mkdir -p "$destination" + git -C "$destination" init -q + git -C "$destination" remote add origin "$origin" + git -C "$destination" fetch -q --no-tags --depth 1 origin "$commit" + [ "$(git -C "$destination" rev-parse FETCH_HEAD)" = "$commit" ] || { + echo "ERROR: fetched commit mismatch for $name" >&2 + exit 1 + } + git -C "$destination" checkout -q --detach "$commit" + [ "$(git -C "$destination" rev-parse 'HEAD^{tree}')" = "$tree" ] || { + echo "ERROR: fetched tree mismatch for $name" >&2 + exit 1 + } + test -z "$(git -C "$destination" status --short)" || { + echo "ERROR: locked dependency checkout is dirty: $name" >&2 + exit 1 + } + printf 'locked_dependency=%s commit=%s tree=%s\n' "$name" "$commit" "$tree" +done < "$lock_file" new file mode 100644 --- /dev/null +++ b/support/installer-release.lock @@ -0,0 +1,9 @@ +# Installer publication remains fail-closed until every blocked field is +# replaced in one reviewed release change. +status=blocked-awaiting-signed-release +version=v0.2.4 +origin=https://git.sr.ht +repository=~lisp/jerboa +signer_identity=jerboa-release +signing_namespace=file +signer_public_key=UNPROVISIONED new file mode 100755 --- /dev/null +++ b/support/nm @@ -0,0 +1,48 @@ +#!/bin/sh +# Jerbuild discovers static native symbols with a tool named `nm`. Apple's +# system nm cannot read object code produced by newer Rust LLVM releases, so a +# partial symbol list can otherwise yield a binary that links but has no TLS at +# runtime. Prefer an LLVM/GNU reader and fail closed when macOS has neither. +set -eu + +if [ -n "${JERBOA_LLVM_NM:-}" ]; then + [ -x "$JERBOA_LLVM_NM" ] || { + echo "configured JERBOA_LLVM_NM is not executable" >&2 + exit 1 + } + exec "$JERBOA_LLVM_NM" "$@" +fi + +for candidate in \ + /opt/homebrew/opt/llvm/bin/llvm-nm \ + /usr/local/opt/llvm/bin/llvm-nm \ + /opt/homebrew/opt/binutils/bin/nm \ + /usr/local/opt/binutils/bin/nm +do + if [ -x "$candidate" ]; then + exec "$candidate" "$@" + fi +done + +if command -v llvm-nm >/dev/null 2>&1; then + exec "$(command -v llvm-nm)" "$@" +fi +if command -v gnm >/dev/null 2>&1; then + exec "$(command -v gnm)" "$@" +fi + +case "$(uname -s)" in + Darwin) + echo "a modern llvm-nm or GNU nm is required for Rust static archives" >&2 + exit 1 + ;; +esac + +for candidate in /usr/bin/nm /bin/nm; do + if [ -x "$candidate" ]; then + exec "$candidate" "$@" + fi +done + +echo "no supported nm implementation was found" >&2 +exit 1 new file mode 100755 --- /dev/null +++ b/support/verified-install.sh @@ -0,0 +1,128 @@ +#!/bin/sh +# Reference installer for the future signed release endpoint. The public site +# must continue returning 503 until SIGNER_PUBLIC_KEY is provisioned here and +# matching signed release artifacts have passed independent review. +set -eu +umask 077 + +VERSION=v0.2.4 +ORIGIN=https://git.sr.ht +REPOSITORY=~lisp/jerboa +SIGNER_IDENTITY=jerboa-release +SIGNING_NAMESPACE=file +SIGNER_PUBLIC_KEY=UNPROVISIONED + +error() { + echo "ERROR: $*" >&2 + exit 1 +} + +[ "$SIGNER_PUBLIC_KEY" != UNPROVISIONED ] || \ + error "release signer is not provisioned; network installation is disabled" + +command -v curl >/dev/null 2>&1 || error "curl is required" +command -v ssh-keygen >/dev/null 2>&1 || error "ssh-keygen is required" +command -v tar >/dev/null 2>&1 || error "tar is required" + +case "${JERBOA_RELEASE_TARGET:-}" in + "") + case "$(uname -s)-$(uname -m)" in + Darwin-arm64) target=macos-arm64 ;; + Linux-x86_64|Linux-amd64) target=linux-amd64 ;; + Linux-aarch64|Linux-arm64) target=linux-arm64 ;; + FreeBSD-amd64|FreeBSD-x86_64) target=freebsd-amd64 ;; + *) error "unsupported platform" ;; + esac + ;; + macos-arm64|linux-amd64|linux-arm64|freebsd-amd64) + target=$JERBOA_RELEASE_TARGET + ;; + *) error "unsupported release target" ;; +esac + +if [ -n "${JERBOA_INSTALL_DIR:-}" ]; then + bindir=$JERBOA_INSTALL_DIR +elif [ -n "${HOME:-}" ]; then + bindir=$HOME/.local/bin +else + error "HOME is unset; set JERBOA_INSTALL_DIR" +fi + +base=$ORIGIN/$REPOSITORY/refs/download/$VERSION +archive_name=jerboa-$VERSION-$target.tar.gz +manifest_name=release-manifest.sha256 +signature_name=$manifest_name.sig + +tmp_parent=${TMPDIR:-/tmp} +[ -d "$tmp_parent" ] && [ -w "$tmp_parent" ] || error "temporary directory is unavailable" +tmp=$(mktemp -d "$tmp_parent/jerboa-install.XXXXXX") +trap 'rm -rf "$tmp"' EXIT HUP INT TERM + +download() { + # Redirects are deliberately disabled: every URL stays on the pinned + # HTTPS origin and immutable version path. + curl -fSs --proto '=https' --tlsv1.2 --max-redirs 0 "$base/$1" -o "$tmp/$1" +} + +download "$manifest_name" +download "$signature_name" +download "$archive_name" + +printf '%s %s\n' "$SIGNER_IDENTITY" "$SIGNER_PUBLIC_KEY" > "$tmp/allowed_signers" +chmod 0600 "$tmp/allowed_signers" +ssh-keygen -Y verify \ + -f "$tmp/allowed_signers" \ + -I "$SIGNER_IDENTITY" \ + -n "$SIGNING_NAMESPACE" \ + -s "$tmp/$signature_name" < "$tmp/$manifest_name" >/dev/null 2>&1 || \ + error "release manifest signature verification failed" + +expected=$(awk -v file="$archive_name" ' + $2 == file { + if (found) exit 2 + value = $1 + found = 1 + } + END { + if (!found) exit 1 + print value + } +' "$tmp/$manifest_name") || error "manifest has no unique archive entry" + +printf '%s\n' "$expected" | grep -Eq '^[0-9a-f]{64}$' || \ + error "manifest archive digest is not lowercase SHA-256" + +if command -v sha256sum >/dev/null 2>&1; then + actual=$(sha256sum "$tmp/$archive_name" | awk '{print $1}') +elif command -v shasum >/dev/null 2>&1; then + actual=$(shasum -a 256 "$tmp/$archive_name" | awk '{print $1}') +elif command -v sha256 >/dev/null 2>&1; then + actual=$(sha256 -q "$tmp/$archive_name") +else + error "a SHA-256 utility is required" +fi +[ "$actual" = "$expected" ] || error "signed-manifest archive digest mismatch" + +root=jerboa-$VERSION-$target +tar -tzf "$tmp/$archive_name" | awk -v root="$root/" ' + index($0, "/") == 1 || $0 ~ /(^|\/)\.\.($|\/)/ || index($0, root) != 1 { exit 1 } +' || error "archive contains an unsafe path" + +# Only ordinary files and directories are allowed. Rejecting links and special +# nodes prevents a later member from escaping through a path created by an +# earlier member during extraction. +tar -tvzf "$tmp/$archive_name" | awk ' + substr($0, 1, 1) != "-" && substr($0, 1, 1) != "d" { exit 1 } +' || error "archive contains links or special files" + +mkdir "$tmp/extract" +tar -xzf "$tmp/$archive_name" -C "$tmp/extract" +[ -x "$tmp/extract/$root/bin/jerboa" ] || error "archive lacks bin/jerboa" + +mkdir -p "$bindir" +install -m 0755 "$tmp/extract/$root/bin/jerboa" "$bindir/jerboa" +for command in jmcp jlsp jerbuild jpkg; do + ln -sf jerboa "$bindir/$command" +done + +echo "Installed signed Jerboa $VERSION for $target into $bindir"