Skip to content

Commit 0843302

Browse files
committed
Get the hardware key PIN in a secure way
1 parent 987d800 commit 0843302

File tree

5 files changed

+184
-34
lines changed

5 files changed

+184
-34
lines changed

pinentry/pinentry.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package pinentry
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
)
11+
12+
// Pinentry gets the PIN from the user to access the smart card or hardware key
13+
type Pinentry struct {
14+
path string
15+
}
16+
17+
// NewPinentry initializes the pinentry program used to get the PIN
18+
func NewPinentry() (*Pinentry, error) {
19+
fromEnv := os.Getenv("SMIMESIGM_PINENTRY")
20+
if len(fromEnv) > 0 {
21+
pinentryFromEnv, err := exec.LookPath(fromEnv)
22+
if err == nil && len(pinentryFromEnv) > 0 {
23+
return &Pinentry{path: pinentryFromEnv}, nil
24+
}
25+
}
26+
27+
executables := pinentryPaths()
28+
for _, programName := range executables {
29+
pinentry, err := exec.LookPath(programName)
30+
if err == nil && len(pinentry) > 0 {
31+
return &Pinentry{path: pinentry}, nil
32+
}
33+
}
34+
35+
return nil, fmt.Errorf("failed to find suitable program to enter pin")
36+
}
37+
38+
// Get executes the pinentry program and returns the PIN entered by the user
39+
// see https://www.gnupg.org/documentation/manuals/assuan/Introduction.html for more details
40+
func (pin *Pinentry) Get(prompt string) (string, error) {
41+
cmd := exec.Command(pin.path)
42+
stdin, err := cmd.StdinPipe()
43+
if err != nil {
44+
return "", err
45+
}
46+
47+
stdout, err := cmd.StdoutPipe()
48+
if err != nil {
49+
return "", err
50+
}
51+
52+
err = cmd.Start()
53+
if err != nil {
54+
return "", err
55+
}
56+
57+
bufferReader := bufio.NewReader(stdout)
58+
lineBytes, _, err := bufferReader.ReadLine()
59+
if err != nil {
60+
return "", err
61+
}
62+
63+
line := string(lineBytes)
64+
if !strings.HasPrefix(line, "OK") {
65+
return "", fmt.Errorf("failed to initialize pinentry, got response: %v", line)
66+
}
67+
68+
terminal := os.Getenv("TERM")
69+
if len(terminal) > 0 {
70+
if ok := setOption(stdin, bufferReader, fmt.Sprintf("OPTION ttytype=%s\n", terminal)); !ok {
71+
return "", fmt.Errorf("failed to set ttytype")
72+
}
73+
}
74+
75+
if ok := setOption(stdin, bufferReader, fmt.Sprintf("OPTION ttyname=%v\n", tty())); !ok {
76+
return "", fmt.Errorf("failed to set ttyname")
77+
}
78+
79+
if ok := setOption(stdin, bufferReader, "SETPROMPT PIN:\n"); !ok {
80+
return "", fmt.Errorf("failed to set prompt")
81+
}
82+
if ok := setOption(stdin, bufferReader, "SETTITLE smimesign\n"); !ok {
83+
return "", fmt.Errorf("failed to set title")
84+
}
85+
if ok := setOption(stdin, bufferReader, fmt.Sprintf("SETDESC %s\n", prompt)); !ok {
86+
return "", fmt.Errorf("failed to set description")
87+
}
88+
89+
_, err = fmt.Fprint(stdin, "GETPIN\n")
90+
if err != nil {
91+
return "", err
92+
}
93+
94+
lineBytes, _, err = bufferReader.ReadLine()
95+
if err != nil {
96+
return "", err
97+
}
98+
99+
line = string(lineBytes)
100+
101+
_, err = fmt.Fprint(stdin, "BYE\n")
102+
if err != nil {
103+
return "", err
104+
}
105+
106+
if err = cmd.Wait(); err != nil {
107+
return "", err
108+
}
109+
110+
if !strings.HasPrefix(line, "D ") {
111+
return "", fmt.Errorf(line)
112+
}
113+
114+
return strings.TrimPrefix(line, "D "), nil
115+
}
116+
117+
func setOption(writer io.Writer, bufferedReader *bufio.Reader, option string) bool {
118+
_, err := fmt.Fprintf(writer, option)
119+
lineBytes, _, err := bufferedReader.ReadLine()
120+
if err != nil {
121+
return false
122+
}
123+
124+
line := string(lineBytes)
125+
if !strings.HasPrefix(line, "OK") {
126+
return false
127+
}
128+
return true
129+
}

