Skip to content

Commit

Permalink
Merge pull request #135 from smlx/serve-options
Browse files Browse the repository at this point in the history
Add configuration options to the serve command
  • Loading branch information
smlx authored Oct 21, 2022
2 parents e025e72 + 9e53b53 commit 1ae8712
Show file tree
Hide file tree
Showing 17 changed files with 105 additions and 41 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/piv-agent
3 changes: 2 additions & 1 deletion cmd/piv-agent/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmd/piv-agent/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package main implements the piv-agent CLI.
package main

import (
Expand Down
19 changes: 12 additions & 7 deletions cmd/piv-agent/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/smlx/piv-agent/internal/keyservice/piv"
"github.com/smlx/piv-agent/internal/notify"
"github.com/smlx/piv-agent/internal/pinentry"
"github.com/smlx/piv-agent/internal/server"
"github.com/smlx/piv-agent/internal/sockets"
Expand All @@ -20,10 +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'"`
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.
Expand All @@ -49,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)
Expand All @@ -65,13 +69,14 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
idle := time.NewTicker(cmd.IdleTimeout)
n := notify.New(log, cmd.TouchNotifyDelay)
g := errgroup.Group{}
// start SSH agent if given in agent-type flag
if _, ok := cmd.AgentTypes["ssh"]; ok {
log.Debug("starting SSH server")
g.Go(func() error {
s := server.NewSSH(log)
a := ssh.NewAgent(p, log, cmd.LoadKeyfile, 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))
Expand All @@ -91,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)
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))
Expand Down
3 changes: 2 additions & 1 deletion cmd/piv-agent/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/piv-agent/setupslots.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strconv"

"github.com/smlx/piv-agent/internal/pinentry"
"github.com/smlx/piv-agent/internal/securitykey"
)

Expand All @@ -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)
}
Expand Down
6 changes: 5 additions & 1 deletion internal/assuan/assuan.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package assuan implements an libgcrypt Assuan protocol server.
package assuan

