0

[crypto] Plumb LAContext to mac signing keys

Plumb LAContext to UserVerifyingSigningKeyMac and UnexportableKeyMac.
Clients can pass an authenticated LAContext to avoid prompting the user
for local authentication on every call to Sign().

For UserVerifyingSigningKeyMac, callers can set the LAContext on the
provider config, and the provider takes ownership. Then, every operation
with that provider will use the LAContext.

UnexportableKeyMac uses a more involved approach where callers need to
pass the LAContext every time a credential is discovered or created from
an objective C source file. This affords more flexibility, which
simplifies the UVKey implementation. Clients interested in this
functionality should look at UVKeys instead anyway.

Bug: 326451282
Change-Id: I5e986e5a76ce8c821ef3abd2d19a5acc4a14e04f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5381486
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Commit-Queue: Adam Langley <agl@chromium.org>
Auto-Submit: Nina Satragno <nsatragno@chromium.org>
Reviewed-by: Adam Langley <agl@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1277093}
This commit is contained in:
Nina Satragno
2024-03-22 21:20:07 +00:00
committed by Chromium LUCI CQ
parent d252d4380d
commit dae4492bb1
11 changed files with 386 additions and 183 deletions

@ -713,11 +713,11 @@ base::flat_map<int32_t, std::vector<uint8_t>> GetNewSecretsToStore(
}
crypto::UserVerifyingKeyProvider::Config MakeUserVerifyingKeyConfig() {
return {
crypto::UserVerifyingKeyProvider::Config config;
#if BUILDFLAG(IS_MAC)
.keychain_access_group = kUserVerifyingKeyKeychainAccessGroup,
config.keychain_access_group = kUserVerifyingKeyKeychainAccessGroup;
#endif // BUILDFLAG(IS_MAC)
};
return config;
}
} // namespace

