diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 3e51d09eea8..da6bf1ec881 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -121,6 +121,7 @@ func Sign() *ffcli.Command { oidcIssuer = flagset.String("oidc-issuer", "https://oauth2.sigstore.dev/auth", "[EXPERIMENTAL] OIDC provider to be used to issue ID token") oidcClientID = flagset.String("oidc-client-id", "sigstore", "[EXPERIMENTAL] OIDC client ID for application") oidcClientSecret = flagset.String("oidc-client-secret", "", "[EXPERIMENTAL] OIDC client secret for application") + attachment = flagset.String("attachment", "", "related image attachment to sign (sbom), default none") annotations = annotationsMap{} ) flagset.Var(&annotations, "a", "extra key=value pairs to sign") @@ -166,7 +167,12 @@ EXAMPLES if len(args) == 0 { return flag.ErrHelp } - + switch *attachment { + case "sbom", "": + break + default: + return flag.ErrHelp + } ko := KeyOpts{ KeyRef: *key, PassFunc: GetPass, @@ -180,8 +186,11 @@ EXAMPLES OIDCClientSecret: *oidcClientSecret, } for _, img := range args { - if err := SignCmd(ctx, ko, annotations.annotations, img, *cert, *upload, *payloadPath, *force, *recursive); err != nil { - return errors.Wrapf(err, "signing %s", img) + if err := SignCmd(ctx, ko, annotations.annotations, img, *cert, *upload, *payloadPath, *force, *recursive, *attachment); err != nil { + if *attachment == "" { + return errors.Wrapf(err, "signing %s", img) + } + return errors.Wrapf(err, "signing attachement %s for image %s", *attachment, img) } } return nil @@ -189,6 +198,28 @@ EXAMPLES } } +func getAttachedImageRef(ctx context.Context, imageRef string, attachment string) (string, error) { + if attachment == "" { + return imageRef, nil + } + if attachment == "sbom" { + ref, err := name.ParseReference(imageRef) + if err != nil { + return "", err + } + + h, err := Digest(ctx, ref) + if err != nil { + return "", err + } + + repo := ref.Context() + dstRef := cosign.AttachedImageTag(repo, h, cosign.SBOMTagSuffix) + return dstRef.Name(), nil + } + return "", fmt.Errorf("unknown attachment type %s", attachment) +} + func getTransitiveImages(rootIndex *remote.Descriptor, repo name.Repository, opts ...remote.Option) ([]name.Digest, error) { var imgs []name.Digest @@ -225,8 +256,13 @@ func getTransitiveImages(rootIndex *remote.Descriptor, repo name.Repository, opt } func SignCmd(ctx context.Context, ko KeyOpts, annotations map[string]interface{}, - imageRef string, certPath string, upload bool, payloadPath string, force bool, recursive bool) error { + inputImg string, certPath string, upload bool, payloadPath string, force bool, recursive bool, attachment string) error { // A key file or token is required unless we're in experimental mode! + imageRef, err := getAttachedImageRef(ctx, inputImg, attachment) + if err != nil { + return fmt.Errorf("unable to resolve attachment %s for image %s", attachment, inputImg) + } + if EnableExperimental() { if nOf(ko.KeyRef, ko.Sk) > 1 { return &KeyParseError{} diff --git a/cmd/cosign/cli/sign_test.go b/cmd/cosign/cli/sign_test.go index 3731a458423..06a37e15c7f 100644 --- a/cmd/cosign/cli/sign_test.go +++ b/cmd/cosign/cli/sign_test.go @@ -34,7 +34,7 @@ func TestSignCmdLocalKeyAndSk(t *testing.T) { Sk: true, }, } { - err := SignCmd(ctx, ko, nil, "", "", false, "", false, false) + err := SignCmd(ctx, ko, nil, "", "", false, "", false, false, "") if (errors.Is(err, &KeyParseError{}) == false) { t.Fatal("expected KeyParseError") } diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index d335c3f09d2..d121ee3af8c 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -41,6 +41,7 @@ type VerifyCommand struct { Slot string Output string RekorURL string + Attachment string Annotations *map[string]interface{} } @@ -52,6 +53,7 @@ func applyVerifyFlags(cmd *VerifyCommand, flagset *flag.FlagSet) { flagset.StringVar(&cmd.RekorURL, "rekor-url", "https://rekor.sigstore.dev", "address of rekor STL server") flagset.BoolVar(&cmd.CheckClaims, "check-claims", true, "whether to check the claims found") flagset.StringVar(&cmd.Output, "output", "json", "output format for the signing image information (default JSON) (json|text)") + flagset.StringVar(&cmd.Attachment, "attachment", "", "related image attachment to sign (none|sbom), default none") // parse annotations flagset.Var(&annotations, "a", "extra key=value pairs to sign") @@ -110,6 +112,13 @@ func (c *VerifyCommand) Exec(ctx context.Context, args []string) (err error) { return flag.ErrHelp } + switch c.Attachment { + case "sbom", "": + break + default: + return flag.ErrHelp + } + if !oneOf(c.KeyRef, c.Sk) && !EnableExperimental() { return &KeyParseError{} } @@ -147,7 +156,11 @@ func (c *VerifyCommand) Exec(ctx context.Context, args []string) (err error) { } co.SigVerifier = pubKey - for _, imageRef := range args { + for _, img := range args { + imageRef, err := getAttachedImageRef(ctx, img, c.Attachment) + if err != nil { + return errors.Wrapf(err, "resolving attachment type %s for image %s", c.Attachment, img) + } ref, err := name.ParseReference(imageRef) if err != nil { return err diff --git a/test/e2e_test.go b/test/e2e_test.go index 8a240a69ba6..7276d48c203 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build e2e // +build e2e package test @@ -62,12 +63,13 @@ var passFunc = func(_ bool) ([]byte, error) { return keyPass, nil } -var verify = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}) error { +var verify = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string) error { cmd := cli.VerifyCommand{ KeyRef: keyRef, RekorURL: rekorURL, CheckClaims: checkClaims, Annotations: &annotations, + Attachment: attachment, } args := []string{imageRef} @@ -89,30 +91,30 @@ func TestSignVerify(t *testing.T) { ctx := context.Background() // Verify should fail at first - mustErr(verify(pubKeyPath, imgName, true, nil), t) + mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) // So should download mustErr(download.SignatureCmd(ctx, imgName), t) // Now sign the image ko := cli.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // Now verify and download should work! - must(verify(pubKeyPath, imgName, true, nil), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) must(download.SignatureCmd(ctx, imgName), t) // Look for a specific annotation - mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}), t) + mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, ""), t) // Sign the image with an annotation annotations := map[string]interface{}{"foo": "bar"} - must(cli.SignCmd(ctx, ko, annotations, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, annotations, imgName, "", true, "", false, false, ""), t) // It should match this time. - must(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}), t) + must(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, ""), t) // But two doesn't work - mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}), t) + mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, ""), t) } func TestSignVerifyClean(t *testing.T) { @@ -130,17 +132,17 @@ func TestSignVerifyClean(t *testing.T) { // Now sign the image ko := cli.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // Now verify and download should work! - must(verify(pubKeyPath, imgName, true, nil), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) must(download.SignatureCmd(ctx, imgName), t) // Now clean signature from the given image must(cli.CleanCmd(ctx, imgName), t) // It doesn't work - mustErr(verify(pubKeyPath, imgName, true, nil), t) + mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) } func TestAttestVerify(t *testing.T) { @@ -177,7 +179,7 @@ func TestAttestVerify(t *testing.T) { // Now verify and download should work! must(verifyAttestation.Exec(ctx, []string{imgName}), t) // Look for a specific annotation - mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}), t) + mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, ""), t) } func TestBundle(t *testing.T) { @@ -204,14 +206,14 @@ func TestBundle(t *testing.T) { } // Sign the image - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // Make sure verify works - must(verify(pubKeyPath, imgName, true, nil), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) // Make sure offline verification works with bundling // use rekor prod since we have hardcoded the public key os.Setenv(serverEnv, "notreal") - must(verify(pubKeyPath, imgName, true, nil), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) } func TestDuplicateSign(t *testing.T) { @@ -228,20 +230,20 @@ func TestDuplicateSign(t *testing.T) { ctx := context.Background() // Verify should fail at first - mustErr(verify(pubKeyPath, imgName, true, nil), t) + mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) // So should download mustErr(download.SignatureCmd(ctx, imgName), t) // Now sign the image ko := cli.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // Now verify and download should work! - must(verify(pubKeyPath, imgName, true, nil), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) must(download.SignatureCmd(ctx, imgName), t) // Signing again should work just fine... - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // but a duplicate signature should not be a uploaded sigRepo, err := cli.TargetRepositoryForImage(ref) if err != nil { @@ -264,7 +266,7 @@ func TestKeyURLVerify(t *testing.T) { keyRef := "https://raw.githubusercontent.com/GoogleContainerTools/distroless/main/cosign.pub" img := "gcr.io/distroless/base:latest" - must(verify(keyRef, img, true, nil), t) + must(verify(keyRef, img, true, nil, ""), t) } func TestGenerateKeyPairEnvVar(t *testing.T) { @@ -330,23 +332,23 @@ func TestMultipleSignatures(t *testing.T) { ctx := context.Background() // Verify should fail at first for both keys - mustErr(verify(pub1, imgName, true, nil), t) - mustErr(verify(pub2, imgName, true, nil), t) + mustErr(verify(pub1, imgName, true, nil, ""), t) + mustErr(verify(pub2, imgName, true, nil, ""), t) // Now sign the image with one key ko := cli.KeyOpts{KeyRef: priv1, PassFunc: passFunc} - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // Now verify should work with that one, but not the other - must(verify(pub1, imgName, true, nil), t) - mustErr(verify(pub2, imgName, true, nil), t) + must(verify(pub1, imgName, true, nil, ""), t) + mustErr(verify(pub2, imgName, true, nil, ""), t) // Now sign with the other key too ko.KeyRef = priv2 - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // Now verify should work with both - must(verify(pub1, imgName, true, nil), t) - must(verify(pub2, imgName, true, nil), t) + must(verify(pub1, imgName, true, nil, ""), t) + must(verify(pub2, imgName, true, nil, ""), t) } func TestSignBlob(t *testing.T) { @@ -611,6 +613,24 @@ func TestAttachSBOM(t *testing.T) { if diff := cmp.Diff(string(want), sboms[0]); diff != "" { t.Errorf("diff: %s", diff) } + + // Generate key pairs to sign the sbom + td1 := t.TempDir() + td2 := t.TempDir() + _, privKeyPath1, pubKeyPath1 := keypair(t, td1) + _, _, pubKeyPath2 := keypair(t, td2) + + // Verify should fail on a bad input + mustErr(verify(pubKeyPath1, imgName, true, nil, "sbom"), t) + mustErr(verify(pubKeyPath2, imgName, true, nil, "sbom"), t) + + // Now sign the sbom with one key + ko1 := cli.KeyOpts{KeyRef: privKeyPath1, PassFunc: passFunc} + must(cli.SignCmd(ctx, ko1, nil, imgName, "", true, "", false, false, "sbom"), t) + + // Now verify should work with that one, but not the other + must(verify(pubKeyPath1, imgName, true, nil, "sbom"), t) + mustErr(verify(pubKeyPath2, imgName, true, nil, "sbom"), t) } func setenv(t *testing.T, k, v string) func() { @@ -636,7 +656,7 @@ func TestTlog(t *testing.T) { ctx := context.Background() // Verify should fail at first - mustErr(verify(pubKeyPath, imgName, true, nil), t) + mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) // Now sign the image without the tlog ko := cli.KeyOpts{ @@ -644,21 +664,21 @@ func TestTlog(t *testing.T) { PassFunc: passFunc, RekorURL: rekorURL, } - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // Now verify should work! - must(verify(pubKeyPath, imgName, true, nil), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) // Now we turn on the tlog! defer setenv(t, cli.ExperimentalEnv, "1")() // Verify shouldn't work since we haven't put anything in it yet. - mustErr(verify(pubKeyPath, imgName, true, nil), t) + mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) // Sign again with the tlog env var on - must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false), t) + must(cli.SignCmd(ctx, ko, nil, imgName, "", true, "", false, false, ""), t) // And now verify works! - must(verify(pubKeyPath, imgName, true, nil), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) } func TestGetPublicKeyCustomOut(t *testing.T) {