//go:generate mockgen -source=assuan.go -destination=../mock/mock_assuan.go -package=mock
Expand All @@ -13,6 +14,7 @@ import (
"strings"

"github.com/smlx/fsm"
"github.com/smlx/piv-agent/internal/notify"
"go.uber.org/zap"
"golang.org/x/crypto/openpgp/s2k"
)
Expand All @@ -33,10 +35,12 @@ type KeyService interface {

// New initialises a new gpg-agent server assuan FSM.
// It returns a *fsm.Machine configured in the ready state.
func New(rw io.ReadWriter, log *zap.Logger, ks ...KeyService) *Assuan {
func New(rw io.ReadWriter, log *zap.Logger, n *notify.Notify,
ks ...KeyService) *Assuan {
var signature []byte
var keygrips, hash [][]byte
assuan := Assuan{
notify: n,
reader: bufio.NewReader(rw),
Machine: fsm.Machine{
State: fsm.State(ready),
Expand Down
20 changes: 14 additions & 6 deletions internal/assuan/assuan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import (
"math/big"
"os"
"testing"
"time"

"github.com/davecgh/go-spew/spew"
"github.com/golang/mock/gomock"
"github.com/smlx/piv-agent/internal/assuan"
"github.com/smlx/piv-agent/internal/keyservice/gpg"
"github.com/smlx/piv-agent/internal/mock"
"github.com/smlx/piv-agent/internal/notify"
"github.com/smlx/piv-agent/internal/securitykey"
"go.uber.org/zap"
"golang.org/x/crypto/cryptobyte"
Expand Down Expand Up @@ -146,7 +148,8 @@ func TestSign(t *testing.T) {
if err != nil {
tt.Fatal(err)
}
a := assuan.New(&mockConn, log, keyService)
n := notify.New(log, 6*time.Second)
a := assuan.New(&mockConn, log, n, keyService)
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := mockConn.ReadBuf.WriteString(in); err != nil {
Expand Down Expand Up @@ -221,7 +224,8 @@ func TestKeyinfo(t *testing.T) {
if err != nil {
tt.Fatal(err)
}
a := assuan.New(&mockConn, log, keyService)
n := notify.New(log, 6*time.Second)
a := assuan.New(&mockConn, log, n, keyService)
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := mockConn.ReadBuf.WriteString(in); err != nil {
Expand Down Expand Up @@ -340,7 +344,8 @@ func TestDecryptRSAKeyfile(t *testing.T) {
// mockConn is a pair of buffers that the assuan statemachine reads/write
// to/from.
mockConn := MockConn{}
a := assuan.New(&mockConn, log, gpg.New(log, mockPES, tc.keyPath))
n := notify.New(log, 6*time.Second)
a := assuan.New(&mockConn, log, n, gpg.New(log, mockPES, tc.keyPath))
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := mockConn.ReadBuf.WriteString(in); err != nil {
Expand Down Expand Up @@ -434,7 +439,8 @@ func TestSignRSAKeyfile(t *testing.T) {
// mockConn is a pair of buffers that the assuan statemachine reads/write
// to/from.
mockConn := MockConn{}
a := assuan.New(&mockConn, log, gpg.New(log, mockPES, tc.keyPath))
n := notify.New(log, 6*time.Second)
a := assuan.New(&mockConn, log, n, gpg.New(log, mockPES, tc.keyPath))
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := mockConn.ReadBuf.WriteString(in); err != nil {
Expand Down Expand Up @@ -516,7 +522,8 @@ func TestReadKey(t *testing.T) {
// mockConn is a pair of buffers that the assuan statemachine reads/write
// to/from.
mockConn := MockConn{}
a := assuan.New(&mockConn, log, gpg.New(log, mockPES, tc.keyPath))
n := notify.New(log, 6*time.Second)
a := assuan.New(&mockConn, log, n, gpg.New(log, mockPES, tc.keyPath))
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := mockConn.ReadBuf.WriteString(in); err != nil {
Expand Down Expand Up @@ -628,7 +635,8 @@ func TestDecryptECDHKeyfile(t *testing.T) {
// mockConn is a pair of buffers that the assuan statemachine reads/write
// to/from.
mockConn := MockConn{}
a := assuan.New(&mockConn, log, gpg.New(log, mockPES, tc.keyPath))
n := notify.New(log, 6*time.Second)
a := assuan.New(&mockConn, log, n, gpg.New(log, mockPES, tc.keyPath))
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := mockConn.ReadBuf.WriteString(in); err != nil {
Expand Down
4 changes: 3 additions & 1 deletion internal/assuan/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sync"

"github.com/smlx/fsm"
"github.com/smlx/piv-agent/internal/notify"
)

//go:generate enumer -type=Event -text -transform upper
Expand Down Expand Up @@ -55,7 +56,8 @@ const (
// Assuan is the Assuan protocol FSM.
type Assuan struct {
fsm.Machine
mu sync.Mutex
mu sync.Mutex
notify *notify.Notify
// buffered IO for linewise reading
reader *bufio.Reader
// data is passed during Occur()
Expand Down
3 changes: 1 addition & 2 deletions internal/assuan/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"math/big"

"github.com/smlx/piv-agent/internal/notify"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
)
Expand Down Expand Up @@ -41,7 +40,7 @@ func (a *Assuan) signRSA() ([]byte, error) {
// separately s-exp encoded. So we have to decode the ASN1 signature, extract
// the params, and re-encode them into the s-exp. Ugh.
func (a *Assuan) signECDSA() ([]byte, error) {
cancel := notify.Touch(nil)
cancel := a.notify.Touch()
defer cancel()
signature, err := a.signer.Sign(rand.Reader, a.hash, a.hashAlgo)
if err != nil {
Expand Down
8 changes: 6 additions & 2 deletions internal/keyservice/piv/keyservice.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package piv implements the PIV keyservice.
package piv

import (
Expand All @@ -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"
)

Expand All @@ -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,
}
}

Expand Down
3 changes: 2 additions & 1 deletion internal/keyservice/piv/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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))
Expand Down
23 changes: 18 additions & 5 deletions internal/notify/touch.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package notify implements a touch notification system.
package notify

import (
Expand All @@ -8,22 +9,34 @@ import (
"go.uber.org/zap"
)

const waitTime = 6 * time.Second
// Notify contains touch notification configuration.
type Notify struct {
log *zap.Logger
touchNotifyDelay time.Duration
}

// New initialises a new Notify struct.
func New(log *zap.Logger, touchNotifyDelay time.Duration) *Notify {
return &Notify{
log: log,
touchNotifyDelay: touchNotifyDelay,
}
}

// Touch starts a goroutine, and waits for a short period. If the returned
// CancelFunc has not been called it sends a notification to remind the user to
// physically touch the Security Key.
func Touch(log *zap.Logger) context.CancelFunc {
func (n *Notify) Touch() context.CancelFunc {
ctx, cancel := context.WithCancel(context.Background())
timer := time.NewTimer(waitTime)
timer := time.NewTimer(n.touchNotifyDelay)
go func() {
select {
case <-ctx.Done():
timer.Stop()
case <-timer.C:
err := beeep.Alert("Security Key Agent", "Waiting for touch...", "")
if err != nil && log != nil {
log.Warn("couldn't send touch notification", zap.Error(err))
if err != nil {
n.log.Warn("couldn't send touch notification", zap.Error(err))
}
}
}()
Expand Down
18 changes: 15 additions & 3 deletions internal/pinentry/pinentry.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package pinentry implements a PIN/passphrase entry dialog.
package pinentry

import (
Expand All @@ -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(
Expand All @@ -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)),
Expand Down
Loading

0 comments on commit 1ae8712

Please sign in to comment.