Skip to content

Commit

Permalink
feat: implement RSA keyfile signing
Browse files Browse the repository at this point in the history
  • Loading branch information
smlx committed Aug 8, 2021
1 parent dc30888 commit 1b8b506
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 16 deletions.
5 changes: 4 additions & 1 deletion internal/assuan/assuan.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,14 @@ func New(rw io.ReadWriter, log *zap.Logger, ks ...KeyService) *Assuan {
}
if keyFound {
_, err = io.WriteString(rw,
fmt.Sprintf("S KEYINFO %s D - - - P - - -\nOK\n",
fmt.Sprintf("S KEYINFO %s D - - - - - - -\nOK\n",
strings.ToUpper(hex.EncodeToString(keygrip))))
} else {
_, err = io.WriteString(rw, "No_Secret_Key\n")
}
case scd:
// ignore scdaemon requests
_, err = io.WriteString(rw, "ERR 100696144 No such device <SCD>\n")
default:
return fmt.Errorf("unknown event: %v", e)
}
Expand Down
102 changes: 100 additions & 2 deletions internal/assuan/assuan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func TestKeyinfo(t *testing.T) {
expect: []string{
"OK Pleased to meet you, process 123456789\n",
"OK\n",
"S KEYINFO 38F053358EFD6C923D08EE4FC4CEB208CBCDF73C D - - - P - - -\n",
"S KEYINFO 38F053358EFD6C923D08EE4FC4CEB208CBCDF73C D - - - - - - -\n",
"OK\n",
},
},
Expand Down Expand Up @@ -269,7 +269,7 @@ func ecdsaPubKeyLoad(path string) (*ecdsa.PublicKey, error) {
return eccKey, nil
}

func TestDecrypt(t *testing.T) {
func TestDecryptRSAKeyfile(t *testing.T) {
var testCases = map[string]struct {
keyPath string
input []string
Expand Down Expand Up @@ -367,3 +367,101 @@ func TestDecrypt(t *testing.T) {
})
}
}

