Skip to content

Commit

Permalink
add -r option for recursively signing multi-arch images
Browse files Browse the repository at this point in the history
Signed-off-by: Jake Sanders <[email protected]>
  • Loading branch information
Jake Sanders committed May 12, 2021
1 parent 8573caa commit dff1687
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 81 deletions.
159 changes: 93 additions & 66 deletions cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,13 @@ func Sign() *ffcli.Command {
sk = flagset.Bool("sk", false, "whether to use a hardware security key")
payloadPath = flagset.String("payload", "", "path to a payload file to use rather than generating one.")
force = flagset.Bool("f", false, "skip warnings and confirmations")
recursive = flagset.Bool("r", false, "if a multi-arch image is specified, additionally sign each discrete image")
annotations = annotationsMap{}
)
flagset.Var(&annotations, "a", "extra key=value pairs to sign")
return &ffcli.Command{
Name: "sign",
ShortUsage: "cosign sign -key <key path>|<kms uri> [-payload <path>] [-a key=value] [-upload=true|false] [-f] <image uri>",
ShortUsage: "cosign sign -key <key path>|<kms uri> [-payload <path>] [-a key=value] [-upload=true|false] [-f] [-r] <image uri>",
ShortHelp: `Sign the supplied container image.`,
LongHelp: `Sign the supplied container image.
Expand All @@ -91,6 +92,9 @@ EXAMPLES
# sign a container image with a local key pair file
cosign sign -key cosign.key <IMAGE>
# sign a multi-arch container image AND all referenced, discrete images
cosign sign -key cosign.key -r <MULTI-ARCH IMAGE>
# sign a container image and add annotations
cosign sign -key cosign.key -a key1=value1 -a key2=value2 <IMAGE>
Expand All @@ -113,7 +117,7 @@ EXAMPLES
Sk: *sk,
}
for _, img := range args {
if err := SignCmd(ctx, so, img, *upload, *payloadPath, *force); err != nil {
if err := SignCmd(ctx, so, img, *upload, *payloadPath, *force, *recursive); err != nil {
return errors.Wrapf(err, "signing %s", img)
}
}
Expand All @@ -130,7 +134,7 @@ type SignOpts struct {
}

func SignCmd(ctx context.Context, so SignOpts,
imageRef string, upload bool, payloadPath string, force bool) error {
imageRef string, upload bool, payloadPath string, force bool, recursive bool) error {

// A key file or token is required unless we're in experimental mode!
if cosign.Experimental() {
Expand All @@ -153,21 +157,25 @@ func SignCmd(ctx context.Context, so SignOpts,
if err != nil {
return errors.Wrap(err, "getting remote image")
}

repo := ref.Context()
img := repo.Digest(get.Digest.String())
// The payload can be specified via a flag to skip generation.
var payload []byte
if payloadPath != "" {
fmt.Fprintln(os.Stderr, "Using payload from:", payloadPath)
payload, err = ioutil.ReadFile(filepath.Clean(payloadPath))
} else {
payload, err = (&sigPayload.Cosign{
Image: img,
Annotations: so.Annotations,
}).MarshalJSON()
}
if err != nil {
return errors.Wrap(err, "payload")

toSign := []name.Digest{img}

if recursive && get.MediaType.IsIndex() {
idx, err := get.ImageIndex()
if err != nil {
return err
}
idxManifest, err := idx.IndexManifest()
if err != nil {
return err
}
for _, manifest := range idxManifest.Manifests {
childImg := repo.Digest(manifest.Digest.String())
toSign = append(toSign, childImg)
}
}

var signer signature.Signer
Expand Down Expand Up @@ -198,71 +206,90 @@ func SignCmd(ctx context.Context, so SignOpts,
cert, chain = k.Cert, k.Chain
}

sig, _, err := signer.Sign(ctx, payload)
if err != nil {
return errors.Wrap(err, "signing")
}

if !upload {
fmt.Println(base64.StdEncoding.EncodeToString(sig))
return nil
}

// sha256:... -> sha256-...
dstRef, err := cosign.DestinationRef(ref, get)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Pushing signature to:", dstRef.String())
uo := cosign.UploadOpts{
Cert: cert,
Chain: chain,
DupeDetector: dupeDetector,
RemoteOpts: []remote.Option{remoteAuth},
}

if !cosign.Experimental() {
_, err := cosign.Upload(ctx, sig, payload, dstRef, uo)
return err
}

// Check if the image is public (no auth in Get)
if !force {
uploadTLog := cosign.Experimental()
if uploadTLog && !force {
if _, err := remote.Get(ref); err != nil {
fmt.Print("warning: uploading to the public transparency log for a private image, please confirm [Y/N]: ")
var response string
if _, err := fmt.Scanln(&response); err != nil {

var tlogConfirmResponse string
if _, err := fmt.Scanln(&tlogConfirmResponse); err != nil {
return err
}
if response != "Y" {
if tlogConfirmResponse != "Y" {
fmt.Println("not uploading to transparency log")
return nil
uploadTLog = false
}
}
}

// Upload the cert or the public key, depending on what we have
var rekorBytes []byte
if cert != "" {
rekorBytes = []byte(cert)
} else {
pemBytes, err := cosign.PublicKeyPem(ctx, signer)
if err != nil {
return nil
if uploadTLog {
// Upload the cert or the public key, depending on what we have
if cert != "" {
rekorBytes = []byte(cert)
} else {
pemBytes, err := cosign.PublicKeyPem(ctx, signer)
if err != nil {
return err
}
rekorBytes = pemBytes
}
rekorBytes = pemBytes
}
entry, err := cosign.UploadTLog(sig, payload, rekorBytes)
if err != nil {
return err
}
fmt.Println("tlog entry created with index: ", *entry.LogIndex)

uo.Bundle = bundle(entry)
uo.AdditionalAnnotations = annotations(entry)
if _, err = cosign.Upload(ctx, sig, payload, dstRef, uo); err != nil {
return errors.Wrap(err, "uploading")
for i, img := range toSign {
// The payload can be specified via a flag to skip generation.
var payload []byte
if payloadPath != "" && i == 0 {
fmt.Fprintln(os.Stderr, "Using payload from:", payloadPath)
payload, err = ioutil.ReadFile(filepath.Clean(payloadPath))
} else {
payload, err = (&sigPayload.Cosign{
Image: img,
Annotations: so.Annotations,
}).MarshalJSON()
}
if err != nil {
return errors.Wrap(err, "payload")
}

sig, _, err := signer.Sign(ctx, payload)
if err != nil {
return errors.Wrap(err, "signing")
}

if !upload {
fmt.Println(base64.StdEncoding.EncodeToString(sig))
continue
}

// sha256:... -> sha256-...
sigRef := cosign.SignaturesRef(img)

uo := cosign.UploadOpts{
Cert: cert,
Chain: chain,
DupeDetector: dupeDetector,
RemoteOpts: []remote.Option{remoteAuth},
}

if uploadTLog {
entry, err := cosign.UploadTLog(sig, payload, rekorBytes)
if err != nil {
return err
}
fmt.Println("tlog entry created with index: ", *entry.LogIndex)

uo.Bundle = bundle(entry)
uo.AdditionalAnnotations = annotations(entry)
}

fmt.Fprintln(os.Stderr, "Pushing signature to:", sigRef.String())
if _, err = cosign.Upload(ctx, sig, payload, sigRef, uo); err != nil {
return errors.Wrap(err, "uploading")
}
}

return nil
}

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, so, "", false, "", false)
err := SignCmd(ctx, so, "", false, "", false, false)
if (errors.Is(err, &KeyParseError{}) == false) {
t.Fatal("expected KeyParseError")
}
Expand Down
8 changes: 5 additions & 3 deletions pkg/cosign/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ type SignedPayload struct {
// }

func Munge(desc v1.Descriptor) string {
return signatureImageTagForDigest(desc.Digest.String())
}

func signatureImageTagForDigest(digest string) string {
// sha256:... -> sha256-...
munged := strings.ReplaceAll(desc.Digest.String(), ":", "-")
munged += ".sig"
return munged
return strings.ReplaceAll(digest, ":", "-") + ".sig"
}

func FetchSignatures(ctx context.Context, ref name.Reference) ([]SignedPayload, *v1.Descriptor, error) {
Expand Down
4 changes: 4 additions & 0 deletions pkg/cosign/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func DockerMediaTypes() bool {
return false
}

func SignaturesRef(signed name.Digest) name.Reference {
return signed.Context().Tag(signatureImageTagForDigest(signed.DigestStr()))
}

func DestinationRef(ref name.Reference, img *remote.Descriptor) (name.Reference, error) {
dstTag := ref.Context().Tag(Munge(img.Descriptor))
wantRepo := os.Getenv(repoEnv)
Expand Down
18 changes: 9 additions & 9 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestSignVerify(t *testing.T) {

// Now sign the image
so := cli.SignOpts{KeyRef: privKeyPath, Pf: passFunc}
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, imgName, true, "", false, false), t)

// Now verify and download should work!
must(verify(pubKeyPath, imgName, true, nil), t)
Expand All @@ -91,7 +91,7 @@ func TestSignVerify(t *testing.T) {

// Sign the image with an annotation
so.Annotations = map[string]interface{}{"foo": "bar"}
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, imgName, true, "", false, false), t)

// It should match this time.
must(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}), t)
Expand Down Expand Up @@ -125,7 +125,7 @@ func TestBundle(t *testing.T) {
}

// Sign the image
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, imgName, true, "", false, false), t)
// Make sure verify works
must(verify(pubKeyPath, imgName, true, nil), t)

Expand Down Expand Up @@ -154,14 +154,14 @@ func TestDuplicateSign(t *testing.T) {

// Now sign the image
so := cli.SignOpts{KeyRef: privKeyPath, Pf: passFunc}
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, imgName, true, "", false, false), t)

// Now verify and download should work!
must(verify(pubKeyPath, imgName, true, nil), t)
must(cli.DownloadCmd(ctx, imgName), t)

// Signing again should work just fine...
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, imgName, true, "", false, false), t)
// but a duplicate signature should not be a uploaded
signatures, _, err := cosign.FetchSignatures(ctx, ref)
if err != nil {
Expand Down Expand Up @@ -216,14 +216,14 @@ func TestMultipleSignatures(t *testing.T) {

// Now sign the image with one key
so := cli.SignOpts{KeyRef: priv1, Pf: passFunc}
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, 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)

// Now sign with the other key too
so.KeyRef = priv2
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, imgName, true, "", false, false), t)

// Now verify should work with both
must(verify(pub1, imgName, true, nil), t)
Expand Down Expand Up @@ -425,7 +425,7 @@ func TestTlog(t *testing.T) {
KeyRef: privKeyPath,
Pf: passFunc,
}
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, imgName, true, "", false, false), t)

// Now verify should work!
must(verify(pubKeyPath, imgName, true, nil), t)
Expand All @@ -437,7 +437,7 @@ func TestTlog(t *testing.T) {
mustErr(verify(pubKeyPath, imgName, true, nil), t)

// Sign again with the tlog env var on
must(cli.SignCmd(ctx, so, imgName, true, "", false), t)
must(cli.SignCmd(ctx, so, imgName, true, "", false, false), t)
// And now verify works!
must(verify(pubKeyPath, imgName, true, nil), t)
}
Expand Down
15 changes: 13 additions & 2 deletions test/e2e_test_secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ export COSIGN_PASSWORD=$pass
img="us-central1-docker.pkg.dev/projectsigstore/cosign-ci/test"
img2="us-central1-docker.pkg.dev/projectsigstore/cosign-ci/test-2"
legacy_img="us-central1-docker.pkg.dev/projectsigstore/cosign-ci/legacy-test"
img_copy="${img}/copy"
for image in $img $img2 $legacy_img
do
(crane delete $(./cosign triangulate $image)) || true
crane cp busybox $image
done
img_copy="${img}/copy"
crane ls $img_copy | while read tag ; do crane delete "${img_copy}:${tag}" ; done

multiarch_img="us-central1-docker.pkg.dev/projectsigstore/cosign-ci/multiarch-test"
crane ls $multiarch_img | while read tag ; do crane delete "${multiarch_img}:${tag}" ; done
crane cp gcr.io/distroless/base $multiarch_img

## sign/verify
./cosign sign -key cosign.key $img
Expand All @@ -49,6 +51,15 @@ crane ls $img_copy | while read tag ; do crane delete "${img_copy}:${tag}" ; don
./cosign copy $img $img_copy
./cosign verify -key cosign.pub $img_copy

# sign recursively
./cosign sign -key cosign.key -r $multiarch_img
./cosign verify -key cosign.pub $multiarch_img # verify image index
for arch in "linux/amd64" "linux/arm64" "linux/s390x"
do
# verify sigs on discrete images
./cosign verify -key cosign.pub "${multiarch_img}@$(crane digest --platform=$arch ${multiarch_img})"
done

## confirm use of OCI media type in signature image
crane manifest $(./cosign triangulate $img) | grep -q "application/vnd.oci.image.config.v1+json"

Expand Down

0 comments on commit dff1687

Please sign in to comment.