Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 73 additions & 17 deletions container/verifier/attestations.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
containerdigest "github.com/opencontainers/go-digest"
"github.com/sigstore/sigstore-go/pkg/bundle"
Expand Down Expand Up @@ -60,33 +61,33 @@ func bundleFromAttestation(imageRef string, keychain authn.Keychain) ([]sigstore

// Loop through all available attestations and extract the bundle
for _, refDesc := range refManifest.Manifests {
if !strings.HasPrefix(refDesc.ArtifactType, "application/vnd.dev.sigstore.bundle") {
// Fast path: skip referrers that are clearly not sigstore bundles without
// fetching the manifest. Only do a deep inspection when the artifact type
// is ambiguous (empty or "application/vnd.oci.empty.v1+json"), which
// happens due to a go-containerregistry bug (google/go-containerregistry#1997)
// where the referrers fallback tag doesn't propagate the inner manifest's
// artifactType.
if !hasSigstoreBundlePrefix(refDesc.ArtifactType) &&
refDesc.ArtifactType != "application/vnd.oci.empty.v1+json" &&
refDesc.ArtifactType != "" {
continue
}

refImg, err := remote.Image(ref.Context().Digest(refDesc.Digest.String()), opts...)
if err != nil {
slog.Debug("error getting referrer image", "error", err)
continue
}
layers, err := refImg.Layers()
if err != nil {
slog.Debug("error getting referrer layers", "error", err)
continue
}
layer0, err := layers[0].Uncompressed()
if err != nil {
slog.Debug("error uncompressing referrer layer", "error", err)
continue
}
bundleBytes, err := io.ReadAll(layer0)
if err != nil {
slog.Debug("error reading referrer layer", "error", err)

// When the index descriptor's artifactType is ambiguous, inspect the
// actual manifest to determine whether this is a sigstore bundle.
if !hasSigstoreBundlePrefix(refDesc.ArtifactType) && !isSigstoreBundle(refImg) {
continue
}
b := &bundle.Bundle{}
err = b.UnmarshalJSON(bundleBytes)

b, err := extractBundleFromImage(refImg)
if err != nil {
slog.Debug("error unmarshalling bundle", "error", err)
slog.Debug("error extracting bundle from referrer", "error", err)
continue
}

Expand All @@ -101,3 +102,58 @@ func bundleFromAttestation(imageRef string, keychain authn.Keychain) ([]sigstore
}
return bundles, nil
}

// extractBundleFromImage reads and parses a sigstore bundle from the first layer of an OCI image.
func extractBundleFromImage(img v1.Image) (*bundle.Bundle, error) {
layers, err := img.Layers()
if err != nil {
return nil, fmt.Errorf("error getting referrer layers: %w", err)
}
if len(layers) == 0 {
return nil, fmt.Errorf("referrer has no layers")
}
layer0, err := layers[0].Uncompressed()
if err != nil {
return nil, fmt.Errorf("error uncompressing referrer layer: %w", err)
}
bundleBytes, err := io.ReadAll(layer0)
if err != nil {
return nil, fmt.Errorf("error reading referrer layer: %w", err)
}
b := &bundle.Bundle{}
if err = b.UnmarshalJSON(bundleBytes); err != nil {
return nil, fmt.Errorf("error unmarshalling bundle: %w", err)
}
return b, nil
}

// isSigstoreBundle inspects the actual manifest of a referrer image to
// determine whether it is a sigstore bundle. This is used as a fallback when
// the referrer index descriptor's artifactType is ambiguous (e.g. GHCR sets it
// to "application/vnd.oci.empty.v1+json" due to google/go-containerregistry#1997).
func isSigstoreBundle(img v1.Image) bool {
mf, err := img.Manifest()
if err != nil {
slog.Debug("error fetching manifest for sigstore bundle check", "error", err)
return false
}

// Check the config descriptor's artifactType (set by cosign v2+ when using OCI 1.1 referrers)
if hasSigstoreBundlePrefix(mf.Config.ArtifactType) {
return true
}

// Check layer media types as a final fallback
for _, layer := range mf.Layers {
if hasSigstoreBundlePrefix(string(layer.MediaType)) {
return true
}
}

return false
}

// hasSigstoreBundlePrefix checks if a media/artifact type string indicates a sigstore bundle.
func hasSigstoreBundlePrefix(s string) bool {
return strings.HasPrefix(s, "application/vnd.dev.sigstore.bundle")
}
252 changes: 252 additions & 0 deletions container/verifier/attestations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package verifier

import (
"fmt"
"testing"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/stretchr/testify/require"
)

// fakeImage implements v1.Image with a configurable manifest for testing
// isSigstoreBundle. Only Manifest() is used by the code under test; all other
// methods panic if called so that accidental usage is caught immediately.
type fakeImage struct {
manifest *v1.Manifest
err error
}

func (f *fakeImage) Manifest() (*v1.Manifest, error) { return f.manifest, f.err }

// Unused interface methods — panic to surface accidental calls.
func (*fakeImage) Layers() ([]v1.Layer, error) { panic("not implemented") }
func (*fakeImage) MediaType() (types.MediaType, error) { panic("not implemented") }
func (*fakeImage) Size() (int64, error) { panic("not implemented") }
func (*fakeImage) ConfigName() (v1.Hash, error) { panic("not implemented") }
func (*fakeImage) ConfigFile() (*v1.ConfigFile, error) { panic("not implemented") }
func (*fakeImage) RawConfigFile() ([]byte, error) { panic("not implemented") }
func (*fakeImage) Digest() (v1.Hash, error) { panic("not implemented") }
func (*fakeImage) RawManifest() ([]byte, error) { panic("not implemented") }
func (*fakeImage) LayerByDigest(v1.Hash) (v1.Layer, error) { panic("not implemented") }
func (*fakeImage) LayerByDiffID(v1.Hash) (v1.Layer, error) { panic("not implemented") }

func TestHasSigstoreBundlePrefix(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
want bool
}{
{
name: "exact v0.1 bundle type",
input: "application/vnd.dev.sigstore.bundle+json;version=0.1",
want: true,
},
{
name: "v0.3 bundle type",
input: "application/vnd.dev.sigstore.bundle.v0.3+json",
want: true,
},
{
name: "bare prefix without version",
input: "application/vnd.dev.sigstore.bundle",
want: true,
},
{
name: "OCI empty type (ambiguous, not a bundle)",
input: "application/vnd.oci.empty.v1+json",
want: false,
},
{
name: "cosign simplesigning type",
input: "application/vnd.dev.cosign.simplesigning.v1+json",
want: false,
},
{
name: "empty string",
input: "",
want: false,
},
{
name: "unrelated media type",
input: "application/json",
want: false,
},
{
name: "partial prefix match (missing dev)",
input: "application/vnd.sigstore.bundle",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := hasSigstoreBundlePrefix(tt.input)
require.Equal(t, tt.want, got)
})
}
}

func TestIsSigstoreBundle(t *testing.T) {
t.Parallel()

tests := []struct {
name string
img v1.Image
want bool
}{
{
name: "config artifactType is sigstore bundle v0.3",
img: &fakeImage{manifest: &v1.Manifest{
Config: v1.Descriptor{
ArtifactType: "application/vnd.dev.sigstore.bundle.v0.3+json",
},
}},
want: true,
},
{
name: "config artifactType is sigstore bundle v0.1",
img: &fakeImage{manifest: &v1.Manifest{
Config: v1.Descriptor{
ArtifactType: "application/vnd.dev.sigstore.bundle+json;version=0.1",
},
}},
want: true,
},
{
name: "layer media type is sigstore bundle",
img: &fakeImage{manifest: &v1.Manifest{
Config: v1.Descriptor{
ArtifactType: "application/vnd.oci.empty.v1+json",
},
Layers: []v1.Descriptor{
{MediaType: types.MediaType("application/vnd.dev.sigstore.bundle.v0.3+json")},
},
}},
want: true,
},
{
name: "neither config nor layers match",
img: &fakeImage{manifest: &v1.Manifest{
Config: v1.Descriptor{
ArtifactType: "application/vnd.oci.empty.v1+json",
},
Layers: []v1.Descriptor{
{MediaType: types.MediaType("application/vnd.oci.image.layer.v1.tar+gzip")},
},
}},
want: false,
},
{
name: "empty manifest (no config, no layers)",
img: &fakeImage{manifest: &v1.Manifest{}},
want: false,
},
{
name: "manifest fetch error returns false",
img: &fakeImage{err: fmt.Errorf("network error")},
want: false,
},
{
name: "multiple layers, second is sigstore bundle",
img: &fakeImage{manifest: &v1.Manifest{
Layers: []v1.Descriptor{
{MediaType: types.MediaType("application/octet-stream")},
{MediaType: types.MediaType("application/vnd.dev.sigstore.bundle.v0.3+json")},
},
}},
want: true,
},
{
name: "cosign simplesigning layer is not a sigstore bundle",
img: &fakeImage{manifest: &v1.Manifest{
Layers: []v1.Descriptor{
{MediaType: types.MediaType("application/vnd.dev.cosign.simplesigning.v1+json")},
},
}},
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := isSigstoreBundle(tt.img)
require.Equal(t, tt.want, got)
})
}
}

