diff --git a/internal/assuan/assuan.go b/internal/assuan/assuan.go index 1c703a6..cb92fef 100644 --- a/internal/assuan/assuan.go +++ b/internal/assuan/assuan.go @@ -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, @@ -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 \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) } diff --git a/internal/assuan/assuan_test.go b/internal/assuan/assuan_test.go index bd398b0..31aee80 100644 --- a/internal/assuan/assuan_test.go +++ b/internal/assuan/assuan_test.go @@ -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") + } + } + }) + } +} diff --git a/internal/assuan/event_enumer.go b/internal/assuan/event_enumer.go index 0821ab9..fcd08ff 100644 --- a/internal/assuan/event_enumer.go +++ b/internal/assuan/event_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _EventName = "INVALIDEVENTCONNECTRESETOPTIONGETINFOHAVEKEYKEYINFOSIGKEYSETKEYDESCSETHASHPKSIGNSETKEYPKDECRYPTSCD" +const _EventName = "INVALIDEVENTCONNECTRESETOPTIONGETINFOHAVEKEYKEYINFOSIGKEYSETKEYDESCSETHASHPKSIGNSETKEYPKDECRYPTSCDREADKEYPASSWD" -var _EventIndex = [...]uint8{0, 12, 19, 24, 30, 37, 44, 51, 57, 67, 74, 80, 86, 95, 98} +var _EventIndex = [...]uint8{0, 12, 19, 24, 30, 37, 44, 51, 57, 67, 74, 80, 86, 95, 98, 105, 111} -const _EventLowerName = "invalideventconnectresetoptiongetinfohavekeykeyinfosigkeysetkeydescsethashpksignsetkeypkdecryptscd" +const _EventLowerName = "invalideventconnectresetoptiongetinfohavekeykeyinfosigkeysetkeydescsethashpksignsetkeypkdecryptscdreadkeypasswd" func (i Event) String() string { if i < 0 || i >= Event(len(_EventIndex)-1) { @@ -38,39 +38,45 @@ func _EventNoOp() { _ = x[setkey-(11)] _ = x[pkdecrypt-(12)] _ = x[scd-(13)] + _ = x[readkey-(14)] + _ = x[passwd-(15)] } -var _EventValues = []Event{invalidEvent, connect, reset, option, getinfo, havekey, keyinfo, sigkey, setkeydesc, sethash, pksign, setkey, pkdecrypt, scd} +var _EventValues = []Event{invalidEvent, connect, reset, option, getinfo, havekey, keyinfo, sigkey, setkeydesc, sethash, pksign, setkey, pkdecrypt, scd, readkey, passwd} var _EventNameToValueMap = map[string]Event{ - _EventName[0:12]: invalidEvent, - _EventLowerName[0:12]: invalidEvent, - _EventName[12:19]: connect, - _EventLowerName[12:19]: connect, - _EventName[19:24]: reset, - _EventLowerName[19:24]: reset, - _EventName[24:30]: option, - _EventLowerName[24:30]: option, - _EventName[30:37]: getinfo, - _EventLowerName[30:37]: getinfo, - _EventName[37:44]: havekey, - _EventLowerName[37:44]: havekey, - _EventName[44:51]: keyinfo, - _EventLowerName[44:51]: keyinfo, - _EventName[51:57]: sigkey, - _EventLowerName[51:57]: sigkey, - _EventName[57:67]: setkeydesc, - _EventLowerName[57:67]: setkeydesc, - _EventName[67:74]: sethash, - _EventLowerName[67:74]: sethash, - _EventName[74:80]: pksign, - _EventLowerName[74:80]: pksign, - _EventName[80:86]: setkey, - _EventLowerName[80:86]: setkey, - _EventName[86:95]: pkdecrypt, - _EventLowerName[86:95]: pkdecrypt, - _EventName[95:98]: scd, - _EventLowerName[95:98]: scd, + _EventName[0:12]: invalidEvent, + _EventLowerName[0:12]: invalidEvent, + _EventName[12:19]: connect, + _EventLowerName[12:19]: connect, + _EventName[19:24]: reset, + _EventLowerName[19:24]: reset, + _EventName[24:30]: option, + _EventLowerName[24:30]: option, + _EventName[30:37]: getinfo, + _EventLowerName[30:37]: getinfo, + _EventName[37:44]: havekey, + _EventLowerName[37:44]: havekey, + _EventName[44:51]: keyinfo, + _EventLowerName[44:51]: keyinfo, + _EventName[51:57]: sigkey, + _EventLowerName[51:57]: sigkey, + _EventName[57:67]: setkeydesc, + _EventLowerName[57:67]: setkeydesc, + _EventName[67:74]: sethash, + _EventLowerName[67:74]: sethash, + _EventName[74:80]: pksign, + _EventLowerName[74:80]: pksign, + _EventName[80:86]: setkey, + _EventLowerName[80:86]: setkey, + _EventName[86:95]: pkdecrypt, + _EventLowerName[86:95]: pkdecrypt, + _EventName[95:98]: scd, + _EventLowerName[95:98]: scd, + _EventName[98:105]: readkey, + _EventLowerName[98:105]: readkey, + _EventName[105:111]: passwd, + _EventLowerName[105:111]: passwd, } var _EventNames = []string{ @@ -88,6 +94,8 @@ var _EventNames = []string{ _EventName[80:86], _EventName[86:95], _EventName[95:98], + _EventName[98:105], + _EventName[105:111], } // EventString retrieves an enum value from the enum constants string name. diff --git a/internal/assuan/fsm.go b/internal/assuan/fsm.go index 75e5e76..48c28d2 100644 --- a/internal/assuan/fsm.go +++ b/internal/assuan/fsm.go @@ -29,6 +29,8 @@ const ( setkey pkdecrypt scd + readkey + passwd ) //go:generate enumer -type=State -text -transform upper @@ -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 { diff --git a/internal/assuan/readkey.go b/internal/assuan/readkey.go new file mode 100644 index 0000000..c658c66 --- /dev/null +++ b/internal/assuan/readkey.go @@ -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 + } +} diff --git a/internal/assuan/testdata/private-subkeys/foo-sub-ecdsa@example.com.sub-ecdsa.gpg b/internal/assuan/testdata/private-subkeys/foo-sub-ecdsa@example.com.sub-ecdsa.gpg new file mode 100644 index 0000000..a068a75 Binary files /dev/null and b/internal/assuan/testdata/private-subkeys/foo-sub-ecdsa@example.com.sub-ecdsa.gpg differ diff --git a/internal/assuan/testdata/private-subkeys/foo-sub@example.com.sub-rsa.gpg b/internal/assuan/testdata/private-subkeys/foo-sub@example.com.sub-rsa.gpg new file mode 100644 index 0000000..e15a9a0 Binary files /dev/null and b/internal/assuan/testdata/private-subkeys/foo-sub@example.com.sub-rsa.gpg differ diff --git a/internal/assuan/testdata/private-subkeys/foo@example.com.primary-rsa.gpg b/internal/assuan/testdata/private-subkeys/foo@example.com.primary-rsa.gpg new file mode 100644 index 0000000..6e4862a Binary files /dev/null and b/internal/assuan/testdata/private-subkeys/foo@example.com.primary-rsa.gpg differ diff --git a/internal/keyservice/gpg/keyservice.go b/internal/keyservice/gpg/keyservice.go index a5792cf..19276f0 100644 --- a/internal/keyservice/gpg/keyservice.go +++ b/internal/keyservice/gpg/keyservice.go @@ -5,6 +5,7 @@ package gpg import ( "bytes" "crypto" + "crypto/ecdsa" "crypto/rsa" "fmt" @@ -55,7 +56,7 @@ func (*KeyService) Name() string { // the given keygrips were found, the found keygrip, and an error, if any. func (g *KeyService) HaveKey(keygrips [][]byte) (bool, []byte, error) { for _, kg := range keygrips { - key, err := g.getKey(kg) + key, err := g.getRSAKey(kg) if err != nil { return false, nil, err } @@ -66,10 +67,44 @@ func (g *KeyService) HaveKey(keygrips [][]byte) (bool, []byte, error) { return false, nil, nil } -// getKey returns a matching private RSA key if the keygrip matches. If a key -// is returned err will be nil. If no key is found, both values may be nil. -func (g *KeyService) getKey(keygrip []byte) (*rsa.PrivateKey, error) { +// decryptPrivateKey decrypts the given private key. +// Returns nil if successful, or an error if the key could not be decrypted. +func (g *KeyService) decryptPrivateKey(k *packet.PrivateKey, uid string) error { var pass []byte + var err error + if k.Encrypted { + // try existing passphrases + for _, pass := range g.passphrases { + if err = k.Decrypt(pass); err == nil { + g.log.Debug("decrypted using cached passphrase", + zap.String("fingerprint", k.KeyIdString())) + break + } + } + } + if k.Encrypted { + // ask for a passphrase + pass, err = g.pinentry.GetPGPPassphrase(uid, + fmt.Sprintf("%X %X %X %X", k.Fingerprint[:5], k.Fingerprint[5:10], + k.Fingerprint[10:15], k.Fingerprint[15:])) + if err != nil { + return fmt.Errorf("couldn't get passphrase for key %s: %v", + k.KeyIdString(), err) + } + g.passphrases = append(g.passphrases, pass) + if err = k.Decrypt(pass); err != nil { + return fmt.Errorf("couldn't decrypt key %s: %v", + k.KeyIdString(), err) + } + g.log.Debug("decrypted using passphrase", + zap.String("fingerprint", k.KeyIdString())) + } + return nil +} + +// getRSAKey returns a matching private RSA key if the keygrip matches. If a key +// is returned err will be nil. If no key is found, both values may be nil. +func (g *KeyService) getRSAKey(keygrip []byte) (*rsa.PrivateKey, error) { var err error for _, pk := range g.privKeys { for _, k := range pk.keys { @@ -80,32 +115,11 @@ func (g *KeyService) getKey(keygrip []byte) (*rsa.PrivateKey, error) { if !bytes.Equal(keygrip, keygripRSA(pubKey)) { continue } - if k.Encrypted { - // try existing passphrases - for _, pass := range g.passphrases { - if err = k.Decrypt(pass); err == nil { - g.log.Debug("decrypted using cached passphrase", - zap.String("fingerprint", k.KeyIdString())) - break - } - } - } - if k.Encrypted { - // ask for a passphrase - pass, err = g.pinentry.GetPGPPassphrase( - fmt.Sprintf("%s (%s) <%s>", pk.uid.Name, pk.uid.Comment, pk.uid.Email), - fmt.Sprintf("%X %X %X %X", k.Fingerprint[:5], k.Fingerprint[5:10], k.Fingerprint[10:15], k.Fingerprint[15:])) - if err != nil { - return nil, fmt.Errorf("couldn't get passphrase for key %s: %v", - k.KeyIdString(), err) - } - g.passphrases = append(g.passphrases, pass) - if err = k.Decrypt(pass); err != nil { - return nil, fmt.Errorf("couldn't decrypt key %s: %v", - k.KeyIdString(), err) - } - g.log.Debug("decrypted using passphrase", - zap.String("fingerprint", k.KeyIdString())) + err = g.decryptPrivateKey(k, + fmt.Sprintf("%s (%s) <%s>", + pk.uid.Name, pk.uid.Comment, pk.uid.Email)) + if err != nil { + return nil, err } privKey, ok := k.PrivateKey.(*rsa.PrivateKey) if !ok { @@ -118,20 +132,66 @@ func (g *KeyService) getKey(keygrip []byte) (*rsa.PrivateKey, error) { return nil, nil } +// getECDSAKey returns a matching private RSA key if the keygrip matches. If a key +// is returned err will be nil. If no key is found, both values may be nil. +func (g *KeyService) getECDSAKey(keygrip []byte) (*ecdsa.PrivateKey, error) { + for _, pk := range g.privKeys { + for _, k := range pk.keys { + pubKey, ok := k.PublicKey.PublicKey.(*ecdsa.PublicKey) + if !ok { + continue + } + pubKeygrip, err := KeygripECDSA(pubKey) + if err != nil { + return nil, fmt.Errorf("couldn't get ECDSA keygrip: %v", err) + } + if !bytes.Equal(keygrip, pubKeygrip) { + continue + } + err = g.decryptPrivateKey(k, + fmt.Sprintf("%s (%s) <%s>", + pk.uid.Name, pk.uid.Comment, pk.uid.Email)) + if err != nil { + return nil, err + } + privKey, ok := k.PrivateKey.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("not an ECDSA key %s: %v", + k.KeyIdString(), err) + } + return privKey, nil + } + } + return nil, nil +} + // GetSigner returns a crypto.Signer associated with the given keygrip. func (g *KeyService) GetSigner(keygrip []byte) (crypto.Signer, error) { - rsaPrivKey, err := g.getKey(keygrip) + rsaPrivKey, err := g.getRSAKey(keygrip) if err != nil { - return nil, fmt.Errorf("couldn't getKey: %v", err) + return nil, fmt.Errorf("couldn't getRSAKey: %v", err) } - return &RSAKey{rsa: rsaPrivKey}, nil + if rsaPrivKey != nil { + return &RSAKey{rsa: rsaPrivKey}, nil + } + ecdsaPrivKey, err := g.getECDSAKey(keygrip) + if err != nil { + return nil, fmt.Errorf("couldn't getECDSAKey: %v", err) + } + if ecdsaPrivKey != nil { + return ecdsaPrivKey, nil + } + return nil, fmt.Errorf("couldn't get signer for keygrip %X", keygrip) } // GetDecrypter returns a crypto.Decrypter associated with the given keygrip. func (g *KeyService) GetDecrypter(keygrip []byte) (crypto.Decrypter, error) { - rsaPrivKey, err := g.getKey(keygrip) + rsaPrivKey, err := g.getRSAKey(keygrip) if err != nil { - return nil, fmt.Errorf("couldn't getKey: %v", err) + return nil, fmt.Errorf("couldn't getRSAKey: %v", err) + } + if rsaPrivKey == nil { + return nil, fmt.Errorf("couldn't get decrypter for keygrip %X", keygrip) } return &RSAKey{rsa: rsaPrivKey}, nil } diff --git a/internal/keyservice/gpg/rsakey.go b/internal/keyservice/gpg/rsakey.go index 1ba1b6e..db5d6c3 100644 --- a/internal/keyservice/gpg/rsakey.go +++ b/internal/keyservice/gpg/rsakey.go @@ -7,13 +7,21 @@ import ( "math/big" ) -// RSAKey represents a GPG loaded from a keyfile. +// RSAKey represents a GPG key loaded from a keyfile. // It implements the crypto.Decrypter and crypto.Signer interfaces. type RSAKey struct { rsa *rsa.PrivateKey } // Decrypt performs RSA decryption as per gpg-agent. +// +// Terrible things about this function (not exhaustive): +// * rolling my own crypto +// * makes well-known RSA implementation mistakes +// * RSA in 2021 +// +// I'd love to not have to do this, but hey, it's for gnupg compatibility. +// Get in touch if you know how to improve this function. func (k *RSAKey) Decrypt(_ io.Reader, ciphertext []byte, _ crypto.DecrypterOpts) ([]byte, error) { c := new(big.Int)