update

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package update implements `redoubt update`: opt-in, never silent, cosign-verified updates of the platform images (plan §3, golden guardrail "auto-update is an attack vector").

Verification is Sigstore keyless: the release workflow signs every image with a short-lived Fulcio certificate bound to its GitHub OIDC identity and records the signature in the Rekor transparency log. This package re-implements the verification cosign performs with the standard library only (golden rule 7 — no new runtime dependency):

  • the ECDSA/Ed25519/RSA signature over the signed payload verifies with the certificate's public key;
  • the certificate chains to the embedded Fulcio roots (the intermediate ships in the signature) and was valid at the time Rekor integrated the entry;
  • the certificate's SubjectAlternativeName URI matches the expected identity (the release workflow of this repository at a release tag) and its OIDC-issuer extension matches the expected issuer (GitHub Actions);
  • the Rekor bundle's signed entry timestamp verifies with the embedded Rekor public key and its body is the hashedrekord of exactly this payload, signature and certificate.

The trust roots are the ones published by the Sigstore public-good instance, vendored in trust/; an operator (or a demo/test) can point at a different root directory.

Index

Constants

View Source
const (
	DefaultIssuer     = "https://token.actions.githubusercontent.com"
	DefaultIdentityRe = `^https://github\.com/BetV3/redoubt/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+`
	DefaultRegistry   = "ghcr.io/betv3/redoubt"
	DefaultRekorURL   = "https://rekor.sigstore.dev"
	DefaultRepo       = "BetV3/redoubt"
)

Default policy: releases of this repository signed by GitHub Actions.

View Source
const PermSettingsManage = "settings.manage"

PermSettingsManage is the permission that may record update events (Owner / Admin).

Variables

View Source
var ErrForbidden = errors.New("update: forbidden")

ErrForbidden is returned when the actor lacks settings.manage.

View Source
var ErrInvalid = errors.New("update: invalid event")

ErrInvalid is returned for malformed events.

View Source
var ErrNotInstalled = errors.New("no Redoubt installation found (missing .env)")

ErrNotInstalled is returned when the bundle directory has no .env.

View Source
var ErrVerification = errors.New("signature verification failed")

ErrVerification wraps every reason a signature is rejected.

View Source
var Images = []string{"platformd", "socket-proxy"}

Images the bundle runs from the release registry.

Functions

func Apply

func Apply(o Options, plan *Plan) (trigger string, err error)

Apply pins the verified digests in .env and drops the apply-update trigger that the systemd path unit (install/systemd/redoubt-apply-update.path) turns into `docker compose pull && up -d`. It never restarts anything itself.

func AutoUpdateFlag

func AutoUpdateFlag(etcDir string) string

AutoUpdateFlag is the file whose presence enables the timer-driven `redoubt update --yes`.

func EnvKey

func EnvKey(name string) string

EnvKey is the .env variable for an image.

func GetEnv

func GetEnv(path, key string) (string, error)

GetEnv returns the value of key in a .env file ("" when absent).

func LatestVersion

func LatestVersion(ctx context.Context, hc *http.Client, repo string) (string, error)

LatestVersion asks the GitHub releases API for the newest tag.

func PayloadDigest

func PayloadDigest(payload []byte) (string, error)

PayloadDigest extracts the image digest a cosign simple-signing payload attests to.

func SetEnv

func SetEnv(path string, values map[string]string) error

SetEnv rewrites KEY=value lines in a compose .env file atomically (temp file + rename), preserving every other line, comments and the file mode. Missing keys are appended.

Types

type Actor

type Actor struct {
	Email string
	Can   func(perm string) bool
}

Actor is the caller recording an update event.

type Event

type Event struct {
	// Kind is "update" (images pinned to verified digests) or "auto_update" (timer toggled).
	Kind    string            `json:"kind"`
	Version string            `json:"version,omitempty"`
	Images  map[string]string `json:"images,omitempty"` // name -> pinned reference
	Enabled bool              `json:"enabled,omitempty"`
	Signer  string            `json:"signer,omitempty"` // verified certificate identity
}

Event is what the host-side CLI reports to the control plane so the audit chain carries it.

type Identity

type Identity struct {
	Subject        string // the SAN URI (workflow identity)
	Issuer         string
	IntegratedTime time.Time
	LogIndex       int64
}

Identity is what a verified signature proves.

func Verify

func Verify(sig Signature, tr TrustRoot, pol Policy) (Identity, error)

Verify checks sig against the trust root and policy. It never consults the network: the Rekor bundle must be present (cosign attaches it to every keyless signature; CLI callers fetch it from Rekor when a signature lacks one).

type Options

type Options struct {
	Version  string // "" = latest GitHub release
	Registry string // e.g. ghcr.io/betv3/redoubt
	Repo     string // GitHub owner/name for the releases API
	Trust    TrustRoot
	Policy   Policy
	Reg      *Registry
	Rekor    *Rekor
	HTTP     *http.Client
	// EtcDir holds .env (and the apply-update trigger); "" = /etc/redoubt.
	EtcDir string
}

Options drive one update check.

type Plan

type Plan struct {
	Version string     `json:"version"`
	Images  []Verified `json:"images"`
	// Current holds the .env values before the update (by image name).
	Current map[string]string `json:"current"`
	// Changed is false when every image is already pinned to the verified digest.
	Changed bool `json:"changed"`
}

Plan is the outcome of a check: what would change and the proof it is genuine.

func Check

func Check(ctx context.Context, o Options) (*Plan, error)

Check resolves and verifies every platform image for the requested version without changing anything.

