Skip to content

Commit

Permalink
Add tsh touchid ls and rm commands (#12505)
Browse files Browse the repository at this point in the history
Implement touch ID credential management via tsh touchid ls and tsh touchid rm.

Departs slightly from RFD command names in order to better match the tsh mfa.

See https://github.com/gravitational/teleport/blob/master/rfd/0054-passwordless-macos.md.

#9160

* Implement touch ID credential listing
* Add the `tsh touchid ls` command
* Implement touch ID credential deletion
* Add the `tsh touchid rm` command
* Delegate MFA prompts to WebAuthn
* Undo changes to tsh.go command switch
* Prompt newline. Trace errors.
* Update e/ to 6abb96b
* Var initialization. Guard against NULL. Return all credentials.
* Address review comments: simplifications and style
  • Loading branch information
codingllama authored May 11, 2022
1 parent 3fd2277 commit 0b34833
Show file tree
Hide file tree
Showing 13 changed files with 364 additions and 30 deletions.
2 changes: 1 addition & 1 deletion e
Submodule e updated from cf63aa to 6abb96
39 changes: 38 additions & 1 deletion lib/auth/touchid/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/gravitational/trace"

wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
log "github.com/sirupsen/logrus"
)

var (
Expand All @@ -50,6 +51,12 @@ type nativeTID interface {
// FindCredentials finds credentials without user interaction.
// An empty user means "all users".
FindCredentials(rpID, user string) ([]CredentialInfo, error)

// ListCredentials lists all registered credentials.
// Requires user interaction.
ListCredentials() ([]CredentialInfo, error)

DeleteCredential(credentialID string) error
}

// CredentialInfo holds information about a Secure Enclave credential.
Expand Down Expand Up @@ -322,7 +329,7 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib.
infos, err := native.FindCredentials(rpID, user)
switch {
case err != nil:
return nil, "", err
return nil, "", trace.Wrap(err)
case len(infos) == 0:
return nil, "", ErrCredentialNotFound
}
Expand Down Expand Up @@ -373,3 +380,33 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib.
},
}, cred.User, nil
}

// ListCredentials lists all registered Secure Enclave credentials.
// Requires user interaction.
func ListCredentials() ([]CredentialInfo, error) {
// Skipped IsAvailable check in favor of a direct call to native.
infos, err := native.ListCredentials()
if err != nil {
return nil, trace.Wrap(err)
}

// Parse public keys.
for i := range infos {
info := &infos[i]
key, err := pubKeyFromRawAppleKey(info.publicKeyRaw)
if err != nil {
log.Warnf("Failed to convert public key: %v", err)
}
info.PublicKey = key // this is OK, even if it's nil
info.publicKeyRaw = nil
}

return infos, nil
}

// DeleteCredential deletes a Secure Enclave credential.
// Requires user interaction.
func DeleteCredential(credentialID string) error {
// Skipped IsAvailable check in favor of a direct call to native.
return native.DeleteCredential(credentialID)
}
65 changes: 54 additions & 11 deletions lib/auth/touchid/api_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import "C"
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"unsafe"

Expand Down Expand Up @@ -130,27 +129,46 @@ func (touchIDImpl) Authenticate(credentialID string, digest []byte) ([]byte, err
}

