| // 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/unexportable_key.h" |
| |
| #import <CoreFoundation/CoreFoundation.h> |
| #import <CryptoTokenKit/CryptoTokenKit.h> |
| #import <Foundation/Foundation.h> |
| #include <LocalAuthentication/LocalAuthentication.h> |
| #import <Security/Security.h> |
| |
| #include <algorithm> |
| #include <iterator> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/apple/bridging.h" |
| #include "base/apple/foundation_util.h" |
| #include "base/apple/scoped_cftyperef.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_policy.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "crypto/apple_keychain_util.h" |
| #include "crypto/apple_keychain_v2.h" |
| #include "crypto/features.h" |
| #include "crypto/signature_verifier.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" |
| #include "third_party/boringssl/src/include/openssl/evp.h" |
| #include "third_party/boringssl/src/include/openssl/mem.h" |
| #include "third_party/boringssl/src/include/openssl/obj.h" |
| |
| using base::apple::CFToNSPtrCast; |
| using base::apple::NSToCFPtrCast; |
| |
| namespace crypto { |
| |
| namespace { |
| |
| // The size of an uncompressed x9.63 encoded EC public key, 04 || X || Y. |
| constexpr size_t kUncompressedPointLength = 65; |
| |
| // The value of the kSecAttrLabel when generating the key. The documentation |
| // claims this should be a user-visible label, but there does not exist any UI |
| // that shows this value. Therefore, it is left untranslated. |
| constexpr char kAttrLabel[] = "Chromium unexportable key"; |
| |
| // Returns a span of a CFDataRef. |
| base::span<const uint8_t> ToSpan(CFDataRef data) { |
| return base::make_span(CFDataGetBytePtr(data), |
| base::checked_cast<size_t>(CFDataGetLength(data))); |
| } |
| |
| // Copies a CFDataRef into a vector of bytes. |
| std::vector<uint8_t> CFDataToVec(CFDataRef data) { |
| base::span<const uint8_t> span = ToSpan(data); |
| return std::vector<uint8_t>(span.begin(), span.end()); |
| } |
| |
| std::optional<std::vector<uint8_t>> Convertx963ToDerSpki( |
| base::span<const uint8_t> x962) { |
| // Parse x9.63 point into an |EC_POINT|. |
| bssl::UniquePtr<EC_GROUP> p256( |
| EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); |
| bssl::UniquePtr<EC_POINT> point(EC_POINT_new(p256.get())); |
| if (x962.size() != kUncompressedPointLength || |
| x962[0] != POINT_CONVERSION_UNCOMPRESSED || |
| !EC_POINT_oct2point(p256.get(), point.get(), x962.data(), x962.size(), |
| /*ctx=*/nullptr)) { |
| LOG(ERROR) << "P-256 public key is not on curve"; |
| return std::nullopt; |
| } |
| // Marshal point into a DER SPKI. |
| bssl::UniquePtr<EC_KEY> ec_key( |
| EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); |
| CHECK(EC_KEY_set_public_key(ec_key.get(), point.get())); |
| bssl::UniquePtr<EVP_PKEY> pkey(EVP_PKEY_new()); |
| CHECK(EVP_PKEY_assign_EC_KEY(pkey.get(), ec_key.release())); |
| bssl::ScopedCBB cbb; |
| uint8_t* der_bytes = nullptr; |
| size_t der_bytes_len = 0; |
| CHECK(CBB_init(cbb.get(), /* initial size */ 128) && |
| EVP_marshal_public_key(cbb.get(), pkey.get()) && |
| CBB_finish(cbb.get(), &der_bytes, &der_bytes_len)); |
| std::vector<uint8_t> ret(der_bytes, der_bytes + der_bytes_len); |
| OPENSSL_free(der_bytes); |
| return ret; |
| } |
| |
| // UnexportableSigningKeyMac is an implementation of the UnexportableSigningKey |
| // interface on top of Apple's Secure Enclave. |
| class UnexportableSigningKeyMac : public UnexportableSigningKey { |
| public: |
| UnexportableSigningKeyMac(base::apple::ScopedCFTypeRef<SecKeyRef> key, |
| CFDictionaryRef key_attributes) |
| : key_(std::move(key)), |
| application_label_( |
| CFDataToVec(base::apple::GetValueFromDictionary<CFDataRef>( |
| key_attributes, |
| kSecAttrApplicationLabel))) { |
| base::apple::ScopedCFTypeRef<SecKeyRef> public_key( |
| AppleKeychainV2::GetInstance().KeyCopyPublicKey(key_.get())); |
| base::apple::ScopedCFTypeRef<CFDataRef> x962_bytes( |
| AppleKeychainV2::GetInstance().KeyCopyExternalRepresentation( |
| public_key.get(), /*error=*/nil)); |
| CHECK(x962_bytes); |
| base::span<const uint8_t> x962_span = ToSpan(x962_bytes.get()); |
| public_key_spki_ = *Convertx963ToDerSpki(x962_span); |
| } |
| |
| ~UnexportableSigningKeyMac() override = default; |
| |
| SignatureVerifier::SignatureAlgorithm Algorithm() const override { |
| return SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256; |
| } |
| |
| std::vector<uint8_t> GetSubjectPublicKeyInfo() const override { |
| return public_key_spki_; |
| } |
| |
| std::vector<uint8_t> GetWrappedKey() const override { |
| return application_label_; |
| } |
| |
| std::optional<std::vector<uint8_t>> SignSlowly( |
| base::span<const uint8_t> data) override { |
| SecKeyAlgorithm algorithm = kSecKeyAlgorithmECDSASignatureMessageX962SHA256; |
| if (!SecKeyIsAlgorithmSupported(key_.get(), kSecKeyOperationTypeSign, |
| algorithm)) { |
| // This is not expected to happen, but it could happen if e.g. the key had |
| // been replaced by a key of a different type with the same label. |
| LOG(ERROR) << "Key does not support ECDSA algorithm"; |
| return std::nullopt; |
| } |
| |
| NSData* nsdata = [NSData dataWithBytes:data.data() length:data.size()]; |
| base::apple::ScopedCFTypeRef<CFErrorRef> error; |
| base::apple::ScopedCFTypeRef<CFDataRef> signature( |
| AppleKeychainV2::GetInstance().KeyCreateSignature( |
| key_.get(), algorithm, NSToCFPtrCast(nsdata), |
| error.InitializeInto())); |
| if (!signature) { |
| LOG(ERROR) << "Error signing with key: " << error.get(); |
| return std::nullopt; |
| } |
| return CFDataToVec(signature.get()); |
| } |
| |
| SecKeyRef GetSecKeyRef() const override { return key_.get(); } |
| |
| private: |
| // The wrapped key as returned by the Keychain API. |
| const base::apple::ScopedCFTypeRef<SecKeyRef> key_; |
| |
| // The MacOS Keychain API sets the application label to the hash of the public |
| // key. We use this to uniquely identify the key in lieu of a wrapped private |
| // key. |
| const std::vector<uint8_t> application_label_; |
| |
| // The public key in DER SPKI format. |
| std::vector<uint8_t> public_key_spki_; |
| }; |
| |
| } // namespace |
| |
| 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::kUserPresenceOrWatch: |
| control_flags |= kSecAccessControlOr | kSecAccessControlBiometryAny | |
| kSecAccessControlDevicePasscode | kSecAccessControlWatch; |
| break; |
| case UnexportableKeyProvider::Config::AccessControl::kNone: |
| // No additional flag. |
| break; |
| } |
| base::apple::ScopedCFTypeRef<SecAccessControlRef> access( |
| SecAccessControlCreateWithFlags( |
| kCFAllocatorDefault, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, |
| control_flags, |
| /*error=*/nil)); |
| CHECK(access); |
| |
| 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::DeleteSigningKeySlowly( |
| base::span<const uint8_t> wrapped_key) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::WILL_BLOCK); |
| 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; |
| } |
| CHECK(!config.keychain_access_group.empty()) |
| << "A keychain access group must be set when using unexportable keys on " |
| "macOS"; |
| if (![AppleKeychainV2::GetInstance().GetTokenIDs() |
| containsObject:CFToNSPtrCast(kSecAttrTokenIDSecureEnclave)]) { |
| return nullptr; |
| } |
| // Inspecting the binary for the entitlement is not available on iOS, assume |
| // it is available. |
| #if !BUILDFLAG(IS_IOS) |
| if (!ExecutableHasKeychainAccessGroupEntitlement( |
| config.keychain_access_group)) { |
| return nullptr; |
| } |
| #endif // !BUILDFLAG(IS_IOS) |
| return std::make_unique<UnexportableKeyProviderMac>(std::move(config)); |
| } |
| |
| } // namespace crypto |