| /* |
| * 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 <Security/SecItem.h> |
| #import <WebCore/AuthenticatorAssertionResponse.h> |
| #import <WebCore/AuthenticatorAttachment.h> |
| #import <WebCore/AuthenticatorAttestationResponse.h> |
| #import <WebCore/CBORReader.h> |
| #import <WebCore/CBORWriter.h> |
| #import <WebCore/ExceptionData.h> |
| #import <WebCore/FidoConstants.h> |
| #import <WebCore/PublicKeyCredentialCreationOptions.h> |
| #import <WebCore/PublicKeyCredentialRequestOptions.h> |
| #import <WebCore/WebAuthenticationConstants.h> |
| #import <WebCore/WebAuthenticationUtils.h> |
| #import <pal/crypto/CryptoDigest.h> |
| #import <wtf/RetainPtr.h> |
| #import <wtf/RunLoop.h> |
| #import <wtf/Vector.h> |
| #import <wtf/cocoa/VectorCocoa.h> |
| #import <wtf/spi/cocoa/SecuritySPI.h> |
| #import <wtf/text/Base64.h> |
| #import <wtf/text/StringHash.h> |
| |
| #if USE(APPLE_INTERNAL_SDK) |
| #import <WebKitAdditions/LocalAuthenticatorAdditions.h> |
| #else |
| static void updateQueryIfNecessary(NSMutableDictionary *) |
| { |
| } |
| #endif |
| |
| namespace WebKit { |
| using namespace fido; |
| using namespace WebCore; |
| using CBOR = cbor::CBORValue; |
| |
| namespace LocalAuthenticatorInternal { |
| |
| // See https://www.w3.org/TR/webauthn/#flags. |
| const uint8_t makeCredentialFlags = 0b01000101; // UP, UV and AT are set. |
| const uint8_t otherMakeCredentialFlags = 0b01000001; // UP and AT are set. |
| const uint8_t getAssertionFlags = 0b00000101; // UP and UV are set. |
| const uint8_t otherGetAssertionFlags = 0b00000001; // UP is set. |
| // 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<AuthenticatorTransport>& 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<String> produceHashSet(const Vector<PublicKeyCredentialDescriptor>& credentialDescriptors) |
| { |
| HashSet<String> 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 Vector<uint8_t> aaguidVector() |
| { |
| static NeverDestroyed<Vector<uint8_t>> aaguidVector = { aaguid, aaguidLength }; |
| return aaguidVector; |
| } |
| |
| static inline RetainPtr<NSData> toNSData(const Vector<uint8_t>& data) |
| { |
| return adoptNS([[NSData alloc] initWithBytes:data.data() length:data.size()]); |
| } |
| |
| static inline RetainPtr<NSData> toNSData(ArrayBuffer* buffer) |
| { |
| ASSERT(buffer); |
| return adoptNS([[NSData alloc] initWithBytes:buffer->data() length:buffer->byteLength()]); |
| } |
| |
| static inline Ref<ArrayBuffer> toArrayBuffer(NSData *data) |
| { |
| return ArrayBuffer::create(reinterpret_cast<const uint8_t*>(data.bytes), data.length); |
| } |
| |
| static inline Ref<ArrayBuffer> toArrayBuffer(const Vector<uint8_t>& data) |
| { |
| return ArrayBuffer::create(data.data(), data.size()); |
| } |
| |
| static std::optional<Vector<Ref<AuthenticatorAssertionResponse>>> getExistingCredentials(const String& rpId) |
| { |
| // Search Keychain for existing credential matched the RP ID. |
| auto query = adoptNS([[NSMutableDictionary alloc] init]); |
| [query setDictionary:@{ |
| (id)kSecClass: (id)kSecClassKey, |
| (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, |
| (id)kSecAttrLabel: rpId, |
| (id)kSecReturnAttributes: @YES, |
| (id)kSecMatchLimit: (id)kSecMatchLimitAll, |
| (id)kSecUseDataProtectionKeychain: @YES |
| }]; |
| updateQueryIfNecessary(query.get()); |
| |
| CFTypeRef attributesArrayRef = nullptr; |
| OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query.get(), &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<Ref<AuthenticatorAssertionResponse>> 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(); |
| |
| auto it = responseMap.find(CBOR(fido::kEntityIdMapKey)); |
| if (it == responseMap.end() || !it->second.isByteString()) { |
| ASSERT_NOT_REACHED(); |
| return std::nullopt; |
| } |
| auto& userHandle = 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(); |
| |
| result.uncheckedAppend(AuthenticatorAssertionResponse::create(toArrayBuffer(attributes[(id)kSecAttrApplicationLabel]), toArrayBuffer(userHandle), String(username), (__bridge SecAccessControlRef)attributes[(id)kSecAttrAccessControl], AuthenticatorAttachment::Platform)); |
| } |
| return result; |
| } |
| |
| } // LocalAuthenticatorInternal |
| |
| void LocalAuthenticator::clearAllCredentials() |
| { |
| // FIXME<rdar://problem/57171201>: 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: (id)String(LocalAuthenticatiorAccessGroup), |
| (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<LocalConnection>&& connection) |
| : m_connection(WTFMove(connection)) |
| { |
| } |
| |
| void LocalAuthenticator::makeCredential() |
| { |
| using namespace LocalAuthenticatorInternal; |
| ASSERT(m_state == State::Init); |
| m_state = State::RequestReceived; |
| auto& creationOptions = std::get<PublicKeyCredentialCreationOptions>(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())); |
| })) { |
| // Obtain consent per Step 3.1 |
| auto callback = [weakThis = WeakPtr { *this }] (LocalAuthenticatorPolicy policy) { |
| ASSERT(RunLoop::isMain()); |
| if (!weakThis) |
| return; |
| |
| if (policy == LocalAuthenticatorPolicy::Allow) |
| weakThis->receiveException({ InvalidStateError, "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator."_s }, WebAuthenticationStatus::LAExcludeCredentialsMatched); |
| else |
| weakThis->receiveException({ NotAllowedError, "This request has been cancelled by the user."_s }); |
| }; |
| observer()->decidePolicyForLocalAuthenticator(WTFMove(callback)); |
| return; |
| } |
| } |
| |
| // Step 6. |
| // Get user consent. |
| if (webAuthenticationModernEnabled()) { |
| 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)); |
| } |
| |
| return; |
| } |
| |
| if (auto* observer = this->observer()) { |
| auto callback = [weakThis = WeakPtr { *this }] (LocalAuthenticatorPolicy policy) { |
| ASSERT(RunLoop::isMain()); |
| if (!weakThis) |
| return; |
| |
| weakThis->continueMakeCredentialAfterDecidePolicy(policy); |
| }; |
| observer->decidePolicyForLocalAuthenticator(WTFMove(callback)); |
| } |
| } |
| |
| void LocalAuthenticator::continueMakeCredentialAfterDecidePolicy(LocalAuthenticatorPolicy policy) |
| { |
| ASSERT(m_state == State::RequestReceived); |
| m_state = State::PolicyDecided; |
| |
| auto& creationOptions = std::get<PublicKeyCredentialCreationOptions>(requestData().options); |
| |
| if (policy == LocalAuthenticatorPolicy::Disallow) { |
| receiveRespond(ExceptionData { UnknownError, "Disallow local authenticator."_s }); |
| return; |
| } |
| |
| RetainPtr<SecAccessControlRef> 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), weakThis = WeakPtr { *this }] (LocalConnection::UserVerification verification, LAContext *context) { |
| ASSERT(RunLoop::isMain()); |
| if (!weakThis) |
| return; |
| |
| weakThis->continueMakeCredentialAfterUserVerification(accessControl.get(), verification, context); |
| }; |
| m_connection->verifyUser(creationOptions.rp.id, getClientDataType(requestData().options), accessControlRef, getUserVerificationRequirement(requestData().options), WTFMove(callback)); |
| } |
| |
| void LocalAuthenticator::continueMakeCredentialAfterReceivingLAContext(LAContext *context) |
| { |
| ASSERT(m_state == State::RequestReceived); |
| m_state = State::PolicyDecided; |
| |
| RetainPtr<SecAccessControlRef> 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::continueMakeCredentialAfterUserVerification(SecAccessControlRef accessControlRef, LocalConnection::UserVerification verification, LAContext *context) |
| { |
| using namespace LocalAuthenticatorInternal; |
| |
| ASSERT(m_state == State::PolicyDecided); |
| m_state = State::UserVerified; |
| auto& creationOptions = std::get<PublicKeyCredentialCreationOptions>(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); |
| 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<CFDataRef> 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<uint8_t> 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<uint8_t> cosePublicKey; |
| { |
| // COSE Encoding |
| Vector<uint8_t> x(ES256FieldElementLength); |
| [nsPublicKeyData getBytes: x.data() range:NSMakeRange(1, ES256FieldElementLength)]; |
| Vector<uint8_t> y(ES256FieldElementLength); |
| [nsPublicKeyData getBytes: y.data() range:NSMakeRange(1 + ES256FieldElementLength, ES256FieldElementLength)]; |
| cosePublicKey = encodeES256PublicKeyAsCBOR(WTFMove(x), WTFMove(y)); |
| } |
| |
| auto flags = verification == LocalConnection::UserVerification::Presence ? otherMakeCredentialFlags : makeCredentialFlags; |
| // Step 12. |
| // Skip Apple Attestation for none attestation. |
| if (creationOptions.attestation == AttestationConveyancePreference::None) { |
| deleteDuplicateCredential(); |
| |
| auto authData = buildAuthData(creationOptions.rp.id, flags, counter, buildAttestedCredentialData(Vector<uint8_t>(aaguidLength, 0), credentialId, cosePublicKey)); |
| auto attestationObject = buildAttestationObject(WTFMove(authData), "", { }, AttestationConveyancePreference::None); |
| receiveRespond(AuthenticatorAttestationResponse::create(credentialId, attestationObject, AuthenticatorAttachment::Platform)); |
| 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<uint8_t>&& credentialId, Vector<uint8_t>&& authData, NSArray *certificates, NSError *error) |
| { |
| using namespace LocalAuthenticatorInternal; |
| |
| ASSERT(m_state == State::UserVerified); |
| m_state = State::Attested; |
| auto& creationOptions = std::get<PublicKeyCredentialCreationOptions>(requestData().options); |
| |
| if (error) { |
| receiveException({ UnknownError, makeString("Couldn't attest: ", String(error.localizedDescription)) }); |
| 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<cbor::CBORValue> 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", WTFMove(attestationStatementMap), creationOptions.attestation); |
| |
| deleteDuplicateCredential(); |
| receiveRespond(AuthenticatorAttestationResponse::create(credentialId, attestationObject, AuthenticatorAttachment::Platform)); |
| } |
| |
| void LocalAuthenticator::getAssertion() |
| { |
| using namespace LocalAuthenticatorInternal; |
| ASSERT(m_state == State::Init); |
| m_state = State::RequestReceived; |
| auto& requestOptions = std::get<PublicKeyCredentialRequestOptions>(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<Ref<WebCore::AuthenticatorAssertionResponse>> 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) { |
| 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<WebCore::AuthenticatorAssertionResponse>&& response) |
| { |
| ASSERT(m_state == State::RequestReceived); |
| m_state = State::ResponseSelected; |
| |
| if (webAuthenticationModernEnabled()) { |
| 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)); |
| return; |
| } |
| |
| auto& requestOptions = std::get<PublicKeyCredentialRequestOptions>(requestData().options); |
| |
| auto accessControlRef = response->accessControl(); |
| auto callback = [ |
| weakThis = WeakPtr { *this }, |
| response = WTFMove(response) |
| ] (LocalConnection::UserVerification verification, LAContext *context) mutable { |
| ASSERT(RunLoop::isMain()); |
| if (!weakThis) |
| return; |
| |
| weakThis->continueGetAssertionAfterUserVerification(WTFMove(response), verification, context); |
| }; |
| m_connection->verifyUser(requestOptions.rpId, getClientDataType(requestData().options), accessControlRef, getUserVerificationRequirement(requestData().options), WTFMove(callback)); |
| } |
| |
| void LocalAuthenticator::continueGetAssertionAfterUserVerification(Ref<WebCore::AuthenticatorAssertionResponse>&& 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<PublicKeyCredentialRequestOptions>(requestData().options); |
| auto authData = buildAuthData(requestOptions.rpId, verification == LocalConnection::UserVerification::Presence ? otherGetAssertionFlags : getAssertionFlags, counter, { }); |
| |
| // Step 11. |
| RetainPtr<CFDataRef> signature; |
| auto nsCredentialId = toNSData(response->rawId()); |
| { |
| NSMutableDictionary *queryDictionary = [@{ |
| (id)kSecClass: (id)kSecClassKey, |
| (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, |
| (id)kSecAttrApplicationLabel: nsCredentialId.get(), |
| (id)kSecReturnRef: @YES, |
| (id)kSecUseDataProtectionKeychain: @YES |
| } mutableCopy]; |
| |
| if (context) |
| queryDictionary[(id)kSecUseAuthenticationContext] = context; |
| |
| auto query = adoptNS(queryDictionary); |
| updateQueryIfNecessary(query.get()); |
| |
| 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 |
| auto query = adoptNS([[NSMutableDictionary alloc] init]); |
| [query setDictionary:@{ |
| (id)kSecClass: (id)kSecClassKey, |
| (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, |
| (id)kSecAttrApplicationLabel: nsCredentialId.get(), |
| (id)kSecUseDataProtectionKeychain: @YES |
| }]; |
| updateQueryIfNecessary(query.get()); |
| |
| NSDictionary *updateParams = @{ |
| (id)kSecAttrLabel: requestOptions.rpId, |
| }; |
| auto status = SecItemUpdate((__bridge CFDictionaryRef)query.get(), (__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())); |
| 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<PublicKeyCredentialCreationOptions>(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; |
| |
| auto query = adoptNS([[NSMutableDictionary alloc] init]); |
| [query setDictionary:@{ |
| (id)kSecClass: (id)kSecClassKey, |
| (id)kSecAttrApplicationLabel: toNSData(credential->rawId()).get(), |
| (id)kSecUseDataProtectionKeychain: @YES |
| }]; |
| updateQueryIfNecessary(query.get()); |
| |
| OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query.get()); |
| 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) |