Documentation
¶
Overview ¶
Package update implements EzyShield's self-update logic: fetching releases from GitHub, verifying SHA256 checksums against checksums.txt, and atomically replacing the on-disk binaries.
The package is split so the high-risk pieces (HTTP fetch, semver compare, checksum parsing, atomic replace) can be unit-tested without exercising the real filesystem or network.
Index ¶
- Constants
- Variables
- func AtomicReplace(tmpPath, finalPath string, mode os.FileMode) error
- func CompareSemver(a, b string) (int, error)
- func DownloadVerified(ctx context.Context, client Downloader, spec AssetSpec) (string, error)
- func ParseChecksums(r io.Reader) (map[string]string, error)
- func RealCosignExec(ctx context.Context, args ...string) ([]byte, error)
- func VerifyChecksumsSignature(ctx context.Context, run CosignExecFunc, checksums, sig, cert []byte) error
- type Asset
- type AssetSpec
- type Client
- func (c *Client) Download(ctx context.Context, u string, dst io.Writer) (int64, error)
- func (c *Client) DownloadChecksums(ctx context.Context, u string) (map[string]string, error)
- func (c *Client) DownloadSmall(ctx context.Context, u string) ([]byte, error)
- func (c *Client) LatestRelease(ctx context.Context) (*Release, error)
- func (c *Client) NewestRelease(ctx context.Context) (*Release, error)
- func (c *Client) ReleaseByTag(ctx context.Context, tag string) (*Release, error)
- type CosignExecFunc
- type Downloader
- type Release
Constants ¶
const ( // DefaultRepo is the public release source. The private repo will be // deprecated; update must never reference it. DefaultRepo = "evertramos/ezy-shield" // DefaultAPIBaseURL is the base for GitHub Releases JSON. DefaultAPIBaseURL = "https://api.github.com" // DefaultDLBaseURL is the base for browser-style asset downloads, used as a // fallback URL when a release JSON omits asset URLs. DefaultDLBaseURL = "https://github.com" // MaxBinarySize caps how many bytes Download will accept. Plenty of headroom // for a stripped Go binary; rejects a runaway response from a hostile mirror. MaxBinarySize = 100 << 20 // 100 MiB )
const ( // CosignOIDCIssuer is GitHub Actions' OIDC token issuer. CosignOIDCIssuer = "https://token.actions.githubusercontent.com" // CosignIdentityRegexp pins repository and workflow file. The ref portion // accepts both release trigger paths: a pushed tag (refs/tags/vX.Y.Z) and // a workflow_dispatch from main (stable) or dev (release candidates). CosignIdentityRegexp = `^https://github\.com/evertramos/ezy-shield/\.github/workflows/release\.yaml@refs/(tags/v[0-9][^ ]*|heads/(main|dev))$` )
Cosign keyless-verification pins for release signatures. These MUST match docs/content/en/security/verifying-releases.md — the docs command and this code verify the same trust chain: checksums.txt was produced by THIS repository's release workflow on GitHub's OIDC issuer, not by a compromised token, a hijacked release, or a mirror.
Variables ¶
var ErrCosignNotFound = errors.New("cosign not found in PATH")
ErrCosignNotFound reports that the cosign binary is not installed. Callers decide the policy (the updater fails closed unless --allow-unsigned is given — a distinct sentinel so that opt-out can never swallow a real verification failure).
var ErrNoStableRelease = errors.New("no stable (non-prerelease) release has been published yet")
ErrNoStableRelease is returned by LatestRelease when GitHub's releases/latest endpoint has nothing to return. That endpoint only ever considers non-prerelease releases — during the release-candidate phase before the first stable tag ships, every published release is a prerelease, so it 404s. This is an expected, named condition (issue #235), distinct from a genuine "release not found" on ReleaseByTag.
Functions ¶
func AtomicReplace ¶
AtomicReplace chmods tmpPath to mode then renames it onto finalPath. The two paths must be on the same filesystem (DownloadVerified ensures this by placing the temp file in filepath.Dir(spec.InstallPath)). "Atomic" here is the POSIX rename(2) guarantee: finalPath always points at the old binary or the new one — never a half-written file.
func CompareSemver ¶
CompareSemver returns -1, 0, or 1 if a<b, a==b, or a>b respectively, using semver precedence (prerelease ranks below the same base version).
Returns an error if either side is not parseable as semver (e.g. "dev", "unknown"). Callers should treat that as "cannot determine — proceed".
func DownloadVerified ¶
DownloadVerified streams spec.URL into a fresh temp file beside spec.InstallPath while computing SHA256, then verifies the digest matches spec.WantSHA256. Returns the temp file path so the caller can hand it to AtomicReplace. On any error the temp file is removed.
The temp file is created with os.CreateTemp, which uses a random suffix and opens with O_CREATE|O_EXCL — so an attacker can't pre-create a symlink at a predictable path and trick us into writing through it. The temp lives in filepath.Dir(spec.InstallPath) so the subsequent os.Rename is atomic (same filesystem).
func ParseChecksums ¶
ParseChecksums parses sha256sum-format output: each line is "<64-hex> <name>" or "<64-hex> *<name>". Comments (#) and blank lines are skipped. Returns a map of filename → lower-case hex digest. Duplicate names keep the first occurrence and an error is returned.
Input is read with a bounded scanner buffer so a hostile checksums file can't drive the parser into unbounded memory.
func RealCosignExec ¶
RealCosignExec locates cosign in PATH and executes it. Returns ErrCosignNotFound when the binary is absent.
func VerifyChecksumsSignature ¶
func VerifyChecksumsSignature(ctx context.Context, run CosignExecFunc, checksums, sig, cert []byte) error
VerifyChecksumsSignature verifies the cosign keyless signature over the raw checksums.txt bytes, using the detached signature and certificate published with the release (checksums.txt.sig / checksums.txt.pem).
The three byte slices are written to a private temp dir because cosign verify-blob operates on files. Any verification failure — wrong identity, wrong issuer, tampered checksums — returns an error carrying cosign's output; the caller must treat that as fatal for the update.
Types ¶
type AssetSpec ¶
type AssetSpec struct {
Name string // asset name in checksums.txt (e.g. "ezyshield-linux-amd64")
URL string // direct HTTPS download URL
WantSHA256 string // lower-case hex digest from checksums.txt
InstallPath string // final on-disk path (atomic destination)
}
AssetSpec describes a single binary asset to download, verify, and install.
type Client ¶
Client fetches release metadata and binary assets. APIBaseURL and Repo are overridable so tests can point the client at an httptest server.
func NewClient ¶
func NewClient() *Client
NewClient returns a Client with conservative timeouts and the default public-repo URLs. Override APIBaseURL / Repo to redirect (env-var case) or for tests.
func (*Client) Download ¶
Download streams the asset at u into dst. It enforces HTTPS, a hard size cap, and returns an error if the response status isn't 200. Returns bytes written.
func (*Client) DownloadChecksums ¶
DownloadChecksums fetches checksums.txt into memory (small file) and parses it. Capped at maxChecksumSize.
func (*Client) DownloadSmall ¶
DownloadSmall fetches a small release asset (checksums, detached signature, certificate) fully into memory, capped at maxChecksumSize.
func (*Client) LatestRelease ¶
LatestRelease fetches /repos/{repo}/releases/latest. GitHub excludes prereleases from this endpoint by design — a 404 during the RC phase (before any stable tag exists) is translated to ErrNoStableRelease so callers can give an actionable message instead of a bare "not found".
func (*Client) NewestRelease ¶
NewestRelease fetches the single most recent release regardless of prerelease status (GET /releases?per_page=1 — GitHub returns releases newest-first). Used only to surface an actionable "pin this exact tag" suggestion when LatestRelease finds no stable release; the result is NEVER auto-installed.
type CosignExecFunc ¶
CosignExecFunc runs cosign with args and returns its combined output. It is injectable so the verification flow is testable without cosign or network access to the Sigstore infrastructure.
type Downloader ¶
type Downloader interface {
Download(ctx context.Context, url string, dst io.Writer) (int64, error)
}
Downloader is the slice of *Client that DownloadVerified needs.