pinentry/pinentry_darwin.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package pinentry
2+
3+
func pinentryPaths() []string {
4+
return []string{
5+
"pinentry-mac",
6+
"pinentry-curses",
7+
"pinentry",
8+
}
9+
}
10+
11+
func tty() string {
12+
return "/dev/tty"
13+
}

pinentry/pinentry_linux.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package pinentry
2+
3+
func pinentryPaths() []string {
4+
// there are many flavours for the GnuPG pinentry program for different linux distros
5+
// this is a non-exhaustive list of some common implementations
6+
return []string{
7+
"pinentry-gnome3",
8+
"pinentry-gtk",
9+
"pinentry-qy",
10+
"pinentry-tty",
11+
"pinentry",
12+
}
13+
}
14+
15+
func tty() string {
16+
return "/dev/tty"
17+
}

pinentry/pinentry_windows.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package pinentry
2+
3+
func pinentryPaths() []string {
4+
return []string{
5+
"pinentry-gtk-2.exe",
6+
"pinentry-qt4.exe",
7+
"pinentry-w32.exe",
8+
"pinentry.exe",
9+
}
10+
}
11+
12+
func tty() string {
13+
return "windows"
14+
}

piv_identity.go

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@ import (
55
"crypto/x509"
66
"fmt"
77
"io"
8-
"io/fs"
9-
"os"
10-
11-
"golang.org/x/crypto/ssh/terminal"
128

139
"github.com/github/certstore"
10+
"github.com/github/smimesign/pinentry"
1411
"github.com/go-piv/piv-go/piv"
1512
"github.com/pkg/errors"
1613
)
@@ -91,8 +88,17 @@ func (ident *PivIdentity) Public() crypto.PublicKey {
9188

9289
// Sign implements the crypto.Signer interface
9390
func (ident *PivIdentity) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
91+
entry, err := pinentry.NewPinentry()
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
pin, err := entry.Get(fmt.Sprintf("Enter PIN for \"%v\"", ident.card))
97+
if err != nil {
98+
return nil, err
99+
}
94100
private, err := ident.yk.PrivateKey(piv.SlotSignature, ident.Public(), piv.KeyAuth{
95-
PINPrompt: ident.promptHardwareKeyPin,
101+
PIN: pin,
96102
})
97103
if err != nil {
98104
return nil, errors.Wrap(err, "failed to get private key for signing")
@@ -105,32 +111,3 @@ func (ident *PivIdentity) Sign(rand io.Reader, digest []byte, opts crypto.Signer
105111
return nil, fmt.Errorf("invalid key type")
106112
}
107113
}
108-
109-
func (ident *PivIdentity) promptHardwareKeyPin() (string, error) {
110-
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, fs.ModeCharDevice)
111-
if err != nil {
112-
return "", err
113-
}
114-
defer tty.Close()
115-
116-
_, err = fmt.Fprintf(tty, "enter PIN for hardware key \"%v\" (press enter for the default PIN):\n", ident.card)
117-
if err != nil {
118-
return "", err
119-
}
120-
121-
var hardwareKeyPin string
122-
pin, err := terminal.ReadPassword(int(tty.Fd()))
123-
if err != nil {
124-
return "", err
125-
}
126-
127-
if len(pin) == 0 {
128-
hardwareKeyPin = piv.DefaultPIN
129-
} else {
130-
hardwareKeyPin = string(pin)
131-
}
132-
133-
_, _ = fmt.Fprintf(tty, "Touch %v now unlock\n", ident.card)
134-
135-
return hardwareKeyPin, nil
136-
}

0 commit comments

Comments
 (0)