From 49ae1291d6111b649212035ea12eeeb23b6df190 Mon Sep 17 00:00:00 2001 From: bsingh-kpt <161393521+bsingh-kpt@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:43:03 -0400 Subject: [PATCH] Yubikey enhancement: adds feature set to support multiple slots Following features are implemented: 1. Multiple slots of yubikey can be used 2. Algorithm support for RSA2048 and RSA3072 for yubikey type only 3. --keytype options enhancement. For yubikey and for each key type SB hierarchy algorithm and slot can be specified. For example, to create a RSA3072 key in slot 9a, --keytype yubikey:RSA3072:9a can be used. Different algorithm and slot can be chosen for each SB key type 4. Subject DN in openssl style can also be specified for certificate generation for each key type 5. KeyConfig is enahanced to support Algorithm and slot for yubikey type only 6. Added key file existence check so that only missing keys are created with create-keys command and avoids unintentional key overwrite 7. Check key certificate first in yubikey and then fallback to its attestation cert if key cert is missing 8. Also supports yubikey retired key slots 9. Adds --prompt option to enable pin prompt for yubikey 10. Adds custom management key support when default is replaced --- backend/backend.go | 27 ++-- backend/yubikey.go | 274 ++++++++++++++++++++++++++++++--------- cmd/sbctl/create-keys.go | 63 ++++++--- config/config.go | 3 + config/yubikeyreader.go | 48 ++++++- go.mod | 5 +- go.sum | 6 +- keys.go | 13 +- 8 files changed, 336 insertions(+), 103 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index eaf9410..1f77f9c 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -127,11 +127,11 @@ func (k *KeyHierarchy) RotateKeyWithBackend(hier hierarchy.Hierarchy, backend Ba var err error switch hier { case hierarchy.PK: - k.PK, err = createKey(k.state, string(backend), hier, k.PK.Description()) + k.PK, err = CreateKey(k.state, k.state.Config.Keys.PK, hier) case hierarchy.KEK: - k.KEK, err = createKey(k.state, string(backend), hier, k.KEK.Description()) + k.KEK, err = CreateKey(k.state, k.state.Config.Keys.KEK, hier) case hierarchy.Db: - k.Db, err = createKey(k.state, string(backend), hier, k.Db.Description()) + k.Db, err = CreateKey(k.state, k.state.Config.Keys.Db, hier) } return err } @@ -190,17 +190,18 @@ func (k *KeyHierarchy) SignFile(hier hierarchy.Hierarchy, peBinary *authenticode return peBinary.Bytes(), nil } -func createKey(state *config.State, backend string, hier hierarchy.Hierarchy, desc string) (KeyBackend, error) { +func CreateKey(state *config.State, key *config.KeyConfig, hier hierarchy.Hierarchy) (KeyBackend, error) { + desc := key.Description if desc == "" { desc = hier.Description() } - switch backend { + switch key.Type { case "file", "": return NewFileKey(hier, desc) case "tpm": return NewTPMKey(state.TPM, desc) case "yubikey": - return NewYubikeyKey(state.Yubikey, hier) + return NewYubikeyKey(state.Yubikey, hier, key) default: return NewFileKey(hier, desc) } @@ -211,17 +212,17 @@ func CreateKeys(state *config.State) (*KeyHierarchy, error) { var err error c := state.Config - hier.PK, err = createKey(state, c.Keys.PK.Type, hierarchy.PK, c.Keys.PK.Description) + hier.PK, err = CreateKey(state, c.Keys.PK, hierarchy.PK) if err != nil { return nil, err } - hier.KEK, err = createKey(state, c.Keys.KEK.Type, hierarchy.KEK, c.Keys.KEK.Description) + hier.KEK, err = CreateKey(state, c.Keys.KEK, hierarchy.KEK) if err != nil { return nil, err } - hier.Db, err = createKey(state, c.Keys.Db.Type, hierarchy.Db, c.Keys.Db.Description) + hier.Db, err = CreateKey(state, c.Keys.Db, hierarchy.Db) if err != nil { return nil, err } @@ -229,7 +230,7 @@ func CreateKeys(state *config.State) (*KeyHierarchy, error) { return &hier, nil } -func readKey(state *config.State, keydir string, kc *config.KeyConfig, hier hierarchy.Hierarchy) (KeyBackend, error) { +func readKey(state *config.State, keydir string, hier hierarchy.Hierarchy) (KeyBackend, error) { path := filepath.Join(keydir, hier.String()) keyname := filepath.Join(path, fmt.Sprintf("%s.key", hier.String())) certname := filepath.Join(path, fmt.Sprintf("%s.pem", hier.String())) @@ -267,11 +268,11 @@ func GetKeyBackend(state *config.State, k hierarchy.Hierarchy) (KeyBackend, erro c := state.Config switch k { case hierarchy.PK: - return readKey(state, c.Keydir, c.Keys.PK, k) + return readKey(state, c.Keydir, k) case hierarchy.KEK: - return readKey(state, c.Keydir, c.Keys.KEK, k) + return readKey(state, c.Keydir, k) case hierarchy.Db: - return readKey(state, c.Keydir, c.Keys.Db, k) + return readKey(state, c.Keydir, k) } return nil, nil } diff --git a/backend/yubikey.go b/backend/yubikey.go index 0c3eb13..4d4fdd4 100644 --- a/backend/yubikey.go +++ b/backend/yubikey.go @@ -14,7 +14,8 @@ import ( "errors" "fmt" "math/big" - "os" + "strconv" + "strings" "time" "github.com/foxboron/sbctl/config" @@ -36,65 +37,149 @@ type Yubikey struct { keytype BackendType cert *x509.Certificate yubikeyReader *config.YubikeyReader + slot piv.Slot algorithm piv.Algorithm pinPolicy piv.PINPolicy touchPolicy piv.TouchPolicy } -func NewYubikeyKey(yubikeyReader *config.YubikeyReader, hier hierarchy.Hierarchy) (*Yubikey, error) { - cert, err := yubikeyReader.GetPIVKeyCert() - if err != nil { - if !errors.Is(err, piv.ErrNotFound) { - return nil, fmt.Errorf("failed finding yubikey: %v", err) +func NewYubikeyKey(yubikeyReader *config.YubikeyReader, hier hierarchy.Hierarchy, keyConfig *config.KeyConfig) (*Yubikey, error) { + var slot piv.Slot + var slotName string + var pivAlg piv.Algorithm + + logging.Println(fmt.Sprintf("\nCreating %s (%s) key...", hier.Description(), hier.String())) + + switch keyConfig.Slot { + case "9c": + slot = piv.SlotSignature + slotName = "Signature" + fmt.Printf("Using slot: %s (%s)\n", slot.String(), slotName) + case "9a": + slot = piv.SlotAuthentication + slotName = "Authentication" + fmt.Printf("Using slot: %s (%s)\n", slot.String(), slotName) + case "9e": + slot = piv.SlotCardAuthentication + slotName = "CardAuthentication" + fmt.Printf("Using slot: %s (%s)\n", slot.String(), slotName) + case "9d": + slot = piv.SlotKeyManagement + slotName = "KeyManagement" + fmt.Printf("Using slot: %s (%s)\n", slot.String(), slotName) + default: + // maybe one of retired slots + var found bool = false + slotHexVal, err := strconv.ParseUint(keyConfig.Slot, 16, 8) + if err == nil { + slot, found = piv.RetiredKeyManagementSlot(uint32(slotHexVal)) + } + if !found { + return nil, fmt.Errorf("yubikey: Invalid key slot %s", keyConfig.Slot) } + slotName = fmt.Sprintf("RetiredKeyManagementSlot:0x%s", keyConfig.Slot) } - if cert != nil { - // if there is a key and it is RSA4096 and overwrite is false, use it + // Try Key cert first then fallback to Attestation cert + // FIXME: Should it be other way around? + cert, err := yubikeyReader.GetPIVKeyCert(slot) + if err != nil && !errors.Is(err, piv.ErrNotFound) { + return nil, fmt.Errorf("yubikey: failed finding yubikey: %v", err) + } + + // Fallback to attestation cert + if cert == nil { + cert, err = yubikeyReader.GetPIVAttestationCert(slot) + if err != nil && !errors.Is(err, piv.ErrNotFound) { + return nil, fmt.Errorf("yubikey: failed finding yubikey: %v", err) + } + } + + // if there is a key and overwrite is false, use it + if cert != nil && !yubikeyReader.Overwrite { + var keyAlgName string + switch yubiPub := cert.PublicKey.(type) { case *rsa.PublicKey: - // RSA4096 Public Key - if yubiPub.N.BitLen() == 4096 && !yubikeyReader.Overwrite { - logging.Println(fmt.Sprintf("Using RSA4096 Key MD5: %x in Yubikey PIV Signature Slot", md5sum(cert.PublicKey))) - } else if !yubikeyReader.Overwrite { - return nil, fmt.Errorf("yubikey key creation failed; %s key present in signature slot", cert.PublicKeyAlgorithm.String()) + // RSA Public Key + bitlen := yubiPub.N.BitLen() + if bitlen < 2048 { + return nil, fmt.Errorf("yubikey: key creation failed; %s key present in %s slot is less than 2048 bits", cert.PublicKeyAlgorithm.String(), slotName) } - } - } - // if overwrite or there is no piv key create one - if yubikeyReader.Overwrite || cert == nil { - if yubikeyReader.Overwrite { - logging.Warn("Overwriting existing key %s in Signature slot", cert.PublicKeyAlgorithm.String()) - } + keyAlgName = fmt.Sprintf("RSA%d", bitlen) + logging.Println(fmt.Sprintf("Using existing %s Key MD5: %x in Yubikey PIV %s Slot", keyAlgName, md5sum(cert.PublicKey), slotName)) - // Generate a private key on the YubiKey. - key := piv.Key{ - Algorithm: piv.AlgorithmRSA4096, - PINPolicy: piv.PINPolicyAlways, - TouchPolicy: piv.TouchPolicyAlways, + default: + if yubikeyReader.Overwrite { + return nil, fmt.Errorf("yubikey: unsupported key type: %s", cert.PublicKey) + } } - logging.Println("Creating RSA4096 key...\nPlease press Yubikey to confirm presence") - newKey, err := yubikeyReader.GenerateKey(piv.DefaultManagementKey, piv.SlotSignature, key) - if err != nil { - return nil, err + switch keyAlgName { + case "RSA2048": + pivAlg = piv.AlgorithmRSA2048 + case "RSA3072": + pivAlg = piv.AlgorithmRSA3072 + case "RSA4096": + pivAlg = piv.AlgorithmRSA4096 + default: + if !yubikeyReader.Overwrite { + return nil, fmt.Errorf("yubikey: unsupported existing yubikey key algorithm: %s", keyAlgName) + } } - logging.Println(fmt.Sprintf("Created RSA4096 key MD5: %x", md5sum(newKey))) - // we overwrote the existing signing key, do not overwrite again if there are other - // key creation operations - yubikeyReader.Overwrite = false + return &Yubikey{ + keytype: YubikeyBackend, + cert: cert, + yubikeyReader: yubikeyReader, + slot: slot, + algorithm: pivAlg, + pinPolicy: piv.PINPolicyAlways, + touchPolicy: piv.TouchPolicyAlways, + }, nil + } + + // if overwrite and there is an existing piv key, print warning + if cert != nil && yubikeyReader.Overwrite { + logging.Warn("Overwriting existing key %s in Yubikey PIV %s Slot", cert.PublicKeyAlgorithm.String(), slotName) + } + + switch keyConfig.Algorithm { + case "RSA2048": + pivAlg = piv.AlgorithmRSA2048 + case "RSA3072": + pivAlg = piv.AlgorithmRSA3072 + case "RSA4096": + pivAlg = piv.AlgorithmRSA4096 + + default: + return nil, fmt.Errorf("yubikey: unsupported public key algorithm %s", keyConfig.Algorithm) + } + + // Get management key + mgmtKey, err := yubikeyReader.GetManagementKey() + if err != nil { + return nil, err } - ykCert, err := yubikeyReader.GetPIVKeyCert() + // Generate a private key on the YubiKey. + key := piv.Key{ + Algorithm: pivAlg, + PINPolicy: piv.PINPolicyAlways, + TouchPolicy: piv.TouchPolicyAlways, + } + logging.Println(fmt.Sprintf("Creating %s key in Yubikey PIV %s Slot...\nPlease press Yubikey to confirm presence", slotName, keyConfig.Algorithm)) + newKey, err := yubikeyReader.GenerateKey(mgmtKey, slot, key) if err != nil { return nil, err } + logging.Println(fmt.Sprintf("Created %s key in Yubikey PIV %s Slot MD5: %x", keyConfig.Algorithm, slotName, md5sum(newKey))) - auth := piv.KeyAuth{PIN: piv.DefaultPIN} - if pin, found := os.LookupEnv("SBCTL_YUBIKEY_PIN"); found { - auth = piv.KeyAuth{PIN: pin} + cert, err = yubikeyReader.GetPIVKeyCert(slot) + if err != nil { + return nil, err } - priv, err := yubikeyReader.PrivateKey(piv.SlotSignature, ykCert.PublicKey, auth) + + priv, err := yubikeyReader.PrivateKey(slot, cert.PublicKey) if err != nil { return nil, err } @@ -107,17 +192,11 @@ func NewYubikeyKey(yubikeyReader *config.YubikeyReader, hier hierarchy.Hierarchy SignatureAlgorithm: x509.SHA256WithRSA, NotBefore: time.Now(), NotAfter: time.Now().AddDate(20, 0, 0), - Subject: pkix.Name{ - Country: []string{hier.Description()}, - CommonName: hier.Description(), - }, + Subject: parseSubject(keyConfig.Subject, hier), } - logging.Println(fmt.Sprintf("Creating %s (%s) key...\nPlease press Yubikey to confirm presence for RSA4096 MD5: %x", - hier.Description(), - hier.String(), - md5sum(ykCert.PublicKey))) - derBytes, err := x509.CreateCertificate(rand.Reader, &c, &c, ykCert.PublicKey, priv) + logging.Println(fmt.Sprintf("Please press Yubikey to confirm presence for %s MD5: %x", keyConfig.Algorithm, md5sum(cert.PublicKey))) + derBytes, err := x509.CreateCertificate(rand.Reader, &c, &c, cert.PublicKey, priv) if err != nil { return nil, err } @@ -131,7 +210,8 @@ func NewYubikeyKey(yubikeyReader *config.YubikeyReader, hier hierarchy.Hierarchy keytype: YubikeyBackend, cert: cert, yubikeyReader: yubikeyReader, - algorithm: piv.AlgorithmRSA4096, + slot: slot, + algorithm: pivAlg, pinPolicy: piv.PINPolicyAlways, touchPolicy: piv.TouchPolicyAlways, }, nil @@ -139,25 +219,48 @@ func NewYubikeyKey(yubikeyReader *config.YubikeyReader, hier hierarchy.Hierarchy func YubikeyFromBytes(yubikeyReader *config.YubikeyReader, keyb, pemb []byte) (*Yubikey, error) { var yubiData YubikeyData + var slot piv.Slot err := json.Unmarshal(keyb, &yubiData) if err != nil { - return nil, fmt.Errorf("error unmarshalling yubikey: %v", err) + return nil, fmt.Errorf("yubikey: error unmarshalling yubikey: %v", err) + } + + switch strings.ToLower(yubiData.Slot) { + case "9c": + slot = piv.SlotSignature + case "9a": + slot = piv.SlotAuthentication + case "9e": + slot = piv.SlotCardAuthentication + case "9d": + slot = piv.SlotKeyManagement + default: + // maybe one of retired slots + var found bool = false + slotHexVal, err := strconv.ParseUint(strings.ToLower(yubiData.Slot), 16, 8) + if err == nil { + slot, found = piv.RetiredKeyManagementSlot(uint32(slotHexVal)) + } + if !found { + return nil, fmt.Errorf("yubikey: Invalid key slot %s", yubiData.Slot) + } } block, _ := pem.Decode(pemb) if block == nil { - return nil, fmt.Errorf("no pem block") + return nil, fmt.Errorf("yubikey: no pem block") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return nil, fmt.Errorf("failed to parse cert: %w", err) + return nil, fmt.Errorf("yubikey: failed to parse cert: %w", err) } return &Yubikey{ keytype: YubikeyBackend, cert: cert, yubikeyReader: yubikeyReader, + slot: slot, algorithm: yubiData.Algorithm, pinPolicy: yubiData.PinPolicy, touchPolicy: yubiData.TouchPolicy, @@ -168,12 +271,7 @@ 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} - } - - priv, err := f.yubikeyReader.PrivateKey(piv.SlotSignature, f.cert.PublicKey, auth) + priv, err := f.yubikeyReader.PrivateKey(f.slot, f.cert.PublicKey) if err != nil { panic(err) } @@ -187,12 +285,13 @@ func (f *Yubikey) Description() string { return f.Certificate().Subject.SerialNu // save YubiKey data to file func (f *Yubikey) PrivateKeyBytes() []byte { + pubKey, _ := x509.MarshalPKIXPublicKey(f.cert.PublicKey) yubiData := YubikeyData{ - Slot: piv.SlotSignature.String(), + Slot: f.slot.String(), Algorithm: f.algorithm, PinPolicy: f.pinPolicy, TouchPolicy: f.touchPolicy, - PublicKey: base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PublicKey(f.cert.PublicKey.(*rsa.PublicKey))), + PublicKey: base64.StdEncoding.EncodeToString(pubKey), } b, err := json.Marshal(yubiData) @@ -205,13 +304,66 @@ func (f *Yubikey) PrivateKeyBytes() []byte { 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") + panic("yubikey: failed producing PEM encoded certificate") } return b.Bytes() } func md5sum(key crypto.PublicKey) []byte { h := md5.New() - h.Write(x509.MarshalPKCS1PublicKey(key.(*rsa.PublicKey))) + pubKey, _ := x509.MarshalPKIXPublicKey(key) + h.Write(pubKey) return h.Sum(nil) } + +func parseSubject(subj string, hier hierarchy.Hierarchy) pkix.Name { + var subject pkix.Name + + if subj != "" { + subject = pkix.Name{} + + fields := strings.SplitSeq(subj, "/") + for field := range fields { + if field == "" { + continue + } + kv := strings.SplitN(field, "=", 2) + if len(kv) != 2 { + continue + } + key := strings.ToUpper(strings.TrimSpace(kv[0])) + value := strings.TrimSpace(kv[1]) + + switch key { + case "C": + subject.Country = append(subject.Country, value) + case "O": + subject.Organization = append(subject.Organization, value) + case "OU": + subject.OrganizationalUnit = append(subject.OrganizationalUnit, value) + case "L": + subject.Locality = append(subject.Locality, value) + case "ST": + subject.Province = append(subject.Province, value) + case "CN": + subject.CommonName = value + case "SERIALNUMBER": + subject.SerialNumber = value + default: + } + } + + // Basic sanity: CN must be supplied + if subject.CommonName == "" { + panic("yubikey: subject missing common name") + } + } else { + // return default + subject = pkix.Name{ + Country: []string{"WW"}, + CommonName: hier.Description(), + } + } + + return subject +} diff --git a/cmd/sbctl/create-keys.go b/cmd/sbctl/create-keys.go index 59b9273..1b55ca5 100644 --- a/cmd/sbctl/create-keys.go +++ b/cmd/sbctl/create-keys.go @@ -4,6 +4,7 @@ import ( "fmt" "path" "path/filepath" + "strings" "github.com/foxboron/sbctl" "github.com/foxboron/sbctl/backend" @@ -21,6 +22,9 @@ var ( KEKKeytype string DbKeytype string PKKeytype string + PKSubject string + KEKSubject string + DbSubject string OverwriteYubikey bool ) @@ -63,25 +67,28 @@ func RunCreateKeys(state *config.State) error { return err } - // Should be own flag type - if Keytype != "" && (Keytype == "file" || Keytype == "tpm" || Keytype == "yubikey") { - state.Config.Keys.PK.Type = Keytype - state.Config.Keys.KEK.Type = Keytype - state.Config.Keys.Db.Type = Keytype + if Keytype != "" && (strings.HasPrefix(Keytype, "file") || strings.HasPrefix(Keytype, "tpm") || strings.HasPrefix(Keytype, "yubikey")) { + state.Config.Keys.PK.Type, state.Config.Keys.PK.Algorithm, state.Config.Keys.PK.Slot = splitKeyType(Keytype) + state.Config.Keys.KEK.Type, state.Config.Keys.KEK.Algorithm, state.Config.Keys.KEK.Slot = splitKeyType(Keytype) + state.Config.Keys.Db.Type, state.Config.Keys.Db.Algorithm, state.Config.Keys.Db.Slot = splitKeyType(Keytype) } else { - if PKKeytype != "" && (PKKeytype == "file" || PKKeytype == "tpm" || PKKeytype == "yubikey") { - state.Config.Keys.PK.Type = PKKeytype + if PKKeytype != "" && (strings.HasPrefix(PKKeytype, "file") || strings.HasPrefix(PKKeytype, "tpm") || strings.HasPrefix(PKKeytype, "yubikey")) { + state.Config.Keys.PK.Type, state.Config.Keys.PK.Algorithm, state.Config.Keys.PK.Slot = splitKeyType(PKKeytype) } - if KEKKeytype != "" && (KEKKeytype == "file" || KEKKeytype == "tpm" || KEKKeytype == "yubikey") { - state.Config.Keys.KEK.Type = KEKKeytype + if KEKKeytype != "" && (strings.HasPrefix(KEKKeytype, "file") || strings.HasPrefix(KEKKeytype, "tpm") || strings.HasPrefix(KEKKeytype, "yubikey")) { + state.Config.Keys.KEK.Type, state.Config.Keys.KEK.Algorithm, state.Config.Keys.KEK.Slot = splitKeyType(KEKKeytype) } - if DbKeytype != "" && (DbKeytype == "file" || DbKeytype == "tpm" || DbKeytype == "yubikey") { - state.Config.Keys.Db.Type = DbKeytype + if DbKeytype != "" && (strings.HasPrefix(DbKeytype, "file") || strings.HasPrefix(DbKeytype, "tpm") || strings.HasPrefix(DbKeytype, "yubikey")) { + state.Config.Keys.Db.Type, state.Config.Keys.Db.Algorithm, state.Config.Keys.Db.Slot = splitKeyType(DbKeytype) } } + state.Config.Keys.PK.Subject = PKSubject + state.Config.Keys.KEK.Subject = KEKSubject + state.Config.Keys.Db.Subject = DbSubject + // if any keytype is yubikey close it appropriately at the end - if Keytype == "yubikey" || PKKeytype == "yubikey" || KEKKeytype == "yubikey" || DbKeytype == "yubikey" { + if strings.HasPrefix(Keytype, "yubikey") || strings.HasPrefix(PKKeytype, "yubikey") || strings.HasPrefix(KEKKeytype, "yubikey") || strings.HasPrefix(DbKeytype, "yubikey") { defer state.Yubikey.Close() } @@ -91,7 +98,6 @@ func RunCreateKeys(state *config.State) error { } logging.Print("Created Owner UUID %s\n", uuid) if !sbctl.CheckIfKeysInitialized(state.Fs, state.Config.Keydir) { - hier, err := backend.CreateKeys(state) if err != nil { logging.NotOk("") @@ -102,8 +108,7 @@ func RunCreateKeys(state *config.State) error { logging.NotOk("") return fmt.Errorf("couldn't initialize secure boot: %w", err) } - logging.Ok("") - logging.Println("Secure boot keys created!") + logging.Ok("Secure boot keys created!") } else { logging.Ok("Secure boot keys have already been created!") } @@ -112,13 +117,16 @@ func RunCreateKeys(state *config.State) error { func createKeysCmdFlags(cmd *cobra.Command) { f := cmd.Flags() - f.BoolVar(&OverwriteYubikey, "yk-overwrite", false, "overwrite existing key if it exists in the Yubikey Signature slot") + f.BoolVar(&OverwriteYubikey, "yk-overwrite", false, "overwrite existing key if it exists in the Yubikey slot") f.StringVarP(&exportPath, "export", "e", "", "export file path") f.StringVarP(&databasePath, "database-path", "d", "", "location to create GUID file") - f.StringVarP(&Keytype, "keytype", "", "", "key type for all keys") - f.StringVarP(&PKKeytype, "pk-keytype", "", "", "PK key type (default: file)") - f.StringVarP(&KEKKeytype, "kek-keytype", "", "", "KEK key type (default: file)") - f.StringVarP(&DbKeytype, "db-keytype", "", "", "db key type (default: file)") + f.StringVarP(&Keytype, "keytype", "", "", "key type for all keys. Algorithm and slot for yubikey can also be specified: yubikey:RSA4096:9c (default: file)") + f.StringVarP(&PKKeytype, "pk-keytype", "", "", "PK key type Algorithm and slot for yubikey can also be specified: yubikey:RSA4096:9c (default: file)") + f.StringVarP(&KEKKeytype, "kek-keytype", "", "", "KEK key type Algorithm and slot for yubikey can also be specified: yubikey:RSA4096:9c (default: file)") + f.StringVarP(&DbKeytype, "db-keytype", "", "", "db key type Algorithm and slot for yubikey can also be specified: yubikey:RSA4096:9c (default: file)") + f.StringVarP(&PKSubject, "pk-subj", "", "", "Subject DN for Platform Key certificate (default: /CN=Platform Key/C=WW/)") + f.StringVarP(&KEKSubject, "kek-subj", "", "", "Subject DN for Key Exchange Key certificate (default: /CN=Key Exchange Key/C=WW/)") + f.StringVarP(&DbSubject, "db-subj", "", "", "Subject DN for Database Key certificate (default: /CN=Database Key/C=WW/)") } func init() { @@ -128,3 +136,18 @@ func init() { Cmd: createKeysCmd, }) } + +func splitKeyType(keyType string) (string, string, string) { + arr := strings.SplitN(keyType, ":", 3) + + if len(arr) == 1 { + return arr[0], "", "" + } + if len(arr) == 2 { + return arr[0], arr[1], "" + } + if len(arr) == 3 { + return arr[0], arr[1], arr[2] + } + return "", "", "" +} diff --git a/config/config.go b/config/config.go index 1c83070..3415317 100644 --- a/config/config.go +++ b/config/config.go @@ -32,6 +32,9 @@ type KeyConfig struct { Privkey string `json:"privkey"` Pubkey string `json:"pubkey"` Type string `json:"type"` + Algorithm string `json:"algorithm"` + Slot string `json:"slot,omitempty"` + Subject string `json:"subject,omitempty"` Description string `json:"description,omitempty"` } diff --git a/config/yubikeyreader.go b/config/yubikeyreader.go index b35c6b8..4e973a0 100644 --- a/config/yubikeyreader.go +++ b/config/yubikeyreader.go @@ -4,6 +4,7 @@ import ( "crypto" "crypto/x509" "fmt" + "os" "strings" "time" @@ -15,13 +16,46 @@ import ( type YubikeyReader struct { key *piv.YubiKey Overwrite bool + Pin string } -func (y *YubikeyReader) GetPIVKeyCert() (*x509.Certificate, error) { +// Fetches PIN protected management key. If it is not stored, default is returned +func (y *YubikeyReader) GetManagementKey() ([]byte, error) { + var err error + if y.Pin == "" { + if pin, found := os.LookupEnv("SBCTL_YUBIKEY_PIN"); found { + y.Pin = pin + } else { + y.Pin = piv.DefaultPIN + } + } + if err = y.connectToYubikey(); err != nil { + return nil, err + } + // FIXME: Should swallow error and return default key? + metadata, err := y.key.Metadata(y.Pin) + if err != nil { + return nil, err + } + if metadata.ManagementKey != nil { + return *metadata.ManagementKey, nil + } else { + return piv.DefaultManagementKey, nil + } +} + +func (y *YubikeyReader) GetPIVKeyCert(slot piv.Slot) (*x509.Certificate, error) { + if err := y.connectToYubikey(); err != nil { + return nil, err + } + return y.key.Certificate(slot) +} + +func (y *YubikeyReader) GetPIVAttestationCert(slot piv.Slot) (*x509.Certificate, error) { if err := y.connectToYubikey(); err != nil { return nil, err } - return y.key.Attest(piv.SlotSignature) + return y.key.Attest(slot) } func (y *YubikeyReader) GenerateKey(key []byte, slot piv.Slot, opts piv.Key) (crypto.PublicKey, error) { @@ -31,7 +65,15 @@ func (y *YubikeyReader) GenerateKey(key []byte, slot piv.Slot, opts piv.Key) (cr return y.key.GenerateKey(key, slot, opts) } -func (y *YubikeyReader) PrivateKey(slot piv.Slot, public crypto.PublicKey, auth piv.KeyAuth) (crypto.PrivateKey, error) { +func (y *YubikeyReader) PrivateKey(slot piv.Slot, public crypto.PublicKey) (crypto.PrivateKey, error) { + if y.Pin == "" { + if pin, found := os.LookupEnv("SBCTL_YUBIKEY_PIN"); found { + y.Pin = pin + } else { + y.Pin = piv.DefaultPIN + } + } + auth := piv.KeyAuth{PIN: y.Pin} if err := y.connectToYubikey(); err != nil { return nil, err } diff --git a/go.mod b/go.mod index 6b5227c..fd6d570 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.24.0 require ( github.com/fatih/color v1.17.0 github.com/foxboron/go-tpm-keyfiles v0.0.0-20240725205618-b7c5a84edf9d - github.com/foxboron/go-uefi v0.0.0-20251010190908-d29549a44f29 - github.com/go-piv/piv-go/v2 v2.3.0 + github.com/foxboron/go-uefi v0.0.0-20250207204325-69fb7dba244f + github.com/go-piv/piv-go/v2 v2.4.0 github.com/goccy/go-yaml v1.11.3 github.com/google/go-attestation v0.5.1 github.com/google/go-tpm v0.9.1 @@ -18,6 +18,7 @@ require ( github.com/spf13/cobra v1.8.1 golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 golang.org/x/sys v0.36.0 + golang.org/x/term v0.35.0 ) require ( diff --git a/go.sum b/go.sum index c8146e0..e2ba4b5 100644 --- a/go.sum +++ b/go.sum @@ -243,8 +243,8 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-piv/piv-go/v2 v2.3.0 h1:kKkrYlgLQTMPA6BiSL25A7/x4CEh2YCG7rtb/aTkx+g= -github.com/go-piv/piv-go/v2 v2.3.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI= +github.com/go-piv/piv-go/v2 v2.4.0 h1:xamQ/fR4MJiw/Ndbk6yi7MVwhjrwlnDAPuaH9zcGb+I= +github.com/go-piv/piv-go/v2 v2.4.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -1077,6 +1077,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/keys.go b/keys.go index a857b5b..336b94c 100644 --- a/keys.go +++ b/keys.go @@ -139,14 +139,23 @@ var SecureBootKeys = []struct { // Check if we have already intialized keys in the given output directory func CheckIfKeysInitialized(vfs afero.Fs, output string) bool { for _, key := range SecureBootKeys { - path := filepath.Join(output, key.Key) - if _, err := vfs.Stat(path); errors.Is(err, os.ErrNotExist) { + found := CheckIfKeyFileExists(vfs, output, key.Key) + if !found { return false } } return true } +// Check if a specific key hierarchy key file exists +func CheckIfKeyFileExists(vfs afero.Fs, output string, key string) bool { + path := filepath.Join(output, key+".key") + if _, err := vfs.Stat(path); errors.Is(err, os.ErrNotExist) { + return false + } + return true +} + func GetEnrolledVendorCerts() []string { db, err := efi.Getdb() if err != nil {