Skip to content

Commit

Permalink
Add "cosign attest" command!
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Lorenc <dlorenc@google.com>
  • Loading branch information
Dan Lorenc committed Jul 22, 2021
1 parent d401496 commit ca01c17
Show file tree
Hide file tree
Showing 11 changed files with 562 additions and 33 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file> -key cosign.pub <image>
```

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 <image>
```

## Detailed Usage

See the [Usage documentation](USAGE.md) for more commands!
Expand Down
245 changes: 245 additions & 0 deletions cmd/cosign/cli/attest.go
Original file line number Diff line number Diff line change
@@ -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 <key path>|<kms uri> [-predicate <path>] [-a key=value] [-upload=true|false] [-f] [-r] <image uri>",
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 <FILE> <IMAGE>
# attach an attestation to a container image with a local key pair file
cosign attest -attestation <FILE> -key cosign.key <IMAGE>
# attach an attestation to a container image with a key pair stored in Azure Key Vault
cosign attest -attestation <FILE> -key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] <IMAGE>
# attach an attestation to a container image with a key pair stored in AWS KMS
cosign attest -attestation <FILE> -key awskms://[ENDPOINT]/[ID/ALIAS/ARN] <IMAGE>
# attach an attestation to a container image with a key pair stored in Google Cloud KMS
cosign attest -attestation <FILE> -key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] <IMAGE>
# attach an attestation to a container image with a key pair stored in Hashicorp Vault
cosign attest -attestation <FILE> -key hashivault://[KEY] <IMAGE>
# attach an attestation to a container image which does not fully support OCI media types
COSIGN_DOCKER_MEDIA_TYPES=1 cosign attest -attestation <FILE> -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
}
72 changes: 42 additions & 30 deletions cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit ca01c17

Please sign in to comment.