[go: nahoru, domu]

blob: 54667be37fa6dec66986bc5175f6b70ee65d3773 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/fido/mac/touch_id_context.h"
#import <CoreFoundation/CoreFoundation.h>
#import <Security/Security.h>
#include "base/bind.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/memory/ptr_util.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/mac/authenticator_config.h"
#include "device/fido/mac/keychain.h"
namespace device {
namespace fido {
namespace mac {
namespace {
API_AVAILABLE(macosx(10.12.2))
base::ScopedCFTypeRef<SecAccessControlRef> DefaultAccessControl() {
// The default access control policy used for WebAuthn credentials stored by
// the Touch ID platform authenticator.
return base::ScopedCFTypeRef<SecAccessControlRef>(
SecAccessControlCreateWithFlags(
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence,
nullptr));
}
// Returns whether the current binary is signed with a keychain-access-groups
// entitlement that contains |keychain_access_group|. This is required for the
// TouchIdAuthenticator to access key material stored in the Touch ID secure
// enclave.
bool BinaryHasKeychainAccessGroupEntitlementBlocking(
const std::string& keychain_access_group) {
// This method makes call into the macOS Security Framework, which may block.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
base::ScopedCFTypeRef<SecCodeRef> code;
if (SecCodeCopySelf(kSecCSDefaultFlags, code.InitializeInto()) !=
errSecSuccess) {
return false;
}
base::ScopedCFTypeRef<CFDictionaryRef> signing_info;
if (SecCodeCopySigningInformation(code, kSecCSDefaultFlags,
signing_info.InitializeInto()) !=
errSecSuccess) {
return false;
}
CFDictionaryRef entitlements =
base::mac::GetValueFromDictionary<CFDictionaryRef>(
signing_info, kSecCodeInfoEntitlementsDict);
if (!entitlements) {
return false;
}
CFArrayRef keychain_access_groups =
base::mac::GetValueFromDictionary<CFArrayRef>(
entitlements,
base::ScopedCFTypeRef<CFStringRef>(
base::SysUTF8ToCFStringRef("keychain-access-groups")));
if (!keychain_access_groups) {
return false;
}
return CFArrayContainsValue(
keychain_access_groups,
CFRangeMake(0, CFArrayGetCount(keychain_access_groups)),
base::ScopedCFTypeRef<CFStringRef>(
base::SysUTF8ToCFStringRef(keychain_access_group)));
}
API_AVAILABLE(macosx(10.12.2))
bool CanCreateSecureEnclaveKeyPairBlocking() {
// CryptoKit offers SecureEnclave.isAvailable but does not have Swift
// bindings. Instead, attempt to create an ephemeral key pair in the secure
// enclave.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
base::ScopedCFTypeRef<CFMutableDictionaryRef> params(
CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks));
CFDictionarySetValue(params, kSecAttrKeyType,
kSecAttrKeyTypeECSECPrimeRandom);
CFDictionarySetValue(params, kSecAttrKeySizeInBits, @256);
CFDictionarySetValue(params, kSecAttrTokenID, kSecAttrTokenIDSecureEnclave);
CFDictionarySetValue(params, kSecAttrIsPermanent, @NO);
base::ScopedCFTypeRef<CFErrorRef> cferr;
base::ScopedCFTypeRef<SecKeyRef> private_key(
Keychain::GetInstance().KeyCreateRandomKey(params,
cferr.InitializeInto()));
if (!private_key) {
FIDO_LOG(DEBUG) << "SecKeyCreateRandomKey failed: " << cferr;
return false;
}
return true;
}
} // namespace
// static
std::unique_ptr<TouchIdContext> TouchIdContext::CreateImpl() {
return base::WrapUnique(new TouchIdContext());
}
// static
TouchIdContext::CreateFuncPtr TouchIdContext::g_create_ =
&TouchIdContext::CreateImpl;
// static
std::unique_ptr<TouchIdContext> TouchIdContext::Create() {
// Testing seam to allow faking Touch ID in tests.
return (*g_create_)();
}
// static
bool TouchIdContext::TouchIdAvailableImplBlocking(AuthenticatorConfig config) {
// Ensure that the binary is signed with the keychain-access-group
// entitlement that is configured by the embedder; that user authentication
// with biometry, watch, or device passcode possible; and that the device has
// a secure enclave.
// TODO(martinkr): The calls into the Security Framework in these methods
// have been observed to hang or crash. We have since moved them off the UI
// thread to keep the browser responsive during hangs at least. But we should
// find a way to eliminate them or perhaps cache the result.
if (!BinaryHasKeychainAccessGroupEntitlementBlocking(
config.keychain_access_group)) {
FIDO_LOG(ERROR)
<< "Touch ID authenticator unavailable because keychain-access-group "
"entitlement is missing or incorrect";
return false;
}
base::scoped_nsobject<LAContext> context([[LAContext alloc] init]);
NSError* nserr;
if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication
error:&nserr]) {
FIDO_LOG(DEBUG) << "canEvaluatePolicy failed: " << nserr;
return false;
}
if (!CanCreateSecureEnclaveKeyPairBlocking()) {
FIDO_LOG(DEBUG) << "No secure enclave";
return false;
}
return true;
}
// Testing seam to allow faking Touch ID in tests.
TouchIdContext::TouchIdAvailableFuncPtr TouchIdContext::g_touch_id_available_ =
&TouchIdContext::TouchIdAvailableImplBlocking;
// static
void TouchIdContext::TouchIdAvailable(
AuthenticatorConfig config,
base::OnceCallback<void(bool is_available)> callback) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::TaskPriority::USER_VISIBLE, base::MayBlock(),
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(g_touch_id_available_, std::move(config)),
std::move(callback));
}
TouchIdContext::TouchIdContext()
: context_([[LAContext alloc] init]),
access_control_(DefaultAccessControl()),
callback_(),
weak_ptr_factory_(this) {}
TouchIdContext::~TouchIdContext() {
// Invalidating the LAContext will dismiss any pending UI dialog (e.g. if the
// transaction was cancelled while we are waiting for a fingerprint). Simply
// releasing the LAContext does not have the same effect.
[context_ invalidate];
}
void TouchIdContext::PromptTouchId(const std::u16string& reason,
Callback callback) {
callback_ = std::move(callback);
scoped_refptr<base::SequencedTaskRunner> runner =
base::SequencedTaskRunnerHandle::Get();
auto weak_self = weak_ptr_factory_.GetWeakPtr();
// If evaluation succeeds (i.e. user provides a fingerprint), |context_| can
// be used for one signing operation. N.B. even in |MakeCredentialOperation|,
// we need to perform a signature for the attestation statement, so we need
// the sign bit there.
[context_ evaluateAccessControl:access_control_
operation:LAAccessControlOperationUseKeySign
localizedReason:base::SysUTF16ToNSString(reason)
reply:^(BOOL success, NSError* error) {
// The reply block is invoked in a separate
// thread. We want to invoke the callback in the
// original thread, so we post it onto the
// originating runner. The weak_self and runner
// variables inside the block are const-copies of
// the ones in the enclosing scope (c.f.
// http://clang.llvm.org/docs/Block-ABI-Apple.html#imported-variables).
if (!success) {
// |error| is autoreleased in this block. It
// is not currently passed onto the other
// thread running the callback; but if it
// were, it would have to be retained first
// (and probably captured in a
// scoped_nsobject<NSError>).
DCHECK(error != nil);
DVLOG(1) << "Touch ID prompt failed: "
<< base::mac::NSToCFCast(error);
}
runner->PostTask(
FROM_HERE,
base::BindOnce(&TouchIdContext::RunCallback,
weak_self, success));
}];
}
void TouchIdContext::RunCallback(bool success) {
std::move(callback_).Run(success);
}
} // namespace mac
} // namespace fido
} // namespace device