summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/WebAuthnUtil.cpp
blob: f437e5d3618dff2ac67d8f2c4da7fa9712876122 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/dom/WebAuthnUtil.h"
#include "mozilla/dom/WebAuthnCBORUtil.h"
#include "nsComponentManagerUtils.h"
#include "nsICryptoHash.h"
#include "nsIEffectiveTLDService.h"
#include "nsNetUtil.h"
#include "mozpkix/pkixutil.h"
#include "nsHTMLDocument.h"
#include "hasht.h"

namespace mozilla::dom {

// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
constexpr auto kGoogleAccountsAppId1 =
    u"https://www.gstatic.com/securitykey/origins.json"_ns;
constexpr auto kGoogleAccountsAppId2 =
    u"https://www.gstatic.com/securitykey/a/google.com/origins.json"_ns;

const uint8_t FLAG_TUP = 0x01;  // Test of User Presence required
const uint8_t FLAG_AT = 0x40;   // Authenticator Data is provided

bool EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin,
                   /* in/out */ nsString& aAppId) {
  // Facet is the specification's way of referring to the web origin.
  nsAutoCString facetString = NS_ConvertUTF16toUTF8(aOrigin);
  nsCOMPtr<nsIURI> facetUri;
  if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri), facetString))) {
    return false;
  }

  // If the facetId (origin) is not HTTPS, reject
  if (!facetUri->SchemeIs("https")) {
    return false;
  }

  // If the appId is empty or null, overwrite it with the facetId and accept
  if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) {
    aAppId.Assign(aOrigin);
    return true;
  }

  // AppID is user-supplied. It's quite possible for this parse to fail.
  nsAutoCString appIdString = NS_ConvertUTF16toUTF8(aAppId);
  nsCOMPtr<nsIURI> appIdUri;
  if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri), appIdString))) {
    return false;
  }

  // if the appId URL is not HTTPS, reject.
  if (!appIdUri->SchemeIs("https")) {
    return false;
  }

  nsAutoCString appIdHost;
  if (NS_FAILED(appIdUri->GetAsciiHost(appIdHost))) {
    return false;
  }

  // Allow localhost.
  if (appIdHost.EqualsLiteral("localhost")) {
    nsAutoCString facetHost;
    if (NS_FAILED(facetUri->GetAsciiHost(facetHost))) {
      return false;
    }

    if (facetHost.EqualsLiteral("localhost")) {
      return true;
    }
  }

  // Run the HTML5 algorithm to relax the same-origin policy, copied from W3C
  // Web Authentication. See Bug 1244959 comment #8 for context on why we are
  // doing this instead of implementing the external-fetch FacetID logic.
  nsCOMPtr<Document> document = aParent->GetDoc();
  if (!document || !document->IsHTMLDocument()) {
    return false;
  }

  nsHTMLDocument* html = document->AsHTMLDocument();
  // Use the base domain as the facet for evaluation. This lets this algorithm
  // relax the whole eTLD+1.
  nsCOMPtr<nsIEffectiveTLDService> tldService =
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
  if (!tldService) {
    return false;
  }

  nsAutoCString lowestFacetHost;
  if (NS_FAILED(tldService->GetBaseDomain(facetUri, 0, lowestFacetHost))) {
    return false;
  }

  if (html->IsRegistrableDomainSuffixOfOrEqualTo(
          NS_ConvertUTF8toUTF16(lowestFacetHost), appIdHost)) {
    return true;
  }

  // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
  if (lowestFacetHost.EqualsLiteral("google.com") &&
      (aAppId.Equals(kGoogleAccountsAppId1) ||
       aAppId.Equals(kGoogleAccountsAppId2))) {
    return true;
  }

  return false;
}

