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: installsstaticcheck/gosecand fails fast ifyamllintis missing (install it viabrew install yamllint,sudo apt-get install yamllint, orpipx install yamllint)fmt-check:gofmt -l $(git ls-files '*.go')(fails if formatting is needed; runmake fmtto 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 binariesyamllint-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.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.
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.
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.
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.
| 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.
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 }}.
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.
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_PATA 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.
- Prefer stdlib/crypto: ed25519 native, SHA256/512.
- No runtime deps: ~6MB static binary.
- Preflight:
--skip-tools-checkoptional (tar required for tar.* extraction; ZIP extraction is pure-Go). - gpg optional:
--pgp-key-file→ temp keyring deleted.
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| 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.
The minisign public key is embedded in:
main.go(EmbeddedMinisignPubkey)scripts/install-sfetch.shscripts/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:
- Announce the rotation in release notes and a security advisory before the first release signed with the new key.
- Publish the new public key through a channel independent of a single GitHub release (project site / signed mailing list / prior release notes).
- 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.
- Do not silently dual-sign with both keys without documenting the transition window.
| 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.
After installing sfetch, you can verify the binary matches the signed release:
sfetch --self-verifyThis 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- Fuzz sig/checksum.
- Cosign/Sigstore.
Audit: make precommit blocks HIGH; MED reviewed/excluded.