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
-}