Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add explicit support for identities stored on hardware keys … #78

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions command_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ func commandSign() error {
return fmt.Errorf("could not find identity matching specified user-id: %s", *localUserOpt)
}

// Git is looking for "\n[GNUPG:] SIG_CREATED ", meaning we need to print a
// line before SIG_CREATED. BEGIN_SIGNING seems appropraite. GPG emits this,
// though GPGSM does not.
sBeginSigning.emit()

cert, err := userIdent.Certificate()
if err != nil {
return errors.Wrap(err, "failed to get idenity certificate")
Expand Down Expand Up @@ -60,6 +55,10 @@ func commandSign() error {
if err = sd.Sign([]*x509.Certificate{cert}, signer); err != nil {
return errors.Wrap(err, "failed to sign message")
}
// Git is looking for "\n[GNUPG:] SIG_CREATED ", meaning we need to print a
// line before SIG_CREATED. BEGIN_SIGNING seems appropraite. GPG emits this,
// though GPGSM does not.
sBeginSigning.emit()
if *detachSignFlag {
sd.Detached()
}
Expand Down
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=
14 changes: 13 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,22 @@ func runCommand() error {
defer store.Close()

// Get list of identities
idents, err = store.Identities()
pivIdents, err := PivIdentities()
if err != nil {
fmt.Fprintln(os.Stderr, "skipping hardware keys")
}
for _, pivIdent := range pivIdents {
idents = append(idents, &pivIdent)
}

storeIdents, err := store.Identities()
if err != nil {
return errors.Wrap(err, "failed to get identities from certificate store")
}
for _, ident := range storeIdents {
idents = append(idents, ident)
}

for _, ident := range idents {
defer ident.Close()
}
Expand Down
128 changes: 128 additions & 0 deletions pinentry/pinentry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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
}
}

for _, programName := range paths {
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
}
9 changes: 9 additions & 0 deletions pinentry/pinentry_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pinentry

var paths = []string{
"pinentry-mac",
"pinentry-curses",
"pinentry",
}

const tty = "/dev/tty"
11 changes: 11 additions & 0 deletions pinentry/pinentry_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package pinentry

var paths = []string{
"pinentry-gnome3",
"pinentry-gtk",
"pinentry-qy",
"pinentry-tty",
"pinentry",
}

const tty = "/dev/tty"
10 changes: 10 additions & 0 deletions pinentry/pinentry_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package pinentry

var paths = []string{
"pinentry-gtk-2.exe",
"pinentry-qt4.exe",
"pinentry-w32.exe",
"pinentry.exe",
}

const tty = "windows"
113 changes: 113 additions & 0 deletions piv_identity.go
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")
}
}