From 6591ce886c65b4743b9c3d741275afc485f473da Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:36:11 -0400 Subject: [PATCH] Remove U2F fallback support from client tools (#43133) (#43274) U2F support was deprecated in favor of WebAuthn many releases ago, however, not all references were removed when working on https://github.com/gravitational/teleport/issues/10375. This eliminates the last remaining inclusions of github.com/flynn/u2f and github.com/flynn/hid from lib/client and drops all support of falling back to U2F if client tools are not built with FIDO2 enabled. In practice, this should only cause problems for people building tsh/tctl locally without setting the correct build flags. All release artifacts published should already be built with the appropriate flags and not cause any issues as a result. Updates https://github.com/gravitational/teleport/issues/43112. --- go.mod | 2 - go.sum | 4 - lib/auth/webauthncli/api.go | 48 +-- lib/auth/webauthncli/export_test.go | 23 - lib/auth/webauthncli/fido2.go | 5 +- lib/auth/webauthncli/fido2_test.go | 39 -- lib/auth/webauthncli/fuzz_test.go | 66 --- lib/auth/webauthncli/u2f.go | 147 ------- lib/auth/webauthncli/u2f_login.go | 162 -------- lib/auth/webauthncli/u2f_login_test.go | 486 ---------------------- lib/auth/webauthncli/u2f_other.go | 26 -- lib/auth/webauthncli/u2f_register.go | 292 ------------- lib/auth/webauthncli/u2f_register_test.go | 221 ---------- lib/auth/webauthncli/u2f_windows.go | 23 - 14 files changed, 10 insertions(+), 1534 deletions(-) delete mode 100644 lib/auth/webauthncli/export_test.go delete mode 100644 lib/auth/webauthncli/fuzz_test.go delete mode 100644 lib/auth/webauthncli/u2f.go delete mode 100644 lib/auth/webauthncli/u2f_login.go delete mode 100644 lib/auth/webauthncli/u2f_login_test.go delete mode 100644 lib/auth/webauthncli/u2f_other.go delete mode 100644 lib/auth/webauthncli/u2f_register.go delete mode 100644 lib/auth/webauthncli/u2f_register_test.go delete mode 100644 lib/auth/webauthncli/u2f_windows.go diff --git a/go.mod b/go.mod index 7393a2e70f841..19e55f3a918ec 100644 --- a/go.mod +++ b/go.mod @@ -84,8 +84,6 @@ require ( github.com/elastic/go-elasticsearch/v8 v8.11.1 github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3 github.com/evanphx/json-patch v5.7.0+incompatible - github.com/flynn/hid v0.0.0-20190502022136-f1b9b6cc019a - github.com/flynn/u2f v0.0.0-20180613185708-15554eb68e5d github.com/fsouza/fake-gcs-server v1.47.7 github.com/fxamacker/cbor/v2 v2.5.0 github.com/ghodss/yaml v1.0.0 diff --git a/go.sum b/go.sum index 94b76fb62ee8e..357f07e5d0872 100644 --- a/go.sum +++ b/go.sum @@ -553,10 +553,6 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/flynn/hid v0.0.0-20190502022136-f1b9b6cc019a h1:fsyWnwbywFpHJS4T55vDW+UUeWP2WomJbB45/jf4If4= -github.com/flynn/hid v0.0.0-20190502022136-f1b9b6cc019a/go.mod h1:Osz+xPHFsGWK9kZCEVcwXazcF/CHjscCVZosNFgwUIY= -github.com/flynn/u2f v0.0.0-20180613185708-15554eb68e5d h1:2D6Rp/MRcrKnRFr7kfgBOJnJPFN0jPfc36ggct5MaK0= -github.com/flynn/u2f v0.0.0-20180613185708-15554eb68e5d/go.mod h1:shcCQPgKtaJz4obqb6Si031WgtSrW+Tj+ZLq/mRNrM8= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= diff --git a/lib/auth/webauthncli/api.go b/lib/auth/webauthncli/api.go index 346473774b78c..8278c77a03634 100644 --- a/lib/auth/webauthncli/api.go +++ b/lib/auth/webauthncli/api.go @@ -110,7 +110,7 @@ type LoginOpts struct { AuthenticatorAttachment AuthenticatorAttachment } -// Login performs client-side, U2F-compatible, Webauthn login. +// Login performs client-side, Webauthn login. // This method blocks until either device authentication is successful or the // context is canceled. Calling Login without a deadline or cancel condition // may cause it to block forever. @@ -180,26 +180,9 @@ func crossPlatformLogin( ctx context.Context, origin string, assertion *wantypes.CredentialAssertion, prompt LoginPrompt, opts *LoginOpts, ) (*proto.MFAAuthenticateResponse, string, error) { - if isLibfido2Enabled() { - log.Debug("FIDO2: Using libfido2 for assertion") - return FIDO2Login(ctx, origin, assertion, prompt, opts) - } - - ackTouch, err := prompt.PromptTouch() - if err != nil { - return nil, "", trace.Wrap(err) - } - - resp, err := U2FLogin(ctx, origin, assertion) - if err != nil { - return nil, "", trace.Wrap(err) - } - - if err := ackTouch(); err != nil { - return nil, "", trace.Wrap(err) - } - - return resp, "" /* credentialUser */, err + log.Debug("FIDO2: Using libfido2 for assertion") + resp, user, err := FIDO2Login(ctx, origin, assertion, prompt, opts) + return resp, user, trace.Wrap(err) } func platformLogin(origin, user string, assertion *wantypes.CredentialAssertion, prompt LoginPrompt) (*proto.MFAAuthenticateResponse, string, error) { @@ -229,7 +212,7 @@ type RegisterPrompt interface { PromptTouch() (TouchAcknowledger, error) } -// Register performs client-side, U2F-compatible, Webauthn registration. +// Register performs client-side, Webauthn registration. // This method blocks until either device authentication is successful or the // context is canceled. Calling Register without a deadline or cancel condition // may cause it block forever. @@ -244,28 +227,15 @@ func Register( return wanwin.Register(ctx, origin, cc) } - if isLibfido2Enabled() { - log.Debug("FIDO2: Using libfido2 for credential creation") - return FIDO2Register(ctx, origin, cc, prompt) - } - - ackTouch, err := prompt.PromptTouch() - if err != nil { - return nil, trace.Wrap(err) - } - - resp, err := U2FRegister(ctx, origin, cc) - if err != nil { - return nil, trace.Wrap(err) - } - - return resp, trace.Wrap(ackTouch()) + log.Debug("FIDO2: Using libfido2 for credential creation") + resp, err := FIDO2Register(ctx, origin, cc, prompt) + return resp, trace.Wrap(err) } // HasPlatformSupport returns true if the platform supports client-side // WebAuthn-compatible logins. func HasPlatformSupport() bool { - return IsFIDO2Available() || touchid.IsAvailable() || isU2FAvailable() + return IsFIDO2Available() || touchid.IsAvailable() } // IsFIDO2Available returns true if FIDO2 is implemented either via native diff --git a/lib/auth/webauthncli/export_test.go b/lib/auth/webauthncli/export_test.go deleted file mode 100644 index 4710d086d7093..0000000000000 --- a/lib/auth/webauthncli/export_test.go +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli - -var U2FDevices = &u2fDevices -var U2FOpen = &u2fOpen -var U2FNewToken = &u2fNewToken diff --git a/lib/auth/webauthncli/fido2.go b/lib/auth/webauthncli/fido2.go index db3595aa456d4..e56841c5c938d 100644 --- a/lib/auth/webauthncli/fido2.go +++ b/lib/auth/webauthncli/fido2.go @@ -28,7 +28,6 @@ import ( "encoding/json" "errors" "fmt" - "os" "sync" "time" @@ -138,9 +137,7 @@ var ( // isLibfido2Enabled returns true if libfido2 is available in the current build. func isLibfido2Enabled() bool { - val, ok := os.LookupEnv("TELEPORT_FIDO2") - // Default to enabled, otherwise obey the env variable. - return !ok || val == "1" + return true } // fido2Login implements FIDO2Login. diff --git a/lib/auth/webauthncli/fido2_test.go b/lib/auth/webauthncli/fido2_test.go index 1f319e537e4f0..0e4486ab85db3 100644 --- a/lib/auth/webauthncli/fido2_test.go +++ b/lib/auth/webauthncli/fido2_test.go @@ -27,7 +27,6 @@ import ( "crypto/rand" "errors" "fmt" - "os" "sync" "testing" "time" @@ -150,44 +149,6 @@ func (p *pinCancelPrompt) PromptTouch() (wancli.TouchAcknowledger, error) { return func() error { return nil }, nil } -func TestIsFIDO2Available(t *testing.T) { - const fido2Key = "TELEPORT_FIDO2" - tests := []struct { - name string - setenv func() - want bool - }{ - { - name: "env var unset", - setenv: func() { - _ = os.Unsetenv(fido2Key) - }, - want: true, - }, - { - name: "env var set to 1", - setenv: func() { - t.Setenv(fido2Key, "1") - }, - want: true, - }, - { - name: "env var set to 0", - setenv: func() { - t.Setenv(fido2Key, "0") - }, - want: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - test.setenv() - got := wancli.IsFIDO2Available() - require.Equal(t, test.want, got, "IsFIDO2Available") - }) - } -} - func TestFIDO2Login(t *testing.T) { resetFIDO2AfterTests(t) wancli.FIDO2PollInterval = 1 * time.Millisecond // run fast on tests diff --git a/lib/auth/webauthncli/fuzz_test.go b/lib/auth/webauthncli/fuzz_test.go deleted file mode 100644 index 570532892df10..0000000000000 --- a/lib/auth/webauthncli/fuzz_test.go +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli - -import ( - "encoding/base64" - "testing" - - "github.com/stretchr/testify/require" -) - -func unverifiedBase64Bytes(str string) []byte { - bytes, _ := base64.StdEncoding.DecodeString(str) - return bytes -} - -func FuzzParseU2FRegistrationResponse(f *testing.F) { - f.Add(unverifiedBase64Bytes("BQR+GkzX1lnNopfxpz1baMSaU1wlqZaJ7tGrOJ14p" + - "QucBTZR4sKwiJZTuponQvXwJuj3zdanzMH1Os7pjFy4IbEegHIXW/sZVdiZUsjdUQH6/WD0" + - "4rllLPEYiiocu/fS1zmntWNBAwI1DOgGJ4FSDsAIidZekwAapqsln+RaNiUgvC4WY0qSYGl" + - "3uDz2O6jbaBCjTcLzifcjyaQb3KGLs3EEPN1eNeJcjACVpyWUMZDSOlFkFaE4q0QMJqCCS3" + - "c3ng/cMIIBazCCARCgAwIBAgIBATAKBggqhkjOPQQDAjASMRAwDgYDVQQKEwdUZXN0IENBM" + - "B4XDTIzMDgxNjE4MjAwNloXDTIzMDgxNjE5MjAwNlowEjEQMA4GA1UEChMHVGVzdCBDQTBZ" + - "MBMGByqGSM49AgEGCCqGSM49AwEHA0IABH4aTNfWWc2il/GnPVtoxJpTXCWplonu0as4nXi" + - "lC5wFNlHiwrCIllO6midC9fAm6PfN1qfMwfU6zumMXLghsR6jVzBVMA4GA1UdDwEB/wQEAw" + - "ICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF/" + - "vV/LdwWaAA1LA3uAYj9ErbjVzAKBggqhkjOPQQDAgNJADBGAiEAixVchjFZ+oEhTXJYCUtx" + - "xi/z4PooqF/tlNGKPUHPD6QCIQCqo129HBg5QaUjXc7dHxGVc3joct+CTSIwtyUKSN6twTB" + - "GAiEApJfP1bm0/sZTUZ8XeN86WdHVb4+Qz3lwB0d1GxkYM7YCIQCJyXkyu4Y7bm0YPP+XB8" + - "3IO2WCmJKNsCT8sZuRRs/ryw==")) - f.Add(unverifiedBase64Bytes("BQSEpSKEdxODGvlDbmWKkhqTzCriCEb72v5+dh1mf" + - "rZwPxa2DihjLO4LrrN79bz/IYT4AtlNlwP3mDDmv1dhl5XpgH5OJ92XUa+lHeR/ScWXrlld" + - "5saUtmuA9Osg3UFK2wActU2Yq0yT8pEzECZba/npHDmSHFs25i0FWiy7ZSSE0hyi2mACyXm" + - "yLyRyEg6mH84aVMvW9M0QjFMDmjaZpqcFbXVkf7luOrvLhzo2kUd4fgAZ5bsVlb6Ggfl7Kb" + - "0q3MPVMIIBajCCARCgAwIBAgIBATAKBggqhkjOPQQDAjASMRAwDgYDVQQKEwdUZXN0IENBM" + - "B4XDTIzMDgxNjE4MjIyMFoXDTIzMDgxNjE5MjIyMFowEjEQMA4GA1UEChMHVGVzdCBDQTBZ" + - "MBMGByqGSM49AgEGCCqGSM49AwEHA0IABISlIoR3E4Ma+UNuZYqSGpPMKuIIRvva/n52HWZ" + - "+tnA/FrYOKGMs7guus3v1vP8hhPgC2U2XA/eYMOa/V2GXlemjVzBVMA4GA1UdDwEB/wQEAw" + - "ICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHM" + - "KsFLCtx6PUVkpDw8DdKQf9C0zAKBggqhkjOPQQDAgNIADBFAiByA6ISaK+iwQ7TC40IPMXm" + - "mHzIf32b0YZwsHTUNf5jDgIhAPDBB5n3wR4d3F+R2PkvbwneqwcwkrrEzpBEXwwsEhpOMEQ" + - "CIFAYEWOJZevn6IxtTBg5w/krrHA9z0pzAHRs13KOPEHEAiArbTczB8nS3HIeCJqUt8wclg" + - "TVPnbu99FYtP5FueW8Hg==")) - - f.Fuzz(func(t *testing.T, b []byte) { - require.NotPanics(t, func() { - _, _ = parseU2FRegistrationResponse(b) - }) - }) -} diff --git a/lib/auth/webauthncli/u2f.go b/lib/auth/webauthncli/u2f.go deleted file mode 100644 index 140b30ac6c8ad..0000000000000 --- a/lib/auth/webauthncli/u2f.go +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli - -import ( - "context" - "errors" - "strings" - "time" - - "github.com/flynn/u2f/u2fhid" - "github.com/flynn/u2f/u2ftoken" - "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" -) - -// DevicePollInterval is the interval between polling attempts on Webauthn or -// U2F devices. -// Used by otherwise tight loops such as RunOnU2FDevices and related methods, -// like Login. -var DevicePollInterval = 200 * time.Millisecond - -// ErrAlreadyRegistered may be used by RunOnU2FDevices callbacks to signify that -// a certain authenticator is already registered, and thus should be removed -// from the loop. -var ErrAlreadyRegistered = errors.New("already registered") - -var errKeyMissingOrNotVerified = errors.New("key missing or user presence not verified") - -// Token represents the actions possible using an U2F/CTAP1 token. -type Token interface { - CheckAuthenticate(req u2ftoken.AuthenticateRequest) error - Authenticate(req u2ftoken.AuthenticateRequest) (*u2ftoken.AuthenticateResponse, error) - Register(req u2ftoken.RegisterRequest) ([]byte, error) -} - -// u2fDevices, u2fOpen and u2fNewToken allows tests to fake interactions with -// devices. -var u2fDevices = u2fhid.Devices -var u2fOpen = u2fhid.Open -var u2fNewToken = func(d u2ftoken.Device) Token { - return u2ftoken.NewToken(d) -} - -type deviceKey struct { - Callback int - Path string -} - -// RunOnU2FDevices polls for new U2F/CTAP1 devices and invokes the callbacks -// against them in regular intervals, running until either one callback succeeds -// or the context is canceled. -// Typically, each callback represents a {credential,rpid} pair to check against -// the device. -// Calling this method using a context without a cancel or deadline means it -// will execute until successful (which may be never). -// Most callers should prefer higher-abstraction functions such as Login. -func RunOnU2FDevices(ctx context.Context, runCredentials ...func(Token) error) error { - ticker := time.NewTicker(DevicePollInterval) - defer ticker.Stop() - - removedDevices := make(map[deviceKey]bool) - for { - switch err := runOnU2FDevicesOnce(removedDevices, runCredentials); { - case errors.Is(err, errKeyMissingOrNotVerified): - // This is expected to happen a few times. - case err != nil: - errMsg := err.Error() - // suppress error spam, this error doesnt prevent u2f from working - if !strings.Contains(errMsg, "hid: privilege violation") && - !strings.Contains(errMsg, "hid: not permitted") { - log.WithError(err).Debug("Error interacting with U2F devices") - } - default: // OK, success. - return nil - } - - select { - case <-ticker.C: - case <-ctx.Done(): - return trace.Wrap(ctx.Err()) - } - } -} - -func runOnU2FDevicesOnce(removedDevices map[deviceKey]bool, runCredentials []func(Token) error) error { - // Ask for devices every iteration, the user may plug a new device. - infos, err := u2fDevices() - if err != nil { - return trace.Wrap(err) - } - - var swallowed []error - for _, info := range infos { - dev, err := u2fOpen(info) - if err != nil { - // u2fhid.Open is a bit more prone to errors, especially "hid: privilege - // violation" errors. Try other devices before bailing. - swallowed = append(swallowed, err) - continue - } - - token := u2fNewToken(dev) - for i, fn := range runCredentials { - key := deviceKey{Callback: i, Path: info.Path} - if info.Path != "" && removedDevices[key] { - // Device previously removed during loop (likely doesn't know the key - // handle or is already registered). - // We may get to a situation where all devices are removed, but we keep - // on trying because the user may plug another device. - continue - } - - switch err := fn(token); { - case err == nil: - return nil // OK, we got it. - case errors.Is(err, u2ftoken.ErrPresenceRequired): - // Wait for user action, they will choose the device to use. - case errors.Is(err, u2ftoken.ErrUnknownKeyHandle) || errors.Is(err, ErrAlreadyRegistered): - removedDevices[key] = true // No need to try this anymore. - case err != nil: - swallowed = append(swallowed, err) - } - } - } - if len(swallowed) > 0 { - return trace.NewAggregate(swallowed...) - } - - return errKeyMissingOrNotVerified // don't wrap, simplifies comparisons -} diff --git a/lib/auth/webauthncli/u2f_login.go b/lib/auth/webauthncli/u2f_login.go deleted file mode 100644 index 732e018fbf703..0000000000000 --- a/lib/auth/webauthncli/u2f_login.go +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "fmt" - - "github.com/flynn/u2f/u2ftoken" - "github.com/go-webauthn/webauthn/protocol" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport/api/client/proto" - wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" -) - -// U2FLogin implements Login for U2F/CTAP1 devices. -// The implementation is backed exclusively by Go code, making it useful in -// scenarios where libfido2 is unavailable. -func U2FLogin(ctx context.Context, origin string, assertion *wantypes.CredentialAssertion) (*proto.MFAAuthenticateResponse, error) { - switch { - case origin == "": - return nil, trace.BadParameter("origin required") - case assertion == nil: - return nil, trace.BadParameter("assertion required") - case len(assertion.Response.AllowedCredentials) == 0 && - assertion.Response.UserVerification == protocol.VerificationRequired: - return nil, trace.BadParameter("Passwordless not supported in U2F mode. Please install a recent version of tsh.") - case len(assertion.Response.Challenge) == 0: - return nil, trace.BadParameter("assertion challenge required") - case assertion.Response.RelyingPartyID == "": - return nil, trace.BadParameter("assertion RPID required") - case len(assertion.Response.AllowedCredentials) == 0: - return nil, trace.BadParameter("assertion has no allowed credentials") - case assertion.Response.UserVerification == protocol.VerificationRequired: - return nil, trace.BadParameter( - "assertion required user verification, but it cannot be guaranteed under CTAP1") - } - - // References: - // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#u2f-authenticatorGetAssertion-interoperability - // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion - - ccdJSON, err := json.Marshal(&CollectedClientData{ - Type: string(protocol.AssertCeremony), - Challenge: base64.RawURLEncoding.EncodeToString(assertion.Response.Challenge), - Origin: origin, - }) - if err != nil { - return nil, trace.Wrap(err) - } - ccdHash := sha256.Sum256(ccdJSON) - rpID := assertion.Response.RelyingPartyID - rpIDHash := sha256.Sum256([]byte(rpID)) - - // Did we get the App ID extension? - var appID string - var appIDHash [32]byte - if value, ok := assertion.Response.Extensions[wantypes.AppIDExtension]; ok { - appID = fmt.Sprint(value) - appIDHash = sha256.Sum256([]byte(appID)) - } - - // Variables below are filled by the callback on success. - var authCred wantypes.CredentialDescriptor - var authResp *u2ftoken.AuthenticateResponse - var usedAppID bool - makeAuthU2F := func(cred wantypes.CredentialDescriptor, req u2ftoken.AuthenticateRequest, appID bool) func(Token) error { - return func(token Token) error { - if err := token.CheckAuthenticate(req); err != nil { - return err // don't wrap, inspected by RunOnU2FDevices - } - resp, err := token.Authenticate(req) - if err != nil { - return err // don't wrap, inspected by RunOnU2FDevices - } - authCred = cred - authResp = resp - usedAppID = appID - return nil - } - } - - // Assemble credential+RPID pairs to attempt. - var fns []func(Token) error - for _, cred := range assertion.Response.AllowedCredentials { - req := u2ftoken.AuthenticateRequest{ - Challenge: ccdHash[:], - Application: rpIDHash[:], - KeyHandle: cred.CredentialID, - } - fns = append(fns, makeAuthU2F(cred, req, false /* appID */)) - if appID != "" { - req.Application = appIDHash[:] - fns = append(fns, makeAuthU2F(cred, req, true /* appID */)) - } - } - - // Run! - if err := RunOnU2FDevices(ctx, fns...); err != nil { - return nil, trace.Wrap(err) - } - - // Assemble extensions. - var exts *wantypes.AuthenticationExtensionsClientOutputs - if usedAppID { - exts = &wantypes.AuthenticationExtensionsClientOutputs{AppID: true} - } - - // Assemble authenticator data. - // RPID (32 bytes) + User Presence (0x01, 1 byte) + Counter (4 bytes) - authData := &bytes.Buffer{} - if usedAppID { - authData.Write(appIDHash[:]) - } else { - authData.Write(rpIDHash[:]) - } - authData.Write(authResp.RawResponse[:5]) // User Presence (1) + Counter (4) - - resp := &wantypes.CredentialAssertionResponse{ - PublicKeyCredential: wantypes.PublicKeyCredential{ - Credential: wantypes.Credential{ - ID: base64.RawURLEncoding.EncodeToString(authCred.CredentialID), - Type: string(protocol.PublicKeyCredentialType), - }, - RawID: authCred.CredentialID, - Extensions: exts, - }, - AssertionResponse: wantypes.AuthenticatorAssertionResponse{ - AuthenticatorResponse: wantypes.AuthenticatorResponse{ - ClientDataJSON: ccdJSON, - }, - AuthenticatorData: authData.Bytes(), - Signature: authResp.Signature, - }, - } - return &proto.MFAAuthenticateResponse{ - Response: &proto.MFAAuthenticateResponse_Webauthn{ - Webauthn: wantypes.CredentialAssertionResponseToProto(resp), - }, - }, nil -} diff --git a/lib/auth/webauthncli/u2f_login_test.go b/lib/auth/webauthncli/u2f_login_test.go deleted file mode 100644 index 082979d0da0f9..0000000000000 --- a/lib/auth/webauthncli/u2f_login_test.go +++ /dev/null @@ -1,486 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli_test - -import ( - "bytes" - "context" - "crypto/ecdsa" - "crypto/sha256" - "crypto/x509" - "encoding/binary" - "fmt" - "testing" - "time" - - "github.com/flynn/hid" - "github.com/flynn/u2f/u2fhid" - "github.com/flynn/u2f/u2ftoken" - "github.com/go-webauthn/webauthn/protocol" - "github.com/gravitational/trace" - "github.com/stretchr/testify/require" - - mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/auth/mocku2f" - wanlib "github.com/gravitational/teleport/lib/auth/webauthn" - wancli "github.com/gravitational/teleport/lib/auth/webauthncli" - wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" -) - -func TestLogin(t *testing.T) { - resetU2FCallbacksAfterTest(t) - - const appID = "https://example.com" - const rpID = "example.com" - const username = "llama" - const origin = appID // URL including protocol - - devUnknown, err := newFakeDevice("unknown" /* name */, "unknown" /* appID */) - require.NoError(t, err) - devAppID, err := newFakeDevice("appid" /* name */, appID /* appID */) - require.NoError(t, err) - - // Create a device that authenticates using the RPID. - // In practice, it would be registered as a Webauthn device. - devRPID, err := newFakeDevice("rpid" /* name */, rpID /* appID */) - require.NoError(t, err) - pubKeyI, err := x509.ParsePKIXPublicKey(devRPID.mfaDevice.GetU2F().PubKey) - require.NoError(t, err) - pubKeyCBOR, err := wanlib.U2FKeyToCBOR(pubKeyI.(*ecdsa.PublicKey)) - require.NoError(t, err) - devRPID.mfaDevice.Device = &types.MFADevice_Webauthn{ - Webauthn: &types.WebauthnDevice{ - CredentialId: devRPID.key.KeyHandle, - PublicKeyCbor: pubKeyCBOR, - }, - } - - // Use a LoginFlow to create and check assertions. - identity := &fakeIdentity{ - Devices: []*types.MFADevice{ - devUnknown.mfaDevice, - devAppID.mfaDevice, - devRPID.mfaDevice, - }, - } - loginFlow := &wanlib.LoginFlow{ - U2F: &types.U2F{ - AppID: appID, - }, - Webauthn: &types.Webauthn{ - RPID: rpID, - }, - Identity: identity, - } - - tests := []struct { - name string - devs []*fakeDevice - setUserPresence *fakeDevice - removeAppID bool - wantErr bool - wantRawID []byte - }{ - { - name: "OK U2F login with App ID", - devs: []*fakeDevice{devUnknown, devAppID}, - setUserPresence: devAppID, - wantRawID: devAppID.key.KeyHandle, - }, - { - name: "OK U2F login with RPID", - devs: []*fakeDevice{devUnknown, devRPID}, - setUserPresence: devRPID, - wantRawID: devRPID.key.KeyHandle, - }, - { - name: "OK U2F login with both App ID and RPID", - devs: []*fakeDevice{devUnknown, devRPID, devAppID}, - setUserPresence: devAppID, // user presence decides the device - wantRawID: devAppID.key.KeyHandle, - }, - { - name: "NOK U2F login with unknown App ID", - devs: []*fakeDevice{devUnknown}, - setUserPresence: devUnknown, // doesn't matter, App ID won't match. - wantErr: true, - }, - { - name: "NOK U2F login without user presence", - devs: []*fakeDevice{devUnknown, devAppID, devRPID}, - // setUserPresent unset, no devices are "tapped". - wantErr: true, - }, - } - ctx := context.Background() - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) - defer cancel() - - // Reset/set user presence flags. - for _, dev := range test.devs { - dev.SetUserPresence(false) - } - test.setUserPresence.SetUserPresence(true) - - assertion, err := loginFlow.Begin(ctx, username, &mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, - }) - require.NoError(t, err) - if test.removeAppID { - assertion.Response.Extensions = nil - } - - fakeDevs := &fakeDevices{devs: test.devs} - fakeDevs.assignU2FCallbacks() - - mfaResp, err := wancli.U2FLogin(ctx, origin, assertion) - switch hasErr := err != nil; { - case hasErr != test.wantErr: - t.Fatalf("Login returned err = %v, wantErr = %v", err, test.wantErr) - case hasErr: - return // OK, error expected - } - require.NotNil(t, mfaResp.GetWebauthn()) - require.Equal(t, test.wantRawID, mfaResp.GetWebauthn().RawId) - - _, err = loginFlow.Finish(ctx, username, wantypes.CredentialAssertionResponseFromProto(mfaResp.GetWebauthn()), &mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, - }) - require.NoError(t, err) - }) - } -} - -func TestLogin_errors(t *testing.T) { - device, err := newFakeDevice("appid" /* name */, "localhost" /* appID */) - require.NoError(t, err) - loginFlow := &wanlib.LoginFlow{ - Webauthn: &types.Webauthn{ - RPID: "localhost", - }, - Identity: &fakeIdentity{ - Devices: []*types.MFADevice{ - device.mfaDevice, - }, - }, - } - - const user = "llama" - const origin = "https://localhost" - ctx := context.Background() - okAssertion, err := loginFlow.Begin(ctx, user, &mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, - }) - require.NoError(t, err) - - tests := []struct { - name string - origin string - getAssertion func() *wantypes.CredentialAssertion - }{ - { - name: "NOK origin empty", - origin: "", - getAssertion: func() *wantypes.CredentialAssertion { - return okAssertion - }, - }, - { - name: "NOK assertion nil", - origin: origin, - getAssertion: func() *wantypes.CredentialAssertion { - return nil - }, - }, - { - name: "NOK assertion empty", - origin: origin, - getAssertion: func() *wantypes.CredentialAssertion { - return &wantypes.CredentialAssertion{} - }, - }, - { - name: "NOK assertion missing challenge", - origin: origin, - getAssertion: func() *wantypes.CredentialAssertion { - assertion, err := loginFlow.Begin(ctx, user, &mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, - }) - require.NoError(t, err) - assertion.Response.Challenge = nil - return assertion - }, - }, - { - name: "NOK assertion missing RPID", - origin: origin, - getAssertion: func() *wantypes.CredentialAssertion { - assertion, err := loginFlow.Begin(ctx, user, &mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, - }) - require.NoError(t, err) - assertion.Response.RelyingPartyID = "" - return assertion - }, - }, - { - name: "NOK assertion missing credentials", - origin: origin, - getAssertion: func() *wantypes.CredentialAssertion { - assertion, err := loginFlow.Begin(ctx, user, &mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, - }) - require.NoError(t, err) - assertion.Response.AllowedCredentials = nil - return assertion - }, - }, - { - name: "NOK assertion invalid user verification requirement", - origin: origin, - getAssertion: func() *wantypes.CredentialAssertion { - assertion, err := loginFlow.Begin(ctx, user, &mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, - }) - require.NoError(t, err) - assertion.Response.UserVerification = protocol.VerificationRequired - return assertion - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(ctx, 1*time.Second) - defer cancel() - - _, err := wancli.U2FLogin(ctx, test.origin, test.getAssertion()) - require.True(t, trace.IsBadParameter(err)) - }) - } -} - -func resetU2FCallbacksAfterTest(t *testing.T) { - oldDevices, oldOpen, oldNewToken := *wancli.U2FDevices, *wancli.U2FOpen, *wancli.U2FNewToken - oldPollInterval := wancli.DevicePollInterval - t.Cleanup(func() { - *wancli.U2FDevices = oldDevices - *wancli.U2FOpen = oldOpen - *wancli.U2FNewToken = oldNewToken - wancli.DevicePollInterval = oldPollInterval - }) -} - -type fakeDevices struct { - devs []*fakeDevice -} - -func (f *fakeDevices) assignU2FCallbacks() { - *wancli.U2FDevices = f.devices - *wancli.U2FOpen = f.open - *wancli.U2FNewToken = f.newToken - wancli.DevicePollInterval = 1 // as tight as possible. -} - -func (f *fakeDevices) devices() ([]*hid.DeviceInfo, error) { - infos := make([]*hid.DeviceInfo, len(f.devs)) - for i, dev := range f.devs { - infos[i] = dev.info - } - return infos, nil -} - -func (f *fakeDevices) open(info *hid.DeviceInfo) (*u2fhid.Device, error) { - for _, dev := range f.devs { - if dev.info == info { - return dev.dev, nil - } - } - return nil, trace.NotFound("device not found") -} - -func (f *fakeDevices) newToken(d u2ftoken.Device) wancli.Token { - innerDev := d.(*u2fhid.Device) - for _, dev := range f.devs { - if dev.dev == innerDev { - return dev - } - } - panic("device not found") -} - -type fakeDevice struct { - name string - appIDHash []byte - key *mocku2f.Key - - mfaDevice *types.MFADevice - info *hid.DeviceInfo - dev *u2fhid.Device - - // presenceCounter is used to simulate u2ftoken.ErrPresenceRequired errors. - presenceCounter int - // userPresent true means that the fakeDevice is ready to Authenticate or - // Register. - userPresent bool -} - -func newFakeDevice(name, appID string) (*fakeDevice, error) { - key, err := mocku2f.Create() - if err != nil { - return nil, err - } - pubKeyDER, err := x509.MarshalPKIXPublicKey(&key.PrivateKey.PublicKey) - if err != nil { - panic(err) - } - - mfaDevice := types.NewMFADevice( - name /* name */, fmt.Sprintf("%X", key.KeyHandle) /* ID */, time.Now() /* addedAt */) - mfaDevice.Device = &types.MFADevice_U2F{ - U2F: &types.U2FDevice{ - KeyHandle: key.KeyHandle, - PubKey: pubKeyDER, - Counter: 0, // always zeroed for simplicity - }, - } - - appIDHash := sha256.Sum256([]byte(appID)) - return &fakeDevice{ - name: name, - appIDHash: appIDHash[:], - key: key, - mfaDevice: mfaDevice, - info: &hid.DeviceInfo{ - Path: fmt.Sprintf("%v-%X", appID, key.KeyHandle), - }, - dev: &u2fhid.Device{}, - }, nil -} - -func (f *fakeDevice) SetUserPresence(present bool) { - if f == nil { - return - } - f.presenceCounter = 0 - f.userPresent = present -} - -func (f *fakeDevice) CheckAuthenticate(req u2ftoken.AuthenticateRequest) error { - // Is this the correct app and key handle? - if !bytes.Equal(req.Application, f.appIDHash) || !bytes.Equal(req.KeyHandle, f.key.KeyHandle) { - return u2ftoken.ErrUnknownKeyHandle - } - return nil -} - -func (f *fakeDevice) Authenticate(req u2ftoken.AuthenticateRequest) (*u2ftoken.AuthenticateResponse, error) { - if err := f.checkUserPresent(); err != nil { - return nil, err // Do no wrap. - } - - rawResp, err := f.key.AuthenticateRaw(req.Application, req.Challenge) - if err != nil { - return nil, trace.Wrap(err) - } - var counter uint32 - if err := binary.Read(bytes.NewReader(rawResp[1:5]), binary.BigEndian, &counter); err != nil { - return nil, err - } - sign := rawResp[5:] - - return &u2ftoken.AuthenticateResponse{ - Counter: counter, - Signature: sign, - RawResponse: rawResp, - }, nil -} - -func (f *fakeDevice) Register(req u2ftoken.RegisterRequest) ([]byte, error) { - if err := f.checkUserPresent(); err != nil { - return nil, err // Do no wrap. - } - - resp, err := f.key.RegisterRaw(req.Application, req.Challenge) - return resp, trace.Wrap(err) -} - -func (f *fakeDevice) checkUserPresent() error { - // Fail presence tests a few times. - const minPresenceCounter = 2 - if !f.userPresent || f.presenceCounter < minPresenceCounter { - f.presenceCounter++ - return u2ftoken.ErrPresenceRequired - } - - // Allowed. - f.presenceCounter = 0 - return nil -} - -type fakeIdentity struct { - User string - Devices []*types.MFADevice - LocalAuth *types.WebauthnLocalAuth - SessionData *wantypes.SessionData -} - -func (f *fakeIdentity) UpsertWebauthnLocalAuth(ctx context.Context, user string, wla *types.WebauthnLocalAuth) error { - f.LocalAuth = wla - return nil -} - -func (f *fakeIdentity) GetWebauthnLocalAuth(ctx context.Context, user string) (*types.WebauthnLocalAuth, error) { - if f.LocalAuth == nil { - return nil, trace.NotFound("not found") // code relies on not found to work properly - } - return f.LocalAuth, nil -} - -func (f *fakeIdentity) GetTeleportUserByWebauthnID(ctx context.Context, webID []byte) (string, error) { - if f.User == "" { - return "", trace.NotFound("not found") - } - return f.User, nil -} - -func (f *fakeIdentity) GetMFADevices(ctx context.Context, user string, withSecrets bool) ([]*types.MFADevice, error) { - return f.Devices, nil -} - -func (f *fakeIdentity) UpsertMFADevice(ctx context.Context, user string, d *types.MFADevice) error { - // Unimportant for the tests here. - return nil -} - -func (f *fakeIdentity) UpsertWebauthnSessionData(ctx context.Context, user, sessionID string, sd *wantypes.SessionData) error { - f.SessionData = sd - return nil -} - -func (f *fakeIdentity) GetWebauthnSessionData(ctx context.Context, user, sessionID string) (*wantypes.SessionData, error) { - return f.SessionData, nil -} - -func (f *fakeIdentity) DeleteWebauthnSessionData(ctx context.Context, user, sessionID string) error { - f.SessionData = nil - return nil -} diff --git a/lib/auth/webauthncli/u2f_other.go b/lib/auth/webauthncli/u2f_other.go deleted file mode 100644 index 0e0f2c4b1b00b..0000000000000 --- a/lib/auth/webauthncli/u2f_other.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !windows -// +build !windows - -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli - -func isU2FAvailable() bool { - return true -} diff --git a/lib/auth/webauthncli/u2f_register.go b/lib/auth/webauthncli/u2f_register.go deleted file mode 100644 index 76dcf5ec8264e..0000000000000 --- a/lib/auth/webauthncli/u2f_register.go +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli - -import ( - "bytes" - "context" - "crypto/ecdh" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/sha256" - "crypto/x509" - "encoding/asn1" - "encoding/base64" - "encoding/binary" - "encoding/json" - "fmt" - "math/big" - - "github.com/flynn/u2f/u2ftoken" - "github.com/fxamacker/cbor/v2" - "github.com/go-webauthn/webauthn/protocol" - "github.com/go-webauthn/webauthn/protocol/webauthncose" - "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" - - "github.com/gravitational/teleport/api/client/proto" - wanlib "github.com/gravitational/teleport/lib/auth/webauthn" - wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" -) - -// U2FRegister implements Register for U2F/CTAP1 devices. -// The implementation is backed exclusively by Go code, making it useful in -// scenarios where libfido2 is unavailable. -func U2FRegister(ctx context.Context, origin string, cc *wantypes.CredentialCreation) (*proto.MFARegisterResponse, error) { - // Preliminary checks, more below. - switch { - case origin == "": - return nil, trace.BadParameter("origin required") - case cc == nil: - return nil, trace.BadParameter("credential creation required") - case cc.Response.RelyingParty.ID == "": - return nil, trace.BadParameter("credential creation missing relying party ID") - } - - // U2F/CTAP1 is limited to ES256, check if it's allowed. - ok := false - for _, params := range cc.Response.Parameters { - if params.Type == protocol.PublicKeyCredentialType && params.Algorithm == webauthncose.AlgES256 { - ok = true - break - } - } - if !ok { - return nil, trace.BadParameter("ES256 not allowed by credential parameters") - } - - // Can we fulfill the authenticator selection? - if aa := cc.Response.AuthenticatorSelection.AuthenticatorAttachment; aa == protocol.Platform { - return nil, trace.BadParameter("platform attachment required by authenticator selection") - } - if rrk := cc.Response.AuthenticatorSelection.RequireResidentKey; rrk != nil && *rrk { - return nil, trace.BadParameter("resident key required by authenticator selection") - } - if uv := cc.Response.AuthenticatorSelection.UserVerification; uv == protocol.VerificationRequired { - return nil, trace.BadParameter("user verification required by authenticator selection") - } - - // Prepare challenge data for the device. - ccdJSON, err := json.Marshal(&CollectedClientData{ - Type: string(protocol.CreateCeremony), - Challenge: base64.RawURLEncoding.EncodeToString(cc.Response.Challenge), - Origin: origin, - }) - if err != nil { - return nil, trace.Wrap(err) - } - ccdHash := sha256.Sum256(ccdJSON) - rpIDHash := sha256.Sum256([]byte(cc.Response.RelyingParty.ID)) - - var appIDHash []byte - if value, ok := cc.Response.Extensions[wantypes.AppIDExtension]; ok { - appID := fmt.Sprint(value) - h := sha256.Sum256([]byte(appID)) - appIDHash = h[:] - } - - // Register! - var rawResp []byte - if err := RunOnU2FDevices(ctx, func(t Token) error { - // Is the authenticator in the credential exclude list? - for _, cred := range cc.Response.CredentialExcludeList { - for _, app := range [][]byte{rpIDHash[:], appIDHash} { - if len(app) == 0 { - continue - } - - // Check if the device is already registered by calling - // CheckAuthenticate. - // If the method succeeds then the device knows about the - // {key handle, app} pair, which means it is already registered. - // CheckAuthenticate doesn't require user interaction. - if err := t.CheckAuthenticate(u2ftoken.AuthenticateRequest{ - Challenge: ccdHash[:], - Application: app, - KeyHandle: cred.CredentialID, - }); err == nil { - log.Warnf( - "WebAuthn: Authenticator already registered under credential ID %q", - base64.RawURLEncoding.EncodeToString(cred.CredentialID)) - return ErrAlreadyRegistered // Remove authenticator from list - } - } - } - - var err error - rawResp, err = t.Register(u2ftoken.RegisterRequest{ - Challenge: ccdHash[:], - Application: rpIDHash[:], - }) - return err - }); err != nil { - return nil, trace.Wrap(err) - } - - // Parse U2F response and convert to Webauthn - after that we are done. - resp, err := parseU2FRegistrationResponse(rawResp) - if err != nil { - return nil, trace.Wrap(err) - } - ccr, err := credentialResponseFromU2F(ccdJSON, rpIDHash[:], resp) - if err != nil { - return nil, trace.Wrap(err) - } - - return &proto.MFARegisterResponse{ - Response: &proto.MFARegisterResponse_Webauthn{ - Webauthn: wantypes.CredentialCreationResponseToProto(ccr), - }, - }, nil -} - -type u2fRegistrationResponse struct { - PubKey *ecdsa.PublicKey - KeyHandle, AttestationCert, Signature []byte -} - -func parseU2FRegistrationResponse(resp []byte) (*u2fRegistrationResponse, error) { - // Reference: - // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-response-message-success - - // minRespLen is based on: - // 1 byte reserved + - // 65 pubKey + - // 1 key handle length + - // N key handle (at least 1) + - // N attestation cert (at least 1, need to parse to find out) + - // N signature (at least 1, spec says 71-73 bytes, YMMV) - const pubKeyLen = 65 - const minRespLen = 1 + pubKeyLen + 4 - if len(resp) < minRespLen { - return nil, trace.BadParameter("U2F response too small, got %v bytes, expected at least %v", len(resp), minRespLen) - } - - // Reads until the key handle length are guaranteed by the size checking - // above. - buf := resp - if buf[0] != 0x05 { - return nil, trace.BadParameter("invalid reserved byte: %v", buf[0]) - } - buf = buf[1:] - - // public key, "4||X||Y" form. - pubKeyBytes := buf[:pubKeyLen] - // Validate pubKey points. - if _, err := ecdh.P256().NewPublicKey(pubKeyBytes); err != nil { - return nil, trace.Wrap(err, "unmarshal public key") - } - // There's no API to pry away X and Y from ecdh.PublicKey, so we do it - // manually, but only after the key is validated. - const uncompressedForm = 4 - if pubKeyBytes[0] != uncompressedForm { - return nil, trace.BadParameter("public key not in uncompressed form") - } - pubKeyBytes = pubKeyBytes[1:] // holds X||Y - l := len(pubKeyBytes) / 2 // holds the size of a coordinate (X or Y) - pubKey := &ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: new(big.Int).SetBytes(pubKeyBytes[:l]), - Y: new(big.Int).SetBytes(pubKeyBytes[l:]), - } - buf = buf[pubKeyLen:] - - // key handle - l = int(buf[0]) // holds the keyHandle length. - buf = buf[1:] - // Size checking resumed from now on. - if len(buf) < l { - return nil, trace.BadParameter("key handle length is %v, but only %v bytes are left", l, len(buf)) - } - keyHandle := buf[:l] - buf = buf[l:] - - // Parse the certificate to figure out its size, then call - // x509.ParseCertificate with a correctly-sized byte slice. - sig, err := asn1.Unmarshal(buf, &asn1.RawValue{}) - if err != nil { - return nil, trace.Wrap(err) - } - - // Parse the cert to check that it is valid - we don't actually need the - // parsed cert after it is proved to be well-formed. - attestationCert := buf[:len(buf)-len(sig)] - if _, err := x509.ParseCertificate(attestationCert); err != nil { - return nil, trace.Wrap(err) - } - - return &u2fRegistrationResponse{ - PubKey: pubKey, - KeyHandle: keyHandle, - AttestationCert: attestationCert, - Signature: sig, - }, nil -} - -func credentialResponseFromU2F(ccdJSON, appIDHash []byte, resp *u2fRegistrationResponse) (*wantypes.CredentialCreationResponse, error) { - // Reference: - // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#fig-u2f-compat-makeCredential - - pubKeyCBOR, err := wanlib.U2FKeyToCBOR(resp.PubKey) - if err != nil { - return nil, trace.Wrap(err) - } - - // Assemble authenticator data. - authData := &bytes.Buffer{} - authData.Write(appIDHash[:]) - // Attested credential data present. - // https://www.w3.org/TR/webauthn-2/#attested-credential-data. - authData.WriteByte(byte(protocol.FlagAttestedCredentialData | protocol.FlagUserPresent)) - binary.Write(authData, binary.BigEndian, uint32(0)) // counter, zeroed - authData.Write(make([]byte, 16)) // AAGUID, zeroed - binary.Write(authData, binary.BigEndian, uint16(len(resp.KeyHandle))) // L - authData.Write(resp.KeyHandle) - authData.Write(pubKeyCBOR) - - // Assemble attestation object - attestationObj, err := cbor.Marshal(&protocol.AttestationObject{ - RawAuthData: authData.Bytes(), - // See https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation. - Format: "fido-u2f", - AttStatement: map[string]interface{}{ - "sig": resp.Signature, - "x5c": []interface{}{resp.AttestationCert}, - }, - }) - if err != nil { - return nil, trace.Wrap(err) - } - - return &wantypes.CredentialCreationResponse{ - PublicKeyCredential: wantypes.PublicKeyCredential{ - Credential: wantypes.Credential{ - ID: base64.RawURLEncoding.EncodeToString(resp.KeyHandle), - Type: string(protocol.PublicKeyCredentialType), - }, - RawID: resp.KeyHandle, - }, - AttestationResponse: wantypes.AuthenticatorAttestationResponse{ - AuthenticatorResponse: wantypes.AuthenticatorResponse{ - ClientDataJSON: ccdJSON, - }, - AttestationObject: attestationObj, - }, - }, nil -} diff --git a/lib/auth/webauthncli/u2f_register_test.go b/lib/auth/webauthncli/u2f_register_test.go deleted file mode 100644 index cea5d7531952e..0000000000000 --- a/lib/auth/webauthncli/u2f_register_test.go +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli_test - -import ( - "context" - "testing" - "time" - - "github.com/go-webauthn/webauthn/protocol" - "github.com/go-webauthn/webauthn/protocol/webauthncose" - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport/api/types" - wanlib "github.com/gravitational/teleport/lib/auth/webauthn" - wancli "github.com/gravitational/teleport/lib/auth/webauthncli" - wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" -) - -func TestRegister(t *testing.T) { - resetU2FCallbacksAfterTest(t) - - const user = "llama" - const rpID = "example.com" - const origin = "https://example.com" - - u2fKey, err := newFakeDevice("u2fkey" /* name */, "" /* appID */) - require.NoError(t, err) - registeredKey, err := newFakeDevice("regkey" /* name */, rpID /* appID */) - require.NoError(t, err) - - // Create a registration flow, we'll use it to both generate credential - // requests and to validate them. - webRegistration := &wanlib.RegistrationFlow{ - Webauthn: &types.Webauthn{ - RPID: rpID, - }, - Identity: &fakeIdentity{ - User: user, - Devices: []*types.MFADevice{ - // Fake a WebAuthn device record, as U2F devices are not excluded from registration. - { - Device: &types.MFADevice_Webauthn{ - Webauthn: &types.WebauthnDevice{ - CredentialId: registeredKey.key.KeyHandle, - }, - }, - }, - }, - }, - } - - ctx := context.Background() - tests := []struct { - name string - devs []*fakeDevice - setUserPresence *fakeDevice - wantErr bool - wantRawID []byte - }{ - { - name: "U2F-compatible registration", - devs: []*fakeDevice{u2fKey}, - setUserPresence: u2fKey, - wantRawID: u2fKey.key.KeyHandle, - }, - { - name: "Registered key ignored", - devs: []*fakeDevice{u2fKey, registeredKey}, - setUserPresence: registeredKey, - wantErr: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // 100ms is an eternity when probing devices at 1ns intervals. - ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) - defer cancel() - - cc, err := webRegistration.Begin(ctx, user, false /* passwordless */) - require.NoError(t, err) - - // Reset/set presence indicator. - for _, dev := range test.devs { - dev.SetUserPresence(false) - } - test.setUserPresence.SetUserPresence(true) - - // Replace U2F library functions with our mocked versions. - fakeDevs := &fakeDevices{devs: test.devs} - fakeDevs.assignU2FCallbacks() - - resp, err := wancli.U2FRegister(ctx, origin, cc) - switch hasErr := err != nil; { - case hasErr != test.wantErr: - t.Fatalf("Register returned err = %v, wantErr = %v", err, test.wantErr) - case hasErr: // OK. - return - } - require.Equal(t, test.wantRawID, resp.GetWebauthn().RawId) - - _, err = webRegistration.Finish(ctx, wanlib.RegisterResponse{ - User: user, - DeviceName: u2fKey.name, - CreationResponse: wantypes.CredentialCreationResponseFromProto(resp.GetWebauthn()), - }) - require.NoError(t, err, "server-side registration failed") - }) - } -} - -func TestRegister_errors(t *testing.T) { - ctx := context.Background() - - const origin = "https://example.com" - webRegistration := &wanlib.RegistrationFlow{ - Webauthn: &types.Webauthn{ - RPID: "example.com", - }, - Identity: &fakeIdentity{}, - } - okCC, err := webRegistration.Begin(ctx, "llama" /* user */, false /* passwordless */) - require.NoError(t, err) - - tests := []struct { - name string - origin string - makeCC func() *wantypes.CredentialCreation - wantErr string - }{ - { - name: "NOK empty origin", - origin: "", - makeCC: func() *wantypes.CredentialCreation { return okCC }, - wantErr: "origin", - }, - { - name: "NOK nil credential creation", - origin: origin, - makeCC: func() *wantypes.CredentialCreation { return nil }, - wantErr: "credential creation required", - }, - { - name: "NOK nil empty creation", - origin: origin, - makeCC: func() *wantypes.CredentialCreation { return &wantypes.CredentialCreation{} }, - wantErr: "relying party", - }, - { - name: "NOK ES256 algorithm not allowed", - origin: origin, - makeCC: func() *wantypes.CredentialCreation { - cp := *okCC - var params []wantypes.CredentialParameter - for _, p := range cp.Response.Parameters { - if p.Algorithm == webauthncose.AlgES256 { - continue - } - params = append(params, p) - } - cp.Response.Parameters = params - return &cp - }, - wantErr: "ES256 not allowed", - }, - { - name: "NOK platform attachment required", - origin: origin, - makeCC: func() *wantypes.CredentialCreation { - cp := *okCC - cp.Response.AuthenticatorSelection.AuthenticatorAttachment = protocol.Platform - return &cp - }, - wantErr: "platform", - }, - { - name: "NOK resident key required", - origin: origin, - makeCC: func() *wantypes.CredentialCreation { - cp := *okCC - rrk := true - cp.Response.AuthenticatorSelection.RequireResidentKey = &rrk - return &cp - }, - wantErr: "resident key", - }, - { - name: "NOK user verification required", - origin: origin, - makeCC: func() *wantypes.CredentialCreation { - cp := *okCC - cp.Response.AuthenticatorSelection.UserVerification = protocol.VerificationRequired - return &cp - }, - wantErr: "user verification", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - _, err := wancli.U2FRegister(ctx, test.origin, test.makeCC()) - require.Error(t, err) - require.Contains(t, err.Error(), test.wantErr) - }) - } -} diff --git a/lib/auth/webauthncli/u2f_windows.go b/lib/auth/webauthncli/u2f_windows.go deleted file mode 100644 index 1d2a0b7e338be..0000000000000 --- a/lib/auth/webauthncli/u2f_windows.go +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package webauthncli - -func isU2FAvailable() bool { - return false -}