mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 13:39:37 +08:00
Implement native macOS methods required by device enrollment, namely methods to Create/Get the device key, Sign challenges with it, and to collect device data (aka serial number). The implementation is rather similar to lib/auth/touchid, but simpler in a few aspects: 1. Device keys don't require user interaction (as in they don't cause a touch ID prompt); and 2. There exists, at most, a single device key at all times. I've added a tiny refactor to reuse Apple public key parsing from touchid, plus some changes so touchid doesn't break in the face of unexpected keys (which the device key didn't cause, per se, but my experiments getting to it did). gravitational/teleport.e#514
237 lines
7.5 KiB
Objective-C
237 lines
7.5 KiB
Objective-C
//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 "credentials.h"
|
|
|
|
#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,
|
|
NSString *label) {
|
|
switch (kind) {
|
|
case LABEL_EXACT:
|
|
return [label isEqualToString:filter];
|
|
case LABEL_PREFIX:
|
|
return [label hasPrefix:filter];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
int findCredentials(BOOL applyFilter, LabelFilter filter,
|
|
CredentialInfo **infosOut) {
|
|
NSDictionary *query = @{
|
|
(id)kSecClass : (id)kSecClassKey,
|
|
(id)kSecAttrKeyType : (id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
(id)kSecMatchLimit : (id)kSecMatchLimitAll,
|
|
(id)kSecReturnRef : @YES,
|
|
(id)kSecReturnAttributes : @YES,
|
|
};
|
|
CFArrayRef items = NULL;
|
|
OSStatus status =
|
|
SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&items);
|
|
switch (status) {
|
|
case errSecSuccess:
|
|
break; // continue below
|
|
case errSecItemNotFound:
|
|
return 0; // aka no items found
|
|
default:
|
|
// Not possible afaik, but let's make sure we keep up the method contract.
|
|
if (status >= 0) {
|
|
status = status * -1;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
NSString *nsFilter = [NSString stringWithUTF8String:filter.value];
|
|
|
|
CFIndex count = CFArrayGetCount(items);
|
|
// Guard against overflows, just in case we ever get that many credentials.
|
|
if (count > INT_MAX) {
|
|
count = INT_MAX;
|
|
}
|
|
*infosOut = calloc(count, sizeof(CredentialInfo));
|
|
int infosLen = 0;
|
|
for (CFIndex i = 0; i < count; i++) {
|
|
CFDictionaryRef attrs = CFArrayGetValueAtIndex(items, i);
|
|
|
|
CFTypeRef label;
|
|
if (!CFDictionaryGetValueIfPresent(attrs, kSecAttrLabel, &label)) {
|
|
continue;
|
|
}
|
|
if (CFGetTypeID(label) != CFStringGetTypeID()) {
|
|
continue;
|
|
}
|
|
NSString *nsLabel = (__bridge NSString *)label;
|
|
if (applyFilter && !matchesLabelFilter(filter.kind, nsFilter, nsLabel)) {
|
|
continue;
|
|
}
|
|
|
|
CFTypeRef appTag;
|
|
if (!CFDictionaryGetValueIfPresent(attrs, kSecAttrApplicationTag,
|
|
&appTag)) {
|
|
continue;
|
|
}
|
|
if (CFGetTypeID(appTag) != CFDataGetTypeID()) {
|
|
continue;
|
|
}
|
|
NSString *nsAppTag =
|
|
[[NSString alloc] initWithData:(__bridge NSData *)appTag
|
|
encoding:NSUTF8StringEncoding];
|
|
|
|
CFTypeRef appLabel;
|
|
if (!CFDictionaryGetValueIfPresent(attrs, kSecAttrApplicationLabel,
|
|
&appLabel)) {
|
|
continue;
|
|
}
|
|
if (CFGetTypeID(appLabel) != CFDataGetTypeID()) {
|
|
continue;
|
|
}
|
|
NSString *nsAppLabel =
|
|
[[NSString alloc] initWithData:(__bridge NSData *)appLabel
|
|
encoding:NSUTF8StringEncoding];
|
|
|
|
// Copy public key representation.
|
|
SecKeyRef privKey = (SecKeyRef)CFDictionaryGetValue(attrs, kSecValueRef);
|
|
SecKeyRef pubKey = SecKeyCopyPublicKey(privKey);
|
|
char *pubKeyB64 = NULL;
|
|
if (pubKey) {
|
|
CFDataRef pubKeyRep =
|
|
SecKeyCopyExternalRepresentation(pubKey, NULL /*error*/);
|
|
if (pubKeyRep) {
|
|
NSData *pubKeyData = CFBridgingRelease(pubKeyRep);
|
|
pubKeyB64 = CopyNSString([pubKeyData base64EncodedStringWithOptions:0]);
|
|
}
|
|
CFRelease(pubKey);
|
|
}
|
|
|
|
CFDateRef creationDate =
|
|
(CFDateRef)CFDictionaryGetValue(attrs, kSecAttrCreationDate);
|
|
NSDate *nsDate = (__bridge NSDate *)creationDate;
|
|
NSISO8601DateFormatter *formatter = [[NSISO8601DateFormatter alloc] init];
|
|
NSString *isoCreationDate = [formatter stringFromDate:nsDate];
|
|
|
|
(*infosOut + infosLen)->label = CopyNSString(nsLabel);
|
|
(*infosOut + infosLen)->app_label = CopyNSString(nsAppLabel);
|
|
(*infosOut + infosLen)->app_tag = CopyNSString(nsAppTag);
|
|
(*infosOut + infosLen)->pub_key_b64 = pubKeyB64;
|
|
(*infosOut + infosLen)->creation_date = CopyNSString(isoCreationDate);
|
|
infosLen++;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
int DeleteNonInteractive(const char *appLabel) {
|
|
return deleteCredential(appLabel);
|
|
}
|