-
Notifications
You must be signed in to change notification settings - Fork 91
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
|
||
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} | ||
tomis007 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We same Is this to prevent us from reading the information from the yubikey multiple times? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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 startThere was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 inSigner()
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?