oci

package
v1.32.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: Apache-2.0 Imports: 34 Imported by: 0

Documentation

Overview

Package oci holds the grc.store OCI mechanics shared by `pvtr publish` (push a signed plugin index) and `pvtr install` (pull + verify one).

This file is the first, deliberately small member: hub discovery. A user configures exactly ONE endpoint — the hub base URL — and the OCI registry host is learned from the hub's /.well-known/grc-store-configuration document (ADR-0026). Nothing in pvtr hardcodes the registry host, mirroring how the hub advertises it via HUB_OCI_PUBLIC_URL.

Index

Constants

View Source
const (
	MediaTypePluginConfig = mediatype.PluginConfig
	MediaTypePluginBinary = mediatype.PluginBinary
)

grc.store plugin media types — re-exported from the shared protocol contract so the wire strings have a single source of truth (grc-store-protocol/mediatype, ADR-0035) instead of a hand-synced copy.

View Source
const BundleMediaType = mediatype.SigstoreBundle

BundleMediaType is the artifactType/mediaType of the Sigstore v0.3 bundle stored as an OCI 1.1 referrer of the plugin index — re-exported from the shared protocol contract (single source of truth; ADR-0035).

View Source
const DefaultHubURL = "https://hub.grc.store"

DefaultHubURL is the production grc.store hub base, used when no hub URL is configured. Override it with the "hub-url" config key — set in config.yml, or via the PVTR_HUB_URL environment variable (e.g. http://localhost:8088 against the local dev stack, or https://hub.preview.grc.store against preview).

View Source
const ReservedPluginSegment = "plugins"

ReservedPluginSegment is the reserved repo path segment distinguishing a plugin OCI repository (<ns>/plugins/<plugin_id>) from a Gemara catalog repo under the same namespace (grc.store ADR-0034 decision 5). Mirrors the hub's store.ReservedPluginSegment. It must stay OCI-distribution-spec-valid (a path component must begin with [a-z0-9]); the earlier "_plugins" value was illegal and was changed to "plugins".

Variables

View Source
var ErrNotIndex = errors.New("plugin tag did not resolve to an OCI image index")

ErrNotIndex is returned when a plugin tag resolves to something other than an OCI image index. The grc.store contract requires the tag to be an index even for a single platform.

View Source
var ErrPluginNotFound = fmt.Errorf("plugin not found on grc.store")

ErrPluginNotFound is returned when the hub has no such plugin coordinate.

Functions

func AttachSignature

func AttachSignature(ctx context.Context, target oras.Target, indexDesc ocispec.Descriptor, sig *SignedBundle) error

AttachSignature pushes the signature bundle as an OCI 1.1 referrer of the already-pushed index: a manifest whose subject is the index descriptor, artifactType BundleMediaType, with one layer (also BundleMediaType) carrying the bundle JSON. This is the exact inverse of pull.go's fetchSignatureBundle. target must be the same repository the index was pushed to.

func FetchBytes

func FetchBytes(ctx context.Context, target content.Fetcher, desc ocispec.Descriptor, limit int64) ([]byte, error)

FetchBytes fetches a descriptor's content (capped). oras's Fetch content-verifies against the descriptor digest internally; the verify walk ALSO re-checks digests explicitly so a mismatch surfaces as a named ErrDigestMismatch (defense-in-depth, not redundancy theater). Exported so the verify package can fetch children of an already-verified index.

When the descriptor declares a size that already exceeds the limit, the fetch is skipped entirely — we know it will be over-cap without touching the wire. When the content arrives, we read limit+1 bytes so that an over-cap stream returns an explicit size error rather than silent truncation (which would surface later as a spurious ErrDigestMismatch, a tampering-shaped signal for what is actually a size-limit condition).

func FetchSignature

func FetchSignature(ctx context.Context, target oras.ReadOnlyTarget, indexDesc ocispec.Descriptor) ([][]byte, error)

FetchSignature discovers all Sigstore bundles attached to an index in a target and returns their raw JSON bytes (nil when unsigned). Re-signing accumulates referrers without removing prior ones, so multiple bundles can be present; the verify walk tries each and proceeds with the first that passes. Exported so a caller that already has an index descriptor + target (e.g. re-verification, tests) can run the same discovery PullIndex does. It is the exact inverse of AttachSignature. The truncation flag is internal to the pull path, so this re-verification helper drops it.

func HubURL

func HubURL() string

HubURL returns the configured hub base URL with no trailing slash. Resolution precedence: the "hub-url" config key (config.yml or PVTR_HUB_URL env, via viper) first, then the PVTR_HUB_URL environment variable read directly (a fallback for pre-viper-init callers such as unit tests), then DefaultHubURL.

func IsPrivateerPlugin

func IsPrivateerPlugin(path string) (bool, error)

IsPrivateerPlugin reports whether the binary at path embeds the Privateer go-plugin handshake marker — i.e. it serves via the SDK's shared.Serve and is therefore a binary `pvtr run` can actually execute. The check is a STATIC byte scan (the binary is NEVER executed — we can't run foreign-arch binaries, and we won't exec untrusted bytes at publish time).

The marker is the go-plugin magic cookie (key + value) from shared.GetHandshakeConfig() — compiled string literals that survive `-s -w` stripping (verified against a real, stripped pvtr-github-repo-scanner build). The cookie strings are pulled from `shared` (DRY) — never hardcoded here, so they can't drift from what the host (`pvtr run`) handshakes with.

func Push

func Push(ctx context.Context, idx *AssembledIndex, opts PushOptions) (indexDigest string, err error)

Push writes an AssembledIndex to the registry under <host>/<coordinate-namespaced repo>:<version> and tags the index. It walks leaves-first: config + binary blobs, then child manifests, then the index. It is content-addressed throughout, so re-pushing identical bytes is a no-op at the registry. Signing is NOT done here — the index digest this produces is what cosign signs (separately) and what the manifest records.

func SignAndAttach

func SignAndAttach(ctx context.Context, idx *AssembledIndex, push PushOptions, signing SignerOptions) error

SignAndAttach signs the assembled index against public-good Fulcio/Rekor with the given signing ID token and attaches the bundle as the index's OCI referrer in the registry. It builds the same repository the index was pushed to (so the referrer lands beside the index). opts carries the registry token for the authenticated push of the referrer.

func Sync

func Sync(ctx context.Context, hubURL, coordinate, tag, upstreamBearer string) error

Sync tells the hub to ingest a pushed plugin index: it re-fetches the index from its own registry, verifies the signature against its embedded trusted root, authorizes by namespace ownership, and persists it. upstreamBearer is the same OIDC token used for the push (the hub authorizes /sync by namespace ownership). hubURL is the hub base (PVTR_HUB_URL).

The hub performs server-side work (signature verification, registry re-fetch) so sync gets a 60-second bound — longer than the 15-second default for plain hub-API GETs. The bound lives on this call's own http.Client; the request still carries the caller's context, so an already-cancelled parent short-circuits immediately. The request is routed through the hub Client's doJSON helper for a consistent error shape.

func ValidateForPublish

func ValidateForPublish(p AssembleParams) error

ValidateForPublish enforces the hub's ingest contract BEFORE assembly/push/ sign, so a malformed index never reaches the registry (avoiding orphaned, signed-but-rejected bytes). It mirrors the checks the hub runs at /sync: non-empty evaluates with namespaced catalogs + at least one requirement id each, a present entrypoint, and version == the resolved tag. Pure (no network); called by `pvtr publish` first.

func ValidatePluginBinaries

func ValidatePluginBinaries(bins []PlatformBinary) error

ValidatePluginBinaries rejects a publish whose dist contains a binary that is not a Privateer plugin (missing the handshake marker) — catching a dist that points at the wrong build, or a binary that doesn't serve via shared.Serve, BEFORE anything is pushed or signed. It scans every resolved binary; a single non-plugin fails the whole publish.

Types

type AssembleParams

type AssembleParams struct {
	// Coordinate is "<namespace>/<plugin_id>" — the grc.store push coordinate.
	Coordinate string
	// Plugin is the "owner/repo" recorded in each config blob's "plugin" field.
	Plugin string
	// Version is the release version (tag without leading v is fine; recorded
	// verbatim in the config blobs).
	Version string
	// License is the publication license as a (canonical) SPDX expression,
	// written into every config blob. grc.store requires it; the caller
	// (pvtr publish) validates and canonicalizes it before assembly.
	License string
	// Binaries are the resolved per-platform binaries (darwin universal already
	// re-expanded by LoadGoReleaserBuild).
	Binaries []PlatformBinary
	// Evaluates is the control-catalog linkage, identical across platforms,
	// written into every config blob. Optional.
	Evaluates []pluginspec.Evaluate
}

AssembleParams are the inputs to AssembleIndex.

type AssembledIndex

type AssembledIndex struct {
	Coordinate string // "<namespace>/<plugin_id>"
	Version    string
	Index      blob   // the OCI image index (the digest the signature covers)
	Manifests  []blob // child image manifests, one per platform descriptor
	Blobs      []blob // config + binary blobs (deduplicated by digest)
}

AssembledIndex is the full set of content-addressed artifacts for a plugin version: the image index, every child manifest, and every config + binary blob. The push layer walks Blobs (leaves first) then Manifests then Index.

func AssembleIndex

func AssembleIndex(p AssembleParams) (*AssembledIndex, error)

AssembleIndex builds the full OCI image index for a plugin version from the resolved per-platform binaries. It is pure (reads the binary files, produces content-addressed blobs in memory) and does no network or signing — signing is cosign's job after this returns, push is oras's. Binary blobs are deduplicated by digest, so the two darwin descriptors over one fat binary share a single layer blob (the §3.1 contract).

func (*AssembledIndex) IndexDigest

func (idx *AssembledIndex) IndexDigest() string

IndexDigest returns the assembled index's digest (sha256:...), the value the signature is over and that the manifest records.

func (*AssembledIndex) PushTo

func (idx *AssembledIndex) PushTo(ctx context.Context, target oras.Target) (string, error)

PushTo writes the assembled index to any oras Target (a remote repository, or an in-memory store for tests), leaves-first: config + binary blobs, then child manifests, then the tagged index. Content-addressed throughout, so re-pushing identical bytes is idempotent. Returns the index digest (what gets signed).

type BrowseItem

type BrowseItem struct {
	Namespace     string `json:"namespace"`
	PluginID      string `json:"plugin_id"`
	LatestVersion string `json:"latest_version"`
	Signed        bool   `json:"signed"`
}

BrowseItem is one entry from GET {hub}/v1/plugins. Only the fields pvtr uses are decoded. namespace+plugin_id form the install coordinate; this endpoint is DISCOVERY, not curation — it lists whatever has been published.

func (BrowseItem) Coordinate

func (b BrowseItem) Coordinate() string

Coordinate returns the "<namespace>/<plugin_id>" install coordinate.

type Client

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

Client fetches the hub discovery document. It is intentionally tiny — a base URL and an HTTP client — so both publish and install share one resolution path.

func NewClient

func NewClient() *Client

NewClient builds a discovery client against the configured hub (PVTR_HUB_URL, default DefaultHubURL).

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the hub base URL this client targets.

func (*Client) Browse

func (c *Client) Browse(ctx context.Context) ([]BrowseItem, error)

Browse lists the plugins published to the configured hub (anonymous). It is a directory for `pvtr list --installable`, NOT an install-time gate — install trust comes from the §6 signature/identity verification, never from presence in this list.

func (*Client) Discover

func (c *Client) Discover(ctx context.Context) (*Discovery, error)

Discover fetches and decodes the hub's /.well-known/grc-store-configuration document.

func (*Client) GetPluginDetails

func (c *Client) GetPluginDetails(ctx context.Context, namespace, pluginID string) (*PluginDetail, error)

GetPluginDetails fetches GET /v1/plugins/<ns>/<id> from the configured hub (anonymous). A 404 yields ErrPluginNotFound (a clear "no such plugin").

type Discovery

type Discovery struct {
	// RegistryURL is the OCI registry origin WITH scheme (e.g.
	// http://localhost:5050). Use RegistryHost to get the scheme-stripped
	// host an oras/Docker reference needs.
	RegistryURL  string `json:"registry_url"`
	HubURL       string `json:"hub_url"`
	OIDCIssuer   string `json:"oidc_issuer,omitempty"`
	OIDCClientID string `json:"oidc_cli_client_id,omitempty"`
}

Discovery is the subset of the hub's /.well-known/grc-store-configuration document that pvtr consumes. Only the fields pvtr acts on are decoded — registry_url (push/pull target), hub_url (for the claim-namespace hint), and the OIDC coordinates publish/login need; unknown fields are ignored.

func (*Discovery) PlainHTTP

func (d *Discovery) PlainHTTP() bool

PlainHTTP reports whether the discovered registry_url uses http:// (local dev registries) and therefore requires plain-HTTP transport instead of TLS. Part of the same single-point-of-truth as RegistryHost — both delegate to parseRegistryURL so the scheme interpretation can never drift between them.

func (*Discovery) RegistryHost

func (d *Discovery) RegistryHost() (string, error)

RegistryHost returns the OCI registry host (no scheme, no trailing slash) to build an oras/Docker reference from. registry_url is advertised WITH a scheme (ADR-0026) but OCI references are host[:port]-only, so the scheme is stripped here — part of the same single-point-of-truth as PlainHTTP.

type FetchedIndex

type FetchedIndex struct {
	Coordinate      string
	Version         string
	IndexDescriptor ocispec.Descriptor
	IndexBytes      []byte
	// SignatureBundles holds the raw Sigstore v0.3 bundle JSON for each
	// signature referrer. Nil (or empty) means unsigned. Multiple entries arise
	// when the index has been re-signed: re-signing pushes a new bundle without
	// removing the old one, so bundles accumulate across signing runs.
	SignatureBundles [][]byte
	// SignaturesTruncated is true when the index carried more signature referrers
	// than maxSignatureReferrers, so not all were inspected. Verify uses it to
	// distinguish "no valid signature within the inspected set" from "genuinely
	// unsigned" — a registry that floods junk referrers must not be able to mask a
	// real signature as absent.
	SignaturesTruncated bool
	// contains filtered or unexported fields
}

FetchedIndex is the raw, NOT-yet-verified result of pulling a plugin index: the index descriptor (its digest), the index bytes, and the signature bundles discovered as referrers (nil when the index carries no signature referrer — that is "unsigned", a verify-time concern, not a fetch error; multiple entries when the index has been re-signed, since AttachSignature never removes prior referrers and pushes are content-addressed/idempotent). Everything here is untrusted until verify runs.

func NewFetchedIndex

func NewFetchedIndex(coordinate, version string, indexDesc ocispec.Descriptor, indexBytes []byte, signatureBundles [][]byte, target oras.ReadOnlyTarget) *FetchedIndex

NewFetchedIndex builds a FetchedIndex from an already-open read-only target (e.g. an in-memory store or an OCI layout) instead of a live registry pull. It is used by tests and by any caller that has the index bytes + bundles in hand. No verification is performed — the result is untrusted input to verify.Index, exactly like PullIndex's output.

func PullIndex

func PullIndex(ctx context.Context, coordinate, version string, opts PullOptions) (*FetchedIndex, error)

PullIndex resolves <host>/<ns>/plugins/<id>:<version>, fetches the index, and discovers the signature bundle referrer. It performs NO verification — the caller passes the result to verify.Index. The anonymous bearer-token dance is handled by oras-go.

func (*FetchedIndex) Target

func (f *FetchedIndex) Target() oras.ReadOnlyTarget

Target exposes the read-only target for the verify walk to fetch children.

type PlatformBinary

type PlatformBinary struct {
	OS         string // GOOS, e.g. "linux", "darwin", "windows"
	Arch       string // GOARCH, e.g. "amd64", "arm64", "386"
	Path       string // absolute path to the binary on disk
	Entrypoint string // go-plugin entrypoint name (extra.Binary), .exe-suffixed on windows
}

PlatformBinary is one resolved (os, arch) -> binary mapping the index assembler turns into a child manifest. Universal darwin binaries are already re-expanded here: the single fat binary yields two PlatformBinaries (darwin/amd64, darwin/arm64) sharing the same Path, so a later content-addressed push gives them the same blob digest.

func HostPlatformBinary

func HostPlatformBinary(bins []PlatformBinary) (PlatformBinary, error)

HostPlatformBinary returns the PlatformBinary matching the running host's os/arch, or an error naming the available platforms. Used by the local push smoke path and as the analogue of the installer's child-selection step.

func LoadGoReleaserBuild

func LoadGoReleaserBuild(distDir string) (version string, bins []PlatformBinary, err error)

LoadGoReleaserBuild reads dist/artifacts.json + dist/metadata.json from a GoReleaser dist directory and returns the release version and the resolved per-platform binaries (darwin universal already re-expanded). Paths in artifacts.json are relative to the repo root GoReleaser ran in (e.g. "dist/<id>_linux_amd64_v1/<bin>"). When the dist directory has been renamed or downloaded to a different location, the path-resolution logic tries two candidates: the original repo-root-relative layout, and a stripped layout inside distDir (see resolvePlatformBinaries).

type PluginDetail

type PluginDetail struct {
	Namespace      string          `json:"namespace"`
	PluginID       string          `json:"plugin_id"`
	LatestVersion  string          `json:"latest_version"`
	SignerIdentity string          `json:"signer_identity"`
	Releases       []PluginRelease `json:"releases"`
}

PluginDetail is the hub's plugin-level record (GET /v1/plugins/<ns>/<id>) — the single source of truth for install resolution: existence, the resolvable versions, and the publisher's pinned signer identity. (signer_identity lives ONLY on this plugin-level endpoint, not the version-detail one.)

func (*PluginDetail) Coordinate

func (d *PluginDetail) Coordinate() string

Coordinate returns "<namespace>/<plugin_id>".

func (*PluginDetail) ResolveRelease

func (d *PluginDetail) ResolveRelease(requestedVersion string) (*PluginRelease, error)

ResolveRelease returns the release to install: the one matching requestedVersion, or the latest_version release when requestedVersion is empty. Resolving against the authoritative release list (not a client guess) is what lets the installer cross-check the hub-recorded index_digest against what the registry actually serves.

type PluginRelease

type PluginRelease struct {
	Version     string `json:"version"`
	IndexDigest string `json:"index_digest"`
	Signed      bool   `json:"signed"`
}

PluginRelease is one version in the plugin's release history.

type PullOptions

type PullOptions struct {
	RegistryHost string
	PlainHTTP    bool
}

PullOptions configures an anonymous plugin-index pull.

type PushOptions

type PushOptions struct {
	// RegistryHost is the scheme-stripped registry host (e.g. "localhost:5050"
	// or "oci.grc.store"), as returned by Discovery.RegistryHost.
	RegistryHost string
	// PlainHTTP forces http:// instead of https:// — required for the local dev
	// zot at localhost:5050. Prod (oci.grc.store) is https, so this defaults
	// false.
	PlainHTTP bool
	// Client is the auth-capable HTTP client. Nil uses oras's anonymous default
	// (fine for a no-auth registry or anonymous pull-only checks; a push to a
	// bearer-gated registry needs a credentialed client).
	Client remote.Client
	// RegistryToken is a zot registry token (from MintRegistryToken) for an
	// authenticated push to a bearer-gated registry. When set (and Client is
	// nil), the push client sends it directly to the registry as the access
	// token — oras does no /v2/token exchange because we already did it. Empty
	// keeps the anonymous default (backward-compatible).
	RegistryToken string
}

PushOptions configures a push to an OCI registry.

type RegistryToken

type RegistryToken struct {
	Token   string
	Actions []string // granted actions on the plugin repo (e.g. ["pull"] or ["pull","push"])
}

RegistryToken is a minted zot registry token plus the actions the hub actually granted on the plugin repo. GrantsPush reports whether push was granted — pvtr checks this BEFORE pushing so an unowned-namespace publish fails fast (legibly), instead of minting a pull-only token and failing at the raw registry push (after already prompting for a sigstore sign-in).

func MintRegistryToken

func MintRegistryToken(ctx context.Context, hubURL, coordinate, upstreamBearer string) (RegistryToken, error)

MintRegistryToken exchanges an upstream OIDC bearer for a zot registry token scoped to push+pull on the plugin repo, and reports the actions the hub actually granted. pvtr does this exchange itself (rather than leaning on oras's OAuth2 assumptions) because the hub's /v2/token is a GET realm keyed on the Authorization header — minting here and handing oras a ready registry token (Credential.AccessToken) is the robust path.

hubURL is the hub base; coordinate is "<ns>/<plugin_id>"; upstreamBearer is the device-grant / GHA-OIDC token (empty → an anonymous pull-only token). The request is routed through the hub Client's doJSON helper so the transport bounds (15s timeout, shared error shape) are consistent with other hub API calls.

func (RegistryToken) GrantsPush

func (t RegistryToken) GrantsPush() bool

GrantsPush reports whether the minted token authorizes pushing to the plugin repo.

type SignedBundle

type SignedBundle struct {
	// JSON is the vnd.dev.sigstore.bundle.v0.3+json bytes (the layer content).
	JSON []byte
}

SignedBundle holds the keyless signature bundle for an index, ready to attach as an OCI referrer. It is the result of SignIndex.

func NewSignedBundle

func NewSignedBundle(json []byte) *SignedBundle

NewSignedBundle wraps raw bundle JSON for AttachSignature (e.g. bytes produced out-of-band, or in tests).

func SignIndex

func SignIndex(ctx context.Context, idx *AssembledIndex, opts SignerOptions) (*SignedBundle, error)

SignIndex produces a keyless Sigstore bundle over the assembled index's bytes (whose sha256 IS the index digest the bundle is bound to, matching the verify policy's WithArtifactDigest). It signs against public-good Fulcio/Rekor using the provided OIDC ID token. It does NOT push anything — AttachSignature does.

type SignerOptions

type SignerOptions struct {
	// IDToken is the OIDC identity token Fulcio mints the signing certificate
	// from. It MUST be from a public-good-Fulcio-trusted issuer (GitHub Actions
	// OIDC with audience "sigstore", or an interactive sigstore login) — NOT the
	// Keycloak registry bearer. Required.
	IDToken string
	// FulcioURL / RekorURL override the public-good endpoints (testing only).
	FulcioURL string
	RekorURL  string
}

SignerOptions configures keyless signing of an assembled index.

Jump to

Keyboard shortcuts

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