func (touchIDImpl) FindCredentials(rpID, user string) ([]CredentialInfo, error) {
infos, res := findCredentialsImpl(rpID, user, func(filter C.LabelFilter, infosC **C.CredentialInfo) C.int {
return C.FindCredentials(filter, infosC)
var filterC C.LabelFilter
if user == "" {
filterC.kind = C.LABEL_PREFIX
}
filterC.value = C.CString(makeLabel(rpID, user))
defer C.free(unsafe.Pointer(filterC.value))

infos, res := readCredentialInfos(func(infosC **C.CredentialInfo) C.int {
return C.FindCredentials(filterC, infosC)
})
if res < 0 {
return nil, fmt.Errorf("failed to find credentials: status %d", res)
return nil, trace.BadParameter("failed to find credentials: status %d", res)
}
return infos, nil
}

func findCredentialsImpl(rpID, user string, find func(C.LabelFilter, **C.CredentialInfo) C.int) ([]CredentialInfo, int) {
var filterC C.LabelFilter
if user == "" {
filterC.kind = C.LABEL_PREFIX
func (touchIDImpl) ListCredentials() ([]CredentialInfo, error) {
// User prompt becomes: ""$binary" is trying to list credentials".
reasonC := C.CString("list credentials")
defer C.free(unsafe.Pointer(reasonC))

var errMsgC *C.char
defer C.free(unsafe.Pointer(errMsgC))

infos, res := readCredentialInfos(func(infosOut **C.CredentialInfo) C.int {
return C.ListCredentials(reasonC, infosOut, &errMsgC)
})
if res < 0 {
errMsg := C.GoString(errMsgC)
return nil, errors.New(errMsg)
}
filterC.value = C.CString(makeLabel(rpID, user))
defer C.free(unsafe.Pointer(filterC.value))

return infos, nil
}

func readCredentialInfos(find func(**C.CredentialInfo) C.int) ([]CredentialInfo, int) {
var infosC *C.CredentialInfo
defer C.free(unsafe.Pointer(infosC))

res := find(filterC, &infosC)
res := find(&infosC)
if res < 0 {
return nil, int(res)
}
Expand Down Expand Up @@ -211,3 +229,28 @@ func findCredentialsImpl(rpID, user string, find func(C.LabelFilter, **C.Credent
}
return infos, int(res)
}

// https://osstatus.com/search/results?framework=Security&search=-25300
const errSecItemNotFound = -25300

func (touchIDImpl) DeleteCredential(credentialID string) error {
// User prompt becomes: ""$binary" is trying to delete credential".
reasonC := C.CString("delete credential")
defer C.free(unsafe.Pointer(reasonC))

idC := C.CString(credentialID)
defer C.free(unsafe.Pointer(idC))

var errC *C.char
defer C.free(unsafe.Pointer(errC))

switch C.DeleteCredential(reasonC, idC, &errC) {
case 0: // aka success
return nil
case errSecItemNotFound:
return ErrCredentialNotFound
default:
errMsg := C.GoString(errC)
return errors.New(errMsg)
}
}
8 changes: 8 additions & 0 deletions lib/auth/touchid/api_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ func (noopNative) Authenticate(credentialID string, digest []byte) ([]byte, erro
func (noopNative) FindCredentials(rpID, user string) ([]CredentialInfo, error) {
return nil, ErrNotAvailable
}

func (noopNative) ListCredentials() ([]CredentialInfo, error) {
return nil, ErrNotAvailable
}

func (noopNative) DeleteCredential(credentialID string) error {
return ErrNotAvailable
}
9 changes: 9 additions & 0 deletions lib/auth/touchid/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"testing"

"github.com/duo-labs/webauthn/protocol"
Expand Down Expand Up @@ -141,6 +142,10 @@ func (f *fakeNative) Authenticate(credentialID string, data []byte) ([]byte, err
return key.Sign(rand.Reader, data, crypto.SHA256)
}

func (f *fakeNative) DeleteCredential(credentialID string) error {
return errors.New("not implemented")
}

func (f *fakeNative) IsAvailable() bool {
return true
}
Expand All @@ -161,6 +166,10 @@ func (f *fakeNative) FindCredentials(rpID, user string) ([]touchid.CredentialInf
return resp, nil
}

func (f *fakeNative) ListCredentials() ([]touchid.CredentialInfo, error) {
return nil, errors.New("not implemented")
}

func (f *fakeNative) Register(rpID, user string, userHandle []byte) (*touchid.CredentialInfo, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions lib/auth/touchid/authenticate.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// go:build touchid
// +build touchid
//go:build touchid
// +build touchid

// Copyright 2022 Gravitational, Inc
//
Expand Down
11 changes: 7 additions & 4 deletions lib/auth/touchid/common.m
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build touchid
// +build touchid

// Copyright 2022 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -12,15 +15,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build touchid
// +build touchid

#include "common.h"

#import <Foundation/Foundation.h>

#include <string.h>

char *CopyNSString(NSString *val) {
return strdup([val UTF8String]);
if (val) {
return strdup([val UTF8String]);
}
return strdup("");
}
13 changes: 13 additions & 0 deletions lib/auth/touchid/credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,17 @@ typedef struct LabelFilter {
// User interaction is not required.
int FindCredentials(LabelFilter filter, CredentialInfo **infosOut);

// ListCredentials finds all registered credentials.
// Returns the numbers of credentials assigned to the infos array, or negative
// on failure (typically an OSStatus code). The caller is expected to free infos
// (and their contents!).
// Requires user interaction.
int ListCredentials(const char *reason, CredentialInfo **infosOut,
char **errOut);

// DeleteCredential deletes a credential by its app_label.
// Requires user interaction.
// Returns zero if successful, non-zero otherwise (typically an OSStatus).
int DeleteCredential(const char *reason, const char *appLabel, char **errOut);

#endif // CREDENTIALS_H_
98 changes: 94 additions & 4 deletions lib/auth/touchid/credentials.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// go:build touchid
// +build touchid
//go:build touchid
// +build touchid

// Copyright 2022 Gravitational, Inc
//
Expand All @@ -19,11 +19,14 @@

#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import <Security/Security.h>

#include <limits.h>
#include <stdlib.h>

#include <dispatch/dispatch.h>

#include "common.h"

BOOL matchesLabelFilter(LabelFilterKind kind, NSString *filter,
Expand All @@ -37,7 +40,8 @@ BOOL matchesLabelFilter(LabelFilterKind kind, NSString *filter,
return NO;
}

int FindCredentials(LabelFilter filter, CredentialInfo **infosOut) {
int findCredentials(BOOL applyFilter, LabelFilter filter,
CredentialInfo **infosOut) {
NSDictionary *query = @{
(id)kSecClass : (id)kSecClassKey,
(id)kSecAttrKeyType : (id)kSecAttrKeyTypeECSECPrimeRandom,
Expand Down Expand Up @@ -75,7 +79,7 @@ int FindCredentials(LabelFilter filter, CredentialInfo **infosOut) {

CFStringRef label = CFDictionaryGetValue(attrs, kSecAttrLabel);
NSString *nsLabel = (__bridge NSString *)label;
if (!matchesLabelFilter(filter.kind, nsFilter, nsLabel)) {
if (applyFilter && !matchesLabelFilter(filter.kind, nsFilter, nsLabel)) {
continue;
}

Expand Down Expand Up @@ -113,3 +117,89 @@ int FindCredentials(LabelFilter filter, CredentialInfo **infosOut) {
CFRelease(items);
return infosLen;
}

int FindCredentials(LabelFilter filter, CredentialInfo **infosOut) {
return findCredentials(YES /* applyFilter */, filter, infosOut);
}

int ListCredentials(const char *reason, CredentialInfo **infosOut,
char **errOut) {
LAContext *ctx = [[LAContext alloc] init];

__block LabelFilter filter;
filter.kind = LABEL_PREFIX;
filter.value = "";

__block int res;
__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) {
res =
findCredentials(NO /* applyFilter */, filter, infosOut);
} 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);
}

return res;
}

OSStatus deleteCredential(const char *appLabel) {
NSData *nsAppLabel = [NSData dataWithBytes:appLabel length:strlen(appLabel)];
NSDictionary *query = @{
(id)kSecClass : (id)kSecClassKey,
(id)kSecAttrKeyType : (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecMatchLimit : (id)kSecMatchLimitOne,
(id)kSecAttrApplicationLabel : nsAppLabel,
};
return SecItemDelete((__bridge CFDictionaryRef)query);
}

int DeleteCredential(const char *reason, const char *appLabel, char **errOut) {
LAContext *ctx = [[LAContext alloc] init];

__block int res;
__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) {
res = deleteCredential(appLabel);
} 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;
}
4 changes: 2 additions & 2 deletions lib/auth/touchid/register.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// go:build touchid
// +build touchid
//go:build touchid
// +build touchid

// Copyright 2022 Gravitational, Inc
//
Expand Down
Loading

0 comments on commit 0b34833

Please sign in to comment.