[WebAuthN] Implement authenticatorMakeCredential
[WebKit-https.git] / Source / WebCore / Modules / webauthn / cocoa / LocalAuthenticator.mm
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "LocalAuthenticator.h"
28
29 #if ENABLE(WEB_AUTHN)
30
31 #import "CBORWriter.h"
32 #import "COSEConstants.h"
33 #import "ExceptionData.h"
34 #import "PublicKeyCredentialCreationOptions.h"
35 #import <Security/SecItem.h>
36 #import <pal/crypto/CryptoDigest.h>
37 #import <pal/spi/cocoa/DeviceIdentitySPI.h>
38 #import <wtf/BlockPtr.h>
39 #import <wtf/MainThread.h>
40 #import <wtf/RetainPtr.h>
41 #import <wtf/Vector.h>
42
43 #import "LocalAuthenticationSoftLink.h"
44
45 namespace WebCore {
46
47 namespace LocalAuthenticatorInternal {
48
49 // UP, UV and AT are set. See https://www.w3.org/TR/webauthn/#flags.
50 const uint8_t authenticatorDataFlags = 69;
51 // FIXME(rdar://problem/38320512): Define Apple AAGUID.
52 const uint8_t AAGUID[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // 16 bytes
53 const size_t maxCredentialIdLength = 0xffff; // 2 bytes
54 const size_t ES256KeySizeInBytes = 32;
55
56 } // LocalAuthenticatorInternal
57
58 void LocalAuthenticator::makeCredential(const Vector<uint8_t>& hash, const PublicKeyCredentialCreationOptions& options, CreationCallback&& callback, ExceptionCallback&& exceptionCallback)
59 {
60     using namespace LocalAuthenticatorInternal;
61
62 #if !PLATFORM(IOS)
63     // FIXME(182772)
64     ASSERT_UNUSED(hash, hash == hash);
65     ASSERT_UNUSED(options, options.rp.name.isEmpty());
66     ASSERT_UNUSED(callback, callback);
67     exceptionCallback({ NotAllowedError, ASCIILiteral("No avaliable authenticators.") });
68 #else
69     // The following implements https://www.w3.org/TR/webauthn/#op-make-cred as of 5 December 2017.
70     // Skip Step 4-5 as requireResidentKey and requireUserVerification are enforced.
71     // Skip Step 9 as extensions are not supported yet.
72     // Step 8 is implicitly captured by all UnknownError exception callbacks.
73     // Step 2.
74     bool canFullfillPubKeyCredParams = false;
75     for (auto& pubKeyCredParam : options.pubKeyCredParams) {
76         if (pubKeyCredParam.type == PublicKeyCredentialType::PublicKey && pubKeyCredParam.alg == COSE::ES256) {
77             canFullfillPubKeyCredParams = true;
78             break;
79         }
80     }
81     if (!canFullfillPubKeyCredParams) {
82         exceptionCallback({ NotSupportedError, ASCIILiteral("The platform attached authenticator doesn't support any provided PublicKeyCredentialParameters.") });
83         return;
84     }
85
86     // Step 3.
87     for (auto& excludeCredential : options.excludeCredentials) {
88         if (excludeCredential.type == PublicKeyCredentialType::PublicKey && excludeCredential.transports.isEmpty()) {
89             // Search Keychain for the Credential ID and RP ID, which is stored in the kSecAttrApplicationLabel and kSecAttrLabel attribute respectively.
90             NSDictionary *query = @{
91                 (id)kSecClass: (id)kSecClassKey,
92                 (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
93                 (id)kSecAttrApplicationLabel: [NSData dataWithBytes:excludeCredential.idVector.data() length:excludeCredential.idVector.size()],
94                 (id)kSecAttrLabel: options.rp.id
95             };
96             OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
97             if (!status) {
98                 exceptionCallback({ NotAllowedError, ASCIILiteral("At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator.") });
99                 return;
100             }
101             if (status != errSecItemNotFound) {
102                 LOG_ERROR("Couldn't query Keychain: %d", status);
103                 exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
104                 return;
105             }
106         }
107     }
108
109     // Step 6.
110     // FIXME(rdar://problem/35900593): Update to a formal UI.
111     // Get user consent.
112     auto context = adoptNS([allocLAContextInstance() init]);
113     NSError *error = nil;
114     NSString *reason = [NSString stringWithFormat:@"Allow %@ to create a public key credential for %@", (id)options.rp.id, (id)options.user.name];
115     if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
116         LOG_ERROR("Couldn't evaluate authentication with biometrics policy: %@", error);
117         // FIXME(182767)
118         exceptionCallback({ NotAllowedError, ASCIILiteral("No avaliable authenticators.") });
119         return;
120     }
121
122     // FIXME(183534): Optimize the following nested callbacks and threading.
123     [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:reason reply:BlockPtr<void(BOOL, NSError *)>::fromCallable([weakThis = m_weakFactory.createWeakPtr(*this), callback = WTFMove(callback), exceptionCallback = WTFMove(exceptionCallback), options = crossThreadCopy(options), hash] (BOOL success, NSError *error) mutable {
124         ASSERT(!isMainThread());
125         if (!success || error) {
126             LOG_ERROR("Couldn't authenticate with biometrics: %@", error);
127             exceptionCallback({ NotAllowedError, ASCIILiteral("Couldn't get user consent.") });
128             return;
129         }
130
131         // Step 7.5.
132         // Userhandle is stored in kSecAttrApplicationTag attribute.
133         // Failures after this point could block users' accounts forever. Should we follow the spec?
134         NSDictionary* deleteQuery = @{
135             (id)kSecClass: (id)kSecClassKey,
136             (id)kSecAttrLabel: options.rp.id,
137             (id)kSecAttrApplicationTag: [NSData dataWithBytes:options.user.idVector.data() length:options.user.idVector.size()],
138         };
139         OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
140         if (status && status != errSecItemNotFound) {
141             LOG_ERROR("Couldn't detele older credential: %d", status);
142             exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
143             return;
144         }
145
146         // Step 7.1, 13. Apple Attestation
147         // FIXME(183534)
148         if (!weakThis)
149             return;
150         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) {
151             ASSERT(!isMainThread());
152             if (error) {
153                 LOG_ERROR("Couldn't attest: %@", error);
154                 exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
155                 return;
156             }
157             // Attestation Certificate and Attestation Issuing CA
158             ASSERT(certificates && ([certificates count] == 2));
159
160             // Step 7.2 - 7.4.
161             // FIXME(183533): A single kSecClassKey item couldn't store all meta data. The following schema is a tentative solution
162             // to accommodate the most important meta data, i.e. RP ID, Credential ID, and userhandle.
163             // kSecAttrLabel: RP ID
164             // kSecAttrApplicationLabel: Credential ID (auto-gen by Keychain)
165             // kSecAttrApplicationTag: userhandle
166             // Noted, the current DeviceIdentity.Framework would only allow us to pass the kSecAttrLabel as the inital attribute
167             // for the Keychain item. Since that's the only clue we have to locate the unique item, we use the pattern username@rp.id
168             // as the initial value.
169             // Also noted, the vale of kSecAttrApplicationLabel is automatically generated by the Keychain, which is a SHA-1 hash of
170             // the public key. We borrow it directly for now to workaround the stated limitations.
171             // Update the Keychain item to the above schema.
172             // FIXME(183533): DeviceIdentity.Framework would insert certificates into Keychain as well. We should update those as well.
173             Vector<uint8_t> credentialId;
174             {
175                 String label(options.user.name);
176                 label.append("@" + options.rp.id + "-rk"); // -rk is added by DeviceIdentity.Framework.
177                 NSDictionary *credentialIdQuery = @{
178                     (id)kSecClass: (id)kSecClassKey,
179                     (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
180                     (id)kSecAttrLabel: label,
181                     (id)kSecReturnAttributes: @YES
182                 };
183                 CFTypeRef attributesRef = NULL;
184                 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)credentialIdQuery, &attributesRef);
185                 if (status) {
186                     LOG_ERROR("Couldn't get Credential ID: %d", status);
187                     exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
188                     return;
189                 }
190                 auto retainAttributes = adoptCF(attributesRef);
191
192                 NSDictionary *nsAttributes = (NSDictionary *)attributesRef;
193                 NSData *nsCredentialId = nsAttributes[(id)kSecAttrApplicationLabel];
194                 credentialId.append(static_cast<const uint8_t*>(nsCredentialId.bytes), nsCredentialId.length);
195
196                 NSDictionary *updateQuery = @{
197                     (id)kSecClass: (id)kSecClassKey,
198                     (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
199                     (id)kSecAttrApplicationLabel: nsCredentialId,
200                 };
201                 NSDictionary *updateParams = @{
202                     (id)kSecAttrLabel: options.rp.id,
203                     (id)kSecAttrApplicationTag: [NSData dataWithBytes:options.user.idVector.data() length:options.user.idVector.size()],
204                 };
205                 status = SecItemUpdate((__bridge CFDictionaryRef)updateQuery, (__bridge CFDictionaryRef)updateParams);
206                 if (status) {
207                     LOG_ERROR("Couldn't update the Keychain item: %d", status);
208                     exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
209                     return;
210                 }
211             }
212
213             // Step 12.
214             // Apple Attestation Cont'
215             // Assemble the attestation object:
216             // https://www.w3.org/TR/webauthn/#attestation-object
217             // FIXME(183534): authData could throttle.
218             Vector<uint8_t> authData;
219             {
220                 // RP ID hash
221                 auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
222                 // FIXME(183534): Test IDN.
223                 ASSERT(options.rp.id.isAllASCII());
224                 auto asciiRpId = options.rp.id.ascii();
225                 crypto->addBytes(asciiRpId.data(), asciiRpId.length());
226                 authData = crypto->computeHash();
227
228                 // FLAGS
229                 authData.append(authenticatorDataFlags);
230
231                 // Step. 10.
232                 // COUNTER
233                 // FIXME(183533): store the counter.
234                 union {
235                     uint32_t integer;
236                     uint8_t bytes[4];
237                 } counter = {0x00000000};
238                 authData.append(counter.bytes, sizeof(counter.bytes));
239
240                 // Step 11.
241                 // AAGUID
242                 authData.append(AAGUID, sizeof(AAGUID));
243
244                 // L
245                 ASSERT(credentialId.size() <= maxCredentialIdLength);
246                 // Assume little endian here.
247                 union {
248                     uint16_t integer;
249                     uint8_t bytes[2];
250                 } credentialIdLength;
251                 credentialIdLength.integer = static_cast<uint16_t>(credentialId.size());
252                 authData.append(credentialIdLength.bytes, sizeof(uint16_t));
253
254                 // CREDENTIAL ID
255                 authData.appendVector(credentialId);
256
257                 // CREDENTIAL PUBLIC KEY
258                 CFDataRef publicKeyDataRef = NULL;
259                 {
260                     auto publicKey = adoptCF(SecKeyCopyPublicKey(privateKey));
261                     CFErrorRef errorRef = NULL;
262                     publicKeyDataRef = SecKeyCopyExternalRepresentation(publicKey.get(), &errorRef);
263                     auto retainError = adoptCF(errorRef);
264                     if (errorRef) {
265                         LOG_ERROR("Couldn't export the public key: %@", (NSError*)errorRef);
266                         exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
267                         return;
268                     }
269                     ASSERT(((NSData *)publicKeyDataRef).length == (1 + 2*ES256KeySizeInBytes)); // 04 | X | Y
270                 }
271                 auto retainPublicKeyData = adoptCF(publicKeyDataRef);
272
273                 // COSE Encoding
274                 // FIXME(183535): Improve CBOR encoder to work with bytes directly.
275                 Vector<uint8_t> x(ES256KeySizeInBytes);
276                 [(NSData *)publicKeyDataRef getBytes: x.data() range:NSMakeRange(1, ES256KeySizeInBytes)];
277                 Vector<uint8_t> y(ES256KeySizeInBytes);
278                 [(NSData *)publicKeyDataRef getBytes: y.data() range:NSMakeRange(1 + ES256KeySizeInBytes, ES256KeySizeInBytes)];
279                 cbor::CBORValue::MapValue publicKeyMap;
280                 publicKeyMap[cbor::CBORValue(COSE::kty)] = cbor::CBORValue(COSE::EC2);
281                 publicKeyMap[cbor::CBORValue(COSE::alg)] = cbor::CBORValue(COSE::ES256);
282                 publicKeyMap[cbor::CBORValue(COSE::crv)] = cbor::CBORValue(COSE::P_256);
283                 publicKeyMap[cbor::CBORValue(COSE::x)] = cbor::CBORValue(WTFMove(x));
284                 publicKeyMap[cbor::CBORValue(COSE::y)] = cbor::CBORValue(WTFMove(y));
285                 auto cosePublicKey = cbor::CBORWriter::write(cbor::CBORValue(WTFMove(publicKeyMap)));
286                 if (!cosePublicKey) {
287                     LOG_ERROR("Couldn't encode the public key into COSE binaries.");
288                     exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
289                     return;
290                 }
291                 authData.appendVector(cosePublicKey.value());
292             }
293
294             cbor::CBORValue::MapValue attestationStatementMap;
295             {
296                 Vector<uint8_t> signature;
297                 {
298                     CFErrorRef errorRef = NULL;
299                     // FIXME(183652): Reduce prompt for biometrics
300                     CFDataRef signatureRef = SecKeyCreateSignature(privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (__bridge CFDataRef)[NSData dataWithBytes:authData.data() length:authData.size()], &errorRef);
301                     auto retainError = adoptCF(errorRef);
302                     if (errorRef) {
303                         LOG_ERROR("Couldn't export the public key: %@", (NSError*)errorRef);
304                         exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
305                         return;
306                     }
307                     auto retainSignature = adoptCF(signatureRef);
308                     NSData *nsSignature = (NSData *)signatureRef;
309                     signature.append(static_cast<const uint8_t*>(nsSignature.bytes), nsSignature.length);
310                 }
311                 attestationStatementMap[cbor::CBORValue("alg")] = cbor::CBORValue(COSE::ES256);
312                 attestationStatementMap[cbor::CBORValue("sig")] = cbor::CBORValue(signature);
313                 Vector<cbor::CBORValue> cborArray;
314                 for (size_t i = 0; i < [certificates count]; i++) {
315                     CFDataRef dataRef = SecCertificateCopyData((__bridge SecCertificateRef)certificates[i]);
316                     auto retainData = adoptCF(dataRef);
317                     NSData *nsData = (NSData *)dataRef;
318                     Vector<uint8_t> data;
319                     data.append(static_cast<const uint8_t*>(nsData.bytes), nsData.length);
320                     cborArray.append(cbor::CBORValue(WTFMove(data)));
321                 }
322                 attestationStatementMap[cbor::CBORValue("x5c")] = cbor::CBORValue(WTFMove(cborArray));
323             }
324
325             cbor::CBORValue::MapValue attestationObjectMap;
326             attestationObjectMap[cbor::CBORValue("authData")] = cbor::CBORValue(authData);
327             attestationObjectMap[cbor::CBORValue("fmt")] = cbor::CBORValue("Apple");
328             attestationObjectMap[cbor::CBORValue("attStmt")] = cbor::CBORValue(WTFMove(attestationStatementMap));
329             auto attestationObject = cbor::CBORWriter::write(cbor::CBORValue(WTFMove(attestationObjectMap)));
330             if (!attestationObject) {
331                 LOG_ERROR("Couldn't encode the attestation object.");
332                 exceptionCallback({ UnknownError, ASCIILiteral("Unknown internal error.") });
333                 return;
334             }
335
336             callback(credentialId, attestationObject.value());
337         }).get());
338     }).get()];
339 #endif // !PLATFORM(IOS)
340 }
341
342 bool LocalAuthenticator::isAvailable() const
343 {
344 #if !PLATFORM(IOS)
345     // FIXME(182772)
346     return false;
347 #else
348     auto context = adoptNS([allocLAContextInstance() init]);
349     NSError *error = nil;
350
351     if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
352         LOG_ERROR("Couldn't evaluate authentication with biometrics policy: %@", error);
353         return true;
354     }
355     return true;
356 #endif // !PLATFORM(IOS)
357 }
358
359 void LocalAuthenticator::issueClientCertificate(const String& rpId, const String& username, const Vector<uint8_t>& hash, CompletionBlock _Nonnull completionHandler) const
360 {
361 // DeviceIdentity.Framework is not avaliable in iOS simulator.
362 #if !PLATFORM(IOS) || PLATFORM(IOS_SIMULATOR)
363     // FIXME(182772)
364     ASSERT_UNUSED(rpId, !rpId.isEmpty());
365     ASSERT_UNUSED(username, !username.isEmpty());
366     ASSERT_UNUSED(hash, !hash.isEmpty());
367     completionHandler(NULL, NULL, [NSError errorWithDomain:@"com.apple.WebKit.WebAuthN" code:1 userInfo:nil]);
368 #else
369     // Apple Attestation
370     ASSERT(hash.size() <= 32);
371
372     SecAccessControlRef accessControlRef;
373     {
374         CFErrorRef errorRef = NULL;
375         accessControlRef = SecAccessControlCreateWithFlags(NULL, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence, &errorRef);
376         auto retainError = adoptCF(errorRef);
377         if (errorRef) {
378             LOG_ERROR("Couldn't create ACL: %@", (NSError *)errorRef);
379             completionHandler(NULL, NULL, [NSError errorWithDomain:@"com.apple.WebKit.WebAuthN" code:1 userInfo:nil]);
380             return;
381         }
382     }
383     auto retainAccessControl = adoptCF(accessControlRef);
384
385     String label(username);
386     label.append("@" + rpId);
387     NSDictionary *options = @{
388         kMAOptionsBAAKeychainLabel: label,
389         // FIXME(rdar://problem/38489134): Need a formal name.
390         kMAOptionsBAAKeychainAccessGroup: @"com.apple.safari.WebAuthN.credentials",
391         kMAOptionsBAAIgnoreExistingKeychainItems: @(YES),
392         // FIXME(rdar://problem/38489134): Determine a proper lifespan.
393         kMAOptionsBAAValidity: @(1440), // Last one day.
394         kMAOptionsBAASCRTAttestation: @(NO),
395         kMAOptionsBAANonce: [NSData dataWithBytes:hash.data() length:hash.size()],
396         kMAOptionsBAAAccessControls: (id)accessControlRef,
397         kMAOptionsBAAOIDSToInclude: @[kMAOptionsBAAOIDNonce]
398     };
399
400     // FIXME(183652): Reduce prompt for biometrics
401     DeviceIdentityIssueClientCertificateWithCompletion(NULL, options, completionHandler);
402 #endif
403 }
404
405 } // namespace WebCore
406
407 #endif // ENABLE(WEB_AUTHN)