Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial yubikey backend keytype support #418

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package backend
import (
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/foxboron/sbctl/logging"
"io"
"os"
"path/filepath"
Expand Down Expand Up @@ -81,8 +83,6 @@ func (k *KeyHierarchy) GetKeyBackend(e efivar.Efivar) KeyBackend {
return k.KEK
case efivar.Db:
return k.Db
// case efivar.Dbx:
// return k.Dbx
default:
panic("invalid key hierarchy")
}
Expand Down Expand Up @@ -200,6 +200,8 @@ func createKey(state *config.State, backend string, hier hierarchy.Hierarchy, de
return NewFileKey(hier, desc)
case "tpm":
return NewTPMKey(state.TPM, desc)
case "yubikey":
return NewYubikeyKey(state.YubikeySigKeys, hier, desc)
default:
return NewFileKey(hier, desc)
}
Expand Down Expand Up @@ -255,6 +257,8 @@ func readKey(state *config.State, keydir string, kc *config.KeyConfig, hier hier
return FileKeyFromBytes(keyb, pemb)
case TPMBackend:
return TPMKeyFromBytes(state.TPM, keyb, pemb)
case YubikeyBackend:
return YubikeyFromBytes(state.YubikeySigKeys, keyb, pemb)
default:
return nil, fmt.Errorf("unknown key")
}
Expand Down Expand Up @@ -295,6 +299,14 @@ func GetKeyHierarchy(vfs afero.Fs, state *config.State) (*KeyHierarchy, error) {
}

func GetBackendType(b []byte) (BackendType, error) {
if json.Valid(b) {
var yubiData YubikeyData
if err := json.Unmarshal(b, &yubiData); err != nil {
logging.Errorf("Error unmarshalling Yubikey: %v\n", err)
return "", err
}
return YubikeyBackend, nil
}
block, _ := pem.Decode(b)
// TODO: Add TSS2 keys
switch block.Type {
Expand Down Expand Up @@ -322,6 +334,8 @@ func InitBackendFromKeys(state *config.State, priv, pem []byte, hier hierarchy.H
return FileKeyFromBytes(priv, pem)
case "tpm":
return TPMKeyFromBytes(state.TPM, priv, pem)
case "yubikey":
return YubikeyFromBytes(state.YubikeySigKeys, priv, pem)
default:
return nil, fmt.Errorf("unknown key backend: %s", t)
}
Expand Down
289 changes: 289 additions & 0 deletions backend/yubikey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
package backend

import (
"bytes"
"crypto"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/foxboron/sbctl/config"
"github.com/foxboron/sbctl/hierarchy"
"github.com/foxboron/sbctl/logging"
"math/big"
"os"
"strings"
"time"

"github.com/go-piv/piv-go/v2/piv"
)

type YubikeyData struct {
Info piv.KeyInfo `json:"Info"`
Slot string `json:"Slot"`
PublicKey string `json:"PublicKey"`
}

var YK *piv.YubiKey
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we can't throw the piv connetion into the Yubikey struct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to do that originally but I ended up needing to open up a piv connection and track piv state in YubikeyFromBytes during the signing workflow and it became more complicated. I ended up going with the most simple approach to start

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can't we just have a pointer in the struct and use connectToYubikey() as a struct method when the actual card is needed?

Everything seems fairly self-contained at the moment?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That approach works for creating keys, but the tricky part is in the Signer interface. the piv-go library only lets you have one handle to the yubikey open at once (https://github.com/go-piv/piv-go/blob/0383b0aa884b2b642e9e3446ea01ba22ccadc83a/v2/piv/piv.go#L107). When you defer yk-piv.Close() after creating the handle in Signer() the signing operation happens after the function is returned and by then the handle is closed. If you leave the handle open you can't open a new handle to the yubikey for future operations.

I can make it work but I would likely need to change the Signer interface to return a function that gets called after signing operations to appropriately Close the handle. What do you think about that approach?


type Yubikey struct {
keytype BackendType
cert *x509.Certificate
pubKeyInfo *piv.KeyInfo
}

func PIVKeyString(algorithm piv.Algorithm) string {
switch algorithm {
case piv.AlgorithmRSA2048:
return "RSA2048"
case piv.AlgorithmRSA3072:
return "RSA3072"
case piv.AlgorithmRSA4096:
return "RSA4096"
case piv.AlgorithmEC256:
return "EC256"
case piv.AlgorithmEC384:
return "EC384"
case piv.AlgorithmEd25519:
return "Ed25519"
case piv.AlgorithmX25519:
return "X25519"
default:
logging.Errorf("Unsupported Yubikey algorithm: %v", algorithm)
return ""
}
}

func NewYubikeyKey(conf *config.YubiConfig, hier hierarchy.Hierarchy, desc string) (*Yubikey, error) {
var pub crypto.PublicKey
if conf.PubKeyInfo != nil {
// Key already setup for sbctl
pub = conf.PubKeyInfo.PublicKey
} else {
// Find a YubiKey and open the reader.
if YK == nil {
var err error
YK, err = connectToYubikey()
if err != nil {
return nil, err
}
}

keyInfo, err := YK.KeyInfo(piv.SlotSignature)
if errors.As(err, &piv.ErrNotFound) {
keyInfo.PublicKey = nil
} else if err != nil {
return nil, err
}

if keyInfo.PublicKey != nil {
// if there is a key and it is RSA2048 and overwrite is false, use it
if keyInfo.Algorithm == piv.AlgorithmRSA2048 && !conf.Overwrite {
logging.Println("RSA2048 Key exists in Yubikey PIV Signature Slot, using the existing key")
pub = keyInfo.PublicKey
conf.PubKeyInfo = &keyInfo
} else if !conf.Overwrite {
return nil, fmt.Errorf("yubikey key creation failed; %s key present in signature slot", PIVKeyString(keyInfo.Algorithm))
}
}
// if there is no key or overwrite is set, create a new one
if keyInfo.PublicKey == nil || conf.Overwrite {
if conf.Overwrite {
logging.Warn("Overwriting existing key %s in Signature slot", PIVKeyString(keyInfo.Algorithm))
}

// Generate a private key on the YubiKey.
key := piv.Key{
Algorithm: piv.AlgorithmRSA2048,
PINPolicy: piv.PINPolicyAlways,
TouchPolicy: piv.TouchPolicyAlways,
}
logging.Println("Creating RSA2048 key...\nPlease press Yubikey to confirm presense")
pub, err = YK.GenerateKey(piv.DefaultManagementKey, piv.SlotSignature, key)
if err != nil {
return nil, err
}
newKeyInfo, err := YK.KeyInfo(piv.SlotSignature)
if err != nil {
return nil, err
}
conf.PubKeyInfo = &newKeyInfo
logging.Println(fmt.Sprintf("Created RSA2048 key MD5: %x", md5sum(newKeyInfo.PublicKey)))
}
}

auth := piv.KeyAuth{PIN: piv.DefaultPIN}
if pin, found := os.LookupEnv("SBCTL_YUBIKEY_PIN"); found {
auth = piv.KeyAuth{PIN: pin}
}
priv, err := YK.PrivateKey(piv.SlotSignature, pub, auth)
if err != nil {
return nil, err
}

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
c := x509.Certificate{
SerialNumber: serialNumber,
PublicKeyAlgorithm: x509.RSA,
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(5, 0, 0),
Subject: pkix.Name{
Country: []string{desc},
CommonName: desc,
},
}

logging.Println(fmt.Sprintf("Creating %s key...\nPlease press Yubikey to confirm presence for key %s MD5: %x",
hier.String(),
PIVKeyString(conf.PubKeyInfo.Algorithm),
md5sum(conf.PubKeyInfo.PublicKey)))
derBytes, err := x509.CreateCertificate(rand.Reader, &c, &c, pub, priv)
if err != nil {
return nil, err
}

cert, err := x509.ParseCertificate(derBytes)
if err != nil {
return nil, err
}

return &Yubikey{
keytype: YubikeyBackend,
cert: cert,
pubKeyInfo: conf.PubKeyInfo,
}, nil
}

func YubikeyFromBytes(c *config.YubiConfig, keyb, pemb []byte) (*Yubikey, error) {
var yubiData YubikeyData
err := json.Unmarshal(keyb, &yubiData)
if err != nil {
logging.Errorf("Error unmarshalling Yubikey: %v\n", err)
return nil, err
}

pubKeyB64, err := base64.StdEncoding.DecodeString(yubiData.PublicKey)
if err != nil {
return nil, err
}
pubKey, err := x509.ParsePKCS1PublicKey(pubKeyB64)
if err != nil {
logging.Errorf("Error parsing public key: %v\n", err)
return nil, err
}
yubiData.Info.PublicKey = pubKey

block, _ := pem.Decode(pemb)
if block == nil {
return nil, fmt.Errorf("no pem block")
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse cert: %w", err)
}

c.PubKeyInfo = &yubiData.Info
return &Yubikey{
keytype: YubikeyBackend,
cert: cert,
pubKeyInfo: &yubiData.Info,
}, nil
}

func connectToYubikey() (*piv.YubiKey, error) {
// List all smartcards connected to the system.
cards, err := piv.Cards()
if err != nil {
return nil, err
}

// Find a YubiKey and open the reader.
var yk *piv.YubiKey
for _, card := range cards {
if strings.Contains(strings.ToLower(card), "yubikey") {
if yk, err = piv.Open(card); err != nil {
return nil, err
}
}
}
if yk == nil {
return nil, fmt.Errorf("yubikey key not found")
}

return yk, nil
}

func (f *Yubikey) Type() BackendType { return f.keytype }
func (f *Yubikey) Certificate() *x509.Certificate { return f.cert }

func (f *Yubikey) Signer() crypto.Signer {
auth := piv.KeyAuth{PIN: piv.DefaultPIN}
if pin, found := os.LookupEnv("SBCTL_YUBIKEY_PIN"); found {
auth = piv.KeyAuth{PIN: pin}
}
if YK == nil {
var err error
YK, err = connectToYubikey()
if err != nil {
panic(err)
}
}
priv, err := YK.PrivateKey(piv.SlotSignature, f.pubKeyInfo.PublicKey, auth)
if err != nil {
panic(err)
}
logging.Println(fmt.Sprintf("Signing operation... please press Yubikey to confirm presence for key %s MD5: %x",
PIVKeyString(f.pubKeyInfo.Algorithm),
md5sum(f.pubKeyInfo.PublicKey)))
return priv.(crypto.Signer)
}

func (f *Yubikey) Description() string { return f.Certificate().Subject.SerialNumber }

// save YubiKey data to file
// the piv.pubKeyInfo.PublicKey is set to `nil` as its default marshal/unmarshal to json does not work
// the public key is instead saved in `PublicKey`
// Saves
//
// {
// "Info": piv.pubKeyInfo,
// "Slot": "signature",
// "PublicKey": publicKey
// }
func (f *Yubikey) PrivateKeyBytes() []byte {
yubiData := YubikeyData{
Info: *f.pubKeyInfo,
Slot: "signature",
PublicKey: base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PublicKey(f.pubKeyInfo.PublicKey.(*rsa.PublicKey))),
Comment on lines +263 to +266
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We same f.pubKeyInfo here, why do we have YubiConfig.PubKeyInfo as well stored as part of the state?

Is this to prevent us from reading the information from the yubikey multiple times?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also stored in the config to make sure when you want to overwrite or create a new one, it only creates a new one on signature slot in the yubikey once

}
yubiData.Info.PublicKey = nil

b, err := json.Marshal(yubiData)
if err != nil {
panic(err)
}
return b
}

func (f *Yubikey) CertificateBytes() []byte {
b := new(bytes.Buffer)
if err := pem.Encode(b, &pem.Block{Type: "CERTIFICATE", Bytes: f.cert.Raw}); err != nil {
panic("failed producing PEM encoded certificate")
}
return b.Bytes()
}

func md5sum(key crypto.PublicKey) []byte {
h := md5.New()
h.Write(x509.MarshalPKCS1PublicKey(key.(*rsa.PublicKey)))
return h.Sum(nil)
}
Loading