diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go index 66334471780..0a1e6120f3e 100644 --- a/cmd/cosign/cli/attest.go +++ b/cmd/cosign/cli/attest.go @@ -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, diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index b23d587fc33..8ecc781bc42 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -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 @@ -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 @@ -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 { @@ -220,7 +221,7 @@ 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...) @@ -228,6 +229,19 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { 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...) diff --git a/cmd/cosign/cli/options/attest.go b/cmd/cosign/cli/options/attest.go index 8139cddaefa..06bab74ce98 100644 --- a/cmd/cosign/cli/options/attest.go +++ b/cmd/cosign/cli/options/attest.go @@ -32,6 +32,7 @@ type AttestOptions struct { TSAServerURL string RekorEntryType string RecordCreationTimestamp bool + NewBundleFormat bool Rekor RekorOptions Fulcio FulcioOptions @@ -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") } diff --git a/pkg/oci/remote/write.go b/pkg/oci/remote/write.go index d0614768e89..efa8f5f9b71 100644 --- a/pkg/oci/remote/write.go +++ b/pkg/oci/remote/write.go @@ -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 @@ -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 +}