Documentation
¶
Overview ¶
Package provenance verifies that a release artifact was built and signed by cloop's own release workflow, rather than merely arriving intact.
Why a checksum is not enough ¶
The release publishes checksums.txt beside the archives, and both the installer and `cloop upgrade` check the download against it. That check is worth keeping — it catches a truncated download and a corrupted mirror — but it proves transport integrity and nothing more. checksums.txt is fetched from the same GitHub release as the artifact it vouches for, so anyone who can replace the asset can replace the checksum alongside it and both sides of the comparison move together. The verification passes and says nothing.
Provenance is the property the checksum cannot express: not "these are the bytes someone published" but "these are the bytes *our release workflow* produced". That distinction is what this package exists for, and it matters here more than in most projects, because the artifact being installed is the executor agent — the component an enterprise deliberately places on high-value hosts and then hands credentials to.
Keyless, and what is actually pinned ¶
Signing is Sigstore keyless: the release workflow proves its identity to Fulcio with the ambient GitHub Actions OIDC token and receives a short-lived certificate naming the workflow that requested it. There is no long-lived private key to leak, rotate, or store in a repository secret — the signing identity is the workflow file itself.
Verification therefore pins two things, and both are necessary:
- The OIDC issuer, so only GitHub's token service can vouch for identity. Without this an attacker could present a certificate from any issuer Fulcio federates with and satisfy the identity match with, say, an email address they control.
- The certificate identity, so only *this repository's release workflow* on a tag is accepted. A signature from another workflow in this repo, a fork, or a branch build fails — which is the point: an attacker who can replace a release asset still cannot make our tagged release workflow sign their bytes.
A subprocess, not a library ¶
pkg/imagepolicy reached this conclusion first and for the same reasons, so this package follows it: sigstore's Go verification path pulls Fulcio, Rekor, TUF and a certificate chain implementation into the process, and here that process is `cloop upgrade` replacing its own binary. cosign is a single static binary, it is what the sigstore documentation describes, and it is what an operator will reach for by hand when a verification fails and they want to know why.
Fail closed, including when the tool is absent ¶
The dangerous failure is not a bad signature — that is a clean refusal. It is "cosign is not installed", which the obvious implementation turns into a skipped check: every artifact reports fine while nothing is verified, and the misconfiguration is invisible precisely because it looks like success. So a missing cosign is a refusal with an install hint, never a warning.
The escape hatch is explicit and separate: callers that set SkipVerify have made a decision a human can find in a command line or a unit file, which is the property a silent degradation lacks.
Index ¶
Constants ¶
const ( // DefaultIssuer is GitHub's OIDC token service, the only issuer whose // assertions about a workflow's identity are accepted. DefaultIssuer = "https://token.actions.githubusercontent.com" // DefaultIdentityRegexp matches the SAN Fulcio issues to this repository's // release workflow when it runs on a tag. // // Anchored at both ends, and `[^/]+` rather than `.*` for the tag, so it // cannot be satisfied by a longer path that merely begins this way — a // workflow reachable at .../release.yml@refs/tags/v1/../../heads/main // would otherwise match. The literal dots in the workflow path are escaped // for the same reason: unescaped, `.github` matches `xgithub`. // // refs/tags, not refs/heads: a release is cut from a tag, so a signature // produced by this same workflow running on a branch is rejected. That // closes the path where someone who can push a branch — but not tag a // release — gets the release workflow to sign artifacts for them. DefaultIdentityRegexp = `^https://github\.com/blechschmidt/cloop/\.github/workflows/release\.yml@refs/tags/v[^/]+$` )
The trust root. These two constants are the whole of what is trusted; every other check in the install and upgrade paths is integrity, not provenance.
const ( IssuerEnv = "CLOOP_PROVENANCE_ISSUER" IdentityEnv = "CLOOP_PROVENANCE_IDENTITY" )
Environment overrides, for an enterprise that builds cloop from a fork and signs with its own workflow.
This exists so that such an operator has an alternative to switching verification off wholesale, which is what a hard-coded identity would force on them — an all-or-nothing pin is one an organisation eventually disables. Repointing the trust root keeps a signature required; only the identity that satisfies it changes.
const BundleSuffix = ".sigstore.json"
BundleSuffix is appended to an artifact's name to form its bundle's name.
One file per artifact, rather than the detached .sig/.pem pair cosign v1 and v2 produced: a Sigstore bundle carries the signature, the signing certificate and the Rekor inclusion proof together, so the transparency-log evidence travels with the signature instead of having to be re-fetched at verification time. cosign v3 removed --output-signature and --output-certificate from sign-blob, so this is also the only shape the current tool emits.
const CosignBinary = "cosign"
CosignBinary is the executable looked up on PATH.
const EdgeIdentityEnv = "CLOOP_PROVENANCE_EDGE_IDENTITY"
EdgeIdentityEnv overrides the edge identity, for an enterprise that builds cloop from a fork and publishes its own edge channel. It is deliberately a separate variable from IdentityEnv: repointing the release pin must not repoint this one, or the release identity would start vouching for edge builds, and the reverse.
const EdgeIdentityRegexp = `^https://github\.com/blechschmidt/cloop/\.github/workflows/edge\.yml@refs/heads/main$`
EdgeIdentityRegexp matches the SAN Fulcio issues to this repository's edge workflow running on main.
Anchored at both ends and with every dot escaped, for the reasons DefaultIdentityRegexp gives. Unlike that one it has no wildcard at all: the ref is the literal refs/heads/main, so edge.yml run from another branch, a tag or a pull request's merge ref does not match.
const EdgeWorkflowPath = ".github/workflows/edge.yml"
EdgeWorkflowPath is the workflow file the edge identity names, relative to the repository root.
const SkipNotice = "provenance verification is DISABLED (--insecure-skip-verify): " +
"the artifact's origin has not been checked"
SkipNotice is the single warning printed wherever verification is disabled.
It is one constant so that every path says the same thing, and so that grepping a support log for it finds an unverified install regardless of which entry point performed it.
const VerifyTimeout = 2 * time.Minute
VerifyTimeout bounds one verification. Keyless verification reads the bundle's own Rekor proof but may still consult the transparency log, so this is a network operation and needs a bound — an unreachable log must fail the upgrade rather than wedge it.
Variables ¶
var ( // ErrCosignMissing reports that the cosign binary is not installed. It is a // sentinel so callers can print an install hint instead of a generic // failure, and so tests can assert the fail-closed behaviour specifically. ErrCosignMissing = errors.New("provenance: cosign is not installed") // ErrBundleMissing reports that no signature bundle accompanies the // artifact. Distinct from a failed verification: it usually means the // artifact came from somewhere other than a cloop release. ErrBundleMissing = errors.New("provenance: no signature bundle for this artifact") // ErrUnverified reports that cosign rejected the signature — wrong // identity, wrong issuer, or bytes that do not match what was signed. ErrUnverified = errors.New("provenance: signature verification failed") )
Functions ¶
func BundleNameFor ¶
BundleNameFor returns the bundle's file name for an artifact name.
func EdgeIdentity ¶ added in v0.0.5
func EdgeIdentity() string
EdgeIdentity returns the edge identity regexp in force: EdgeIdentityEnv when set, else EdgeIdentityRegexp.
Types ¶
type Channel ¶ added in v0.0.5
type Channel string
Channel names a stream of signed builds a device can install from, and with it the signing identity a build must carry.
const ( // ChannelStable is published releases, signed by release.yml on a tag. // Every device follows it; it is what an absent setting means. ChannelStable Channel = "stable" // ChannelEdge is the hub's own builds: commits on main that passed CI, // signed by edge.yml on main. A device follows it only when an operator // on that device opted in. ChannelEdge Channel = "edge" )
func ParseChannel ¶ added in v0.0.5
ParseChannel reads a channel name. Empty means ChannelStable.
type Verifier ¶
type Verifier struct {
// Binary overrides the cosign executable. Tests set it; deployments do not.
Binary string
// Issuer and Identity override the pinned trust root. Empty means the
// DefaultIssuer/DefaultIdentityRegexp constants, after the environment
// overrides above are applied.
Issuer string
Identity string
}
Verifier checks Sigstore bundles with the cosign CLI.
The zero value is usable: it finds cosign on PATH and pins the trust root above.
func (*Verifier) Available ¶
Available reports whether cosign can be found. It does not run a verification.
Callers use this to fail *early* with an install hint — before downloading an archive that is about to be refused — not to decide whether to verify. Nothing in this package treats an unavailable cosign as a pass.
func (*Verifier) ForChannel ¶ added in v0.0.5
ForChannel returns a copy of v that requires the signing identity of channel c. The binary and issuer carry over; the identity does not.
For ChannelStable the copy is v as configured — the release identity, or whatever Identity/IdentityEnv repoint it to. For ChannelEdge the identity is EdgeIdentity() whatever v.Identity says: a verifier configured for releases must not lend its pin to an edge build, and that holds for an explicit Identity as much as for the environment.
func (*Verifier) TrustRoot ¶
TrustRoot describes what a verification will require, for display.
Operators need to be able to read the pin without running an upgrade, and a support reply of "check that your identity matches" is only actionable if there is a command that prints the identity in force.
func (*Verifier) VerifyBlob ¶
VerifyBlob checks that blobPath's bytes were signed, by the pinned identity, with the signature material in bundlePath.
Both paths must already exist on disk: cosign reads files, so a caller holding a download in memory must stage it — to a private temporary directory, never to the destination, so that nothing is placed where it could be executed before it has been proven.