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 <jsand@google.com>
  • Loading branch information
Jake Sanders committed May 17, 2021
1 parent 8f24b6a commit 00ba379
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 88 deletions.
196 changes: 131 additions & 65 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 @@ -129,8 +133,43 @@ type SignOpts struct {
Pf cosign.PassFunc
}

func getTransitiveImages(rootIndex *remote.Descriptor, repo name.Repository, opts ...remote.Option) ([]name.Digest, error) {
var imgs []name.Digest

indexDescs := []*remote.Descriptor{rootIndex}

for len(indexDescs) > 0 {
indexDesc := indexDescs[len(indexDescs)-1]
indexDescs = indexDescs[:len(indexDescs)-1]

idx, err := indexDesc.ImageIndex()
if err != nil {
return nil, err
}
idxManifest, err := idx.IndexManifest()
if err != nil {
return nil, err
}
for _, manifest := range idxManifest.Manifests {
if manifest.MediaType.IsIndex() {
nextIndexName := repo.Digest(manifest.Digest.String())
indexDesc, err := remote.Get(nextIndexName, opts...)
if err != nil {
return nil, errors.Wrap(err, "getting recursive image index")
}
indexDescs = append(indexDescs, indexDesc)

}
childImg := repo.Digest(manifest.Digest.String())
imgs = append(imgs, childImg)
}
}

return imgs, nil
}

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 +192,18 @@ 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() {
imgs, err := getTransitiveImages(get, repo, remoteAuth)
if err != nil {
return err
}
toSign = append(toSign, imgs...)
}

var signer signature.Signer
Expand Down Expand Up @@ -198,71 +234,101 @@ 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

var staticPayload []byte
if payloadPath != "" {
fmt.Fprintln(os.Stderr, "Using payload from:", payloadPath)
staticPayload, err = ioutil.ReadFile(filepath.Clean(payloadPath))
if err != nil {
return errors.Wrap(err, "payload from file")
}
}
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 len(toSign) > 0 {
img := toSign[0]
toSign = toSign[1:]
// The payload can be specified via a flag to skip generation.
payload := staticPayload
if len(payload) == 0 {
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, err := cosign.SignaturesRef(img)
if err != nil {
return err
}

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
23 changes: 16 additions & 7 deletions pkg/cosign/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ func DockerMediaTypes() bool {
return false
}

func DestinationRef(ref name.Reference, img *remote.Descriptor) (name.Reference, error) {
dstTag := ref.Context().Tag(Munge(img.Descriptor))
func substituteRepo(img name.Reference) (name.Reference, error) {
wantRepo := os.Getenv(repoEnv)
if wantRepo == "" {
return dstTag, nil
return img, nil
}
reg := img.Context().RegistryStr()
// strip registry from image
oldImage := strings.TrimPrefix(dstTag.Name(), dstTag.RegistryStr())
newSubrepo := strings.TrimPrefix(wantRepo, dstTag.RegistryStr())
oldImage := strings.TrimPrefix(img.Name(), reg)
newSubrepo := strings.TrimPrefix(wantRepo, reg)

// replace old subrepo with new one
subRepo := strings.Split(oldImage, "/")
Expand All @@ -74,13 +74,22 @@ func DestinationRef(ref name.Reference, img *remote.Descriptor) (name.Reference,
}
newRepo := strings.Join(subRepo, "/")
// add the tag back in if we lost it
if !strings.Contains(newRepo, ":") {
if dstTag, isTag := img.(name.Tag); isTag && !strings.Contains(newRepo, ":") {
newRepo = newRepo + ":" + dstTag.TagStr()
}
subbed := dstTag.RegistryStr() + newRepo
subbed := reg + newRepo
return name.ParseReference(subbed)
}

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

func DestinationRef(ref name.Reference, img *remote.Descriptor) (name.Reference, error) {
dstTag := ref.Context().Tag(Munge(img.Descriptor))
return substituteRepo(dstTag)
}

// Upload will upload the signature, public key and payload to the tlog
func UploadTLog(signature, payload []byte, pemBytes []byte) (*models.LogEntryAnon, error) {
rekorClient, err := app.GetRekorClient(TlogServer())
Expand Down
Loading

0 comments on commit 00ba379

Please sign in to comment.