diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 9e11c548e12..b084b9e34d0 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -28,6 +28,7 @@ type VerifyOptions struct { Attachment string Output string SignatureRef string + LocalImage bool SecurityKey SecurityKeyOptions Rekor RekorOptions @@ -67,6 +68,9 @@ func (o *VerifyOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.SignatureRef, "signature", "", "signature content or path or remote URL") + + cmd.Flags().BoolVar(&o.LocalImage, "local-image", false, + "whether the path to the image is a local directory") } // VerifyAttestationOptions is the top level wrapper for the `verify attestation` command. diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index a99731b17f2..9d986b03198 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -53,6 +53,9 @@ against the transparency log.`, # signature digest algorithm cosign verify --key cosign.pub --signature-digest-algorithm sha512 + # verify image with an on-disk signed image from 'cosign save' + cosign verify --key cosign.pub --local-image + # verify image with public key provided by URL cosign verify --key https://host.for/[FILE] @@ -97,6 +100,7 @@ against the transparency log.`, Annotations: annotations, HashAlgorithm: hashAlgorithm, SignatureRef: o.SignatureRef, + LocalImage: o.LocalImage, } return v.Exec(cmd.Context(), args) diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index e131869cdca..6e625ab6a52 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -58,6 +58,7 @@ type VerifyCommand struct { Annotations sigs.AnnotationsMap SignatureRef string HashAlgorithm crypto.Hash + LocalImage bool } // Exec runs the verification command @@ -138,22 +139,31 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { co.SigVerifier = pubKey for _, img := range images { - ref, err := name.ParseReference(img) - if err != nil { - return errors.Wrap(err, "parsing reference") - } - ref, err = sign.GetAttachedImageRef(ref, c.Attachment, ociremoteOpts...) - if err != nil { - return errors.Wrapf(err, "resolving attachment type %s for image %s", c.Attachment, img) - } + if c.LocalImage { + verified, bundleVerified, err := cosign.VerifyLocalImageSignatures(ctx, img, co) + if err != nil { + return err + } + PrintVerificationHeader(img, co, bundleVerified) + PrintVerification(img, verified, c.Output) + } else { + ref, err := name.ParseReference(img) + if err != nil { + return errors.Wrap(err, "parsing reference") + } + ref, err = sign.GetAttachedImageRef(ref, c.Attachment, ociremoteOpts...) + if err != nil { + return errors.Wrapf(err, "resolving attachment type %s for image %s", c.Attachment, img) + } - verified, bundleVerified, err := cosign.VerifyImageSignatures(ctx, ref, co) - if err != nil { - return err - } + verified, bundleVerified, err := cosign.VerifyImageSignatures(ctx, ref, co) + if err != nil { + return err + } - PrintVerificationHeader(ref.Name(), co, bundleVerified) - PrintVerification(ref.Name(), verified, c.Output) + PrintVerificationHeader(ref.Name(), co, bundleVerified) + PrintVerification(ref.Name(), verified, c.Output) + } } return nil diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index ad08cecd623..105f7f879cf 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -63,6 +63,7 @@ cosign dockerfile verify [flags] -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret + --local-image whether the path to the image is a local directory -o, --output string output format for the signing image information (json|text) (default "json") --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") --signature string signature content or path or remote URL diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index f18e8990b32..92cc350c365 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -57,6 +57,7 @@ cosign manifest verify [flags] -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret + --local-image whether the path to the image is a local directory -o, --output string output format for the signing image information (json|text) (default "json") --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") --signature string signature content or path or remote URL diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index a2da90c5ed2..d7b74259966 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -35,6 +35,9 @@ cosign verify [flags] # signature digest algorithm cosign verify --key cosign.pub --signature-digest-algorithm sha512 + # verify image with an on-disk signed image from 'cosign save' + cosign verify --key cosign.pub --local-image + # verify image with public key provided by URL cosign verify --key https://host.for/[FILE] @@ -67,6 +70,7 @@ cosign verify [flags] -h, --help help for verify --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the public key file, KMS URI or Kubernetes Secret + --local-image whether the path to the image is a local directory -o, --output string output format for the signing image information (json|text) (default "json") --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") --signature string signature content or path or remote URL diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 6381c845b67..7f2833e3a13 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -39,6 +39,7 @@ import ( ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/layout" ociremote "github.com/sigstore/cosign/pkg/oci/remote" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" @@ -202,7 +203,7 @@ func (fos *fakeOCISignatures) Get() ([]oci.Signature, error) { return fos.signatures, nil } -// VerifySignatures does all the main cosign checks in a loop, returning the verified signatures. +// VerifyImageSignatures does all the main cosign checks in a loop, returning the verified signatures. // If there were no valid signatures, we return an error. func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { // Enforce this up front. @@ -231,6 +232,56 @@ func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co } } + return verifySignatures(ctx, sigs, h, co) +} + +// VerifyLocalImageSignatures verifies signatures from a saved, local image, without any network calls, returning the verified signatures. +// If there were no valid signatures, we return an error. +func VerifyLocalImageSignatures(ctx context.Context, path string, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + // Enforce this up front. + if co.RootCerts == nil && co.SigVerifier == nil { + return nil, false, errors.New("one of verifier or root certs is required") + } + + se, err := layout.SignedImageIndex(path) + if err != nil { + return nil, false, err + } + + var h v1.Hash + // Verify either an image index or image. + ii, err := se.SignedImageIndex(v1.Hash{}) + if err != nil { + return nil, false, err + } + i, err := se.SignedImage(v1.Hash{}) + if err != nil { + return nil, false, err + } + switch { + case ii != nil: + h, err = ii.Digest() + if err != nil { + return nil, false, err + } + case i != nil: + h, err = i.Digest() + if err != nil { + return nil, false, err + } + default: + return nil, false, errors.New("must verify either an image index or image") + } + + sigs, err := se.Signatures() + if err != nil { + return nil, false, err + } + + return verifySignatures(ctx, sigs, h, co) +} + +func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { sl, err := sigs.Get() if err != nil { return nil, false, err diff --git a/test/e2e_test.go b/test/e2e_test.go index 10641246e95..abda9270ada 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -88,6 +88,22 @@ var verify = func(keyRef, imageRef string, checkClaims bool, annotations map[str return cmd.Exec(context.Background(), args) } +// Used to verify local images stored on disk +var verifyLocal = func(keyRef, path string, checkClaims bool, annotations map[string]interface{}, attachment string) error { + cmd := cliverify.VerifyCommand{ + KeyRef: keyRef, + CheckClaims: checkClaims, + Annotations: sigs.AnnotationsMap{Annotations: annotations}, + Attachment: attachment, + HashAlgorithm: crypto.SHA256, + LocalImage: true, + } + + args := []string{path} + + return cmd.Exec(context.Background(), args) +} + func TestSignVerify(t *testing.T) { repo, stop := reg(t) defer stop() @@ -648,6 +664,9 @@ func TestSaveLoad(t *testing.T) { imageDir := t.TempDir() must(cli.SaveCmd(ctx, options.SaveOptions{Directory: imageDir}, imgName), t) + // verify the local image using a local key + must(verifyLocal(pubKeyPath, imageDir, true, nil, ""), t) + // load the image from the temp dir into a new image and verify the new image imgName2 := path.Join(repo, fmt.Sprintf("save-load-%d-2", i)) must(cli.LoadCmd(ctx, options.LoadOptions{Directory: imageDir}, imgName2), t)