From 364cadc9e41fc8e2bfc0c124b78676588db610a4 Mon Sep 17 00:00:00 2001 From: dlorenc Date: Thu, 22 Jul 2021 20:40:01 -0500 Subject: [PATCH] Add "cosign attest" command! (#458) Signed-off-by: Dan Lorenc --- README.md | 20 +++ cmd/cosign/cli/attest.go | 245 +++++++++++++++++++++++++++ cmd/cosign/cli/sign.go | 72 ++++---- cmd/cosign/cli/verify_attestation.go | 162 ++++++++++++++++++ cmd/cosign/main.go | 2 + go.mod | 1 + go.sum | 3 + pkg/cosign/fetch.go | 5 +- pkg/cosign/upload.go | 28 ++- pkg/cosign/verifiers.go | 20 +++ test/e2e_test.go | 37 ++++ 11 files changed, 562 insertions(+), 33 deletions(-) create mode 100644 cmd/cosign/cli/attest.go create mode 100644 cmd/cosign/cli/verify_attestation.go diff --git a/README.md b/README.md index b9c4d8af98f..36673e36ea0 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,26 @@ tlog entry created with index: 5198 Pushing signature to: us.gcr.io/dlorenc-vmtest2/wasm:sha256-9e7a511fb3130ee4641baf1adc0400bed674d4afc3f1b81bb581c3c8f613f812.sig ``` +#### In-Toto Attestations + +Cosign also has built-in support for [in-toto.io](in-toto.io) attestations. +The specification for these is defined [here](https://github.com/in-toto/attestation). + +You can create and sign one from a local predicate file using the following commands: + +```shell +$ cosign attest -predicate -key cosign.pub +``` + +All of the standard key management systems are supported. +Payloads are signed using the DSSE signing spec, defined [here](https://github.com/secure-systems-lab/dsse). + +To verify: + +```shell +$ cosign verify-attestation -key cosign.pub +``` + ## Detailed Usage See the [Usage documentation](USAGE.md) for more commands! diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go new file mode 100644 index 00000000000..5145718889a --- /dev/null +++ b/cmd/cosign/cli/attest.go @@ -0,0 +1,245 @@ +// +// 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 ( + "bytes" + "context" + _ "crypto/sha256" // for `crypto.SHA256` + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/google/go-containerregistry/pkg/name" + ggcrV1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/peterbourgon/ff/v3/ffcli" + "github.com/pkg/errors" + + "github.com/sigstore/cosign/pkg/cosign" + cremote "github.com/sigstore/cosign/pkg/cosign/remote" + + rekorClient "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/sigstore/pkg/signature/dsse" + "github.com/sigstore/sigstore/pkg/signature/options" +) + +func Attest() *ffcli.Command { + var ( + flagset = flag.NewFlagSet("cosign attest", flag.ExitOnError) + key = flagset.String("key", "", "path to the private key file, KMS URI or Kubernetes Secret") + cert = flagset.String("cert", "", "Path to the x509 certificate to include in the Signature") + upload = flagset.Bool("upload", true, "whether to upload the signature") + sk = flagset.Bool("sk", false, "whether to use a hardware security key") + slot = flagset.String("slot", "", "security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)") + predicatePath = flagset.String("predicate", "", "path to the predicate file.") + force = flagset.Bool("f", false, "skip warnings and confirmations") + idToken = flagset.String("identity-token", "", "[EXPERIMENTAL] identity token to use for certificate from fulcio") + ) + return &ffcli.Command{ + Name: "attest", + ShortUsage: "cosign attest -key | [-predicate ] [-a key=value] [-upload=true|false] [-f] [-r] ", + ShortHelp: `Attest the supplied container image.`, + LongHelp: `Attest the supplied container image. + +EXAMPLES + # attach an attestation to a container image Google sign-in (experimental) + COSIGN_EXPERIMENTAL=1 cosign attest -attestation + + # attach an attestation to a container image with a local key pair file + cosign attest -attestation -key cosign.key + + # attach an attestation to a container image with a key pair stored in Azure Key Vault + cosign attest -attestation -key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + + # attach an attestation to a container image with a key pair stored in AWS KMS + cosign attest -attestation -key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + + # attach an attestation to a container image with a key pair stored in Google Cloud KMS + cosign attest -attestation -key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] + + # attach an attestation to a container image with a key pair stored in Hashicorp Vault + cosign attest -attestation -key hashivault://[KEY] + + # attach an attestation to a container image which does not fully support OCI media types + COSIGN_DOCKER_MEDIA_TYPES=1 cosign attest -attestation -key cosign.key legacy-registry.example.com/my/image + `, + FlagSet: flagset, + Exec: func(ctx context.Context, args []string) error { + if len(args) == 0 { + return flag.ErrHelp + } + + ko := KeyOpts{ + KeyRef: *key, + PassFunc: GetPass, + Sk: *sk, + Slot: *slot, + IDToken: *idToken, + } + for _, img := range args { + if err := AttestCmd(ctx, ko, img, *cert, *upload, *predicatePath, *force); err != nil { + return errors.Wrapf(err, "signing %s", img) + } + } + return nil + }, + } +} + +const intotoPayloadType = "application/vnd.in-toto+json" + +func AttestCmd(ctx context.Context, ko KeyOpts, imageRef string, certPath string, + upload bool, predicatePath string, force bool) error { + + // A key file or token is required unless we're in experimental mode! + if EnableExperimental() { + if nOf(ko.KeyRef, ko.Sk) > 1 { + return &KeyParseError{} + } + } else { + if !oneOf(ko.KeyRef, ko.Sk) { + return &KeyParseError{} + } + } + + remoteOpts := DefaultRegistryClientOpts(ctx) + + ref, err := name.ParseReference(imageRef) + if err != nil { + return errors.Wrap(err, "parsing reference") + } + get, err := remote.Get(ref, remoteOpts...) + if err != nil { + return errors.Wrap(err, "getting remote image") + } + + repo := ref.Context() + img := repo.Digest(get.Digest.String()) + + sv, err := signerFromKeyOpts(ctx, certPath, ko) + if err != nil { + return errors.Wrap(err, "getting signer") + } + wrapped := dsse.WrapSigner(sv, intotoPayloadType) + + fmt.Fprintln(os.Stderr, "Using payload from:", predicatePath) + rawPayload, err := ioutil.ReadFile(filepath.Clean(predicatePath)) + if err != nil { + return errors.Wrap(err, "payload from file") + } + + sh := in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "cosign.sigstore.dev/attestation/v1", + Subject: []in_toto.Subject{ + { + Name: repo.String(), + Digest: map[string]string{ + "sha256": get.Digest.Hex, + }, + }, + }, + }, + Predicate: CosignAttestation{ + Data: string(rawPayload), + }, + } + + payload, err := json.Marshal(sh) + if err != nil { + return err + } + sig, err := wrapped.SignMessage(bytes.NewReader(payload), options.WithContext(ctx)) + if err != nil { + return errors.Wrap(err, "signing") + } + + if !upload { + fmt.Println(base64.StdEncoding.EncodeToString(sig)) + return nil + } + + sigRepo, err := TargetRepositoryForImage(ref) + if err != nil { + return err + } + imgHash, err := ggcrV1.NewHash(img.Identifier()) + if err != nil { + return err + } + attRef := cosign.AttachedImageTag(sigRepo, &remote.Descriptor{ + Descriptor: ggcrV1.Descriptor{ + Digest: imgHash, + }, + }, cosign.AttestationTagSuffix) + + uo := cremote.UploadOpts{ + Cert: sv.Cert, + Chain: sv.Chain, + DupeDetector: sv, + RemoteOpts: remoteOpts, + } + + uploadTLog, err := shouldUploadToTlog(ref, force, ko.RekorURL) + if err != nil { + return err + } + + if uploadTLog { + var rekorBytes []byte + + // Upload the cert or the public key, depending on what we have + if sv.Cert != nil { + rekorBytes = sv.Cert + } else { + pemBytes, err := cosign.PublicKeyPem(sv, options.WithContext(ctx)) + if err != nil { + return err + } + rekorBytes = pemBytes + } + rekorClient, err := rekorClient.GetRekorClient(ko.RekorURL) + if err != nil { + return err + } + entry, err := cosign.UploadAttestationTLog(rekorClient, sig, rekorBytes) + if err != nil { + return err + } + fmt.Println("tlog entry created with index: ", *entry.LogIndex) + + uo.Bundle = bundle(entry) + uo.AdditionalAnnotations = parseAnnotations(entry) + } + + fmt.Fprintln(os.Stderr, "Pushing attestation to:", attRef.String()) + if _, err = cremote.UploadSignature(sig, payload, attRef, uo); err != nil { + return errors.Wrap(err, "uploading") + } + + return nil +} + +type CosignAttestation struct { + Data string +} diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 7c3c16174fb..3a6719e9f92 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -77,6 +77,31 @@ func (a *annotationsMap) String() string { return strings.Join(s, ",") } +func shouldUploadToTlog(ref name.Reference, force bool, url string) (bool, error) { + // Check if the image is public (no auth in Get) + if !EnableExperimental() { + return false, nil + } + // Experimental is on! + if force { + return true, nil + } + + if _, err := remote.Get(ref); err != nil { + fmt.Fprintf(os.Stderr, "warning: uploading to the transparency log at %s for a private image, please confirm [Y/N]: ", url) + + var tlogConfirmResponse string + if _, err := fmt.Scanln(&tlogConfirmResponse); err != nil { + return false, err + } + if tlogConfirmResponse != "Y" { + fmt.Println("not uploading to transparency log") + return false, nil + } + } + return true, nil +} + func Sign() *ffcli.Command { var ( flagset = flag.NewFlagSet("cosign sign", flag.ExitOnError) @@ -238,36 +263,6 @@ func SignCmd(ctx context.Context, ko KeyOpts, annotations map[string]interface{} return errors.Wrap(err, "getting signer") } - // Check if the image is public (no auth in Get) - uploadTLog := EnableExperimental() - if uploadTLog && !force { - if _, err := remote.Get(ref); err != nil { - fmt.Printf("warning: uploading to the transparency log at %s for a private image, please confirm [Y/N]: ", ko.RekorURL) - - var tlogConfirmResponse string - if _, err := fmt.Scanln(&tlogConfirmResponse); err != nil { - return err - } - if tlogConfirmResponse != "Y" { - fmt.Println("not uploading to transparency log") - uploadTLog = false - } - } - } - - var rekorBytes []byte - if uploadTLog { - // Upload the cert or the public key, depending on what we have - rekorBytes = sv.Cert - if rekorBytes == nil { - pemBytes, err := cosign.PublicKeyPem(sv, options.WithContext(ctx)) - if err != nil { - return err - } - rekorBytes = pemBytes - } - } - var staticPayload []byte if payloadPath != "" { fmt.Fprintln(os.Stderr, "Using payload from:", payloadPath) @@ -323,7 +318,24 @@ func SignCmd(ctx context.Context, ko KeyOpts, annotations map[string]interface{} RemoteOpts: remoteOpts, } + // Check if the image is public (no auth in Get) + uploadTLog, err := shouldUploadToTlog(ref, force, ko.RekorURL) + if err != nil { + return err + } + if uploadTLog { + var rekorBytes []byte + // Upload the cert or the public key, depending on what we have + if sv.Cert != nil { + rekorBytes = sv.Cert + } else { + pemBytes, err := cosign.PublicKeyPem(sv, options.WithContext(ctx)) + if err != nil { + return err + } + rekorBytes = pemBytes + } rekorClient, err := rekorClient.GetRekorClient(ko.RekorURL) if err != nil { return err diff --git a/cmd/cosign/cli/verify_attestation.go b/cmd/cosign/cli/verify_attestation.go new file mode 100644 index 00000000000..286a19b7a3d --- /dev/null +++ b/cmd/cosign/cli/verify_attestation.go @@ -0,0 +1,162 @@ +// +// 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" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/peterbourgon/ff/v3/ffcli" + "github.com/pkg/errors" + + "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/fulcio" + "github.com/sigstore/cosign/pkg/cosign/pivkey" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/dsse" +) + +// VerifyAttestationCommand verifies a signature on a supplied container image +type VerifyAttestationCommand struct { + CheckClaims bool + KeyRef string + Sk bool + Slot string + Output string + FulcioURL string + RekorURL string +} + +func applyVerifyAttestationFlags(cmd *VerifyAttestationCommand, flagset *flag.FlagSet) { + flagset.StringVar(&cmd.KeyRef, "key", "", "path to the public key file, URL, KMS URI or Kubernetes Secret") + flagset.BoolVar(&cmd.Sk, "sk", false, "whether to use a hardware security key") + flagset.StringVar(&cmd.Slot, "slot", "", "security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)") + flagset.BoolVar(&cmd.CheckClaims, "check-claims", true, "whether to check the claims found") + flagset.StringVar(&cmd.Output, "output", "json", "output the signing image information. Default JSON.") + flagset.StringVar(&cmd.FulcioURL, "fulcio-url", "https://fulcio.sigstore.dev", "[EXPERIMENTAL] address of sigstore PKI server") + flagset.StringVar(&cmd.RekorURL, "rekor-url", "https://rekor.sigstore.dev", "[EXPERIMENTAL] address of rekor STL server") +} + +// Verify builds and returns an ffcli command +func VerifyAttestation() *ffcli.Command { + cmd := VerifyAttestationCommand{} + flagset := flag.NewFlagSet("cosign verify-attestation", flag.ExitOnError) + applyVerifyAttestationFlags(&cmd, flagset) + + return &ffcli.Command{ + Name: "verify-attestation", + ShortUsage: "cosign verify-attestation -key || [ ...]", + ShortHelp: "Verify an attestation on the supplied container image", + LongHelp: `Verify an attestation on an image by checking the claims +against the transparency log. + +EXAMPLES + # verify cosign attestations on the image + cosign verify-attestation + + # verify multiple images + cosign verify-attestation ... + + # additionally verify specified annotations + cosign verify-attestation -a key1=val1 -a key2=val2 + + # (experimental) additionally, verify with the transparency log + COSIGN_EXPERIMENTAL=1 cosign verify-attestation + + # verify image with public key + cosign verify-attestation -key cosign.pub + + # verify image with public key provided by URL + cosign verify-attestation -key https://host.for/ + + # verify image with public key stored in Google Cloud KMS + cosign verify-attestation -key gcpkms://projects//locations/global/keyRings//cryptoKeys/ + + # verify image with public key stored in Hashicorp Vault + cosign verify-attestation -key hashivault:/// `, + + FlagSet: flagset, + Exec: cmd.Exec, + } +} + +// Exec runs the verification command +func (c *VerifyAttestationCommand) Exec(ctx context.Context, args []string) (err error) { + if len(args) == 0 { + return flag.ErrHelp + } + + if !oneOf(c.KeyRef, c.Sk) && !EnableExperimental() { + return &KeyParseError{} + } + + co := &cosign.CheckOpts{ + RootCerts: fulcio.Roots, + RegistryClientOpts: DefaultRegistryClientOpts(ctx), + SigTagSuffixOverride: cosign.AttestationTagSuffix, + } + if c.CheckClaims { + co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier + } + if EnableExperimental() { + co.RekorURL = c.RekorURL + } + keyRef := c.KeyRef + + // Keys are optional! + var pubKey signature.Verifier + if keyRef != "" { + pubKey, err = publicKeyFromKeyRef(ctx, keyRef) + if err != nil { + return errors.Wrap(err, "loading public key") + } + } else if c.Sk { + sk, err := pivkey.GetKeyWithSlot(c.Slot) + if err != nil { + return errors.Wrap(err, "opening piv token") + } + defer sk.Close() + pubKey, err = sk.Verifier() + if err != nil { + return errors.Wrap(err, "initializing piv token verifier") + } + } + co.SigVerifier = dsse.WrapVerifier(pubKey) + + for _, imageRef := range args { + ref, err := name.ParseReference(imageRef) + if err != nil { + return err + } + sigRepo, err := TargetRepositoryForImage(ref) + if err != nil { + return err + } + co.SignatureRepo = sigRepo + //TODO: this is really confusing, it's actually a return value for the printed verification below + co.VerifyBundle = false + + verified, err := cosign.Verify(ctx, ref, co) + if err != nil { + return err + } + + PrintVerification(imageRef, verified, co, "text") + } + + return nil +} diff --git a/cmd/cosign/main.go b/cmd/cosign/main.go index 37d6de43cbf..ceaef46976a 100644 --- a/cmd/cosign/main.go +++ b/cmd/cosign/main.go @@ -49,8 +49,10 @@ func main() { // Signing cli.Verify(), cli.Sign(), + cli.Attest(), cli.Generate(), cli.SignBlob(), + cli.VerifyAttestation(), cli.VerifyBlob(), cli.VerifyDockerfile(), // Upload sub-tree diff --git a/go.mod b/go.mod index 80e8b0a4251..18aa24c9eaa 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/trillian v1.3.14-0.20210713114448-df474653733c github.com/hashicorp/vault/api v1.1.1 // indirect + github.com/in-toto/in-toto-golang v0.2.1-0.20210627200632-886210ae2ab9 github.com/manifoldco/promptui v0.8.0 github.com/open-policy-agent/opa v0.30.2 github.com/peterbourgon/ff/v3 v3.1.0 diff --git a/go.sum b/go.sum index 43f1a8e0c3f..7ada24e0a82 100644 --- a/go.sum +++ b/go.sum @@ -261,6 +261,7 @@ github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= @@ -787,6 +788,7 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/in-toto/in-toto-golang v0.2.1-0.20210627200632-886210ae2ab9 h1:j7klXz5kh0ydPmHkBtJ/Al27G1/au4sH7OkGhkgRJWg= github.com/in-toto/in-toto-golang v0.2.1-0.20210627200632-886210ae2ab9/go.mod h1:Skbg04kmfB7IAnEIsspKPg/ny1eiFt/TgPr9SDCHusA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -1125,6 +1127,7 @@ github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shibumi/go-pathspec v1.2.0 h1:KVKEDHYk7bQolRMs7nfzjT3SBOCgcXFJzccnj9bsGbA= github.com/shibumi/go-pathspec v1.2.0/go.mod h1:bDxCftD0fST3qXIlHoQ/fChsU4mWMVklXp1yPErQaaY= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sigstore/fulcio v0.0.0-20210720153316-846105495d38 h1:mEOzCQ8N5WIpi9BtClLCl3Z2nNIYBNPaAQ/wwF/p4II= diff --git a/pkg/cosign/fetch.go b/pkg/cosign/fetch.go index 8ed6b9b0250..03f848aea8f 100644 --- a/pkg/cosign/fetch.go +++ b/pkg/cosign/fetch.go @@ -51,8 +51,9 @@ type SignedPayload struct { // } const ( - SignatureTagSuffix = ".sig" - SBOMTagSuffix = ".sbom" + SignatureTagSuffix = ".sig" + SBOMTagSuffix = ".sbom" + AttestationTagSuffix = ".att" ) func AttachedImageTag(repo name.Repository, imgDesc *remote.Descriptor, tagSuffix string) name.Tag { diff --git a/pkg/cosign/upload.go b/pkg/cosign/upload.go index 81a0119dc1c..e1260b0847d 100644 --- a/pkg/cosign/upload.go +++ b/pkg/cosign/upload.go @@ -26,6 +26,7 @@ import ( "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/client/entries" "github.com/sigstore/rekor/pkg/generated/models" + intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" ) @@ -36,8 +37,21 @@ func UploadTLog(rekorClient *client.Rekor, signature, payload []byte, pemBytes [ APIVersion: swag.String(re.APIVersion()), Spec: re.RekordObj, } + return doUpload(rekorClient, &returnVal) +} + +func UploadAttestationTLog(rekorClient *client.Rekor, signature, pemBytes []byte) (*models.LogEntryAnon, error) { + e := intotoEntry(signature, pemBytes) + returnVal := models.Intoto{ + APIVersion: swag.String(e.APIVersion()), + Spec: e.IntotoObj, + } + return doUpload(rekorClient, &returnVal) +} + +func doUpload(rekorClient *client.Rekor, pe models.ProposedEntry) (*models.LogEntryAnon, error) { params := entries.NewCreateLogEntryParams() - params.SetProposedEntry(&returnVal) + params.SetProposedEntry(pe) resp, err := rekorClient.Entries.CreateLogEntry(params) if err != nil { // If the entry already exists, we get a specific error. @@ -58,6 +72,18 @@ func UploadTLog(rekorClient *client.Rekor, signature, payload []byte, pemBytes [ return nil, errors.New("bad response from server") } +func intotoEntry(signature, pubKey []byte) intoto_v001.V001Entry { + pub := strfmt.Base64(pubKey) + return intoto_v001.V001Entry{ + IntotoObj: models.IntotoV001Schema{ + Content: &models.IntotoV001SchemaContent{ + Envelope: string(signature), + }, + PublicKey: &pub, + }, + } +} + func rekorEntry(payload, signature, pubKey []byte) rekord_v001.V001Entry { return rekord_v001.V001Entry{ RekordObj: models.RekordV001Schema{ diff --git a/pkg/cosign/verifiers.go b/pkg/cosign/verifiers.go index 0155c8c3cc6..ad509e0ce81 100644 --- a/pkg/cosign/verifiers.go +++ b/pkg/cosign/verifiers.go @@ -19,6 +19,7 @@ import ( "encoding/json" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/sigstore/sigstore/pkg/signature/payload" ) @@ -40,3 +41,22 @@ func SimpleClaimVerifier(sp SignedPayload, desc *v1.Descriptor, annotations map[ } return nil } + +func IntotoSubjectClaimVerifier(sp SignedPayload, desc *v1.Descriptor, _ map[string]interface{}) error { + st := &in_toto.Statement{} + if err := json.Unmarshal(sp.Payload, st); err != nil { + return err + } + + for _, subj := range st.StatementHeader.Subject { + dgst, ok := subj.Digest["sha256"] + if !ok { + continue + } + subjDigest := "sha256:" + dgst + if subjDigest == desc.Digest.String() { + return nil + } + } + return errors.New("no matching subject digest found") +} diff --git a/test/e2e_test.go b/test/e2e_test.go index a4aa6ff76df..d5a9ac90566 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -143,6 +143,43 @@ func TestSignVerifyClean(t *testing.T) { mustErr(verify(pubKeyPath, imgName, true, nil), t) } +func TestAttestVerify(t *testing.T) { + repo, stop := reg(t) + defer stop() + td := t.TempDir() + + imgName := path.Join(repo, "cosign-attest-e2e") + + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + _, privKeyPath, pubKeyPath := keypair(t, td) + + ctx := context.Background() + + // Verify should fail at first + verifyAttestation := cli.VerifyAttestationCommand{ + KeyRef: pubKeyPath, + } + + attestation := "helloworld" + ap := filepath.Join(td, "attestation") + if err := ioutil.WriteFile(ap, []byte(attestation), 0600); err != nil { + t.Fatal(err) + } + + mustErr(verifyAttestation.Exec(ctx, []string{imgName}), t) + + // Now attest the image + ko := cli.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + must(cli.AttestCmd(ctx, ko, imgName, "", true, ap, false), t) + + // Now verify and download should work! + must(verifyAttestation.Exec(ctx, []string{imgName}), t) + // Look for a specific annotation + mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}), t) +} + func TestBundle(t *testing.T) { // turn on the tlog defer setenv(t, cli.ExperimentalEnv, "1")()