Skip to content

Commit

Permalink
Allow explicit Touch ID prompts (#14492)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
codingllama committed Jul 19, 2022
1 parent a1170ec commit f31b9cb
Show file tree
Hide file tree
Showing 8 changed files with 468 additions and 27 deletions.
36 changes: 29 additions & 7 deletions lib/auth/touchid/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down
102 changes: 87 additions & 15 deletions lib/auth/touchid/api_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package touchid
// #cgo LDFLAGS: -framework CoreFoundation -framework Foundation -framework LocalAuthentication -framework Security
// #include <stdlib.h>
// #include "authenticate.h"
// #include "context.h"
// #include "credential_info.h"
// #include "credentials.h"
// #include "diag.h"
Expand All @@ -29,8 +30,8 @@ import "C"

import (
"encoding/base64"
"errors"
"fmt"
"runtime/cgo"
"strings"
"time"
"unsafe"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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))
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -293,27 +358,34 @@ 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)
}
}

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)
}
14 changes: 13 additions & 1 deletion lib/auth/touchid/api_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading

0 comments on commit f31b9cb

Please sign in to comment.