Skip to content

Commit

Permalink
Add support for new bundle specification in cosign attest
Browse files Browse the repository at this point in the history
Signed-off-by: Cody Soyland <[email protected]>
  • Loading branch information
codysoyland committed Sep 25, 2024
1 parent 677a262 commit a8e4b5a
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 4 deletions.
1 change: 1 addition & 0 deletions cmd/cosign/cli/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func Attest() *cobra.Command {
OIDCProvider: o.OIDC.Provider,
SkipConfirmation: o.SkipConfirmation,
TSAServerURL: o.TSAServerURL,
NewBundleFormat: o.NewBundleFormat,
}
attestCommand := attest.AttestCommand{
KeyOpts: ko,
Expand Down
22 changes: 18 additions & 4 deletions cmd/cosign/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import (

type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)

func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*models.LogEntryAnon, error) {
rekorBytes, err := sv.Bytes(ctx)
if err != nil {
return nil, err
Expand All @@ -64,7 +64,7 @@ func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string,
return nil, err
}
fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex)
return cbundle.EntryToBundle(entry), nil
return entry, nil
}

// nolint
Expand Down Expand Up @@ -208,8 +208,9 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
if err != nil {
return fmt.Errorf("should upload to tlog: %w", err)
}
var rekorEntry *models.LogEntryAnon
if shouldUpload {
bundle, err := uploadToTlog(ctx, sv, c.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
rekorEntry, err = uploadToTlog(ctx, sv, c.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
if c.RekorEntryType == "intoto" {
return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b)
} else {
Expand All @@ -220,14 +221,27 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
if err != nil {
return err
}
opts = append(opts, static.WithBundle(bundle))
opts = append(opts, static.WithBundle(cbundle.EntryToBundle(rekorEntry)))
}

sig, err := static.NewAttestation(signedPayload, opts...)
if err != nil {
return err
}

if c.KeyOpts.NewBundleFormat {
signerBytes, err := sv.Bytes(ctx)
if err != nil {
return err
}
// TODO: Add TSA timestamp
bundleBytes, err := makeNewBundle(sv, rekorEntry, payload, signedPayload, signerBytes, nil)
if err != nil {
return err
}
return ociremote.WriteAttestationNewBundleFormat(digest.Repository, bundleBytes, ociremoteOpts...)
}

// We don't actually need to access the remote entity to attach things to it
// so we use a placeholder here.
se := ociremote.SignedUnknown(digest, ociremoteOpts...)
Expand Down
3 changes: 3 additions & 0 deletions cmd/cosign/cli/options/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type AttestOptions struct {
TSAServerURL string
RekorEntryType string
RecordCreationTimestamp bool
NewBundleFormat bool

Rekor RekorOptions
Fulcio FulcioOptions
Expand Down Expand Up @@ -90,4 +91,6 @@ func (o *AttestOptions) AddFlags(cmd *cobra.Command) {

cmd.Flags().BoolVar(&o.RecordCreationTimestamp, "record-creation-timestamp", false,
"set the createdAt timestamp in the attestation artifact to the time it was created; by default, cosign sets this to the zero value")

cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false, "attach a Sigstore bundle using OCI referrers API")
}
123 changes: 123 additions & 0 deletions pkg/oci/remote/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
ociexperimental "github.com/sigstore/cosign/v2/internal/pkg/oci/remote"
"github.com/sigstore/cosign/v2/pkg/oci"
ctypes "github.com/sigstore/cosign/v2/pkg/types"
sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
)

// WriteSignedImageIndexImages writes the images within the image index
Expand Down Expand Up @@ -221,3 +222,125 @@ func (taggable taggableManifest) RawManifest() ([]byte, error) {
func (taggable taggableManifest) MediaType() (types.MediaType, error) {
return taggable.mediaType, nil
}

func WriteAttestationNewBundleFormat(d name.Repository, bundleBytes []byte, opts ...Option) error {
o := makeOptions(d, opts...)

signTarget := d.String()
ref, err := name.ParseReference(signTarget, o.NameOpts...)
if err != nil {
return err
}
desc, err := remote.Head(ref, o.ROpt...)
if err != nil {
return err
}

// Write the empty config layer
configLayer := static.NewLayer([]byte("{}"), "application/vnd.oci.image.config.v1+json")
configDigest, err := configLayer.Digest()
if err != nil {
return fmt.Errorf("failed to calculate digest: %w", err)
}
configSize, err := configLayer.Size()
if err != nil {
return fmt.Errorf("failed to calculate size: %w", err)
}
err = remote.WriteLayer(d, configLayer, o.ROpt...)
if err != nil {
return fmt.Errorf("failed to upload layer: %w", err)
}

// generate bundle media type string
bundleMediaType, err := sgbundle.MediaTypeString("0.3")
if err != nil {
return fmt.Errorf("failed to generate bundle media type string: %w", err)
}

// Write the bundle layer
layer := static.NewLayer(bundleBytes, types.MediaType(bundleMediaType))
blobDigest, err := layer.Digest()
if err != nil {
return fmt.Errorf("failed to calculate digest: %w", err)
}

blobSize, err := layer.Size()
if err != nil {
return fmt.Errorf("failed to calculate size: %w", err)
}

err = remote.WriteLayer(d, layer, o.ROpt...)
if err != nil {
return fmt.Errorf("failed to upload layer: %w", err)
}

// Create a manifest that includes the blob as a layer
manifest := referrerManifest{v1.Manifest{
SchemaVersion: 2,
MediaType: types.OCIManifestSchema1,
Config: v1.Descriptor{
MediaType: types.MediaType("application/vnd.oci.empty.v1+json"),
ArtifactType: bundleMediaType,
Digest: configDigest,
Size: configSize,
},
Layers: []v1.Descriptor{
{
MediaType: types.MediaType(bundleMediaType),
Digest: blobDigest,
Size: blobSize,
},
},
Subject: &v1.Descriptor{
MediaType: types.OCIManifestSchema1,
Digest: desc.Digest,
Size: desc.Size,
},
// TODO: Add annotations org.opencontainers.image.created, dev.sigstore.bundle.content, and dev.sigstore.bundle.predicateType
// See https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md
}, bundleMediaType}

targetRef, err := manifest.targetRef(d)
if err != nil {
return fmt.Errorf("failed to create target reference: %w", err)
}

if err := remote.Put(targetRef, manifest, o.ROpt...); err != nil {
return fmt.Errorf("failed to upload manifest: %w", err)
}

// TODO: add support for tag fallback scheme for non-compliant registries

return nil
}

// referrerManifest implements Taggable for use in remote.Put.
// This type also augments the built-in v1.Manifest with an ArtifactType field
// which is part of the OCI 1.1 Image Manifest spec but is unsupported by
// go-containerregistry at this time.
// See https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#image-manifest-property-descriptions
// and https://github.com/google/go-containerregistry/pull/1931
type referrerManifest struct {
v1.Manifest
ArtifactType string `json:"artifactType,omitempty"`
}

func (r referrerManifest) RawManifest() ([]byte, error) {
return json.Marshal(r)
}

func (r referrerManifest) targetRef(repo name.Repository) (name.Reference, error) {
manifestBytes, err := r.RawManifest()
if err != nil {
return nil, err
}
digest, _, err := v1.SHA256(bytes.NewReader(manifestBytes))
if err != nil {
return nil, err
}
return name.ParseReference(fmt.Sprintf("%s/%s@%s", repo.RegistryStr(), repo.RepositoryStr(), digest.String()))
}

func (r referrerManifest) MediaType() (types.MediaType, error) {
return types.OCIManifestSchema1, nil
}

0 comments on commit a8e4b5a

Please sign in to comment.