Skip to content

Commit

Permalink
feat: also list decryption keys for gnupg
Browse files Browse the repository at this point in the history
  • Loading branch information
smlx committed Oct 13, 2021
1 parent 75ec5b4 commit d7e1711
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 133 deletions.
1 change: 0 additions & 1 deletion cmd/piv-agent/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func (cmd *ListCmd) Run(l *zap.Logger) error {
}
}
if keyformats["gpg"] {
fmt.Println("\nGPG keys:")
for _, k := range securityKeys {
ss, err := k.StringsGPG(cmd.PGPName, cmd.PGPEmail)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion internal/assuan/assuan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ func TestKeyinfo(t *testing.T) {
defer ctrl.Finish()
var mockSecurityKey = mock.NewMockSecurityKey(ctrl)
mockSecurityKey.EXPECT().SigningKeys().AnyTimes().Return(
[]securitykey.SigningKey{{Public: pubKey}})
[]securitykey.SigningKey{
{CryptoKey: securitykey.CryptoKey{Public: pubKey}},
})
keyService := mock.NewMockKeyService(ctrl)
keyService.EXPECT().HaveKey(gomock.Any()).AnyTimes().Return(
true, keygrip, nil)
Expand Down
2 changes: 1 addition & 1 deletion internal/keyservice/piv/keyservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,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)
cryptoPrivKey, err := sk.PrivateKey(&signingKey.CryptoKey)
if err != nil {
return nil, fmt.Errorf("couldn't get private key from slot")
}
Expand Down
2 changes: 1 addition & 1 deletion internal/keyservice/piv/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type SecurityKey interface {
Card() string
Close() error
Comment(*securitykey.SlotSpec) string
PrivateKey(*securitykey.SigningKey) (crypto.PrivateKey, error)
PrivateKey(*securitykey.CryptoKey) (crypto.PrivateKey, error)
SigningKeys() []securitykey.SigningKey
StringsGPG(string, string) ([]string, error)
StringsSSH() []string
Expand Down
2 changes: 1 addition & 1 deletion internal/mock/mock_pivservice.go

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

4 changes: 3 additions & 1 deletion internal/pinentry/pinentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

// A SecurityKey is a physical hardware token that requires a PIN.
type SecurityKey interface {
Card() string
Retries() (int, error)
Serial() uint32
}
Expand All @@ -32,7 +33,8 @@ func GetPin(k SecurityKey) func() (string, error) {
return "", fmt.Errorf("couldn't get retries for security key: %w", err)
}
err = p.Set("desc",
fmt.Sprintf("serial number: %d, attempts remaining: %d", k.Serial(), r))
fmt.Sprintf("%s #%d\r(%d attempts remaining)",
k.Card(), k.Serial(), r))
if err != nil {
return "", fmt.Errorf("couldn't set desc on PIN pinentry: %w", err)
}
Expand Down
44 changes: 44 additions & 0 deletions internal/securitykey/decryptionkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package securitykey

import (
"crypto/ecdsa"
"errors"
"fmt"

"github.com/go-piv/piv-go/piv"
"golang.org/x/crypto/openpgp/packet"
)

// DecryptionKey is a cryptographic decryption key on a hardware security
// device.
type DecryptionKey struct {
CryptoKey
PubPGP *packet.PublicKey
}

// decryptionKeys returns the decryption keys available on the given yubikey.
func decryptionKeys(yk *piv.YubiKey) ([]DecryptionKey, error) {
var decryptionKeys []DecryptionKey
for _, s := range defaultDecryptSlots {
cert, err := yk.Certificate(s.Slot)
if err != nil {
if errors.Is(err, piv.ErrNotFound) {
continue
}
return nil, fmt.Errorf("couldn't get certificate for slot %x: %v",
s.Slot.Key, err)
}
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("invalid public key type: %T", cert.PublicKey)
}
decryptionKeys = append(decryptionKeys, DecryptionKey{
CryptoKey: CryptoKey{
Public: pubKey,
SlotSpec: s,
},
PubPGP: packet.NewECDSAPublicKey(cert.NotBefore, pubKey),
})
}
return decryptionKeys, nil
}
96 changes: 43 additions & 53 deletions internal/securitykey/securitykey.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,29 @@ package securitykey

import (
"crypto"
"crypto/ecdsa"
"crypto/x509"
"errors"
"fmt"

"github.com/go-piv/piv-go/piv"
"github.com/smlx/piv-agent/internal/pinentry"
"golang.org/x/crypto/openpgp/packet"
"golang.org/x/crypto/ssh"
)

// SigningKey is a public signing key on a security key / hardware token.
type SigningKey struct {
SlotSpec *SlotSpec
Public crypto.PublicKey
PubSSH ssh.PublicKey
PubPGP *packet.PublicKey
}

// A SecurityKey is a physical hardware token which implements PIV, such as a
// Yubikey. It provides a convenient abstraction around the low-level
// piv.YubiKey object.
type SecurityKey struct {
card string
serial uint32
yubikey *piv.YubiKey
signingKeys []SigningKey
card string
serial uint32
yubikey *piv.YubiKey
signingKeys []SigningKey
decryptionKeys []DecryptionKey
cryptoKeys []CryptoKey
}

