/* * Copyright (C) 2018-2022 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 "AuthenticationServicesCoreSoftLink.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #if USE(APPLE_INTERNAL_SDK) #import #else static void updateQueryIfNecessary(NSMutableDictionary *) { } static inline String groupForAttributes(NSDictionary *attributes) { return nullString(); } static bool shouldUpdateQuery() { return false; } #endif namespace WebKit { using namespace fido; using namespace WebCore; using namespace WebAuthn; using CBOR = cbor::CBORValue; namespace LocalAuthenticatorInternal { // Credential ID is currently SHA-1 of the corresponding public key. const uint16_t credentialIdLength = 20; const uint64_t counter = 0; const uint8_t aaguid[] = { 0xF2, 0x4A, 0x8E, 0x70, 0xD0, 0xD3, 0xF8, 0x2C, 0x29, 0x37, 0x32, 0x52, 0x3C, 0xC4, 0xDE, 0x5A }; // Randomly generated. static inline bool emptyTransportsOrContain(const Vector& transports, AuthenticatorTransport target) { return transports.isEmpty() ? true : transports.contains(target); } // A Base64 encoded string of the Credential ID is used as the key of the hash set. static inline HashSet produceHashSet(const Vector& credentialDescriptors) { HashSet result; for (auto& credentialDescriptor : credentialDescriptors) { if (emptyTransportsOrContain(credentialDescriptor.transports, AuthenticatorTransport::Internal) && credentialDescriptor.type == PublicKeyCredentialType::PublicKey && credentialDescriptor.id.length() == credentialIdLength) result.add(base64EncodeToString(credentialDescriptor.id.data(), credentialDescriptor.id.length())); } return result; } static inline uint8_t authDataFlags(ClientDataType type, LocalConnection::UserVerification verification, bool synchronizable) { auto flags = userPresenceFlag; if (verification != LocalConnection::UserVerification::Presence) flags |= userVerifiedFlag; if (type == ClientDataType::Create) flags |= attestedCredentialDataIncludedFlag; if (synchronizable) flags |= backupEligibilityFlag | backupStateFlag; return flags; } static inline Vector aaguidVector() { static NeverDestroyed> aaguidVector = { aaguid, aaguidLength }; return aaguidVector; } static inline RetainPtr toNSData(const Vector& data) { return adoptNS([[NSData alloc] initWithBytes:data.data() length:data.size()]); } static inline RetainPtr toNSData(ArrayBuffer* buffer) { ASSERT(buffer); return adoptNS([[NSData alloc] initWithBytes:buffer->data() length:buffer->byteLength()]); } static inline Ref toArrayBuffer(NSData *data) { return ArrayBuffer::create(reinterpret_cast(data.bytes), data.length); } static inline Ref toArrayBuffer(const Vector& data) { return ArrayBuffer::create(data.data(), data.size()); } static std::optional>> getExistingCredentials(const String& rpId) { // Search Keychain for existing credential matched the RP ID. NSDictionary *query = @{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, (id)kSecAttrSynchronizable: (id)kSecAttrSynchronizableAny, (id)kSecAttrLabel: rpId, (id)kSecReturnAttributes: @YES, (id)kSecMatchLimit: (id)kSecMatchLimitAll, (id)kSecUseDataProtectionKeychain: @YES }; CFTypeRef attributesArrayRef = nullptr; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesArrayRef); if (status && status != errSecItemNotFound) return std::nullopt; auto retainAttributesArray = adoptCF(attributesArrayRef); NSArray *sortedAttributesArray = [(NSArray *)attributesArrayRef sortedArrayUsingComparator:^(NSDictionary *a, NSDictionary *b) { return [b[(id)kSecAttrModificationDate] compare:a[(id)kSecAttrModificationDate]]; }]; Vector> result; result.reserveInitialCapacity(sortedAttributesArray.count); for (NSDictionary *attributes in sortedAttributesArray) { auto decodedResponse = cbor::CBORReader::read(vectorFromNSData(attributes[(id)kSecAttrApplicationTag])); if (!decodedResponse || !decodedResponse->isMap()) { ASSERT_NOT_REACHED(); return std::nullopt; } auto& responseMap = decodedResponse->getMap(); RefPtr userHandle; auto it = responseMap.find(CBOR(fido::kEntityIdMapKey)); if (it != responseMap.end() && it->second.isByteString()) { userHandle = toArrayBuffer(it->second.getByteString()); } it = responseMap.find(CBOR(fido::kEntityNameMapKey)); if (it == responseMap.end() || !it->second.isString()) { ASSERT_NOT_REACHED(); return std::nullopt; } auto& username = it->second.getString(); auto response = AuthenticatorAssertionResponse::create(toArrayBuffer(attributes[(id)kSecAttrApplicationLabel]), WTFMove(userHandle), String(username), (__bridge SecAccessControlRef)attributes[(id)kSecAttrAccessControl], AuthenticatorAttachment::Platform); auto group = groupForAttributes(attributes); if (!group.isNull()) response->setGroup(group); if ([[attributes allKeys] containsObject:bridge_cast(kSecAttrSynchronizable)]) response->setSynchronizable([attributes[(id)kSecAttrSynchronizable] isEqual:@YES]); it = responseMap.find(CBOR(fido::kDisplayNameMapKey)); if (it != responseMap.end() && it->second.isString()) response->setDisplayName(it->second.getString()); result.uncheckedAppend(WTFMove(response)); } return result; } static Vector transports() { Vector transports = { WebCore::AuthenticatorTransport::Internal }; if (shouldUpdateQuery()) transports.append(WebCore::AuthenticatorTransport::Hybrid); return transports; } } // LocalAuthenticatorInternal void LocalAuthenticator::clearAllCredentials() { // FIXME: We should guard the method with a first party entitlement once WebAuthn is avaliable for third parties. auto query = adoptNS([[NSMutableDictionary alloc] init]); [query setDictionary:@{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrAccessGroup: @(LocalAuthenticatorAccessGroup), (id)kSecUseDataProtectionKeychain: @YES }]; updateQueryIfNecessary(query.get()); OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query.get()); if (status && status != errSecItemNotFound) LOG_ERROR(makeString("Couldn't clear all credential: "_s, status).utf8().data()); } LocalAuthenticator::LocalAuthenticator(UniqueRef&& connection) : m_connection(WTFMove(connection)) { } void LocalAuthenticator::makeCredential() { using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::Init); m_state = State::RequestReceived; auto& creationOptions = std::get(requestData().options); // 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 receiveResponds. // Skip Step 10 as counter is constantly 0. // Step 2. if (notFound == creationOptions.pubKeyCredParams.findIf([] (auto& pubKeyCredParam) { return pubKeyCredParam.type == PublicKeyCredentialType::PublicKey && pubKeyCredParam.alg == COSE::ES256; })) { receiveException({ NotSupportedError, "The platform attached authenticator doesn't support any provided PublicKeyCredentialParameters."_s }); return; } // Step 3. auto existingCredentials = getExistingCredentials(creationOptions.rp.id); if (!existingCredentials) { receiveException({ UnknownError, makeString("Couldn't get existing credentials") }); return; } m_existingCredentials = WTFMove(*existingCredentials); auto excludeCredentialIds = produceHashSet(creationOptions.excludeCredentials); if (!excludeCredentialIds.isEmpty()) { if (notFound != m_existingCredentials.findIf([&excludeCredentialIds] (auto& credential) { auto* rawId = credential->rawId(); ASSERT(rawId); return excludeCredentialIds.contains(base64EncodeToString(rawId->data(), rawId->byteLength())); })) { receiveException({ InvalidStateError, "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator."_s }, WebAuthenticationStatus::LAExcludeCredentialsMatched); return; } } if (auto* observer = this->observer()) { auto callback = [weakThis = WeakPtr { *this }] (LAContext *context) { ASSERT(RunLoop::isMain()); if (!weakThis) return; weakThis->continueMakeCredentialAfterReceivingLAContext(context); }; observer->requestLAContextForUserVerification(WTFMove(callback)); } } void LocalAuthenticator::continueMakeCredentialAfterReceivingLAContext(LAContext *context) { ASSERT(m_state == State::RequestReceived); m_state = State::PolicyDecided; RetainPtr accessControl; { CFErrorRef errorRef = nullptr; accessControl = adoptCF(SecAccessControlCreateWithFlags(NULL, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence, &errorRef)); auto retainError = adoptCF(errorRef); if (errorRef) { receiveException({ UnknownError, makeString("Couldn't create access control: ", String(((NSError*)errorRef).localizedDescription)) }); return; } } SecAccessControlRef accessControlRef = accessControl.get(); auto callback = [accessControl = WTFMove(accessControl), context = retainPtr(context), weakThis = WeakPtr { *this }] (LocalConnection::UserVerification verification) { ASSERT(RunLoop::isMain()); if (!weakThis) return; weakThis->continueMakeCredentialAfterUserVerification(accessControl.get(), verification, context.get()); }; m_connection->verifyUser(accessControlRef, context, WTFMove(callback)); } void LocalAuthenticator::processClientExtensions(std::variant, Ref> response) { WTF::switchOn(response, [&](const Ref& response) { auto& creationOptions = std::get(requestData().options); if (creationOptions.extensions && creationOptions.extensions->credProps) { auto extensionOutputs = response->extensions(); extensionOutputs.credProps = AuthenticationExtensionsClientOutputs::CredentialPropertiesOutput { true /* rk */ }; response->setExtensions(WTFMove(extensionOutputs)); } }, [&](const Ref& options) { }); } void LocalAuthenticator::continueMakeCredentialAfterUserVerification(SecAccessControlRef accessControlRef, LocalConnection::UserVerification verification, LAContext *context) { using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::PolicyDecided); m_state = State::UserVerified; auto& creationOptions = std::get(requestData().options); if (!validateUserVerification(verification)) return; // Here is the keychain schema. // kSecAttrLabel: RP ID // kSecAttrApplicationLabel: Credential ID (auto-gen by Keychain) // kSecAttrApplicationTag: { "id": UserEntity.id, "name": UserEntity.name, "displayName": UserEntity.name} (CBOR encoded) // Noted, the vale of kSecAttrApplicationLabel is automatically generated by the Keychain, which is a SHA-1 hash of // the public key. const auto& secAttrLabel = creationOptions.rp.id; // id, name, and displayName are required in PublicKeyCredentialUserEntity // https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialuserentity cbor::CBORValue::MapValue userEntityMap; userEntityMap[cbor::CBORValue(fido::kEntityIdMapKey)] = cbor::CBORValue(creationOptions.user.id); userEntityMap[cbor::CBORValue(fido::kEntityNameMapKey)] = cbor::CBORValue(creationOptions.user.name); userEntityMap[cbor::CBORValue(fido::kDisplayNameMapKey)] = cbor::CBORValue(creationOptions.user.displayName); userEntityMap[cbor::CBORValue(userEntityLastModifiedKey)] = cbor::CBORValue((int64_t)WallTime::now().secondsSinceEpoch().value()); auto userEntity = cbor::CBORWriter::write(cbor::CBORValue(WTFMove(userEntityMap))); ASSERT(userEntity); auto secAttrApplicationTag = toNSData(*userEntity); // Step 7. // The above-to-create private key will be inserted into keychain while using SEP. auto privateKey = m_connection->createCredentialPrivateKey(context, accessControlRef, secAttrLabel, secAttrApplicationTag.get()); if (!privateKey) { receiveException({ UnknownError, "Couldn't create private key."_s }); return; } RetainPtr publicKeyDataRef; { auto publicKey = adoptCF(SecKeyCopyPublicKey(privateKey.get())); CFErrorRef errorRef = nullptr; publicKeyDataRef = adoptCF(SecKeyCopyExternalRepresentation(publicKey.get(), &errorRef)); auto retainError = adoptCF(errorRef); if (errorRef) { receiveException({ UnknownError, makeString("Couldn't export the public key: ", String(((NSError*)errorRef).localizedDescription)) }); return; } ASSERT(((NSData *)publicKeyDataRef.get()).length == (1 + 2 * ES256FieldElementLength)); // 04 | X | Y } NSData *nsPublicKeyData = (NSData *)publicKeyDataRef.get(); // Query credentialId in the keychain could be racy as it is the only unique identifier // of the key item. Instead we calculate that, and examine its equaity in DEBUG build. Vector credentialId; { auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_1); digest->addBytes(nsPublicKeyData.bytes, nsPublicKeyData.length); credentialId = digest->computeHash(); m_provisionalCredentialId = toNSData(credentialId); #ifndef NDEBUG auto query = adoptNS([[NSMutableDictionary alloc] init]); [query setDictionary:@{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, (id)kSecAttrLabel: secAttrLabel, (id)kSecAttrApplicationLabel: m_provisionalCredentialId.get(), (id)kSecUseDataProtectionKeychain: @YES }]; updateQueryIfNecessary(query.get()); OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query.get(), nullptr); ASSERT(!status); #endif // NDEBUG } // Step 11. https://www.w3.org/TR/webauthn/#attested-credential-data // credentialPublicKey Vector cosePublicKey; { // COSE Encoding Vector x(ES256FieldElementLength); [nsPublicKeyData getBytes: x.data() range:NSMakeRange(1, ES256FieldElementLength)]; Vector y(ES256FieldElementLength); [nsPublicKeyData getBytes: y.data() range:NSMakeRange(1 + ES256FieldElementLength, ES256FieldElementLength)]; cosePublicKey = encodeES256PublicKeyAsCBOR(WTFMove(x), WTFMove(y)); } auto flags = authDataFlags(ClientDataType::Create, verification, shouldUpdateQuery()); // Step 12. // Skip Apple Attestation for none attestation. if (creationOptions.attestation == AttestationConveyancePreference::None) { deleteDuplicateCredential(); auto authData = buildAuthData(creationOptions.rp.id, flags, counter, buildAttestedCredentialData(Vector(aaguidLength, 0), credentialId, cosePublicKey)); auto attestationObject = buildAttestationObject(WTFMove(authData), String { emptyString() }, { }, AttestationConveyancePreference::None); auto response = AuthenticatorAttestationResponse::create(credentialId, attestationObject, AuthenticatorAttachment::Platform, transports()); processClientExtensions(response); receiveRespond(WTFMove(response)); return; } // Step 13. Apple Attestation auto authData = buildAuthData(creationOptions.rp.id, flags, counter, buildAttestedCredentialData(aaguidVector(), credentialId, cosePublicKey)); auto nsAuthData = toNSData(authData); auto callback = [credentialId = WTFMove(credentialId), authData = WTFMove(authData), weakThis = WeakPtr { *this }] (NSArray * _Nullable certificates, NSError * _Nullable error) mutable { ASSERT(RunLoop::isMain()); if (!weakThis) return; weakThis->continueMakeCredentialAfterAttested(WTFMove(credentialId), WTFMove(authData), certificates, error); }; m_connection->getAttestation(privateKey.get(), nsAuthData.get(), toNSData(requestData().hash).get(), WTFMove(callback)); } void LocalAuthenticator::continueMakeCredentialAfterAttested(Vector&& credentialId, Vector&& authData, NSArray *certificates, NSError *error) { using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::UserVerified); m_state = State::Attested; auto& creationOptions = std::get(requestData().options); if (error) { LOG_ERROR("Couldn't attest: %s", String(error.localizedDescription).utf8().data()); auto attestationObject = buildAttestationObject(WTFMove(authData), String { emptyString() }, { }, AttestationConveyancePreference::None); receiveRespond(AuthenticatorAttestationResponse::create(credentialId, attestationObject, AuthenticatorAttachment::Platform, transports())); return; } // Attestation Certificate and Attestation Issuing CA ASSERT(certificates && ([certificates count] == 2)); // Step 13. Apple Attestation Cont' // Assemble the attestation object: // https://www.w3.org/TR/webauthn/#attestation-object cbor::CBORValue::MapValue attestationStatementMap; { Vector cborArray; for (size_t i = 0; i < [certificates count]; i++) cborArray.append(cbor::CBORValue(vectorFromNSData((NSData *)adoptCF(SecCertificateCopyData((__bridge SecCertificateRef)certificates[i])).get()))); attestationStatementMap[cbor::CBORValue("x5c")] = cbor::CBORValue(WTFMove(cborArray)); } auto attestationObject = buildAttestationObject(WTFMove(authData), "apple"_s, WTFMove(attestationStatementMap), creationOptions.attestation); deleteDuplicateCredential(); auto response = AuthenticatorAttestationResponse::create(credentialId, attestationObject, AuthenticatorAttachment::Platform, transports()); processClientExtensions(response); receiveRespond(WTFMove(response)); } void LocalAuthenticator::getAssertion() { using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::Init); m_state = State::RequestReceived; auto& requestOptions = std::get(requestData().options); // 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. // Skip Step 9 as counter is constantly 0. // 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. auto allowCredentialIds = produceHashSet(requestOptions.allowCredentials); if (!requestOptions.allowCredentials.isEmpty() && allowCredentialIds.isEmpty()) { receiveException({ NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s }, WebAuthenticationStatus::LANoCredential); return; } // Search Keychain for the RP ID. auto existingCredentials = getExistingCredentials(requestOptions.rpId); if (!existingCredentials) { receiveException({ UnknownError, makeString("Couldn't get existing credentials") }); return; } m_existingCredentials = WTFMove(*existingCredentials); Vector> assertionResponses; assertionResponses.reserveInitialCapacity(m_existingCredentials.size()); for (auto& credential : m_existingCredentials) { if (allowCredentialIds.isEmpty()) { assertionResponses.uncheckedAppend(credential.copyRef()); continue; } auto* rawId = credential->rawId(); if (allowCredentialIds.contains(base64EncodeToString(rawId->data(), rawId->byteLength()))) assertionResponses.uncheckedAppend(credential.copyRef()); } if (assertionResponses.isEmpty()) { receiveException({ NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s }, WebAuthenticationStatus::LANoCredential); return; } // Step 6-7. User consent is implicitly acquired by selecting responses. m_connection->filterResponses(assertionResponses); if (auto* observer = this->observer()) { auto callback = [this, weakThis = WeakPtr { *this }] (AuthenticatorAssertionResponse* response) { RELEASE_ASSERT(RunLoop::isMain()); if (!weakThis) return; auto result = m_existingCredentials.findIf([expectedResponse = response] (auto& response) { return response.ptr() == expectedResponse; }); if (result == notFound) return; continueGetAssertionAfterResponseSelected(m_existingCredentials[result].copyRef()); }; observer->selectAssertionResponse(WTFMove(assertionResponses), WebAuthenticationSource::Local, WTFMove(callback)); } } void LocalAuthenticator::continueGetAssertionAfterResponseSelected(Ref&& response) { ASSERT(m_state == State::RequestReceived); m_state = State::ResponseSelected; auto accessControlRef = response->accessControl(); LAContext *context = response->laContext(); auto callback = [ weakThis = WeakPtr { *this }, response = WTFMove(response) ] (LocalConnection::UserVerification verification) mutable { ASSERT(RunLoop::isMain()); if (!weakThis) return; weakThis->continueGetAssertionAfterUserVerification(WTFMove(response), verification, response->laContext()); }; m_connection->verifyUser(accessControlRef, context, WTFMove(callback)); } void LocalAuthenticator::continueGetAssertionAfterUserVerification(Ref&& response, LocalConnection::UserVerification verification, LAContext *context) { using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::ResponseSelected); m_state = State::UserVerified; if (!validateUserVerification(verification)) return; // Step 10. auto requestOptions = std::get(requestData().options); auto flags = authDataFlags(ClientDataType::Get, verification, response->synchronizable()); auto authData = buildAuthData(requestOptions.rpId, flags, counter, { }); // Step 11. RetainPtr signature; auto nsCredentialId = toNSData(response->rawId()); { NSMutableDictionary *queryDictionary = [@{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, (id)kSecAttrSynchronizable: (id)kSecAttrSynchronizableAny, (id)kSecAttrApplicationLabel: nsCredentialId.get(), (id)kSecReturnRef: @YES, (id)kSecUseDataProtectionKeychain: @YES } mutableCopy]; if (context) queryDictionary[(id)kSecUseAuthenticationContext] = context; auto query = adoptNS(queryDictionary); CFTypeRef privateKeyRef = nullptr; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query.get(), &privateKeyRef); if (status) { receiveException({ UnknownError, makeString("Couldn't get the private key reference: ", status) }); return; } auto privateKey = adoptCF(privateKeyRef); NSMutableData *dataToSign = [NSMutableData dataWithBytes:authData.data() length:authData.size()]; [dataToSign appendBytes:requestData().hash.data() length:requestData().hash.size()]; CFErrorRef errorRef = nullptr; // FIXME: Converting CFTypeRef to SecKeyRef is quite subtle here. signature = adoptCF(SecKeyCreateSignature((__bridge SecKeyRef)((id)privateKeyRef), kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (__bridge CFDataRef)dataToSign, &errorRef)); auto retainError = adoptCF(errorRef); if (errorRef) { receiveException({ UnknownError, makeString("Couldn't generate the signature: ", String(((NSError*)errorRef).localizedDescription)) }); return; } } // Extra step: update the Keychain item with the same value to update its modification date such that LRU can be used // for selectAssertionResponse NSDictionary *query = @{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, (id)kSecAttrSynchronizable: (id)kSecAttrSynchronizableAny, (id)kSecAttrApplicationLabel: nsCredentialId.get(), (id)kSecUseDataProtectionKeychain: @YES }; NSDictionary *updateParams = @{ (id)kSecAttrLabel: requestOptions.rpId, }; auto status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)updateParams); if (status) LOG_ERROR("Couldn't update the Keychain item: %d", status); // Step 13. response->setAuthenticatorData(WTFMove(authData)); response->setSignature(toArrayBuffer((NSData *)signature.get())); processClientExtensions(response); receiveRespond(WTFMove(response)); } void LocalAuthenticator::receiveException(ExceptionData&& exception, WebAuthenticationStatus status) const { LOG_ERROR(exception.message.utf8().data()); // Roll back the just created credential. if (m_provisionalCredentialId) { auto query = adoptNS([[NSMutableDictionary alloc] init]); [query setDictionary:@{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrApplicationLabel: m_provisionalCredentialId.get(), (id)kSecUseDataProtectionKeychain: @YES }]; updateQueryIfNecessary(query.get()); OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query.get()); if (status) LOG_ERROR(makeString("Couldn't delete provisional credential while handling error: "_s, status).utf8().data()); } if (auto* observer = this->observer()) observer->authenticatorStatusUpdated(status); receiveRespond(WTFMove(exception)); return; } void LocalAuthenticator::deleteDuplicateCredential() const { using namespace LocalAuthenticatorInternal; auto& creationOptions = std::get(requestData().options); m_existingCredentials.findIf([creationOptions] (auto& credential) { auto* userHandle = credential->userHandle(); ASSERT(userHandle); if (userHandle->byteLength() != creationOptions.user.id.length()) return false; if (memcmp(userHandle->data(), creationOptions.user.id.data(), userHandle->byteLength())) return false; NSDictionary *query = @{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrApplicationLabel: toNSData(credential->rawId()).get(), (id)kSecAttrSynchronizable: (id)kSecAttrSynchronizableAny, (id)kSecUseDataProtectionKeychain: @YES }; OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); if (status && status != errSecItemNotFound) LOG_ERROR(makeString("Couldn't delete older credential: "_s, status).utf8().data()); return true; }); } bool LocalAuthenticator::validateUserVerification(LocalConnection::UserVerification verification) const { if (verification == LocalConnection::UserVerification::Cancel) { if (auto* observer = this->observer()) observer->cancelRequest(); return false; } if (verification == LocalConnection::UserVerification::No) { receiveException({ NotAllowedError, "Couldn't verify user."_s }); return false; } return true; } } // namespace WebKit #endif // ENABLE(WEB_AUTHN)