Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add -r option for recursively signing multi-arch images #320

Merged
merged 1 commit into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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