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#include "config.h"
27#include "AuthenticatorCoordinator.h"
28
29#if ENABLE(WEB_AUTHN)
30
31#include "AbortSignal.h"
32#include "AuthenticatorAssertionResponse.h"
33#include "AuthenticatorAttestationResponse.h"
34#include "AuthenticatorCoordinatorClient.h"
35#include "JSBasicCredential.h"
36#include "PublicKeyCredential.h"
37#include "PublicKeyCredentialCreationOptions.h"
38#include "PublicKeyCredentialData.h"
39#include "PublicKeyCredentialRequestOptions.h"
40#include "RegistrableDomain.h"
41#include "SchemeRegistry.h"
42#include "SecurityOrigin.h"
43#include <pal/crypto/CryptoDigest.h>
44#include <wtf/JSONValues.h>
45#include <wtf/NeverDestroyed.h>
46#include <wtf/text/Base64.h>
47
48namespace WebCore {
49
50namespace AuthenticatorCoordinatorInternal {
51
52enum class ClientDataType {
53 Create,
54 Get
55};
56
57// FIXME(181948): Add token binding ID and extensions.
58static Ref<ArrayBuffer> produceClientDataJson(ClientDataType type, const BufferSource& challenge, const SecurityOrigin& origin)
59{
60 auto object = JSON::Object::create();
61 switch (type) {
62 case ClientDataType::Create:
63 object->setString("type"_s, "webauthn.create"_s);
64 break;
65 case ClientDataType::Get:
66 object->setString("type"_s, "webauthn.get"_s);
67 break;
68 }
69 object->setString("challenge"_s, WTF::base64URLEncode(challenge.data(), challenge.length()));
70 object->setString("origin"_s, origin.toRawString());
71
72 auto utf8JSONString = object->toJSONString().utf8();
73 return ArrayBuffer::create(utf8JSONString.data(), utf8JSONString.length());
74}
75
76static Vector<uint8_t> produceClientDataJsonHash(const ArrayBuffer& clientDataJson)
77{
78 auto crypto = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
79 crypto->addBytes(clientDataJson.data(), clientDataJson.byteLength());
80 return crypto->computeHash();
81}
82
83static bool needsAppIdQuirks(const String& host, const String& appId)
84{
85 // FIXME(197524): Remove this quirk in 2023. As an early adopter of U2F features, Google has a large number of
86 // existing device registrations that authenticate 'google.com' against 'gstatic.com'. Firefox and other browsers
87 // have agreed to grant an exception to the AppId rules for a limited time period (5 years from January, 2018) to
88 // allow existing Google users to seamlessly transition to proper WebAuthN behavior.
89 if (equalLettersIgnoringASCIICase(host, "google.com") || host.endsWithIgnoringASCIICase(".google.com"))
90 return (appId == "https://www.gstatic.com/securitykey/origins.json"_s) || (appId == "https://www.gstatic.com/securitykey/a/google.com/origins.json"_s);
91 return false;
92}
93
94// The following roughly implements Step 1-3 of the spec to avoid the complexity of making unnecessary network requests:
95// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-if-a-caller-s-facetid-is-authorized-for-an-appid
96// It follows what Chrome and Firefox do, see:
97// https://bugzilla.mozilla.org/show_bug.cgi?id=1244959#c8
98// https://bugs.chromium.org/p/chromium/issues/detail?id=818303
99static String processAppIdExtension(const SecurityOrigin& facetId, const String& appId)
100{
101 // Step 1. Skipped since facetId should always be secure origins.
102 ASSERT(SchemeRegistry::shouldTreatURLSchemeAsSecure(facetId.protocol()));
103
104 // Step 2. Follow Chrome and Firefox to use the origin directly without adding a trailing slash.
105 if (appId.isEmpty())
106 return facetId.toString();
107
108 // Step 3. Relax the comparison to same site.
109 URL appIdURL(URL(), appId);
110 if (!appIdURL.isValid() || facetId.protocol() != appIdURL.protocol() || (RegistrableDomain(appIdURL) != RegistrableDomain::uncheckedCreateFromHost(facetId.host()) && !needsAppIdQuirks(facetId.host(), appId)))
111 return String();
112 return appId;
113}
114
115} // namespace AuthenticatorCoordinatorInternal
116
117AuthenticatorCoordinator::AuthenticatorCoordinator(std::unique_ptr<AuthenticatorCoordinatorClient>&& client)
118 : m_client(WTFMove(client))
119{
120}
121
122void AuthenticatorCoordinator::setClient(std::unique_ptr<AuthenticatorCoordinatorClient>&& client)
123{
124 m_client = WTFMove(client);
125}
126
127void AuthenticatorCoordinator::create(const SecurityOrigin& callerOrigin, const PublicKeyCredentialCreationOptions& options, bool sameOriginWithAncestors, RefPtr<AbortSignal>&& abortSignal, CredentialPromise&& promise) const
128{
129 using namespace AuthenticatorCoordinatorInternal;
130
131 // The following implements https://www.w3.org/TR/webauthn/#createCredential as of 5 December 2017.
132 // Extensions are not supported. Skip Step 11-12.
133 // Step 1, 3, 16 are handled by the caller.
134 // Step 2.
135 if (!sameOriginWithAncestors) {
136 promise.reject(Exception { NotAllowedError, "The origin of the document is not the same as its ancestors."_s });
137 return;
138 }
139
140 // Step 5. Skipped since SecurityOrigin doesn't have the concept of "opaque origin".
141 // Step 6. The effective domain may be represented in various manners, such as a domain or an ip address.
142 // Only the domain format of host is permitted in WebAuthN.
143 if (URL::hostIsIPAddress(callerOrigin.domain())) {
144 promise.reject(Exception { SecurityError, "The effective domain of the document is not a valid domain."_s });
145 return;
146 }
147
148 // Step 7.
149 if (!options.rp.id.isEmpty() && !callerOrigin.isMatchingRegistrableDomainSuffix(options.rp.id)) {
150 promise.reject(Exception { SecurityError, "The provided RP ID is not a registrable domain suffix of the effective domain of the document."_s });
151 return;
152 }
153 if (options.rp.id.isEmpty())
154 options.rp.id = callerOrigin.domain();
155
156 // Step 8-10.
157 // Most of the jobs are done by bindings. However, we can't know if the JSValue of options.pubKeyCredParams
158 // is empty or not. Return NotSupportedError as long as it is empty.
159 if (options.pubKeyCredParams.isEmpty()) {
160 promise.reject(Exception { NotSupportedError, "No desired properties of the to be created credential are provided."_s });
161 return;
162 }
163
164 // Step 13-15.
165 auto clientDataJson = produceClientDataJson(ClientDataType::Create, options.challenge, callerOrigin);
166 auto clientDataJsonHash = produceClientDataJsonHash(clientDataJson);
167
168 // Step 4, 17-21.
169 if (!m_client) {
170 promise.reject(Exception { UnknownError, "Unknown internal error."_s });
171 return;
172 }
173
174 auto completionHandler = [clientDataJson = WTFMove(clientDataJson), promise = WTFMove(promise), abortSignal = WTFMove(abortSignal)] (const WebCore::PublicKeyCredentialData& data, const WebCore::ExceptionData& exception) mutable {
175 if (abortSignal && abortSignal->aborted()) {
176 promise.reject(Exception { AbortError, "Aborted by AbortSignal."_s });
177 return;
178 }
179
180 data.clientDataJSON = WTFMove(clientDataJson);
181 if (auto publicKeyCredential = PublicKeyCredential::tryCreate(data)) {
182 promise.resolve(publicKeyCredential.get());
183 return;
184 }
185 ASSERT(!exception.message.isNull());
186 promise.reject(exception.toException());
187 };
188 // Async operations are dispatched and handled in the messenger.
189 m_client->makeCredential(clientDataJsonHash, options, WTFMove(completionHandler));
190}
191
192void AuthenticatorCoordinator::discoverFromExternalSource(const SecurityOrigin& callerOrigin, const PublicKeyCredentialRequestOptions& options, bool sameOriginWithAncestors, RefPtr<AbortSignal>&& abortSignal, CredentialPromise&& promise) const
193{
194 using namespace AuthenticatorCoordinatorInternal;
195
196 // The following implements https://www.w3.org/TR/webauthn/#createCredential as of 5 December 2017.
197 // Step 1, 3, 13 are handled by the caller.
198 // Step 2.
199 if (!sameOriginWithAncestors) {
200 promise.reject(Exception { NotAllowedError, "The origin of the document is not the same as its ancestors."_s });
201 return;
202 }
203
204 // Step 5. Skipped since SecurityOrigin doesn't have the concept of "opaque origin".
205 // Step 6. The effective domain may be represented in various manners, such as a domain or an ip address.
206 // Only the domain format of host is permitted in WebAuthN.
207 if (URL::hostIsIPAddress(callerOrigin.domain())) {
208 promise.reject(Exception { SecurityError, "The effective domain of the document is not a valid domain."_s });
209 return;
210 }
211
212 // Step 7.
213 if (!options.rpId.isEmpty() && !callerOrigin.isMatchingRegistrableDomainSuffix(options.rpId)) {
214 promise.reject(Exception { SecurityError, "The provided RP ID is not a registrable domain suffix of the effective domain of the document."_s });
215 return;
216 }
217 if (options.rpId.isEmpty())
218 options.rpId = callerOrigin.domain();
219
220 // Step 8-9.
221 // Only FIDO AppID Extension is supported.
222 if (options.extensions && !options.extensions->appid.isNull()) {
223 // The following implements https://www.w3.org/TR/webauthn/#sctn-appid-extension as of 4 March 2019.
224 auto appid = processAppIdExtension(callerOrigin, options.extensions->appid);
225 if (!appid) {
226 promise.reject(Exception { SecurityError, "The origin of the document is not authorized for the provided App ID."_s });
227 return;
228 }
229 options.extensions->appid = appid;
230 }
231
232 // Step 10-12.
233 auto clientDataJson = produceClientDataJson(ClientDataType::Get, options.challenge, callerOrigin);
234 auto clientDataJsonHash = produceClientDataJsonHash(clientDataJson);
235
236 // Step 4, 14-19.
237 if (!m_client) {
238 promise.reject(Exception { UnknownError, "Unknown internal error."_s });
239 return;
240 }
241
242 auto completionHandler = [clientDataJson = WTFMove(clientDataJson), promise = WTFMove(promise), abortSignal = WTFMove(abortSignal)] (const WebCore::PublicKeyCredentialData& data, const WebCore::ExceptionData& exception) mutable {
243 if (abortSignal && abortSignal->aborted()) {
244 promise.reject(Exception { AbortError, "Aborted by AbortSignal."_s });
245 return;
246 }
247
248 data.clientDataJSON = WTFMove(clientDataJson);
249 if (auto publicKeyCredential = PublicKeyCredential::tryCreate(data)) {
250 promise.resolve(publicKeyCredential.get());
251 return;
252 }
253 ASSERT(!exception.message.isNull());
254 promise.reject(exception.toException());
255 };
256 // Async operations are dispatched and handled in the messenger.
257 m_client->getAssertion(clientDataJsonHash, options, WTFMove(completionHandler));
258}
259
260void AuthenticatorCoordinator::isUserVerifyingPlatformAuthenticatorAvailable(DOMPromiseDeferred<IDLBoolean>&& promise) const
261{
262 // The following implements https://www.w3.org/TR/webauthn/#isUserVerifyingPlatformAuthenticatorAvailable
263 // as of 5 December 2017.
264 if (!m_client) {
265 promise.reject(Exception { UnknownError, "Unknown internal error."_s });
266 return;
267 }
268
269 // FIXME(182767): We should consider more on the assessment of the return value. Right now, we return true/false
270 // immediately according to platform specific procedures.
271 auto completionHandler = [promise = WTFMove(promise)] (bool result) mutable {
272 promise.resolve(result);
273 };
274 // Async operation are dispatched and handled in the messenger.
275 m_client->isUserVerifyingPlatformAuthenticatorAvailable(WTFMove(completionHandler));
276}
277
278} // namespace WebCore
279
280#endif // ENABLE(WEB_AUTHN)
281