Skip to content

Commit 9d625ce

Browse files
committed
Add explicit support for identities stored on hardware keys (like Yubikey).
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
1 parent 3e90229 commit 9d625ce

File tree

5 files changed

+130
-1
lines changed

5 files changed

+130
-1
lines changed

command_sign_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import (
44
"crypto/x509"
55
"testing"
66

7-
"github.com/github/ietf-cms/protocol"
87
"github.com/github/ietf-cms"
8+
"github.com/github/ietf-cms/protocol"
99
"github.com/stretchr/testify/require"
1010
)
1111

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ require (
88
github.com/github/certstore v0.1.0
99
github.com/github/fakeca v0.1.0
1010
github.com/github/ietf-cms v0.1.0
11+
github.com/go-piv/piv-go v1.7.0 // indirect
1112
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b
1213
github.com/pkg/errors v0.8.1
1314
github.com/pmezard/go-difflib v1.0.0
1415
github.com/stretchr/testify v1.3.0
1516
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
17+
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
1618
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
99
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
1010
github.com/github/ietf-cms v0.1.0 h1:D+O9re6xDeWTYRpAFTfM0dm5NqJUcXZKFGOQg5Iq6Ls=
1111
github.com/github/ietf-cms v0.1.0/go.mod h1:eJEmhqWUqjpuS6OoXiqtuTmzOx4u81npQrXOzt/sPqo=
12+
github.com/go-piv/piv-go v1.7.0 h1:rfjdFdASfGV5KLJhSjgpGJ5lzVZVtRWn8ovy/H9HQ/U=
13+
github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk=
1214
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b h1:K1wa7ads2Bu1PavI6LfBRMYSy6Zi+Rky0OhWBfrmkmY=
1315
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
1416
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
2729
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
2830
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
2931
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
32+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
33+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
34+
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
35+
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
3036
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ func runCommand() error {
8484
if err != nil {
8585
return errors.Wrap(err, "failed to get identities from certificate store")
8686
}
87+
88+
pivIdents, err := PivIdentities()
89+
if err != nil {
90+
fmt.Fprintln(os.Stderr, "skipping hardware keys")
91+
}
92+
for _, pivIdent := range pivIdents {
93+
idents = append(idents, &pivIdent)
94+
}
95+
8796
for _, ident := range idents {
8897
defer ident.Close()
8998
}

piv_identity.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package main
2+
3+
import (
4+
"crypto"
5+
"crypto/x509"
6+
"fmt"
7+
"github.com/github/certstore"
8+
"github.com/go-piv/piv-go/piv"
9+
"github.com/pkg/errors"
10+
"golang.org/x/term"
11+
"io"
12+
)
13+
14+
// PivIdentities enumerates identities stored in the signature slot inside hardware keys
15+
func PivIdentities() ([]PivIdentity, error) {
16+
cards, err := piv.Cards()
17+
if err != nil {
18+
return nil, err
19+
}
20+
var identities []PivIdentity
21+
for _, card := range cards {
22+
yk, err := piv.Open(card)
23+
if err != nil {
24+
continue
25+
}
26+
cert, err := yk.Certificate(piv.SlotSignature)
27+
if err != nil {
28+
continue
29+
}
30+
if cert != nil {
31+
pin := promptHardwareKeyPin(card)
32+
ident := PivIdentity{card: card, pin: pin, yk: yk}
33+
identities = append(identities, ident)
34+
}
35+
}
36+
return identities, nil
37+
}
38+
39+
// PivIdentity is an entity identity stored in a hardware key PIV applet
40+
type PivIdentity struct {
41+
card string
42+
pin string
43+
yk *piv.YubiKey
44+
}
45+
46+
var _ certstore.Identity = (*PivIdentity)(nil)
47+
var _ crypto.Signer = (*PivIdentity)(nil)
48+
49+
// Certificate implements the certstore.Identity interface
50+
func (ident *PivIdentity) Certificate() (*x509.Certificate, error) {
51+
return ident.yk.Certificate(piv.SlotSignature)
52+
}
53+
54+
// CertificateChain implements the certstore.Identity interface
55+
func (ident *PivIdentity) CertificateChain() ([]*x509.Certificate, error) {
56+
return []*x509.Certificate{}, nil
57+
}
58+
59+
// Signer implements the certstore.Identity interface
60+
func (ident *PivIdentity) Signer() (crypto.Signer, error) {
61+
return ident, nil
62+
}
63+
64+
// Delete implements the certstore.Identity interface
65+
func (ident *PivIdentity) Delete() error {
66+
panic("deleting identities on PIV applet is not supported")
67+
}
68+
69+
// Close implements the certstore.Identity interface
70+
func (ident *PivIdentity) Close() {
71+
_ = ident.yk.Close()
72+
}
73+
74+
// Public implements the crypto.Signer interface
75+
func (ident *PivIdentity) Public() crypto.PublicKey {
76+
cert, err := ident.Certificate()
77+
if err != nil {
78+
return nil
79+
}
80+
return cert.PublicKey
81+
}
82+
83+
// Sign implements the crypto.Signer interface
84+
func (ident *PivIdentity) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
85+
fmt.Printf("Touch \"%v\" now to sign\n", ident.card)
86+
private, err := ident.yk.PrivateKey(piv.SlotSignature, ident.Public(), piv.KeyAuth{PIN: ident.pin})
87+
if err != nil {
88+
return nil, errors.Wrap(err, "failed to get private key for signing")
89+
}
90+
switch private.(type) {
91+
case *piv.ECDSAPrivateKey:
92+
return private.(*piv.ECDSAPrivateKey).Sign(rand, digest, opts)
93+
default:
94+
return nil, fmt.Errorf("invalid key type")
95+
}
96+
}
97+
98+
func promptHardwareKeyPin(name string) string {
99+
fmt.Printf("enter PIN for hardware key \"%v\" (press enetr for default):\n", name)
100+
pin, err := term.ReadPassword(0)
101+
var hardwareKeyPin string
102+
if err != nil {
103+
hardwareKeyPin = piv.DefaultPIN
104+
} else {
105+
if len(pin) > 0 {
106+
hardwareKeyPin = string(pin)
107+
} else {
108+
hardwareKeyPin = piv.DefaultPIN
109+
}
110+
}
111+
return hardwareKeyPin
112+
}

0 commit comments

Comments
 (0)