LocalAuthenticator.mm [plain text]
/*
* Copyright (C) 2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "LocalAuthenticator.h"
#if ENABLE(WEB_AUTHN)
#import "CBORWriter.h"
#import "COSEConstants.h"
#import "ExceptionData.h"
#import "PublicKeyCredentialCreationOptions.h"
#import "PublicKeyCredentialRequestOptions.h"
#import <Security/SecItem.h>
#import <pal/crypto/CryptoDigest.h>
#import <pal/spi/cocoa/DeviceIdentitySPI.h>
#import <wtf/BlockPtr.h>
#import <wtf/HashSet.h>
#import <wtf/MainThread.h>
#import <wtf/ProcessPrivilege.h>
#import <wtf/RetainPtr.h>
#import <wtf/Vector.h>
#import <wtf/text/StringHash.h>
#import "LocalAuthenticationSoftLink.h"
namespace WebCore {
namespace LocalAuthenticatorInternal {
// See https://www.w3.org/TR/webauthn/#flags.
const uint8_t makeCredentialFlags = 0b01000101; // UP, UV and AT are set.
const uint8_t getAssertionFlags = 0b00000101; // UP and UV are set.
// FIXME(rdar://problem/38320512): Define Apple AAGUID.
const uint8_t AAGUID[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // 16 bytes
// Credential ID is currently SHA-1 of the corresponding public key.
// FIXME(183534): Assume little endian here.
const union {
uint16_t integer;
uint8_t bytes[2];
} credentialIdLength = {0x0014};
const size_t ES256KeySizeInBytes = 32;
const size_t authDataPrefixFixedSize = 37; // hash(32) + flags(1) + counter(4)
#if PLATFORM(IOS)
// https://www.w3.org/TR/webauthn/#sec-authenticator-data
static Vector<uint8_t> buildAuthData(const String& rpId, const uint8_t flags, const uint32_t counter, const Vector<uint8_t>& optionalAttestedCredentialData)
{
Vector<uint8_t> authData;
authData.reserveCapacity(authDataPrefixFixedSize + optionalAttestedCredentialData.size());
// RP ID hash
auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
// FIXME(183534): Test IDN.
ASSERT(rpId.isAllASCII());
auto asciiRpId = rpId.ascii();
crypto->addBytes(asciiRpId.data(), asciiRpId.length());
authData = crypto->computeHash();
// FLAGS
authData.append(flags);
// COUNTER
// FIXME(183534): Assume little endian here.
union {
uint32_t integer;
uint8_t bytes[4];
} counterUnion;
counterUnion.integer = counter;
authData.append(counterUnion.bytes, sizeof(counterUnion.bytes));
// ATTESTED CRED. DATA
authData.appendVector(optionalAttestedCredentialData);
return authData;
}
inline HashSet<String> produceHashSet(const Vector<PublicKeyCredentialDescriptor>& credentialDescriptors)
{
HashSet<String> result;
for (auto& credentialDescriptor : credentialDescriptors) {
if (credentialDescriptor.transports.isEmpty() && credentialDescriptor.type == PublicKeyCredentialType::PublicKey && credentialDescriptor.idVector.size() == credentialIdLength.integer)
result.add(String(reinterpret_cast<const char*>(credentialDescriptor.idVector.data()), credentialDescriptor.idVector.size()));
}
return result;
}
#endif // !PLATFORM(IOS)
} // LocalAuthenticatorInternal
LocalAuthenticator::LocalAuthenticator()
{
RELEASE_ASSERT(hasProcessPrivilege(ProcessPrivilege::CanAccessCredentials));
}
void LocalAuthenticator::makeCredential(const Vector<uint8_t>& hash, const PublicKeyCredentialCreationOptions& options, CreationCallback&& callback, ExceptionCallback&& exceptionCallback)
{
using namespace LocalAuthenticatorInternal;
#if !PLATFORM(IOS)
// FIXME(182772)
ASSERT_UNUSED(hash, hash == hash);
ASSERT_UNUSED(options, !options.rp.id.isEmpty());
ASSERT_UNUSED(callback, callback);
exceptionCallback({ NotAllowedError, "No avaliable authenticators."_s });
#else
// The following implements https://www.w3.org/TR/webauthn/#op-make-cred as of 5 December 2017.
// Skip Step 4-5 as requireResidentKey and requireUserVerification are enforced.
// Skip Step 9 as extensions are not supported yet.
// Step 8 is implicitly captured by all UnknownError exception callbacks.
// Step 2.
bool canFullfillPubKeyCredParams = false;
for (auto& pubKeyCredParam : options.pubKeyCredParams) {
if (pubKeyCredParam.type == PublicKeyCredentialType::PublicKey && pubKeyCredParam.alg == COSE::ES256) {
canFullfillPubKeyCredParams = true;
break;
}
}
if (!canFullfillPubKeyCredParams) {
exceptionCallback({ NotSupportedError, "The platform attached authenticator doesn't support any provided PublicKeyCredentialParameters."_s });
return;
}
// Step 3.
HashSet<String> excludeCredentialIds = produceHashSet(options.excludeCredentials);
if (!excludeCredentialIds.isEmpty()) {
// Search Keychain for the RP ID.
NSDictionary *query = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
(id)kSecAttrLabel: options.rp.id,
(id)kSecReturnAttributes: @YES,
(id)kSecMatchLimit: (id)kSecMatchLimitAll,
};
CFTypeRef attributesArrayRef = nullptr;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesArrayRef);
if (status && status != errSecItemNotFound) {
LOG_ERROR("Couldn't query Keychain: %d", status);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
auto retainAttributesArray = adoptCF(attributesArrayRef);
for (NSDictionary *nsAttributes in (NSArray *)attributesArrayRef) {
NSData *nsCredentialId = nsAttributes[(id)kSecAttrApplicationLabel];
if (excludeCredentialIds.contains(String(reinterpret_cast<const char*>(nsCredentialId.bytes), nsCredentialId.length))) {
exceptionCallback({ NotAllowedError, "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator."_s });
return;
}
}
}
// Step 6.
// FIXME(rdar://problem/35900593): Update to a formal UI.
// Get user consent.
auto context = adoptNS([allocLAContextInstance() init]);
NSError *error = nil;
if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
LOG_ERROR("Couldn't evaluate authentication with biometrics policy: %@", error);
// FIXME(182767)
exceptionCallback({ NotAllowedError, "No avaliable authenticators."_s });
return;
}
NSString *reason = [NSString stringWithFormat:@"Allow %@ to create a public key credential for %@", (id)options.rp.id, (id)options.user.name];
// FIXME(183534): Optimize the following nested callbacks and threading.
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:reason reply:BlockPtr<void(BOOL, NSError *)>::fromCallable([weakThis = makeWeakPtr(*this), callback = WTFMove(callback), exceptionCallback = WTFMove(exceptionCallback), options = crossThreadCopy(options), hash] (BOOL success, NSError *error) mutable {
ASSERT(!isMainThread());
if (!success || error) {
LOG_ERROR("Couldn't authenticate with biometrics: %@", error);
exceptionCallback({ NotAllowedError, "Couldn't get user consent."_s });
return;
}
// Step 7.5.
// Userhandle is stored in kSecAttrApplicationTag attribute.
// Failures after this point could block users' accounts forever. Should we follow the spec?
NSDictionary* deleteQuery = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrLabel: options.rp.id,
(id)kSecAttrApplicationTag: [NSData dataWithBytes:options.user.idVector.data() length:options.user.idVector.size()],
};
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
if (status && status != errSecItemNotFound) {
LOG_ERROR("Couldn't detele older credential: %d", status);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
// Step 7.1, 13. Apple Attestation
// FIXME(183534)
if (!weakThis)
return;
weakThis->issueClientCertificate(options.rp.id, options.user.name, hash, BlockPtr<void(SecKeyRef, NSArray *, NSError *)>::fromCallable([callback = WTFMove(callback), exceptionCallback = WTFMove(exceptionCallback), options = crossThreadCopy(options)] (_Nullable SecKeyRef privateKey, NSArray * _Nullable certificates, NSError * _Nullable error) {
ASSERT(!isMainThread());
if (error) {
LOG_ERROR("Couldn't attest: %@", error);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
// Attestation Certificate and Attestation Issuing CA
ASSERT(certificates && ([certificates count] == 2));
// Step 7.2-7.4.
// FIXME(183533): A single kSecClassKey item couldn't store all meta data. The following schema is a tentative solution
// to accommodate the most important meta data, i.e. RP ID, Credential ID, and userhandle.
// kSecAttrLabel: RP ID
// kSecAttrApplicationLabel: Credential ID (auto-gen by Keychain)
// kSecAttrApplicationTag: userhandle
// Noted, the current DeviceIdentity.Framework would only allow us to pass the kSecAttrLabel as the inital attribute
// for the Keychain item. Since that's the only clue we have to locate the unique item, we use the pattern username@rp.id
// as the initial value.
// Also noted, the vale of kSecAttrApplicationLabel is automatically generated by the Keychain, which is a SHA-1 hash of
// the public key. We borrow it directly for now to workaround the stated limitations.
// Update the Keychain item to the above schema.
// FIXME(183533): DeviceIdentity.Framework would insert certificates into Keychain as well. We should update those as well.
Vector<uint8_t> credentialId;
{
String label(options.user.name);
label.append("@" + options.rp.id + "-rk"); // -rk is added by DeviceIdentity.Framework.
NSDictionary *credentialIdQuery = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
(id)kSecAttrLabel: label,
(id)kSecReturnAttributes: @YES
};
CFTypeRef attributesRef = nullptr;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)credentialIdQuery, &attributesRef);
if (status) {
LOG_ERROR("Couldn't get Credential ID: %d", status);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
auto retainAttributes = adoptCF(attributesRef);
NSDictionary *nsAttributes = (NSDictionary *)attributesRef;
NSData *nsCredentialId = nsAttributes[(id)kSecAttrApplicationLabel];
credentialId.append(reinterpret_cast<const uint8_t*>(nsCredentialId.bytes), nsCredentialId.length);
NSDictionary *updateQuery = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
(id)kSecAttrApplicationLabel: nsCredentialId,
};
NSDictionary *updateParams = @{
(id)kSecAttrLabel: options.rp.id,
(id)kSecAttrApplicationTag: [NSData dataWithBytes:options.user.idVector.data() length:options.user.idVector.size()],
};
status = SecItemUpdate((__bridge CFDictionaryRef)updateQuery, (__bridge CFDictionaryRef)updateParams);
if (status) {
LOG_ERROR("Couldn't update the Keychain item: %d", status);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
}
// Step 10.
// FIXME(183533): store the counter.
uint32_t counter = 0;
// FIXME(183534): attestedCredentialData could throttle.
// Step 11. https://www.w3.org/TR/webauthn/#attested-credential-data
Vector<uint8_t> attestedCredentialData;
{
// aaguid
attestedCredentialData.append(AAGUID, sizeof(AAGUID));
// credentialIdLength
ASSERT(credentialId.size() == credentialIdLength.integer);
// FIXME(183534): Assume little endian here.
attestedCredentialData.append(credentialIdLength.bytes, sizeof(uint16_t));
// credentialId
attestedCredentialData.appendVector(credentialId);
// credentialPublicKey
RetainPtr<CFDataRef> publicKeyDataRef;
{
auto publicKey = adoptCF(SecKeyCopyPublicKey(privateKey));
CFErrorRef errorRef = nullptr;
publicKeyDataRef = adoptCF(SecKeyCopyExternalRepresentation(publicKey.get(), &errorRef));
auto retainError = adoptCF(errorRef);
if (errorRef) {
LOG_ERROR("Couldn't export the public key: %@", (NSError*)errorRef);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
ASSERT(((NSData *)publicKeyDataRef.get()).length == (1 + 2 * ES256KeySizeInBytes)); // 04 | X | Y
}
// COSE Encoding
// FIXME(183535): Improve CBOR encoder to work with bytes directly.
Vector<uint8_t> x(ES256KeySizeInBytes);
[(NSData *)publicKeyDataRef.get() getBytes: x.data() range:NSMakeRange(1, ES256KeySizeInBytes)];
Vector<uint8_t> y(ES256KeySizeInBytes);
[(NSData *)publicKeyDataRef.get() getBytes: y.data() range:NSMakeRange(1 + ES256KeySizeInBytes, ES256KeySizeInBytes)];
cbor::CBORValue::MapValue publicKeyMap;
publicKeyMap[cbor::CBORValue(COSE::kty)] = cbor::CBORValue(COSE::EC2);
publicKeyMap[cbor::CBORValue(COSE::alg)] = cbor::CBORValue(COSE::ES256);
publicKeyMap[cbor::CBORValue(COSE::crv)] = cbor::CBORValue(COSE::P_256);
publicKeyMap[cbor::CBORValue(COSE::x)] = cbor::CBORValue(WTFMove(x));
publicKeyMap[cbor::CBORValue(COSE::y)] = cbor::CBORValue(WTFMove(y));
auto cosePublicKey = cbor::CBORWriter::write(cbor::CBORValue(WTFMove(publicKeyMap)));
if (!cosePublicKey) {
LOG_ERROR("Couldn't encode the public key into COSE binaries.");
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
attestedCredentialData.appendVector(cosePublicKey.value());
}
// Step 12.
auto authData = buildAuthData(options.rp.id, makeCredentialFlags, counter, attestedCredentialData);
// Step 13. Apple Attestation Cont'
// Assemble the attestation object:
// https://www.w3.org/TR/webauthn/#attestation-object
cbor::CBORValue::MapValue attestationStatementMap;
{
Vector<uint8_t> signature;
{
CFErrorRef errorRef = nullptr;
// FIXME(183652): Reduce prompt for biometrics
auto signatureRef = adoptCF(SecKeyCreateSignature(privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (__bridge CFDataRef)[NSData dataWithBytes:authData.data() length:authData.size()], &errorRef));
auto retainError = adoptCF(errorRef);
if (errorRef) {
LOG_ERROR("Couldn't generate the signature: %@", (NSError*)errorRef);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
auto nsSignature = (NSData *)signatureRef.get();
signature.append(reinterpret_cast<const uint8_t*>(nsSignature.bytes), nsSignature.length);
}
attestationStatementMap[cbor::CBORValue("alg")] = cbor::CBORValue(COSE::ES256);
attestationStatementMap[cbor::CBORValue("sig")] = cbor::CBORValue(signature);
Vector<cbor::CBORValue> cborArray;
for (size_t i = 0; i < [certificates count]; i++) {
CFDataRef dataRef = SecCertificateCopyData((__bridge SecCertificateRef)certificates[i]);
auto retainData = adoptCF(dataRef);
NSData *nsData = (NSData *)dataRef;
Vector<uint8_t> data;
data.append(reinterpret_cast<const uint8_t*>(nsData.bytes), nsData.length);
cborArray.append(cbor::CBORValue(WTFMove(data)));
}
attestationStatementMap[cbor::CBORValue("x5c")] = cbor::CBORValue(WTFMove(cborArray));
}
cbor::CBORValue::MapValue attestationObjectMap;
attestationObjectMap[cbor::CBORValue("authData")] = cbor::CBORValue(authData);
attestationObjectMap[cbor::CBORValue("fmt")] = cbor::CBORValue("Apple");
attestationObjectMap[cbor::CBORValue("attStmt")] = cbor::CBORValue(WTFMove(attestationStatementMap));
auto attestationObject = cbor::CBORWriter::write(cbor::CBORValue(WTFMove(attestationObjectMap)));
if (!attestationObject) {
LOG_ERROR("Couldn't encode the attestation object.");
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
callback(credentialId, attestationObject.value());
}).get());
}).get()];
#endif // !PLATFORM(IOS)
}
void LocalAuthenticator::getAssertion(const Vector<uint8_t>& hash, const PublicKeyCredentialRequestOptions& options, RequestCallback&& callback, ExceptionCallback&& exceptionCallback)
{
using namespace LocalAuthenticatorInternal;
#if !PLATFORM(IOS)
// FIXME(182772)
ASSERT_UNUSED(hash, hash == hash);
ASSERT_UNUSED(options, !options.rpId.isEmpty());
ASSERT_UNUSED(callback, callback);
exceptionCallback({ NotAllowedError, "No avaliable authenticators."_s });
#else
// The following implements https://www.w3.org/TR/webauthn/#op-get-assertion as of 5 December 2017.
// Skip Step 2 as requireUserVerification is enforced.
// Skip Step 8 as extensions are not supported yet.
// Step 12 is implicitly captured by all UnknownError exception callbacks.
// Step 3-5. Unlike the spec, if an allow list is provided and there is no intersection between existing ones and the allow list, we always return NotAllowedError.
HashSet<String> allowCredentialIds = produceHashSet(options.allowCredentials);
if (!options.allowCredentials.isEmpty() && allowCredentialIds.isEmpty()) {
exceptionCallback({ NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s });
return;
}
// Search Keychain for the RP ID.
NSDictionary *query = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
(id)kSecAttrLabel: options.rpId,
(id)kSecReturnAttributes: @YES,
(id)kSecMatchLimit: (id)kSecMatchLimitAll,
};
CFTypeRef attributesArrayRef = nullptr;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesArrayRef);
if (status && status != errSecItemNotFound) {
LOG_ERROR("Couldn't query Keychain: %d", status);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
auto retainAttributesArray = adoptCF(attributesArrayRef);
NSArray *intersectedCredentialsAttributes = nil;
if (options.allowCredentials.isEmpty())
intersectedCredentialsAttributes = (NSArray *)attributesArrayRef;
else {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:allowCredentialIds.size()];
for (NSDictionary *nsAttributes in (NSArray *)attributesArrayRef) {
NSData *nsCredentialId = nsAttributes[(id)kSecAttrApplicationLabel];
if (allowCredentialIds.contains(String(reinterpret_cast<const char*>(nsCredentialId.bytes), nsCredentialId.length)))
[result addObject:nsAttributes];
}
intersectedCredentialsAttributes = result;
}
if (!intersectedCredentialsAttributes.count) {
exceptionCallback({ NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s });
return;
}
// Step 6.
// FIXME(rdar://problem/35900534): We don't have an UI to prompt users for selecting intersectedCredentials, and therefore we always use the first one for now.
NSDictionary *selectedCredentialAttributes = intersectedCredentialsAttributes[0];
// Step 7. Get user consent.
// FIXME(183534): The lifetime of context is managed by reply and the early return, which is a bit subtle.
LAContext *context = [allocLAContextInstance() init];
NSError *error = nil;
if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
auto retainContext = adoptNS(context);
LOG_ERROR("Couldn't evaluate authentication with biometrics policy: %@", error);
// FIXME(182767)
exceptionCallback({ NotAllowedError, "No avaliable authenticators."_s });
return;
}
Vector<uint8_t> credentialId;
NSData *nsCredentialId = selectedCredentialAttributes[(id)kSecAttrApplicationLabel];
credentialId.append(reinterpret_cast<const uint8_t*>(nsCredentialId.bytes), nsCredentialId.length);
Vector<uint8_t> userhandle;
NSData *nsUserhandle = selectedCredentialAttributes[(id)kSecAttrApplicationTag];
userhandle.append(reinterpret_cast<const uint8_t*>(nsUserhandle.bytes), nsUserhandle.length);
auto reply = BlockPtr<void(BOOL, NSError *)>::fromCallable([callback = WTFMove(callback), exceptionCallback = WTFMove(exceptionCallback), rpId = options.rpId.isolatedCopy(), hash, credentialId = WTFMove(credentialId), userhandle = WTFMove(userhandle), context = adoptNS(context)] (BOOL success, NSError *error) mutable {
ASSERT(!isMainThread());
if (!success || error) {
LOG_ERROR("Couldn't authenticate with biometrics: %@", error);
exceptionCallback({ NotAllowedError, "Couldn't get user consent."_s });
return;
}
// Step 9-10.
// FIXME(183533): Due to the stated Keychain limitations, we can't save the counter value.
// Therefore, it is always zero.
uint32_t counter = 0;
auto authData = buildAuthData(rpId, getAssertionFlags, counter, { });
// Step 11.
Vector<uint8_t> signature;
{
NSDictionary *query = @{
(id)kSecClass: (id)kSecClassKey,
(id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
(id)kSecAttrApplicationLabel: [NSData dataWithBytes:credentialId.data() length:credentialId.size()],
(id)kSecUseAuthenticationContext: context.get(),
(id)kSecReturnRef: @YES,
};
CFTypeRef privateKeyRef = nullptr;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &privateKeyRef);
if (status) {
LOG_ERROR("Couldn't get the private key reference: %d", status);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
auto privateKey = adoptCF(privateKeyRef);
NSMutableData *dataToSign = [NSMutableData dataWithBytes:authData.data() length:authData.size()];
[dataToSign appendBytes:hash.data() length:hash.size()];
CFErrorRef errorRef = nullptr;
// FIXME: Converting CFTypeRef to SecKeyRef is quite subtle here.
auto signatureRef = adoptCF(SecKeyCreateSignature((__bridge SecKeyRef)((id)privateKeyRef), kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (__bridge CFDataRef)dataToSign, &errorRef));
auto retainError = adoptCF(errorRef);
if (errorRef) {
LOG_ERROR("Couldn't generate the signature: %@", (NSError*)errorRef);
exceptionCallback({ UnknownError, "Unknown internal error."_s });
return;
}
auto nsSignature = (NSData *)signatureRef.get();
signature.append(reinterpret_cast<const uint8_t*>(nsSignature.bytes), nsSignature.length);
}
// Step 13.
callback(credentialId, authData, signature, userhandle);
});
// FIXME(183533): Use userhandle instead of username due to the stated Keychain limitations.
NSString *reason = [NSString stringWithFormat:@"Log into %@ with %@.", (id)options.rpId, selectedCredentialAttributes[(id)kSecAttrApplicationTag]];
[context evaluateAccessControl:(__bridge SecAccessControlRef)selectedCredentialAttributes[(id)kSecAttrAccessControl] operation:LAAccessControlOperationUseKeySign localizedReason:reason reply:reply.get()];
#endif // !PLATFORM(IOS)
}
bool LocalAuthenticator::isAvailable() const
{
#if !PLATFORM(IOS)
// FIXME(182772)
return false;
#else
auto context = adoptNS([allocLAContextInstance() init]);
NSError *error = nil;
if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
LOG_ERROR("Couldn't evaluate authentication with biometrics policy: %@", error);
return true;
}
return true;
#endif // !PLATFORM(IOS)
}
void LocalAuthenticator::issueClientCertificate(const String& rpId, const String& username, const Vector<uint8_t>& hash, CompletionBlock _Nonnull completionHandler) const
{
// DeviceIdentity.Framework is not avaliable in iOS simulator.
#if !PLATFORM(IOS) || PLATFORM(IOS_SIMULATOR)
// FIXME(182772)
ASSERT_UNUSED(rpId, !rpId.isEmpty());
ASSERT_UNUSED(username, !username.isEmpty());
ASSERT_UNUSED(hash, !hash.isEmpty());
completionHandler(NULL, NULL, [NSError errorWithDomain:@"com.apple.WebKit.WebAuthN" code:1 userInfo:nil]);
#else
// Apple Attestation
ASSERT(hash.size() <= 32);
RetainPtr<SecAccessControlRef> accessControlRef;
{
CFErrorRef errorRef = nullptr;
accessControlRef = adoptCF(SecAccessControlCreateWithFlags(NULL, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence, &errorRef));
auto retainError = adoptCF(errorRef);
if (errorRef) {
LOG_ERROR("Couldn't create ACL: %@", (NSError *)errorRef);
completionHandler(NULL, NULL, [NSError errorWithDomain:@"com.apple.WebKit.WebAuthN" code:1 userInfo:nil]);
return;
}
}
String label(username);
label.append("@" + rpId);
NSDictionary *options = @{
kMAOptionsBAAKeychainLabel: label,
// FIXME(rdar://problem/38489134): Need a formal name.
kMAOptionsBAAKeychainAccessGroup: @"com.apple.safari.WebAuthN.credentials",
kMAOptionsBAAIgnoreExistingKeychainItems: @(YES),
// FIXME(rdar://problem/38489134): Determine a proper lifespan.
kMAOptionsBAAValidity: @(1440), // Last one day.
kMAOptionsBAASCRTAttestation: @(NO),
kMAOptionsBAANonce: [NSData dataWithBytes:hash.data() length:hash.size()],
kMAOptionsBAAAccessControls: (id)accessControlRef.get(),
kMAOptionsBAAOIDSToInclude: @[kMAOptionsBAAOIDNonce]
};
// FIXME(183652): Reduce prompt for biometrics
DeviceIdentityIssueClientCertificateWithCompletion(NULL, options, completionHandler);
#endif
}
} // namespace WebCore
#endif // ENABLE(WEB_AUTHN)