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)