Skip to content

Commit

Permalink
feat: implement ECDH for PIV hardware
Browse files Browse the repository at this point in the history
  • Loading branch information
smlx committed Oct 13, 2021
1 parent 266a7b3 commit 1c59de8
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 8 deletions.
5 changes: 4 additions & 1 deletion internal/assuan/assuan.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ func New(rw io.ReadWriter, log *zap.Logger, ks ...KeyService) *Assuan {
// ignore scdaemon requests
_, err = io.WriteString(rw, "ERR 100696144 No such device <SCD>\n")
case readkey:
// READKEY argument is a keygrip
// READKEY argument is a keygrip, optionally prefixed by "--".
if bytes.Equal(assuan.data[0], []byte("--")) {
assuan.data = assuan.data[1:]
}
// return information about the given key
keygrips, err = hexDecode(assuan.data...)
if err != nil {
Expand Down
56 changes: 56 additions & 0 deletions internal/keyservice/piv/ecdhkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package piv

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
"io"
"regexp"

pivgo "github.com/go-piv/piv-go/piv"
"github.com/smlx/piv-agent/internal/assuan"
)

var ciphertextECDH = regexp.MustCompile(
`^D \(7:enc-val\(4:ecdh\(1:s\d+:.+\)\(1:e(\d+):(.+)\)\)\)$`)

// ECDHKey implements ECDH using an underlying ECDSA key.
type ECDHKey struct {
ecdsa *pivgo.ECDSAPrivateKey
}

// Decrypt performs ECDH as per gpg-agent.
func (k *ECDHKey) Decrypt(_ io.Reader, sexp []byte,
_ crypto.DecrypterOpts) ([]byte, error) {
// parse out the ephemeral public key
matches := ciphertextECDH.FindAllSubmatch(sexp, -1)
ciphertext := matches[0][2]
// undo the buggy encoding sent by gpg
ciphertext = assuan.PercentDecodeSExp(ciphertext)
// unmarshal the ephemeral key
ephPubX, ephPubY := elliptic.Unmarshal(elliptic.P256(), ciphertext)
if ephPubX == nil {
return nil, fmt.Errorf("couldn't unmarshal ephemeral key")
}
// create the public key
ephPub := ecdsa.PublicKey{
Curve: elliptic.P256(),
X: ephPubX,
Y: ephPubY,
}
// marshal, encode, and return the result
shared, err := k.ecdsa.SharedKey(&ephPub)
if err != nil {
return nil, fmt.Errorf("couldn't generate shared secret: %v", err)
}
sharedLen := len(shared)
shared = assuan.PercentEncodeSExp(shared)
return []byte(fmt.Sprintf("D (5:value%d:%s)\nOK\n", sharedLen, shared)), nil
}

// Public implements the other required method of the crypto.Decrypter and
// crypto.Signer interfaces.
func (k *ECDHKey) Public() crypto.PublicKey {
return k.ecdsa.Public()
}
42 changes: 35 additions & 7 deletions internal/keyservice/piv/keyservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"sync"

pivgo "github.com/go-piv/piv-go/piv"
"github.com/smlx/piv-agent/internal/keyservice/gpg"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -40,8 +41,8 @@ func (p *KeyService) Keygrips() ([][]byte, error) {
return nil, fmt.Errorf("couldn't get security keys: %w", err)
}
for _, sk := range securityKeys {
for _, signingKey := range sk.SigningKeys() {
ecdsaPubKey, ok := signingKey.Public.(*ecdsa.PublicKey)
for _, cryptoKey := range sk.CryptoKeys() {
ecdsaPubKey, ok := cryptoKey.Public.(*ecdsa.PublicKey)
if !ok {
// TODO: handle other key types
continue
Expand Down Expand Up @@ -91,8 +92,8 @@ func (p *KeyService) GetSigner(keygrip []byte) (crypto.Signer, error) {
return nil, fmt.Errorf("couldn't get security keys: %w", err)
}
for _, sk := range securityKeys {
for _, signingKey := range sk.SigningKeys() {
ecdsaPubKey, ok := signingKey.Public.(*ecdsa.PublicKey)
for _, cryptoKey := range sk.CryptoKeys() {
ecdsaPubKey, ok := cryptoKey.Public.(*ecdsa.PublicKey)
if !ok {
// TODO: handle other key types
continue
Expand All @@ -102,7 +103,7 @@ func (p *KeyService) GetSigner(keygrip []byte) (crypto.Signer, error) {
return nil, fmt.Errorf("couldn't get keygrip: %w", err)
}
if bytes.Equal(thisKeygrip, keygrip) {
cryptoPrivKey, err := sk.PrivateKey(&signingKey.CryptoKey)
cryptoPrivKey, err := sk.PrivateKey(&cryptoKey)
if err != nil {
return nil, fmt.Errorf("couldn't get private key from slot")
}
Expand All @@ -119,6 +120,33 @@ func (p *KeyService) GetSigner(keygrip []byte) (crypto.Signer, error) {

// GetDecrypter returns a crypto.Decrypter associated with the given keygrip.
func (p *KeyService) GetDecrypter(keygrip []byte) (crypto.Decrypter, error) {
// TODO: implement this
return nil, fmt.Errorf("not implemented")
securityKeys, err := p.SecurityKeys()
if err != nil {
return nil, fmt.Errorf("couldn't get security keys: %w", err)
}
for _, sk := range securityKeys {
for _, cryptoKey := range sk.CryptoKeys() {
ecdsaPubKey, ok := cryptoKey.Public.(*ecdsa.PublicKey)
if !ok {
// TODO: handle other key types
continue
}
thisKeygrip, err := gpg.KeygripECDSA(ecdsaPubKey)
if err != nil {
return nil, fmt.Errorf("couldn't get keygrip: %w", err)
}
if bytes.Equal(thisKeygrip, keygrip) {
cryptoPrivKey, err := sk.PrivateKey(&cryptoKey)
if err != nil {
return nil, fmt.Errorf("couldn't get private key from slot")
}
privKey, ok := cryptoPrivKey.(*pivgo.ECDSAPrivateKey)
if !ok {
return nil, fmt.Errorf("private key is invalid type")
}
return &ECDHKey{ecdsa: privKey}, nil
}
}
}
return nil, fmt.Errorf("couldn't find keygrip")
}
1 change: 1 addition & 0 deletions internal/keyservice/piv/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type SecurityKey interface {
Comment(*securitykey.SlotSpec) string
PrivateKey(*securitykey.CryptoKey) (crypto.PrivateKey, error)
SigningKeys() []securitykey.SigningKey
CryptoKeys() []securitykey.CryptoKey
StringsGPG(string, string) ([]string, error)
StringsSSH() []string
}
Expand Down
14 changes: 14 additions & 0 deletions internal/mock/mock_pivservice.go

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

0 comments on commit 1c59de8

Please sign in to comment.