Skip to content

Commit

Permalink
clean command for deleting signature metadata from a given image (#324)
Browse files Browse the repository at this point in the history
Signed-off-by: Batuhan Apaydın <[email protected]>
  • Loading branch information
developer-guy authored and Jake Sanders committed May 17, 2021
1 parent 5e92be2 commit 9543ec6
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 88 deletions.
77 changes: 77 additions & 0 deletions cmd/cosign/cli/clean.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// Copyright 2021 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cli

import (
"context"
"flag"
"fmt"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/sigstore/cosign/pkg/cosign"
)

func Clean() *ffcli.Command {
var (
flagset = flag.NewFlagSet("cosign clean", flag.ExitOnError)
)

return &ffcli.Command{
Name: "clean",
ShortUsage: "cosign clean <image uri>",
ShortHelp: "Remove all signatures from an image",
FlagSet: flagset,
Exec: func(ctx context.Context, args []string) error {
if len(args) != 1 {
return flag.ErrHelp
}

return CleanCmd(ctx, args[0])
},
}
}

func CleanCmd(_ context.Context, imageRef string) error {
ref, err := name.ParseReference(imageRef)
if err != nil {
return err
}
// TODO: just return the descriptor directly if we have a digest reference.
desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
if err != nil {
return err
}

dstRef, err := cosign.DestinationRef(ref, desc)
if err != nil {
return err
}

signRef := dstRef.Context().Tag(cosign.Munge(desc.Descriptor))
fmt.Println(signRef)

fmt.Println("Deleting signature metadata...")

err = remote.Delete(signRef, remote.WithAuthFromKeychain(authn.DefaultKeychain))
if err != nil {
return err
}

return nil
}
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
2 changes: 1 addition & 1 deletion cmd/cosign/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func main() {
FlagSet: rootFlagSet,
Subcommands: []*ffcli.Command{
cli.Verify(), cli.Sign(), cli.Upload(), cli.Generate(), cli.Download(), cli.GenerateKeyPair(), cli.SignBlob(),
cli.VerifyBlob(), cli.Triangulate(), cli.Version(), cli.PublicKey(), pivcli.PivKey(), cli.Copy()},
cli.VerifyBlob(), cli.Triangulate(), cli.Version(), cli.PublicKey(), pivcli.PivKey(), cli.Copy(), cli.Clean()},
Exec: func(context.Context, []string) error {
return flag.ErrHelp
},
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
Loading

0 comments on commit 9543ec6

Please sign in to comment.