diff --git a/lib/auth/webauthnwin/api.go b/lib/auth/webauthnwin/api.go index 5d3cdcefe22e5..3ff51e2d97890 100644 --- a/lib/auth/webauthnwin/api.go +++ b/lib/auth/webauthnwin/api.go @@ -35,6 +35,7 @@ import ( "github.com/go-webauthn/webauthn/protocol/webauthncose" "github.com/gravitational/trace" + "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client/proto" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" ) @@ -305,3 +306,8 @@ func Diag(ctx context.Context) (*DiagResult, error) { return res, nil } + +// getPackageLogger returns a logger with component="WebAuthnWin". +func getPackageLogger() *slog.Logger { + return slog.With(teleport.ComponentKey, "WebAuthnWin") +} diff --git a/lib/auth/webauthnwin/api_test.go b/lib/auth/webauthnwin/api_test.go index 7b37a45753d26..ef5c85f0e6897 100644 --- a/lib/auth/webauthnwin/api_test.go +++ b/lib/auth/webauthnwin/api_test.go @@ -178,6 +178,8 @@ func TestLogin(t *testing.T) { }, } + const wantOptsVersion uint32 = 9 + tests := []struct { name string origin string @@ -191,10 +193,8 @@ func TestLogin(t *testing.T) { origin: origin, assertionIn: func() *wantypes.CredentialAssertion { return okAssertion }, assertFn: func(t *testing.T, car *wanpb.CredentialAssertionResponse, req *getAssertionRequest) { - assert.Equal(t, uint32(5), req.opts.dwVersion) - + assert.Equal(t, wantOptsVersion, req.opts.dwVersion) assert.Equal(t, webauthnUserVerificationDiscouraged, req.opts.dwUserVerificationRequirement) - assert.Equal(t, webauthnAttachmentAny, req.opts.dwAuthenticatorAttachment) }, }, @@ -208,10 +208,8 @@ func TestLogin(t *testing.T) { }, opts: LoginOpts{AuthenticatorAttachment: AttachmentPlatform}, assertFn: func(t *testing.T, car *wanpb.CredentialAssertionResponse, req *getAssertionRequest) { - assert.Equal(t, uint32(5), req.opts.dwVersion) - + assert.Equal(t, wantOptsVersion, req.opts.dwVersion) assert.Equal(t, webauthnUserVerificationRequired, req.opts.dwUserVerificationRequirement) - assert.Equal(t, webauthnAttachmentPlatform, req.opts.dwAuthenticatorAttachment) }, }, @@ -225,10 +223,8 @@ func TestLogin(t *testing.T) { }, opts: LoginOpts{AuthenticatorAttachment: AttachmentCrossPlatform}, assertFn: func(t *testing.T, car *wanpb.CredentialAssertionResponse, req *getAssertionRequest) { - assert.Equal(t, uint32(5), req.opts.dwVersion) - + assert.Equal(t, wantOptsVersion, req.opts.dwVersion) assert.Equal(t, webauthnUserVerificationPreferred, req.opts.dwUserVerificationRequirement) - assert.Equal(t, webauthnAttachmentCrossPlatform, req.opts.dwAuthenticatorAttachment) }, }, @@ -242,8 +238,7 @@ func TestLogin(t *testing.T) { }, opts: LoginOpts{AuthenticatorAttachment: AttachmentCrossPlatform}, assertFn: func(t *testing.T, car *wanpb.CredentialAssertionResponse, req *getAssertionRequest) { - assert.Equal(t, uint32(5), req.opts.dwVersion) - + assert.Equal(t, wantOptsVersion, req.opts.dwVersion) assert.Equal(t, webauthnUserVerificationDiscouraged, req.opts.dwUserVerificationRequirement) }, }, diff --git a/lib/auth/webauthnwin/conv.go b/lib/auth/webauthnwin/conv.go index 875128c86b78f..95867a9928dd8 100644 --- a/lib/auth/webauthnwin/conv.go +++ b/lib/auth/webauthnwin/conv.go @@ -19,6 +19,7 @@ package webauthnwin import ( + "context" "encoding/json" "strings" "syscall" @@ -37,25 +38,52 @@ func assertOptionsToCType(in wantypes.PublicKeyCredentialRequestOptions, loginOp } var dwAuthenticatorAttachment uint32 + var credentialHint string if loginOpts != nil { switch loginOpts.AuthenticatorAttachment { case AttachmentPlatform: - dwAuthenticatorAttachment = 1 + dwAuthenticatorAttachment = webauthnAttachmentPlatform + getPackageLogger().DebugContext(context.Background(), + "Using platform attachment", + "attachment", dwAuthenticatorAttachment, + ) case AttachmentCrossPlatform: - dwAuthenticatorAttachment = 2 + dwAuthenticatorAttachment = webauthnAttachmentCrossPlatform + // If we are setting cross-platform we imply a physical security-key (not + // a phone). + // See https://github.com/gravitational/teleport/issues/62060. + credentialHint = webauthnCredentialHintSecurityKey + getPackageLogger().DebugContext(context.Background(), + "Using cross-platform attachment with security-key hint", + "attachment", dwAuthenticatorAttachment, + "credential_hint", credentialHint, + ) } } + var cCredentialHints uint32 // number of hints + var pCredentialHints *uint16 // pointer to array of NULL-terminated UTF-16 strings + if credentialHint != "" { + var err error + pCredentialHints, err = utf16PtrFromString(credentialHint) + if err != nil { + return nil, trace.Wrap(err) + } + cCredentialHints = 1 + } + return &webauthnAuthenticatorGetAssertionOptions{ // https://github.com/microsoft/webauthn/blob/7ab979cc833bfab9a682ed51761309db57f56c8c/webauthn.h#L36-L97 // contains information about different versions. // We can set newest version and it still works on older APIs. - dwVersion: 5, + dwVersion: 9, dwTimeoutMilliseconds: uint32(in.Timeout), dwAuthenticatorAttachment: dwAuthenticatorAttachment, dwUserVerificationRequirement: userVerificationToCType(in.UserVerification), // TODO(tobiaszheller): support U2fAppId. pAllowCredentialList: allowCredList, + cCredentialHints: cCredentialHints, + ppwszCredentialHints: pCredentialHints, }, nil } diff --git a/lib/auth/webauthnwin/ctypes.go b/lib/auth/webauthnwin/ctypes.go index a987293e6bea6..2189f3bf5dd40 100644 --- a/lib/auth/webauthnwin/ctypes.go +++ b/lib/auth/webauthnwin/ctypes.go @@ -29,6 +29,10 @@ const ( webauthnUserVerificationRequired = uint32(1) webauthnUserVerificationPreferred = uint32(2) webauthnUserVerificationDiscouraged = uint32(3) + + webauthnCredentialHintSecurityKey = "security-key" + webauthnCredentialHintClientDevice = "client-device" + webauthnCredentialHintHybrid = "hybrid" ) type webauthnRPEntityInformation struct { @@ -240,6 +244,7 @@ type webauthnClientData struct { pwszHashAlgID *uint16 } +//nolint:unused // This type maps a C struct. We need to keep the layout consistent. type webauthnAuthenticatorGetAssertionOptions struct { dwVersion uint32 // Time that the operation is expected to complete within. @@ -289,13 +294,60 @@ type webauthnAuthenticatorGetAssertionOptions struct { // // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_5 // - // This field is kept just to keep size of struct valid. - _ uint32 - // Size of pbCredLargeBlob - // This field is kept just to keep size of struct valid. - _ uint32 - // This field is kept just to keep size of struct valid. - _ *byte + + _ uint32 // dwCredLargeBlobOperation + cbCredLargeBlob uint32 + pbCredLargeBlob *byte + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_6 + // + + // PRF values which will be converted into HMAC-SECRET values according to WebAuthn Spec. + _ uint32 // pHmacSecretSaltValues + + // Optional. BrowserInPrivate Mode. Defaulting to FALSE. + bBrowserInPrivateMode bool + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7 + // + + // Deprecated + // Optional. Linked Device Connection Info. + _ uint32 // pLinkedDevice + + // Optional. Allowlist MUST contain 1 credential applicable for Hybrid transport. + bAutoFill bool + + // Size of pbJsonExt + cbJsonExt uint32 + pbJsonExt *byte + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_8 + // + + // PublicKeyCredentialHints (https://w3c.github.io/webauthn/#enum-hints) + cCredentialHints uint32 + ppwszCredentialHints *uint16 + + // + // The following fields have been added in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_9 + // + // https://github.com/microsoft/webauthn/blob/c3ed95fd7603441a0253c55c14e79239cb556a9f/webauthn.h#L958. + // + + // Web Origin. For Remote Web App scenario. + pwszRemoteWebOrigin *uint16 + + // UTF-8 encoded JSON serialization of the PublicKeyCredentialRequestOptions. + cbPublicKeyCredentialRequestOptionsJSON uint32 + pbPublicKeyCredentialRequestOptionsJSON *byte + + // Authenticator ID got from WebAuthNGetAuthenticatorList API. + cbAuthenticatorID uint32 + pbAuthenticatorID *byte } //nolint:unused // TODO: remove when linter runs on windows build tag diff --git a/lib/auth/webauthnwin/webauthn_windows.go b/lib/auth/webauthnwin/webauthn_windows.go index 0f07071ba498e..8eb80fdc3c53a 100644 --- a/lib/auth/webauthnwin/webauthn_windows.go +++ b/lib/auth/webauthnwin/webauthn_windows.go @@ -54,6 +54,8 @@ func newNativeImpl() *nativeImpl { hasCompileSupport: true, } + // Do not hold onto this logger. It is created before tsh has a chance to + // initialize it properly. logger := slog.With(teleport.ComponentKey, "WebAuthnWin") ctx := context.Background() @@ -115,6 +117,21 @@ func (n *nativeImpl) GetAssertion(origin string, in *getAssertionRequest) (*want return nil, trace.Wrap(err) } + logger := getPackageLogger() + logger.DebugContext(context.Background(), "WebAuthn.dll API version", + "version", n.webauthnAPIVersion, + ) + + if n.webauthnAPIVersion < int(in.opts.dwVersion) { + const legacyVersion = 5 + in.opts.dwVersion = legacyVersion + logger.DebugContext(context.Background(), + "WebAuthn.dll too old, falling back to legacy version", + "api_version", n.webauthnAPIVersion, + "legacy_version", in.opts.dwVersion, + ) + } + var out *webauthnAssertion ret, err := webAuthNAuthenticatorGetAssertion(hwnd, in.rpID, in.clientData, in.opts, &out) if ret != 0 {