diff --git a/backend/backend.go b/backend/backend.go index ce32407..35e5777 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -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" @@ -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") } @@ -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) } @@ -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") } @@ -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 { @@ -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) } diff --git a/backend/yubikey.go b/backend/yubikey.go new file mode 100644 index 0000000..24ee57f --- /dev/null +++ b/backend/yubikey.go @@ -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} + 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))), + } + 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) +} \ No newline at end of file diff --git a/cmd/sbctl/create-keys.go b/cmd/sbctl/create-keys.go index 6c76adb..ef7964e 100644 --- a/cmd/sbctl/create-keys.go +++ b/cmd/sbctl/create-keys.go @@ -15,10 +15,13 @@ import ( ) var ( - exportPath string - databasePath string - Keytype string - PKKeytype, KEKKeytype, DbKeytype string + exportPath string + databasePath string + Keytype string + KEKKeytype string + DbKeytype string + PKKeytype string + OverwriteYubikey bool ) var createKeysCmd = &cobra.Command{ @@ -48,6 +51,11 @@ func RunCreateKeys(state *config.State) error { state.Config.GUID = databasePath } + if OverwriteYubikey { + logging.Warn("Overwriting Yubikey option enabled") + state.YubikeySigKeys.Overwrite = true + } + if err := sbctl.CreateDirectory(state.Fs, state.Config.Keydir); err != nil { return err } @@ -56,18 +64,18 @@ func RunCreateKeys(state *config.State) error { } // Should be own flag type - if Keytype != "" && (Keytype == "file" || Keytype == "tpm") { + 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 } else { - if PKKeytype != "" && (PKKeytype == "file" || PKKeytype == "tpm") { + if PKKeytype != "" && (PKKeytype == "file" || PKKeytype == "tpm" || PKKeytype == "yubikey") { state.Config.Keys.PK.Type = PKKeytype } - if KEKKeytype != "" && (KEKKeytype == "file" || KEKKeytype == "tpm") { + if KEKKeytype != "" && (KEKKeytype == "file" || KEKKeytype == "tpm" || KEKKeytype == "yubikey") { state.Config.Keys.KEK.Type = KEKKeytype } - if DbKeytype != "" && (DbKeytype == "file" || DbKeytype == "tpm") { + if DbKeytype != "" && (DbKeytype == "file" || DbKeytype == "tpm" || DbKeytype == "yubikey") { state.Config.Keys.Db.Type = DbKeytype } } @@ -78,7 +86,6 @@ func RunCreateKeys(state *config.State) error { } logging.Print("Created Owner UUID %s\n", uuid) if !sbctl.CheckIfKeysInitialized(state.Fs, state.Config.Keydir) { - logging.Print("Creating secure boot keys...") hier, err := backend.CreateKeys(state) if err != nil { @@ -100,6 +107,7 @@ 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.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") diff --git a/cmd/sbctl/main.go b/cmd/sbctl/main.go index 4054d24..5c38e5c 100644 --- a/cmd/sbctl/main.go +++ b/cmd/sbctl/main.go @@ -96,7 +96,8 @@ func main() { // We need to set this after we have parsed stuff rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { state := &config.State{ - Fs: fs, + Fs: fs, + YubikeySigKeys: &config.YubiConfig{}, TPM: func() transport.TPMCloser { return rwc }, diff --git a/config/config.go b/config/config.go index 78f6433..896cedf 100644 --- a/config/config.go +++ b/config/config.go @@ -12,6 +12,7 @@ import ( "github.com/foxboron/go-uefi/efi/util" "github.com/foxboron/go-uefi/efivarfs" + "github.com/go-piv/piv-go/v2/piv" "github.com/google/go-tpm/tpm2/transport" "github.com/google/uuid" "github.com/spf13/afero" @@ -35,6 +36,11 @@ type KeyConfig struct { Description string `json:"description,omitempty"` } +type YubiConfig struct { + PubKeyInfo *piv.KeyInfo + Overwrite bool +} + type Keys struct { PK *KeyConfig `json:"pk"` KEK *KeyConfig `json:"kek"` @@ -135,10 +141,11 @@ func NewConfig(b []byte) (*Config, error) { // Key creation is going to require differen callbacks to we abstract them away type State struct { - Fs afero.Fs - TPM func() transport.TPMCloser - Config *Config - Efivarfs *efivarfs.Efivarfs + Fs afero.Fs + TPM func() transport.TPMCloser + Config *Config + Efivarfs *efivarfs.Efivarfs + YubikeySigKeys *YubiConfig } func (s *State) IsInstalled() bool { diff --git a/go.mod b/go.mod index 9572d64..14b18d7 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ 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-20250207204325-69fb7dba244f + github.com/go-piv/piv-go/v2 v2.3.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 diff --git a/go.sum b/go.sum index 39ff4c4..4562314 100644 --- a/go.sum +++ b/go.sum @@ -243,6 +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-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= diff --git a/lsm/lsm.go b/lsm/lsm.go index 96f834d..fd5a135 100644 --- a/lsm/lsm.go +++ b/lsm/lsm.go @@ -29,7 +29,7 @@ func LandlockRulesFromConfig(conf *config.Config) { landlock.RWDirs( filepath.Dir(conf.Keydir), // It seems to me that RWFiles should work on efivars, but it doesn't. - // TODO: Lock this down to induvidual files? + // TODO: Lock this down to individual files? "/sys/firmware/efi/efivars/", ).IgnoreIfMissing(), landlock.ROFiles( @@ -41,6 +41,8 @@ func LandlockRulesFromConfig(conf *config.Config) { conf.GUID, conf.FilesDb, conf.BundlesDb, + //TODO will this be different for others for yubikeys? + "/usr/lib/libpcsclite_real.so.1", // Enable the TPM devices by default if they exist "/dev/tpm0", "/dev/tpmrm0", ).IgnoreIfMissing(),