// signingKeys returns the signing keys available on the given yubikey.
func signingKeys(yk *piv.YubiKey) ([]SigningKey, error) {
var signingKeys []SigningKey
for _, s := range defaultSignSlots {
cert, err := yk.Certificate(s.Slot)
if err != nil {
if errors.Is(err, piv.ErrNotFound) {
continue
}
return nil, fmt.Errorf("couldn't get certificate for slot %x: %v",
s.Slot.Key, err)
}
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("invalid public key type: %T", cert.PublicKey)
}
pubSSH, err := ssh.NewPublicKey(cert.PublicKey)
if err != nil {
return nil, fmt.Errorf("couldn't convert public key: %v", err)
}
signingKeys = append(signingKeys, SigningKey{
Public: pubKey,
PubSSH: pubSSH,
PubPGP: packet.NewECDSAPublicKey(cert.NotBefore, pubKey),
SlotSpec: &SlotSpec{
Slot: s.Slot,
TouchPolicy: s.TouchPolicy,
},
})
}
return signingKeys, nil
// CryptoKey represents a cryptographic key on a hardware security device.
type CryptoKey struct {
SlotSpec SlotSpec
Public crypto.PublicKey
}

// New returns a security key identified by card string.
Expand All @@ -75,16 +38,32 @@ func New(card string) (*SecurityKey, error) {
return nil, fmt.Errorf(`couldn't get serial for card "%s": %v`,
card, err)
}

signingKeys, err := signingKeys(yk)
if err != nil {
return nil, fmt.Errorf(`couldn't get signing keys for card "%s": %v`,
card, err)
}
var cryptoKeys []CryptoKey
for _, k := range signingKeys {
cryptoKeys = append(cryptoKeys, k.CryptoKey)
}

decryptionKeys, err := decryptionKeys(yk)
if err != nil {
return nil, fmt.Errorf(`couldn't get decryption keys for card "%s": %v`,
card, err)
}
for _, k := range decryptionKeys {
cryptoKeys = append(cryptoKeys, k.CryptoKey)
}
return &SecurityKey{
card: card,
serial: serial,
yubikey: yk,
signingKeys: signingKeys,
card: card,
serial: serial,
yubikey: yk,
signingKeys: signingKeys,
decryptionKeys: decryptionKeys,
cryptoKeys: cryptoKeys,
}, nil
}

Expand All @@ -103,9 +82,20 @@ func (k *SecurityKey) SigningKeys() []SigningKey {
return k.signingKeys
}

// DecryptionKeys returns the slice of decryption keys held by the SecurityKey.
func (k *SecurityKey) DecryptionKeys() []DecryptionKey {
return k.decryptionKeys
}

// CryptoKeys returns the slice of cryptographic signing and decryption keys
// held by the SecurityKey.
func (k *SecurityKey) CryptoKeys() []CryptoKey {
return k.cryptoKeys
}

// PrivateKey returns the private key of the given public signing key.
func (k *SecurityKey) PrivateKey(s *SigningKey) (crypto.PrivateKey, error) {
return k.yubikey.PrivateKey(s.SlotSpec.Slot, s.Public,
func (k *SecurityKey) PrivateKey(c *CryptoKey) (crypto.PrivateKey, error) {
return k.yubikey.PrivateKey(c.SlotSpec.Slot, c.Public,
piv.KeyAuth{PINPrompt: pinentry.GetPin(k)})
}

Expand Down
50 changes: 50 additions & 0 deletions internal/securitykey/signingkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package securitykey

import (
"crypto/ecdsa"
"errors"
"fmt"

"github.com/go-piv/piv-go/piv"
"golang.org/x/crypto/openpgp/packet"
"golang.org/x/crypto/ssh"
)

// SigningKey is a public signing key on a security key / hardware token.
type SigningKey struct {
CryptoKey
PubSSH ssh.PublicKey
PubPGP *packet.PublicKey
}

// signingKeys returns the signing keys available on the given yubikey.
func signingKeys(yk *piv.YubiKey) ([]SigningKey, error) {
var signingKeys []SigningKey
for _, s := range defaultSignSlots {
cert, err := yk.Certificate(s.Slot)
if err != nil {
if errors.Is(err, piv.ErrNotFound) {
continue
}
return nil, fmt.Errorf("couldn't get certificate for slot %x: %v",
s.Slot.Key, err)
}
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("invalid public key type: %T", cert.PublicKey)
}
pubSSH, err := ssh.NewPublicKey(cert.PublicKey)
if err != nil {
return nil, fmt.Errorf("couldn't convert public key: %v", err)
}
signingKeys = append(signingKeys, SigningKey{
CryptoKey: CryptoKey{
Public: pubKey,
SlotSpec: s,
},
PubSSH: pubSSH,
PubPGP: packet.NewECDSAPublicKey(cert.NotBefore, pubKey),
})
}
return signingKeys, nil
}
Loading

0 comments on commit d7e1711

Please sign in to comment.