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
- Variables
- func AttachSignature(ctx context.Context, target oras.Target, indexDesc ocispec.Descriptor, ...) error
- func FetchBytes(ctx context.Context, target content.Fetcher, desc ocispec.Descriptor, ...) ([]byte, error)
- func FetchSignature(ctx context.Context, target oras.ReadOnlyTarget, indexDesc ocispec.Descriptor) ([][]byte, error)
- func HubURL() string
- func IsPrivateerPlugin(path string) (bool, error)
- func Push(ctx context.Context, idx *AssembledIndex, opts PushOptions) (indexDigest string, err error)
- func SignAndAttach(ctx context.Context, idx *AssembledIndex, push PushOptions, ...) error
- func Sync(ctx context.Context, hubURL, coordinate, tag, upstreamBearer string) error
- func ValidateForPublish(p AssembleParams) error
- func ValidatePluginBinaries(bins []PlatformBinary) error
- type AssembleParams
- type AssembledIndex
- type BrowseItem
- type Client
- type Discovery
- type FetchedIndex
- type PlatformBinary
- type PluginDetail
- type PluginRelease
- type PullOptions
- type PushOptions
- type RegistryToken
- type SignedBundle
- type SignerOptions
Constants ¶
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.
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).
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).
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 ¶
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.
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 ¶
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 ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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 ¶
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.