Skip to content

Commit

Permalink
Add explicit support for identities stored on hardware keys (like Yub…
Browse files Browse the repository at this point in the history
…ikey).

Since Apple no longer support enumerating certificates stored on hardware keys in the Keychain Access application,
this PR explicitly tries enumerate certificates stored in the "signature" slot for hardware keys that support PIV applets.

Implementation details:
- The hardware key PIN is prompted for at the beginning to make sure we don't interfere with the output git expects while signing
- To make this as easy as possible, this PR adds a new struct called `PivIdentity` which implements `certstore.Identity` interface
- The `PivIdentity` struct has an open handle to a `*piv.Yubikey` and needs to be closed properly when done using it
  • Loading branch information
yoavamit committed May 17, 2021
1 parent 3e90229 commit 9d625ce
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 1 deletion.
2 changes: 1 addition & 1 deletion command_sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
9 changes: 9 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
112 changes: 112 additions & 0 deletions piv_identity.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 9d625ce

Please sign in to comment.