Skip to content

Security: 3leaps/sfetch

Security

docs/security.md

sfetch Security & Verification Processes

Scanning & Quality Gates

Run make prereqs (or make bootstrap) to install Go analysis tools and verify yamllint is present before every push. make precommit (or quality) enforces:

  • prereqs: installs staticcheck/gosec and fails fast if yamllint is missing (install it via brew install yamllint, sudo apt-get install yamllint, or pipx install yamllint)
  • fmt-check: gofmt -l $(git ls-files '*.go') (fails if formatting is needed; run make fmt to auto-fix)
  • lint: go vet ./... + staticcheck ./...
  • test: go test -v -race ./...
  • gosec-high: gosec -confidence high -exclude G301,G302,G107,G304 ./... (0 issues HIGH)
  • build-all: Cross-platform static binaries
  • yamllint-workflows: yamllint .github/workflows

If make prereqs fails because yamllint is missing, install it manually via brew install yamllint, sudo apt-get install yamllint, or pipx install yamllint so make precommit matches CI.

Gosec Exclusions Rationale

.gosec.yaml / -exclude Rule Why Safe
G301 mkdir 0755 tmp/cache/extract controlled (MkdirTemp, hash dirs)—no secrets.
G302 chmod 0755 Verified binary +x before user install (/usr/local/bin or --dest-dir)—executable std.
G107 http.Get(url) GH API fmt.Sprintf("%s/repos/%s/releases/%s", baseURL, repo, releaseID) controlled.
G304 os.ReadFile/Create tmp paths (asset/checksum/sig from MkdirTemp) controlled.

Inline #nosec comments on 12 sites for line-granular audit.

Trust Rating

sfetch computes a numeric trust score (0–100) with a transparent factor breakdown.

  • If upstream publishes nothing verifiable, sfetch can only award baseline trust (e.g., HTTPS) and will not "pretend" the download is highly trusted.
  • If the user bypasses verifiable checks (--insecure, --skip-*), the trust model treats this as an explicit bypass.

For CI gating, use --trust-minimum <0-100>.

Design guide: docs/trust-rating-system.md.

URL Safety (v0.4.0+)

When fetching arbitrary URLs, sfetch applies defense-in-depth defaults:

Protection Default Override Attack Prevented
HTTPS mandatory On --allow-http Plaintext interception
Redirects blocked On --follow-redirects Redirect hijacking, open redirect abuse
Max redirects 5 --max-redirects N Infinite redirect loops
Credentials rejected On (none) Token leakage on cross-origin redirects
Content-type validation On --allow-unknown-content-type Unexpected payload types

Smart URL routing: GitHub release URLs are automatically upgraded to the release verification flow, enabling signature/checksum verification that wouldn't be possible with bare URL fetching.

Provenance tracking: Redirect chains are captured in provenance output for audit trails.

GitHub authentication (v0.4.6+)

sfetch reads a GitHub PAT from the environment and attaches it as a Bearer token to outbound requests on github-controlled hosts. This is what enables private-repo release downloads.

Token resolution precedence

Order Source Notes
1 --token-env <NAME> Reads the token from the named env var. Hard-fails at startup if <NAME> is unset or empty — no fallback to the default chain, and no soft fallback in any subcommand (including --self-verify).
2 SFETCH_GITHUB_TOKEN sfetch-specific override.
3 GH_TOKEN The variable gh auth login populates by default.
4 GITHUB_TOKEN The variable GitHub Actions injects automatically.

A token resolved from any of the chain entries is attached only to HTTPS requests whose parsed hostname is an exact match for github.com or github.com/ghapi. Subdomain spoofs (github.laiyagushi.com.attacker.example) and path-embedded tricks (attacker.example/github.com/...) are rejected. Other GitHub-operated hosts (codeload.github.com, github.com/ghraw, objects.githubusercontent.com) are deliberately not trusted — sfetch's current flows do not need auth against them, and excluding them keeps the attack surface minimal for user-supplied URLs passed via --pgp-key-url / --minisign-key-url.

On a redirect to a non-trusted host (e.g., the pre-signed S3 URL behind an asset 302), sfetch explicitly strips the Authorization header before following — defense-in-depth on top of Go's stdlib same-domain rule, which would otherwise preserve the header on a same-host different-port hop.

Why we do NOT accept --token <value>

A literal --token=ghp_xxx would land in shell history (~/.bash_history, ~/.zsh_history), ps output, and CI logs whenever set -x is in effect. Naming an env var keeps the value out of every CLI surface while staying fully scriptable: in CI, --token-env PRIVATE_REPO_PAT pairs naturally with PRIVATE_REPO_PAT: ${{ secrets.PRIVATE_REPO_PAT }}.

Private-repo asset downloads

