Export Sigstore bundle retrieval, offline verification, and identity API - #192
Conversation
ToolHive's skills lock-file feature (RFC THV-0080, Stack 2) needs to verify skill artifacts on install and re-verify stored bundles on sync without duplicating this package's Sigstore internals. Promote the existing machinery to a public API: RetrieveBundles surfaces the bundles from both supported layouts (cosign .sig-tag manifests and attestations) with their canonical serialization for durable storage; VerifyBundle binds an expected signer identity into the verification policy itself rather than comparing after the fact, with nil meaning trust-on-first-use; VerifyBundleOffline re-verifies a stored bundle against an embedded public-good trusted root with no network access; VerifyBundleWithKey covers the cosign key-pair flow, whose bundles carry no certificate or transparency material; and Identity/IdentityFromResult export the signer-identity extraction including the GitHub Actions issuer normalization. The embedded trusted_root.json is a point-in-time snapshot of the public-good instance, documented as such — callers needing live freshness keep using New's TUF fetch. Fixes #176
|
Multi-axis panel review, verified against sigstore-go v1.2.2 source (module cache) and its docs. Posting findings; happy to discuss any. ✅ Verified correct against the sigstore-go APIThese were checked directly in
🔴 Ship-blockerPanic on attacker-controlled OCI annotations — now on a public trust boundary. 🟠 Spec mismatch (vs #176)The PR ships a superset API whose shape doesn't match the three requested signatures, so the toolhive-side thin wrapper can't be written against the spec:
🟡 Corrected finding (downgraded)
🟡 Confirmed footgun
🟡 Lifecycle gapFrozen 🔵 Lower-priority
Net: the security core is sound and correctly uses sigstore-go. Before merge I'd want the annotation panic fixed and the #176 signature alignment + scope question resolved; the rest is hardening/ergonomics. Nice work on the in-policy identity binding and the clean reuse of the existing internals. |
Harden and align the exported bundle API from review feedback: - Fix panics on attacker-controlled OCI annotations: every type assertion in getVerificationMaterialTlogEntries is now two-valued, returning an error for malformed registry-supplied bundle annotations instead of crashing the process. - Align with the #176 spec: RetrieveBundles takes a context (threaded through all registry fetches), IdentityFromResult returns an Identity value, and VerifyBundleOffline takes the combined "<algorithm>:<hex>" artifact digest. - Require explicit verifier options in VerifyBundle so public-good defaults are never silently applied to mismatched trusted material; DefaultVerifierOptions() exposes the public-good set and VerifyBundleOffline keeps applying it (instance is known there). - Wrap verification failures in a branchable ErrVerificationFailed sentinel and preserve the error chain when mapping to ErrNoBundles. - Serialize Bundle.Raw via the bundle's own MarshalJSON explicitly. - Cap the attestation referrer layer read at MaxAttestationsBytesLimit, matching the signature-manifest path. - Document the embedded trusted root's provenance and compromise-rotation staleness (tufroots/README.md, doc comment), add package doc.go with an Alpha stability marker, and list container/verifier in the CLAUDE.md package table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the thorough panel review — all actionable items addressed in 5127935. Point-by-point: 🔴 Ship-blocker — fixedEvery type assertion in 🟠 Spec mismatch — aligned where it helps, justified where it deviates
🟡 Footgun — resolved by requiring explicit opts
🟡 Lifecycle — documented; drift-check proposed as follow-upAdded 🔵 Lower-priority — all taken except one
|
Summary
ToolHive's skills lock-file feature (RFC THV-0080, Stack 2 — tracked in stacklok/toolhive#5899) needs to verify skill artifacts on install and re-verify stored bundles during
sync --check, without duplicating this package's Sigstore internals. This promotes the existing machinery incontainer/verifierto a public API:RetrieveBundles(imageRef, keychain) ([]Bundle, error)— surfaces the bundles from both layouts the package already handles internally (cosign.sig-tag signature manifests and attestations). EachBundlecarries its canonical JSON serialization (Raw) so consumers can store it durably and re-verify later. ReturnsErrNoBundlesfor unsigned artifacts.VerifyBundle(b, trustedMaterial, expected *Identity, opts...)— whenexpectedis non-nil, the signer identity is bound into the Sigstore verification policy itself (verify.WithCertificateIdentity) rather than compared post-hoc in Go;nilexpected is the trust-on-first-use case (chain-of-trust only, caller records the identity from the result). GitHub-Actions-derived identities match the SAN via a repo-anchored prefix.VerifyBundleOffline(raw, digestAlgo, digestHex, expected)— genuinely offline re-verification of a stored bundle against an embedded public-goodtrusted_root.json(newOfflineTrustedMaterial()): no TUF refresh, no network. The embedded root is a point-in-time snapshot, documented as such; live-freshness callers keep usingNew's TUF fetch.VerifyBundleWithKey(b, pubKeyPEM)— the cosign key-pair flow: key-signed bundles carry no certificate or transparency material, so this usesPublicKeyMaterial+WithNoObserverTimestamps+ aWithKeypolicy, kept as an explicit entry point rather than auto-detection.Identity/IdentityFromResult— exports the signer-identity extraction (including the GitHub Actions issuer normalization insignerIdentityFromCertificate).Existing exported surface (
New,VerifyServer,GetVerificationResults) is unchanged.github.com/sigstore/sigstoremoves from indirect to direct (cryptoutils/signature used by the key-material path).Fixes #176
Test plan
VerifyBundleWithKey(this is the contract ToolHive's signing E2E will rely on); wrong-digest and wrong-key rejectionsOfflineTrustedMaterialparses the embedded root and carries Fulcio CAs + Rekor logsVerifyBundleOfflineround-trips storedRawbundles (failure at verification, not parsing) and rejects malformed inputgo test ./container/...andtask lintgreen🤖 Generated with Claude Code