/* * 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. */ #include "config.h" #include "LocalAuthenticator.h" #if ENABLE(WEB_AUTHN) #import #import #import #import #import #import #import #import #import #import #import #import #import #import namespace WebKit { using 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. // Credential ID is currently SHA-1 of the corresponding public key. const uint16_t credentialIdLength = 20; #if PLATFORM(IOS_FAMILY) static inline bool emptyTransportsOrContain(const Vector& transports, AuthenticatorTransport target) { return transports.isEmpty() ? true : transports.contains(target); } static inline HashSet produceHashSet(const Vector& credentialDescriptors) { HashSet result; for (auto& credentialDescriptor : credentialDescriptors) { if (emptyTransportsOrContain(credentialDescriptor.transports, AuthenticatorTransport::Internal) && credentialDescriptor.type == PublicKeyCredentialType::PublicKey && credentialDescriptor.idVector.size() == credentialIdLength) result.add(String(reinterpret_cast(credentialDescriptor.idVector.data()), credentialDescriptor.idVector.size())); } return result; } static inline Vector toVector(NSData *data) { Vector result; result.append(reinterpret_cast(data.bytes), data.length); return result; } #endif // !PLATFORM(IOS_FAMILY) } // LocalAuthenticatorInternal LocalAuthenticator::LocalAuthenticator(UniqueRef&& connection) : m_connection(WTFMove(connection)) { } void LocalAuthenticator::makeCredential() { // FIXME(182772) using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::Init); m_state = State::RequestReceived; #if PLATFORM(IOS_FAMILY) // 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. // Step 2. bool canFullfillPubKeyCredParams = false; for (auto& pubKeyCredParam : requestData().creationOptions.pubKeyCredParams) { if (pubKeyCredParam.type == PublicKeyCredentialType::PublicKey && pubKeyCredParam.alg == COSE::ES256) { canFullfillPubKeyCredParams = true; break; } } if (!canFullfillPubKeyCredParams) { receiveRespond(ExceptionData { NotSupportedError, "The platform attached authenticator doesn't support any provided PublicKeyCredentialParameters."_s }); return; } // Step 3. HashSet excludeCredentialIds = produceHashSet(requestData().creationOptions.excludeCredentials); if (!excludeCredentialIds.isEmpty()) { // Search Keychain for the RP ID. NSDictionary *query = @{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, (id)kSecAttrLabel: requestData().creationOptions.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); receiveRespond(ExceptionData { 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(nsCredentialId.bytes), nsCredentialId.length))) { receiveRespond(ExceptionData { 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 callback = [weakThis = makeWeakPtr(*this)](LocalConnection::UserConsent consent) { ASSERT(RunLoop::isMain()); if (!weakThis) return; weakThis->continueMakeCredentialAfterUserConsented(consent); }; m_connection->getUserConsent( String::format("Allow %s to create a public key credential for %s", requestData().creationOptions.rp.id.utf8().data(), requestData().creationOptions.user.name.utf8().data()), WTFMove(callback)); #endif // !PLATFORM(IOS_FAMILY) } void LocalAuthenticator::continueMakeCredentialAfterUserConsented(LocalConnection::UserConsent consent) { // FIXME(182772) ASSERT(m_state == State::RequestReceived); m_state = State::UserConsented; #if PLATFORM(IOS_FAMILY) if (consent == LocalConnection::UserConsent::No) { receiveRespond(ExceptionData { 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: requestData().creationOptions.rp.id, (id)kSecAttrApplicationTag: [NSData dataWithBytes:requestData().creationOptions.user.idVector.data() length:requestData().creationOptions.user.idVector.size()], }; OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery); if (status && status != errSecItemNotFound) { LOG_ERROR("Couldn't detele older credential: %d", status); receiveRespond(ExceptionData { UnknownError, "Unknown internal error."_s }); return; } // Step 7.1, 13. Apple Attestation auto callback = [weakThis = makeWeakPtr(*this)](SecKeyRef _Nullable privateKey, NSArray * _Nullable certificates, NSError * _Nullable error) { ASSERT(RunLoop::isMain()); if (!weakThis) return; weakThis->continueMakeCredentialAfterAttested(privateKey, certificates, error); }; m_connection->getAttestation(requestData().creationOptions.rp.id, requestData().creationOptions.user.name, requestData().hash, WTFMove(callback)); #endif // !PLATFORM(IOS_FAMILY) } void LocalAuthenticator::continueMakeCredentialAfterAttested(SecKeyRef privateKey, NSArray *certificates, NSError *error) { // FIXME(182772) using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::UserConsented); m_state = State::Attested; #if PLATFORM(IOS_FAMILY) if (error) { LOG_ERROR("Couldn't attest: %@", error); receiveRespond(ExceptionData { 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 credentialId; { // -rk is added by DeviceIdentity.Framework. String label = makeString(requestData().creationOptions.user.name, "@", requestData().creationOptions.rp.id, "-rk"); 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); receiveRespond(ExceptionData { UnknownError, "Unknown internal error."_s }); return; } auto retainAttributes = adoptCF(attributesRef); NSDictionary *nsAttributes = (NSDictionary *)attributesRef; credentialId = toVector(nsAttributes[(id)kSecAttrApplicationLabel]); NSDictionary *updateQuery = @{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, (id)kSecAttrApplicationLabel: nsAttributes[(id)kSecAttrApplicationLabel], }; NSDictionary *updateParams = @{ (id)kSecAttrLabel: requestData().creationOptions.rp.id, (id)kSecAttrApplicationTag: [NSData dataWithBytes:requestData().creationOptions.user.idVector.data() length:requestData().creationOptions.user.idVector.size()], }; status = SecItemUpdate((__bridge CFDictionaryRef)updateQuery, (__bridge CFDictionaryRef)updateParams); if (status) { LOG_ERROR("Couldn't update the Keychain item: %d", status); receiveRespond(ExceptionData { UnknownError, "Unknown internal error."_s }); return; } } // Step 10. // FIXME(183533): store the counter. uint32_t counter = 0; // Step 11. https://www.w3.org/TR/webauthn/#attested-credential-data // credentialPublicKey Vector cosePublicKey; { RetainPtr 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); receiveRespond(ExceptionData { UnknownError, "Unknown internal error."_s }); return; } ASSERT(((NSData *)publicKeyDataRef.get()).length == (1 + 2 * ES256FieldElementLength)); // 04 | X | Y } // COSE Encoding Vector x(ES256FieldElementLength); [(NSData *)publicKeyDataRef.get() getBytes: x.data() range:NSMakeRange(1, ES256FieldElementLength)]; Vector y(ES256FieldElementLength); [(NSData *)publicKeyDataRef.get() getBytes: y.data() range:NSMakeRange(1 + ES256FieldElementLength, ES256FieldElementLength)]; cosePublicKey = encodeES256PublicKeyAsCBOR(WTFMove(x), WTFMove(y)); } // FIXME(rdar://problem/38320512): Define Apple AAGUID. auto attestedCredentialData = buildAttestedCredentialData(Vector(aaguidLength, 0), credentialId, cosePublicKey); // Step 12. auto authData = buildAuthData(requestData().creationOptions.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 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); receiveRespond(ExceptionData { UnknownError, "Unknown internal error."_s }); return; } signature = toVector((NSData *)signatureRef.get()); } attestationStatementMap[cbor::CBORValue("alg")] = cbor::CBORValue(COSE::ES256); attestationStatementMap[cbor::CBORValue("sig")] = cbor::CBORValue(signature); Vector cborArray; for (size_t i = 0; i < [certificates count]; i++) cborArray.append(cbor::CBORValue(toVector((NSData *)adoptCF(SecCertificateCopyData((__bridge SecCertificateRef)certificates[i])).get()))); attestationStatementMap[cbor::CBORValue("x5c")] = cbor::CBORValue(WTFMove(cborArray)); } auto attestationObject = buildAttestationObject(WTFMove(authData), "Apple", WTFMove(attestationStatementMap)); receiveRespond(PublicKeyCredentialData { ArrayBuffer::create(credentialId.data(), credentialId.size()), true, nullptr, ArrayBuffer::create(attestationObject.data(), attestationObject.size()), nullptr, nullptr, nullptr }); #endif // !PLATFORM(IOS_FAMILY) } void LocalAuthenticator::getAssertion() { // FIXME(182772) using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::Init); m_state = State::RequestReceived; #if PLATFORM(IOS_FAMILY) // 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 allowCredentialIds = produceHashSet(requestData().requestOptions.allowCredentials); if (!requestData().requestOptions.allowCredentials.isEmpty() && allowCredentialIds.isEmpty()) { receiveRespond(ExceptionData { 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: requestData().requestOptions.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); receiveRespond(ExceptionData { UnknownError, "Unknown internal error."_s }); return; } auto retainAttributesArray = adoptCF(attributesArrayRef); NSArray *intersectedCredentialsAttributes = nil; if (requestData().requestOptions.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(nsCredentialId.bytes), nsCredentialId.length))) [result addObject:nsAttributes]; } intersectedCredentialsAttributes = result; } if (!intersectedCredentialsAttributes.count) { receiveRespond(ExceptionData { 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(rdar://problem/35900593): Update to a formal UI. auto callback = [ weakThis = makeWeakPtr(*this), credentialId = toVector(selectedCredentialAttributes[(id)kSecAttrApplicationLabel]), userhandle = toVector(selectedCredentialAttributes[(id)kSecAttrApplicationTag]) ](LocalConnection::UserConsent consent, LAContext *context) { ASSERT(RunLoop::isMain()); if (!weakThis) return; weakThis->continueGetAssertionAfterUserConsented(consent, context, credentialId, userhandle); }; m_connection->getUserConsent( String::format("Log into %s with %s.", requestData().requestOptions.rpId.utf8().data(), selectedCredentialAttributes[(id)kSecAttrApplicationTag]), (__bridge SecAccessControlRef)selectedCredentialAttributes[(id)kSecAttrAccessControl], WTFMove(callback)); #endif // !PLATFORM(IOS_FAMILY) } void LocalAuthenticator::continueGetAssertionAfterUserConsented(LocalConnection::UserConsent consent, LAContext *context, const Vector& credentialId, const Vector& userhandle) { // FIXME(182772) using namespace LocalAuthenticatorInternal; ASSERT(m_state == State::RequestReceived); m_state = State::UserConsented; #if PLATFORM(IOS_FAMILY) if (consent == LocalConnection::UserConsent::No) { receiveRespond(ExceptionData { 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(requestData().requestOptions.rpId, getAssertionFlags, counter, { }); // Step 11. Vector signature; { NSDictionary *query = @{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate, (id)kSecAttrApplicationLabel: [NSData dataWithBytes:credentialId.data() length:credentialId.size()], (id)kSecUseAuthenticationContext: context, (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); receiveRespond(ExceptionData { UnknownError, "Unknown internal error."_s }); 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. 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); receiveRespond(ExceptionData { UnknownError, "Unknown internal error."_s }); return; } signature = toVector((NSData *)signatureRef.get()); } // Step 13. receiveRespond(PublicKeyCredentialData { ArrayBuffer::create(credentialId.data(), credentialId.size()), false, nullptr, nullptr, ArrayBuffer::create(authData.data(), authData.size()), ArrayBuffer::create(signature.data(), signature.size()), ArrayBuffer::create(userhandle.data(), userhandle.size()) }); #endif // !PLATFORM(IOS_FAMILY) } } // namespace WebKit #endif // ENABLE(WEB_AUTHN)