bundle

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package bundle publishes and pulls signed release bundles as OCI artifacts.

Layout: artifactType application/vnd.halos.release.v1+tar; config blob is manifest.json; one layer is the bundle tar. The signature covers the tar digest and is stored in a manifest annotation. Immutable release tags are "v<version>". Which release a ring serves is decided by a signed ring pointer ("ring-<name>.pointer", see pointer.go); the plain "ring-<name>" tag is kept for humans and tooling only and is never trusted by halod.

Index

Constants

View Source
const (
	ArtifactType    = "application/vnd.halos.release.v1+tar"
	ConfigMediaType = "application/vnd.halos.release.config.v1+json"
	LayerMediaType  = "application/vnd.halos.release.layer.v1.tar"
	AnnSignature    = "dev.halos.signature"
	AnnSigType      = "dev.halos.signature.type"
)
View Source
const (
	PointerArtifactType = "application/vnd.halos.pointer.v1"
	PointerConfigType   = "application/vnd.halos.pointer.v1+json"
	DefaultPointerTTL   = 7 * 24 * time.Hour
	DefaultMaxLayerSize = 256 << 20
)

Signed ring pointers (TUF-lite). A ring tag alone is unauthenticated: whoever controls the registry could retag it to an older signed release or to one built for another ring. The pointer is a signed statement {org, ring, digest, seq, issuedAt, expiresAt} stored as its own OCI artifact under tag "ring-<name>.pointer". halod trusts the pointer, never the ring tag: it checks org/ring against its config, refuses seq regressions (freshness), refuses expired pointers, and only then fetches the release it names. Rollback is a NEW pointer with a higher seq naming an older digest.

Variables

This section is empty.

Functions

func DefaultPointerStatePath

func DefaultPointerStatePath() (string, error)

DefaultPointerStatePath is $XDG_STATE_HOME/halos/pointers.json (~/.local/state/halos/pointers.json when XDG_STATE_HOME is unset).

func ExecRunner

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

ExecRunner is the real Runner.

func GenerateKeyPair

func GenerateKeyPair() (privPEM, pubPEM []byte, err error)

GenerateKeyPair returns PKCS#8 private and PKIX public PEM.

func ParseRef

func ParseRef(ref string) (repo, tag string)

ParseRef splits "host/repo:tag" into repo and tag ("" if absent).

func PointerChannels

func PointerChannels(ctx context.Context, src oras.ReadOnlyTarget, v Verifier, p Pointer) ([]string, error)

PointerChannels fetches and verifies the release p names and returns the channels it routes p.Ring's devices to. Read-only (dashboards).

func PointerTag

func PointerTag(ring string) string

PointerTag is the tag holding ring's signed pointer.

func Publish

func Publish(ctx context.Context, dst oras.Target, rel *release.Release, s Signer, rings ...string) (ocispec.Descriptor, error)

Publish pushes rel, signs it, tags it v<version> (immutable: an existing tag pointing elsewhere is an error), and writes a signed pointer (plus the informational ring-<name> tag) for each ring given. s must be ed25519 or a MultiSigner whose first signer is ed25519; further signers (cosign) are stored as co-signatures. Registries must also enforce tag immutability. Publish is stateless; PublishRing takes PointerOptions.

func PublishChannel

func PublishChannel(ctx context.Context, dst oras.Target, v *release.Release, s Signer, opts *PointerOptions) (string, error)

PublishChannel publishes one client-axis variant release on its own channel (policy.ChannelName of its manifest) with the same preflight and signer-state checks as a ring pointer. No ring pointer moves: devices reach the channel only once a ring release lists the experiment (PublishRing), so this pre-stages a canary without exposing anyone. Returns the channel.

func PublishRing

func PublishRing(ctx context.Context, dst oras.Target, rel *release.Release, variants []*release.Release, s Signer, ring string, opts *PointerOptions) (ocispec.Descriptor, []string, error)

PublishRing publishes a ring release and its variant releases (as built by release.BuildRing): every variant on its channel first, then the ring, so a device never sees a ring manifest naming a channel that is not yet published. Everything is checked before anything is pushed: the variants must be exactly the channels rel's manifest lists for ring. Returns the ring release's descriptor and the channels written.

func Pull

Pull fetches tag and verifies the signature over the tar digest with v before downloading the layer (capped at DefaultMaxLayerSize). It proves authenticity only, not freshness or ring scope: halod must use PullRing. A nil verifier is refused so unverified content can never be returned.

func Repository