func TestSignRSAKeyfile(t *testing.T) {
var testCases = map[string]struct {
keyPath string
input []string
expect []string
}{
// test data is taken from a successful decrypt by gpg-agent
"decrypt file": {
keyPath: "testdata/private/foo@example.com.priv.key",
input: []string{
"RESET\n",
"OPTION ttyname=/dev/pts/1\n",
"OPTION ttytype=screen\n",
"OPTION lc-ctype=C.UTF-8\n",
"OPTION lc-messages=C\n",
"GETINFO version\n",
"OPTION allow-pinentry-notify\n",
"OPTION agent-awareness=2.1.0\n",
"SCD SERIALNO\n",
"HAVEKEY FC0F9A401ADDB33C0F7225CCA83BFC14E7FEBC7D\n",
"KEYINFO FC0F9A401ADDB33C0F7225CCA83BFC14E7FEBC7D\n",
"RESET\n",
"SIGKEY FC0F9A401ADDB33C0F7225CCA83BFC14E7FEBC7D\n",
"SETKEYDESC Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key:%0A%22foo@example.com%22%0A3072-bit+RSA+key,+ID+8D0381C18D1E7CA6,%0Acreated+2021-08-04.%0A\n",
"SETHASH 8 5963E1FA635CA32A85CA43CDCE3CB7A0CB0429B0EB1A94D1AEF08801D3BEB465\n",
"PKSIGN\n",
},
expect: []string{
"OK Pleased to meet you, process 123456789\n",
"OK\n",
"OK\n",
"OK\n",
"OK\n",
"OK\n",
"D 2.2.27\n",
"OK\n",
"OK\n",
"OK\n",
"ERR 100696144 No such device <SCD>\n",
"OK\n",
"S KEYINFO FC0F9A401ADDB33C0F7225CCA83BFC14E7FEBC7D D - - - - - - -\n",
"OK\n",
"OK\n",
"OK\n",
"OK\n",
"OK\n",
"\x44\x20\x28\x37\x3a\x73\x69\x67\x2d\x76\x61\x6c\x28\x33\x3a\x72\x73\x61\x28\x31\x3a\x73\x33\x38\x34\x3a\xb3\x26\x74\x5f\x59\xb5\x50\x8a\x46\x37\xa0\xc0\x91\x3a\x4b\x18\x61\xcb\x4f\xd2\x52\x5d\xbc\xe5\x51\x41\x00\x25\x30\x44\x08\x20\x25\x30\x41\xac\x0b\xff\x3e\xed\x6a\xa4\xf0\xdc\xb9\x1f\x8f\x76\xf1\x30\x8f\xce\xdc\xf5\x79\x2d\x2f\x06\x52\x3b\x49\xd5\x7d\xa1\x4a\xa2\x38\x81\x56\x6c\x59\xb0\x56\x22\xd8\x13\xeb\x7a\xee\xb1\xc5\xd6\xe9\xa0\x3a\xf4\x1b\x12\xa0\x85\x74\xe9\x93\x80\x7d\x7f\x24\xc8\x59\x9d\xb2\x8a\xe6\xc3\x95\xee\x50\x4c\x12\x4a\x1d\x84\x46\x3f\xa2\xc8\x96\xc2\xdf\xb7\x3d\x54\xa0\x55\x4a\x46\x4b\x35\x9f\xf0\x32\x9a\xd9\x0e\xe8\xa3\xa9\xb1\x3b\xa6\x52\x63\x02\xce\x36\x8f\x94\x18\x39\x3e\x11\x26\xb0\xa9\x71\xb8\x1c\x35\x47\xe8\x78\x8d\x12\xcf\x42\x96\xc7\x37\x25\x30\x41\x16\xa4\xbb\x83\x42\xe0\xa7\xed\x11\x35\x84\x5b\x40\xcd\x52\xc5\xd2\xf4\xe2\x86\x8b\x23\x42\x54\xda\xd1\xcd\xfc\x3e\xb2\x84\x1e\x2b\x04\xfb\x72\x04\x2f\xa9\x80\xf7\xa3\x13\x9a\xee\xe0\x26\x17\x6f\xdb\x57\x91\x85\xce\xbc\x5a\x97\x62\x8b\xa4\xa2\x54\x1c\x03\xc0\x3a\x9b\x8e\x4b\x32\x5e\x39\x71\x25\x30\x44\x8e\xae\x14\x09\x05\xcb\x77\x8d\x61\x2a\x4b\x1f\x19\x21\x8a\x68\x80\xd0\x4e\x53\x30\xc3\xab\x03\xd3\x79\x77\x55\xff\x2e\x46\xe3\x08\x03\x86\xef\xe1\xed\x34\x20\x08\x7a\xee\x1f\x0e\xd6\xf0\xbe\xe7\xdd\xab\xf6\x46\xec\xce\xd5\xa6\xc4\xf4\x02\x58\x5a\xcb\x6d\x9f\x2e\xf7\x24\x71\x9e\x13\x24\x22\x42\xe4\x48\xd5\x25\x32\x35\x1f\xac\xfc\x2c\xe2\x5c\x7c\xdb\xaf\xd2\x45\x3c\x99\xe1\xba\xd3\xd4\x95\x9d\xf8\xa1\x21\xca\x3f\xf9\x7b\x08\x50\x75\x13\x7a\x3d\xc9\x48\x9d\x4a\x93\xb6\xb5\x7a\x15\xef\xa6\x4d\xa9\x87\x41\x0e\xde\x25\x32\x35\x04\x18\x41\xa9\x4d\x9c\xbf\x12\x1f\x48\xc0\xa8\x92\xfd\x37\x7d\xec\x29\x29\x29\x0a",
"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.NewKeyfileService(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")
}
}
})
}
}
12 changes: 8 additions & 4 deletions internal/assuan/event_enumer.go

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