@ -83,6 +83,12 @@ component("crypto") {
if (is_mac) {
sources += [
"apple_keychain_mac.cc",
"mac_security_services_lock.cc",
"mac_security_services_lock.h",
"scoped_lacontext.h",
"scoped_lacontext.mm",
"unexportable_key_mac.h",
"unexportable_key_mac.mm",
"user_verifying_key_mac.mm",
]
} else if (is_ios) {
@ -98,13 +104,6 @@ component("crypto") {
]
}
if (is_mac) {
sources += [
"mac_security_services_lock.cc",
"mac_security_services_lock.h",
"unexportable_key_mac.mm",
]
}
if (is_win) {
sources += [
"scoped_capi_types.h",
@ -252,6 +251,7 @@ static_library("test_support") {
frameworks = [
"CoreFoundation.framework",
"Foundation.framework",
"LocalAuthentication.framework",
"Security.framework",
]
}

46
crypto/scoped_lacontext.h Normal file

@ -0,0 +1,46 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CRYPTO_SCOPED_LACONTEXT_H_
#define CRYPTO_SCOPED_LACONTEXT_H_
#if defined(__OBJC__)
#import <LocalAuthentication/LocalAuthentication.h>
#endif // defined(__OBJC__)
#include <memory>
#include "crypto/crypto_export.h"
namespace crypto {
// ScopedLAContext can hold an `LAContext` and is safe to pass around from C++.
// ScopedLAContext functions as a unique pointer. The UI can create one with an
// authenticated LAContext, then pass it down to the platform.
class CRYPTO_EXPORT ScopedLAContext {
public:
#if defined(__OBJC__)
// Takes ownership of |lacontext|.
explicit ScopedLAContext(LAContext* lacontext);
#endif // defined(__OBJC__)
~ScopedLAContext();
ScopedLAContext(ScopedLAContext&) = delete;
ScopedLAContext(ScopedLAContext&&);
ScopedLAContext& operator=(const ScopedLAContext&) = delete;
ScopedLAContext& operator=(ScopedLAContext&&);
#if defined(__OBJC__)
// release returns the last `LAContext` passed on construction and drops its
// reference to it. It is invalid to to call release more than once.
LAContext* release();
#endif // defined(__OBJC__)
private:
struct ObjCStorage;
std::unique_ptr<ObjCStorage> storage_;
};
} // namespace crypto
#endif // CRYPTO_SCOPED_LACONTEXT_H_

@ -0,0 +1,35 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "crypto/scoped_lacontext.h"
#include <memory>
#include <LocalAuthentication/LocalAuthentication.h>
#include "base/check.h"
namespace crypto {
struct ScopedLAContext::ObjCStorage {
LAContext* __strong context;
};
ScopedLAContext::ScopedLAContext(LAContext* lacontext)
: storage_(std::make_unique<ObjCStorage>()) {
storage_->context = lacontext;
}
ScopedLAContext::ScopedLAContext(ScopedLAContext&&) = default;
ScopedLAContext& ScopedLAContext::operator=(ScopedLAContext&& other) = default;
ScopedLAContext::~ScopedLAContext() = default;
LAContext* ScopedLAContext::release() {
CHECK(storage_);
LAContext* context = storage_->context;
storage_.reset();
return context;
}
} // namespace crypto

@ -76,8 +76,6 @@ class CRYPTO_EXPORT UnexportableKeyProvider {
// Note that if you set this and choose not to pass an authenticated
// LAContext when signing, macOS will prompt the user for biometrics and
// the thread will block until that resolves.
// TODO(nsatragno): allow some way to pass an authenticated LAContext when
// signing.
kUserPresence,
};

@ -0,0 +1,72 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CRYPTO_UNEXPORTABLE_KEY_MAC_H_
#define CRYPTO_UNEXPORTABLE_KEY_MAC_H_
#include <memory>
#if defined(__OBJC__)
#import <LocalAuthentication/LocalAuthentication.h>
#endif // defined(__OBJC__)
#include "crypto/unexportable_key.h"
namespace crypto {
// UserVerifyingKeyProviderMac is an implementation of the
// UserVerifyingKeyProvider interface on top of Apple's Secure Enclave. Callers
// must provide a keychain access group when instantiating this class. This
// means that the build must be codesigned for any of this to work.
// https://developer.apple.com/documentation/bundleresources/entitlements/keychain-access-groups?language=objc
//
// Only NIST P-256 elliptic curves are supported.
//
// Unlike Windows keys, macOS will store key metadata locally. Callers are
// responsible for deleting keys when they are no longer needed.
class UnexportableKeyProviderMac : public UnexportableKeyProvider {
public:
explicit UnexportableKeyProviderMac(Config config);
~UnexportableKeyProviderMac() override;
#if defined(__OBJC__)
// Like UnexportableKeyProvider::FromWrappedSigningKeySlowly, but lets you
// pass an authenticated LAContext to avoid having macOS prompt the user for
// user verification.
std::unique_ptr<UnexportableSigningKey> FromWrappedSigningKeySlowly(
base::span<const uint8_t> wrapped_key,
LAContext* lacontext);
// Like UnexportableKeyProvider::GenerateSigningKeySlowly, but lets you pass
// an authenticated LAContext to avoid having macOS prompt the user for user
// verification.
std::unique_ptr<UnexportableSigningKey> GenerateSigningKeySlowly(
base::span<const SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms,
LAContext* lacontext);
#endif // defined(__OBJC__)
// UnexportableKeyProvider:
std::optional<SignatureVerifier::SignatureAlgorithm> SelectAlgorithm(
base::span<const SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms) override;
std::unique_ptr<UnexportableSigningKey> GenerateSigningKeySlowly(
base::span<const SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms) override;
std::unique_ptr<UnexportableSigningKey> FromWrappedSigningKeySlowly(
base::span<const uint8_t> wrapped_key) override;
bool DeleteSigningKey(base::span<const uint8_t> wrapped_key) override;
private:
struct ObjCStorage;
const Config::AccessControl access_control_;
std::unique_ptr<ObjCStorage> objc_storage_;
};
std::unique_ptr<UnexportableKeyProviderMac> GetUnexportableKeyProviderMac(
UnexportableKeyProvider::Config config);
} // namespace crypto
#endif // CRYPTO_UNEXPORTABLE_KEY_MAC_H_

@ -12,6 +12,7 @@
#import <CoreFoundation/CoreFoundation.h>
#import <CryptoTokenKit/CryptoTokenKit.h>
#import <Foundation/Foundation.h>
#include <LocalAuthentication/LocalAuthentication.h>
#import <Security/Security.h>
#include "base/apple/bridging.h"
@ -29,6 +30,7 @@
#include "crypto/features.h"
#include "crypto/signature_verifier.h"
#include "crypto/unexportable_key.h"
#include "crypto/unexportable_key_mac.h"
#include "third_party/boringssl/src/include/openssl/bn.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
#include "third_party/boringssl/src/include/openssl/ec.h"
@ -165,145 +167,157 @@ class UnexportableSigningKeyMac : public UnexportableSigningKey {
std::vector<uint8_t> public_key_spki_;
};
// UserVerifyingKeyProviderMac is an implementation of the
// UserVerifyingKeyProvider interface on top of Apple's Secure Enclave. Callers
// must provide a keychain access group when instantiating this class. This
// means that the build must be codesigned for any of this to work.
// https://developer.apple.com/documentation/bundleresources/entitlements/keychain-access-groups?language=objc
//
// Only NIST P-256 elliptic curves are supported.
//
// Unlike Windows keys, macOS will store key metadata locally. Callers are
// responsible for deleting keys when they are no longer needed.
// TODO(nsatragno): add facilities to remove keys.
class UnexportableKeyProviderMac : public UnexportableKeyProvider {
public:
explicit UnexportableKeyProviderMac(Config config)
: keychain_access_group_(
base::SysUTF8ToNSString(std::move(config.keychain_access_group))),
application_tag_(
base::SysUTF8ToNSString(std::move(config.application_tag))),
access_control_(config.access_control) {}
~UnexportableKeyProviderMac() override = default;
std::optional<SignatureVerifier::SignatureAlgorithm> SelectAlgorithm(
base::span<const SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms) override {
return base::Contains(acceptable_algorithms,
SignatureVerifier::ECDSA_SHA256)
? std::make_optional(SignatureVerifier::ECDSA_SHA256)
: std::nullopt;
}
std::unique_ptr<UnexportableSigningKey> GenerateSigningKeySlowly(
base::span<const SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms) override {
// The Secure Enclave only supports elliptic curve keys.
if (!SelectAlgorithm(acceptable_algorithms)) {
return nullptr;
}
// Generate the key pair.
SecAccessControlCreateFlags control_flags =
kSecAccessControlPrivateKeyUsage;
switch (access_control_) {
case UnexportableKeyProvider::Config::AccessControl::kUserPresence:
control_flags |= kSecAccessControlUserPresence;
break;
case UnexportableKeyProvider::Config::AccessControl::kNone:
// No additional flag.
break;
}
base::apple::ScopedCFTypeRef<SecAccessControlRef> access(
SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, control_flags,
/*error=*/nil));
NSDictionary* key_attributes = @{
CFToNSPtrCast(kSecAttrIsPermanent) : @YES,
CFToNSPtrCast(kSecAttrAccessControl) : (__bridge id)access.get(),
};
NSDictionary* attributes = @{
CFToNSPtrCast(kSecUseDataProtectionKeychain) : @YES,
CFToNSPtrCast(kSecAttrKeyType) :
CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
CFToNSPtrCast(kSecAttrKeySizeInBits) : @256,
CFToNSPtrCast(kSecAttrTokenID) :
CFToNSPtrCast(kSecAttrTokenIDSecureEnclave),
CFToNSPtrCast(kSecPrivateKeyAttrs) : key_attributes,
CFToNSPtrCast(kSecAttrAccessGroup) : keychain_access_group_,
CFToNSPtrCast(kSecAttrLabel) : base::SysUTF8ToNSString(kAttrLabel),
CFToNSPtrCast(kSecAttrApplicationTag) : application_tag_,
};
base::apple::ScopedCFTypeRef<CFErrorRef> error;
base::apple::ScopedCFTypeRef<SecKeyRef> private_key(
AppleKeychainV2::GetInstance().KeyCreateRandomKey(
NSToCFPtrCast(attributes), error.InitializeInto()));
if (!private_key) {
LOG(ERROR) << "Could not create private key: " << error.get();
return nullptr;
}
base::apple::ScopedCFTypeRef<CFDictionaryRef> key_metadata =
AppleKeychainV2::GetInstance().KeyCopyAttributes(private_key.get());
return std::make_unique<UnexportableSigningKeyMac>(std::move(private_key),
key_metadata.get());
}
std::unique_ptr<UnexportableSigningKey> FromWrappedSigningKeySlowly(
base::span<const uint8_t> wrapped_key) override {
base::apple::ScopedCFTypeRef<CFTypeRef> key_data;
NSDictionary* query = @{
CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassKey),
CFToNSPtrCast(kSecAttrKeyType) :
CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
CFToNSPtrCast(kSecReturnRef) : @YES,
CFToNSPtrCast(kSecReturnAttributes) : @YES,
CFToNSPtrCast(kSecAttrAccessGroup) : keychain_access_group_,
CFToNSPtrCast(kSecAttrApplicationLabel) :
[NSData dataWithBytes:wrapped_key.data() length:wrapped_key.size()],
};
AppleKeychainV2::GetInstance().ItemCopyMatching(NSToCFPtrCast(query),
key_data.InitializeInto());
CFDictionaryRef key_attributes =
base::apple::CFCast<CFDictionaryRef>(key_data.get());
if (!key_attributes) {
return nullptr;
}
base::apple::ScopedCFTypeRef<SecKeyRef> key(
base::apple::GetValueFromDictionary<SecKeyRef>(key_attributes,
kSecValueRef),
base::scoped_policy::RETAIN);
return std::make_unique<UnexportableSigningKeyMac>(std::move(key),
key_attributes);
}
bool DeleteSigningKey(base::span<const uint8_t> wrapped_key) override {
NSDictionary* query = @{
CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassKey),
CFToNSPtrCast(kSecAttrKeyType) :
CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
CFToNSPtrCast(kSecAttrAccessGroup) : keychain_access_group_,
CFToNSPtrCast(kSecAttrApplicationLabel) :
[NSData dataWithBytes:wrapped_key.data() length:wrapped_key.size()],
};
OSStatus result =
AppleKeychainV2::GetInstance().ItemDelete(NSToCFPtrCast(query));
return result == errSecSuccess;
}
private:
NSString* __strong keychain_access_group_;
NSString* __strong application_tag_;
const Config::AccessControl access_control_;
};
} // namespace
std::unique_ptr<UnexportableKeyProvider> GetUnexportableKeyProviderMac(
struct UnexportableKeyProviderMac::ObjCStorage {
NSString* __strong keychain_access_group_;
NSString* __strong application_tag_;
};
UnexportableKeyProviderMac::UnexportableKeyProviderMac(Config config)
: access_control_(config.access_control),
objc_storage_(std::make_unique<ObjCStorage>()) {
objc_storage_->keychain_access_group_ =
base::SysUTF8ToNSString(std::move(config.keychain_access_group));
objc_storage_->application_tag_ =
base::SysUTF8ToNSString(std::move(config.application_tag));
}
UnexportableKeyProviderMac::~UnexportableKeyProviderMac() = default;
std::optional<SignatureVerifier::SignatureAlgorithm>
UnexportableKeyProviderMac::SelectAlgorithm(
base::span<const SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms) {
return base::Contains(acceptable_algorithms, SignatureVerifier::ECDSA_SHA256)
? std::make_optional(SignatureVerifier::ECDSA_SHA256)
: std::nullopt;
}
std::unique_ptr<UnexportableSigningKey>
UnexportableKeyProviderMac::GenerateSigningKeySlowly(
base::span<const SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms) {
return GenerateSigningKeySlowly(acceptable_algorithms, /*lacontext=*/nil);
}
std::unique_ptr<UnexportableSigningKey>
UnexportableKeyProviderMac::GenerateSigningKeySlowly(
base::span<const SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms,
LAContext* lacontext) {
// The Secure Enclave only supports elliptic curve keys.
if (!SelectAlgorithm(acceptable_algorithms)) {
return nullptr;
}
// Generate the key pair.
SecAccessControlCreateFlags control_flags = kSecAccessControlPrivateKeyUsage;
switch (access_control_) {
case UnexportableKeyProvider::Config::AccessControl::kUserPresence:
control_flags |= kSecAccessControlUserPresence;
break;
case UnexportableKeyProvider::Config::AccessControl::kNone:
// No additional flag.
break;
}
base::apple::ScopedCFTypeRef<SecAccessControlRef> access(
SecAccessControlCreateWithFlags(
kCFAllocatorDefault, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
control_flags,
/*error=*/nil));
NSMutableDictionary* key_attributes =
[NSMutableDictionary dictionaryWithDictionary:@{
CFToNSPtrCast(kSecAttrIsPermanent) : @YES,
CFToNSPtrCast(kSecAttrAccessControl) : (__bridge id)access.get(),
}];
if (lacontext) {
key_attributes[CFToNSPtrCast(kSecUseAuthenticationContext)] = lacontext;
}
NSDictionary* attributes = @{
CFToNSPtrCast(kSecUseDataProtectionKeychain) : @YES,
CFToNSPtrCast(kSecAttrKeyType) :
CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
CFToNSPtrCast(kSecAttrKeySizeInBits) : @256,
CFToNSPtrCast(kSecAttrTokenID) :
CFToNSPtrCast(kSecAttrTokenIDSecureEnclave),
CFToNSPtrCast(kSecPrivateKeyAttrs) : key_attributes,
CFToNSPtrCast(kSecAttrAccessGroup) : objc_storage_->keychain_access_group_,
CFToNSPtrCast(kSecAttrLabel) : base::SysUTF8ToNSString(kAttrLabel),
CFToNSPtrCast(kSecAttrApplicationTag) : objc_storage_->application_tag_,
};
base::apple::ScopedCFTypeRef<CFErrorRef> error;
base::apple::ScopedCFTypeRef<SecKeyRef> private_key(
AppleKeychainV2::GetInstance().KeyCreateRandomKey(
NSToCFPtrCast(attributes), error.InitializeInto()));
if (!private_key) {
LOG(ERROR) << "Could not create private key: " << error.get();
return nullptr;
}
base::apple::ScopedCFTypeRef<CFDictionaryRef> key_metadata =
AppleKeychainV2::GetInstance().KeyCopyAttributes(private_key.get());
return std::make_unique<UnexportableSigningKeyMac>(std::move(private_key),
key_metadata.get());
}
std::unique_ptr<UnexportableSigningKey>
UnexportableKeyProviderMac::FromWrappedSigningKeySlowly(
base::span<const uint8_t> wrapped_key) {
return FromWrappedSigningKeySlowly(wrapped_key, /*lacontext=*/nil);
}
std::unique_ptr<UnexportableSigningKey>
UnexportableKeyProviderMac::FromWrappedSigningKeySlowly(
base::span<const uint8_t> wrapped_key,
LAContext* lacontext) {
base::apple::ScopedCFTypeRef<CFTypeRef> key_data;
NSMutableDictionary* query = [NSMutableDictionary dictionaryWithDictionary:@{
CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassKey),
CFToNSPtrCast(kSecAttrKeyType) :
CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
CFToNSPtrCast(kSecReturnRef) : @YES,
CFToNSPtrCast(kSecReturnAttributes) : @YES,
CFToNSPtrCast(kSecAttrAccessGroup) : objc_storage_->keychain_access_group_,
CFToNSPtrCast(kSecAttrApplicationLabel) :
[NSData dataWithBytes:wrapped_key.data() length:wrapped_key.size()],
}];
if (lacontext) {
query[CFToNSPtrCast(kSecUseAuthenticationContext)] = lacontext;
}
AppleKeychainV2::GetInstance().ItemCopyMatching(NSToCFPtrCast(query),
key_data.InitializeInto());
CFDictionaryRef key_attributes =
base::apple::CFCast<CFDictionaryRef>(key_data.get());
if (!key_attributes) {
return nullptr;
}
base::apple::ScopedCFTypeRef<SecKeyRef> key(
base::apple::GetValueFromDictionary<SecKeyRef>(key_attributes,
kSecValueRef),
base::scoped_policy::RETAIN);
return std::make_unique<UnexportableSigningKeyMac>(std::move(key),
key_attributes);
}
bool UnexportableKeyProviderMac::DeleteSigningKey(
base::span<const uint8_t> wrapped_key) {
NSDictionary* query = @{
CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassKey),
CFToNSPtrCast(kSecAttrKeyType) :
CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
CFToNSPtrCast(kSecAttrAccessGroup) : objc_storage_->keychain_access_group_,
CFToNSPtrCast(kSecAttrApplicationLabel) :
[NSData dataWithBytes:wrapped_key.data() length:wrapped_key.size()],
};
OSStatus result =
AppleKeychainV2::GetInstance().ItemDelete(NSToCFPtrCast(query));
return result == errSecSuccess;
}
std::unique_ptr<UnexportableKeyProviderMac> GetUnexportableKeyProviderMac(
UnexportableKeyProvider::Config config) {
if (!base::FeatureList::IsEnabled(crypto::kEnableMacUnexportableKeys)) {
return nullptr;

@ -14,6 +14,12 @@ std::unique_ptr<UserVerifyingKeyProvider> (*g_mock_provider)() = nullptr;
} // namespace
UserVerifyingKeyProvider::Config::Config() = default;
UserVerifyingKeyProvider::Config::Config(Config&& config) = default;
UserVerifyingKeyProvider::Config& UserVerifyingKeyProvider::Config::operator=(
Config&& config) = default;
UserVerifyingKeyProvider::Config::~Config() = default;
UserVerifyingSigningKey::~UserVerifyingSigningKey() = default;
UserVerifyingKeyProvider::~UserVerifyingKeyProvider() = default;

@ -16,6 +16,7 @@
#include "base/memory/ref_counted.h"
#include "build/build_config.h"
#include "crypto/crypto_export.h"
#include "crypto/scoped_lacontext.h"
#include "crypto/signature_verifier.h"
#include "crypto/unexportable_key.h"
@ -74,7 +75,14 @@ class CRYPTO_EXPORT RefCountedUserVerifyingSigningKey
// UserVerifyingKeyProvider creates |UserVerifyingSigningKey|s.
class CRYPTO_EXPORT UserVerifyingKeyProvider {
public:
struct Config {
struct CRYPTO_EXPORT Config {
Config();
Config(const Config& config) = delete;
Config& operator=(const Config& config) = delete;
Config(Config&& config);
Config& operator=(Config&& config);
~Config();
#if BUILDFLAG(IS_MAC)
// The keychain access group the key is shared with. The binary must be
// codesigned with the corresponding entitlement.
@ -82,6 +90,11 @@ class CRYPTO_EXPORT UserVerifyingKeyProvider {
// This must be set to a non empty value when using user verifying keys on
// macOS.
std::string keychain_access_group;
// Optional LAContext to be used when retrieving and storing keys. Passing
// an authenticated LAContext lets you call UserVerifyingSigningKey::Sign()
// without triggering a macOS local authentication prompt.
std::optional<ScopedLAContext> lacontext;
#endif // BUILDFLAG(IS_MAC)
};

@ -20,7 +20,9 @@
#include "base/task/thread_pool.h"
#include "base/threading/scoped_thread_priority.h"
#include "crypto/apple_keychain_v2.h"
#include "crypto/scoped_lacontext.h"
#include "crypto/unexportable_key.h"
#include "crypto/unexportable_key_mac.h"
#include "crypto/user_verifying_key.h"
namespace crypto {
@ -102,7 +104,8 @@ class UserVerifyingSigningKeyMac : public UserVerifyingSigningKey {
class UserVerifyingKeyProviderMac : public UserVerifyingKeyProvider {
public:
explicit UserVerifyingKeyProviderMac(UserVerifyingKeyProvider::Config config)
: config_(std::move(config)) {}
: lacontext_(config.lacontext ? config.lacontext->release() : nil),
config_(std::move(config)) {}
~UserVerifyingKeyProviderMac() override = default;
void GenerateUserVerifyingSigningKey(
@ -110,14 +113,15 @@ class UserVerifyingKeyProviderMac : public UserVerifyingKeyProvider {
acceptable_algorithms,
base::OnceCallback<void(std::unique_ptr<UserVerifyingSigningKey>)>
callback) override {
std::unique_ptr<UnexportableKeyProvider> key_provider =
GetUnexportableKeyProvider(MakeUnexportableKeyConfig());
std::unique_ptr<UnexportableKeyProviderMac> key_provider =
GetUnexportableKeyProviderMac(MakeUnexportableKeyConfig());
if (!key_provider) {
std::move(callback).Run(nullptr);
return;
}
std::unique_ptr<UnexportableSigningKey> key =
key_provider->GenerateSigningKeySlowly(acceptable_algorithms);
key_provider->GenerateSigningKeySlowly(acceptable_algorithms,
lacontext_);
if (!key) {
std::move(callback).Run(nullptr);
return;
@ -131,14 +135,14 @@ class UserVerifyingKeyProviderMac : public UserVerifyingKeyProvider {
base::OnceCallback<void(std::unique_ptr<UserVerifyingSigningKey>)>
callback) override {
std::vector<uint8_t> wrapped_key(key_label.begin(), key_label.end());
std::unique_ptr<UnexportableKeyProvider> key_provider =
GetUnexportableKeyProvider(MakeUnexportableKeyConfig());
std::unique_ptr<UnexportableKeyProviderMac> key_provider =
GetUnexportableKeyProviderMac(MakeUnexportableKeyConfig());
if (!key_provider) {
std::move(callback).Run(nullptr);
return;
}
std::unique_ptr<UnexportableSigningKey> key =
key_provider->FromWrappedSigningKeySlowly(wrapped_key);
key_provider->FromWrappedSigningKeySlowly(wrapped_key, lacontext_);
if (!key) {
std::move(callback).Run(nullptr);
return;
@ -168,6 +172,7 @@ class UserVerifyingKeyProviderMac : public UserVerifyingKeyProvider {
UnexportableKeyProvider::Config::AccessControl::kUserPresence,
};
}
LAContext* __strong lacontext_;
const UserVerifyingKeyProvider::Config config_;
};

@ -7,6 +7,8 @@
#include <iterator>
#include <memory>
#include <LocalAuthentication/LocalAuthentication.h>
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
@ -16,6 +18,7 @@
#include "crypto/fake_apple_keychain_v2.h"
#include "crypto/features.h"
#include "crypto/scoped_fake_apple_keychain_v2.h"
#include "crypto/scoped_lacontext.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace crypto {
@ -26,9 +29,11 @@ constexpr char kTestKeychainAccessGroup[] = "test-keychain-access-group";
constexpr SignatureVerifier::SignatureAlgorithm kAcceptableAlgos[] = {
SignatureVerifier::ECDSA_SHA256};
const UserVerifyingKeyProvider::Config config = {
.keychain_access_group = kTestKeychainAccessGroup,
};
UserVerifyingKeyProvider::Config MakeConfig() {
UserVerifyingKeyProvider::Config config;
config.keychain_access_group = kTestKeychainAccessGroup;
return config;
}
class UserVerifyingKeyMacTest : public testing::Test {
public:
@ -95,36 +100,45 @@ class UserVerifyingKeyMacTest : public testing::Test {
kEnableMacUnexportableKeys};
std::unique_ptr<UserVerifyingKeyProvider> provider_ =
crypto::GetUserVerifyingKeyProvider(config);
crypto::GetUserVerifyingKeyProvider(MakeConfig());
};
TEST_F(UserVerifyingKeyMacTest, RoundTrip) {
std::unique_ptr<UserVerifyingSigningKey> key =
GenerateUserVerifyingSigningKey();
ASSERT_TRUE(key);
ASSERT_TRUE(!key->GetKeyLabel().empty());
for (bool use_lacontext : {false, true}) {
SCOPED_TRACE(use_lacontext);
UserVerifyingKeyProvider::Config config = MakeConfig();
if (use_lacontext) {
config.lacontext = ScopedLAContext([[LAContext alloc] init]);
}
provider_ = crypto::GetUserVerifyingKeyProvider(std::move(config));
const std::vector<uint8_t> spki = key->GetPublicKey();
const uint8_t message[] = {1, 2, 3, 4};
std::optional<std::vector<uint8_t>> signature = Sign(key.get(), message);
ASSERT_TRUE(signature);
std::unique_ptr<UserVerifyingSigningKey> key =
GenerateUserVerifyingSigningKey();
ASSERT_TRUE(key);
ASSERT_TRUE(!key->GetKeyLabel().empty());
crypto::SignatureVerifier verifier;
ASSERT_TRUE(verifier.VerifyInit(kAcceptableAlgos[0], *signature, spki));
verifier.VerifyUpdate(message);
ASSERT_TRUE(verifier.VerifyFinal());
const std::vector<uint8_t> spki = key->GetPublicKey();
const uint8_t message[] = {1, 2, 3, 4};
std::optional<std::vector<uint8_t>> signature = Sign(key.get(), message);
ASSERT_TRUE(signature);
std::unique_ptr<UserVerifyingSigningKey> key2 =
GetUserVerifyingSigningKey(key->GetKeyLabel());
ASSERT_TRUE(key2);
crypto::SignatureVerifier verifier;
ASSERT_TRUE(verifier.VerifyInit(kAcceptableAlgos[0], *signature, spki));
verifier.VerifyUpdate(message);
ASSERT_TRUE(verifier.VerifyFinal());
std::optional<std::vector<uint8_t>> signature2 = Sign(key.get(), message);
ASSERT_TRUE(signature2);
std::unique_ptr<UserVerifyingSigningKey> key2 =
GetUserVerifyingSigningKey(key->GetKeyLabel());
ASSERT_TRUE(key2);
crypto::SignatureVerifier verifier2;
ASSERT_TRUE(verifier2.VerifyInit(kAcceptableAlgos[0], *signature2, spki));
verifier2.VerifyUpdate(message);
ASSERT_TRUE(verifier2.VerifyFinal());
std::optional<std::vector<uint8_t>> signature2 = Sign(key.get(), message);
ASSERT_TRUE(signature2);
crypto::SignatureVerifier verifier2;
ASSERT_TRUE(verifier2.VerifyInit(kAcceptableAlgos[0], *signature2, spki));
verifier2.VerifyUpdate(message);
ASSERT_TRUE(verifier2.VerifyFinal());
}
}
TEST_F(UserVerifyingKeyMacTest, SecureEnclaveAvailability) {
@ -149,7 +163,7 @@ TEST_F(UserVerifyingKeyMacTest, SecureEnclaveAvailability) {
scoped_fake_apple_keychain_.keychain()->set_uv_method(test.uv_method);
std::optional<bool> result;
base::RunLoop run_loop;
AreUserVerifyingKeysSupported(config,
AreUserVerifyingKeysSupported(MakeConfig(),
base::BindLambdaForTesting([&](bool ret) {
result = ret;
run_loop.Quit();