Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow explicit Touch ID prompts #14492

Merged
merged 5 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
codingllama marked this conversation as resolved.
Show resolved Hide resolved
)

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)
codingllama marked this conversation as resolved.
Show resolved Hide resolved
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