Skip to content

Commit

Permalink
feat: implement ecdsa keyfile signing support
Browse files Browse the repository at this point in the history
  • Loading branch information
smlx committed Aug 10, 2021
1 parent 51d6056 commit e3a8b08
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 70 deletions.
36 changes: 34 additions & 2 deletions internal/assuan/assuan.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ func New(rw io.ReadWriter, log *zap.Logger, ks ...KeyService) *Assuan {
}
keyFound, keygrip, err = haveKey(ks, keygrips)
if err != nil {
_, _ = io.WriteString(rw, "ERR 1 couldn't check for keygrip\n")
return fmt.Errorf("couldn't check for keygrip: %v", err)
_, _ = io.WriteString(rw, "ERR 1 couldn't match keygrip\n")
return fmt.Errorf("couldn't match keygrip: %v", err)
}
if keyFound {
_, err = io.WriteString(rw,
Expand All @@ -108,6 +108,38 @@ func New(rw io.ReadWriter, log *zap.Logger, ks ...KeyService) *Assuan {
case scd:
// ignore scdaemon requests
_, err = io.WriteString(rw, "ERR 100696144 No such device <SCD>\n")
case readkey:
// READKEY argument is a keygrip
// return information about the given key
keygrips, err = hexDecode(assuan.data...)
if err != nil {
return fmt.Errorf("couldn't decode keygrips: %v", err)
}
var signer crypto.Signer
for _, k := range ks {
signer, err = k.GetSigner(keygrips[0])
if err == nil {
break
}
}
if signer == nil {
_, _ = io.WriteString(rw, "ERR 1 couldn't match keygrip\n")
return fmt.Errorf("couldn't match keygrip: %v", err)
}
readKeyData, err := readKeyData(signer.Public())
if err != nil {
_, _ = io.WriteString(rw, "ERR 1 couldn't get key data\n")
return fmt.Errorf("couldn't get key data: %v", err)
}
_, err = io.WriteString(rw, readKeyData)
case setkeydesc:
// ignore this event since we don't currently use the client's
// description in the prompt
_, err = io.WriteString(rw, "OK\n")
case passwd:
// ignore this event since we assume that if the key is decrypted the
// user has permissions
_, err = io.WriteString(rw, "OK\n")
default:
return fmt.Errorf("unknown event: %v", e)
}
Expand Down
86 changes: 86 additions & 0 deletions internal/assuan/assuan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,89 @@ func TestSignRSAKeyfile(t *testing.T) {
})
}
}

