Skip to content

Commit

Permalink
feat: fall back to keyfiles for gpg-agent signing functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
smlx committed Aug 4, 2021
1 parent 06cfaa1 commit 0861193
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 21 deletions.
11 changes: 10 additions & 1 deletion cmd/piv-agent/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package main
import (
"context"
"fmt"
"os"
"path/filepath"
"time"

"github.com/coreos/go-systemd/activation"
"github.com/smlx/piv-agent/internal/pinentry"
"github.com/smlx/piv-agent/internal/pivservice"
"github.com/smlx/piv-agent/internal/server"
"github.com/smlx/piv-agent/internal/ssh"
Expand Down Expand Up @@ -72,10 +75,16 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
return err
})
}
// start GPG agent if given in agent-type flag
home, err := os.UserHomeDir()
if err != nil {
log.Warn("couldn't determine $HOME", zap.Error(err))
}
fallbackKeys := filepath.Join(home, ".gnupg", "piv-agent.secring.gpg")
if _, ok := cmd.AgentTypes["gpg"]; ok {
log.Debug("starting GPG server")
g.Go(func() error {
s := server.NewGPG(p, log)
s := server.NewGPG(p, &pinentry.PINEntry{}, log, fallbackKeys)
err := s.Serve(ctx, ls[cmd.AgentTypes["gpg"]], exit, cmd.ExitTimeout)
if err != nil {
log.Debug("exiting GPG server", zap.Error(err))
Expand Down
47 changes: 38 additions & 9 deletions internal/assuan/assuan.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"fmt"
"io"
Expand All @@ -27,6 +28,11 @@ type PIVService interface {
SecurityKeys() ([]pivservice.SecurityKey, error)
}

// The GPGService interface provides GPG functions used by the Assuan FSM.
type GPGService interface {
GetKey([]byte) *rsa.PrivateKey
}

// hashFunction maps the code used by assuan to the relevant hash function.
var hashFunction = map[uint64]crypto.Hash{
8: crypto.SHA256,
Expand All @@ -35,7 +41,7 @@ var hashFunction = map[uint64]crypto.Hash{

// New initialises a new gpg-agent server assuan FSM.
// It returns a *fsm.Machine configured in the ready state.
func New(w io.Writer, p PIVService) *Assuan {
func New(w io.Writer, p PIVService, g GPGService) *Assuan {
var err error
var keyFound bool
var keygrip, signature []byte
Expand Down Expand Up @@ -77,7 +83,7 @@ func New(w io.Writer, p PIVService) *Assuan {
if err != nil {
return fmt.Errorf("couldn't decode keygrips: %v", err)
}
keyFound, _, err = haveKey(p, keygrips)
keyFound, _, err = haveKey(p, g, keygrips)
if err != nil {
_, _ = io.WriteString(w, "ERR 1 couldn't check for keygrip\n")
return fmt.Errorf("couldn't check for keygrip: %v", err)
Expand All @@ -95,7 +101,7 @@ func New(w io.Writer, p PIVService) *Assuan {
if err != nil {
return fmt.Errorf("couldn't decode keygrips: %v", err)
}
keyFound, keygrip, err = haveKey(p, keygrips)
keyFound, keygrip, err = haveKey(p, g, keygrips)
if err != nil {
_, _ = io.WriteString(w, "ERR 1 couldn't check for keygrip\n")
return fmt.Errorf("couldn't check for keygrip: %v", err)
Expand Down Expand Up @@ -124,7 +130,11 @@ func New(w io.Writer, p PIVService) *Assuan {
if err != nil {
return fmt.Errorf("couldn't decode keygrips: %v", err)
}
assuan.signingPrivKey, err = getKey(p, keygrips[0])
assuan.signingPrivKey, err = tokenSigner(p, keygrips[0])
if err != nil {
// fall back to keyfiles
assuan.signingPrivKey, err = keyfileSigner(g, keygrips[0])
}
if err != nil {
_, _ = io.WriteString(w, "ERR 1 couldn't get key from keygrip\n")
return fmt.Errorf("couldn't get key from keygrip: %v", err)
Expand Down Expand Up @@ -188,19 +198,20 @@ func New(w io.Writer, p PIVService) *Assuan {
// PIVService, and false otherwise.
// It takes keygrips in raw byte format, so keygrip in hex-encoded form must
// first be decoded before being passed to this function.
func haveKey(p PIVService, keygrips [][]byte) (bool, []byte, error) {
func haveKey(p PIVService, g GPGService, keygrips [][]byte) (bool, []byte, error) {
securityKeys, err := p.SecurityKeys()
if err != nil {
return false, nil, fmt.Errorf("couldn't get security keys: %w", err)
}
// check against tokens
for _, sk := range securityKeys {
for _, signingKey := range sk.SigningKeys() {
ecdsaPubKey, ok := signingKey.Public.(*ecdsa.PublicKey)
if !ok {
// TODO: handle other key types
continue
}
thisKeygrip, err := gpg.Keygrip(ecdsaPubKey)
thisKeygrip, err := gpg.KeygripECDSA(ecdsaPubKey)
if err != nil {
return false, nil, fmt.Errorf("couldn't get keygrip: %w", err)
}
Expand All @@ -211,14 +222,20 @@ func haveKey(p PIVService, keygrips [][]byte) (bool, []byte, error) {
}
}
}
// also check against keyfiles
for _, kg := range keygrips {
if key := g.GetKey(kg); key != nil {
return true, kg, nil
}
}
return false, nil, nil
}

// getKey returns the security key associated with the given keygrip.
// tokenSigner returns the security key associated with the given keygrip.
// If the keygrip doesn't match any known key, err will be non-nil.
// It takes a keygrip in raw byte format, so a keygrip in hex-encoded form must
// first be decoded before being passed to this function.
func getKey(p PIVService, keygrip []byte) (crypto.Signer, error) {
func tokenSigner(p PIVService, keygrip []byte) (crypto.Signer, error) {
securityKeys, err := p.SecurityKeys()
if err != nil {
return nil, fmt.Errorf("couldn't get security keys: %w", err)
Expand All @@ -230,7 +247,7 @@ func getKey(p PIVService, keygrip []byte) (crypto.Signer, error) {
// TODO: handle other key types
continue
}
thisKeygrip, err := gpg.Keygrip(ecdsaPubKey)
thisKeygrip, err := gpg.KeygripECDSA(ecdsaPubKey)
if err != nil {
return nil, fmt.Errorf("couldn't get keygrip: %w", err)
}
Expand All @@ -250,6 +267,18 @@ func getKey(p PIVService, keygrip []byte) (crypto.Signer, error) {
return nil, fmt.Errorf("no matching key")
}

// keyfileSigner returns a crypto.Signer associated with the given keygrip.
// If the keygrip doesn't match any known key, err will be non-nil.
// It takes a keygrip in raw byte format, so a keygrip in hex-encoded form must
// first be decoded before being passed to this function.
// The path is to a secret keys file exported from gpg.
func keyfileSigner(g GPGService, keygrip []byte) (crypto.Signer, error) {
if key := g.GetKey(keygrip); key != nil {
return key, nil
}
return nil, fmt.Errorf("no matching key")
}

func hexDecode(data ...[]byte) ([][]byte, error) {
var decoded [][]byte
for _, d := range data {
Expand Down
46 changes: 44 additions & 2 deletions internal/assuan/assuan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"crypto"
"crypto/ecdsa"
"encoding/hex"
"fmt"
"io"
"math/big"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/smlx/piv-agent/internal/mock"
"github.com/smlx/piv-agent/internal/pivservice"
"github.com/smlx/piv-agent/internal/securitykey"
"github.com/smlx/piv-agent/internal/server"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
"golang.org/x/crypto/openpgp"
Expand Down Expand Up @@ -130,7 +132,7 @@ func TestSign(t *testing.T) {
writeBuf := bytes.Buffer{}
// readBuf is the buffer that the assuan statemachine reads from
readBuf := bytes.Buffer{}
a := assuan.New(&writeBuf, pivService)
a := assuan.New(&writeBuf, pivService, nil)
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := readBuf.WriteString(in); err != nil {
Expand Down Expand Up @@ -194,7 +196,7 @@ func TestKeyinfo(t *testing.T) {
writeBuf := bytes.Buffer{}
// readBuf is the buffer that the assuan statemachine reads from
readBuf := bytes.Buffer{}
a := assuan.New(&writeBuf, pivService)
a := assuan.New(&writeBuf, pivService, nil)
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := readBuf.WriteString(in); err != nil {
Expand Down Expand Up @@ -247,3 +249,43 @@ func ecdsaPubKeyLoad(path string) (*ecdsa.PublicKey, error) {
}
return eccKey, nil
}

func hexMustDecode(s string) []byte {
raw, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return raw
}

func TestKeyfileSigner(t *testing.T) {
var testCases = map[string]struct {
path string
keygrip []byte
protected bool
}{
"unprotected key": {
path: "testdata/private/bar@example.com.gpg",
keygrip: hexMustDecode("23F4477DF0F0C0963F8C4DFDEA8911CE65CC7CE3"),
},
"protected key": {
path: "testdata/private/bar-protected@example.com.gpg",
keygrip: hexMustDecode("75B7C5A35213E71BA282F64317DDB90EC5C3FEE0"),
protected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
ctrl := gomock.NewController(tt)
defer ctrl.Finish()
var mockPES = mock.NewMockPINEntryService(ctrl)
if tc.protected {
mockPES.EXPECT().GetPGPPassphrase(gomock.Any()).Return([]byte("trustno1"), nil)
}
g := server.NewGPG(nil, mockPES, nil, tc.path)
if _, err := assuan.KeyfileSigner(g, tc.keygrip); err != nil {
tt.Fatalf("couldn't find keygrip: %v", err)
}
})
}
}
3 changes: 3 additions & 0 deletions internal/assuan/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package assuan

import (
"crypto"
"crypto/rsa"
"sync"

"github.com/smlx/fsm"
Expand Down Expand Up @@ -54,6 +55,8 @@ type Assuan struct {
signingPrivKey crypto.Signer
hashAlgo crypto.Hash
hash []byte
// fallback keys
fallbackRSA []rsa.PrivateKey
}

// Occur handles an event occurence.
Expand Down
8 changes: 8 additions & 0 deletions internal/assuan/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package assuan

import "crypto"

// KeyfileSigner wraps keyfileSigner() for testing purposes.
func KeyfileSigner(g GPGService, keygrip []byte) (crypto.Signer, error) {
return keyfileSigner(g, keygrip)
}
Binary file not shown.
Binary file not shown.
13 changes: 11 additions & 2 deletions internal/gpg/keygrip.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gpg
import (
"bytes"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha1"
"fmt"
"math/big"
Expand All @@ -13,7 +14,7 @@ type part struct {
value []byte
}

// Keygrip calculates a keygrip for an ECDSA public key. This is a SHA1 hash of
// KeygripECDSA calculates a keygrip for an ECDSA public key. This is a SHA1 hash of
// public key parameters. It is pretty much undocumented outside of the
// libgcrypt codebase.
//
Expand All @@ -22,7 +23,7 @@ type part struct {
// key is byte-encoded, the parts are s-exp encoded in a particular order, and
// then the s-exp is sha1-hashed to produced the keygrip, which is generally
// displayed hex-encoded.
func Keygrip(pubKey *ecdsa.PublicKey) ([]byte, error) {
func KeygripECDSA(pubKey *ecdsa.PublicKey) ([]byte, error) {
if pubKey == nil {
return nil, fmt.Errorf("nil key")
}
Expand Down Expand Up @@ -85,3 +86,11 @@ func compute(parts []part) ([]byte, error) {
s := sha1.Sum(h.Bytes())
return s[:], nil
}

// KeygripRSA calculates a keygrip for an RSA public key.
func KeygripRSA(pubKey *rsa.PublicKey) []byte {
keygrip := sha1.New()
keygrip.Write([]byte{0})
keygrip.Write(pubKey.N.Bytes())
return keygrip.Sum(nil)
}
4 changes: 2 additions & 2 deletions internal/gpg/keygrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestTrezorCompat(t *testing.T) {
priv.D = tc.input
priv.PublicKey.X, priv.PublicKey.Y = curve.ScalarBaseMult(tc.input.Bytes())

keygrip, err := gpg.Keygrip(&priv.PublicKey)
keygrip, err := gpg.KeygripECDSA(&priv.PublicKey)
if err != nil {
tt.Fatal(err)
}
Expand Down Expand Up @@ -90,7 +90,7 @@ func TestKeyGrip(t *testing.T) {
tt.Fatal("wrong curve")
}

keygrip, err := gpg.Keygrip(eccKey)
keygrip, err := gpg.KeygripECDSA(eccKey)
if err != nil {
tt.Fatal(err)
}
Expand Down
38 changes: 38 additions & 0 deletions internal/mock/mock_assuan.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 0861193

Please sign in to comment.