Skip to content

Commit

Permalink
Add cosign attachment signing and verification (#615)
Browse files Browse the repository at this point in the history
Signed-off-by: Sambhav Kothari <sambhavs.email@gmail.com>
  • Loading branch information
sambhav authored Sep 3, 2021
1 parent de3f9d6 commit 79fa380
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 40 deletions.
44 changes: 40 additions & 4 deletions cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -180,15 +186,40 @@ 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
},
}
}

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

Expand Down Expand Up @@ -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{}
Expand Down
2 changes: 1 addition & 1 deletion cmd/cosign/cli/sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
15 changes: 14 additions & 1 deletion cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type VerifyCommand struct {
Slot string
Output string
RekorURL string
Attachment string
Annotations *map[string]interface{}
}

Expand All @@ -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")
Expand Down Expand Up @@ -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{}
}
Expand Down Expand Up @@ -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
Expand Down
88 changes: 54 additions & 34 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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() {
Expand All @@ -636,29 +656,29 @@ 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{
KeyRef: privKeyPath,
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) {
Expand Down

0 comments on commit 79fa380

Please sign in to comment.