Skip to content
Draft
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
14 changes: 14 additions & 0 deletions pkg/oci/remote/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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"
"github.com/sigstore/cosign/v3/pkg/cosign/env"
)
Expand All @@ -42,6 +43,7 @@ type options struct {
SBOMSuffix string
TagPrefix string
TargetRepository name.Repository
SubjectDescriptor *v1.Descriptor
ROpt []remote.Option
NameOpts []name.Option
OriginalOptions []Option
Expand Down Expand Up @@ -129,6 +131,18 @@ func WithTargetRepository(repo name.Repository) Option {
}
}

// WithSubjectDescriptor is a functional option for providing the subject
// descriptor to embed when writing an OCI referrer, instead of resolving it
// via a HEAD request against the subject reference. This permits writing
// referrers whose subject manifest is not present in the registry. The
// descriptor is used verbatim; the caller is responsible for providing a
// valid descriptor whose digest matches the subject reference.
func WithSubjectDescriptor(desc *v1.Descriptor) Option {
return func(o *options) {
o.SubjectDescriptor = desc
}
}

// TargetRepositoryFromOptions extracts the TargetRepository that a
// WithTargetRepository option would have set in the provided options.
// Returns the zero name.Repository if no override was provided.
Expand Down
46 changes: 34 additions & 12 deletions pkg/oci/remote/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,7 @@ func writeEmptyConfigLayer(o *options) (v1.Hash, int64, error) {
func WriteReferrer(d name.Digest, artifactType string, layers []v1.Layer, annotations map[string]string, opts ...Option) error {
o := makeOptions(d.Repository, opts...)

signTarget := d.String()
ref, err := name.ParseReference(signTarget, o.NameOpts...)
if err != nil {
return err
}
desc, err := remoteHead(ref, o.ROpt...)
subject, err := subjectDescriptor(d, o)
if err != nil {
return err
}
Expand Down Expand Up @@ -381,12 +376,8 @@ func WriteReferrer(d name.Digest, artifactType string, layers []v1.Layer, annota
Digest: configDigest,
Size: configSize,
},
Layers: layerDescriptors,
Subject: &v1.Descriptor{
MediaType: desc.MediaType,
Digest: desc.Digest,
Size: desc.Size,
},
Layers: layerDescriptors,
Subject: subject,
Annotations: annotations,
}, artifactType}

Expand All @@ -402,6 +393,37 @@ func WriteReferrer(d name.Digest, artifactType string, layers []v1.Layer, annota
return nil
}

// subjectDescriptor returns the descriptor to embed as the referrer's subject.
// A descriptor provided via WithSubjectDescriptor is used verbatim, allowing
// referrers whose subject manifest is not present in the registry; otherwise
// the subject is resolved with a HEAD request.
func subjectDescriptor(d name.Digest, o *options) (*v1.Descriptor, error) {
if subject := o.SubjectDescriptor; subject != nil {
dig, err := v1.NewHash(d.DigestStr())
if err != nil {
return nil, err
}
if subject.Digest != dig {
return nil, fmt.Errorf("subject descriptor digest %q does not match %q", subject.Digest, d.String())
}
return subject, nil
}

ref, err := name.ParseReference(d.String(), o.NameOpts...)
if err != nil {
return nil, err
}
desc, err := remoteHead(ref, o.ROpt...)
if err != nil {
return nil, err
}
return &v1.Descriptor{
MediaType: desc.MediaType,
Digest: desc.Digest,
Size: desc.Size,
}, nil
}

func WriteAttestationNewBundleFormat(d name.Digest, bundleBytes []byte, predicateType string, opts ...Option) error {
// generate bundle media type string
bundleMediaType, err := sgbundle.MediaTypeString("0.3")
Expand Down
93 changes: 93 additions & 0 deletions pkg/oci/remote/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package remote

import (
"fmt"
"reflect"
"strings"
"testing"

Expand Down Expand Up @@ -253,6 +254,98 @@ func TestWriteAttestationNewBundleFormat(t *testing.T) {
}
}

func TestWriteAttestationNewBundleFormatWithSubjectDescriptor(t *testing.T) {
// Save original functions
origHead := remoteHead
origWriteLayer := remoteWriteLayer
origPut := remotePut
t.Cleanup(func() {
remoteHead = origHead
remoteWriteLayer = origWriteLayer
remotePut = origPut
})

bundleBytes := []byte(`{"payload":"test","signatures":[]}`)
predicateType := "https://test.predicate.type"
digest := name.MustParseReference("gcr.io/test/image@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").(name.Digest)
subjectHash := v1.Hash{Algorithm: "sha256", Hex: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}

// The subject manifest does not exist in the registry; remoteHead must not
// be consulted when a subject descriptor is provided.
remoteHead = func(name.Reference, ...remote.Option) (*v1.Descriptor, error) {
t.Error("remoteHead should not be called when WithSubjectDescriptor is used")
return nil, fmt.Errorf("MANIFEST_UNKNOWN")
}

remoteWriteLayer = func(name.Repository, v1.Layer, ...remote.Option) error {
return nil
}

var capturedManifest remote.Taggable
remotePut = func(_ name.Reference, manifest remote.Taggable, _ ...remote.Option) error {
capturedManifest = manifest
return nil
}

t.Run("descriptor used verbatim", func(t *testing.T) {
capturedManifest = nil
subject := &v1.Descriptor{
MediaType: types.OCIImageIndex,
Digest: subjectHash,
Size: 123,
}

err := WriteAttestationNewBundleFormat(digest, bundleBytes, predicateType, WithSubjectDescriptor(subject))
if err != nil {
t.Fatalf("WriteAttestationNewBundleFormat() = %v", err)
}

refManifest, ok := capturedManifest.(referrerManifest)
if !ok {
t.Fatalf("Expected referrerManifest, got %T", capturedManifest)
}
if refManifest.Subject == nil {
t.Fatal("Expected subject to be set")
}
if !reflect.DeepEqual(refManifest.Subject, subject) {
t.Errorf("Subject = %+v, want %+v", *refManifest.Subject, *subject)
}
})

t.Run("zero digest rejected", func(t *testing.T) {
capturedManifest = nil
subject := &v1.Descriptor{
MediaType: types.OCIManifestSchema1,
Size: 456,
}

err := WriteAttestationNewBundleFormat(digest, bundleBytes, predicateType, WithSubjectDescriptor(subject))
if err == nil {
t.Fatal("Expected error for subject descriptor without digest")
}
if capturedManifest != nil {
t.Error("Expected no manifest upload for subject descriptor without digest")
}
})

t.Run("digest mismatch rejected", func(t *testing.T) {
capturedManifest = nil
subject := &v1.Descriptor{
MediaType: types.OCIManifestSchema1,
Digest: v1.Hash{Algorithm: "sha256", Hex: "feedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedface"},
Size: 789,
}

err := WriteAttestationNewBundleFormat(digest, bundleBytes, predicateType, WithSubjectDescriptor(subject))
if err == nil {
t.Fatal("Expected error for mismatched subject digest")
}
if capturedManifest != nil {
t.Error("Expected no manifest upload on digest mismatch")
}
})
}

func TestWriteAttestationsReferrer(t *testing.T) {
// Save original functions
origHead := remoteHead
Expand Down
Loading