From f31b9cb179cfe8c5ed9b1d39c9e7ab7d410fdbe5 Mon Sep 17 00:00:00 2001 From: Alan Parra Date: Mon, 18 Jul 2022 18:25:36 -0300 Subject: [PATCH] Allow explicit Touch ID prompts (#14492) Allow explicit Touch ID prompts to be triggered via Go code, which will be used to guard the (upcoming) credential picker prompt. To avoid double-prompting users during Touch ID authentication we have to set a grace period in the underlying LAContext and share it between the functions. Note that AuthContextGuard (native) uses the LAContext explicitly, whereas Authenticate (native) uses it through the SecItemCopyMatching query dictionary. No UX visible changes are made in the PR, despite the fact that prompting occurs a bit earlier. #13901 * Allow explicit Touch ID prompts --- lib/auth/touchid/api.go | 36 ++++-- lib/auth/touchid/api_darwin.go | 102 +++++++++++++--- lib/auth/touchid/api_other.go | 14 ++- lib/auth/touchid/api_test.go | 204 +++++++++++++++++++++++++++++++- lib/auth/touchid/authenticate.h | 4 +- lib/auth/touchid/authenticate.m | 7 +- lib/auth/touchid/context.h | 43 +++++++ lib/auth/touchid/context.m | 85 +++++++++++++ 8 files changed, 468 insertions(+), 27 deletions(-) create mode 100644 lib/auth/touchid/context.h create mode 100644 lib/auth/touchid/context.m diff --git a/lib/auth/touchid/api.go b/lib/auth/touchid/api.go index 9892b6f0dab5a..f70a2f08053ee 100644 --- a/lib/auth/touchid/api.go +++ b/lib/auth/touchid/api.go @@ -57,17 +57,31 @@ func promptPlatform() { } } +// AuthContext is an optional, shared authentication context. +// Allows reusing a single authentication prompt/gesture between different +// functions, provided the functions are invoked in a short time interval. +// Only used by native touchid implementations. +type AuthContext interface { + // Guard guards the invocation of fn behind an authentication check. + Guard(fn func()) error + // Close closes the context, releasing any held resources. + Close() +} + // nativeTID represents the native Touch ID interface. // Implementors must provide a global variable called `native`. type nativeTID interface { Diag() (*DiagResult, error) + // NewAuthContext creates a new AuthContext. + NewAuthContext() AuthContext + // Register creates a new credential in the Secure Enclave. Register(rpID, user string, userHandle []byte) (*CredentialInfo, error) // Authenticate authenticates using the specified credential. // Requires user interaction. - Authenticate(credentialID string, digest []byte) ([]byte, error) + Authenticate(actx AuthContext, credentialID string, digest []byte) ([]byte, error) // FindCredentials finds credentials without user interaction. // An empty user means "all users". @@ -281,7 +295,7 @@ func Register(origin string, cc *wanlib.CredentialCreation) (*Registration, erro } promptPlatform() - sig, err := native.Authenticate(credentialID, attData.digest) + sig, err := native.Authenticate(nil /* actx */, credentialID, attData.digest) if err != nil { return nil, trace.Wrap(err) } @@ -433,8 +447,6 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib. return nil, "", errors.New("relying party ID required") } - // TODO(codingllama): Share the same LAContext between search and - // authentication, so we can protect both with user interaction. rpID := assertion.Response.RelyingPartyID infos, err := native.FindCredentials(rpID, user) switch { @@ -457,15 +469,25 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib. if !ok { return nil, "", ErrCredentialNotFound } - log.Debugf("Touch ID: using credential %q", cred.CredentialID) + + // Guard first read of chosen credential with an explicit check. + // A more meaningful check can be made once the credential picker is + // implemented. + actx := native.NewAuthContext() + defer actx.Close() + promptPlatform() + if err := actx.Guard(func() { + log.Debugf("Touch ID: using credential %q", cred.CredentialID) + }); err != nil { + return nil, "", trace.Wrap(err) + } attData, err := makeAttestationData(protocol.AssertCeremony, origin, rpID, assertion.Response.Challenge, nil /* cred */) if err != nil { return nil, "", trace.Wrap(err) } - promptPlatform() - sig, err := native.Authenticate(cred.CredentialID, attData.digest) + sig, err := native.Authenticate(actx, cred.CredentialID, attData.digest) if err != nil { return nil, "", trace.Wrap(err) } diff --git a/lib/auth/touchid/api_darwin.go b/lib/auth/touchid/api_darwin.go index af17f2bac1561..3a36ad3b8d768 100644 --- a/lib/auth/touchid/api_darwin.go +++ b/lib/auth/touchid/api_darwin.go @@ -21,6 +21,7 @@ package touchid // #cgo LDFLAGS: -framework CoreFoundation -framework Foundation -framework LocalAuthentication -framework Security // #include // #include "authenticate.h" +// #include "context.h" // #include "credential_info.h" // #include "credentials.h" // #include "diag.h" @@ -29,8 +30,8 @@ import "C" import ( "encoding/base64" - "errors" "fmt" + "runtime/cgo" "strings" "time" "unsafe" @@ -50,6 +51,10 @@ const ( // rpID are domain names, so it's safe to assume they won't have spaces in them. // https://www.w3.org/TR/webauthn-2/#relying-party-identifier labelSeparator = " " + + // promptReason is the LAContext / Touch ID prompt. + // The final prompt is: "$binary is trying to authenticate user". + promptReason = "authenticate user" ) type parsedLabel struct { @@ -100,6 +105,66 @@ func (touchIDImpl) Diag() (*DiagResult, error) { }, nil } +//export runGoFuncHandle +func runGoFuncHandle(handle C.uintptr_t) { + val := cgo.Handle(handle).Value() + fn, ok := val.(func()) + if !ok { + log.Warnf("Touch ID: received unexpected function handle: %T", val) + return + } + fn() +} + +// touchIDContext wraps C.AuthContext into an authContext shell. +type touchIDContext struct { + ctx *C.AuthContext +} + +func (c *touchIDContext) Guard(fn func()) error { + reasonC := C.CString(promptReason) + defer C.free(unsafe.Pointer(reasonC)) + + // Passing Go function pointers directly to CGO is not doable, so we pass a + // handle and have an exported Go function run it. + // See https://github.com/golang/go/wiki/cgo#function-variables. + handle := cgo.NewHandle(fn) + defer handle.Delete() + + var errMsgC *C.char + defer C.free(unsafe.Pointer(errMsgC)) + + res := C.AuthContextGuard(c.ctx, reasonC, C.uintptr_t(handle), &errMsgC) + if res != 0 { + errMsg := C.GoString(errMsgC) + return errorFromStatus("guard", int(res), errMsg) + } + + return nil +} + +func (c *touchIDContext) Close() { + if c.ctx == nil { + return + } + C.AuthContextClose(c.ctx) + c.ctx = nil +} + +// getNativeContext returns the C.AuthContext within ctx, or nil. +func getNativeContext(ctx AuthContext) *C.AuthContext { + if tctx, ok := ctx.(*touchIDContext); ok { + return tctx.ctx + } + return nil +} + +func (touchIDImpl) NewAuthContext() AuthContext { + return &touchIDContext{ + ctx: &C.AuthContext{}, + } +} + func (touchIDImpl) Register(rpID, user string, userHandle []byte) (*CredentialInfo, error) { credentialID := uuid.NewString() userHandleB64 := base64.RawURLEncoding.EncodeToString(userHandle) @@ -122,7 +187,7 @@ func (touchIDImpl) Register(rpID, user string, userHandle []byte) (*CredentialIn if res := C.Register(req, &pubKeyC, &errMsgC); res != 0 { errMsg := C.GoString(errMsgC) - return nil, errors.New(errMsg) + return nil, errorFromStatus("register", int(res), errMsg) } pubKeyB64 := C.GoString(pubKeyC) @@ -137,7 +202,9 @@ func (touchIDImpl) Register(rpID, user string, userHandle []byte) (*CredentialIn }, nil } -func (touchIDImpl) Authenticate(credentialID string, digest []byte) ([]byte, error) { +func (touchIDImpl) Authenticate(actx AuthContext, credentialID string, digest []byte) ([]byte, error) { + authCtx := getNativeContext(actx) + var req C.AuthenticateRequest req.app_label = C.CString(credentialID) req.digest = (*C.char)(C.CBytes(digest)) @@ -153,9 +220,9 @@ func (touchIDImpl) Authenticate(credentialID string, digest []byte) ([]byte, err C.free(unsafe.Pointer(errMsgC)) }() - if res := C.Authenticate(req, &sigOutC, &errMsgC); res != 0 { + if res := C.Authenticate(authCtx, req, &sigOutC, &errMsgC); res != 0 { errMsg := C.GoString(errMsgC) - return nil, errors.New(errMsg) + return nil, errorFromStatus("authenticate", int(res), errMsg) } sigB64 := C.GoString(sigOutC) @@ -174,14 +241,13 @@ func (touchIDImpl) FindCredentials(rpID, user string) ([]CredentialInfo, error) return C.FindCredentials(filterC, infosC) }) if res < 0 { - return nil, trace.BadParameter("failed to find credentials: status %d", res) + return nil, errorFromStatus("finding credentials", res, "" /* msg */) } return infos, nil } func (touchIDImpl) ListCredentials() ([]CredentialInfo, error) { - // User prompt becomes: ""$binary" is trying to list credentials". - reasonC := C.CString("list credentials") + reasonC := C.CString(promptReason) defer C.free(unsafe.Pointer(reasonC)) var errMsgC *C.char @@ -197,7 +263,7 @@ func (touchIDImpl) ListCredentials() ([]CredentialInfo, error) { }) if res < 0 { errMsg := C.GoString(errMsgC) - return nil, errors.New(errMsg) + return nil, errorFromStatus("listing credentials", int(res), errMsg) } return infos, nil @@ -283,8 +349,7 @@ func readCredentialInfos(find func(**C.CredentialInfo) C.int) ([]CredentialInfo, const errSecItemNotFound = -25300 func (touchIDImpl) DeleteCredential(credentialID string) error { - // User prompt becomes: ""$binary" is trying to delete credential". - reasonC := C.CString("delete credential") + reasonC := C.CString(promptReason) defer C.free(unsafe.Pointer(reasonC)) idC := C.CString(credentialID) @@ -293,14 +358,14 @@ func (touchIDImpl) DeleteCredential(credentialID string) error { var errC *C.char defer C.free(unsafe.Pointer(errC)) - switch C.DeleteCredential(reasonC, idC, &errC) { + switch res := C.DeleteCredential(reasonC, idC, &errC); res { case 0: // aka success return nil case errSecItemNotFound: return ErrCredentialNotFound default: errMsg := C.GoString(errC) - return errors.New(errMsg) + return errorFromStatus("delete credential", int(res), errMsg) } } @@ -308,12 +373,19 @@ func (touchIDImpl) DeleteNonInteractive(credentialID string) error { idC := C.CString(credentialID) defer C.free(unsafe.Pointer(idC)) - switch status := C.DeleteNonInteractive(idC); status { + switch res := C.DeleteNonInteractive(idC); res { case 0: // aka success return nil case errSecItemNotFound: return ErrCredentialNotFound default: - return fmt.Errorf("non-interactive delete failed: status %d", status) + return errorFromStatus("non-interactive delete", int(res), "" /* msg */) + } +} + +func errorFromStatus(prefix string, status int, msg string) error { + if msg != "" { + return fmt.Errorf("%v: %v", prefix, msg) } + return fmt.Errorf("%v: status %d", prefix, status) } diff --git a/lib/auth/touchid/api_other.go b/lib/auth/touchid/api_other.go index ed00e5742cdf0..8ed0b5a6d9485 100644 --- a/lib/auth/touchid/api_other.go +++ b/lib/auth/touchid/api_other.go @@ -25,11 +25,23 @@ func (noopNative) Diag() (*DiagResult, error) { return &DiagResult{}, nil } +type noopAuthContext struct{} + +func (noopAuthContext) Guard(fn func()) error { + return ErrNotAvailable +} + +func (noopAuthContext) Close() {} + +func (noopNative) NewAuthContext() AuthContext { + return noopAuthContext{} +} + func (noopNative) Register(rpID, user string, userHandle []byte) (*CredentialInfo, error) { return nil, ErrNotAvailable } -func (noopNative) Authenticate(credentialID string, digest []byte) ([]byte, error) { +func (noopNative) Authenticate(actx AuthContext, credentialID string, digest []byte) ([]byte, error) { return nil, ErrNotAvailable } diff --git a/lib/auth/touchid/api_test.go b/lib/auth/touchid/api_test.go index 8f54ef7aa9f96..fb4eebae3555c 100644 --- a/lib/auth/touchid/api_test.go +++ b/lib/auth/touchid/api_test.go @@ -22,10 +22,12 @@ import ( "crypto/rand" "encoding/json" "errors" + "io" "testing" "time" "github.com/duo-labs/webauthn/protocol" + "github.com/duo-labs/webauthn/protocol/webauthncose" "github.com/duo-labs/webauthn/webauthn" "github.com/google/uuid" "github.com/gravitational/teleport/lib/auth/touchid" @@ -35,6 +37,11 @@ import ( wanlib "github.com/gravitational/teleport/lib/auth/webauthn" ) +func init() { + // Make tests silent. + touchid.PromptWriter = io.Discard +} + func TestRegisterAndLogin(t *testing.T) { n := *touchid.Native t.Cleanup(func() { @@ -69,7 +76,8 @@ func TestRegisterAndLogin(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - *touchid.Native = &fakeNative{} + fake := &fakeNative{} + *touchid.Native = fake webUser := test.webUser origin := test.origin @@ -81,6 +89,7 @@ func TestRegisterAndLogin(t *testing.T) { reg, err := touchid.Register(origin, (*wanlib.CredentialCreation)(cc)) require.NoError(t, err, "Register failed") + assert.Equal(t, 1, fake.userPrompts, "unexpected number of Registation prompts") // We have to marshal and parse ccr due to an unavoidable quirk of the // webauthn API. @@ -106,6 +115,7 @@ func TestRegisterAndLogin(t *testing.T) { assertionResp, actualUser, err := touchid.Login(origin, user, assertion) require.NoError(t, err, "Login failed") assert.Equal(t, test.wantUser, actualUser, "actualUser mismatch") + assert.Equal(t, 2, fake.userPrompts, "unexpected number of Login prompts") // Same as above: easiest way to validate the assertion is to marshal // and then parse the body. @@ -334,6 +344,158 @@ func TestLogin_findsCorrectCredential(t *testing.T) { } } +func TestLogin_noCredentials_failsWithoutUserInteraction(t *testing.T) { + n := *touchid.Native + t.Cleanup(func() { + *touchid.Native = n + }) + + fake := &fakeNative{} + *touchid.Native = fake + + const origin = "https://goteleport.com" + baseAssertion := &wanlib.CredentialAssertion{ + Response: protocol.PublicKeyCredentialRequestOptions{ + Challenge: []byte{1, 2, 3, 4, 5}, // arbitrary + RelyingPartyID: "goteleport.com", + UserVerification: protocol.VerificationRequired, + }, + } + mfaAssertion := *baseAssertion + mfaAssertion.Response.UserVerification = protocol.VerificationDiscouraged + mfaAssertion.Response.AllowedCredentials = []protocol.CredentialDescriptor{ + { + Type: protocol.PublicKeyCredentialType, + CredentialID: []byte{1, 2, 3, 4, 5}, // arbitrary + }, + } + + // Run empty credentials tests first. + for _, test := range []struct { + name string + user string + assertion *wanlib.CredentialAssertion + }{ + { + name: "passwordless empty user", + assertion: baseAssertion, + }, + { + name: "passwordless explicit user", + user: "llama", + assertion: baseAssertion, + }, + { + name: "MFA empty user", + user: "", // Typically MFA comes with an empty user + assertion: &mfaAssertion, + }, + { + name: "MFA explicit user", + user: "llama", + assertion: &mfaAssertion, + }, + } { + t.Run(test.name, func(t *testing.T) { + fake.userPrompts = 0 // reset before test + _, _, err := touchid.Login(origin, test.user, test.assertion) + assert.ErrorIs(t, err, touchid.ErrCredentialNotFound, "Login error mismatch") + assert.Zero(t, fake.userPrompts, "Login caused user interaction with no credentials") + }) + } + + // Register a couple of credentials for the following tests. + const userLlama = "llama" + const userAlpaca = "alpaca" + rrk := true + cc1 := &wanlib.CredentialCreation{ + Response: protocol.PublicKeyCredentialCreationOptions{ + Challenge: []byte{1, 2, 3, 4, 5}, // arbitrary, not important here + RelyingParty: protocol.RelyingPartyEntity{ + CredentialEntity: protocol.CredentialEntity{ + Name: "Teleport", + }, + ID: baseAssertion.Response.RelyingPartyID, + }, + User: protocol.UserEntity{ + CredentialEntity: protocol.CredentialEntity{ + Name: userLlama, + }, + DisplayName: "Llama", + ID: []byte{1, 1, 1, 1, 1}, + }, + Parameters: []protocol.CredentialParameter{ + { + Type: protocol.PublicKeyCredentialType, + Algorithm: webauthncose.AlgES256, + }, + }, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + RequireResidentKey: &rrk, + UserVerification: protocol.VerificationRequired, + }, + Attestation: protocol.PreferDirectAttestation, + }, + } + cc2 := *cc1 + cc2.Response.User = protocol.UserEntity{ + CredentialEntity: protocol.CredentialEntity{ + Name: userAlpaca, + }, + DisplayName: "Alpaca", + ID: []byte{1, 1, 1, 1, 2}, + } + for _, cc := range []*wanlib.CredentialCreation{cc1, &cc2} { + reg, err := touchid.Register(origin, cc) + require.NoError(t, err, "Register failed") + require.NoError(t, reg.Confirm(), "Confirm failed") + } + + mfaAllCreds := mfaAssertion + mfaAllCreds.Response.AllowedCredentials = nil + for _, c := range fake.creds { + mfaAllCreds.Response.AllowedCredentials = append(mfaAllCreds.Response.AllowedCredentials, protocol.CredentialDescriptor{ + Type: protocol.PublicKeyCredentialType, + CredentialID: []byte(c.id), + }) + } + + // Test absence of user prompts with existing credentials. + for _, test := range []struct { + name string + user string + assertion *wanlib.CredentialAssertion + }{ + { + name: "passwordless existing credentials", + user: "camel", // not registered + assertion: baseAssertion, + }, + { + name: "MFA unknown credential IDs (1)", + user: "", // any user + assertion: &mfaAssertion, // missing correct credential IDs + }, + { + name: "MFA unknown credential IDs (2)", + user: userLlama, // known user + assertion: &mfaAssertion, // missing correct credential IDs + }, + { + name: "MFA credentials for another user", + user: "camel", // unknown user + assertion: &mfaAllCreds, // credential IDs correct but for other users + }, + } { + t.Run(test.name, func(t *testing.T) { + fake.userPrompts = 0 // reset before test + _, _, err := touchid.Login(origin, test.user, test.assertion) + assert.ErrorIs(t, err, touchid.ErrCredentialNotFound, "Login error mismatch") + assert.Zero(t, fake.userPrompts, "Login caused user interaction with no credentials") + }) + } +} + type credentialHandle struct { rpID, user string id string @@ -350,6 +512,10 @@ type fakeNative struct { // lastAuthnCredential is the last credential ID used in a successful // Authenticate call. lastAuthnCredential string + + // userPrompts counts the number of user-visible prompts that would be caused + // by various methods. + userPrompts int } func (f *fakeNative) Diag() (*touchid.DiagResult, error) { @@ -363,7 +529,28 @@ func (f *fakeNative) Diag() (*touchid.DiagResult, error) { }, nil } -func (f *fakeNative) Authenticate(credentialID string, data []byte) ([]byte, error) { +type fakeAuthContext struct { + countPrompts func(ctx touchid.AuthContext) + prompted bool +} + +func (c *fakeAuthContext) Guard(fn func()) error { + c.countPrompts(c) + fn() + return nil +} + +func (c *fakeAuthContext) Close() { + c.prompted = false +} + +func (f *fakeNative) NewAuthContext() touchid.AuthContext { + return &fakeAuthContext{ + countPrompts: f.countPrompts, + } +} + +func (f *fakeNative) Authenticate(actx touchid.AuthContext, credentialID string, data []byte) ([]byte, error) { var key *ecdsa.PrivateKey for _, cred := range f.creds { if cred.id == credentialID { @@ -375,6 +562,7 @@ func (f *fakeNative) Authenticate(credentialID string, data []byte) ([]byte, err return nil, touchid.ErrCredentialNotFound } + f.countPrompts(actx) sig, err := key.Sign(rand.Reader, data, crypto.SHA256) if err != nil { return nil, err @@ -383,6 +571,18 @@ func (f *fakeNative) Authenticate(credentialID string, data []byte) ([]byte, err return sig, nil } +func (f *fakeNative) countPrompts(actx touchid.AuthContext) { + switch c, ok := actx.(*fakeAuthContext); { + case ok && c.prompted: + return // Already prompted + case ok: + c.prompted = true + fallthrough + default: + f.userPrompts++ + } +} + func (f *fakeNative) DeleteCredential(credentialID string) error { return errors.New("not implemented") } diff --git a/lib/auth/touchid/authenticate.h b/lib/auth/touchid/authenticate.h index 8c091c73c1e62..3cb1b970008f4 100644 --- a/lib/auth/touchid/authenticate.h +++ b/lib/auth/touchid/authenticate.h @@ -17,6 +17,7 @@ #include +#include "context.h" #include "credential_info.h" typedef struct AuthenticateRequest { @@ -29,6 +30,7 @@ typedef struct AuthenticateRequest { // it. The digest is expected to be in SHA256. // Authenticate requires user interaction. // Returns zero if successful, non-zero otherwise. -int Authenticate(AuthenticateRequest req, char **sigB64Out, char **errOut); +int Authenticate(AuthContext *actx, AuthenticateRequest req, char **sigB64Out, + char **errOut); #endif // AUTHENTICATE_H_ diff --git a/lib/auth/touchid/authenticate.m b/lib/auth/touchid/authenticate.m index d1285378b4861..59459b88ce3f6 100644 --- a/lib/auth/touchid/authenticate.m +++ b/lib/auth/touchid/authenticate.m @@ -22,8 +22,10 @@ #import #include "common.h" +#include "context.h" -int Authenticate(AuthenticateRequest req, char **sigB64Out, char **errOut) { +int Authenticate(AuthContext *actx, AuthenticateRequest req, char **sigB64Out, + char **errOut) { NSData *appLabel = [NSData dataWithBytes:req.app_label length:strlen(req.app_label)]; NSDictionary *query = @{ @@ -32,7 +34,10 @@ int Authenticate(AuthenticateRequest req, char **sigB64Out, char **errOut) { (id)kSecMatchLimit : (id)kSecMatchLimitOne, (id)kSecReturnRef : @YES, (id)kSecAttrApplicationLabel : appLabel, + // ctx takes effect in the SecKeyCreateSignature call below. + (id)kSecUseAuthenticationContext : (id)GetLAContextFromAuth(actx), }; + SecKeyRef privateKey = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey); diff --git a/lib/auth/touchid/context.h b/lib/auth/touchid/context.h new file mode 100644 index 0000000000000..2f1f03df5fc65 --- /dev/null +++ b/lib/auth/touchid/context.h @@ -0,0 +1,43 @@ +// Copyright 2022 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CONTEXT_H_ +#define CONTEXT_H_ + +#import + +#include + +// AuthContext is an optional, shared authentication context. +// Allows reusing a single authentication prompt/gesture between different +// functions, provided the functions are invoked in a short time interval. +typedef struct AuthContext { + LAContext *la_ctx; +} AuthContext; + +// GetLAContextFromAuth gets the LAContext from ctx, or returns a new LAContext +// instance. +LAContext *GetLAContextFromAuth(AuthContext *actx); + +// AuthContextGuard guards the invocation of a Go function handle behind an +// authentication prompt. +// The expected Go function signature is `func ()`. +// Returns zero if successful, non-zero otherwise. +int AuthContextGuard(AuthContext *actx, const char *reason, uintptr_t handle, + char **errOut); + +// AuthContextClose releases resources held by ctx. +void AuthContextClose(AuthContext *actx); + +#endif // CONTEXT_H_ diff --git a/lib/auth/touchid/context.m b/lib/auth/touchid/context.m new file mode 100644 index 0000000000000..0257168b8f11f --- /dev/null +++ b/lib/auth/touchid/context.m @@ -0,0 +1,85 @@ +//go:build touchid +// +build touchid + +// Copyright 2022 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "context.h" + +#import +#import +#import + +#include + +#include + +#include "common.h" + +// runGoFuncHandle is provided via CGO exports. +// (#include "_cgo_export.h" also works.) +extern void runGoFuncHandle(uintptr_t handle); + +LAContext *GetLAContextFromAuth(AuthContext *actx) { + if (actx == NULL) { + return [[LAContext alloc] init]; + } + if (actx->la_ctx == NULL) { + actx->la_ctx = [[LAContext alloc] init]; + actx->la_ctx.touchIDAuthenticationAllowableReuseDuration = 10; // seconds + } + return actx->la_ctx; +} + +int AuthContextGuard(AuthContext *actx, const char *reason, uintptr_t handle, + char **errOut) { + LAContext *ctx = GetLAContextFromAuth(actx); + + __block int res = 0; + __block NSString *nsError = NULL; + + // A semaphore is needed, otherwise we return before the prompt has a chance + // to resolve. + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + [ctx evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + localizedReason:[NSString stringWithUTF8String:reason] + reply:^void(BOOL success, NSError *_Nullable error) { + if (success) { + runGoFuncHandle(handle); + } else { + res = -1; + nsError = [error localizedDescription]; + } + dispatch_semaphore_signal(sema); + }]; + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + // sema released by ARC. + + if (nsError) { + *errOut = CopyNSString(nsError); + } else if (res != errSecSuccess) { + CFStringRef err = SecCopyErrorMessageString(res, NULL); + NSString *nsErr = (__bridge_transfer NSString *)err; + *errOut = CopyNSString(nsErr); + } + + return res; +} + +void AuthContextClose(AuthContext *actx) { + if (actx == NULL) { + return; + } + actx->la_ctx = NULL; // Let ARC collect the LAContext. +}