// TestBundleFromAttestation_FilterLogic validates the fast-path and
// deep-inspection filtering in bundleFromAttestation by documenting the
// expected skip/inspect behavior for different artifactType values in the
// referrers index. We cannot call bundleFromAttestation directly (it hits the
// network), so instead we verify the two helper predicates that drive the
// filtering logic, mirroring the conditions in the loop:
//
// skip = !hasSigstoreBundlePrefix(at) && at != "application/vnd.oci.empty.v1+json" && at != ""
// deepInspect = !hasSigstoreBundlePrefix(at) (only reached when not skipped)
func TestBundleFromAttestation_FilterPredicates(t *testing.T) {
t.Parallel()

tests := []struct {
name string
artType string
wantSkip bool // true = fast-path skip (no manifest fetch)
wantDeep bool // true = needs deep inspection via isSigstoreBundle
}{
{
name: "sigstore bundle v0.3 - accepted without deep inspect",
artType: "application/vnd.dev.sigstore.bundle.v0.3+json",
wantSkip: false,
wantDeep: false,
},
{
name: "OCI empty (go-containerregistry bug) - needs deep inspect",
artType: "application/vnd.oci.empty.v1+json",
wantSkip: false,
wantDeep: true,
},
{
name: "empty string - needs deep inspect",
artType: "",
wantSkip: false,
wantDeep: true,
},
{
name: "cosign simplesigning - fast-path skip",
artType: "application/vnd.dev.cosign.simplesigning.v1+json",
wantSkip: true,
wantDeep: false, // never reached
},
{
name: "arbitrary OCI type - fast-path skip",
artType: "application/vnd.oci.image.manifest.v1+json",
wantSkip: true,
wantDeep: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

// Replicate the skip condition from bundleFromAttestation
skip := !hasSigstoreBundlePrefix(tt.artType) &&
tt.artType != "application/vnd.oci.empty.v1+json" &&
tt.artType != ""
require.Equal(t, tt.wantSkip, skip, "skip predicate mismatch")

if !skip {
deepInspect := !hasSigstoreBundlePrefix(tt.artType)
require.Equal(t, tt.wantDeep, deepInspect, "deep-inspect predicate mismatch")
}
})
}
}
Loading
Loading