func Repository(repoRef string, plainHTTP bool) (*remote.Repository, error)

Repository opens a remote registry repository ("host/org/name") using docker-config credentials when available.

func RingTag

func RingTag(ring string) string

RingTag / VersionTag name the tags Publish writes.

func Stage

func Stage(ctx context.Context, dst oras.Target, rel *release.Release, s Signer) (already bool, err error)

Stage pushes, signs and tags rel as v<version> with no pointer. Re-staging the same release is a no-op (a new OCI manifest would carry a new creation time and trip tag immutability); a v-tag naming a different release is an error. It reports whether rel was already there.

func VersionTag

func VersionTag(ver string) string

Types

type Cosign

type Cosign struct {
	Bin     string // default "cosign"
	Key     string // signing key ref (Sign) or public key ref (Verify)
	Keyless bool   // opt in to transparency-log upload/verification
	Runner  Runner // default ExecRunner
}

Cosign signs/verifies the digest string as a blob using `cosign sign-blob` / `verify-blob` with a key reference (file, KMS URI, ...).

Cosign is a co-signer only: halod verifies ed25519 and never shells out to cosign as root, so Publish refuses a cosign-only signer (wrap it as MultiSigner{ed25519, Cosign}). Key-based signing does NOT upload to the public Rekor transparency log (release digests of a private org would otherwise be published) unless Keyless is set. Keyless (Fulcio + Rekor) verification needs certificate identity/issuer pinning and a --bundle transport, which is not implemented here.

func (Cosign) Sign

func (c Cosign) Sign(ctx context.Context, digest string) ([]byte, error)

func (Cosign) Type

func (Cosign) Type() string

func (Cosign) Verify

func (c Cosign) Verify(ctx context.Context, digest string, sig []byte) error

type Ed25519Signer

type Ed25519Signer struct{ Key ed25519.PrivateKey }

Ed25519Signer signs with an ed25519 key.

func ParseEd25519Signer

func ParseEd25519Signer(privPEM []byte) (Ed25519Signer, error)

ParseEd25519Signer parses a PKCS#8 PEM private key.

func (Ed25519Signer) Sign

func (s Ed25519Signer) Sign(_ context.Context, digest string) ([]byte, error)

func (Ed25519Signer) Type

func (Ed25519Signer) Type() string

type Ed25519Verifier

type Ed25519Verifier struct{ Key ed25519.PublicKey }

Ed25519Verifier verifies with an ed25519 public key.

func ParseEd25519Verifier

func ParseEd25519Verifier(pubPEM []byte) (Ed25519Verifier, error)

ParseEd25519Verifier parses a PKIX PEM public key.

func (Ed25519Verifier) Type

func (Ed25519Verifier) Type() string

func (Ed25519Verifier) Verify

func (v Ed25519Verifier) Verify(_ context.Context, digest string, sig []byte) error

type MultiSigner

type MultiSigner []Signer

MultiSigner signs with its first signer (the one halod verifies; must be ed25519) and records every other signer (e.g. Cosign) as a co-signature on the release manifest.

func (MultiSigner) Sign

func (m MultiSigner) Sign(ctx context.Context, d string) ([]byte, error)

func (MultiSigner) Type

func (m MultiSigner) Type() string

type Pointer

type Pointer struct {
	Org    string `json:"org"`
	Ring   string `json:"ring"`
	Digest string `json:"digest"` // release tar digest (what the release signature covers)
	// Manifest/ManifestSize address the release's OCI manifest by content, so
	// halod never resolves a mutable tag to find it.
	Manifest     string    `json:"manifest"`
	ManifestSize int64     `json:"manifestSize"`
	Seq          uint64    `json:"seq"`
	IssuedAt     time.Time `json:"issuedAt"`
	ExpiresAt    time.Time `json:"expiresAt"`
}

Pointer is the signed ring -> release statement.

func PromoteRing

func PromoteRing(ctx context.Context, t oras.Target, from Source, ring string, s Signer, opts *PointerOptions) ([]Pointer, error)

