diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d735310b..06b093f7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y sbsigntool + sudo apt-get install -y sbsigntool libfido2-dev sudo snap install core core18 sudo snap install tpm2-simulator-chrisccoulson - name: Build diff --git a/fido2/export_test.go b/fido2/export_test.go new file mode 100644 index 00000000..80a80c22 --- /dev/null +++ b/fido2/export_test.go @@ -0,0 +1,24 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package fido2 + +const ( + PlatformName = platformName +) diff --git a/fido2/fido2.go b/fido2/fido2.go new file mode 100644 index 00000000..32d22c21 --- /dev/null +++ b/fido2/fido2.go @@ -0,0 +1,242 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package fido2 + +import ( + "errors" + "fmt" + "log" + + "golang.org/x/exp/slices" + + "github.com/keys-pub/go-libfido2" + "github.com/snapcore/secboot" +) + +const ( + rpId = "com.ubuntu" // Relying Party ID + rpName = "Canonical Ubuntu" // Relying Party Name + userName = "secboot" // User name +) + +var ( + ErrNoFIDO2DevicesFound = errors.New("no FIDO2 devices found") +) + +type FIDO2Authenticator struct { + device *libfido2.Device + info *libfido2.DeviceInfo + authRequestor secboot.AuthRequestor + // clientPin + // uv built-in + // bio capabilities +} + +func (f *FIDO2Authenticator) ClientPinRequired() bool { + pinSet := false + bioEnrolled := false + for _, option := range f.info.Options { + switch option.Value == "true" { + case option.Name == "clientPin": + pinSet = true + case option.Name == "bioEnroll": + bioEnrolled = true + } + } + + if bioEnrolled { + return false + } + + if pinSet { + return true + } + + return false +} + +func (f *FIDO2Authenticator) maybeRequestPin(purpose string) (string, error) { + var pin string + if f.ClientPinRequired() { + // TODO this should be implemented by a more fido specific interface + pin, err := f.authRequestor.RequestPassphrase(purpose, "") + if err != nil { + return pin, err + } + } + + return pin, nil +} + +func (f *FIDO2Authenticator) requestTouch(purpose string) error { + // TODO this should be implemented by a more fido specific interface + _, err := f.authRequestor.RequestRecoveryKey(purpose, "") + + return err +} + +func (f *FIDO2Authenticator) MakeFDECredential(salt []byte) (credentialID []byte, secret []byte, err error) { + pin, err := f.maybeRequestPin("to create FDE credential") + if err != nil { + return nil, nil, err + } + + // TODO: This is used for contextual binding of the credential, what can we use it for? + // is also signed with the pinUvAuthToken and passed to the make credential call in the + // pinUvAuthParam parameter. Mostly used by webauthn. Set it to empty for now. + // cdh := libfido2.RandBytes(32) + cdh := make([]byte, 32) + + // TODO: This can be the identifier of the device if any + // userID := libfido2.RandBytes(32) + userID := make([]byte, 32) + + f.requestTouch("to create FDE credential") + + attest, err := f.device.MakeCredential( + cdh, + libfido2.RelyingParty{ + ID: rpId, + Name: rpName, + }, + libfido2.User{ + ID: userID, + Name: userName, + DisplayName: userName, + }, + libfido2.ES256, // Algorithm + pin, + &libfido2.MakeCredentialOpts{ + Extensions: []libfido2.Extension{libfido2.HMACSecretExtension}, + }, + ) + if err != nil { + return nil, nil, err + } + + // TODO Here we can verify attest.AuthData using the attest.Sig against attest.Cert to ensure the + // credential was created by a trusted authenticator. + // This is defined in https://www.w3.org/TR/webauthn-2/#sctn-attestation and is used by RPs in the + // full WebAuthn flow. + + // TODO Using AuthData we can verify: + // the user present bit + // the user verified bit + // that the extension data indeed included (hmac-secret: true) + // log.Printf("AuthData: %s\n", hex.EncodeToString(attest.AuthData)) + + secret, err = f.GetHmacSecret(attest.CredentialID, salt) + + return attest.CredentialID, secret, err +} + +func (f *FIDO2Authenticator) GetHmacSecret(credentialID []byte, salt []byte) (secret []byte, err error) { + pin, err := f.maybeRequestPin("to create FDE credential") + if err != nil { + return nil, err + } + + cdh := make([]byte, 32) + + f.requestTouch("to retrieve secret") + + assertion, err := f.device.Assertion( + rpId, + cdh, + [][]byte{credentialID}, + pin, + &libfido2.AssertionOpts{ + Extensions: []libfido2.Extension{libfido2.HMACSecretExtension}, + UP: libfido2.True, + HMACSalt: salt, + }, + ) + if err != nil { + return nil, err + } + + return assertion.HMACSecret, nil +} + +func verify(device *libfido2.Device) (*libfido2.DeviceInfo, error) { + devType, err := device.Type() + if err != nil { + return nil, err + } + if devType != libfido2.FIDO2 { + return nil, fmt.Errorf("device is not a FIDO2 device: %v", devType) + } + + info, err := device.Info() + if err != nil { + return nil, err + } + + if !slices.Contains(info.Versions, "FIDO_2_0") { + return nil, fmt.Errorf("device does not support CTAP 2.1: %v", device) + } + + if !slices.Contains(info.Extensions, "hmac-secret") { + return nil, fmt.Errorf("device does not support hmac-secret extension: %v", device) + } + + return info, nil +} + +func NewFIDO2Authenticator(authRequestor secboot.AuthRequestor) (*FIDO2Authenticator, error) { + locs, err := libfido2.DeviceLocations() + if err != nil { + return nil, fmt.Errorf("cannot find devices: %v", err) + } + if len(locs) == 0 { + return nil, ErrNoFIDO2DevicesFound + } + + fmt.Printf("Using device: %+v\n", locs[0]) + + path := locs[0].Path + device, err := libfido2.NewDevice(path) + if err != nil { + return nil, err + } + + info, err := verify(device) + if err != nil { + return nil, fmt.Errorf("device verification failed: %v", err) + } + + return &FIDO2Authenticator{ + device: device, + info: info, + authRequestor: authRequestor, + }, nil +} + +func ConnectToFIDO2Authenticator(authRequestor secboot.AuthRequestor) (*FIDO2Authenticator, error) { + + fido2Authenticator, err := NewFIDO2Authenticator(authRequestor) + if err != nil { + return nil, err + } + + log.Printf("Info: %+v\n", fido2Authenticator.info) + + return fido2Authenticator, err +} diff --git a/fido2/fido2_test.go b/fido2/fido2_test.go new file mode 100644 index 00000000..d2c90846 --- /dev/null +++ b/fido2/fido2_test.go @@ -0,0 +1,45 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package fido2_test + +import ( + "os" + "testing" + + "github.com/snapcore/secboot/fido2" + testutil "github.com/snapcore/secboot/internal/testutil" + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +type fidoTestSuite struct{} + +var _ = Suite(&fidoTestSuite{}) + +func (s *fidoTestSuite) TestConnect(c *C) { + authRequestor := &testutil.MockFidoAuthRequestor{Pin: ""} + _, err := fido2.ConnectToFIDO2Authenticator(authRequestor) + c.Check(err, IsNil) +} diff --git a/fido2/keydata.go b/fido2/keydata.go new file mode 100644 index 00000000..d74791e4 --- /dev/null +++ b/fido2/keydata.go @@ -0,0 +1,307 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package fido2 + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "encoding/asn1" + "encoding/json" + "fmt" + "hash" + "io" + + "golang.org/x/crypto/cryptobyte" + + "github.com/snapcore/secboot" + cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" +) + +const ( + nonceSize = 12 +) + +var ( + nilHash hashAlg = 0 + sha1Oid = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} + sha224Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 4} + sha256Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} + sha384Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2} + sha512Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3} + + secbootNewKeyData = secboot.NewKeyData +) + +// hashAlg corresponds to a digest algorithm. +// XXX: This is the third place this appears now - we almost certainly want to put this +// in one place. Maybe for another PR. +type hashAlg crypto.Hash + +func (a hashAlg) Available() bool { + return crypto.Hash(a).Available() +} + +func (a hashAlg) New() hash.Hash { + return crypto.Hash(a).New() +} + +func (a hashAlg) Size() int { + return crypto.Hash(a).Size() +} + +func (a hashAlg) MarshalASN1(b *cryptobyte.Builder) { + b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) { // AlgorithmIdentifier ::= SEQUENCE { + var oid asn1.ObjectIdentifier + + switch crypto.Hash(a) { + case crypto.SHA1: + oid = sha1Oid + case crypto.SHA224: + oid = sha224Oid + case crypto.SHA256: + oid = sha256Oid + case crypto.SHA384: + oid = sha384Oid + case crypto.SHA512: + oid = sha512Oid + default: + b.SetError(fmt.Errorf("unknown hash algorithm: %v", crypto.Hash(a))) + return + } + b.AddASN1ObjectIdentifier(oid) // algorithm OBJECT IDENTIFIER + b.AddASN1NULL() // parameters ANY DEFINED BY algorithm OPTIONAL + }) +} + +func (a hashAlg) MarshalJSON() ([]byte, error) { + var s string + + switch crypto.Hash(a) { + case crypto.SHA1: + s = "sha1" + case crypto.SHA224: + s = "sha224" + case crypto.SHA256: + s = "sha256" + case crypto.SHA384: + s = "sha384" + case crypto.SHA512: + s = "sha512" + case crypto.Hash(nilHash): + s = "null" + default: + return nil, fmt.Errorf("unknown hash algorithm: %v", crypto.Hash(a)) + } + + return json.Marshal(s) +} + +func (a *hashAlg) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + switch s { + case "sha1": + *a = hashAlg(crypto.SHA1) + case "sha224": + *a = hashAlg(crypto.SHA224) + case "sha256": + *a = hashAlg(crypto.SHA256) + case "sha384": + *a = hashAlg(crypto.SHA384) + case "sha512": + *a = hashAlg(crypto.SHA512) + default: + // be permissive here and allow everything to be + // unmarshalled. + *a = nilHash + } + + return nil +} + +type additionalData struct { + Version int + Generation int + KDFAlg hashAlg + AuthMode secboot.AuthMode + SaltProvider []byte +} + +func (d additionalData) MarshalASN1(b *cryptobyte.Builder) { + b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) { + b.AddASN1Int64(int64(d.Version)) + b.AddASN1Int64(int64(d.Generation)) + d.KDFAlg.MarshalASN1(b) + b.AddASN1Enum(int64(d.AuthMode)) + b.AddASN1OctetString(d.SaltProvider) + }) +} + +type keyData struct { + Version int `json:"version"` + + // the nonce used for the GCM step + Nonce []byte `json:"nonce"` + + // The FIDO2 credential ID that is associated with this key data + CredentialID []byte `json:"credential_id"` + + // Alg is the digest algorithm used for creating the salt passed to + // the authenticator's hmac-secret and for deriving the final symmetric key. + Alg hashAlg `json:"alg"` // the digest algorithm +} + +func newFIDO2ProtectedKey(authenticator *FIDO2Authenticator, providerName string, symA []byte, primaryKey secboot.PrimaryKey) (fkd *keyData, encryptedPayload []byte, primaryKeyOut secboot.PrimaryKey, unlockKey secboot.DiskUnlockKey, err error) { + kdfAlg := crypto.SHA256 + if len(symA) < kdfAlg.Size() { + return nil, nil, nil, nil, fmt.Errorf("input symmetric key must be at least %d bytes long", kdfAlg.Size()) + } + + // The salt passed as input to the authenticator's hmac-secret, is created as salt=HMAC-SHA3_256(symA, "ubuntu-fde-fido2"). + // This is done to hide the actual symmetric key from the authenticator. + salt := make([]byte, kdfAlg.Size()) + r := hmac.New(kdfAlg.New, symA) + r.Write([]byte("ubuntu-fde-fido2")) + salt = r.Sum(nil) + + // Communicate with the fido2 authenticator to retrieve hmac-secret(salt) + credentialID, hmacSecret, err := authenticator.MakeFDECredential(salt) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("cannot create FIDO2 credential: %w", err) + } + + if len(hmacSecret) < kdfAlg.Size() { + return nil, nil, nil, nil, fmt.Errorf("hmac-secret must be at least %d bytes long", kdfAlg.Size()) + } + + // Combine the result with the original symmetric key using HMAC-SHA256 to obtain the final symmetric key + symB := make([]byte, kdfAlg.Size()) + r = hmac.New(kdfAlg.New, symA) + r.Write(hmacSecret) + symB = r.Sum(nil) + + // Create payload + unlockKey, payload, err := secboot.MakeDiskUnlockKey(rand.Reader, kdfAlg, primaryKey) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("cannot create new unlock key: %w", err) + } + + // Encrypt using aead + nonce := make([]byte, nonceSize) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, nil, nil, nil, fmt.Errorf("cannot obtain required random bytes: %w", err) + } + + aad := additionalData{ + Version: 1, + Generation: secboot.KeyDataGeneration, + KDFAlg: hashAlg(kdfAlg), + AuthMode: secboot.AuthModeNone, + SaltProvider: []byte(providerName), + } + builder := cryptobyte.NewBuilder(nil) + aad.MarshalASN1(builder) + aadBytes, err := builder.Bytes() + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("cannot serialize AAD: %w", err) + } + + b, err := aes.NewCipher(symB) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("cannot create cipher: %w", err) + } + + aead, err := cipher.NewGCM(b) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("cannot create AEAD: %w", err) + } + ciphertext := aead.Seal(nil, nonce, payload, aadBytes) + + fkd = &keyData{ + Version: 1, + Nonce: nonce, + CredentialID: credentialID, + Alg: hashAlg(kdfAlg), + } + + return fkd, ciphertext, primaryKey, unlockKey, nil +} + +func NewFIDO2ProtectedKey(authenticator *FIDO2Authenticator, providerName string, symA []byte, primaryKey secboot.PrimaryKey) (protectedKey *secboot.KeyData, primaryKeyOut secboot.PrimaryKey, unlockKey secboot.DiskUnlockKey, err error) { + fkd, fidoEncPayload, primaryKey, unlockKey, err := newFIDO2ProtectedKey(authenticator, providerName, symA, primaryKey) + if err != nil { + return nil, nil, nil, err + } + + kd, err := secbootNewKeyData(&secboot.KeyParams{ + Handle: fkd, + EncryptedPayload: fidoEncPayload, + PlatformName: platformName, + KDFAlg: crypto.Hash(fkd.Alg), + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("cannot create key data: %w", err) + } + + return kd, primaryKey, unlockKey, nil +} + +func NewFIDO2ProtectedKeyWithSaltProvider(authenticator *FIDO2Authenticator, pkd *secboot.KeyData, primaryKey secboot.PrimaryKey) (protectedKey *secboot.KeyData, primaryKeyOut secboot.PrimaryKey, unlockKey secboot.DiskUnlockKey, err error) { + providerName, providerPlatformKeyData, sym, err := pkd.RecoverSymmetricKey() + if err != nil { + return nil, nil, nil, err + } + + fkd, fidoEncPayload, primaryKey, unlockKey, err := newFIDO2ProtectedKey(authenticator, providerName, sym, primaryKey) + if err != nil { + return nil, nil, nil, err + } + + combinedPlatformKeyData := &providerKeyData{ + Version: 1, + Provider: providerPlatformKeyData, + Fido2: fkd, + } + + combinedPlatformName := providerName + "-" + platformName + + kdOut, err := secbootNewKeyData(&secboot.KeyParams{ + Handle: *combinedPlatformKeyData, + EncryptedPayload: fidoEncPayload, + PlatformName: combinedPlatformName, + KDFAlg: crypto.Hash(fkd.Alg), + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("cannot create key data: %w", err) + } + + return kdOut, primaryKey, unlockKey, nil +} + +// TODO +// func NewFIDO2PassphraseProtectedKey(authenticator *FIDO2Authenticator, primaryKey secboot.PrimaryKey, salt []byte) (protectedKey *secboot.KeyData, primaryKeyOut secboot.PrimaryKey, unlockKey secboot.DiskUnlockKey, err error) { +// return nil, nil, nil, nil +// } diff --git a/fido2/keydata_test.go b/fido2/keydata_test.go new file mode 100644 index 00000000..a173115d --- /dev/null +++ b/fido2/keydata_test.go @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package fido2_test + +import ( + . "gopkg.in/check.v1" +) + +type keydataSuite struct{} + +var _ = Suite(&keydataSuite{}) diff --git a/fido2/platform.go b/fido2/platform.go new file mode 100644 index 00000000..d0c38218 --- /dev/null +++ b/fido2/platform.go @@ -0,0 +1,165 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Package fido2 is a platform for recovering keys that are protected by an authenticator +// that implements the CTAP2 protocol. +package fido2 + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "encoding/json" + "errors" + "fmt" + + "golang.org/x/crypto/cryptobyte" + + "github.com/snapcore/secboot" +) + +const ( + platformName = "fido2" +) + +var ( + ErrNoFIDO2ProviderRegistered = errors.New("no appropriate FIDO2 provider is registered") +) + +func recoverKeys(kd *keyData, providerName string, encryptedPayload, symA []byte, authenticator *FIDO2Authenticator) ([]byte, error) { + + kdfAlg := kd.Alg + + if len(symA) < kdfAlg.Size() { + return nil, fmt.Errorf("input symmetric key must be at least %d bytes long", kdfAlg.Size()) + } + + salt := make([]byte, kdfAlg.Size()) + r := hmac.New(kdfAlg.New, symA) + r.Write([]byte("ubuntu-fde-fido2")) + salt = r.Sum(nil) + + hmacSecret, err := authenticator.GetHmacSecret(kd.CredentialID, salt) + if err != nil { + return nil, fmt.Errorf("cannot get hmac-secret from FIDO2 token: %w", err) + } + + if len(hmacSecret) < kdfAlg.Size() { + return nil, fmt.Errorf("hmac-secret must be at least %d bytes long", kdfAlg.Size()) + } + + symB := make([]byte, kdfAlg.Size()) + r = hmac.New(kdfAlg.New, symA) + r.Write(hmacSecret) + symB = r.Sum(nil) + + aad := additionalData{ + Version: 1, + Generation: secboot.KeyDataGeneration, + KDFAlg: hashAlg(kdfAlg), + AuthMode: secboot.AuthModeNone, + SaltProvider: []byte(providerName), + } + builder := cryptobyte.NewBuilder(nil) + aad.MarshalASN1(builder) + aadBytes, err := builder.Bytes() + if err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: fmt.Errorf("cannot serialize AAD: %w", err), + } + } + + b, err := aes.NewCipher(symB) + if err != nil { + return nil, fmt.Errorf("cannot create cipher: %w", err) + } + + aead, err := cipher.NewGCMWithNonceSize(b, len(kd.Nonce)) + if err != nil { + return nil, fmt.Errorf("cannot create AEAD: %w", err) + } + + payload, err := aead.Open(nil, kd.Nonce, encryptedPayload, aadBytes) + if err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: fmt.Errorf("cannot open payload: %w", err), + } + } + + return payload, nil +} + +func RecoverKeys(data *secboot.PlatformKeyData, encryptedPayload, sym []byte, authenticator *FIDO2Authenticator) ([]byte, error) { + var kd keyData + if err := json.Unmarshal(data.EncodedHandle, &kd); err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: err, + } + } + + return recoverKeys(&kd, "", encryptedPayload, sym, authenticator) +} + +type providerKeyData struct { + Version int `json:"version"` + Provider *secboot.PlatformKeyData `json:"provider"` + Fido2 *keyData `json:"fido2"` +} + +var RecoverKeysWithFIDOProvider = func(providerName string, data *secboot.PlatformKeyData, encryptedPayload []byte, authenticator *FIDO2Authenticator) ([]byte, error) { + handler, _, err := secboot.RegisteredPlatformKeyDataHandler(providerName) + if err != nil { + return nil, err + } + + // TODO consistency check that the flags indicate that the platform can be used as a fido2 hmac-secret salt provider + provider, ok := handler.(secboot.FIDO2Provider) + if !ok { + return nil, fmt.Errorf("%s handler %T does not implement FIDO2Provider", providerName, handler) + } + + var kd providerKeyData + if err := json.Unmarshal(data.EncodedHandle, &kd); err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: err, + } + } + + symA, err := provider.GetSymmetricKey(kd.Provider, nil) + if err != nil { + return nil, err + } + + payload, err := recoverKeys(kd.Fido2, providerName, encryptedPayload, symA, authenticator) + if err != nil { + return nil, err + } + + return payload, nil + +} + +func init() { + // NOTE: the fido2 platform doesn't register itself as a standalone platform. + // For now it is only used through the tpm2+fido2 platform. +} diff --git a/fido2/platform_test.go b/fido2/platform_test.go new file mode 100644 index 00000000..31e43017 --- /dev/null +++ b/fido2/platform_test.go @@ -0,0 +1,96 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package fido2_test + +import ( + "crypto/rand" + + . "gopkg.in/check.v1" + + "github.com/snapcore/secboot" + . "github.com/snapcore/secboot/fido2" + + testutil "github.com/snapcore/secboot/internal/testutil" +) + +type platformSuite struct { +} + +var _ = Suite(&platformSuite{}) + +func (s *platformSuite) TestPlatformName(c *C) { + c.Check(PlatformName, Equals, "fido2") +} + +func (s *platformSuite) TestRecoverKeys(c *C) { + primaryKey := make(secboot.PrimaryKey, 32) + rand.Read(primaryKey) + + salt := make([]byte, 32) + rand.Read(salt) + + // Using a physical FIDO2 authenticator with 12345 set as the PIN + authRequestor := &testutil.MockFidoAuthRequestor{Pin: "12345"} + + authenticator, err := ConnectToFIDO2Authenticator(authRequestor) + c.Assert(err, IsNil) + + kd, expectedPrimaryKey, expectedUnlockKey, err := NewFIDO2ProtectedKey(authenticator, "", salt, primaryKey) + c.Assert(err, IsNil) + + flags := secboot.PlatformKeyDataHandlerFlags(0).AddPlatformFlags(1) + secboot.RegisterPlatformKeyDataHandler(PlatformName, testutil.NewPlainFidoSaltProvider(salt, authRequestor), flags) + + unlockKey, primaryKey, err := kd.RecoverKeys() + c.Check(err, IsNil) + c.Check(unlockKey, DeepEquals, expectedUnlockKey) + c.Check(primaryKey, DeepEquals, expectedPrimaryKey) +} + +func (s *platformSuite) TestRecoverKeysBio(c *C) { + primaryKey := make(secboot.PrimaryKey, 32) + rand.Read(primaryKey) + + salt := make([]byte, 32) + rand.Read(salt) + + // Using a physical FIDO2 authenticator with 12345 set as the PIN and a fingerprint enrolled + // but not passing the PIN. + // Fingerprint reading failure causes this test to fail. + authRequestor := &testutil.MockFidoAuthRequestor{Pin: ""} + + authenticator, err := ConnectToFIDO2Authenticator(authRequestor) + c.Assert(err, IsNil) + + kd, expectedPrimaryKey, expectedUnlockKey, err := NewFIDO2ProtectedKey(authenticator, "", salt, primaryKey) + c.Assert(err, IsNil) + + flags := secboot.PlatformKeyDataHandlerFlags(0).AddPlatformFlags(1) + secboot.RegisterPlatformKeyDataHandler(PlatformName, testutil.NewPlainFidoSaltProvider(salt, authRequestor), flags) + + unlockKey, primaryKey, err := kd.RecoverKeys() + c.Check(err, IsNil) + c.Check(unlockKey, DeepEquals, expectedUnlockKey) + c.Check(primaryKey, DeepEquals, expectedPrimaryKey) +} + +// TODO +// func (s *platformSuite) TestRecoverKeysWithSaltProvider(c *C) { +// } diff --git a/go.mod b/go.mod index dc37cdd4..e0e53d66 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/canonical/go-tpm2 v1.13.0 github.com/canonical/tcglog-parser v0.0.0-20240924110432-d15eaf652981 github.com/jessevdk/go-flags v1.5.0 + github.com/keys-pub/go-libfido2 v1.5.3 github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85 golang.org/x/crypto v0.23.0 golang.org/x/sys v0.21.0 @@ -25,6 +26,7 @@ require ( github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18 // indirect github.com/kr/text v0.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/net v0.21.0 // indirect diff --git a/go.sum b/go.sum index f91b2e09..00761e84 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2/go.mod h1: github.com/canonical/tcglog-parser v0.0.0-20240924110432-d15eaf652981 h1:vrUzSfbhl8mzdXPzjxq4jXZPCCNLv18jy6S7aVTS2tI= github.com/canonical/tcglog-parser v0.0.0-20240924110432-d15eaf652981/go.mod h1:ywdPBqUGkuuiitPpVWCfilf2/gq+frhq4CNiNs9KyHU= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -31,6 +33,8 @@ github.com/jessevdk/go-flags v1.4.1-0.20180927143258-7309ec74f752/go.mod h1:4FA2 github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/keys-pub/go-libfido2 v1.5.3 h1:vtgHxlSB43u6lj0TSuA3VvT6z3E7VI+L1a2hvMFdECk= +github.com/keys-pub/go-libfido2 v1.5.3/go.mod h1:P0V19qHwJNY0htZwZDe9Ilvs/nokGhdFX7faKFyZ6+U= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18 h1:fth7xdJYakAjo/XH38edyXuBEqYGJ8Me0RPolN1ZiQE= @@ -41,6 +45,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mvo5/goconfigparser v0.0.0-20200803085309-72e476556adb/go.mod h1:xmt4k1xLDl8Tdan+0S/jmMK2uSUBSzTc18+5GN5Vea8= github.com/mvo5/libseccomp-golang v0.9.1-0.20180308152521-f4de83b52afb/go.mod h1:RduRpSkQHOCvZTbGgT/NJUGjFBFkYlVedimxssQ64ag= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502024300-f57e1d55ea18/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/snapcore/bolt v1.3.2-0.20210908134111-63c8bfcf7af8/go.mod h1:Z6z3sf12AMDjT/4tbT/PmzzdACAxkWGhkuKWiVpTWLM= @@ -50,6 +58,9 @@ github.com/snapcore/secboot v0.0.0-20211018143212-802bb19ca263/go.mod h1:72paVOk github.com/snapcore/snapd v0.0.0-20201005140838-501d14ac146e/go.mod h1:3xrn7QDDKymcE5VO2rgWEQ5ZAUGb9htfwlXnoel6Io8= github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85 h1:hd2S2lKACVYbRXAKHKc6lz5DBccpeFCERTLXCVEdFrM= github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85/go.mod h1:Ab4TsNgVast9nXAN8KVydI5G/hTHncgiQ4S1sAWjIXg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -61,6 +72,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -82,6 +94,7 @@ gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= gopkg.in/tylerb/graceful.v1 v1.2.15/go.mod h1:yBhekWvR20ACXVObSSdD3u6S9DeSylanL2PAbAC/uJ8= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/testutil/fido.go b/internal/testutil/fido.go new file mode 100644 index 00000000..05f4a04d --- /dev/null +++ b/internal/testutil/fido.go @@ -0,0 +1,82 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package testutil + +import ( + "errors" + "fmt" + + "github.com/snapcore/secboot" + "github.com/snapcore/secboot/fido2" +) + +var ( + secbootNewSystemdAuthRequestor = secboot.NewSystemdAuthRequestor + fido2RecoverKeysWithFIDOProvider = fido2.RecoverKeysWithFIDOProvider +) + +type MockFidoAuthRequestor struct { + Pin string +} + +func (r *MockFidoAuthRequestor) RequestPassphrase(volumeName, sourceDevicePath string) (string, error) { + fmt.Println("Enter PIN (autofilled): ", r.Pin) + return r.Pin, nil +} + +// TODO: This is temporarily used to prompt the user to touch the key. It doesn't return anything +func (r *MockFidoAuthRequestor) RequestRecoveryKey(volumeName, sourceDevicePath string) (secboot.RecoveryKey, error) { + fmt.Println("Touch key", volumeName) + return secboot.RecoveryKey{}, nil +} + +type platformKeyDataHandler struct { + salt []byte + authRequestor secboot.AuthRequestor +} + +func (h *platformKeyDataHandler) RecoverKeys(data *secboot.PlatformKeyData, encryptedPayload []byte) ([]byte, error) { + authenticator, err := fido2.ConnectToFIDO2Authenticator(h.authRequestor) + if err != nil { + return nil, err + } + + return fido2.RecoverKeys(data, encryptedPayload, h.salt, authenticator) + +} + +func (h *platformKeyDataHandler) RecoverKeysWithAuthKey(data *secboot.PlatformKeyData, encryptedPayload, key []byte) ([]byte, error) { + return nil, errors.New("unsupported action") +} + +func (h *platformKeyDataHandler) ChangeAuthKey(data *secboot.PlatformKeyData, old, new []byte, context any) ([]byte, error) { + return nil, errors.New("unsupported action") +} + +func (h *platformKeyDataHandler) GetSymmetricKey(data *secboot.PlatformKeyData, authKey []byte) ([]byte, error) { + return h.salt, nil +} + +func NewPlainFidoSaltProvider(salt []byte, authRequestor secboot.AuthRequestor) *platformKeyDataHandler { + return &platformKeyDataHandler{ + salt: salt, + authRequestor: authRequestor, + } +} diff --git a/keydata.go b/keydata.go index af8dbf98..12c31e25 100644 --- a/keydata.go +++ b/keydata.go @@ -730,6 +730,32 @@ func (d *KeyData) WriteAtomic(w KeyDataWriter) error { return nil } +// RecoverSymmetricKey attempts to recover a secret protected by this keydata's platform +// which is meant to be used as a salt for the FIDO2 hmac-secret extension. +// +// Only the tpm2 platform currently implements this. +// TODO more extensive comment +func (d *KeyData) RecoverSymmetricKey() (string, *PlatformKeyData, []byte, error) { + handlerInfo, exists := keyDataHandlers[d.data.PlatformName] + if !exists { + return "", nil, nil, ErrNoPlatformHandlerRegistered + } + + // TODO consistency check that the flags indicate that the platform can be used as a fido2 hmac-secret salt provider + + provider, ok := handlerInfo.handler.(FIDO2Provider) + if !ok { + return "", nil, nil, fmt.Errorf("%s handler %T does not implement FIDO2Provider", d.data.PlatformName, provider) + } + + sym, err := provider.GetSymmetricKey(d.platformKeyData(), nil) + if err != nil { + return "", nil, nil, err + } + + return d.data.PlatformName, d.platformKeyData(), sym, nil +} + // ReadKeyData reads the key data from the supplied KeyDataReader, returning a // new KeyData object. // @@ -882,3 +908,7 @@ func MakeDiskUnlockKey(rand io.Reader, alg crypto.Hash, primaryKey PrimaryKey) ( return pk.unlockKey(alg), cleartextPayload, nil } + +type FIDO2Provider interface { + GetSymmetricKey(*PlatformKeyData, []byte) ([]byte, error) +} diff --git a/platform.go b/platform.go index 674249f7..756ccd6e 100644 --- a/platform.go +++ b/platform.go @@ -92,6 +92,8 @@ type PlatformKeyDataHandlerFlags uint64 const platformKeyDataHandlerCommonFlagsMask uint64 = 0xffffff0000000000 +// TODO define a common flag which indicates that the platform can be used as FIDO2's hmac-secret salt provider + // AddPlatformFlags adds the platform defined flags to the common flags, // returning a new flags value. This package doesn't define the meaning of // the specified flags and it does not use or interpret them in any way. diff --git a/tpm2/fido2/export_test.go b/tpm2/fido2/export_test.go new file mode 100644 index 00000000..73fe320c --- /dev/null +++ b/tpm2/fido2/export_test.go @@ -0,0 +1,58 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package tpm2_fido2 + +import ( + "github.com/snapcore/secboot" + "github.com/snapcore/secboot/fido2" +) + +const ( + PlatformName = platformName +) + +func MockSecbootNewKeyData(fn func(*secboot.KeyParams) (*secboot.KeyData, error)) (restore func()) { + orig := secbootNewKeyData + secbootNewKeyData = fn + return func() { + secbootNewKeyData = orig + } +} + +func MockFido2NewFIDO2ProtectedKeyWithSaltProvider(fn func()) (restore func()) { + orig := fido2NewFIDO2ProtectedKeyWithSaltProvider + fido2NewFIDO2ProtectedKeyWithSaltProvider = func(authenticator *fido2.FIDO2Authenticator, kd *secboot.KeyData, primaryKey secboot.PrimaryKey) (*secboot.KeyData, secboot.PrimaryKey, secboot.DiskUnlockKey, error) { + fn() + return orig(authenticator, kd, primaryKey) + } + return func() { + fido2NewFIDO2ProtectedKeyWithSaltProvider = orig + } +} + +func MockSecbootNewSystemdAuthRequestor(authRequestor secboot.AuthRequestor) (restore func()) { + orig := secbootNewSystemdAuthRequestor + secbootNewSystemdAuthRequestor = func(string, string) secboot.AuthRequestor { + return authRequestor + } + return func() { + secbootNewSystemdAuthRequestor = orig + } +} diff --git a/tpm2/fido2/keydata.go b/tpm2/fido2/keydata.go new file mode 100644 index 00000000..faf99f56 --- /dev/null +++ b/tpm2/fido2/keydata.go @@ -0,0 +1,40 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package tpm2_fido2 + +import ( + "github.com/snapcore/secboot" + "github.com/snapcore/secboot/fido2" + "github.com/snapcore/secboot/tpm2" +) + +var ( + secbootNewKeyData = secboot.NewKeyData + fido2NewFIDO2ProtectedKeyWithSaltProvider = fido2.NewFIDO2ProtectedKeyWithSaltProvider +) + +func NewTPM2FIDO2ProtectedKey(tpm *tpm2.Connection, params *tpm2.ProtectKeyParams, authenticator *fido2.FIDO2Authenticator) (protectedKey *secboot.KeyData, primaryKey secboot.PrimaryKey, unlockKey secboot.DiskUnlockKey, err error) { + tkd, primaryKey, _, err := tpm2.NewTPMProtectedKey(tpm, params) + return fido2NewFIDO2ProtectedKeyWithSaltProvider(authenticator, tkd, primaryKey) +} + +// TODO +// func NewTPM2FIDO2PassphraseProtectedKey(tpm *tpm2.Connection, params *ProtectKeyParams) (protectedKey *secboot.KeyData, primaryKey secboot.PrimaryKey, unlockKey secboot.DiskUnlockKey, err error) { +// } diff --git a/tpm2/fido2/platform.go b/tpm2/fido2/platform.go new file mode 100644 index 00000000..bd05afc4 --- /dev/null +++ b/tpm2/fido2/platform.go @@ -0,0 +1,60 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package tpm2_fido2 + +import ( + "errors" + + "github.com/snapcore/secboot" + "github.com/snapcore/secboot/fido2" +) + +const ( + platformName = "tpm2-fido2" +) + +var ( + fido2RecoverKeysWithFIDOProvider = fido2.RecoverKeysWithFIDOProvider + secbootNewSystemdAuthRequestor = secboot.NewSystemdAuthRequestor +) + +type platformKeyDataHandler struct{} + +func (pkdh *platformKeyDataHandler) RecoverKeys(data *secboot.PlatformKeyData, encryptedPayload []byte) ([]byte, error) { + authRequestor := secbootNewSystemdAuthRequestor("", "") + authenticator, err := fido2.ConnectToFIDO2Authenticator(authRequestor) + if err != nil { + return nil, err + } + return fido2RecoverKeysWithFIDOProvider("tpm2", data, encryptedPayload, authenticator) +} + +func (*platformKeyDataHandler) RecoverKeysWithAuthKey(data *secboot.PlatformKeyData, encryptedPayload, key []byte) ([]byte, error) { + return nil, errors.New("unsupported action") +} + +func (*platformKeyDataHandler) ChangeAuthKey(data *secboot.PlatformKeyData, old, new []byte, context any) ([]byte, error) { + return nil, errors.New("unsupported action") +} + +func init() { + secbootPlatformFlags := secboot.PlatformKeyDataHandlerFlags(0).AddPlatformFlags(3) + secboot.RegisterPlatformKeyDataHandler(platformName, new(platformKeyDataHandler), secbootPlatformFlags) +} diff --git a/tpm2/fido2/platform_test.go b/tpm2/fido2/platform_test.go new file mode 100644 index 00000000..d3b7b9f1 --- /dev/null +++ b/tpm2/fido2/platform_test.go @@ -0,0 +1,136 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package tpm2_fido2_test + +import ( + . "gopkg.in/check.v1" + + "github.com/canonical/go-tpm2" + "github.com/snapcore/secboot" + "github.com/snapcore/secboot/fido2" + "github.com/snapcore/secboot/internal/testutil" + "github.com/snapcore/secboot/internal/tpm2test" + . "github.com/snapcore/secboot/tpm2" + . "github.com/snapcore/secboot/tpm2/fido2" +) + +type platformSuite struct { + tpm2test.TPMTest + + lastEncryptedPayload []byte +} + +var _ = Suite(&platformSuite{}) + +func (s *platformSuite) SetUpSuite(c *C) { + s.TPMFeatures = tpm2test.TPMFeatureOwnerHierarchy | + tpm2test.TPMFeatureEndorsementHierarchy | + tpm2test.TPMFeatureLockoutHierarchy | + tpm2test.TPMFeaturePCR | + tpm2test.TPMFeatureNV +} + +func (s *platformSuite) SetUpTest(c *C) { + s.TPMTest.SetUpTest(c) + + c.Check(s.TPM().EnsureProvisioned(ProvisionModeWithoutLockout, nil), Equals, ErrTPMProvisioningRequiresLockout) + + s.lastEncryptedPayload = nil + s.AddCleanup(MockSecbootNewKeyData(func(params *secboot.KeyParams) (*secboot.KeyData, error) { + s.lastEncryptedPayload = params.EncryptedPayload + return secboot.NewKeyData(params) + })) + origKdf := secboot.SetArgon2KDF(&testutil.MockArgon2KDF{}) + s.AddCleanup(func() { secboot.SetArgon2KDF(origKdf) }) +} + +func (s *platformSuite) TestPlatformName(c *C) { + c.Check(PlatformName, Equals, "tpm2-fido2") +} + +func (s *platformSuite) TestRecoverKeys(c *C) { + params := &ProtectKeyParams{ + PCRProfile: tpm2test.NewPCRProfileFromCurrentValues(tpm2.HashAlgorithmSHA256, []int{7}), + PCRPolicyCounterHandle: s.NextAvailableHandle(c, 0x0181fff0), + Role: "foo", + } + + // This is needed because the combined TPM2+FIDO2 platform first creates the TPM protected key + // and then the FIDO2 platform needs to recover the symmetric secret from the TPM in order to + // pass it to the FIDO2 authenticator. Since the FIDO2 API is provider agnostic/isn't supplied with a + // TPM connection, we need to temporarily close the mock connection prior to calling it, for the tests. + restore := MockFido2NewFIDO2ProtectedKeyWithSaltProvider(func() { + s.AddCleanup(s.CloseMockConnection(c)) + }) + defer restore() + + // Using a physical FIDO2 authenticator with 12345 set as the PIN + authRequestor := &testutil.MockFidoAuthRequestor{Pin: "12345"} + + authenticator, err := fido2.ConnectToFIDO2Authenticator(authRequestor) + c.Assert(err, IsNil) + + kd, expectedPrimaryKey, expectedUnlockKey, err := NewTPM2FIDO2ProtectedKey(s.TPM(), params, authenticator) + c.Assert(err, IsNil) + + restore = MockSecbootNewSystemdAuthRequestor(authRequestor) + defer restore() + + unlockKey, primaryKey, err := kd.RecoverKeys() + c.Check(err, IsNil) + c.Check(unlockKey, DeepEquals, expectedUnlockKey) + c.Check(primaryKey, DeepEquals, expectedPrimaryKey) +} + +func (s *platformSuite) TestRecoverKeysBio(c *C) { + params := &ProtectKeyParams{ + PCRProfile: tpm2test.NewPCRProfileFromCurrentValues(tpm2.HashAlgorithmSHA256, []int{7}), + PCRPolicyCounterHandle: s.NextAvailableHandle(c, 0x0181fff0), + Role: "foo", + } + + // This is needed because the combined TPM2+FIDO2 platform first creates the TPM protected key + // and then the FIDO2 platform needs to recover the symmetric secret from the TPM in order to + // pass it to the FIDO2 authenticator. Since the FIDO2 API is provider agnostic/isn't supplied with a + // TPM connection, we need to temporarily close the mock connection prior to calling it, for the tests. + restore := MockFido2NewFIDO2ProtectedKeyWithSaltProvider(func() { + s.AddCleanup(s.CloseMockConnection(c)) + }) + defer restore() + + // Using a physical FIDO2 authenticator with 12345 set as the PIN and a fingerprint enrolled + // but not passing the PIN. + // Fingerprint reading failure causes this test to fail. + authRequestor := &testutil.MockFidoAuthRequestor{Pin: ""} + + authenticator, err := fido2.ConnectToFIDO2Authenticator(authRequestor) + c.Assert(err, IsNil) + + kd, expectedPrimaryKey, expectedUnlockKey, err := NewTPM2FIDO2ProtectedKey(s.TPM(), params, authenticator) + c.Assert(err, IsNil) + + restore = MockSecbootNewSystemdAuthRequestor(authRequestor) + defer restore() + + unlockKey, primaryKey, err := kd.RecoverKeys() + c.Check(err, IsNil) + c.Check(unlockKey, DeepEquals, expectedUnlockKey) + c.Check(primaryKey, DeepEquals, expectedPrimaryKey) +} diff --git a/tpm2/fido2/tpm2_fido2_test.go b/tpm2/fido2/tpm2_fido2_test.go new file mode 100644 index 00000000..a803cd20 --- /dev/null +++ b/tpm2/fido2/tpm2_fido2_test.go @@ -0,0 +1,79 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package tpm2_fido2_test + +import ( + "flag" + "fmt" + "math/rand" + "os" + "testing" + "time" + + tpm2_testutil "github.com/canonical/go-tpm2/testutil" + + "github.com/canonical/go-tpm2" + . "gopkg.in/check.v1" + + . "github.com/snapcore/secboot/tpm2" +) + +var ( + testCACert []byte + testEkCert []byte + + testAuth = []byte("1234") +) + +func init() { + tpm2_testutil.AddCommandLineFlags() +} + +func Test(t *testing.T) { TestingT(t) } + +// Set the hierarchy auth to testAuth. Fatal on failure +func setHierarchyAuthForTest(t *testing.T, tpm *Connection, hierarchy tpm2.ResourceContext) { + if err := tpm.HierarchyChangeAuth(hierarchy, testAuth, nil); err != nil { + t.Fatalf("HierarchyChangeAuth failed: %v", err) + } +} + +func TestMain(m *testing.M) { + // Provide a way for run-tests to configure this in a way that + // can be ignored by other suites + if _, ok := os.LookupEnv("USE_MSSIM"); ok { + tpm2_testutil.TPMBackend = tpm2_testutil.TPMBackendMssim + } + + flag.Parse() + rand.Seed(time.Now().UnixNano()) + os.Exit(func() int { + if tpm2_testutil.TPMBackend == tpm2_testutil.TPMBackendMssim { + simulatorCleanup, err := tpm2_testutil.LaunchTPMSimulator(nil) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot launch TPM simulator: %v\n", err) + return 1 + } + defer simulatorCleanup() + } + + return m.Run() + }()) +} diff --git a/tpm2/platform.go b/tpm2/platform.go index 0a56aa21..9310150c 100644 --- a/tpm2/platform.go +++ b/tpm2/platform.go @@ -38,20 +38,13 @@ const platformName = "tpm2" type platformKeyDataHandler struct{} -func (h *platformKeyDataHandler) recoverKeysCommon(data *secboot.PlatformKeyData, encryptedPayload, authKey []byte) ([]byte, error) { +func (h *platformKeyDataHandler) recoverSealedKeyData(data *secboot.PlatformKeyData) (*SealedKeyData, error) { if data.Generation < 0 || int64(data.Generation) > math.MaxUint32 { return nil, &secboot.PlatformHandlerError{ Type: secboot.PlatformHandlerErrorInvalidData, Err: fmt.Errorf("invalid key data generation: %d", data.Generation)} } - kdfAlg, err := hashAlgorithmIdFromCryptoHash(data.KDFAlg) - if err != nil { - return nil, &secboot.PlatformHandlerError{ - Type: secboot.PlatformHandlerErrorInvalidData, - Err: errors.New("invalid KDF algorithm")} - } - var k *SealedKeyData if err := json.Unmarshal(data.EncodedHandle, &k); err != nil { return nil, &secboot.PlatformHandlerError{ @@ -67,6 +60,10 @@ func (h *platformKeyDataHandler) recoverKeysCommon(data *secboot.PlatformKeyData Err: fmt.Errorf("invalid key data version: %d", k.data.Version())} } + return k, nil +} + +func (h *platformKeyDataHandler) recoverSymmetricKey(k *SealedKeyData, authKey []byte) ([]byte, error) { tpm, err := ConnectToDefaultTPM() switch { case err == ErrNoTPM2Device: @@ -102,6 +99,27 @@ func (h *platformKeyDataHandler) recoverKeysCommon(data *secboot.PlatformKeyData return nil, xerrors.Errorf("cannot unseal key: %w", err) } + return symKey, nil +} + +func (h *platformKeyDataHandler) recoverKeysCommon(data *secboot.PlatformKeyData, encryptedPayload, authKey []byte) ([]byte, error) { + k, err := h.recoverSealedKeyData(data) + if err != nil { + return nil, err + } + + symKey, err := h.recoverSymmetricKey(k, authKey) + if err != nil { + return nil, err + } + + kdfAlg, err := hashAlgorithmIdFromCryptoHash(data.KDFAlg) + if err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: errors.New("invalid KDF algorithm")} + } + payload, err := k.data.Decrypt(symKey, encryptedPayload, uint32(data.Generation), []byte(data.Role), kdfAlg, data.AuthMode) if err != nil { return nil, &secboot.PlatformHandlerError{ @@ -239,7 +257,18 @@ func (h *platformKeyDataHandler) ChangeAuthKey(data *secboot.PlatformKeyData, ol return newHandle, nil } +func (h *platformKeyDataHandler) GetSymmetricKey(data *secboot.PlatformKeyData, authKey []byte) ([]byte, error) { + k, err := h.recoverSealedKeyData(data) + if err != nil { + return nil, err + } + + return h.recoverSymmetricKey(k, authKey) +} + func init() { + // TODO define a common flag which indicates that the platform can be used as FIDO2's hmac-secret salt provider + // Just use the flags to describe the current version of this platform. flags := secboot.PlatformKeyDataHandlerFlags(0).AddPlatformFlags(3) secboot.RegisterPlatformKeyDataHandler(platformName, new(platformKeyDataHandler), flags)