From a09dd83029b23bb9ce8cfd3951336dd8680c735d Mon Sep 17 00:00:00 2001 From: Samuele Verzi Date: Tue, 28 Jul 2026 12:08:36 +0200 Subject: [PATCH 1/2] Reconstruct bundles for key-signed cosign signatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundle reconstruction required the Fulcio certificate and tlog annotations on the simple-signing layer, so the classic key-signed cosign layout ("cosign sign --key" — signature annotation only) was reported as unsigned by RetrieveBundles. Key-signed layers now build public-key verification material, making them retrievable and verifiable with VerifyBundleWithKey, while certificate-bearing layers keep requiring tlog entries. A reconstructed key-signed bundle still fails keyless verification, so nothing certificate-less can sneak through the Fulcio path. Needed by ToolHive's skill signing flow (stacklok/toolhive#5899), which verifies key-signed skill artifacts at install time. Co-Authored-By: Claude Fable 5 --- container/verifier/keysigned_test.go | 130 +++++++++++++++++++++++++++ container/verifier/sigstore.go | 22 ++++- 2 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 container/verifier/keysigned_test.go diff --git a/container/verifier/keysigned_test.go b/container/verifier/keysigned_test.go new file mode 100644 index 0000000..87cec01 --- /dev/null +++ b/container/verifier/keysigned_test.go @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package verifier + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testCosignSimpleSigningMediaType = "application/vnd.dev.cosign.simplesigning.v1+json" + +// pushKeySignedArtifact pushes a random artifact plus a classic key-signed +// cosign signature manifest (payload layer + signature annotation at the +// sha256-.sig tag — what "cosign sign --key" produces) and returns the +// artifact ref and the signer's public key PEM. +func pushKeySignedArtifact(t *testing.T, host string) (ref string, pubPEM []byte) { + t.Helper() + + img, err := random.Image(256, 1) + require.NoError(t, err) + ref = host + "/test/artifact:v1" + parsedRef, err := name.ParseReference(ref) + require.NoError(t, err) + require.NoError(t, remote.Write(parsedRef, img)) + imgDigest, err := img.Digest() + require.NoError(t, err) + + // The simple-signing payload binds the artifact manifest digest. + payload := fmt.Sprintf( + `{"critical":{"identity":{"docker-reference":%q},"image":{"docker-manifest-digest":%q},`+ + `"type":"cosign container image signature"}}`, + strings.SplitN(ref, ":", 2)[0], imgDigest.String()) + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + payloadDigest := sha256.Sum256([]byte(payload)) + sig, err := priv.Sign(rand.Reader, payloadDigest[:], nil) + require.NoError(t, err) + pubPEM, err = cryptoutils.MarshalPublicKeyToPEM(priv.Public()) + require.NoError(t, err) + + layer := static.NewLayer([]byte(payload), types.MediaType(testCosignSimpleSigningMediaType)) + sigImg, err := mutate.Append(empty.Image, mutate.Addendum{ + Layer: layer, + Annotations: map[string]string{ + "dev.cosignproject.cosign/signature": base64.StdEncoding.EncodeToString(sig), + }, + MediaType: types.MediaType(testCosignSimpleSigningMediaType), + }) + require.NoError(t, err) + sigImg = mutate.MediaType(sigImg, types.OCIManifestSchema1) + + h, err := v1.NewHash(imgDigest.String()) + require.NoError(t, err) + sigTag := parsedRef.Context().Tag(fmt.Sprint(h.Algorithm, "-", h.Hex, ".sig")) + require.NoError(t, remote.Write(sigTag, sigImg)) + return ref, pubPEM +} + +// TestRetrieveKeySignedBundleRoundTrip proves the classic key-signed cosign +// layout — signature annotation, no certificate, no tlog entry — is +// retrievable and verifies against the signing key. +func TestRetrieveKeySignedBundleRoundTrip(t *testing.T) { + t.Parallel() + + reg := httptest.NewServer(registry.New()) + t.Cleanup(reg.Close) + host := strings.TrimPrefix(reg.URL, "http://") + + ref, pubPEM := pushKeySignedArtifact(t, host) + + bundles, err := RetrieveBundles(t.Context(), ref, nil) + require.NoError(t, err, "a key-signed signature manifest must be retrievable") + require.Len(t, bundles, 1) + assert.NotEmpty(t, bundles[0].Raw) + + _, err = VerifyBundleWithKey(bundles[0], pubPEM) + require.NoError(t, err, "the reconstructed bundle must verify against the signing key") + + // A different key must not verify it. + otherPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + otherPub, err := cryptoutils.MarshalPublicKeyToPEM(otherPriv.Public()) + require.NoError(t, err) + _, err = VerifyBundleWithKey(bundles[0], otherPub) + require.ErrorIs(t, err, ErrVerificationFailed) +} + +// TestKeySignedBundleFailsKeylessVerification ensures a reconstructed +// key-signed bundle is not silently trusted by the keyless (Fulcio) path. +func TestKeySignedBundleFailsKeylessVerification(t *testing.T) { + t.Parallel() + + reg := httptest.NewServer(registry.New()) + t.Cleanup(reg.Close) + host := strings.TrimPrefix(reg.URL, "http://") + + ref, _ := pushKeySignedArtifact(t, host) + bundles, err := RetrieveBundles(t.Context(), ref, nil) + require.NoError(t, err) + require.Len(t, bundles, 1) + + tm, err := OfflineTrustedMaterial() + require.NoError(t, err) + opts, err := DefaultVerifierOptions() + require.NoError(t, err) + _, err = VerifyBundle(bundles[0], tm, nil, opts...) + require.Error(t, err, "a certificate-less bundle must not pass keyless verification") +} diff --git a/container/verifier/sigstore.go b/container/verifier/sigstore.go index 2d13876..d3500d8 100644 --- a/container/verifier/sigstore.go +++ b/container/verifier/sigstore.go @@ -166,10 +166,22 @@ func getSimpleSigningLayersFromSignatureManifest( // getBundleVerificationMaterial returns the bundle verification material from the simple signing layer func getBundleVerificationMaterial(manifestLayer v1.Descriptor) ( *protobundle.VerificationMaterial, error) { - // 1. Get the signing certificate chain + // 1. Get the signing certificate chain. A layer without a certificate + // annotation is the classic key-signed cosign layout ("cosign sign + // --key"): the only verification material is the signature itself, and + // trust is established by the verifier supplying the public key. Build + // public-key verification material so such bundles are retrievable and + // verifiable with key-based trusted material. signingCert, err := getVerificationMaterialX509CertificateChain(manifestLayer) if err != nil { - return nil, fmt.Errorf("error getting signing certificate: %w", err) + if !hasCosignSignatureAnnotation(manifestLayer) { + return nil, fmt.Errorf("error getting signing certificate: %w", err) + } + return &protobundle.VerificationMaterial{ + Content: &protobundle.VerificationMaterial_PublicKey{ + PublicKey: &protocommon.PublicKeyIdentifier{}, + }, + }, nil } // 2. Get the transparency log entries @@ -185,6 +197,12 @@ func getBundleVerificationMaterial(manifestLayer v1.Descriptor) ( }, nil } +// hasCosignSignatureAnnotation reports whether the simple signing layer +// carries a cosign signature annotation. +func hasCosignSignatureAnnotation(layer v1.Descriptor) bool { + return layer.Annotations["dev.cosignproject.cosign/signature"] != "" +} + // getVerificationMaterialX509CertificateChain returns the verification material X509 certificate chain from the // simple signing layer func getVerificationMaterialX509CertificateChain(manifestLayer v1.Descriptor) ( From 71d805a7df02c1ceb779fb6493e77dc1639fd304 Mon Sep 17 00:00:00 2001 From: Samuele Verzi Date: Tue, 28 Jul 2026 13:08:34 +0200 Subject: [PATCH 2/2] Address review: layout classification, offline key verify Classify the signature layout by certificate annotation presence instead of the PEM decoder error, so a corrupt certificate on a keyless layer stays a retrieval failure rather than being silently reclassified as key-signed (the annotations are registry-supplied, attacker-controlled data). Guarded by a test. Complete the stored-bundle story for key-signed bundles: the Bundle docs promised offline re-verification but the only offline entry point hardcoded Fulcio material, so stored key-signed bundles could never re-verify. Add VerifyBundleOfflineWithKey, a HasCertificate discriminator, a "cosign-keypair" public-key hint on reconstructed bundles so stored bytes are self-describing, and scope the package docs to say which entry point serves which layout. Also from review: per-layer extraction failures now log the layer digest and error; the round-trip test asserts the bundle digest binds the simple-signing payload; the keyless-rejection test asserts the typed sentinel; test-local duplicates of the exported media type and a redundant hash round-trip are gone. Co-Authored-By: Claude Fable 5 --- container/verifier/bundles.go | 49 ++++++++++++++++-- container/verifier/doc.go | 5 ++ container/verifier/keysigned_test.go | 77 +++++++++++++++++++++++----- container/verifier/sigstore.go | 34 ++++++++---- 4 files changed, 137 insertions(+), 28 deletions(-) diff --git a/container/verifier/bundles.go b/container/verifier/bundles.go index ce61a43..ef2da82 100644 --- a/container/verifier/bundles.go +++ b/container/verifier/bundles.go @@ -26,8 +26,9 @@ import ( const DigestAlgorithmSHA256 = "sha256" // ErrNoBundles is returned by RetrieveBundles when the artifact carries no -// Sigstore signature or attestation in any supported layout — i.e. the -// artifact is unsigned as far as this package can tell. +// Sigstore signature or attestation in any supported layout — keyless +// (certificate-bearing), key-signed ("cosign sign --key"), or attestation — +// i.e. the artifact is unsigned as far as this package can tell. var ErrNoBundles = errors.New("no sigstore bundles found for artifact") // ErrVerificationFailed wraps every cryptographic verification failure @@ -38,7 +39,11 @@ var ErrVerificationFailed = errors.New("sigstore bundle verification failed") // Bundle is a Sigstore bundle retrieved for an artifact, in both parsed and // serialized form. Raw is the canonical JSON encoding, suitable for durable -// storage and later re-verification with VerifyBundleOffline. +// storage and later re-verification — with VerifyBundleOffline for keyless +// (certificate-bearing) bundles, or VerifyBundleOfflineWithKey for bundles +// reconstructed from the key-signed cosign layout (HasCertificate tells the +// two apart; key-signed bundles carry a "cosign-keypair" public-key hint +// instead of a certificate). type Bundle struct { // Parsed is the decoded bundle. Parsed *bundle.Bundle @@ -51,6 +56,18 @@ type Bundle struct { DigestHex string } +// HasCertificate reports whether the bundle carries a signing certificate — +// i.e. it came from a keyless (Fulcio) flow and verifies with VerifyBundle; +// a false result is the key-signed layout, verifying with +// VerifyBundleWithKey / VerifyBundleOfflineWithKey. +func (b Bundle) HasCertificate() bool { + if b.Parsed == nil { + return false + } + vm := b.Parsed.GetVerificationMaterial() + return vm.GetCertificate() != nil || vm.GetX509CertificateChain() != nil +} + // Identity is the signer identity extracted from a verified Sigstore bundle. type Identity struct { // SignerIdentity is the certificate's subject identity. For @@ -289,6 +306,32 @@ func VerifyBundleOffline( }, tm, expected, opts...) } +// VerifyBundleOfflineWithKey re-verifies a stored key-signed bundle (the +// Raw form of a bundle whose HasCertificate is false) against the artifact +// digest ("sha256:") and the given PEM public key. Key verification +// needs no trust root or network in the first place; this entry point only +// adds the parse step for stored bundles. +func VerifyBundleOfflineWithKey( + rawBundle []byte, + artifactDigest string, + pubKeyPEM []byte, +) (*verify.VerificationResult, error) { + digestAlgo, digestHex, ok := strings.Cut(artifactDigest, ":") + if !ok || digestAlgo == "" || digestHex == "" { + return nil, fmt.Errorf("artifact digest %q is not in : form", artifactDigest) + } + parsed := &bundle.Bundle{} + if err := parsed.UnmarshalJSON(rawBundle); err != nil { + return nil, fmt.Errorf("parsing stored bundle: %w", err) + } + return VerifyBundleWithKey(Bundle{ + Parsed: parsed, + Raw: rawBundle, + DigestAlgo: digestAlgo, + DigestHex: digestHex, + }, pubKeyPEM) +} + // identityPolicyOption translates an expected Identity into a Sigstore // certificate-identity policy. For identities recorded from GitHub Actions // certificates the SAN is the repository URI + workflow path (+ "@ref"), so diff --git a/container/verifier/doc.go b/container/verifier/doc.go index 32ee8f6..a14ab0e 100644 --- a/container/verifier/doc.go +++ b/container/verifier/doc.go @@ -29,6 +29,11 @@ // // store bundles[0].Raw and identity; later: // _, err = verifier.VerifyBundleOffline(storedRaw, "sha256:"+digestHex, &identity) // +// Key-signed bundles (Bundle.HasCertificate() == false, the "cosign sign +// --key" layout) carry no certificate identity; verify them with +// VerifyBundleWithKey at retrieval time and VerifyBundleOfflineWithKey for +// stored Raw bytes — both take the signer's PEM public key. +// // # Stability // // This package is Alpha stability. The API may change without notice. diff --git a/container/verifier/keysigned_test.go b/container/verifier/keysigned_test.go index 87cec01..2961c8e 100644 --- a/container/verifier/keysigned_test.go +++ b/container/verifier/keysigned_test.go @@ -9,6 +9,7 @@ import ( "crypto/rand" "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" "net/http/httptest" "strings" @@ -16,7 +17,6 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/random" @@ -28,13 +28,11 @@ import ( "github.com/stretchr/testify/require" ) -const testCosignSimpleSigningMediaType = "application/vnd.dev.cosign.simplesigning.v1+json" - // pushKeySignedArtifact pushes a random artifact plus a classic key-signed // cosign signature manifest (payload layer + signature annotation at the // sha256-.sig tag — what "cosign sign --key" produces) and returns the // artifact ref and the signer's public key PEM. -func pushKeySignedArtifact(t *testing.T, host string) (ref string, pubPEM []byte) { +func pushKeySignedArtifact(t *testing.T, host string) (ref string, pubPEM []byte, payload string) { t.Helper() img, err := random.Image(256, 1) @@ -47,7 +45,7 @@ func pushKeySignedArtifact(t *testing.T, host string) (ref string, pubPEM []byte require.NoError(t, err) // The simple-signing payload binds the artifact manifest digest. - payload := fmt.Sprintf( + payload = fmt.Sprintf( `{"critical":{"identity":{"docker-reference":%q},"image":{"docker-manifest-digest":%q},`+ `"type":"cosign container image signature"}}`, strings.SplitN(ref, ":", 2)[0], imgDigest.String()) @@ -60,22 +58,20 @@ func pushKeySignedArtifact(t *testing.T, host string) (ref string, pubPEM []byte pubPEM, err = cryptoutils.MarshalPublicKeyToPEM(priv.Public()) require.NoError(t, err) - layer := static.NewLayer([]byte(payload), types.MediaType(testCosignSimpleSigningMediaType)) + layer := static.NewLayer([]byte(payload), types.MediaType(MediaTypeCosignSimpleSigningV1JSON)) sigImg, err := mutate.Append(empty.Image, mutate.Addendum{ Layer: layer, Annotations: map[string]string{ "dev.cosignproject.cosign/signature": base64.StdEncoding.EncodeToString(sig), }, - MediaType: types.MediaType(testCosignSimpleSigningMediaType), + MediaType: types.MediaType(MediaTypeCosignSimpleSigningV1JSON), }) require.NoError(t, err) sigImg = mutate.MediaType(sigImg, types.OCIManifestSchema1) - h, err := v1.NewHash(imgDigest.String()) - require.NoError(t, err) - sigTag := parsedRef.Context().Tag(fmt.Sprint(h.Algorithm, "-", h.Hex, ".sig")) + sigTag := parsedRef.Context().Tag(fmt.Sprint(imgDigest.Algorithm, "-", imgDigest.Hex, ".sig")) require.NoError(t, remote.Write(sigTag, sigImg)) - return ref, pubPEM + return ref, pubPEM, payload } // TestRetrieveKeySignedBundleRoundTrip proves the classic key-signed cosign @@ -88,16 +84,27 @@ func TestRetrieveKeySignedBundleRoundTrip(t *testing.T) { t.Cleanup(reg.Close) host := strings.TrimPrefix(reg.URL, "http://") - ref, pubPEM := pushKeySignedArtifact(t, host) + ref, pubPEM, payload := pushKeySignedArtifact(t, host) bundles, err := RetrieveBundles(t.Context(), ref, nil) require.NoError(t, err, "a key-signed signature manifest must be retrievable") require.Len(t, bundles, 1) assert.NotEmpty(t, bundles[0].Raw) + assert.False(t, bundles[0].HasCertificate(), "the key-signed layout carries no certificate") + + // The bundle's digest must bind the simple-signing payload — that is + // what the signature covers, per the cosign convention. + payloadDigest := sha256.Sum256([]byte(payload)) + assert.Equal(t, hex.EncodeToString(payloadDigest[:]), bundles[0].DigestHex, + "the bundle digest must be the simple-signing payload digest") _, err = VerifyBundleWithKey(bundles[0], pubPEM) require.NoError(t, err, "the reconstructed bundle must verify against the signing key") + // The stored Raw form re-verifies offline with the key. + _, err = VerifyBundleOfflineWithKey(bundles[0].Raw, DigestAlgorithmSHA256+":"+bundles[0].DigestHex, pubPEM) + require.NoError(t, err, "the stored bundle must re-verify offline with the key") + // A different key must not verify it. otherPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) @@ -105,6 +112,8 @@ func TestRetrieveKeySignedBundleRoundTrip(t *testing.T) { require.NoError(t, err) _, err = VerifyBundleWithKey(bundles[0], otherPub) require.ErrorIs(t, err, ErrVerificationFailed) + _, err = VerifyBundleOfflineWithKey(bundles[0].Raw, DigestAlgorithmSHA256+":"+bundles[0].DigestHex, otherPub) + require.ErrorIs(t, err, ErrVerificationFailed) } // TestKeySignedBundleFailsKeylessVerification ensures a reconstructed @@ -116,7 +125,7 @@ func TestKeySignedBundleFailsKeylessVerification(t *testing.T) { t.Cleanup(reg.Close) host := strings.TrimPrefix(reg.URL, "http://") - ref, _ := pushKeySignedArtifact(t, host) + ref, _, _ := pushKeySignedArtifact(t, host) bundles, err := RetrieveBundles(t.Context(), ref, nil) require.NoError(t, err) require.Len(t, bundles, 1) @@ -126,5 +135,45 @@ func TestKeySignedBundleFailsKeylessVerification(t *testing.T) { opts, err := DefaultVerifierOptions() require.NoError(t, err) _, err = VerifyBundle(bundles[0], tm, nil, opts...) - require.Error(t, err, "a certificate-less bundle must not pass keyless verification") + assert.ErrorIs(t, err, ErrVerificationFailed, + "a certificate-less bundle must not pass keyless verification") +} + +// TestCorruptCertificateIsNotReclassifiedAsKeySigned guards the layout +// classification: a layer whose certificate annotation is present but +// malformed is a broken keyless signature and must stay a retrieval +// failure — not silently become a "key-signed" bundle. +func TestCorruptCertificateIsNotReclassifiedAsKeySigned(t *testing.T) { + t.Parallel() + + reg := httptest.NewServer(registry.New()) + t.Cleanup(reg.Close) + host := strings.TrimPrefix(reg.URL, "http://") + + img, err := random.Image(256, 1) + require.NoError(t, err) + ref := host + "/test/corrupt:v1" + parsedRef, err := name.ParseReference(ref) + require.NoError(t, err) + require.NoError(t, remote.Write(parsedRef, img)) + imgDigest, err := img.Digest() + require.NoError(t, err) + + layer := static.NewLayer([]byte(`{"critical":{}}`), types.MediaType(MediaTypeCosignSimpleSigningV1JSON)) + sigImg, err := mutate.Append(empty.Image, mutate.Addendum{ + Layer: layer, + Annotations: map[string]string{ + "dev.cosignproject.cosign/signature": base64.StdEncoding.EncodeToString([]byte("sig")), + "dev.sigstore.cosign/certificate": "not a pem certificate", + }, + MediaType: types.MediaType(MediaTypeCosignSimpleSigningV1JSON), + }) + require.NoError(t, err) + sigImg = mutate.MediaType(sigImg, types.OCIManifestSchema1) + sigTag := parsedRef.Context().Tag(fmt.Sprint(imgDigest.Algorithm, "-", imgDigest.Hex, ".sig")) + require.NoError(t, remote.Write(sigTag, sigImg)) + + _, err = RetrieveBundles(t.Context(), ref, nil) + require.ErrorIs(t, err, ErrNoBundles, + "a corrupt certificate must not be reclassified as a key-signed bundle") } diff --git a/container/verifier/sigstore.go b/container/verifier/sigstore.go index d3500d8..acd92db 100644 --- a/container/verifier/sigstore.go +++ b/container/verifier/sigstore.go @@ -53,14 +53,16 @@ func bundleFromSigstoreSignedImage(ctx context.Context, imageRef string, keychai // Build the verification material for the bundle verificationMaterial, err := getBundleVerificationMaterial(layer) if err != nil { - slog.Error("error getting bundle verification material") + slog.Error("error getting bundle verification material", + "layer_digest", layer.Digest.String(), "error", err) continue } // Build the message signature for the bundle msgSignature, err := getBundleMsgSignature(layer) if err != nil { - slog.Error("error getting bundle message signature") + slog.Error("error getting bundle message signature", + "layer_digest", layer.Digest.String(), "error", err) continue } @@ -166,23 +168,27 @@ func getSimpleSigningLayersFromSignatureManifest( // getBundleVerificationMaterial returns the bundle verification material from the simple signing layer func getBundleVerificationMaterial(manifestLayer v1.Descriptor) ( *protobundle.VerificationMaterial, error) { - // 1. Get the signing certificate chain. A layer without a certificate - // annotation is the classic key-signed cosign layout ("cosign sign + // 1. Classify the layout by certificate annotation PRESENCE. A layer + // without one is the classic key-signed cosign layout ("cosign sign // --key"): the only verification material is the signature itself, and - // trust is established by the verifier supplying the public key. Build - // public-key verification material so such bundles are retrievable and - // verifiable with key-based trusted material. - signingCert, err := getVerificationMaterialX509CertificateChain(manifestLayer) - if err != nil { + // trust is established by the verifier supplying the public key. A + // certificate annotation that is present but malformed must remain an + // error — the annotations are registry-supplied (attacker-controlled), + // and a corrupt keyless layer must not be reclassified as key-signed. + if manifestLayer.Annotations["dev.sigstore.cosign/certificate"] == "" { if !hasCosignSignatureAnnotation(manifestLayer) { - return nil, fmt.Errorf("error getting signing certificate: %w", err) + return nil, errors.New("layer carries neither certificate nor signature annotation") } return &protobundle.VerificationMaterial{ Content: &protobundle.VerificationMaterial_PublicKey{ - PublicKey: &protocommon.PublicKeyIdentifier{}, + PublicKey: &protocommon.PublicKeyIdentifier{Hint: keySignedPublicKeyHint}, }, }, nil } + signingCert, err := getVerificationMaterialX509CertificateChain(manifestLayer) + if err != nil { + return nil, fmt.Errorf("error getting signing certificate: %w", err) + } // 2. Get the transparency log entries tlogEntries, err := getVerificationMaterialTlogEntries(manifestLayer) @@ -197,6 +203,12 @@ func getBundleVerificationMaterial(manifestLayer v1.Descriptor) ( }, nil } +// keySignedPublicKeyHint marks a reconstructed bundle as originating from +// the key-signed cosign layout: the stored bundle carries no key material +// itself, so offline re-verification requires the caller to supply the +// public key (VerifyBundleOfflineWithKey). +const keySignedPublicKeyHint = "cosign-keypair" + // hasCosignSignatureAnnotation reports whether the simple signing layer // carries a cosign signature annotation. func hasCosignSignatureAnnotation(layer v1.Descriptor) bool {