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

Implement gpg-agent #39

Merged
merged 12 commits into from
Jul 26, 2021
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
test: mod-tidy generate
go test -v ./...

mod-tidy:
go mod tidy

generate:
go generate ./...
58 changes: 33 additions & 25 deletions cmd/piv-agent/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,51 @@ import (
"fmt"
"strings"

"github.com/go-piv/piv-go/piv"
"github.com/smlx/piv-agent/internal/token"
"github.com/smlx/piv-agent/internal/pivservice"
"go.uber.org/zap"
"golang.org/x/crypto/ssh"
)

// ListCmd represents the list command.
type ListCmd struct{}

var touchStringMap = map[piv.TouchPolicy]string{
piv.TouchPolicyNever: "never",
piv.TouchPolicyAlways: "always",
piv.TouchPolicyCached: "cached",
type ListCmd struct {
KeyFormats []string `kong:"default='ssh',enum='ssh,gpg',help='Key formats to list (ssh, gpg)'"`
PGPName string `kong:"default='piv-agent',help='Name set on synthesized PGP identities'"`
PGPEmail string `kong:"default='[email protected]',help='Email set on synthesized PGP identities'"`
}

// Run the list command.
func (cmd *ListCmd) Run(log *zap.Logger) error {
securityKeys, err := token.List(log)
func (cmd *ListCmd) Run(l *zap.Logger) error {
p := pivservice.New(l)
securityKeys, err := p.SecurityKeys()
if err != nil {
return fmt.Errorf("couldn't get security keys: %w", err)
}
fmt.Println("security keys (cards):")
for _, sk := range securityKeys {
fmt.Println(sk.Card)
fmt.Println("Security keys (cards):")
for _, k := range securityKeys {
fmt.Println(k.Card())
}
sshKeySpecs, err := token.SSHKeySpecs(securityKeys)
if err != nil {
return fmt.Errorf("couldn't get SSH public keys: %w", err)
keyformats := map[string]bool{}
for _, f := range cmd.KeyFormats {
keyformats[f] = true
}
if keyformats["ssh"] {
fmt.Println("\nSSH keys:")
for _, k := range securityKeys {
for _, s := range k.StringsSSH() {
fmt.Println(strings.TrimSpace(s))
}
}
}
fmt.Println("ssh keys:")
for _, sks := range sshKeySpecs {
fmt.Printf("%s %s\n",
strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(sks.PubKey)), "\n"),
fmt.Sprintf("%v #%v, touch policy: %s",
sks.Card,
sks.Serial,
touchStringMap[sks.TouchPolicy]))
if keyformats["gpg"] {
fmt.Println("\nGPG keys:")
for _, k := range securityKeys {
ss, err := k.StringsGPG(cmd.PGPName, cmd.PGPEmail)
if err != nil {
return fmt.Errorf("couldn't get GPG keys as strings: %v", err)
}
for _, s := range ss {
fmt.Println(s)
}
}
}
return nil
}
2 changes: 1 addition & 1 deletion cmd/piv-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type CLI struct {
Debug bool `kong:"help='Enable debug logging'"`
Serve ServeCmd `kong:"cmd,default=1,help='(default) Listen for signing requests'"`
Setup SetupCmd `kong:"cmd,help='Set up the security key for use with SSH'"`
List ListCmd `kong:"cmd,help='List SSH keys available on each security key'"`
List ListCmd `kong:"cmd,help='List signing keys available on each security key'"`
}

func main() {
Expand Down
109 changes: 69 additions & 40 deletions cmd/piv-agent/serve.go
Original file line number Diff line number Diff line change
@@ -1,73 +1,102 @@
package main

import (
"errors"
"context"
"fmt"
"io"
"net"
"time"

"github.com/coreos/go-systemd/activation"
pivagent "github.com/smlx/piv-agent/internal/agent"
"github.com/smlx/piv-agent/internal/pivservice"
"github.com/smlx/piv-agent/internal/server"
"github.com/smlx/piv-agent/internal/ssh"
"go.uber.org/zap"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/sync/errgroup"
)

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=32m,help='Exit after this period to drop transaction and key file passphrase cache'"`
AgentTypes agentTypeFlag `kong:"default='ssh=0;gpg=1',help='Agent types to handle'"`
}

// validAgents is the list of agents supported by piv-agent.
var validAgents = []string{"ssh", "gpg"}

// AfterApply validates the given agent types.
func (flagAgents *agentTypeFlag) AfterApply() error {
for flagAgent := range map[string]uint(*flagAgents) {
valid := false
for _, validAgent := range validAgents {
if flagAgent == validAgent {
valid = true
}
}
if !valid {
return fmt.Errorf("invalid agent-type: %v", flagAgent)
}
}
return nil
}

// Run the listen command to start listening for ssh-agent requests.
func (cmd *ServeCmd) Run(log *zap.Logger) error {
log.Info("startup", zap.String("version", version),
zap.String("build date", date))
p := pivservice.New(log)
// use systemd socket activation
listeners, err := activation.Listeners()
ls, err := activation.Listeners()
if err != nil {
return fmt.Errorf("cannot retrieve listeners: %w", err)
}
if len(listeners) != 1 {
return fmt.Errorf("unexpected number of sockets, expected: 1, received: %v",
len(listeners))
// validate given agent types
if len(ls) != len(cmd.AgentTypes) {
return fmt.Errorf("wrong number of agent sockets: wanted %v, received %v",
len(cmd.AgentTypes), len(ls))
}
// prepare dependencies
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
exit := time.NewTicker(cmd.ExitTimeout)
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)
err := s.Serve(ctx, a, ls[cmd.AgentTypes["ssh"]], exit, cmd.ExitTimeout)
cancel()
return err
})
}
// start the exit timer
exitTicker := time.NewTicker(cmd.ExitTimeout)
// start serving connections
newConns := make(chan net.Conn)
go func(l net.Listener, log *zap.Logger) {
for {
c, err := l.Accept()
if _, ok := cmd.AgentTypes["gpg"]; ok {
log.Debug("starting GPG server")
g.Go(func() error {
s := server.NewGPG(p, log)
err := s.Serve(ctx, ls[cmd.AgentTypes["gpg"]], exit, cmd.ExitTimeout)
if err != nil {
log.Error("accept error", zap.Error(err))
close(newConns)
return
log.Debug("exiting GPG server", zap.Error(err))
} else {
log.Debug("exiting GPG server successfully")
}
newConns <- c
}
}(listeners[0], log)

a := pivagent.New(log, cmd.LoadKeyfile)
cancel()
return err
})
}
loop:
for {
select {
case conn, ok := <-newConns:
if !ok {
return fmt.Errorf("listen socket closed")
}
// reset the exit timer
exitTicker.Reset(cmd.ExitTimeout)
log.Debug("start serving connection")
if err = agent.ServeAgent(a, conn); err != nil {
if errors.Is(err, io.EOF) {
log.Debug("finish serving connection")
continue
}
return fmt.Errorf("serveAgent error: %w", err)
}
case <-exitTicker.C:
case <-ctx.Done():
log.Debug("exit done")
break loop
case <-exit.C:
log.Debug("exit timeout")
return nil
cancel()
break loop
}
}
return g.Wait()
}
12 changes: 6 additions & 6 deletions cmd/piv-agent/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"os"
"strconv"

