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

Implement the Touch ID credential picker #14493

Merged
merged 7 commits into from
Jul 19, 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
125 changes: 96 additions & 29 deletions lib/auth/touchid/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,9 @@ type DiagResult struct {

// CredentialInfo holds information about a Secure Enclave credential.
type CredentialInfo struct {
UserHandle []byte
CredentialID string
RPID string
User string
User UserInfo
PublicKey *ecdsa.PublicKey
CreateTime time.Time

Expand All @@ -125,6 +124,12 @@ type CredentialInfo struct {
publicKeyRaw []byte
}

// UserInfo holds information about a credential owner.
type UserInfo struct {
UserHandle []byte
codingllama marked this conversation as resolved.
Show resolved Hide resolved
Name string
}

var (
cachedDiag *DiagResult
cachedDiagMU sync.Mutex
Expand Down Expand Up @@ -423,10 +428,18 @@ func makeAttestationData(ceremony protocol.CeremonyType, origin, rpID string, ch
}, nil
}

// CredentialPicker allows users to choose a credential for login.
type CredentialPicker interface {
// PromptCredential prompts the user to pick a credential from the list.
// Prompts only happen if there is more than one credential to choose from.
// Must return one of the pointers from the slice or an error.
PromptCredential(creds []*CredentialInfo) (*CredentialInfo, error)
}

// Login authenticates using a Secure Enclave-backed biometric credential.
// It returns the assertion response and the user that owns the credential to
// sign it.
func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib.CredentialAssertionResponse, string, error) {
func Login(origin, user string, assertion *wanlib.CredentialAssertion, picker CredentialPicker) (*wanlib.CredentialAssertionResponse, string, error) {
if !IsAvailable() {
return nil, "", ErrNotAvailable
}
Expand All @@ -445,6 +458,8 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib.
return nil, "", errors.New("challenge required")
case assertion.Response.RelyingPartyID == "":
return nil, "", errors.New("relying party ID required")
case picker == nil:
return nil, "", errors.New("picker required")
}

rpID := assertion.Response.RelyingPartyID
Expand All @@ -464,29 +479,34 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib.
return i1.CreateTime.After(i2.CreateTime)
})

// Verify infos against allowed credentials, if any.
cred, ok := findAllowedCredential(infos, assertion.Response.AllowedCredentials)
if !ok {
return nil, "", ErrCredentialNotFound
}

// Guard first read of chosen credential with an explicit check.
// A more meaningful check can be made once the credential picker is
// implemented.
// Prepare authentication context and prompt for the credential picker.
actx := native.NewAuthContext()
defer actx.Close()
promptPlatform()
if err := actx.Guard(func() {
log.Debugf("Touch ID: using credential %q", cred.CredentialID)
}); err != nil {

var prompted bool
promptOnce := func() {
if prompted {
return
}
promptPlatform()
prompted = true
}

cred, err := pickCredential(
actx,
infos, assertion.Response.AllowedCredentials,
picker, promptOnce, user != "" /* userRequested */)
if err != nil {
return nil, "", trace.Wrap(err)
}
log.Debugf("Touch ID: using credential %q", cred.CredentialID)

attData, err := makeAttestationData(protocol.AssertCeremony, origin, rpID, assertion.Response.Challenge, nil /* cred */)
if err != nil {
return nil, "", trace.Wrap(err)
}

promptOnce() // In case the picker prompt didn't happen.
sig, err := native.Authenticate(actx, cred.CredentialID, attData.digest)
if err != nil {
return nil, "", trace.Wrap(err)
Expand All @@ -506,26 +526,73 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib.
},
AuthenticatorData: attData.rawAuthData,
Signature: sig,
UserHandle: cred.UserHandle,
UserHandle: cred.User.UserHandle,
},
}, cred.User, nil
}, cred.User.Name, nil
}

func findAllowedCredential(infos []CredentialInfo, allowedCredentials []protocol.CredentialDescriptor) (CredentialInfo, bool) {
if len(infos) > 0 && len(allowedCredentials) == 0 {
// Default to "first" credential for passwordless
return infos[0], true
}

for _, info := range infos {
for _, cred := range allowedCredentials {
if info.CredentialID == string(cred.CredentialID) {
return info, true
func pickCredential(
actx AuthContext,
infos []CredentialInfo, allowedCredentials []protocol.CredentialDescriptor,
picker CredentialPicker, promptOnce func(), userRequested bool) (*CredentialInfo, error) {
// Handle early exits.
switch l := len(infos); {
// MFA.
case len(allowedCredentials) > 0:
for _, info := range infos {
for _, cred := range allowedCredentials {
if info.CredentialID == string(cred.CredentialID) {
return &info, nil
}
}
}
return nil, ErrCredentialNotFound

// Single credential or specific user requested.
codingllama marked this conversation as resolved.
Show resolved Hide resolved
// A requested user means that all credentials are for that user, so there
// would be nothing to pick.
case l == 1 || userRequested:
return &infos[0], nil
}

// Dedup users to avoid confusion.
// This assumes credentials are sorted from most to less preferred.
knownUsers := make(map[string]struct{})
deduped := make([]*CredentialInfo, 0, len(infos))
for _, c := range infos {
if _, ok := knownUsers[c.User.Name]; ok {
continue
}
knownUsers[c.User.Name] = struct{}{}

c := c // Avoid capture-by-reference errors
deduped = append(deduped, &c)
}
if len(deduped) == 1 {
return deduped[0], nil
}

promptOnce()
var choice *CredentialInfo
var choiceErr error
if err := actx.Guard(func() {
choice, choiceErr = picker.PromptCredential(deduped)
}); err != nil {
return nil, trace.Wrap(err)
}
if choiceErr != nil {
return nil, trace.Wrap(choiceErr)
}

return CredentialInfo{}, false
// Is choice a pointer within the slice?
// We could work around this requirement, but it seems better to constrain the
// picker API from the start.
espadolini marked this conversation as resolved.
Show resolved Hide resolved
for _, c := range deduped {
if c == choice {
return choice, nil
}
}
return nil, fmt.Errorf("picker returned invalid credential: %#v", choice)
}

// ListCredentials lists all registered Secure Enclave credentials.
Expand Down
6 changes: 4 additions & 2 deletions lib/auth/touchid/api_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,10 +334,12 @@ func readCredentialInfos(find func(**C.CredentialInfo) C.int) ([]CredentialInfo,
}

infos = append(infos, CredentialInfo{
UserHandle: userHandle,
CredentialID: credentialID,
RPID: parsedLabel.rpID,
User: parsedLabel.user,
User: UserInfo{
UserHandle: userHandle,
Name: parsedLabel.user,
},
CreateTime: createTime,
publicKeyRaw: pubKeyRaw,
})
Expand Down
Loading