nsresult ReadToCryptoBuffer(pkix::Reader& aSrc, /* out */ CryptoBuffer& aDest,
                            uint32_t aLen) {
  if (aSrc.EnsureLength(aLen) != pkix::Success) {
    return NS_ERROR_DOM_UNKNOWN_ERR;
  }

  if (!aDest.SetCapacity(aLen, mozilla::fallible)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  for (uint32_t offset = 0; offset < aLen; ++offset) {
    uint8_t b;
    if (aSrc.Read(b) != pkix::Success) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }
    if (!aDest.AppendElement(b, mozilla::fallible)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }

  return NS_OK;
}

// Format:
// 32 bytes: SHA256 of the RP ID
// 1 byte: flags (TUP & AT)
// 4 bytes: sign counter
// variable: attestation data struct
// variable: CBOR-format extension auth data (optional, not flagged)
nsresult AssembleAuthenticatorData(const CryptoBuffer& rpIdHashBuf,
                                   const uint8_t flags,
                                   const CryptoBuffer& counterBuf,
                                   const CryptoBuffer& attestationDataBuf,
                                   /* out */ CryptoBuffer& authDataBuf) {
  if (NS_WARN_IF(!authDataBuf.SetCapacity(
          32 + 1 + 4 + attestationDataBuf.Length(), mozilla::fallible))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  if (rpIdHashBuf.Length() != 32 || counterBuf.Length() != 4) {
    return NS_ERROR_INVALID_ARG;
  }

  (void)authDataBuf.AppendElements(rpIdHashBuf, mozilla::fallible);
  (void)authDataBuf.AppendElement(flags, mozilla::fallible);
  (void)authDataBuf.AppendElements(counterBuf, mozilla::fallible);
  (void)authDataBuf.AppendElements(attestationDataBuf, mozilla::fallible);
  return NS_OK;
}

// attestation data struct format:
// - 16 bytes: AAGUID
// - 2 bytes: Length of Credential ID
// - L bytes: Credential ID
// - variable: CBOR-format public key
nsresult AssembleAttestationData(const CryptoBuffer& aaguidBuf,
                                 const CryptoBuffer& keyHandleBuf,
                                 const CryptoBuffer& pubKeyObj,
                                 /* out */ CryptoBuffer& attestationDataBuf) {
  if (NS_WARN_IF(!attestationDataBuf.SetCapacity(
          aaguidBuf.Length() + 2 + keyHandleBuf.Length() + pubKeyObj.Length(),
          mozilla::fallible))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  if (keyHandleBuf.Length() > 0xFFFF) {
    return NS_ERROR_INVALID_ARG;
  }

  (void)attestationDataBuf.AppendElements(aaguidBuf, mozilla::fallible);
  (void)attestationDataBuf.AppendElement((keyHandleBuf.Length() >> 8) & 0xFF,
                                         mozilla::fallible);
  (void)attestationDataBuf.AppendElement((keyHandleBuf.Length() >> 0) & 0xFF,
                                         mozilla::fallible);
  (void)attestationDataBuf.AppendElements(keyHandleBuf, mozilla::fallible);
  (void)attestationDataBuf.AppendElements(pubKeyObj, mozilla::fallible);
  return NS_OK;
}

nsresult AssembleAttestationObject(const CryptoBuffer& aRpIdHash,
                                   const CryptoBuffer& aPubKeyBuf,
                                   const CryptoBuffer& aKeyHandleBuf,
                                   const CryptoBuffer& aAttestationCertBuf,
                                   const CryptoBuffer& aSignatureBuf,
                                   bool aForceNoneAttestation,
                                   /* out */ CryptoBuffer& aAttestationObjBuf) {
  // Construct the public key object
  CryptoBuffer pubKeyObj;
  nsresult rv = CBOREncodePublicKeyObj(aPubKeyBuf, pubKeyObj);
  if (NS_FAILED(rv)) {
    return rv;
  }

  mozilla::dom::CryptoBuffer aaguidBuf;
  if (NS_WARN_IF(!aaguidBuf.SetCapacity(16, mozilla::fallible))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  // FIDO U2F devices have no AAGUIDs, so they'll be all zeros until we add
  // support for CTAP2 devices.
  for (int i = 0; i < 16; i++) {
    // SetCapacity was just called, these cannot fail.
    (void)aaguidBuf.AppendElement(0x00, mozilla::fallible);
  }

  // During create credential, counter is always 0 for U2F
  // See https://github.com/w3c/webauthn/issues/507
  mozilla::dom::CryptoBuffer counterBuf;
  if (NS_WARN_IF(!counterBuf.SetCapacity(4, mozilla::fallible))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  // SetCapacity was just called, these cannot fail.
  (void)counterBuf.AppendElement(0x00, mozilla::fallible);
  (void)counterBuf.AppendElement(0x00, mozilla::fallible);
  (void)counterBuf.AppendElement(0x00, mozilla::fallible);
  (void)counterBuf.AppendElement(0x00, mozilla::fallible);

  // Construct the Attestation Data, which slots into the end of the
  // Authentication Data buffer.
  CryptoBuffer attDataBuf;
  rv = AssembleAttestationData(aaguidBuf, aKeyHandleBuf, pubKeyObj, attDataBuf);
  if (NS_FAILED(rv)) {
    return rv;
  }

  CryptoBuffer authDataBuf;
  // attDataBuf always contains data, so per [1] we have to set the AT flag.
  // [1] https://w3c.github.io/webauthn/#sec-authenticator-data
  const uint8_t flags = FLAG_TUP | FLAG_AT;
  rv = AssembleAuthenticatorData(aRpIdHash, flags, counterBuf, attDataBuf,
                                 authDataBuf);
  if (NS_FAILED(rv)) {
    return rv;
  }

  // Direct attestation might have been requested by the RP.
  // The user might override this and request anonymization anyway.
  if (aForceNoneAttestation) {
    rv = CBOREncodeNoneAttestationObj(authDataBuf, aAttestationObjBuf);
  } else {
    rv = CBOREncodeFidoU2FAttestationObj(authDataBuf, aAttestationCertBuf,
                                         aSignatureBuf, aAttestationObjBuf);
  }

  return rv;
}

nsresult U2FDecomposeSignResponse(const CryptoBuffer& aResponse,
                                  /* out */ uint8_t& aFlags,
                                  /* out */ CryptoBuffer& aCounterBuf,
                                  /* out */ CryptoBuffer& aSignatureBuf) {
  if (aResponse.Length() < 5) {
    return NS_ERROR_INVALID_ARG;
  }

  Span<const uint8_t> rspView = Span(aResponse);
  aFlags = rspView[0];

  if (NS_WARN_IF(!aCounterBuf.AppendElements(rspView.FromTo(1, 5),
                                             mozilla::fallible))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  if (NS_WARN_IF(
          !aSignatureBuf.AppendElements(rspView.From(5), mozilla::fallible))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  return NS_OK;
}

nsresult U2FDecomposeRegistrationResponse(
    const CryptoBuffer& aResponse,
    /* out */ CryptoBuffer& aPubKeyBuf,
    /* out */ CryptoBuffer& aKeyHandleBuf,
    /* out */ CryptoBuffer& aAttestationCertBuf,
    /* out */ CryptoBuffer& aSignatureBuf) {
  // U2F v1.1 Format via
  // http://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html
  //
  // Bytes  Value
  // 1      0x05
  // 65     public key
  // 1      key handle length
  // *      key handle
  // ASN.1  attestation certificate
  // *      attestation signature

  pkix::Input u2fResponse;
  u2fResponse.Init(aResponse.Elements(), aResponse.Length());

  pkix::Reader input(u2fResponse);

  uint8_t b;
  if (input.Read(b) != pkix::Success) {
    return NS_ERROR_DOM_UNKNOWN_ERR;
  }
  if (b != 0x05) {
    return NS_ERROR_DOM_UNKNOWN_ERR;
  }

  nsresult rv = ReadToCryptoBuffer(input, aPubKeyBuf, 65);
  if (NS_FAILED(rv)) {
    return rv;
  }

  uint8_t handleLen;
  if (input.Read(handleLen) != pkix::Success) {
    return NS_ERROR_DOM_UNKNOWN_ERR;
  }

  rv = ReadToCryptoBuffer(input, aKeyHandleBuf, handleLen);
  if (NS_FAILED(rv)) {
    return rv;
  }

  // We have to parse the ASN.1 SEQUENCE on the outside to determine the cert's
  // length.
  pkix::Input cert;
  if (pkix::der::ExpectTagAndGetTLV(input, pkix::der::SEQUENCE, cert) !=
      pkix::Success) {
    return NS_ERROR_DOM_UNKNOWN_ERR;
  }

  pkix::Reader certInput(cert);
  rv = ReadToCryptoBuffer(certInput, aAttestationCertBuf, cert.GetLength());
  if (NS_FAILED(rv)) {
    return rv;
  }

  // The remainder of u2fResponse is the signature
  pkix::Input u2fSig;
  input.SkipToEnd(u2fSig);
  pkix::Reader sigInput(u2fSig);
  rv = ReadToCryptoBuffer(sigInput, aSignatureBuf, u2fSig.GetLength());
  if (NS_FAILED(rv)) {
    return rv;
  }

  MOZ_ASSERT(input.AtEnd());
  return NS_OK;
}

nsresult U2FDecomposeECKey(const CryptoBuffer& aPubKeyBuf,
                           /* out */ CryptoBuffer& aXcoord,
                           /* out */ CryptoBuffer& aYcoord) {
  pkix::Input pubKey;
  pubKey.Init(aPubKeyBuf.Elements(), aPubKeyBuf.Length());

  pkix::Reader input(pubKey);
  uint8_t b;
  if (input.Read(b) != pkix::Success) {
    return NS_ERROR_DOM_UNKNOWN_ERR;
  }
  if (b != 0x04) {
    return NS_ERROR_DOM_UNKNOWN_ERR;
  }

  nsresult rv = ReadToCryptoBuffer(input, aXcoord, 32);
  if (NS_FAILED(rv)) {
    return rv;
  }

  rv = ReadToCryptoBuffer(input, aYcoord, 32);
  if (NS_FAILED(rv)) {
    return rv;
  }

  MOZ_ASSERT(input.AtEnd());
  return NS_OK;
}

static nsresult HashCString(nsICryptoHash* aHashService, const nsACString& aIn,
                            /* out */ CryptoBuffer& aOut) {
  MOZ_ASSERT(aHashService);

  nsresult rv = aHashService->Init(nsICryptoHash::SHA256);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aHashService->Update(
      reinterpret_cast<const uint8_t*>(aIn.BeginReading()), aIn.Length());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsAutoCString fullHash;
  // Passing false below means we will get a binary result rather than a
  // base64-encoded string.
  rv = aHashService->Finish(false, fullHash);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (NS_WARN_IF(!aOut.Assign(fullHash))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  return NS_OK;
}

nsresult HashCString(const nsACString& aIn, /* out */ CryptoBuffer& aOut) {
  nsresult srv;
  nsCOMPtr<nsICryptoHash> hashService =
      do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
  if (NS_FAILED(srv)) {
    return srv;
  }

  srv = HashCString(hashService, aIn, aOut);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult BuildTransactionHashes(const nsCString& aRpId,
                                const nsCString& aClientDataJSON,
                                /* out */ CryptoBuffer& aRpIdHash,
                                /* out */ CryptoBuffer& aClientDataHash) {
  nsresult srv;
  nsCOMPtr<nsICryptoHash> hashService =
      do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
  if (NS_FAILED(srv)) {
    return srv;
  }

  if (!aRpIdHash.SetLength(SHA256_LENGTH, fallible)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  srv = HashCString(hashService, aRpId, aRpIdHash);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    return NS_ERROR_FAILURE;
  }

  if (!aClientDataHash.SetLength(SHA256_LENGTH, fallible)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  srv = HashCString(hashService, aClientDataJSON, aClientDataHash);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

}  // namespace mozilla::dom