From 21a535a546bcf8c853d9a63fb7aeb5301f2e6078 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Wed, 16 Jul 2025 20:15:27 +0300 Subject: [PATCH 1/7] fido2: add API for go-libfido2 --- fido2/fido2.go | 241 ++++++++++++++++++++++++++++++++++++++++++++ fido2/fido2_test.go | 45 +++++++++ go.mod | 2 + go.sum | 13 +++ 4 files changed, 301 insertions(+) create mode 100644 fido2/fido2.go create mode 100644 fido2/fido2_test.go diff --git a/fido2/fido2.go b/fido2/fido2.go new file mode 100644 index 00000000..bc04153e --- /dev/null +++ b/fido2/fido2.go @@ -0,0 +1,241 @@ +// -*- 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" + "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/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= From d9ccd89a98fd16d8149a6a79090a6444c0f63d30 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Wed, 16 Jul 2025 20:23:23 +0300 Subject: [PATCH 2/7] multiple: add the new FIDO2 platform the new FIDO2 platform is not meant to be used standalone but only as an additional factor on top of existing platforms that support it. --- fido2/export_test.go | 24 +++ fido2/keydata.go | 307 ++++++++++++++++++++++++++++++++++++++ fido2/keydata_test.go | 28 ++++ fido2/platform.go | 165 ++++++++++++++++++++ fido2/platform_test.go | 96 ++++++++++++ internal/testutil/fido.go | 82 ++++++++++ 6 files changed, 702 insertions(+) create mode 100644 fido2/export_test.go create mode 100644 fido2/keydata.go create mode 100644 fido2/keydata_test.go create mode 100644 fido2/platform.go create mode 100644 fido2/platform_test.go create mode 100644 internal/testutil/fido.go 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/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/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, + } +} From b20a589d0feb474477252dec333be2730e57a99c Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Wed, 16 Jul 2025 20:26:05 +0300 Subject: [PATCH 3/7] multiple: add support for FIDO2 related actions in platform agnostic API platforms that implement the FIDO2ProviderInterface will need to implement the GetSymmetricKey function which will be used by the FIDO2 platform in order to retrieve a symmetric key. This symmetric key will then be used in conjuction with a FIDO2 authenticator to derive the final symmetric key which will be used for unlocking the disk unlocking key. Platform agnostic API's Keydata was extended with a new function RecoverFIDO2Salt() which will call the handler's GetSymmetricKey if the KeyData indicates that its handler is a platform which requires FIDO2. --- keydata.go | 30 ++++++++++++++++++++++++++++++ platform.go | 2 ++ 2 files changed, 32 insertions(+) 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. From 51c89b3b01c708ddcaba163a3849be58768864af Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Wed, 16 Jul 2025 20:30:25 +0300 Subject: [PATCH 4/7] tpm2/platform.go: make tpm2 a FIDO2Provider --- tpm2/platform.go | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) 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) From ba2842211a85ff6deb427a06368fd99bb5604c0b Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Wed, 16 Jul 2025 20:31:27 +0300 Subject: [PATCH 5/7] fido2/tpm2: add the new tpm2-fido2 platform the tpm2-fido2 platform extends the existing plain tpm2 platform to add support for FIDO2 authenticators. It is essentially implemented as a shim platform which first unseals the necessary key material from the TPM and then passes it on to the FIDO2 platform in order to get the final key material required for unlocking the disk unlocking key. --- tpm2/fido2/export_test.go | 58 +++++++++++++++ tpm2/fido2/keydata.go | 40 ++++++++++ tpm2/fido2/platform.go | 60 +++++++++++++++ tpm2/fido2/platform_test.go | 136 ++++++++++++++++++++++++++++++++++ tpm2/fido2/tpm2_fido2_test.go | 79 ++++++++++++++++++++ 5 files changed, 373 insertions(+) create mode 100644 tpm2/fido2/export_test.go create mode 100644 tpm2/fido2/keydata.go create mode 100644 tpm2/fido2/platform.go create mode 100644 tpm2/fido2/platform_test.go create mode 100644 tpm2/fido2/tpm2_fido2_test.go 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() + }()) +} From 6bbec6270da5b74c59bcf96cf0845a3dafd2595f Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Tue, 29 Jul 2025 19:23:56 +0300 Subject: [PATCH 6/7] fido2: use exp/slices to support Go 1.18 --- fido2/fido2.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fido2/fido2.go b/fido2/fido2.go index bc04153e..32d22c21 100644 --- a/fido2/fido2.go +++ b/fido2/fido2.go @@ -23,7 +23,8 @@ import ( "errors" "fmt" "log" - "slices" + + "golang.org/x/exp/slices" "github.com/keys-pub/go-libfido2" "github.com/snapcore/secboot" From d1215fe929d86faaaed73658455c3117861b2a71 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Wed, 10 Sep 2025 11:39:41 +0300 Subject: [PATCH 7/7] .g/workflows/test.yaml: add libfido2-dev to dependencies --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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