-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add explicit support for identities stored on hardware keys (like Yub…
…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
Showing
10 changed files
with
312 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package pinentry | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
) | ||
|
||
// Pinentry gets the PIN from the user to access the smart card or hardware key | ||
type Pinentry struct { | ||
path string | ||
} | ||
|
||
// NewPinentry initializes the pinentry program used to get the PIN | ||
func NewPinentry() (*Pinentry, error) { | ||
fromEnv := os.Getenv("SMIMESIGM_PINENTRY") | ||
if len(fromEnv) > 0 { | ||
pinentryFromEnv, err := exec.LookPath(fromEnv) | ||
if err == nil && len(pinentryFromEnv) > 0 { | ||
return &Pinentry{path: pinentryFromEnv}, nil | ||
} | ||
} | ||
|
||
executables := pinentryPaths() | ||
for _, programName := range executables { | ||
pinentry, err := exec.LookPath(programName) | ||
if err == nil && len(pinentry) > 0 { | ||
return &Pinentry{path: pinentry}, nil | ||
} | ||
} | ||
|
||
return nil, fmt.Errorf("failed to find suitable program to enter pin") | ||
} | ||
|
||
// Get executes the pinentry program and returns the PIN entered by the user | ||
// see https://www.gnupg.org/documentation/manuals/assuan/Introduction.html for more details | ||
func (pin *Pinentry) Get(prompt string) (string, error) { | ||
cmd := exec.Command(pin.path) | ||
stdin, err := cmd.StdinPipe() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
stdout, err := cmd.StdoutPipe() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
err = cmd.Start() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
bufferReader := bufio.NewReader(stdout) | ||
lineBytes, _, err := bufferReader.ReadLine() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
line := string(lineBytes) | ||
if !strings.HasPrefix(line, "OK") { | ||
return "", fmt.Errorf("failed to initialize pinentry, got response: %v", line) | ||
} | ||
|
||
terminal := os.Getenv("TERM") | ||
if len(terminal) > 0 { | ||
if ok := setOption(stdin, bufferReader, fmt.Sprintf("OPTION ttytype=%s\n", terminal)); !ok { | ||
return "", fmt.Errorf("failed to set ttytype") | ||
} | ||
} | ||
|
||
if ok := setOption(stdin, bufferReader, fmt.Sprintf("OPTION ttyname=%v\n", tty())); !ok { | ||
return "", fmt.Errorf("failed to set ttyname") | ||
} | ||
|
||
if ok := setOption(stdin, bufferReader, "SETPROMPT PIN:\n"); !ok { | ||
return "", fmt.Errorf("failed to set prompt") | ||
} | ||
if ok := setOption(stdin, bufferReader, "SETTITLE smimesign\n"); !ok { | ||
return "", fmt.Errorf("failed to set title") | ||
} | ||
if ok := setOption(stdin, bufferReader, fmt.Sprintf("SETDESC %s\n", prompt)); !ok { | ||
return "", fmt.Errorf("failed to set description") | ||
} | ||
|
||
_, err = fmt.Fprint(stdin, "GETPIN\n") | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
lineBytes, _, err = bufferReader.ReadLine() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
line = string(lineBytes) | ||
|
||
_, err = fmt.Fprint(stdin, "BYE\n") | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
if err = cmd.Wait(); err != nil { | ||
return "", err | ||
} | ||
|
||
if !strings.HasPrefix(line, "D ") { | ||
return "", fmt.Errorf(line) | ||
} | ||
|
||
return strings.TrimPrefix(line, "D "), nil | ||
} | ||
|
||
func setOption(writer io.Writer, bufferedReader *bufio.Reader, option string) bool { | ||
_, err := fmt.Fprintf(writer, option) | ||
lineBytes, _, err := bufferedReader.ReadLine() | ||
if err != nil { | ||
return false | ||
} | ||
|
||
line := string(lineBytes) | ||
if !strings.HasPrefix(line, "OK") { | ||
return false | ||
} | ||
return true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package pinentry | ||
|
||
func pinentryPaths() []string { | ||
return []string{ | ||
"pinentry-mac", | ||
"pinentry-curses", | ||
"pinentry", | ||
} | ||
} | ||
|
||
func tty() string { | ||
return "/dev/tty" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package pinentry | ||
|
||
func pinentryPaths() []string { | ||
// there are many flavours for the GnuPG pinentry program for different linux distros | ||
// this is a non-exhaustive list of some common implementations | ||
return []string{ | ||
"pinentry-gnome3", | ||
"pinentry-gtk", | ||
"pinentry-qy", | ||
"pinentry-tty", | ||
"pinentry", | ||
} | ||
} | ||
|
||
func tty() string { | ||
return "/dev/tty" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package pinentry | ||
|
||
func pinentryPaths() []string { | ||
return []string{ | ||
"pinentry-gtk-2.exe", | ||
"pinentry-qt4.exe", | ||
"pinentry-w32.exe", | ||
"pinentry.exe", | ||
} | ||
} | ||
|
||
func tty() string { | ||
return "windows" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package main | ||
|
||
import ( | ||
"crypto" | ||
"crypto/x509" | ||
"fmt" | ||
"io" | ||
|
||
"github.com/github/certstore" | ||
"github.com/github/smimesign/pinentry" | ||
"github.com/go-piv/piv-go/piv" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// 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 { | ||
ident := PivIdentity{card: card, 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) { | ||
cert, err := ident.Certificate() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return []*x509.Certificate{cert}, 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) { | ||
entry, err := pinentry.NewPinentry() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
pin, err := entry.Get(fmt.Sprintf("Enter PIN for \"%v\"", ident.card)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
private, err := ident.yk.PrivateKey(piv.SlotSignature, ident.Public(), piv.KeyAuth{ | ||
PIN: 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") | ||
} | ||
} |