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/. */
7 #include "mozilla/dom/WebAuthnUtil.h"
8 #include "mozilla/dom/WebAuthnCBORUtil.h"
9 #include "nsComponentManagerUtils.h"
10 #include "nsICryptoHash.h"
11 #include "nsIEffectiveTLDService.h"
12 #include "nsNetUtil.h"
13 #include "mozpkix/pkixutil.h"
14 #include "nsHTMLDocument.h"
17 namespace mozilla::dom
{
19 // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
20 constexpr auto kGoogleAccountsAppId1
=
21 u
"https://www.gstatic.com/securitykey/origins.json"_ns
;
22 constexpr auto kGoogleAccountsAppId2
=
23 u
"https://www.gstatic.com/securitykey/a/google.com/origins.json"_ns
;
25 const uint8_t FLAG_TUP
= 0x01; // Test of User Presence required
26 const uint8_t FLAG_AT
= 0x40; // Authenticator Data is provided
28 bool EvaluateAppID(nsPIDOMWindowInner
* aParent
, const nsString
& aOrigin
,
29 /* in/out */ nsString
& aAppId
) {
30 // Facet is the specification's way of referring to the web origin.
31 nsAutoCString facetString
= NS_ConvertUTF16toUTF8(aOrigin
);
32 nsCOMPtr
<nsIURI
> facetUri
;
33 if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri
), facetString
))) {
37 // If the facetId (origin) is not HTTPS, reject
38 if (!facetUri
->SchemeIs("https")) {
42 // If the appId is empty or null, overwrite it with the facetId and accept
43 if (aAppId
.IsEmpty() || aAppId
.EqualsLiteral("null")) {
44 aAppId
.Assign(aOrigin
);
48 // AppID is user-supplied. It's quite possible for this parse to fail.
49 nsAutoCString appIdString
= NS_ConvertUTF16toUTF8(aAppId
);
50 nsCOMPtr
<nsIURI
> appIdUri
;
51 if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri
), appIdString
))) {
55 // if the appId URL is not HTTPS, reject.
56 if (!appIdUri
->SchemeIs("https")) {
60 nsAutoCString appIdHost
;
61 if (NS_FAILED(appIdUri
->GetAsciiHost(appIdHost
))) {
66 if (appIdHost
.EqualsLiteral("localhost")) {
67 nsAutoCString facetHost
;
68 if (NS_FAILED(facetUri
->GetAsciiHost(facetHost
))) {
72 if (facetHost
.EqualsLiteral("localhost")) {
77 // Run the HTML5 algorithm to relax the same-origin policy, copied from W3C
78 // Web Authentication. See Bug 1244959 comment #8 for context on why we are
79 // doing this instead of implementing the external-fetch FacetID logic.
80 nsCOMPtr
<Document
> document
= aParent
->GetDoc();
81 if (!document
|| !document
->IsHTMLDocument()) {
85 nsHTMLDocument
* html
= document
->AsHTMLDocument();
86 // Use the base domain as the facet for evaluation. This lets this algorithm
87 // relax the whole eTLD+1.
88 nsCOMPtr
<nsIEffectiveTLDService
> tldService
=
89 do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID
);
94 nsAutoCString lowestFacetHost
;
95 if (NS_FAILED(tldService
->GetBaseDomain(facetUri
, 0, lowestFacetHost
))) {
99 if (html
->IsRegistrableDomainSuffixOfOrEqualTo(
100 NS_ConvertUTF8toUTF16(lowestFacetHost
), appIdHost
)) {
104 // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
105 if (lowestFacetHost
.EqualsLiteral("google.com") &&
106 (aAppId
.Equals(kGoogleAccountsAppId1
) ||
107 aAppId
.Equals(kGoogleAccountsAppId2
))) {
114 nsresult
ReadToCryptoBuffer(pkix::Reader
& aSrc
, /* out */ CryptoBuffer
& aDest
,
116 if (aSrc
.EnsureLength(aLen
) != pkix::Success
) {
117 return NS_ERROR_DOM_UNKNOWN_ERR
;
120 if (!aDest
.SetCapacity(aLen
, mozilla::fallible
)) {
121 return NS_ERROR_OUT_OF_MEMORY
;
124 for (uint32_t offset
= 0; offset
< aLen
; ++offset
) {
126 if (aSrc
.Read(b
) != pkix::Success
) {
127 return NS_ERROR_DOM_UNKNOWN_ERR
;
129 if (!aDest
.AppendElement(b
, mozilla::fallible
)) {
130 return NS_ERROR_OUT_OF_MEMORY
;
138 // 32 bytes: SHA256 of the RP ID
139 // 1 byte: flags (TUP & AT)
140 // 4 bytes: sign counter
141 // variable: attestation data struct
142 // variable: CBOR-format extension auth data (optional, not flagged)
143 nsresult
AssembleAuthenticatorData(const CryptoBuffer
& rpIdHashBuf
,
145 const CryptoBuffer
& counterBuf
,
146 const CryptoBuffer
& attestationDataBuf
,
147 /* out */ CryptoBuffer
& authDataBuf
) {
148 if (NS_WARN_IF(!authDataBuf
.SetCapacity(
149 32 + 1 + 4 + attestationDataBuf
.Length(), mozilla::fallible
))) {
150 return NS_ERROR_OUT_OF_MEMORY
;
152 if (rpIdHashBuf
.Length() != 32 || counterBuf
.Length() != 4) {
153 return NS_ERROR_INVALID_ARG
;
156 (void)authDataBuf
.AppendElements(rpIdHashBuf
, mozilla::fallible
);
157 (void)authDataBuf
.AppendElement(flags
, mozilla::fallible
);
158 (void)authDataBuf
.AppendElements(counterBuf
, mozilla::fallible
);
159 (void)authDataBuf
.AppendElements(attestationDataBuf
, mozilla::fallible
);
163 // attestation data struct format:
164 // - 16 bytes: AAGUID
165 // - 2 bytes: Length of Credential ID
166 // - L bytes: Credential ID
167 // - variable: CBOR-format public key
168 nsresult
AssembleAttestationData(const CryptoBuffer
& aaguidBuf
,
169 const CryptoBuffer
& keyHandleBuf
,
170 const CryptoBuffer
& pubKeyObj
,
171 /* out */ CryptoBuffer
& attestationDataBuf
) {
172 if (NS_WARN_IF(!attestationDataBuf
.SetCapacity(
173 aaguidBuf
.Length() + 2 + keyHandleBuf
.Length() + pubKeyObj
.Length(),
174 mozilla::fallible
))) {
175 return NS_ERROR_OUT_OF_MEMORY
;
177 if (keyHandleBuf
.Length() > 0xFFFF) {
178 return NS_ERROR_INVALID_ARG
;
181 (void)attestationDataBuf
.AppendElements(aaguidBuf
, mozilla::fallible
);
182 (void)attestationDataBuf
.AppendElement((keyHandleBuf
.Length() >> 8) & 0xFF,
184 (void)attestationDataBuf
.AppendElement((keyHandleBuf
.Length() >> 0) & 0xFF,
186 (void)attestationDataBuf
.AppendElements(keyHandleBuf
, mozilla::fallible
);
187 (void)attestationDataBuf
.AppendElements(pubKeyObj
, mozilla::fallible
);
191 nsresult
AssembleAttestationObject(const CryptoBuffer
& aRpIdHash
,
192 const CryptoBuffer
& aPubKeyBuf
,
193 const CryptoBuffer
& aKeyHandleBuf
,
194 const CryptoBuffer
& aAttestationCertBuf
,
195 const CryptoBuffer
& aSignatureBuf
,
196 bool aForceNoneAttestation
,
197 /* out */ CryptoBuffer
& aAttestationObjBuf
) {
198 // Construct the public key object
199 CryptoBuffer pubKeyObj
;
200 nsresult rv
= CBOREncodePublicKeyObj(aPubKeyBuf
, pubKeyObj
);
205 mozilla::dom::CryptoBuffer aaguidBuf
;
206 if (NS_WARN_IF(!aaguidBuf
.SetCapacity(16, mozilla::fallible
))) {
207 return NS_ERROR_OUT_OF_MEMORY
;
210 // FIDO U2F devices have no AAGUIDs, so they'll be all zeros until we add
211 // support for CTAP2 devices.
212 for (int i
= 0; i
< 16; i
++) {
213 // SetCapacity was just called, these cannot fail.
214 (void)aaguidBuf
.AppendElement(0x00, mozilla::fallible
);
217 // During create credential, counter is always 0 for U2F
218 // See https://github.com/w3c/webauthn/issues/507
219 mozilla::dom::CryptoBuffer counterBuf
;
220 if (NS_WARN_IF(!counterBuf
.SetCapacity(4, mozilla::fallible
))) {
221 return NS_ERROR_OUT_OF_MEMORY
;
223 // SetCapacity was just called, these cannot fail.
224 (void)counterBuf
.AppendElement(0x00, mozilla::fallible
);
225 (void)counterBuf
.AppendElement(0x00, mozilla::fallible
);
226 (void)counterBuf
.AppendElement(0x00, mozilla::fallible
);
227 (void)counterBuf
.AppendElement(0x00, mozilla::fallible
);
229 // Construct the Attestation Data, which slots into the end of the
230 // Authentication Data buffer.
231 CryptoBuffer attDataBuf
;
232 rv
= AssembleAttestationData(aaguidBuf
, aKeyHandleBuf
, pubKeyObj
, attDataBuf
);
237 CryptoBuffer authDataBuf
;
238 // attDataBuf always contains data, so per [1] we have to set the AT flag.
239 // [1] https://w3c.github.io/webauthn/#sec-authenticator-data
240 const uint8_t flags
= FLAG_TUP
| FLAG_AT
;
241 rv
= AssembleAuthenticatorData(aRpIdHash
, flags
, counterBuf
, attDataBuf
,
247 // Direct attestation might have been requested by the RP.
248 // The user might override this and request anonymization anyway.
249 if (aForceNoneAttestation
) {
250 rv
= CBOREncodeNoneAttestationObj(authDataBuf
, aAttestationObjBuf
);
252 rv
= CBOREncodeFidoU2FAttestationObj(authDataBuf
, aAttestationCertBuf
,
253 aSignatureBuf
, aAttestationObjBuf
);
259 nsresult
U2FDecomposeSignResponse(const CryptoBuffer
& aResponse
,
260 /* out */ uint8_t& aFlags
,
261 /* out */ CryptoBuffer
& aCounterBuf
,
262 /* out */ CryptoBuffer
& aSignatureBuf
) {
263 if (aResponse
.Length() < 5) {
264 return NS_ERROR_INVALID_ARG
;
267 Span
<const uint8_t> rspView
= Span(aResponse
);
270 if (NS_WARN_IF(!aCounterBuf
.AppendElements(rspView
.FromTo(1, 5),
271 mozilla::fallible
))) {
272 return NS_ERROR_OUT_OF_MEMORY
;
276 !aSignatureBuf
.AppendElements(rspView
.From(5), mozilla::fallible
))) {
277 return NS_ERROR_OUT_OF_MEMORY
;
283 nsresult
U2FDecomposeRegistrationResponse(
284 const CryptoBuffer
& aResponse
,
285 /* out */ CryptoBuffer
& aPubKeyBuf
,
286 /* out */ CryptoBuffer
& aKeyHandleBuf
,
287 /* out */ CryptoBuffer
& aAttestationCertBuf
,
288 /* out */ CryptoBuffer
& aSignatureBuf
) {
289 // U2F v1.1 Format via
290 // http://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html
295 // 1 key handle length
297 // ASN.1 attestation certificate
298 // * attestation signature
300 pkix::Input u2fResponse
;
301 u2fResponse
.Init(aResponse
.Elements(), aResponse
.Length());
303 pkix::Reader
input(u2fResponse
);
306 if (input
.Read(b
) != pkix::Success
) {
307 return NS_ERROR_DOM_UNKNOWN_ERR
;
310 return NS_ERROR_DOM_UNKNOWN_ERR
;
313 nsresult rv
= ReadToCryptoBuffer(input
, aPubKeyBuf
, 65);
319 if (input
.Read(handleLen
) != pkix::Success
) {
320 return NS_ERROR_DOM_UNKNOWN_ERR
;
323 rv
= ReadToCryptoBuffer(input
, aKeyHandleBuf
, handleLen
);
328 // We have to parse the ASN.1 SEQUENCE on the outside to determine the cert's
331 if (pkix::der::ExpectTagAndGetTLV(input
, pkix::der::SEQUENCE
, cert
) !=
333 return NS_ERROR_DOM_UNKNOWN_ERR
;
336 pkix::Reader
certInput(cert
);
337 rv
= ReadToCryptoBuffer(certInput
, aAttestationCertBuf
, cert
.GetLength());
342 // The remainder of u2fResponse is the signature
344 input
.SkipToEnd(u2fSig
);
345 pkix::Reader
sigInput(u2fSig
);
346 rv
= ReadToCryptoBuffer(sigInput
, aSignatureBuf
, u2fSig
.GetLength());
351 MOZ_ASSERT(input
.AtEnd());
355 nsresult
U2FDecomposeECKey(const CryptoBuffer
& aPubKeyBuf
,
356 /* out */ CryptoBuffer
& aXcoord
,
357 /* out */ CryptoBuffer
& aYcoord
) {
359 pubKey
.Init(aPubKeyBuf
.Elements(), aPubKeyBuf
.Length());
361 pkix::Reader
input(pubKey
);
363 if (input
.Read(b
) != pkix::Success
) {
364 return NS_ERROR_DOM_UNKNOWN_ERR
;
367 return NS_ERROR_DOM_UNKNOWN_ERR
;
370 nsresult rv
= ReadToCryptoBuffer(input
, aXcoord
, 32);
375 rv
= ReadToCryptoBuffer(input
, aYcoord
, 32);
380 MOZ_ASSERT(input
.AtEnd());
384 static nsresult
HashCString(nsICryptoHash
* aHashService
, const nsACString
& aIn
,
385 /* out */ CryptoBuffer
& aOut
) {
386 MOZ_ASSERT(aHashService
);
388 nsresult rv
= aHashService
->Init(nsICryptoHash::SHA256
);
389 if (NS_WARN_IF(NS_FAILED(rv
))) {
393 rv
= aHashService
->Update(
394 reinterpret_cast<const uint8_t*>(aIn
.BeginReading()), aIn
.Length());
395 if (NS_WARN_IF(NS_FAILED(rv
))) {
399 nsAutoCString fullHash
;
400 // Passing false below means we will get a binary result rather than a
401 // base64-encoded string.
402 rv
= aHashService
->Finish(false, fullHash
);
403 if (NS_WARN_IF(NS_FAILED(rv
))) {
407 if (NS_WARN_IF(!aOut
.Assign(fullHash
))) {
408 return NS_ERROR_OUT_OF_MEMORY
;
414 nsresult
HashCString(const nsACString
& aIn
, /* out */ CryptoBuffer
& aOut
) {
416 nsCOMPtr
<nsICryptoHash
> hashService
=
417 do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID
, &srv
);
418 if (NS_FAILED(srv
)) {
422 srv
= HashCString(hashService
, aIn
, aOut
);
423 if (NS_WARN_IF(NS_FAILED(srv
))) {
424 return NS_ERROR_FAILURE
;
430 nsresult
BuildTransactionHashes(const nsCString
& aRpId
,
431 const nsCString
& aClientDataJSON
,
432 /* out */ CryptoBuffer
& aRpIdHash
,
433 /* out */ CryptoBuffer
& aClientDataHash
) {
435 nsCOMPtr
<nsICryptoHash
> hashService
=
436 do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID
, &srv
);
437 if (NS_FAILED(srv
)) {
441 if (!aRpIdHash
.SetLength(SHA256_LENGTH
, fallible
)) {
442 return NS_ERROR_OUT_OF_MEMORY
;
444 srv
= HashCString(hashService
, aRpId
, aRpIdHash
);
445 if (NS_WARN_IF(NS_FAILED(srv
))) {
446 return NS_ERROR_FAILURE
;
449 if (!aClientDataHash
.SetLength(SHA256_LENGTH
, fallible
)) {
450 return NS_ERROR_OUT_OF_MEMORY
;
452 srv
= HashCString(hashService
, aClientDataJSON
, aClientDataHash
);
453 if (NS_WARN_IF(NS_FAILED(srv
))) {
454 return NS_ERROR_FAILURE
;
460 } // namespace mozilla::dom