5 changes: 5 additions & 0 deletions internal/assuan/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
pksign
setkey
pkdecrypt
scd
)

//go:generate enumer -type=State -text -transform upper
Expand Down Expand Up @@ -97,6 +98,10 @@ var assuanTransitions = []fsm.Transition{
Src: fsm.State(connected),
Event: fsm.Event(keyinfo),
Dst: fsm.State(connected),
}, {
Src: fsm.State(connected),
Event: fsm.Event(scd),
Dst: fsm.State(connected),
},
// signing transitions
{
Expand Down
24 changes: 23 additions & 1 deletion internal/assuan/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"math/big"

"github.com/smlx/piv-agent/internal/gpg"
"github.com/smlx/piv-agent/internal/notify"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
Expand All @@ -13,12 +14,33 @@ import (
// sign performs signing of the specified "hash" data, using the specified
// "hashAlgo" hash algorithm. It then encodes the response into an s-expression
// and returns it as a byte slice.
func (a *Assuan) sign() ([]byte, error) {
switch a.signer.(type) {
case *gpg.RSAKey:
return a.signRSA()
default:
// default also handles mock signers in the test suite
return a.signECDSA()
}
}

// signRSA returns a signature for the given hash.
func (a *Assuan) signRSA() ([]byte, error) {
signature, err := a.signer.Sign(rand.Reader, a.hash, a.hashAlgo)
if err != nil {
return nil, fmt.Errorf("couldn't sign: %v", err)
}
return []byte(fmt.Sprintf(`D (7:sig-val(3:rsa(1:s%d:%s)))`, len(signature),
percentEncodeSExp(signature))), nil
}

// signECDSA returns a signature for the given hash.
//
// This function's complexity is due to the fact that while Sign() returns the
// r and s components of the signature ASN1-encoded, gpg expects them to be
// 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) sign() ([]byte, error) {
func (a *Assuan) signECDSA() ([]byte, error) {
cancel := notify.Touch(nil)
defer cancel()
signature, err := a.signer.Sign(rand.Reader, a.hash, a.hashAlgo)
Expand Down
13 changes: 6 additions & 7 deletions internal/gpg/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package gpg
import (
"crypto"
"crypto/rsa"
"fmt"
"io"
"math/big"
)
Expand All @@ -19,21 +18,21 @@ func (k *RSAKey) Decrypt(_ io.Reader, ciphertext []byte,
_ crypto.DecrypterOpts) ([]byte, error) {
c := new(big.Int)
c.SetBytes(ciphertext)
// libgcrypt does this, not sure if required
// TODO: libgcrypt does this, not sure if required?
c.Rem(c, k.rsa.N)
// perform arithmetic manually
c.Exp(c, k.rsa.D, k.rsa.N)
return c.Bytes(), nil
}

// Public implements the crypto.Decrypter interface.
// Public implements the other required method of the crypto.Decrypter and
// crypto.Signer interfaces.
func (k *RSAKey) Public() crypto.PublicKey {
return k.rsa.Public()
}

// Sign performs RSA signing as per gpg-agent.
func (k *RSAKey) Sign(_ io.Reader, digest []byte,
_ crypto.SignerOpts) ([]byte, error) {
// TODO: implement this
return nil, fmt.Errorf("not implemented")
func (k *RSAKey) Sign(r io.Reader, digest []byte,
o crypto.SignerOpts) ([]byte, error) {
return rsa.SignPKCS1v15(r, k.rsa, o.HashFunc(), digest)
}
2 changes: 1 addition & 1 deletion internal/pivservice/pivservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (p *PIVService) GetSigner(keygrip []byte) (crypto.Signer, error) {
}
}
}
return nil, nil
return nil, fmt.Errorf("couldn't find keygrip")
}

// GetDecrypter returns a crypto.Decrypter associated with the given keygrip.
Expand Down

0 comments on commit 1b8b506

Please sign in to comment.