Skip to content

Commit

Permalink
Merge pull request #39 from smlx/gpg-agent
Browse files Browse the repository at this point in the history
Implement gpg-agent
  • Loading branch information
smlx committed Jul 26, 2021
2 parents 51da9b7 + 9fd8183 commit 84b16c5
Show file tree
Hide file tree
Showing 38 changed files with 2,287 additions and 435 deletions.
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='noreply@example.com',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: foo@example.com
```

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 foo@example.com
```

0 comments on commit 84b16c5

Please sign in to comment.