update

package
v0.2.0-rc.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

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

View Source
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
)
View Source
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

View Source
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).

View Source
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

func AtomicReplace(tmpPath, finalPath string, mode os.FileMode) error

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

func CompareSemver(a, b string) (int, error)

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

func DownloadVerified(ctx context.Context, client Downloader, spec AssetSpec) (string, error)

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

func ParseChecksums(r io.Reader) (map[string]string, error)

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

func RealCosignExec(ctx context.Context, args ...string) ([]byte, error)

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 Asset

type Asset struct {
	Name string `json:"name"`
	URL  string `json:"browser_download_url"`
}

Asset mirrors the GitHub Releases "assets[]" entries we care about.

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

type Client struct {
	HTTP       *http.Client
	APIBaseURL string
	Repo       string
}

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

func (c *Client) Download(ctx context.Context, u string, dst io.Writer) (int64, error)

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

func (c *Client) DownloadChecksums(ctx context.Context, u string) (map[string]string, error)

DownloadChecksums fetches checksums.txt into memory (small file) and parses it. Capped at maxChecksumSize.

func (*Client) DownloadSmall

func (c *Client) DownloadSmall(ctx context.Context, u string) ([]byte, error)

DownloadSmall fetches a small release asset (checksums, detached signature, certificate) fully into memory, capped at maxChecksumSize.

func (*Client) LatestRelease

func (c *Client) LatestRelease(ctx context.Context) (*Release, error)

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

func (c *Client) NewestRelease(ctx context.Context) (*Release, error)

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.

func (*Client) ReleaseByTag

func (c *Client) ReleaseByTag(ctx context.Context, tag string) (*Release, error)

ReleaseByTag fetches /repos/{repo}/releases/tags/{tag}. tag must be a semver-shaped string (no path traversal, no slashes).

type CosignExecFunc

type CosignExecFunc func(ctx context.Context, args ...string) ([]byte, error)

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.

type Release

type Release struct {
	TagName string  `json:"tag_name"`
	Assets  []Asset `json:"assets"`
}

Release is the strict subset of GitHub's release JSON we decode. We intentionally omit other fields to keep the trust boundary small: an attacker controlling the release JSON shouldn't be able to influence behavior through fields we ignore.

func (*Release) FindAsset

func (r *Release) FindAsset(name string) (Asset, bool)

FindAsset returns the asset whose Name exactly matches name, or false.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL