Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for new bundle specification in cosign attest #3888

Closed
Closed
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
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
47 changes: 34 additions & 13 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 @@ -174,20 +174,28 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
if sv.Cert != nil {
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
}
var timestampBytes []byte
var tsaPayload []byte
if c.KeyOpts.TSAServerURL != "" {
// TODO - change this when we implement protobuf / new bundle support
// We need to decide what signature to send to the timestamp authority.
//
// Historically, cosign sent the entire JSON DSSE Envelope to the
// timestamp authority. However, when sigstore clients are verifying a
// bundle they will use the DSSE Sig field, so we choose what signature
// to send to the timestamp authority based on our output format.
//
// See cmd/cosign/cli/attest/attest_blob.go
responseBytes, err := tsa.GetTimestampedSignature(signedPayload, tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL))
// Historically, cosign sent `signedPayload`, which is the entire JSON DSSE
// Envelope. However, when sigstore clients are verifying a bundle they
// will use the DSSE Sig field, so we choose what signature to send to
// the timestamp authority based on our output format.
if c.KeyOpts.NewBundleFormat {
tsaPayload, err = getEnvelopeSigBytes(signedPayload)
if err != nil {
return err
}
} else {
tsaPayload = signedPayload
}
timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL))
if err != nil {
return err
}
bundle := cbundle.TimestampToRFC3161Timestamp(responseBytes)
bundle := cbundle.TimestampToRFC3161Timestamp(timestampBytes)

opts = append(opts, static.WithRFC3161Timestamp(bundle))
}
Expand All @@ -208,8 +216,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 +229,26 @@ 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
}
bundleBytes, err := makeNewBundle(sv, rekorEntry, payload, signedPayload, signerBytes, timestampBytes)
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
25 changes: 7 additions & 18 deletions cmd/cosign/cli/attest/attest_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error

var rfc3161Timestamp *cbundle.RFC3161Timestamp
var timestampBytes []byte
var tsaPayload []byte
var rekorEntry *models.LogEntryAnon

if c.TSAServerURL != "" {
Expand All @@ -172,28 +173,16 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
// will use the DSSE Sig field, so we choose what signature to send to
// the timestamp authority based on our output format.
if c.NewBundleFormat {
var envelope dsse.Envelope
err = json.Unmarshal(sig, &envelope)
if err != nil {
return err
}
if len(envelope.Signatures) == 0 {
return fmt.Errorf("envelope has no signatures")
}
envelopeSigBytes, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig)
if err != nil {
return err
}

timestampBytes, err = tsa.GetTimestampedSignature(envelopeSigBytes, client.NewTSAClient(c.TSAServerURL))
tsaPayload, err = getEnvelopeSigBytes(sig)
if err != nil {
return err
}
} else {
timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClient(c.TSAServerURL))
if err != nil {
return err
}
tsaPayload = sig
}
timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, client.NewTSAClient(c.TSAServerURL))
if err != nil {
return err
}
rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes)
// TODO: Consider uploading RFC3161 TS to Rekor
Expand Down
16 changes: 16 additions & 0 deletions cmd/cosign/cli/attest/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
package attest

import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"

"github.com/secure-systems-lab/go-securesystemslib/dsse"
)

func predicateReader(predicatePath string) (io.ReadCloser, error) {
Expand All @@ -33,3 +37,15 @@ func predicateReader(predicatePath string) (io.ReadCloser, error) {
}
return f, nil
}

func getEnvelopeSigBytes(envelopeBytes []byte) ([]byte, error) {
var envelope dsse.Envelope
err := json.Unmarshal(envelopeBytes, &envelope)
if err != nil {
return nil, err
}
if len(envelope.Signatures) == 0 {
return nil, fmt.Errorf("envelope has no signatures")
}
return base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig)
}
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
}
Loading