func TestReadKey(t *testing.T) {
var testCases = map[string]struct {
keyPath string
input []string
expect []string
}{
"rsa": {
keyPath: "testdata/private-subkeys",
input: []string{
"RESET\n",
"READKEY EA8E47C68880D1620FF10CC7CB91E5605758CC8D\n",
"SETKEYDESC Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key:%0A%22foo@example.com%22%0A3072-bit+RSA+key,+ID+AD024955495A860B,%0Acreated+2021-08-07.%0A\n",
"PASSWD --verify B242AADA8260B77F0F5069F127D6B7E4F44B5FAA\n",
},
expect: []string{
"OK Pleased to meet you, process 123456789\n",
"OK\n",
"\x44\x20\x28\x31\x30\x3a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x28\x33\x3a\x72\x73\x61\x28\x31\x3a\x6e\x33\x38\x35\x3a\x00\xbe\xe3\x07\x13\x3c\xae\xd7\x10\xe4\xdd\x84\x20\xc3\x96\xba\xdc\xe0\x09\x6d\xce\xbf\xc2\x55\xe3\x24\x4b\x96\x76\xf5\xd9\xcf\x02\x58\xbf\x69\x16\xcf\x2a\xa4\xdc\x8c\x82\x57\xb0\x5a\x16\x74\xf6\xd5\x21\xee\xdc\xce\x89\x64\xcd\x66\xf5\xee\x89\x09\xa6\x44\xce\x9d\x03\xc0\x44\x4d\x90\xdf\x60\x07\xc6\xf8\x2f\x98\x07\x9b\x95\xb3\xe5\x16\xb8\x1d\x59\xd1\x19\x97\x4c\x36\xbd\xce\xc7\xe1\x17\x7d\x6a\xdc\xa0\x16\x93\x2c\x91\x70\x7c\xf2\x1b\xd9\x5b\x4a\xd5\x46\x65\x9e\x09\xcc\x38\xbe\x86\xbd\xdd\xbf\x91\x7c\x04\x6c\xba\x38\xaf\xe6\xb4\xbb\x38\xa0\x3b\x3b\x07\x60\x2e\xbb\x6d\x45\x31\x1b\x0e\x37\x85\xdb\xa0\x93\xa5\x5c\xf6\xde\x69\x9e\x66\x3e\xa2\x3c\xf9\x59\x4b\x18\xc5\x5b\xdb\x4d\xa8\xcb\x80\xe6\xf9\x52\x1e\x2c\xb8\xab\xac\x7b\x14\xe9\xa8\x6a\x6d\xc6\x51\xb1\x74\x02\xa5\x13\x58\x66\x25\x32\x35\x3b\xed\xe3\x63\xb2\x7a\x8f\x93\x9b\x2c\x04\xdd\xf6\x56\xa9\xb2\x40\x34\xa9\x9b\xe6\xe1\x33\x5b\xe2\xa8\x12\x18\x48\x4e\xa6\xb7\xdd\xbf\xf0\xd2\x70\x18\x7b\x9d\xd3\xec\x55\x5f\xb7\xe8\x07\x1a\x90\x1e\xe4\x68\xa9\x67\x5c\xda\xe9\xea\x29\x19\xeb\x4c\x1c\x6a\x44\x06\x39\xea\xa2\xda\x29\x49\xdf\xd1\x00\x86\x5a\xe2\xe2\xe0\xa4\xa6\x2f\x74\x57\xbc\x78\x75\xa9\xd6\x81\xb1\x11\xbd\xca\x08\x17\x56\x9f\x42\xfe\x3f\x1a\xd1\x7e\xb2\x90\x27\x8a\x31\x8c\x88\x32\x3a\x28\x90\x10\xaf\x4d\xf8\x51\x94\x6f\x29\x21\xa4\x74\xfb\x65\x24\xcc\x5f\x48\x68\xdd\xff\x41\xb2\xe4\xa7\xbf\x25\x32\x35\xbe\x8d\xd8\x9f\x95\xd3\x7d\xe8\xf2\x4b\x78\xa1\x93\x29\xa5\x8b\xfa\x8d\x83\x6e\xbf\x9c\x5b\x1e\x38\xe3\x47\x60\xc6\xde\x4a\xd0\x78\x80\x6f\x20\xbf\xfd\x63\x12\x6f\xdd\xa3\x81\xf5\xf9\x29\x28\x31\x3a\x65\x33\x3a\x01\x00\x01\x29\x29\x29\x0a",
"OK\n",
"OK\n",
},
},
"ecdsa": {
keyPath: "testdata/private-subkeys",
input: []string{
"RESET\n",
"READKEY 586A6F8E9CD839FD26D868D084DDFEBB0CCC7EF0\n",
"SETKEYDESC Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key:%0A%22foo@example.com%22%0A3072-bit+RSA+key,+ID+AD024955495A860B,%0Acreated+2021-08-07.%0A\n",
"PASSWD --verify B242AADA8260B77F0F5069F127D6B7E4F44B5FAA\n",
},
expect: []string{
"OK Pleased to meet you, process 123456789\n",
"OK\n",
"\x44\x20\x28\x31\x30\x3a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x28\x33\x3a\x65\x63\x63\x28\x35\x3a\x63\x75\x72\x76\x65\x31\x30\x3a\x4e\x49\x53\x54\x20\x50\x2d\x32\x35\x36\x29\x28\x31\x3a\x71\x36\x35\x3a\x04\xbf\x06\xac\x95\x31\xae\x04\x93\x98\x21\x03\x83\x35\x9d\x4e\x58\x92\xa2\xe9\x24\x2f\x76\x54\x67\x45\xf0\x35\x28\xf4\x47\x14\x59\x26\x0c\xf9\x1b\x24\x10\x6b\x07\xe3\x33\x05\x4c\xcb\x96\xe2\xdd\x96\xd4\x0f\x3e\x4b\xd7\x67\x44\xdb\x82\x42\x24\xe6\x8b\x7f\xa6\x29\x29\x29\x0a",
"OK\n",
"OK\n",
},
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
ctrl := gomock.NewController(tt)
defer ctrl.Finish()
// no securityKeys available
mockPES := mock.NewMockPINEntryService(ctrl)
log, err := zap.NewDevelopment()
if err != nil {
tt.Fatal(err)
}
keyfileService, err := gpg.New(log, mockPES, tc.keyPath)
if err != nil {
tt.Fatal(err)
}
// mockConn is a pair of buffers that the assuan statemachine reads/write
// to/from.
mockConn := MockConn{}
a := assuan.New(&mockConn, log, keyfileService)
// write all the lines into the statemachine
for _, in := range tc.input {
if _, err := mockConn.ReadBuf.WriteString(in); err != nil {
tt.Fatal(err)
}
}
// start the state machine
if err := a.Run(); err != nil {
tt.Fatal(err)
}
// check the responses
for _, expected := range tc.expect {
//spew.Dump(mockConn.WriteBuf.String())
line, err := mockConn.WriteBuf.ReadString(byte('\n'))
if err != nil && err != io.EOF {
tt.Fatal(err)
}
if line != expected {
fmt.Println("got")
spew.Dump(line)
fmt.Println("expected")
spew.Dump(expected)
tt.Fatalf("error")
}
}
})
}
}
72 changes: 40 additions & 32 deletions internal/assuan/event_enumer.go

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

