Skip to content

Commit 287c142

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 287c142

File tree

5 files changed

+155
-2
lines changed

5 files changed

+155
-2
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: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,22 @@ func runCommand() error {
8080
defer store.Close()
8181

8282
// Get list of identities
83-
idents, err = store.Identities()
83+
pivIdents, err := PivIdentities()
84+
if err != nil {
85+
fmt.Fprintln(os.Stderr, "skipping hardware keys")
86+
}
87+
for _, pivIdent := range pivIdents {
88+
idents = append(idents, &pivIdent)
89+
}
90+
91+
storeIdents, err := store.Identities()
8492
if err != nil {
8593
return errors.Wrap(err, "failed to get identities from certificate store")
8694
}
95+
for _, ident := range storeIdents {
96+
idents = append(idents, ident)
97+
}
98+
8799
for _, ident := range idents {
88100
defer ident.Close()
89101
}

piv_identity.go

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

0 commit comments

Comments
 (0)