diff --git a/backend/backend.go b/backend/backend.go index eaf94107..6c2af477 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -10,6 +10,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/foxboron/go-uefi/authenticode" "github.com/foxboron/go-uefi/efivar" @@ -127,11 +128,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 +191,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 { - case "file", "": + switch { + case key.Type == "file": return NewFileKey(hier, desc) - case "tpm": + case key.Type == "tpm": return NewTPMKey(state.TPM, desc) - case "yubikey": - return NewYubikeyKey(state.Yubikey, hier) + case strings.HasPrefix(key.Type, "yubikey"): + return NewYubikeyKey(state.Yubikey, hier, key.Type) default: return NewFileKey(hier, desc) } @@ -211,17 +213,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 } diff --git a/backend/yubikey.go b/backend/yubikey.go index 0c3eb13d..56b41a0e 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,102 +37,159 @@ 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() +func NewYubikeyKey(yubikeyReader *config.YubikeyReader, hier hierarchy.Hierarchy, keyType string) (*Yubikey, error) { + var slot piv.Slot + var slotName string + var pivAlg piv.Algorithm + + algorithm, slotNumber := splitYubiKeyType(keyType) + + slot, slotName, err := resolvePIVSlot(slotNumber) if err != nil { - if !errors.Is(err, piv.ErrNotFound) { - return nil, fmt.Errorf("failed finding yubikey: %v", err) - } + return nil, err } - if cert != nil { - // if there is a key and it is RSA4096 and overwrite is false, use it - 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()) - } - } + // Load private key from slot + priv, pub, err := yubikeyReader.PrivateKey(slot) + if err != nil && !errors.Is(err, piv.ErrNotFound) { + return nil, err } - // 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()) + + // If there is no key or overwrite is set, generate a new one + if priv == nil || yubikeyReader.Overwrite { + if priv != nil && yubikeyReader.Overwrite { + logging.Warn("overwriting existing key in Yubikey PIV %s Slot", slotName) + } + + switch 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", algorithm) + } + + // Get management key + mgmtKey, err := yubikeyReader.GetManagementKey() + if err != nil { + return nil, err } - // Generate a private key on the YubiKey. - key := piv.Key{ - Algorithm: piv.AlgorithmRSA4096, + // Generate a private keyOptions on the YubiKey. + keyOptions := piv.Key{ + Algorithm: pivAlg, PINPolicy: piv.PINPolicyAlways, TouchPolicy: piv.TouchPolicyAlways, } - logging.Println("Creating RSA4096 key...\nPlease press Yubikey to confirm presence") - newKey, err := yubikeyReader.GenerateKey(piv.DefaultManagementKey, piv.SlotSignature, key) + logging.Println(fmt.Sprintf("creating %s key in Yubikey PIV %s Slot... This might take some time!", algorithm, slotName)) + newKey, err := yubikeyReader.GenerateKey(mgmtKey, slot, keyOptions) if err != nil { return nil, err } - logging.Println(fmt.Sprintf("Created RSA4096 key MD5: %x", md5sum(newKey))) + logging.Println(fmt.Sprintf("created %s key in Yubikey PIV %s Slot (MD5: %x)", algorithm, slotName, md5sum(newKey))) - // we overwrote the existing signing key, do not overwrite again if there are other - // key creation operations - yubikeyReader.Overwrite = false + // Load newly created private key + priv, pub, err = yubikeyReader.PrivateKey(slot) + if err != nil { + return nil, err + } + } else { + logging.Print("re-using existing key (MD5: %x) in Yubikey PIV %s Slot\n", md5sum(pub), slotName) } - ykCert, err := yubikeyReader.GetPIVKeyCert() - if err != nil { - return nil, err + // Check compatibility of private key + switch yubiPub := pub.(type) { + case *rsa.PublicKey: + switch bitlen := yubiPub.N.BitLen(); bitlen { + case 2048: + pivAlg = piv.AlgorithmRSA2048 + case 3072: + pivAlg = piv.AlgorithmRSA3072 + case 4096: + pivAlg = piv.AlgorithmRSA4096 + default: + return nil, fmt.Errorf("unsupported bitlen of existing yubikey key: %d", bitlen) + } + + default: + return nil, fmt.Errorf("yubikey: unsupported key type: %T", yubiPub) } - auth := piv.KeyAuth{PIN: piv.DefaultPIN} - if pin, found := os.LookupEnv("SBCTL_YUBIKEY_PIN"); found { - auth = piv.KeyAuth{PIN: pin} + // Load certificate from slot + cert, err := yubikeyReader.GetPIVKeyCert(slot) + if err != nil && !errors.Is(err, piv.ErrNotFound) { + return nil, fmt.Errorf("yubikey: failed finding certificate: %v", err) } - priv, err := yubikeyReader.PrivateKey(piv.SlotSignature, ykCert.PublicKey, auth) - if err != nil { - return nil, err + + // If there is no certificate or overwrite is set, generate a new one and save it to the slot + if cert == nil || yubikeyReader.Overwrite { + if cert != nil && yubikeyReader.Overwrite { + logging.Warn("Overwriting existing certificate in Yubikey PIV %s Slot\n", slotName) + } + + 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(20, 0, 0), + Subject: pkix.Name{ + Country: []string{hier.Description()}, + CommonName: hier.Description(), + }, + } + + logging.Print("confirm presence on YubiKey to sign certificate with key (MD5: %x)\n", md5sum(pub)) + 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 + } + + err = yubikeyReader.SetPIVCert(slot, cert) + if err != nil { + logging.Errorf("could not save cert to yubikey: %v", err) + } + } else { + logging.Print("re-using existing certificate %s in Yubikey PIV %s Slot\n", cert.Subject, slotName) } - 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(20, 0, 0), - Subject: pkix.Name{ - Country: []string{hier.Description()}, - CommonName: hier.Description(), - }, - } - - 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) + // Check whether certificate was signed with matching private key, as this + // does not have to be the case if existing keys or certs are re-used. + pubCertBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) if err != nil { - return nil, err + return nil, fmt.Errorf("can not marshal public key of certificate: %v", err) } - - cert, err = x509.ParseCertificate(derBytes) + pubBytes, err := x509.MarshalPKIXPublicKey(pub) if err != nil { - return nil, err + return nil, fmt.Errorf("can not marshal public key: %v", err) + } + if !bytes.Equal(pubBytes, pubCertBytes) { + return nil, fmt.Errorf("mismatch between key and certificate in Yubikey PIV %s Slot", slotName) } return &Yubikey{ keytype: YubikeyBackend, cert: cert, yubikeyReader: yubikeyReader, - algorithm: piv.AlgorithmRSA4096, + slot: slot, + algorithm: pivAlg, pinPolicy: piv.PINPolicyAlways, touchPolicy: piv.TouchPolicyAlways, }, nil @@ -141,23 +199,48 @@ func YubikeyFromBytes(yubikeyReader *config.YubikeyReader, keyb, pemb []byte) (* var yubiData YubikeyData 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) + } + + slot, slotname, err := resolvePIVSlot(yubiData.Slot) + if err != nil { + return nil, err } 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) + } + + _, pub, err := yubikeyReader.PrivateKey(slot) + if err != nil { + return nil, fmt.Errorf("error when loading YubiKey: %v", err) + } + + keyPubKey, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, fmt.Errorf("eror when marshalling public key of YubiKey key: %v", err) + } + + certPubKey, err := x509.MarshalPKIXPublicKey(cert.PublicKey.(*rsa.PublicKey)) + if err != nil { + return nil, fmt.Errorf("eror when marshalling public key of YubiKey certificate: %v", err) + } + + if !bytes.Equal(certPubKey, keyPubKey) { + logging.Warn("saved certificate is not signed by key in YubiKey PIV %s Slot; wrong YubiKey?", slotname) } return &Yubikey{ keytype: YubikeyBackend, cert: cert, yubikeyReader: yubikeyReader, + slot: slot, algorithm: yubiData.Algorithm, pinPolicy: yubiData.PinPolicy, touchPolicy: yubiData.TouchPolicy, @@ -168,18 +251,11 @@ 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, pub, err := f.yubikeyReader.PrivateKey(f.slot) if err != nil { - panic(err) + panic(fmt.Errorf("could not access private key for signing operation: %v", err)) } - logging.Println(fmt.Sprintf("Signing operation... please press Yubikey to confirm presence for key %s MD5: %x", - f.cert.PublicKeyAlgorithm.String(), - md5sum(f.cert.PublicKey))) + logging.Print("Signing operation... please press Yubikey to confirm presence for key (MD5: %x)\n", md5sum(pub)) return priv.(crypto.Signer) } @@ -187,17 +263,18 @@ 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) if err != nil { - panic(err) + panic(fmt.Errorf("could not marshal private key: %v", err)) } return b } @@ -205,13 +282,59 @@ 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 resolvePIVSlot(slot string) (piv.Slot, string, error) { + var pivSlot piv.Slot + var slotName string + + switch strings.ToLower(slot) { + case "9c": + pivSlot = piv.SlotSignature + slotName = "Signature" + case "9a": + pivSlot = piv.SlotAuthentication + slotName = "Authentication" + case "9e": + pivSlot = piv.SlotCardAuthentication + slotName = "CardAuthentication" + case "9d": + pivSlot = piv.SlotKeyManagement + slotName = "KeyManagement" + default: + // maybe one of retired slots + var found bool = false + slotHexVal, err := strconv.ParseUint(slot, 16, 8) + if err == nil { + pivSlot, found = piv.RetiredKeyManagementSlot(uint32(slotHexVal)) + } + if !found { + return piv.Slot{}, "", fmt.Errorf("yubikey: Invalid key slot %s", slot) + } + slotName = fmt.Sprintf("RetiredKeyManagementSlot:0x%s", slot) + } + return pivSlot, slotName, nil +} + +func splitYubiKeyType(keyType string) (string, string) { + arr := strings.SplitN(keyType, ":", 3) + + switch len(arr) { + case 2: + return arr[1], piv.SlotSignature.String() + case 3: + return arr[1], arr[2] + default: + return "RSA4096", piv.SlotSignature.String() + } +} diff --git a/cmd/sbctl/create-keys.go b/cmd/sbctl/create-keys.go index 59b9273e..4e981b47 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" @@ -63,25 +64,24 @@ func RunCreateKeys(state *config.State) error { return err } - // Should be own flag type - if Keytype != "" && (Keytype == "file" || Keytype == "tpm" || Keytype == "yubikey") { + if Keytype != "" && (strings.HasPrefix(Keytype, "file") || strings.HasPrefix(Keytype, "tpm") || strings.HasPrefix(Keytype, "yubikey")) { state.Config.Keys.PK.Type = Keytype state.Config.Keys.KEK.Type = Keytype state.Config.Keys.Db.Type = Keytype } else { - if PKKeytype != "" && (PKKeytype == "file" || PKKeytype == "tpm" || PKKeytype == "yubikey") { + if PKKeytype != "" && (strings.HasPrefix(PKKeytype, "file") || strings.HasPrefix(PKKeytype, "tpm") || strings.HasPrefix(PKKeytype, "yubikey")) { state.Config.Keys.PK.Type = PKKeytype } - if KEKKeytype != "" && (KEKKeytype == "file" || KEKKeytype == "tpm" || KEKKeytype == "yubikey") { + if KEKKeytype != "" && (strings.HasPrefix(KEKKeytype, "file") || strings.HasPrefix(KEKKeytype, "tpm") || strings.HasPrefix(KEKKeytype, "yubikey")) { state.Config.Keys.KEK.Type = KEKKeytype } - if DbKeytype != "" && (DbKeytype == "file" || DbKeytype == "tpm" || DbKeytype == "yubikey") { + if DbKeytype != "" && (strings.HasPrefix(DbKeytype, "file") || strings.HasPrefix(DbKeytype, "tpm") || strings.HasPrefix(DbKeytype, "yubikey")) { state.Config.Keys.Db.Type = DbKeytype } } // 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 +91,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 +101,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,10 +110,10 @@ 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 and certificate 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(&Keytype, "keytype", "", "", "key type for all keys. (default: file)") 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)") diff --git a/config/yubikeyreader.go b/config/yubikeyreader.go index 98eb7c72..7fb5a8e0 100644 --- a/config/yubikeyreader.go +++ b/config/yubikeyreader.go @@ -4,6 +4,7 @@ import ( "crypto" "crypto/x509" "fmt" + "os" "strings" "time" @@ -15,13 +16,50 @@ 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 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) SetPIVCert(slot piv.Slot, cert *x509.Certificate) error { + if err := y.connectToYubikey(); err != nil { + return err + } + managementKey, err := y.GetManagementKey() + if err != nil { + return err + } + return y.key.SetCertificate(managementKey, slot, cert) } func (y *YubikeyReader) GenerateKey(key []byte, slot piv.Slot, opts piv.Key) (crypto.PublicKey, error) { @@ -31,11 +69,40 @@ 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) (crypto.PrivateKey, crypto.PublicKey, error) { + var pubKey crypto.PublicKey + if err := y.connectToYubikey(); err != nil { - return nil, err + return nil, nil, err + } + auth := piv.KeyAuth{PIN: y.pin} + + version := y.key.Version() + + if version.Major > 5 || (version.Major == 5 && version.Minor >= 3) { + keyInfo, err := y.key.KeyInfo(slot) + if err != nil { + return nil, nil, err + } + pubKey = keyInfo.PublicKey + + } else if version.Major > 4 || (version.Major == 4 && version.Minor >= 3) { + attestationCert, err := y.key.Attest(slot) + if err != nil { + return nil, nil, err + } + pubKey = attestationCert.PublicKey + + } else { + return nil, nil, fmt.Errorf("Unsupported YubiKey Version, too old: %d.%d.%d", version.Major, version.Minor, version.Patch) } - return y.key.PrivateKey(slot, public, auth) + + privKey, err := y.key.PrivateKey(slot, pubKey, auth) + if err != nil { + return nil, nil, err + } + + return privKey, pubKey, err } func connectToYubikeyWithTimeout(waitTime time.Duration) (*piv.YubiKey, error) { @@ -103,6 +170,12 @@ func (y *YubikeyReader) connectToYubikey() error { return err } + if pin, found := os.LookupEnv("SBCTL_YUBIKEY_PIN"); found { + y.pin = pin + } else { + y.pin = piv.DefaultPIN + } + y.key = yk return nil } diff --git a/docs/sbctl.8.txt b/docs/sbctl.8.txt index 8ec721ef..497a2092 100644 --- a/docs/sbctl.8.txt +++ b/docs/sbctl.8.txt @@ -95,11 +95,11 @@ EFI signing commands *-p*, *--partial*;; Enroll keys only for the hierarchy specified. - + + + Valid values are: db, KEK, PK. *--custom-bytes*;; - Enroll a custom bytefile provided by its path to the efivar specified by partial. + Enroll a custom bytefile provided by its path to the efivar specified by partial. *-a*, *--append*;; Instead of replacing the currently enrolled keys, append the provided one. @@ -108,30 +108,30 @@ EFI signing commands Set the keytype for all signing keys used by sbctl. This includes PK, KEK and db keys. + - Default: file + Default: file + - Valid values are: file, tpm - + Valid values are: file, tpm, yubikey + *--pk-keytype*;; Set the PK key type. + - Default: file + Default: file + - Valid values are: file, tpm + Valid values are: file, tpm, yubikey *--kek-keytype*;; Set the KEK key type. + - Default: file + Default: file + - Valid values are: file, tpm + Valid values are: file, tpm, yubikey *--db-keytype*;; Set the db key type. + - Default: file + Default: file + - Valid values are: file, tpm + Valid values are: file, tpm, yubikey **sign** ...:: @@ -197,7 +197,7 @@ EFI signing commands *-p*, *--partial*;; Reset keys only for the hierarchy specified. - + + + Valid values are: db, KEK, PK. **rotate-keys**:: @@ -210,7 +210,7 @@ EFI signing commands *-p*, *--partial*;; Rotate keys only for the hierarchy specified. - + + + Valid values are: db, KEK, PK. *-k*, *--key-file*;; @@ -223,30 +223,30 @@ EFI signing commands Set the keytype for all signing keys used by sbctl. This includes PK, KEK and db keys. + - Default: file + Default: file + - Valid values are: file, tpm - + Valid values are: file, tpm, yubikey + *--pk-keytype*;; Set the PK key type. + - Default: file + Default: file + - Valid values are: file, tpm + Valid values are: file, tpm, yubikey *--kek-keytype*;; Set the KEK key type. + - Default: file + Default: file + - Valid values are: file, tpm + Valid values are: file, tpm, yubikey *--db-keytype*;; Set the db key type. + - Default: file + Default: file + - Valid values are: file, tpm + Valid values are: file, tpm, yubikey **export-enrolled-keys**:: Export already enrolled keys from the system. @@ -398,8 +398,8 @@ Tip: systemd-boot will automatically show entries for any bundles found in Supported key types ------------------- -sbctl currently supports storing keys as plain unencrypted files, or as TPM -shielded keys. +sbctl currently supports storing keys as plain unencrypted files, as TPM +shielded keys, or as PIV key/certificate on a YubiKey. Plain unencrypted files (*file*) should only be used when the root partition is encrypted. This is the default key type for historic reasons. File keys are @@ -410,6 +410,15 @@ an accessible TPM on the system. TPM policies are not supported which means we can't seal keys towards the system state. Note that TPM keys are hardcoded to RSA 2048, which is usually the highest bit strength supported by TPMs. +PIV keys/certificates on YubiKeys (*yubikey*) are utilizing the PIV slots of a +YubiKey, by default the PIV Signature slot. If there already are existing keys +or certificates they will be reused, otherwise they are created and saved to the +YubiKey. By default, the keys are RSA4096, but both algorithm and PIV slot can +be specified by appending to the yubikey type: default is *yubikey:RSA4096:9c*. +Supported algorithms are RSA4096, RSA3072, and RSA2048. +If a non-default PIV-PIN is set, it needs to be specified on env +*SBCTL_YUBIKEY_PIN*. + When creating a key hierarchy for Secure Boot, the user can decide which key type each of the keys in the hierarchy gets. Because TPMs can be slow to sign, it's generally advised to keep PK and KEK in the TPM, while the db key can be a diff --git a/go.mod b/go.mod index 6b5227c6..fd6d570e 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 c8146e00..e2ba4b58 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=