"github.com/smlx/piv-agent/internal/token"
"github.com/smlx/piv-agent/internal/securitykey"
"golang.org/x/crypto/ssh/terminal"
)

Expand Down Expand Up @@ -55,13 +55,13 @@ func (cmd *SetupCmd) Run() error {
if cmd.PIN < 100000 || cmd.PIN > 99999999 {
return fmt.Errorf("invalid PIN, must be 6-8 digits")
}
k, err := token.Get(cmd.Card)
k, err := securitykey.New(cmd.Card)
if err != nil {
return fmt.Errorf("couldn't get security key: %w", err)
return fmt.Errorf("couldn't get security key: %v", err)
}
err = token.Setup(k, strconv.FormatUint(cmd.PIN, 10), version,
cmd.ResetSecurityKey, cmd.AllTouchPolicies)
if errors.Is(err, token.ErrNotReset) {
err = k.Setup(strconv.FormatUint(cmd.PIN, 10), version,
cmd.ResetSecurityKey)
if errors.Is(err, securitykey.ErrNotReset) {
return fmt.Errorf("--reset-security-key not specified: %w", err)
}
return err
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28
github.com/go-piv/piv-go v1.8.0
github.com/golang/mock v1.5.0
github.com/gopasspw/gopass v1.10.2-0.20201105185611-36c5888f3a49
github.com/smlx/fsm v0.2.0
go.uber.org/zap v1.18.1
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/sync v0.0.0-20190423024810-112230192c58
)
12 changes: 11 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/alecthomas/kong v0.2.17 h1:URDISCI96MIgcIlQyoCAlhOmrSw6pZScBNkctg8r0W0=
github.com/alecthomas/kong v0.2.17/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/blang/semver v0.0.0-20190414182527-1a9109f8c4a1/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
Expand Down Expand Up @@ -33,6 +34,8 @@ github.com/godbus/dbus v0.0.0-20190623212516-8a1682060722/go.mod h1:bBOAhwG1umN6
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gokyle/twofactor v1.0.1/go.mod h1:4gxzH1eaE/F3Pct/sCDNOylP0ClofUO5j4XZN9tKtLE=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
Expand Down Expand Up @@ -97,6 +100,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smlx/fsm v0.2.0 h1:ScVvTCAXoazgsDkuakG0UqGNyuo3kU9Qwf/gzwY4o4o=
github.com/smlx/fsm v0.2.0/go.mod h1:LiXoNZ+m3neHxSVsc8KN7ed0mbiY6K/1MKj+HcZzhkQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
Expand All @@ -120,17 +125,20 @@ go.uber.org/zap v1.18.1 h1:CSUJ2mjFszzEWt4CdKISEuChVIXGBn3lAPwkRGyVrc4=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand All @@ -149,9 +157,11 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
24 changes: 24 additions & 0 deletions internal/assuan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Generate sample ECC key like so

```
gpg --full-gen-key --expert
# select ECC sign only
# use e.g. Name: foo bar, Email: [email protected]
```

Generate signing traces like so:

```
echo foo | strace -xs 1024 /usr/bin/gpg --verbose --status-fd=2 -bsau C54A8868468BC138 2> gpg-agent.sign.strace
# grep the agent socket
grep '(5'
# reads
grep '^read'
# writes
grep '^write'
```

Export key for use in CI:
```
gpg --export -ao /tmp/C54A8868468BC138.asc [email protected]
```
Loading