14 changes: 14 additions & 0 deletions internal/assuan/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
setkey
pkdecrypt
scd
readkey
passwd
)

//go:generate enumer -type=State -text -transform upper
Expand Down Expand Up @@ -102,6 +104,18 @@ var assuanTransitions = []fsm.Transition{
Src: fsm.State(connected),
Event: fsm.Event(scd),
Dst: fsm.State(connected),
}, {
Src: fsm.State(connected),
Event: fsm.Event(readkey),
Dst: fsm.State(connected),
}, {
Src: fsm.State(connected),
Event: fsm.Event(setkeydesc),
Dst: fsm.State(connected),
}, {
Src: fsm.State(connected),
Event: fsm.Event(passwd),
Dst: fsm.State(connected),
},
// signing transitions
{
Expand Down
39 changes: 39 additions & 0 deletions internal/assuan/readkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package assuan

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"fmt"
"math/big"
)

// readKeyData returns information about the given key in a libgcrypt-specific
// format
func readKeyData(pub crypto.PublicKey) (string, error) {
switch k := pub.(type) {
case *rsa.PublicKey:
n := k.N.Bytes()
nLen := len(n) // need the actual byte length before munging
n = percentEncodeSExp(n) // ugh
ei := new(big.Int)
ei.SetInt64(int64(k.E))
e := ei.Bytes()
// prefix the key with a null byte for compatibility
return fmt.Sprintf("D (10:public-key(3:rsa(1:n%d:\x00%s)(1:e%d:%s)))\n",
nLen+1, n, len(e), e), nil
case *ecdsa.PublicKey:
switch k.Curve {
case elliptic.P256():
q := elliptic.Marshal(k.Curve, k.X, k.Y)
return fmt.Sprintf(
"D (10:public-key(3:ecc(5:curve10:NIST P-256)(1:q%d:%s)))\n",
len(q), q), nil
default:
return "", fmt.Errorf("unsupported curve: %T", k.Curve)
}
default:
return "", nil
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit e3a8b08

Please sign in to comment.