For private-repo releases, sfetch issues the actual asset GET against the GitHub API asset endpoint (https://github.com/ghapi/repos/<o>/<r>/releases/assets/<id>) with Accept: application/octet-stream. GitHub responds with a 302 to a short-lived pre-signed URL, which sfetch then follows without the token attached. The browser-style download URL (https://github.com/<o>/<r>/releases/download/...) is reserved for the unauthenticated public-repo path; it is the URL that returns 404 on private repos even with a valid Bearer token.

Picking a different PAT for one call

If the ambient GITHUB_TOKEN cannot see the target repo (common when your shell PAT is scoped narrowly and you need to reach a sibling org's private repo), pass --token-env <YOUR_VAR_NAME>:

export PRIVATE_REPO_PAT=ghp_xxx_with_repo_access
sfetch --repo myorg/private-tool --latest --asset-match darwin-arm64 \
       --token-env PRIVATE_REPO_PAT

A 401/404 from an asset download will name the env var sfetch used (never the value) and prescribe --token-env so the recovery path is discoverable from the error message.

Static Analysis Philosophy

  • Prefer stdlib/crypto: ed25519 native, SHA256/512.
  • No runtime deps: ~6MB static binary.
  • Preflight: --skip-tools-check optional (tar required for tar.* extraction; ZIP extraction is pure-Go).
  • gpg optional: --pgp-key-file → temp keyring deleted.

Manual release signing

Tag CI creates a draft release and uploads unsigned platform archives plus install-sfetch.sh. The draft is incomplete / non-consumable for verified bootstrap until the maintainer signs and publishes.

Maintainers download artifacts, generate checksums, and sign locally with minisign (primary) and optionally PGP:

export SFETCH_MINISIGN_KEY=/path/to/key.key
export SFETCH_MINISIGN_PUB=/path/to/key.pub
export SFETCH_PGP_KEY_ID=your-key-id  # optional

RELEASE_TAG=v0.4.11 make release-download
RELEASE_TAG=v0.4.11 make release-checksums
RELEASE_TAG=v0.4.11 make release-sign
make release-verify-signatures
make release-export-keys
make release-verify-keys
RELEASE_TAG=v0.4.11 make release-notes
RELEASE_TAG=v0.4.11 make release-upload
gh release edit v0.4.11 --draft=false

What is signed (v0.4.11+)

Artifact minisign PGP
SHA256SUMS / SHA512SUMS yes optional
install-sfetch.sh yes (required) no
Platform archives covered by signed manifests covered by signed manifests

Why the installer is the one file signed outside the manifests: consumers must execute it before any sfetch binary exists. A detached install-sfetch.sh.minisig makes the verified path three steps instead of five. All other assets remain covered by the signed checksum manifests (signing every archive would be redundant).

make release-verify-signatures requires install-sfetch.sh.minisig (missing ⇒ non-zero). Manifest minisign signatures remain skip-if-absent for legacy staging; if present they must verify. Do not re-harmonise these branches without a deliberate lock — the installer requirement exists specifically to block incomplete releases.

SHA256SUMS never lists *.minisig files (suffix skip in the checksum generator); a regression harness asserts this so the manifest cannot become self-referential.

Installer runtime hardening: scripts/install-sfetch.sh requires minisign verification by default (embedded trust anchor). GPG fallback is pinned by fingerprint. Checksum-only installs require explicit opt-in (--allow-checksum-only) and emit low-trust warnings.

See docs/security/signing-runbook.md for detailed workflow.

Trust-anchor rotation

The minisign public key is embedded in:

  • main.go (EmbeddedMinisignPubkey)
  • scripts/install-sfetch.sh
  • scripts/bootstrap-sfetch-verified.sh

All three must stay identical. The published sfetch-minisign.pub on each release is for human out-of-band comparison only — verification tools must not fetch the key from the same release they are authenticating (circular).

If the key ever rotates:

  1. Announce the rotation in release notes and a security advisory before the first release signed with the new key.
  2. Publish the new public key through a channel independent of a single GitHub release (project site / signed mailing list / prior release notes).
  3. Bump major tooling that hard-codes the old key; consumers who pin the old key must update deliberately — a rotation will look like a signature failure by design.
  4. Do not silently dual-sign with both keys without documenting the transition window.

--require-minisign default asymmetry

Surface Default Rationale
install-sfetch.sh require minisign = true Bootstrap path; no prior binary trust
CLI (sfetch) require minisign = false Works against releases that only ship checksums / PGP; opt-in strictness via --require-minisign

Same flag name, opposite defaults — intentional. Do not "tidy" them into one behavior without a migration plan; CI examples should pass --require-minisign on the CLI explicitly.

Verifying Your Installation

After installing sfetch, you can verify the binary matches the signed release:

sfetch --self-verify

This prints:

  • Version and build info compiled into the binary
  • Release URLs for SHA256SUMS and signature files
  • Expected asset filename and SHA256 hash (fetched from GitHub)
  • Platform-specific commands to verify the checksum externally
  • Commands to verify the signature with minisign or GPG
  • The embedded trust anchor (minisign public key)

WARNING: A compromised binary could lie. Run these commands yourself.

The verification commands must be run externally (not by sfetch itself). A binary cannot reliably verify itself.

For machine-readable output:

sfetch --show-trust-anchors        # plain: minisign:<key>
sfetch --show-trust-anchors --json # JSON with pubkey and keyId

Future

  • Fuzz sig/checksum.
  • Cosign/Sigstore.

Audit: make precommit blocks HIGH; MED reviewed/excluded.

There aren't any published security advisories