Skip to content

Commit

Permalink
cmd/sign: Add -cert flag (#451)
Browse files Browse the repository at this point in the history
* pivkey: Refactor

This refactor abstracts away the need to directly call the upstream
piv-go library, implements a common SignerVerifier interface so there
is no need to maintain separate types for ECDSA and RSA private keys,
and overall is more idiomatic. Additionally, it fixes an issue where the
previous initialization functions would lock access to the PIV token,
preventing subsequent access even if the originall caller no longer had
a need to access the token.

Signed-off-by: James Alseth <james@jalseth.me>

* cmd/sign: Add -cert flag

The `-cert` flag allows for the certificate to be included in the
Signature object when signing with `-key` or `-sk`. Adding this
certificate ensures that a verifying party does not need to fetch the
certificate from some unknown location before being able to validate the
identity of the actor that created the signature.

Signed-off-by: James Alseth <james@jalseth.me>
  • Loading branch information
jalseth committed Jul 19, 2021
1 parent fd17d7f commit 9adaad5
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 137 deletions.
11 changes: 3 additions & 8 deletions cmd/cosign/cli/pivcli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,22 +160,17 @@ func (a *Attestations) Output() {
}

func AttestationCmd(_ context.Context, slotArg string) (*Attestations, error) {
slot := pivkey.SlotForName(slotArg)
if slot == nil {
return nil, flag.ErrHelp
}

yk, err := pivkey.GetKey()
yk, err := pivkey.GetKeyWithSlot(slotArg)
if err != nil {
return nil, err
}
defer yk.Close()
deviceCert, err := yk.AttestationCertificate()
deviceCert, err := yk.GetAttestationCertificate()
if err != nil {
return nil, err
}

keyCert, err := yk.Attest(*slot)
keyCert, err := yk.Attest()
if err != nil {
return nil, err
}
Expand Down
12 changes: 9 additions & 3 deletions cmd/cosign/cli/public_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os"

"github.com/peterbourgon/ff/v3/ffcli"
"github.com/pkg/errors"

"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/pivkey"
Expand Down Expand Up @@ -117,11 +118,16 @@ func GetPublicKey(ctx context.Context, opts Pkopts, writer NamedWriter, pf cosig
}
k = s
case opts.Sk:
sk, err := pivkey.NewPublicKeyProvider(opts.Slot)
sk, err := pivkey.GetKeyWithSlot(opts.Slot)
if err != nil {
return err
return errors.Wrap(err, "opening piv token")
}
defer sk.Close()
pk, err := sk.Verifier()
if err != nil {
return errors.Wrap(err, "initializing piv token verifier")
}
k = sk
k = pk
}

pemBytes, err := cosign.PublicKeyPem(k, options.WithContext(ctx))
Expand Down
71 changes: 66 additions & 5 deletions cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ package cli
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/rsa"
_ "crypto/sha256" // for `crypto.SHA256`
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -76,6 +80,7 @@ func Sign() *ffcli.Command {
var (
flagset = flag.NewFlagSet("cosign sign", 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)")
Expand Down Expand Up @@ -135,7 +140,7 @@ EXAMPLES
IDToken: *idToken,
}
for _, img := range args {
if err := SignCmd(ctx, so, img, *upload, *payloadPath, *force, *recursive); err != nil {
if err := SignCmd(ctx, so, img, *cert, *upload, *payloadPath, *force, *recursive); err != nil {
return errors.Wrapf(err, "signing %s", img)
}
}
Expand Down Expand Up @@ -189,7 +194,7 @@ func getTransitiveImages(rootIndex *remote.Descriptor, repo name.Repository, opt
}

func SignCmd(ctx context.Context, so SignOpts,
imageRef string, upload bool, payloadPath string, force bool, recursive bool) error {
imageRef string, certPath string, upload bool, payloadPath string, force bool, recursive bool) error {

// A key file or token is required unless we're in experimental mode!
if EnableExperimental() {
Expand Down Expand Up @@ -234,19 +239,75 @@ func SignCmd(ctx context.Context, so SignOpts,
var cert, chain string
switch {
case so.Sk:
sk, err := pivkey.NewSignerVerifier(so.Slot)
sk, err := pivkey.GetKeyWithSlot(so.Slot)
defer sk.Close()
if err != nil {
return err
}
signer = sk
dupeDetector = sk
skSigner, err := sk.SignerVerifier()
if err != nil {
return err
}
signer = skSigner
dupeDetector = skSigner

// Handle the -cert flag.
// With PIV, we assume the certificate is in the same slot on the PIV
// token as the private key. If it's not there, show a warning to the
// user.
certFromPIV, err := sk.Certificate()
if err != nil {
fmt.Fprintln(os.Stderr, "warning: no x509 certificate retrieved from the PIV token")
break
}
cert = string(cosign.CertToPem(certFromPIV))

case so.KeyRef != "":
k, err := signerVerifierFromKeyRef(ctx, so.KeyRef, so.Pf)
if err != nil {
return errors.Wrap(err, "reading key")
}
signer = k
dupeDetector = k

// Handle the -cert flag
if certPath == "" {
break
}
certBytes, err := ioutil.ReadFile(certPath)
if err != nil {
return errors.Wrap(err, "read certificate")
}
// Handle PEM.
if bytes.HasPrefix(certBytes, []byte("-----")) {
decoded, _ := pem.Decode(certBytes)
if decoded.Type != "CERTIFICATE" {
return fmt.Errorf("supplied PEM file is not a certificate: %s", certPath)
}
certBytes = decoded.Bytes
}
parsedCert, err := x509.ParseCertificate(certBytes)
if err != nil {
return errors.Wrap(err, "parse x509 certificate")
}
pk, err := k.PublicKey()
if err != nil {
return errors.Wrap(err, "get public key")
}
switch kt := parsedCert.PublicKey.(type) {
case *ecdsa.PublicKey:
if !kt.Equal(pk) {
return errors.New("public key in certificate does not match that in the signing key")
}
case *rsa.PublicKey:
if !kt.Equal(pk) {
return errors.New("public key in certificate does not match that in the signing key")
}
default:
return fmt.Errorf("unsupported key type: %T", parsedCert.PublicKey)
}
cert = string(cosign.CertToPem(parsedCert))

default: // Keyless!
fmt.Fprintln(os.Stderr, "Generating ephemeral keys...")
k, err := fulcio.NewSigner(ctx, so.IDToken)
Expand Down
9 changes: 7 additions & 2 deletions cmd/cosign/cli/sign_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,14 @@ func SignBlobCmd(ctx context.Context, ko KeyOpts, payloadPath string, b64 bool,
}
signer = k
case ko.Sk:
k, err := pivkey.NewSignerVerifier(ko.Slot)
sk, err := pivkey.GetKeyWithSlot(ko.Slot)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "opening piv token")
}
defer sk.Close()
k, err := sk.SignerVerifier()
if err != nil {
return nil, errors.Wrap(err, "initializing signer on piv token")
}
signer = k
default:
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, false)
err := SignCmd(ctx, so, "", "", false, "", false, false)
if (errors.Is(err, &KeyParseError{}) == false) {
t.Fatal("expected KeyParseError")
}
Expand Down
9 changes: 7 additions & 2 deletions cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,14 @@ func (c *VerifyCommand) Exec(ctx context.Context, args []string) (err error) {
return errors.Wrap(err, "loading public key")
}
} else if c.Sk {
pubKey, err = pivkey.NewPublicKeyProvider(c.Slot)
sk, err := pivkey.GetKeyWithSlot(c.Slot)
if err != nil {
return errors.Wrap(err, "initializing security key")
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 = pubKey
Expand Down
7 changes: 6 additions & 1 deletion cmd/cosign/cli/verify_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ func VerifyBlobCmd(ctx context.Context, ko KeyOpts, certRef, sigRef, blobRef str
return errors.Wrap(err, "loading public key")
}
case ko.Sk:
pubKey, err = pivkey.NewPublicKeyProvider(ko.Slot)
sk, err := pivkey.GetKeyWithSlot(ko.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, "loading public key from token")
}
Expand Down
60 changes: 45 additions & 15 deletions pkg/cosign/pivkey/disabled.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,72 @@
package pivkey

import (
"context"
"crypto"
"crypto/x509"
"errors"
"io"

"github.com/sigstore/sigstore/pkg/signature"
)

func NewPublicKeyProvider(slotName string) (signature.Verifier, error) {
// The empty struct is used so this file never imports piv-go which is
// dependent on cgo and will fail to build if imported.
type empty struct{} //nolint

type Key struct{}

func GetKey() (*Key, error) {
return nil, errors.New("unimplemented")
}

func GetKeyWithSlot(slot string) (*Key, error) {
return nil, errors.New("unimplemented")
}

func NewSigner() (signature.Signer, error) {
func (k *Key) Close() {}

func (k *Key) Authenticate(pin string) {}

func (k *Key) SetSlot(slot string) {}

func (k *Key) Attest() (*x509.Certificate, error) {
return nil, errors.New("unimplemented")
}

type PIVSigner struct {
Priv crypto.PrivateKey
Pub crypto.PrivateKey
signature.ECDSAVerifier
func (k *Key) GetAttestationCertificate() (*x509.Certificate, error) {
return nil, errors.New("unimplemented")
}

func (ps *PIVSigner) Sign(ctx context.Context, rawPayload []byte) ([]byte, []byte, error) {
return nil, nil, errors.New("unimplemented")
func (k *Key) SetManagementKey(old, new [24]byte) error {
return errors.New("unimplemented")
}

func (ps *PIVSigner) SignMessage(rawPayload io.Reader, opts ...signature.SignOption) ([]byte, error) {
func (k *Key) SetPIN(old, new string) error {
return errors.New("unimplemented")
}

func (k *Key) SetPUK(old, new string) error {
return errors.New("unimplemented")
}

func (k *Key) Reset() error {
return errors.New("unimplemented")
}

func (k *Key) Unblock(puk, newPIN string) error {
return errors.New("unimplemented")
}

func (k *Key) GenerateKey(mgmtKey [24]byte, slot *empty, opts *empty) (*empty, error) { //nolint
return nil, errors.New("unimplemented")
}

func (ps *PIVSigner) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) {
func (k *Key) Verifier() (signature.Verifier, error) {
return nil, errors.New("unimplemented")
}

var _ signature.Signer = &PIVSigner{}
func (k *Key) Certificate() (*x509.Certificate, error) {
return nil, errors.New("unimplemented")
}

func NewSignerVerifier(slotName string) (signature.SignerVerifier, error) {
func (k *Key) SignerVerifier() (signature.SignerVerifier, error) {
return nil, errors.New("unimplemented")
}
Loading

0 comments on commit 9adaad5

Please sign in to comment.