type Policy

type Policy struct {
	Issuer     string
	IdentityRe *regexp.Regexp
}

Policy names who may have signed a release.

func DefaultPolicy

func DefaultPolicy() Policy

DefaultPolicy is the policy for official releases.

type Recorder

type Recorder struct {
	Audit audit.Sink
	Now   func() time.Time
}

Recorder is the service that turns update events into audit records (RBAC lives here, golden rule 6; every state change audited, golden rule 5).

func (*Recorder) Record

func (r *Recorder) Record(ctx context.Context, actor Actor, ev Event) error

Record validates and audits ev on behalf of actor.

func (*Recorder) RecordVersionChange

func (r *Recorder) RecordVersionChange(ctx context.Context, prev, current string)

RecordVersionChange audits a control-plane restart under a different version than the previous run (the applied side of an update, whoever triggered it). prev is "" on first start.

type Ref

type Ref struct {
	Host, Path, Tag, Digest string
}

Ref is a parsed image reference.

func ParseRef

func ParseRef(s string) (Ref, error)

ParseRef parses host/path[:tag][@digest].

func (Ref) String

func (r Ref) String() string

String renders the reference (digest wins over tag).

type Registry

type Registry struct {
	HTTP *http.Client
	// PlainHTTP allows http:// for loopback registries (demos and tests only).
	PlainHTTP bool
	// contains filtered or unexported fields
}

Registry is a minimal OCI distribution client (stdlib): resolve a tag to a digest and fetch cosign's signature manifest for it. Anonymous token auth (GHCR, Docker Hub) is handled.

func (*Registry) Resolve

func (c *Registry) Resolve(ctx context.Context, ref Ref) (string, error)

Resolve returns the manifest digest a reference points at (index or manifest, as pushed).

func (*Registry) Signatures

func (c *Registry) Signatures(ctx context.Context, ref Ref, digest string) ([]Signature, error)

Signatures fetches cosign's signature manifest (tag sha256-<hex>.sig) for digest and returns every signature it carries, payload included.

type Rekor

type Rekor struct {
	URL  string
	HTTP *http.Client
}

Rekor looks up transparency-log entries for signatures that ship without a bundle.

func (*Rekor) Lookup

func (r *Rekor) Lookup(ctx context.Context, payload []byte) ([]*RekorBundle, error)

Lookup finds the entries recorded for payload and returns them as bundles; the caller verifies them like offline ones (nothing from the log is trusted before its SET checks out).

type RekorBundle

type RekorBundle struct {
	SignedEntryTimestamp string `json:"SignedEntryTimestamp"`
	Payload              struct {
		Body           string `json:"body"`
		IntegratedTime int64  `json:"integratedTime"`
		LogIndex       int64  `json:"logIndex"`
		LogID          string `json:"logID"`
	} `json:"Payload"`
}

RekorBundle is cosign's offline transparency-log proof (the `dev.sigstore.cosign/bundle` annotation / the `--bundle` file): the entry Rekor integrated plus its signed timestamp.

func ParseBundle

func ParseBundle(data []byte) (*RekorBundle, error)

ParseBundle parses a cosign bundle JSON document.

type Signature

type Signature struct {
	Payload   []byte
	Signature []byte // DER (ECDSA) / raw (Ed25519) / PKCS#1 v1.5 (RSA)
	CertPEM   []byte
	ChainPEM  []byte
	Bundle    *RekorBundle
}

Signature is one cosign signature: the payload that was signed, the raw signature, the signing certificate, its chain and the Rekor bundle.

type TrustRoot

type TrustRoot struct {
	Fulcio   *x509.CertPool
	RekorKey crypto.PublicKey
}

TrustRoot holds the certificate roots and the transparency-log key signatures are checked against.

func DefaultTrustRoot

func DefaultTrustRoot() (TrustRoot, error)

DefaultTrustRoot returns the embedded Sigstore public-good trust root.

func LoadTrustRoot

func LoadTrustRoot(dir string) (TrustRoot, error)

LoadTrustRoot reads fulcio.pem and rekor.pub from dir (demo / private Sigstore instances).

func ParseTrustRoot

func ParseTrustRoot(fulcioPEM, rekorPEM []byte) (TrustRoot, error)

ParseTrustRoot parses PEM certificate(s) and a PEM public key.

type Verified

type Verified struct {
	Name     string   `json:"name"`
	Ref      string   `json:"ref"`
	Digest   string   `json:"digest"`
	Identity Identity `json:"identity"`
}

Verified is one image whose signature passed.

Directories

Path Synopsis
Package sigtest is a miniature Sigstore for tests and demos: a Fulcio-like CA that issues short-lived code-signing certificates carrying the OIDC identity extensions, a Rekor-like key that signs entry timestamps, and an in-memory OCI registry that serves images and their cosign signature manifests.
Package sigtest is a miniature Sigstore for tests and demos: a Fulcio-like CA that issues short-lived code-signing certificates carrying the OIDC identity extensions, a Rekor-like key that signs entry timestamps, and an in-memory OCI registry that serves images and their cosign signature manifests.
demosign command
demosign signs images in a local (plain-HTTP, loopback) registry with a throwaway Sigstore (sigtest) and writes the matching trust root, so `make demo-phase3` can exercise `redoubt update` end to end without GitHub OIDC.
demosign signs images in a local (plain-HTTP, loopback) registry with a throwaway Sigstore (sigtest) and writes the matching trust root, so `make demo-phase3` can exercise `redoubt update` end to end without GitHub OIDC.

Jump to

Keyboard shortcuts

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