PromoteRing points ring at the release from names (verified with s's key) and, if that release was built for ring, points each of its channels at the variant release published with it (tag v<release.VariantVersion>, whose signed manifest must match). Every release is fetched and verified, and every pointer about to be replaced passes preflight, before any pointer is written. Rollback is PromoteRing to an older version: ring and channels return to that snapshot together, each with a higher seq. Returns the ring pointer first, then one per channel (Pointer.Ring is the channel).

func PullRing

PullRing is halod's entry point: verify the ring pointer (signature, org, ring, seq, expiry), then fetch the release it names by content address, verify its signature BEFORE downloading the layer, cap the layer size, and check the digest. Nothing unverified is returned.

func ReadPointer

func ReadPointer(ctx context.Context, src oras.ReadOnlyTarget, ring string, v Verifier) (p Pointer, found bool, err error)

ReadPointer fetches and verifies ring's pointer. found=false (nil error) means no pointer exists yet.

func RefreshRing

func RefreshRing(ctx context.Context, t oras.Target, ring string, s Signer, opts *PointerOptions) ([]Pointer, error)

RefreshRing re-signs ring's pointer and the pointer of every channel its current release routes to (same releases, higher seq, new expiry). Each served pointer must be unexpired and pass the state check (and the ring's must name opts.ExpectDigest, if set), and each release is re-verified, before anything is written. Returns the ring pointer first.

func ServedPointer

func ServedPointer(ctx context.Context, src oras.ReadOnlyTarget, s Signer, ring string) (p Pointer, found bool, err error)

ServedPointer reads and verifies (with s's public key) the pointer ring currently serves. found is false when the ring was never published.

type PointerMark

type PointerMark struct {
	Seq      uint64    `json:"seq"`
	Digest   string    `json:"digest"`
	IssuedAt time.Time `json:"issuedAt"`
}

PointerMark is the last pointer the signer wrote for one ring or channel.

type PointerOptions

type PointerOptions struct {
	// State remembers the pointers this signer wrote; a served pointer older
	// than (or forked from) the recorded one is refused, and every write is recorded.
	State *PointerState
	// ExpectDigest, if set, is the release digest the operation must act on:
	// the source release of a promote/rollback, the ring's current release on refresh.
	ExpectDigest string
	// AdoptExisting lets a signer whose State has no record for a ring take
	// over the pointer the registry serves (first run with a new or evicted
	// state file). Without it, or an ExpectDigest naming the served release,
	// such a pointer is refused: a missing state file must not silently turn
	// replay protection off.
	AdoptExisting bool
	// BeforeSign is called with each pointer, and the version of the release
	// it names, just before it is signed.
	BeforeSign func(p Pointer, version string)
	// contains filtered or unexported fields
}

PointerOptions are the signer-side guards every pointer write runs. The zero value (or nil) is stateless: writes still refuse foreign-org pointers, and refresh/promote-from-ring still refuse expired pointers, but a replayed unexpired pointer is only caught by State or ExpectDigest.

type PointerState

type PointerState struct {
	// contains filtered or unexported fields
}

PointerState is the on-disk signer state (a JSON file; CI can cache it). Keys are "<registry repo>#<ring>". Not safe for concurrent writers: run one signer per state file.

func LoadPointerState

func LoadPointerState(path, scope string) (*PointerState, error)

LoadPointerState reads path (a missing file is empty state) scoped to the registry repo scope.

func (*PointerState) Mark

func (s *PointerState) Mark(ring string) (PointerMark, bool)

Mark returns the recorded pointer for ring.

type RingOptions

type RingOptions struct {
	Org, Ring string
	// MinSeq/LastDigest are the last applied pointer for this ring (zero on first run).
	MinSeq     uint64
	LastDigest string
	Now        time.Time
	// MaxLayerSize caps the release tar; 0 = DefaultMaxLayerSize.
	MaxLayerSize int64
}

RingOptions are halod's freshness/scope requirements for PullRing.

type Runner

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

Runner executes a binary and returns combined output; injectable for tests.

type Signer

type Signer interface {
	Type() string
	Sign(ctx context.Context, digest string) ([]byte, error)
}

Signer signs a release digest string (e.g. "sha256:ab..."). The digest is of the bundle tar, so it transitively covers manifest.json and every file.

type Source

type Source struct {
	Ring     string // the release Ring's signed pointer names (promote)
	Version  string // tag v<Version> (rollback)
	Manifest string // the release's OCI manifest digest (rollback by digest)
}

Source names the release a ring is pointed at. Set exactly one field. None of them trusts a mutable tag on its own: a ring source follows that ring's signed pointer, a version source must carry that version in its signed manifest, and a manifest source is a content address.

func (Source) String

func (src Source) String() string

type Verifier

type Verifier interface {
	Type() string
	Verify(ctx context.Context, digest string, sig []byte) error
}

Verifier checks a signature made by the matching Signer type.

Jump to

Keyboard shortcuts

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