From 6d0edd73620d88ea361ead025ade5a99a6499182 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Fri, 21 Oct 2022 17:57:36 +0800 Subject: [PATCH] feat: make the pinentry binary name configurable Some environments do not implement the alternatives system, so make the pinentry binary name configurable. Defaults to `pinentry`. --- cmd/piv-agent/list.go | 3 ++- cmd/piv-agent/serve.go | 18 ++++++++++-------- cmd/piv-agent/setup.go | 3 ++- cmd/piv-agent/setupslots.go | 3 ++- internal/keyservice/piv/keyservice.go | 8 ++++++-- internal/keyservice/piv/list.go | 3 ++- internal/pinentry/pinentry.go | 18 +++++++++++++++--- internal/securitykey/securitykey.go | 8 ++++++-- internal/ssh/agent.go | 8 ++++---- 9 files changed, 49 insertions(+), 23 deletions(-) diff --git a/cmd/piv-agent/list.go b/cmd/piv-agent/list.go index bef9bf2..10b25c6 100644 --- a/cmd/piv-agent/list.go +++ b/cmd/piv-agent/list.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/smlx/piv-agent/internal/keyservice/piv" + "github.com/smlx/piv-agent/internal/pinentry" "go.uber.org/zap" ) @@ -17,7 +18,7 @@ type ListCmd struct { // Run the list command. func (cmd *ListCmd) Run(l *zap.Logger) error { - p := piv.New(l) + p := piv.New(l, pinentry.New("pinentry")) securityKeys, err := p.SecurityKeys() if err != nil { return fmt.Errorf("couldn't get security keys: %w", err) diff --git a/cmd/piv-agent/serve.go b/cmd/piv-agent/serve.go index 850b280..8bd213f 100644 --- a/cmd/piv-agent/serve.go +++ b/cmd/piv-agent/serve.go @@ -21,11 +21,12 @@ type agentTypeFlag map[string]uint // ServeCmd represents the listen command. type ServeCmd struct { - LoadKeyfile bool `kong:"default=true,help='Load the key file from ~/.ssh/id_ed25519'"` - ExitTimeout time.Duration `kong:"default=12h,help='Exit after this period to drop transaction and key file passphrase cache, even if service is in use'"` - IdleTimeout time.Duration `kong:"default=32m,help='Exit after this period of disuse'"` - TouchNotifyDelay time.Duration `kong:"default=6s,help='Display a notification after this period when waiting for a touch'"` - AgentTypes agentTypeFlag `kong:"default='ssh=0;gpg=1',help='Agent types to handle'"` + LoadKeyfile bool `kong:"default=true,help='Load the key file from ~/.ssh/id_ed25519'"` + ExitTimeout time.Duration `kong:"default=12h,help='Exit after this period to drop transaction and key file passphrase cache, even if service is in use'"` + IdleTimeout time.Duration `kong:"default=32m,help='Exit after this period of disuse'"` + TouchNotifyDelay time.Duration `kong:"default=6s,help='Display a notification after this period when waiting for a touch'"` + PinentryBinaryName string `kong:"default='pinentry',help='Pinentry binary which will be used, must be in $PATH'"` + AgentTypes agentTypeFlag `kong:"default='ssh=0;gpg=1',help='Agent types to handle'"` } // validAgents is the list of agents supported by piv-agent. @@ -51,7 +52,8 @@ func (flagAgents *agentTypeFlag) AfterApply() error { func (cmd *ServeCmd) Run(log *zap.Logger) error { log.Info("startup", zap.String("version", version), zap.String("build date", date)) - p := piv.New(log) + pe := pinentry.New(cmd.PinentryBinaryName) + p := piv.New(log, pe) defer p.CloseAll() // use FDs passed via socket activation ls, err := sockets.Get(validAgents) @@ -74,7 +76,7 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error { log.Debug("starting SSH server") g.Go(func() error { s := server.NewSSH(log) - a := ssh.NewAgent(p, log, cmd.LoadKeyfile, n, cancel) + a := ssh.NewAgent(p, pe, log, cmd.LoadKeyfile, n, cancel) err := s.Serve(ctx, a, ls[cmd.AgentTypes["ssh"]], idle, cmd.IdleTimeout) if err != nil { log.Debug("exiting SSH server", zap.Error(err)) @@ -94,7 +96,7 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error { if _, ok := cmd.AgentTypes["gpg"]; ok { log.Debug("starting GPG server") g.Go(func() error { - s := server.NewGPG(p, &pinentry.PINEntry{}, log, fallbackKeys, n) + s := server.NewGPG(p, pe, log, fallbackKeys, n) err := s.Serve(ctx, ls[cmd.AgentTypes["gpg"]], idle, cmd.IdleTimeout) if err != nil { log.Debug("exiting GPG server", zap.Error(err)) diff --git a/cmd/piv-agent/setup.go b/cmd/piv-agent/setup.go index 9be8ca5..b7776ff 100644 --- a/cmd/piv-agent/setup.go +++ b/cmd/piv-agent/setup.go @@ -7,6 +7,7 @@ import ( "os" "strconv" + "github.com/smlx/piv-agent/internal/pinentry" "github.com/smlx/piv-agent/internal/securitykey" "golang.org/x/crypto/ssh/terminal" ) @@ -56,7 +57,7 @@ func (cmd *SetupCmd) Run() error { if cmd.PIN < 100000 || cmd.PIN > 99999999 { return fmt.Errorf("invalid PIN, must be 6-8 digits") } - k, err := securitykey.New(cmd.Card) + k, err := securitykey.New(cmd.Card, pinentry.New("pinentry")) if err != nil { return fmt.Errorf("couldn't get security key: %v", err) } diff --git a/cmd/piv-agent/setupslots.go b/cmd/piv-agent/setupslots.go index 8dcd247..e14a6df 100644 --- a/cmd/piv-agent/setupslots.go +++ b/cmd/piv-agent/setupslots.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" + "github.com/smlx/piv-agent/internal/pinentry" "github.com/smlx/piv-agent/internal/securitykey" ) @@ -30,7 +31,7 @@ func (cmd *SetupSlotsCmd) Run() error { if cmd.PIN < 100000 || cmd.PIN > 99999999 { return fmt.Errorf("invalid PIN, must be 6-8 digits") } - k, err := securitykey.New(cmd.Card) + k, err := securitykey.New(cmd.Card, pinentry.New("pinentry")) if err != nil { return fmt.Errorf("couldn't get security key: %v", err) } diff --git a/internal/keyservice/piv/keyservice.go b/internal/keyservice/piv/keyservice.go index 2f3e7cf..566bfdf 100644 --- a/internal/keyservice/piv/keyservice.go +++ b/internal/keyservice/piv/keyservice.go @@ -1,3 +1,4 @@ +// Package piv implements the PIV keyservice. package piv import ( @@ -9,6 +10,7 @@ import ( pivgo "github.com/go-piv/piv-go/piv" "github.com/smlx/piv-agent/internal/keyservice/gpg" + "github.com/smlx/piv-agent/internal/pinentry" "go.uber.org/zap" ) @@ -17,13 +19,15 @@ import ( type KeyService struct { mu sync.Mutex log *zap.Logger + pinentry *pinentry.PINEntry securityKeys []SecurityKey } // New constructs a PIV and returns it. -func New(l *zap.Logger) *KeyService { +func New(l *zap.Logger, pe *pinentry.PINEntry) *KeyService { return &KeyService{ - log: l, + log: l, + pinentry: pe, } } diff --git a/internal/keyservice/piv/list.go b/internal/keyservice/piv/list.go index 6b5e093..f4a410f 100644 --- a/internal/keyservice/piv/list.go +++ b/internal/keyservice/piv/list.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/go-piv/piv-go/piv" + "github.com/smlx/piv-agent/internal/pinentry" "github.com/smlx/piv-agent/internal/securitykey" "go.uber.org/zap" ) @@ -39,7 +40,7 @@ func (p *KeyService) reloadSecurityKeys() error { return fmt.Errorf("couldn't get cards: %v", err) } for _, card := range cards { - sk, err := securitykey.New(card) + sk, err := securitykey.New(card, pinentry.New("pinentry")) if err != nil { p.log.Warn("couldn't get SecurityKey", zap.String("card", card), zap.Error(err)) diff --git a/internal/pinentry/pinentry.go b/internal/pinentry/pinentry.go index c5b5882..7594a9c 100644 --- a/internal/pinentry/pinentry.go +++ b/internal/pinentry/pinentry.go @@ -1,3 +1,4 @@ +// Package pinentry implements a PIN/passphrase entry dialog. package pinentry import ( @@ -14,16 +15,26 @@ type SecurityKey interface { } // PINEntry implements useful pinentry service methods. -type PINEntry struct{} +type PINEntry struct { + binaryName string +} + +// New initialises a new PINEntry. +func New(binaryName string) *PINEntry { + return &PINEntry{ + binaryName: binaryName, + } +} // GetPin uses pinentry to get the pin of the given token. -func GetPin(k SecurityKey) func() (string, error) { +func (pe *PINEntry) GetPin(k SecurityKey) func() (string, error) { return func() (string, error) { r, err := k.Retries() if err != nil { return "", fmt.Errorf("couldn't get retries for security key: %w", err) } c, err := gpm.NewClient( + gpm.WithBinaryName(pe.binaryName), gpm.WithTitle("piv-agent PIN Prompt"), gpm.WithPrompt("Please enter PIN:"), gpm.WithDesc( @@ -43,8 +54,9 @@ func GetPin(k SecurityKey) func() (string, error) { } // GetPassphrase uses pinentry to get the passphrase of the given key file. -func (*PINEntry) GetPassphrase(desc, keyID string, tries int) ([]byte, error) { +func (pe *PINEntry) GetPassphrase(desc, keyID string, tries int) ([]byte, error) { c, err := gpm.NewClient( + gpm.WithBinaryName(pe.binaryName), gpm.WithTitle("piv-agent Passphrase Prompt"), gpm.WithPrompt("Please enter passphrase"), gpm.WithDesc(fmt.Sprintf("%s\r(%d attempts remaining)", desc, tries)), diff --git a/internal/securitykey/securitykey.go b/internal/securitykey/securitykey.go index 3d12b16..f7d57e8 100644 --- a/internal/securitykey/securitykey.go +++ b/internal/securitykey/securitykey.go @@ -1,3 +1,5 @@ +// Package securitykey provides an interface to a physical security key such as +// a Yubikey. package securitykey import ( @@ -19,6 +21,7 @@ type SecurityKey struct { signingKeys []SigningKey decryptingKeys []DecryptingKey cryptoKeys []CryptoKey + pinentry *pinentry.PINEntry } // CryptoKey represents a cryptographic key on a hardware security device. @@ -28,7 +31,7 @@ type CryptoKey struct { } // New returns a security key identified by card string. -func New(card string) (*SecurityKey, error) { +func New(card string, pe *pinentry.PINEntry) (*SecurityKey, error) { yk, err := piv.Open(card) if err != nil { return nil, fmt.Errorf(`couldn't open card "%s": %v`, card, err) @@ -64,6 +67,7 @@ func New(card string) (*SecurityKey, error) { signingKeys: sks, decryptingKeys: dks, cryptoKeys: cryptoKeys, + pinentry: pe, }, nil } @@ -98,7 +102,7 @@ func (k *SecurityKey) CryptoKeys() []CryptoKey { // PrivateKey returns the private key of the given public signing key. func (k *SecurityKey) PrivateKey(c *CryptoKey) (crypto.PrivateKey, error) { return k.yubikey.PrivateKey(c.SlotSpec.Slot, c.Public, - piv.KeyAuth{PINPrompt: pinentry.GetPin(k)}) + piv.KeyAuth{PINPrompt: k.pinentry.GetPin(k)}) } // Close closes the underlying yubikey. diff --git a/internal/ssh/agent.go b/internal/ssh/agent.go index d31bdbf..5d74142 100644 --- a/internal/ssh/agent.go +++ b/internal/ssh/agent.go @@ -44,10 +44,10 @@ var ErrUnknownKey = errors.New("requested signature of unknown key") var passphrases = map[string][]byte{} // NewAgent returns a new Agent. -func NewAgent(p *piv.KeyService, log *zap.Logger, loadKeyfile bool, - n *notify.Notify, cancel context.CancelFunc) *Agent { - return &Agent{piv: p, log: log, notify: n, loadKeyfile: loadKeyfile, - cancel: cancel} +func NewAgent(p *piv.KeyService, pe *pinentry.PINEntry, log *zap.Logger, + loadKeyfile bool, n *notify.Notify, cancel context.CancelFunc) *Agent { + return &Agent{piv: p, pinentry: pe, log: log, notify: n, + loadKeyfile: loadKeyfile, cancel: cancel} } // List returns the identities known to the agent.