diff --git a/command_sign_test.go b/command_sign_test.go index 85c9c4d..84364f8 100644 --- a/command_sign_test.go +++ b/command_sign_test.go @@ -4,8 +4,8 @@ import ( "crypto/x509" "testing" - "github.com/github/ietf-cms/protocol" "github.com/github/ietf-cms" + "github.com/github/ietf-cms/protocol" "github.com/stretchr/testify/require" ) diff --git a/go.mod b/go.mod index 514abfd..e1d79f8 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,11 @@ require ( github.com/github/certstore v0.1.0 github.com/github/fakeca v0.1.0 github.com/github/ietf-cms v0.1.0 + github.com/go-piv/piv-go v1.7.0 // indirect github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b github.com/pkg/errors v0.8.1 github.com/pmezard/go-difflib v1.0.0 github.com/stretchr/testify v1.3.0 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 + golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect ) diff --git a/go.sum b/go.sum index cf43f3c..9f530b2 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/github/ietf-cms v0.1.0 h1:D+O9re6xDeWTYRpAFTfM0dm5NqJUcXZKFGOQg5Iq6Ls= github.com/github/ietf-cms v0.1.0/go.mod h1:eJEmhqWUqjpuS6OoXiqtuTmzOx4u81npQrXOzt/sPqo= +github.com/go-piv/piv-go v1.7.0 h1:rfjdFdASfGV5KLJhSjgpGJ5lzVZVtRWn8ovy/H9HQ/U= +github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b h1:K1wa7ads2Bu1PavI6LfBRMYSy6Zi+Rky0OhWBfrmkmY= github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= @@ -27,4 +29,8 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8U golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 8d57535..654d389 100644 --- a/main.go +++ b/main.go @@ -84,6 +84,15 @@ func runCommand() error { if err != nil { return errors.Wrap(err, "failed to get identities from certificate store") } + + pivIdents, err := PivIdentities() + if err != nil { + fmt.Fprintln(os.Stderr, "skipping hardware keys") + } + for _, pivIdent := range pivIdents { + idents = append(idents, &pivIdent) + } + for _, ident := range idents { defer ident.Close() } diff --git a/piv_identity.go b/piv_identity.go new file mode 100644 index 0000000..defa6e8 --- /dev/null +++ b/piv_identity.go @@ -0,0 +1,112 @@ +package main + +import ( + "crypto" + "crypto/x509" + "fmt" + "github.com/github/certstore" + "github.com/go-piv/piv-go/piv" + "github.com/pkg/errors" + "golang.org/x/term" + "io" +) + +// PivIdentities enumerates identities stored in the signature slot inside hardware keys +func PivIdentities() ([]PivIdentity, error) { + cards, err := piv.Cards() + if err != nil { + return nil, err + } + var identities []PivIdentity + for _, card := range cards { + yk, err := piv.Open(card) + if err != nil { + continue + } + cert, err := yk.Certificate(piv.SlotSignature) + if err != nil { + continue + } + if cert != nil { + pin := promptHardwareKeyPin(card) + ident := PivIdentity{card: card, pin: pin, yk: yk} + identities = append(identities, ident) + } + } + return identities, nil +} + +// PivIdentity is an entity identity stored in a hardware key PIV applet +type PivIdentity struct { + card string + pin string + yk *piv.YubiKey +} + +var _ certstore.Identity = (*PivIdentity)(nil) +var _ crypto.Signer = (*PivIdentity)(nil) + +// Certificate implements the certstore.Identity interface +func (ident *PivIdentity) Certificate() (*x509.Certificate, error) { + return ident.yk.Certificate(piv.SlotSignature) +} + +// CertificateChain implements the certstore.Identity interface +func (ident *PivIdentity) CertificateChain() ([]*x509.Certificate, error) { + return []*x509.Certificate{}, nil +} + +// Signer implements the certstore.Identity interface +func (ident *PivIdentity) Signer() (crypto.Signer, error) { + return ident, nil +} + +// Delete implements the certstore.Identity interface +func (ident *PivIdentity) Delete() error { + panic("deleting identities on PIV applet is not supported") +} + +// Close implements the certstore.Identity interface +func (ident *PivIdentity) Close() { + _ = ident.yk.Close() +} + +// Public implements the crypto.Signer interface +func (ident *PivIdentity) Public() crypto.PublicKey { + cert, err := ident.Certificate() + if err != nil { + return nil + } + return cert.PublicKey +} + +// Sign implements the crypto.Signer interface +func (ident *PivIdentity) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + fmt.Printf("Touch \"%v\" now to sign\n", ident.card) + private, err := ident.yk.PrivateKey(piv.SlotSignature, ident.Public(), piv.KeyAuth{PIN: ident.pin}) + if err != nil { + return nil, errors.Wrap(err, "failed to get private key for signing") + } + switch private.(type) { + case *piv.ECDSAPrivateKey: + return private.(*piv.ECDSAPrivateKey).Sign(rand, digest, opts) + default: + return nil, fmt.Errorf("invalid key type") + } +} + +func promptHardwareKeyPin(name string) string { + fmt.Printf("enter PIN for hardware key \"%v\" (press enetr for default):\n", name) + pin, err := term.ReadPassword(0) + var hardwareKeyPin string + if err != nil { + hardwareKeyPin = piv.DefaultPIN + } else { + if len(pin) > 0 { + hardwareKeyPin = string(pin) + } else { + hardwareKeyPin = piv.DefaultPIN + } + } + return hardwareKeyPin +}