/src/mozilla-central/dom/webauthn/WebAuthnManager.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
2 | | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ |
3 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
4 | | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
5 | | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
6 | | |
7 | | #include "hasht.h" |
8 | | #include "nsHTMLDocument.h" |
9 | | #include "nsIURIMutator.h" |
10 | | #include "nsThreadUtils.h" |
11 | | #include "WebAuthnCoseIdentifiers.h" |
12 | | #include "mozilla/dom/AuthenticatorAttestationResponse.h" |
13 | | #include "mozilla/dom/Promise.h" |
14 | | #include "mozilla/dom/PWebAuthnTransaction.h" |
15 | | #include "mozilla/dom/WebAuthnManager.h" |
16 | | #include "mozilla/dom/WebAuthnTransactionChild.h" |
17 | | #include "mozilla/dom/WebAuthnUtil.h" |
18 | | #include "mozilla/ipc/BackgroundChild.h" |
19 | | #include "mozilla/ipc/PBackgroundChild.h" |
20 | | |
21 | | using namespace mozilla::ipc; |
22 | | |
23 | | namespace mozilla { |
24 | | namespace dom { |
25 | | |
26 | | /*********************************************************************** |
27 | | * Statics |
28 | | **********************************************************************/ |
29 | | |
30 | | namespace { |
31 | | static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager"); |
32 | | } |
33 | | |
34 | | NS_IMPL_ISUPPORTS(WebAuthnManager, nsIDOMEventListener); |
35 | | |
36 | | /*********************************************************************** |
37 | | * Utility Functions |
38 | | **********************************************************************/ |
39 | | |
40 | | static nsresult |
41 | | AssembleClientData(const nsAString& aOrigin, |
42 | | const CryptoBuffer& aChallenge, |
43 | | const nsAString& aType, |
44 | | const AuthenticationExtensionsClientInputs& aExtensions, |
45 | | /* out */ nsACString& aJsonOut) |
46 | 0 | { |
47 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
48 | 0 |
|
49 | 0 | nsString challengeBase64; |
50 | 0 | nsresult rv = aChallenge.ToJwkBase64(challengeBase64); |
51 | 0 | if (NS_WARN_IF(NS_FAILED(rv))) { |
52 | 0 | return NS_ERROR_FAILURE; |
53 | 0 | } |
54 | 0 | |
55 | 0 | CollectedClientData clientDataObject; |
56 | 0 | clientDataObject.mType.Assign(aType); |
57 | 0 | clientDataObject.mChallenge.Assign(challengeBase64); |
58 | 0 | clientDataObject.mOrigin.Assign(aOrigin); |
59 | 0 | clientDataObject.mHashAlgorithm.AssignLiteral(u"SHA-256"); |
60 | 0 | clientDataObject.mClientExtensions = aExtensions; |
61 | 0 |
|
62 | 0 | nsAutoString temp; |
63 | 0 | if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) { |
64 | 0 | return NS_ERROR_FAILURE; |
65 | 0 | } |
66 | 0 | |
67 | 0 | aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp)); |
68 | 0 | return NS_OK; |
69 | 0 | } |
70 | | |
71 | | nsresult |
72 | | GetOrigin(nsPIDOMWindowInner* aParent, |
73 | | /*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost) |
74 | 0 | { |
75 | 0 | MOZ_ASSERT(aParent); |
76 | 0 | nsCOMPtr<nsIDocument> doc = aParent->GetDoc(); |
77 | 0 | MOZ_ASSERT(doc); |
78 | 0 |
|
79 | 0 | nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); |
80 | 0 | nsresult rv = nsContentUtils::GetUTFOrigin(principal, aOrigin); |
81 | 0 | if (NS_WARN_IF(NS_FAILED(rv)) || |
82 | 0 | NS_WARN_IF(aOrigin.IsEmpty())) { |
83 | 0 | return NS_ERROR_FAILURE; |
84 | 0 | } |
85 | 0 | |
86 | 0 | if (aOrigin.EqualsLiteral("null")) { |
87 | 0 | // 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a |
88 | 0 | // DOMException whose name is "NotAllowedError", and terminate this |
89 | 0 | // algorithm |
90 | 0 | MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug, ("Rejecting due to opaque origin")); |
91 | 0 | return NS_ERROR_DOM_NOT_ALLOWED_ERR; |
92 | 0 | } |
93 | 0 |
|
94 | 0 | nsCOMPtr<nsIURI> originUri; |
95 | 0 | if (NS_FAILED(principal->GetURI(getter_AddRefs(originUri)))) { |
96 | 0 | return NS_ERROR_FAILURE; |
97 | 0 | } |
98 | 0 | if (NS_FAILED(originUri->GetAsciiHost(aHost))) { |
99 | 0 | return NS_ERROR_FAILURE; |
100 | 0 | } |
101 | 0 | |
102 | 0 | return NS_OK; |
103 | 0 | } |
104 | | |
105 | | nsresult |
106 | | RelaxSameOrigin(nsPIDOMWindowInner* aParent, |
107 | | const nsAString& aInputRpId, |
108 | | /* out */ nsACString& aRelaxedRpId) |
109 | 0 | { |
110 | 0 | MOZ_ASSERT(aParent); |
111 | 0 | nsCOMPtr<nsIDocument> doc = aParent->GetDoc(); |
112 | 0 | MOZ_ASSERT(doc); |
113 | 0 |
|
114 | 0 | nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); |
115 | 0 | nsCOMPtr<nsIURI> uri; |
116 | 0 | if (NS_FAILED(principal->GetURI(getter_AddRefs(uri)))) { |
117 | 0 | return NS_ERROR_FAILURE; |
118 | 0 | } |
119 | 0 | nsAutoCString originHost; |
120 | 0 | if (NS_FAILED(uri->GetAsciiHost(originHost))) { |
121 | 0 | return NS_ERROR_FAILURE; |
122 | 0 | } |
123 | 0 | nsCOMPtr<nsIDocument> document = aParent->GetDoc(); |
124 | 0 | if (!document || !document->IsHTMLDocument()) { |
125 | 0 | return NS_ERROR_FAILURE; |
126 | 0 | } |
127 | 0 | nsHTMLDocument* html = document->AsHTMLDocument(); |
128 | 0 | // See if the given RP ID is a valid domain string. |
129 | 0 | // (We use the document's URI here as a template so we don't have to come up |
130 | 0 | // with our own scheme, etc. If we can successfully set the host as the given |
131 | 0 | // RP ID, then it should be a valid domain string.) |
132 | 0 | nsCOMPtr<nsIURI> inputRpIdURI; |
133 | 0 | nsresult rv = NS_MutateURI(uri) |
134 | 0 | .SetHost(NS_ConvertUTF16toUTF8(aInputRpId)) |
135 | 0 | .Finalize(inputRpIdURI); |
136 | 0 | if (NS_FAILED(rv)) { |
137 | 0 | return NS_ERROR_DOM_SECURITY_ERR; |
138 | 0 | } |
139 | 0 | nsAutoCString inputRpId; |
140 | 0 | if (NS_FAILED(inputRpIdURI->GetAsciiHost(inputRpId))) { |
141 | 0 | return NS_ERROR_FAILURE; |
142 | 0 | } |
143 | 0 | if (!html->IsRegistrableDomainSuffixOfOrEqualTo( |
144 | 0 | NS_ConvertUTF8toUTF16(inputRpId), originHost)) { |
145 | 0 | return NS_ERROR_DOM_SECURITY_ERR; |
146 | 0 | } |
147 | 0 | |
148 | 0 | aRelaxedRpId.Assign(inputRpId); |
149 | 0 | return NS_OK; |
150 | 0 | } |
151 | | |
152 | | /*********************************************************************** |
153 | | * WebAuthnManager Implementation |
154 | | **********************************************************************/ |
155 | | |
156 | | void |
157 | | WebAuthnManager::ClearTransaction() |
158 | 0 | { |
159 | 0 | if (!NS_WARN_IF(mTransaction.isNothing())) { |
160 | 0 | StopListeningForVisibilityEvents(); |
161 | 0 | } |
162 | 0 |
|
163 | 0 | mTransaction.reset(); |
164 | 0 | Unfollow(); |
165 | 0 | } |
166 | | |
167 | | void |
168 | | WebAuthnManager::RejectTransaction(const nsresult& aError) |
169 | 0 | { |
170 | 0 | if (!NS_WARN_IF(mTransaction.isNothing())) { |
171 | 0 | mTransaction.ref().mPromise->MaybeReject(aError); |
172 | 0 | } |
173 | 0 |
|
174 | 0 | ClearTransaction(); |
175 | 0 | } |
176 | | |
177 | | void |
178 | | WebAuthnManager::CancelTransaction(const nsresult& aError) |
179 | 0 | { |
180 | 0 | if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) { |
181 | 0 | mChild->SendRequestCancel(mTransaction.ref().mId); |
182 | 0 | } |
183 | 0 |
|
184 | 0 | RejectTransaction(aError); |
185 | 0 | } |
186 | | |
187 | | WebAuthnManager::~WebAuthnManager() |
188 | 0 | { |
189 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
190 | 0 |
|
191 | 0 | if (mTransaction.isSome()) { |
192 | 0 | RejectTransaction(NS_ERROR_ABORT); |
193 | 0 | } |
194 | 0 |
|
195 | 0 | if (mChild) { |
196 | 0 | RefPtr<WebAuthnTransactionChild> c; |
197 | 0 | mChild.swap(c); |
198 | 0 | c->Disconnect(); |
199 | 0 | } |
200 | 0 | } |
201 | | |
202 | | already_AddRefed<Promise> |
203 | | WebAuthnManager::MakeCredential(const PublicKeyCredentialCreationOptions& aOptions, |
204 | | const Optional<OwningNonNull<AbortSignal>>& aSignal) |
205 | 0 | { |
206 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
207 | 0 |
|
208 | 0 | if (mTransaction.isSome()) { |
209 | 0 | CancelTransaction(NS_ERROR_ABORT); |
210 | 0 | } |
211 | 0 |
|
212 | 0 | nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent); |
213 | 0 |
|
214 | 0 | ErrorResult rv; |
215 | 0 | RefPtr<Promise> promise = Promise::Create(global, rv); |
216 | 0 | if (rv.Failed()) { |
217 | 0 | return nullptr; |
218 | 0 | } |
219 | 0 | |
220 | 0 | // Abort the request if aborted flag is already set. |
221 | 0 | if (aSignal.WasPassed() && aSignal.Value().Aborted()) { |
222 | 0 | promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); |
223 | 0 | return promise.forget(); |
224 | 0 | } |
225 | 0 | |
226 | 0 | nsString origin; |
227 | 0 | nsCString rpId; |
228 | 0 | rv = GetOrigin(mParent, origin, rpId); |
229 | 0 | if (NS_WARN_IF(rv.Failed())) { |
230 | 0 | promise->MaybeReject(rv); |
231 | 0 | return promise.forget(); |
232 | 0 | } |
233 | 0 | |
234 | 0 | // Enforce 5.4.3 User Account Parameters for Credential Generation |
235 | 0 | // When we add UX, we'll want to do more with this value, but for now |
236 | 0 | // we just have to verify its correctness. |
237 | 0 | { |
238 | 0 | CryptoBuffer userId; |
239 | 0 | userId.Assign(aOptions.mUser.mId); |
240 | 0 | if (userId.Length() > 64) { |
241 | 0 | promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR); |
242 | 0 | return promise.forget(); |
243 | 0 | } |
244 | 0 | } |
245 | 0 | |
246 | 0 | // If timeoutSeconds was specified, check if its value lies within a |
247 | 0 | // reasonable range as defined by the platform and if not, correct it to the |
248 | 0 | // closest value lying within that range. |
249 | 0 | |
250 | 0 | uint32_t adjustedTimeout = 30000; |
251 | 0 | if (aOptions.mTimeout.WasPassed()) { |
252 | 0 | adjustedTimeout = aOptions.mTimeout.Value(); |
253 | 0 | adjustedTimeout = std::max(15000u, adjustedTimeout); |
254 | 0 | adjustedTimeout = std::min(120000u, adjustedTimeout); |
255 | 0 | } |
256 | 0 |
|
257 | 0 | if (aOptions.mRp.mId.WasPassed()) { |
258 | 0 | // If rpId is specified, then invoke the procedure used for relaxing the |
259 | 0 | // same-origin restriction by setting the document.domain attribute, using |
260 | 0 | // rpId as the given value but without changing the current document’s |
261 | 0 | // domain. If no errors are thrown, set rpId to the value of host as |
262 | 0 | // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId. |
263 | 0 | // Otherwise, reject promise with a DOMException whose name is |
264 | 0 | // "SecurityError", and terminate this algorithm. |
265 | 0 |
|
266 | 0 | if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRp.mId.Value(), rpId))) { |
267 | 0 | promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); |
268 | 0 | return promise.forget(); |
269 | 0 | } |
270 | 0 | } |
271 | 0 | |
272 | 0 | // <https://w3c.github.io/webauthn/#sctn-appid-extension> |
273 | 0 | if (aOptions.mExtensions.mAppid.WasPassed()) { |
274 | 0 | promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); |
275 | 0 | return promise.forget(); |
276 | 0 | } |
277 | 0 | |
278 | 0 | // TODO: Move this logic into U2FTokenManager in Bug 1409220. |
279 | 0 | |
280 | 0 | // Process each element of mPubKeyCredParams using the following steps, to |
281 | 0 | // produce a new sequence acceptableParams. |
282 | 0 | nsTArray<PublicKeyCredentialParameters> acceptableParams; |
283 | 0 | for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) { |
284 | 0 | // Let current be the currently selected element of |
285 | 0 | // mPubKeyCredParams. |
286 | 0 |
|
287 | 0 | // If current.type does not contain a PublicKeyCredentialType |
288 | 0 | // supported by this implementation, then stop processing current and move |
289 | 0 | // on to the next element in mPubKeyCredParams. |
290 | 0 | if (aOptions.mPubKeyCredParams[a].mType != PublicKeyCredentialType::Public_key) { |
291 | 0 | continue; |
292 | 0 | } |
293 | 0 | |
294 | 0 | nsString algName; |
295 | 0 | if (NS_FAILED(CoseAlgorithmToWebCryptoId(aOptions.mPubKeyCredParams[a].mAlg, |
296 | 0 | algName))) { |
297 | 0 | continue; |
298 | 0 | } |
299 | 0 | |
300 | 0 | if (!acceptableParams.AppendElement(aOptions.mPubKeyCredParams[a], |
301 | 0 | mozilla::fallible)){ |
302 | 0 | promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); |
303 | 0 | return promise.forget(); |
304 | 0 | } |
305 | 0 | } |
306 | 0 |
|
307 | 0 | // If acceptableParams is empty and mPubKeyCredParams was not empty, cancel |
308 | 0 | // the timer started in step 2, reject promise with a DOMException whose name |
309 | 0 | // is "NotSupportedError", and terminate this algorithm. |
310 | 0 | if (acceptableParams.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) { |
311 | 0 | promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); |
312 | 0 | return promise.forget(); |
313 | 0 | } |
314 | 0 | |
315 | 0 | // If excludeList is undefined, set it to the empty list. |
316 | 0 | // |
317 | 0 | // If extensions was specified, process any extensions supported by this |
318 | 0 | // client platform, to produce the extension data that needs to be sent to the |
319 | 0 | // authenticator. If an error is encountered while processing an extension, |
320 | 0 | // skip that extension and do not produce any extension data for it. Call the |
321 | 0 | // result of this processing clientExtensions. |
322 | 0 | // |
323 | 0 | // Currently no extensions are supported |
324 | 0 | // |
325 | 0 | // Use attestationChallenge, callerOrigin and rpId, along with the token |
326 | 0 | // binding key associated with callerOrigin (if any), to create a ClientData |
327 | 0 | // structure representing this request. Choose a hash algorithm for hashAlg |
328 | 0 | // and compute the clientDataJSON and clientDataHash. |
329 | 0 | |
330 | 0 | CryptoBuffer challenge; |
331 | 0 | if (!challenge.Assign(aOptions.mChallenge)) { |
332 | 0 | promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); |
333 | 0 | return promise.forget(); |
334 | 0 | } |
335 | 0 | |
336 | 0 | nsAutoCString clientDataJSON; |
337 | 0 | nsresult srv = AssembleClientData(origin, challenge, |
338 | 0 | NS_LITERAL_STRING("webauthn.create"), |
339 | 0 | aOptions.mExtensions, clientDataJSON); |
340 | 0 | if (NS_WARN_IF(NS_FAILED(srv))) { |
341 | 0 | promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); |
342 | 0 | return promise.forget(); |
343 | 0 | } |
344 | 0 | |
345 | 0 | nsTArray<WebAuthnScopedCredential> excludeList; |
346 | 0 | for (const auto& s: aOptions.mExcludeCredentials) { |
347 | 0 | WebAuthnScopedCredential c; |
348 | 0 | CryptoBuffer cb; |
349 | 0 | cb.Assign(s.mId); |
350 | 0 | c.id() = cb; |
351 | 0 | excludeList.AppendElement(c); |
352 | 0 | } |
353 | 0 |
|
354 | 0 | if (!MaybeCreateBackgroundActor()) { |
355 | 0 | promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); |
356 | 0 | return promise.forget(); |
357 | 0 | } |
358 | 0 | |
359 | 0 | // TODO: Add extension list building |
360 | 0 | nsTArray<WebAuthnExtension> extensions; |
361 | 0 |
|
362 | 0 | const auto& selection = aOptions.mAuthenticatorSelection; |
363 | 0 | const auto& attachment = selection.mAuthenticatorAttachment; |
364 | 0 | const AttestationConveyancePreference& attestation = aOptions.mAttestation; |
365 | 0 |
|
366 | 0 | // Does the RP require attachment == "platform"? |
367 | 0 | bool requirePlatformAttachment = |
368 | 0 | attachment.WasPassed() && attachment.Value() == AuthenticatorAttachment::Platform; |
369 | 0 |
|
370 | 0 | // Does the RP require user verification? |
371 | 0 | bool requireUserVerification = |
372 | 0 | selection.mUserVerification == UserVerificationRequirement::Required; |
373 | 0 |
|
374 | 0 | // Does the RP desire direct attestation? Indirect attestation is not |
375 | 0 | // implemented, and thus is equivilent to None. |
376 | 0 | bool requestDirectAttestation = |
377 | 0 | attestation == AttestationConveyancePreference::Direct; |
378 | 0 |
|
379 | 0 | // Create and forward authenticator selection criteria. |
380 | 0 | WebAuthnAuthenticatorSelection authSelection(selection.mRequireResidentKey, |
381 | 0 | requireUserVerification, |
382 | 0 | requirePlatformAttachment); |
383 | 0 |
|
384 | 0 | WebAuthnMakeCredentialExtraInfo extra(extensions, |
385 | 0 | authSelection, |
386 | 0 | requestDirectAttestation); |
387 | 0 |
|
388 | 0 | WebAuthnMakeCredentialInfo info(origin, |
389 | 0 | NS_ConvertUTF8toUTF16(rpId), |
390 | 0 | challenge, |
391 | 0 | clientDataJSON, |
392 | 0 | adjustedTimeout, |
393 | 0 | excludeList, |
394 | 0 | extra); |
395 | 0 |
|
396 | 0 | ListenForVisibilityEvents(); |
397 | 0 |
|
398 | 0 | AbortSignal* signal = nullptr; |
399 | 0 | if (aSignal.WasPassed()) { |
400 | 0 | signal = &aSignal.Value(); |
401 | 0 | Follow(signal); |
402 | 0 | } |
403 | 0 |
|
404 | 0 | MOZ_ASSERT(mTransaction.isNothing()); |
405 | 0 | mTransaction = Some(WebAuthnTransaction(promise)); |
406 | 0 | mChild->SendRequestRegister(mTransaction.ref().mId, info); |
407 | 0 |
|
408 | 0 | return promise.forget(); |
409 | 0 | } |
410 | | |
411 | | already_AddRefed<Promise> |
412 | | WebAuthnManager::GetAssertion(const PublicKeyCredentialRequestOptions& aOptions, |
413 | | const Optional<OwningNonNull<AbortSignal>>& aSignal) |
414 | 0 | { |
415 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
416 | 0 |
|
417 | 0 | if (mTransaction.isSome()) { |
418 | 0 | CancelTransaction(NS_ERROR_ABORT); |
419 | 0 | } |
420 | 0 |
|
421 | 0 | nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent); |
422 | 0 |
|
423 | 0 | ErrorResult rv; |
424 | 0 | RefPtr<Promise> promise = Promise::Create(global, rv); |
425 | 0 | if (rv.Failed()) { |
426 | 0 | return nullptr; |
427 | 0 | } |
428 | 0 | |
429 | 0 | // Abort the request if aborted flag is already set. |
430 | 0 | if (aSignal.WasPassed() && aSignal.Value().Aborted()) { |
431 | 0 | promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); |
432 | 0 | return promise.forget(); |
433 | 0 | } |
434 | 0 | |
435 | 0 | nsString origin; |
436 | 0 | nsCString rpId; |
437 | 0 | rv = GetOrigin(mParent, origin, rpId); |
438 | 0 | if (NS_WARN_IF(rv.Failed())) { |
439 | 0 | promise->MaybeReject(rv); |
440 | 0 | return promise.forget(); |
441 | 0 | } |
442 | 0 | |
443 | 0 | // If timeoutSeconds was specified, check if its value lies within a |
444 | 0 | // reasonable range as defined by the platform and if not, correct it to the |
445 | 0 | // closest value lying within that range. |
446 | 0 | |
447 | 0 | uint32_t adjustedTimeout = 30000; |
448 | 0 | if (aOptions.mTimeout.WasPassed()) { |
449 | 0 | adjustedTimeout = aOptions.mTimeout.Value(); |
450 | 0 | adjustedTimeout = std::max(15000u, adjustedTimeout); |
451 | 0 | adjustedTimeout = std::min(120000u, adjustedTimeout); |
452 | 0 | } |
453 | 0 |
|
454 | 0 | if (aOptions.mRpId.WasPassed()) { |
455 | 0 | // If rpId is specified, then invoke the procedure used for relaxing the |
456 | 0 | // same-origin restriction by setting the document.domain attribute, using |
457 | 0 | // rpId as the given value but without changing the current document’s |
458 | 0 | // domain. If no errors are thrown, set rpId to the value of host as |
459 | 0 | // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId. |
460 | 0 | // Otherwise, reject promise with a DOMException whose name is |
461 | 0 | // "SecurityError", and terminate this algorithm. |
462 | 0 |
|
463 | 0 | if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRpId.Value(), rpId))) { |
464 | 0 | promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); |
465 | 0 | return promise.forget(); |
466 | 0 | } |
467 | 0 | } |
468 | 0 | |
469 | 0 | CryptoBuffer rpIdHash; |
470 | 0 | if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) { |
471 | 0 | promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); |
472 | 0 | return promise.forget(); |
473 | 0 | } |
474 | 0 | |
475 | 0 | // Use assertionChallenge, callerOrigin and rpId, along with the token binding |
476 | 0 | // key associated with callerOrigin (if any), to create a ClientData structure |
477 | 0 | // representing this request. Choose a hash algorithm for hashAlg and compute |
478 | 0 | // the clientDataJSON and clientDataHash. |
479 | 0 | CryptoBuffer challenge; |
480 | 0 | if (!challenge.Assign(aOptions.mChallenge)) { |
481 | 0 | promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); |
482 | 0 | return promise.forget(); |
483 | 0 | } |
484 | 0 | |
485 | 0 | nsAutoCString clientDataJSON; |
486 | 0 | nsresult srv = AssembleClientData(origin, challenge, |
487 | 0 | NS_LITERAL_STRING("webauthn.get"), |
488 | 0 | aOptions.mExtensions, clientDataJSON); |
489 | 0 | if (NS_WARN_IF(NS_FAILED(srv))) { |
490 | 0 | promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); |
491 | 0 | return promise.forget(); |
492 | 0 | } |
493 | 0 | |
494 | 0 | nsTArray<WebAuthnScopedCredential> allowList; |
495 | 0 | for (const auto& s: aOptions.mAllowCredentials) { |
496 | 0 | if (s.mType == PublicKeyCredentialType::Public_key) { |
497 | 0 | WebAuthnScopedCredential c; |
498 | 0 | CryptoBuffer cb; |
499 | 0 | cb.Assign(s.mId); |
500 | 0 | c.id() = cb; |
501 | 0 |
|
502 | 0 | // Serialize transports. |
503 | 0 | if (s.mTransports.WasPassed()) { |
504 | 0 | uint8_t transports = 0; |
505 | 0 | for (const auto& t: s.mTransports.Value()) { |
506 | 0 | if (t == AuthenticatorTransport::Usb) { |
507 | 0 | transports |= U2F_AUTHENTICATOR_TRANSPORT_USB; |
508 | 0 | } |
509 | 0 | if (t == AuthenticatorTransport::Nfc) { |
510 | 0 | transports |= U2F_AUTHENTICATOR_TRANSPORT_NFC; |
511 | 0 | } |
512 | 0 | if (t == AuthenticatorTransport::Ble) { |
513 | 0 | transports |= U2F_AUTHENTICATOR_TRANSPORT_BLE; |
514 | 0 | } |
515 | 0 | } |
516 | 0 | c.transports() = transports; |
517 | 0 | } |
518 | 0 |
|
519 | 0 | allowList.AppendElement(c); |
520 | 0 | } |
521 | 0 | } |
522 | 0 |
|
523 | 0 | if (!MaybeCreateBackgroundActor()) { |
524 | 0 | promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); |
525 | 0 | return promise.forget(); |
526 | 0 | } |
527 | 0 | |
528 | 0 | // Does the RP require user verification? |
529 | 0 | bool requireUserVerification = |
530 | 0 | aOptions.mUserVerification == UserVerificationRequirement::Required; |
531 | 0 |
|
532 | 0 | // If extensions were specified, process any extensions supported by this |
533 | 0 | // client platform, to produce the extension data that needs to be sent to the |
534 | 0 | // authenticator. If an error is encountered while processing an extension, |
535 | 0 | // skip that extension and do not produce any extension data for it. Call the |
536 | 0 | // result of this processing clientExtensions. |
537 | 0 | nsTArray<WebAuthnExtension> extensions; |
538 | 0 |
|
539 | 0 | // <https://w3c.github.io/webauthn/#sctn-appid-extension> |
540 | 0 | if (aOptions.mExtensions.mAppid.WasPassed()) { |
541 | 0 | nsString appId(aOptions.mExtensions.mAppid.Value()); |
542 | 0 |
|
543 | 0 | // Check that the appId value is allowed. |
544 | 0 | if (!EvaluateAppID(mParent, origin, U2FOperation::Sign, appId)) { |
545 | 0 | promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); |
546 | 0 | return promise.forget(); |
547 | 0 | } |
548 | 0 | |
549 | 0 | CryptoBuffer appIdHash; |
550 | 0 | if (!appIdHash.SetLength(SHA256_LENGTH, fallible)) { |
551 | 0 | promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); |
552 | 0 | return promise.forget(); |
553 | 0 | } |
554 | 0 | |
555 | 0 | // We need the SHA-256 hash of the appId. |
556 | 0 | srv = HashCString(NS_ConvertUTF16toUTF8(appId), appIdHash); |
557 | 0 | if (NS_WARN_IF(NS_FAILED(srv))) { |
558 | 0 | promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); |
559 | 0 | return promise.forget(); |
560 | 0 | } |
561 | 0 | |
562 | 0 | // Append the hash and send it to the backend. |
563 | 0 | extensions.AppendElement(WebAuthnExtensionAppId(appIdHash)); |
564 | 0 | } |
565 | 0 |
|
566 | 0 | WebAuthnGetAssertionExtraInfo extra(extensions, requireUserVerification); |
567 | 0 |
|
568 | 0 | WebAuthnGetAssertionInfo info(origin, |
569 | 0 | NS_ConvertUTF8toUTF16(rpId), |
570 | 0 | challenge, |
571 | 0 | clientDataJSON, |
572 | 0 | adjustedTimeout, |
573 | 0 | allowList, |
574 | 0 | extra); |
575 | 0 |
|
576 | 0 | ListenForVisibilityEvents(); |
577 | 0 |
|
578 | 0 | AbortSignal* signal = nullptr; |
579 | 0 | if (aSignal.WasPassed()) { |
580 | 0 | signal = &aSignal.Value(); |
581 | 0 | Follow(signal); |
582 | 0 | } |
583 | 0 |
|
584 | 0 | MOZ_ASSERT(mTransaction.isNothing()); |
585 | 0 | mTransaction = Some(WebAuthnTransaction(promise)); |
586 | 0 | mChild->SendRequestSign(mTransaction.ref().mId, info); |
587 | 0 |
|
588 | 0 | return promise.forget(); |
589 | 0 | } |
590 | | |
591 | | already_AddRefed<Promise> |
592 | | WebAuthnManager::Store(const Credential& aCredential) |
593 | 0 | { |
594 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
595 | 0 |
|
596 | 0 | if (mTransaction.isSome()) { |
597 | 0 | CancelTransaction(NS_ERROR_ABORT); |
598 | 0 | } |
599 | 0 |
|
600 | 0 | nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent); |
601 | 0 |
|
602 | 0 | ErrorResult rv; |
603 | 0 | RefPtr<Promise> promise = Promise::Create(global, rv); |
604 | 0 | if (rv.Failed()) { |
605 | 0 | return nullptr; |
606 | 0 | } |
607 | 0 | |
608 | 0 | promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); |
609 | 0 | return promise.forget(); |
610 | 0 | } |
611 | | |
612 | | void |
613 | | WebAuthnManager::FinishMakeCredential(const uint64_t& aTransactionId, |
614 | | const WebAuthnMakeCredentialResult& aResult) |
615 | 0 | { |
616 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
617 | 0 |
|
618 | 0 | // Check for a valid transaction. |
619 | 0 | if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) { |
620 | 0 | return; |
621 | 0 | } |
622 | 0 | |
623 | 0 | CryptoBuffer clientDataBuf; |
624 | 0 | if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) { |
625 | 0 | RejectTransaction(NS_ERROR_OUT_OF_MEMORY); |
626 | 0 | return; |
627 | 0 | } |
628 | 0 | |
629 | 0 | CryptoBuffer attObjBuf; |
630 | 0 | if (NS_WARN_IF(!attObjBuf.Assign(aResult.AttestationObject()))) { |
631 | 0 | RejectTransaction(NS_ERROR_OUT_OF_MEMORY); |
632 | 0 | return; |
633 | 0 | } |
634 | 0 | |
635 | 0 | CryptoBuffer keyHandleBuf; |
636 | 0 | if (NS_WARN_IF(!keyHandleBuf.Assign(aResult.KeyHandle()))) { |
637 | 0 | RejectTransaction(NS_ERROR_OUT_OF_MEMORY); |
638 | 0 | return; |
639 | 0 | } |
640 | 0 | |
641 | 0 | nsAutoString keyHandleBase64Url; |
642 | 0 | nsresult rv = keyHandleBuf.ToJwkBase64(keyHandleBase64Url); |
643 | 0 | if (NS_WARN_IF(NS_FAILED(rv))) { |
644 | 0 | RejectTransaction(rv); |
645 | 0 | return; |
646 | 0 | } |
647 | 0 | |
648 | 0 | // Create a new PublicKeyCredential object and populate its fields with the |
649 | 0 | // values returned from the authenticator as well as the clientDataJSON |
650 | 0 | // computed earlier. |
651 | 0 | RefPtr<AuthenticatorAttestationResponse> attestation = |
652 | 0 | new AuthenticatorAttestationResponse(mParent); |
653 | 0 | attestation->SetClientDataJSON(clientDataBuf); |
654 | 0 | attestation->SetAttestationObject(attObjBuf); |
655 | 0 |
|
656 | 0 | RefPtr<PublicKeyCredential> credential = |
657 | 0 | new PublicKeyCredential(mParent); |
658 | 0 | credential->SetId(keyHandleBase64Url); |
659 | 0 | credential->SetType(NS_LITERAL_STRING("public-key")); |
660 | 0 | credential->SetRawId(keyHandleBuf); |
661 | 0 | credential->SetResponse(attestation); |
662 | 0 |
|
663 | 0 | mTransaction.ref().mPromise->MaybeResolve(credential); |
664 | 0 | ClearTransaction(); |
665 | 0 | } |
666 | | |
667 | | void |
668 | | WebAuthnManager::FinishGetAssertion(const uint64_t& aTransactionId, |
669 | | const WebAuthnGetAssertionResult& aResult) |
670 | 0 | { |
671 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
672 | 0 |
|
673 | 0 | // Check for a valid transaction. |
674 | 0 | if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) { |
675 | 0 | return; |
676 | 0 | } |
677 | 0 | |
678 | 0 | CryptoBuffer clientDataBuf; |
679 | 0 | if (!clientDataBuf.Assign(aResult.ClientDataJSON())) { |
680 | 0 | RejectTransaction(NS_ERROR_OUT_OF_MEMORY); |
681 | 0 | return; |
682 | 0 | } |
683 | 0 | |
684 | 0 | CryptoBuffer credentialBuf; |
685 | 0 | if (!credentialBuf.Assign(aResult.KeyHandle())) { |
686 | 0 | RejectTransaction(NS_ERROR_OUT_OF_MEMORY); |
687 | 0 | return; |
688 | 0 | } |
689 | 0 | |
690 | 0 | CryptoBuffer signatureBuf; |
691 | 0 | if (!signatureBuf.Assign(aResult.Signature())) { |
692 | 0 | RejectTransaction(NS_ERROR_OUT_OF_MEMORY); |
693 | 0 | return; |
694 | 0 | } |
695 | 0 | |
696 | 0 | CryptoBuffer authenticatorDataBuf; |
697 | 0 | if (!authenticatorDataBuf.Assign(aResult.AuthenticatorData())) { |
698 | 0 | RejectTransaction(NS_ERROR_OUT_OF_MEMORY); |
699 | 0 | return; |
700 | 0 | } |
701 | 0 | |
702 | 0 | nsAutoString credentialBase64Url; |
703 | 0 | nsresult rv = credentialBuf.ToJwkBase64(credentialBase64Url); |
704 | 0 | if (NS_WARN_IF(NS_FAILED(rv))) { |
705 | 0 | RejectTransaction(rv); |
706 | 0 | return; |
707 | 0 | } |
708 | 0 | |
709 | 0 | // If any authenticator returns success: |
710 | 0 | |
711 | 0 | // Create a new PublicKeyCredential object named value and populate its fields |
712 | 0 | // with the values returned from the authenticator as well as the |
713 | 0 | // clientDataJSON computed earlier. |
714 | 0 | RefPtr<AuthenticatorAssertionResponse> assertion = |
715 | 0 | new AuthenticatorAssertionResponse(mParent); |
716 | 0 | assertion->SetClientDataJSON(clientDataBuf); |
717 | 0 | assertion->SetAuthenticatorData(authenticatorDataBuf); |
718 | 0 | assertion->SetSignature(signatureBuf); |
719 | 0 |
|
720 | 0 | RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent); |
721 | 0 | credential->SetId(credentialBase64Url); |
722 | 0 | credential->SetType(NS_LITERAL_STRING("public-key")); |
723 | 0 | credential->SetRawId(credentialBuf); |
724 | 0 | credential->SetResponse(assertion); |
725 | 0 |
|
726 | 0 | // Forward client extension results. |
727 | 0 | for (auto& ext: aResult.Extensions()) { |
728 | 0 | if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) { |
729 | 0 | bool appid = ext.get_WebAuthnExtensionResultAppId().AppId(); |
730 | 0 | credential->SetClientExtensionResultAppId(appid); |
731 | 0 | } |
732 | 0 | } |
733 | 0 |
|
734 | 0 | mTransaction.ref().mPromise->MaybeResolve(credential); |
735 | 0 | ClearTransaction(); |
736 | 0 | } |
737 | | |
738 | | void |
739 | | WebAuthnManager::RequestAborted(const uint64_t& aTransactionId, |
740 | | const nsresult& aError) |
741 | 0 | { |
742 | 0 | MOZ_ASSERT(NS_IsMainThread()); |
743 | 0 |
|
744 | 0 | if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) { |
745 | 0 | RejectTransaction(aError); |
746 | 0 | } |
747 | 0 | } |
748 | | |
749 | | void |
750 | | WebAuthnManager::Abort() |
751 | 0 | { |
752 | 0 | CancelTransaction(NS_ERROR_DOM_ABORT_ERR); |
753 | 0 | } |
754 | | |
755 | | } |
756 | | } |