diff --git a/cmd/cosign/cli/attach/attach.go b/cmd/cosign/cli/attach/attach.go index 971d4bed45c..15440379cb9 100644 --- a/cmd/cosign/cli/attach/attach.go +++ b/cmd/cosign/cli/attach/attach.go @@ -28,6 +28,7 @@ import ( ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/cosign/v3/pkg/types" + "github.com/sigstore/sigstore-go/pkg/bundle" ) func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, signedPayloads []string, imageRef string) error { @@ -37,7 +38,28 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, signed } for _, payload := range signedPayloads { - if err := attachAttestation(ctx, ociremoteOpts, payload, imageRef, regOpts.NameOptions()); err != nil { + fmt.Fprintf(os.Stderr, "Using payload from: %s", payload) + + ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...) + if err != nil { + return err + } + if _, ok := ref.(name.Digest); !ok { + ui.Warnf(ctx, ui.TagReferenceMessage, imageRef) + } + + digest, err := ociremote.ResolveDigest(ref, ociremoteOpts...) + if err != nil { + return err + } + + // Detect if we are using new bundle format + b, err := bundle.LoadJSONFromPath(payload) + if err == nil { + return attachAttestationNewBundle(ociremoteOpts, b, digest) + } + + if err := attachAttestation(ociremoteOpts, payload, digest); err != nil { return fmt.Errorf("attaching payload from %s: %w", payload, err) } } @@ -45,8 +67,29 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, signed return nil } -func attachAttestation(ctx context.Context, remoteOpts []ociremote.Option, signedPayload, imageRef string, nameOpts []name.Option) error { - fmt.Fprintf(os.Stderr, "Using payload from: %s", signedPayload) +func attachAttestationNewBundle(remoteOpts []ociremote.Option, b *bundle.Bundle, digest name.Digest) error { + envelope, err := b.Envelope() + if err != nil { + return err + } + if envelope == nil { + return fmt.Errorf("bundle does not have DSSE envelope") + } + statement, err := envelope.Statement() + if err != nil { + return err + } + if statement == nil { + return fmt.Errorf("unable to understand bundle envelope statement") + } + bundleBytes, err := b.MarshalJSON() + if err != nil { + return err + } + return ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, statement.PredicateType, remoteOpts...) +} + +func attachAttestation(remoteOpts []ociremote.Option, signedPayload string, digest name.Digest) error { attestationFile, err := os.Open(signedPayload) if err != nil { return err @@ -73,22 +116,6 @@ func attachAttestation(ctx context.Context, remoteOpts []ociremote.Option, signe return fmt.Errorf("could not attach attestation without having signatures") } - ref, err := name.ParseReference(imageRef, nameOpts...) - if err != nil { - return err - } - if _, ok := ref.(name.Digest); !ok { - ui.Warnf(ctx, ui.TagReferenceMessage, imageRef) - } - digest, err := ociremote.ResolveDigest(ref, remoteOpts...) - if err != nil { - return err - } - // Overwrite "ref" with a digest to avoid a race where we use a tag - // multiple times, and it potentially points to different things at - // each access. - ref = digest // nolint - opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} att, err := static.NewAttestation(payload, opts...) if err != nil { diff --git a/cmd/cosign/cli/attach/sig.go b/cmd/cosign/cli/attach/sig.go index 41418b01e63..c09fe03df3e 100644 --- a/cmd/cosign/cli/attach/sig.go +++ b/cmd/cosign/cli/attach/sig.go @@ -30,16 +30,10 @@ import ( "github.com/sigstore/cosign/v3/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/static" + sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" ) func SignatureCmd(ctx context.Context, regOpts options.RegistryOptions, sigRef, payloadRef, certRef, certChainRef, timeStampedSigRef, rekorBundleRef, imageRef string) error { - b64SigBytes, err := signatureBytes(sigRef) - if err != nil { - return err - } else if len(b64SigBytes) == 0 { - return errors.New("empty signature") - } - ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...) if err != nil { return err @@ -52,10 +46,12 @@ func SignatureCmd(ctx context.Context, regOpts options.RegistryOptions, sigRef, if err != nil { return err } - // Overwrite "ref" with a digest to avoid a race where we use a tag - // multiple times, and it potentially points to different things at - // each access. - ref = digest // nolint + + // Detect if we are using new bundle format + b, err := sgbundle.LoadJSONFromPath(payloadRef) + if err == nil { + return attachAttestationNewBundle(ociremoteOpts, b, digest) + } var payload []byte if payloadRef == "" { @@ -67,6 +63,13 @@ func SignatureCmd(ctx context.Context, regOpts options.RegistryOptions, sigRef, return err } + b64SigBytes, err := signatureBytes(sigRef) + if err != nil { + return err + } else if len(b64SigBytes) == 0 { + return errors.New("empty signature") + } + sig, err := static.NewSignature(payload, string(b64SigBytes)) if err != nil { return err diff --git a/cmd/cosign/cli/download.go b/cmd/cosign/cli/download.go index 687fedb7084..a38f2e966fc 100644 --- a/cmd/cosign/cli/download.go +++ b/cmd/cosign/cli/download.go @@ -49,7 +49,7 @@ func downloadSignature() *cobra.Command { Args: cobra.ExactArgs(1), PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, args []string) error { - return download.SignatureCmd(cmd.Context(), *o, args[0]) + return download.SignatureCmd(cmd.Context(), *o, args[0], cmd.OutOrStdout()) }, } @@ -94,7 +94,7 @@ func downloadAttestation() *cobra.Command { Args: cobra.ExactArgs(1), PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, args []string) error { - return download.AttestationCmd(cmd.Context(), *o, *ao, args[0]) + return download.AttestationCmd(cmd.Context(), *o, *ao, args[0], cmd.OutOrStdout()) }, } diff --git a/cmd/cosign/cli/download/attestation.go b/cmd/cosign/cli/download/attestation.go index 814021eacff..cbbb965a7c4 100644 --- a/cmd/cosign/cli/download/attestation.go +++ b/cmd/cosign/cli/download/attestation.go @@ -19,7 +19,7 @@ import ( "context" "encoding/json" "errors" - "fmt" + "io" "github.com/google/go-containerregistry/pkg/name" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" @@ -28,7 +28,7 @@ import ( ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" ) -func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, attOptions options.AttestationDownloadOptions, imageRef string) error { +func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, attOptions options.AttestationDownloadOptions, imageRef string, out io.Writer) error { ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...) if err != nil { return err @@ -46,6 +46,35 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, attOpt } } + // Try bundles first + newBundles, _, err := cosign.GetBundles(ctx, ref, ociremoteOpts) + if err == nil && len(newBundles) > 0 { + for _, eachBundle := range newBundles { + if predicateType != "" { + envelope, err := eachBundle.Envelope() + if err != nil || envelope == nil { + continue + } + statement, err := envelope.Statement() + if err != nil || statement == nil { + continue + } + if statement.PredicateType != predicateType { + continue + } + } + b, err := json.Marshal(eachBundle) + if err != nil { + return err + } + _, err = out.Write(append(b, byte('\n'))) + if err != nil { + return err + } + } + return nil + } + se, err := ociremote.SignedEntity(ref, ociremoteOpts...) var entityNotFoundError *ociremote.EntityNotFoundError if err != nil { @@ -76,7 +105,10 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, attOpt if err != nil { return err } - fmt.Println(string(b)) + _, err = out.Write(append(b, byte('\n'))) + if err != nil { + return err + } } return nil } diff --git a/cmd/cosign/cli/download/signature.go b/cmd/cosign/cli/download/signature.go index 67fb9b87e77..deb3d67c3be 100644 --- a/cmd/cosign/cli/download/signature.go +++ b/cmd/cosign/cli/download/signature.go @@ -18,14 +18,14 @@ package download import ( "context" "encoding/json" - "fmt" + "io" "github.com/google/go-containerregistry/pkg/name" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/pkg/cosign" ) -func SignatureCmd(ctx context.Context, regOpts options.RegistryOptions, imageRef string) error { +func SignatureCmd(ctx context.Context, regOpts options.RegistryOptions, imageRef string, out io.Writer) error { ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...) if err != nil { return err @@ -34,6 +34,23 @@ func SignatureCmd(ctx context.Context, regOpts options.RegistryOptions, imageRef if err != nil { return err } + + // Try bundles first + newBundles, _, err := cosign.GetBundles(ctx, ref, ociremoteOpts) + if err == nil && len(newBundles) > 0 { + for _, eachBundle := range newBundles { + b, err := json.Marshal(eachBundle) + if err != nil { + return err + } + _, err = out.Write(append(b, byte('\n'))) + if err != nil { + return err + } + } + return nil + } + signatures, err := cosign.FetchSignaturesForReference(ctx, ref, ociremoteOpts...) if err != nil { return err @@ -43,7 +60,10 @@ func SignatureCmd(ctx context.Context, regOpts options.RegistryOptions, imageRef if err != nil { return err } - fmt.Println(string(b)) + _, err = out.Write(append(b, byte('\n'))) + if err != nil { + return err + } } return nil } diff --git a/cmd/cosign/cli/options/attach.go b/cmd/cosign/cli/options/attach.go index e0aac3fb85b..ee55697fc88 100644 --- a/cmd/cosign/cli/options/attach.go +++ b/cmd/cosign/cli/options/attach.go @@ -47,6 +47,9 @@ func (o *AttachSignatureOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.Payload, "payload", "", "path to the payload covered by the signature") + cmd.Flags().StringVar(&o.Payload, "bundle", "", + "path to bundle containing signature (alias for payload)") + cmd.Flags().StringVar(&o.Cert, "certificate", "", "path to the X.509 certificate in PEM format to include in the OCI Signature") diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 9f8427863aa..ae6b54e647c 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -137,7 +137,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if !c.LocalImage { ref, err := name.ParseReference(images[0], c.NameOptions...) if err == nil && c.NewBundleFormat { - newBundles, _, err := cosign.GetBundles(ctx, ref, co, c.NameOptions...) + newBundles, _, err := cosign.GetBundles(ctx, ref, co.RegistryClientOpts, c.NameOptions...) if len(newBundles) == 0 || err != nil { co.NewBundleFormat = false } diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 781626f202b..edbfac10c66 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -122,7 +122,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e if !c.LocalImage { ref, err := name.ParseReference(images[0], c.NameOptions...) if err == nil && c.NewBundleFormat { - newBundles, _, err := cosign.GetBundles(ctx, ref, co, c.NameOptions...) + newBundles, _, err := cosign.GetBundles(ctx, ref, co.RegistryClientOpts, c.NameOptions...) if len(newBundles) == 0 || err != nil { co.NewBundleFormat = false } diff --git a/doc/cosign_attach_signature.md b/doc/cosign_attach_signature.md index a0355876e0e..e9b00729deb 100644 --- a/doc/cosign_attach_signature.md +++ b/doc/cosign_attach_signature.md @@ -35,6 +35,7 @@ cosign attach signature [flags] --allow-http-registry whether to allow using HTTP protocol while connecting to registries. Don't use this for anything but testing --allow-insecure-registry whether to allow insecure connections to registries (e.g., with expired or self-signed TLS certificates). Don't use this for anything but testing --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] + --bundle string path to bundle containing signature (alias for payload) --certificate string path to the X.509 certificate in PEM format to include in the OCI Signature --certificate-chain string path to a list of CA X.509 certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate. Included in the OCI Signature -h, --help help for signature diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index f4755934d53..db9a864d564 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -1621,10 +1621,10 @@ func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name return verifySignatures(ctx, sigs, h, co) } -func GetBundles(_ context.Context, signedImgRef name.Reference, co *CheckOpts, nameOpts ...name.Option) ([]*sgbundle.Bundle, *v1.Hash, error) { +func GetBundles(_ context.Context, signedImgRef name.Reference, registryClientOpts []ociremote.Option, nameOpts ...name.Option) ([]*sgbundle.Bundle, *v1.Hash, error) { // This is a carefully optimized sequence for fetching the signatures of the // entity that minimizes registry requests when supplied with a digest input - digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...) + digest, err := ociremote.ResolveDigest(signedImgRef, registryClientOpts...) if err != nil { if terr := (&transport.Error{}); errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound { return nil, nil, &ErrImageTagNotFound{ @@ -1638,7 +1638,7 @@ func GetBundles(_ context.Context, signedImgRef name.Reference, co *CheckOpts, n return nil, nil, err } - index, err := ociremote.Referrers(digest, "", co.RegistryClientOpts...) + index, err := ociremote.Referrers(digest, "", registryClientOpts...) if err != nil { return nil, nil, err } @@ -1648,7 +1648,7 @@ func GetBundles(_ context.Context, signedImgRef name.Reference, co *CheckOpts, n if err != nil { return nil, nil, err } - bundle, err := ociremote.Bundle(st, co.RegistryClientOpts...) + bundle, err := ociremote.Bundle(st, registryClientOpts...) if err != nil { // There may be non-Sigstore referrers in the index, so we can ignore them. // TODO: Should we surface any errors here (e.g. if the bundle is invalid)? @@ -1668,7 +1668,7 @@ func GetBundles(_ context.Context, signedImgRef name.Reference, co *CheckOpts, n // verifyImageAttestationsSigstoreBundle verifies attestations from attached sigstore bundles func verifyImageAttestationsSigstoreBundle(ctx context.Context, signedImgRef name.Reference, co *CheckOpts, nameOpts ...name.Option) (checkedAttestations []oci.Signature, atLeastOneBundleVerified bool, err error) { - bundles, hash, err := GetBundles(ctx, signedImgRef, co, nameOpts...) + bundles, hash, err := GetBundles(ctx, signedImgRef, co.RegistryClientOpts, nameOpts...) if err != nil { return nil, false, err } diff --git a/pkg/cosign/verify_oci_test.go b/pkg/cosign/verify_oci_test.go index f9d44b7c379..3ad8690b998 100644 --- a/pkg/cosign/verify_oci_test.go +++ b/pkg/cosign/verify_oci_test.go @@ -53,7 +53,7 @@ func TestGetBundles_Empty(t *testing.T) { assert.NoError(t, err) // If tag doesn't exist, should return ErrImageTagNotFound - bundles, hash, err := GetBundles(context.Background(), ref, &CheckOpts{}) + bundles, hash, err := GetBundles(context.Background(), ref, []ociremote.Option{}) imgTagNotFound := &ErrImageTagNotFound{} assert.ErrorAs(t, err, &imgTagNotFound) assert.Len(t, bundles, 0) @@ -65,7 +65,7 @@ func TestGetBundles_Empty(t *testing.T) { assert.NoError(t, remote.Write(ref, img)) // Check that no matching attestation error is returned - bundles, hash, err = GetBundles(context.Background(), ref, &CheckOpts{}) + bundles, hash, err = GetBundles(context.Background(), ref, []ociremote.Option{}) var noMatchErr *ErrNoMatchingAttestations assert.ErrorAs(t, err, &noMatchErr) assert.Len(t, bundles, 0) @@ -81,7 +81,7 @@ func TestGetBundles_Empty(t *testing.T) { assert.NoError(t, err) // Should still return no matching attestation error, as it failed to parse the bundle - bundles, hash, err = GetBundles(context.Background(), ref, &CheckOpts{}) + bundles, hash, err = GetBundles(context.Background(), ref, []ociremote.Option{}) assert.ErrorAs(t, err, &noMatchErr) assert.Len(t, bundles, 0) assert.Nil(t, hash) @@ -111,7 +111,7 @@ func TestGetBundles_Valid(t *testing.T) { assert.NoError(t, err) // Retrieve the attestation - bundles, hash, err := GetBundles(context.Background(), ref, &CheckOpts{}) + bundles, hash, err := GetBundles(context.Background(), ref, []ociremote.Option{}) assert.NoError(t, err) assert.Len(t, bundles, 1) assert.NotNil(t, hash) diff --git a/test/e2e_test.go b/test/e2e_test.go index 95714bc34d7..05d2043db68 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -109,7 +109,7 @@ func TestSignVerify(t *testing.T) { // Verify should fail at first mustErr(verify(pubKeyPath, imgName, true, nil, "", false), t) // So should download - mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now sign the image ko := options.KeyOpts{ @@ -126,7 +126,7 @@ func TestSignVerify(t *testing.T) { // Now verify and download should work! must(verify(pubKeyPath, imgName, true, nil, "", false), t) - must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Ensure it verifies if you default to the new protobuf bundle format cmd := cliverify.VerifyCommand{ @@ -175,7 +175,7 @@ func TestSignVerifyCertBundle(t *testing.T) { // Verify should fail at first mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, nil, "", true), t) // So should download - mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now sign the image ko := options.KeyOpts{ @@ -195,7 +195,7 @@ func TestSignVerifyCertBundle(t *testing.T) { must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, nil, "", ignoreTlog), t) // verification with certificate chain instead of root/intermediate files should work as well must(verifyCertChain(pubKeyPath, certChainFile, certFile, imgName, true, nil, "", ignoreTlog), t) - must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Look for a specific annotation mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) @@ -246,7 +246,7 @@ func TestSignVerifyClean(t *testing.T) { // Now verify and download should work! must(verify(pubKeyPath, imgName, true, nil, "", false), t) - must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now clean signature from the given image must(cli.CleanCmd(ctx, options.RegistryOptions{}, "all", imgName, true), t) @@ -288,7 +288,7 @@ func TestImportSignVerifyClean(t *testing.T) { // Now verify and download should work! must(verify(pubKeyPath, imgName, true, nil, "", false), t) - must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now clean signature from the given image must(cli.CleanCmd(ctx, options.RegistryOptions{}, "all", imgName, true), t) @@ -1507,7 +1507,7 @@ func TestAttestationDownload(t *testing.T) { // Call download.AttestationCmd() to ensure success attOpts := options.AttestationDownloadOptions{} - must(download.AttestationCmd(ctx, regOpts, attOpts, imgName), t) + must(download.AttestationCmd(ctx, regOpts, attOpts, imgName, os.Stdout), t) attestations, err := cosign.FetchAttestationsForReference(ctx, ref, attOpts.PredicateType, ociremoteOpts...) if err != nil { @@ -1603,7 +1603,7 @@ func TestAttestationDownloadWithPredicateType(t *testing.T) { attOpts := options.AttestationDownloadOptions{ PredicateType: "vuln", } - must(download.AttestationCmd(ctx, regOpts, attOpts, imgName), t) + must(download.AttestationCmd(ctx, regOpts, attOpts, imgName, os.Stdout), t) predicateType, _ := options.ParsePredicateType(attOpts.PredicateType) attestations, err := cosign.FetchAttestationsForReference(ctx, ref, predicateType, ociremoteOpts...) @@ -1653,7 +1653,7 @@ func TestAttestationDownloadWithBadPredicateType(t *testing.T) { attOpts := options.AttestationDownloadOptions{ PredicateType: "vuln", } - mustErr(download.AttestationCmd(ctx, regOpts, attOpts, imgName), t) + mustErr(download.AttestationCmd(ctx, regOpts, attOpts, imgName, os.Stdout), t) } func TestAttestationReplaceCreate(t *testing.T) { @@ -2505,7 +2505,7 @@ func TestDuplicateSign(t *testing.T) { // Verify should fail at first mustErr(verify(pubKeyPath, imgName, true, nil, "", true), t) // So should download - mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now sign the image ko := options.KeyOpts{ @@ -2520,7 +2520,7 @@ func TestDuplicateSign(t *testing.T) { // Now verify and download should work! // Ignore the tlog, because uploading to the tlog causes new signatures with new timestamp entries to be appended. must(verify(pubKeyPath, imgName, true, nil, "", true), t) - must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Signing again should work just fine... must(sign.SignCmd(ro, ko, so, []string{imgName}), t) @@ -3217,6 +3217,104 @@ func TestSaveLoadAttestation(t *testing.T) { must(verifyAttestation.Exec(ctx, []string{imageDir}), t) } +func TestAttestDownloadAttachNewBundle(t *testing.T) { + repo, stop := reg(t) + defer stop() + + imgName := path.Join(repo, "attest-new-bundle") + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + // Download should fail before attesting + ctx := context.Background() + regOpts := options.RegistryOptions{} + attOpts := options.AttestationDownloadOptions{} + mustErr(download.AttestationCmd(ctx, regOpts, attOpts, imgName, os.Stdout), t) + + // Attest first image + td := t.TempDir() + _, privKeyPath, _ := keypair(t, td) + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, NewBundleFormat: true} + + slsaAttestation := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }` + slsaAttestationPath := filepath.Join(td, "attestation.slsa.json") + if err := os.WriteFile(slsaAttestationPath, []byte(slsaAttestation), 0600); err != nil { + t.Fatal(err) + } + + attestCommand := attest.AttestCommand{ + KeyOpts: ko, + PredicatePath: slsaAttestationPath, + PredicateType: "slsaprovenance", + RekorEntryType: "dsse", + } + + must(attestCommand.Exec(ctx, imgName), t) + + // Download should now succeed - redirect stdout to use with attach + out := bytes.Buffer{} + must(download.AttestationCmd(ctx, regOpts, attOpts, imgName, &out), t) + + // Create a new image to attach to + img2Name := path.Join(repo, "attest-new-bundle-2") + _, _, cleanup = mkimage(t, img2Name) + defer cleanup() + + bundlePath := filepath.Join(td, "downloaded-bundle.sigstore.json") + if err := os.WriteFile(bundlePath, out.Bytes(), 0600); err != nil { + t.Fatal(err) + } + + must(attach.AttestationCmd(ctx, regOpts, []string{bundlePath}, img2Name), t) + + // Download should succeed on second image + must(download.AttestationCmd(ctx, regOpts, attOpts, img2Name, os.Stdout), t) +} + +func TestSignDownloadAttachNewBundle(t *testing.T) { + repo, stop := reg(t) + defer stop() + + imgName := path.Join(repo, "sign-new-bundle") + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + // Download should fail before attesting + ctx := context.Background() + regOpts := options.RegistryOptions{} + mustErr(download.SignatureCmd(ctx, regOpts, imgName, os.Stdout), t) + + // Sign first image + td := t.TempDir() + _, privKeyPath, _ := keypair(t, td) + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + so := options.SignOptions{ + NewBundleFormat: true, + Upload: true, + } + + must(sign.SignCmd(ro, ko, so, []string{imgName}), t) + + // Download should now succeed - redirect stdout to use with attach + out := bytes.Buffer{} + must(download.SignatureCmd(ctx, regOpts, imgName, &out), t) + + // Create a new image to attach to + img2Name := path.Join(repo, "sign-new-bundle-2") + _, _, cleanup = mkimage(t, img2Name) + defer cleanup() + + bundlePath := filepath.Join(td, "downloaded-bundle.sigstore.json") + if err := os.WriteFile(bundlePath, out.Bytes(), 0600); err != nil { + t.Fatal(err) + } + + must(attach.SignatureCmd(ctx, regOpts, "", bundlePath, "", "", "", "", img2Name), t) + + // Download should succeed on second image + must(download.SignatureCmd(ctx, regOpts, img2Name, os.Stdout), t) +} + func TestAttachSBOM(t *testing.T) { td := t.TempDir() err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td)