Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/dom/webauthn/WebAuthnUtil.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 "mozilla/dom/WebAuthnUtil.h"
8
#include "nsIEffectiveTLDService.h"
9
#include "nsNetUtil.h"
10
#include "pkixutil.h"
11
12
namespace mozilla {
13
namespace dom {
14
15
// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
16
NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1,
17
  "https://www.gstatic.com/securitykey/origins.json");
18
NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId2,
19
  "https://www.gstatic.com/securitykey/a/google.com/origins.json");
20
21
const uint8_t FLAG_TUP = 0x01; // Test of User Presence required
22
const uint8_t FLAG_AT = 0x40; // Authenticator Data is provided
23
24
bool
25
EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin,
26
              const U2FOperation& aOp, /* in/out */ nsString& aAppId)
27
0
{
28
0
  // Facet is the specification's way of referring to the web origin.
29
0
  nsAutoCString facetString = NS_ConvertUTF16toUTF8(aOrigin);
30
0
  nsCOMPtr<nsIURI> facetUri;
31
0
  if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri), facetString))) {
32
0
    return false;
33
0
  }
34
0
35
0
  // If the facetId (origin) is not HTTPS, reject
36
0
  bool facetIsHttps = false;
37
0
  if (NS_FAILED(facetUri->SchemeIs("https", &facetIsHttps)) || !facetIsHttps) {
38
0
    return false;
39
0
  }
40
0
41
0
  // If the appId is empty or null, overwrite it with the facetId and accept
42
0
  if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) {
43
0
    aAppId.Assign(aOrigin);
44
0
    return true;
45
0
  }
46
0
47
0
  // AppID is user-supplied. It's quite possible for this parse to fail.
48
0
  nsAutoCString appIdString = NS_ConvertUTF16toUTF8(aAppId);
49
0
  nsCOMPtr<nsIURI> appIdUri;
50
0
  if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri), appIdString))) {
51
0
    return false;
52
0
  }
53
0
54
0
  // if the appId URL is not HTTPS, reject.
55
0
  bool appIdIsHttps = false;
56
0
  if (NS_FAILED(appIdUri->SchemeIs("https", &appIdIsHttps)) || !appIdIsHttps) {
57
0
    return false;
58
0
  }
59
0
60
0
  nsAutoCString appIdHost;
61
0
  if (NS_FAILED(appIdUri->GetAsciiHost(appIdHost))) {
62
0
    return false;
63
0
  }
64
0
65
0
  // Allow localhost.
66
0
  if (appIdHost.EqualsLiteral("localhost")) {
67
0
    nsAutoCString facetHost;
68
0
    if (NS_FAILED(facetUri->GetAsciiHost(facetHost))) {
69
0
      return false;
70
0
    }
71
0
72
0
    if (facetHost.EqualsLiteral("localhost")) {
73
0
      return true;
74
0
    }
75
0
  }
76
0
77
0
  // Run the HTML5 algorithm to relax the same-origin policy, copied from W3C
78
0
  // Web Authentication. See Bug 1244959 comment #8 for context on why we are
79
0
  // doing this instead of implementing the external-fetch FacetID logic.
80
0
  nsCOMPtr<nsIDocument> document = aParent->GetDoc();
81
0
  if (!document || !document->IsHTMLDocument()) {
82
0
    return false;
83
0
  }
84
0
85
0
  nsHTMLDocument* html = document->AsHTMLDocument();
86
0
  // Use the base domain as the facet for evaluation. This lets this algorithm
87
0
  // relax the whole eTLD+1.
88
0
  nsCOMPtr<nsIEffectiveTLDService> tldService =
89
0
    do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
90
0
  if (!tldService) {
91
0
    return false;
92
0
  }
93
0
94
0
  nsAutoCString lowestFacetHost;
95
0
  if (NS_FAILED(tldService->GetBaseDomain(facetUri, 0, lowestFacetHost))) {
96
0
    return false;
97
0
  }
98
0
99
0
  if (html->IsRegistrableDomainSuffixOfOrEqualTo(NS_ConvertUTF8toUTF16(lowestFacetHost),
100
0
                                                 appIdHost)) {
101
0
    return true;
102
0
  }
103
0
104
0
  // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
105
0
  if (aOp == U2FOperation::Sign && lowestFacetHost.EqualsLiteral("google.com") &&
106
0
      (aAppId.Equals(kGoogleAccountsAppId1) ||
107
0
       aAppId.Equals(kGoogleAccountsAppId2))) {
108
0
    return true;
109
0
  }
110
0
111
0
  return false;
112
0
}
113
114
115
nsresult
116
ReadToCryptoBuffer(pkix::Reader& aSrc, /* out */ CryptoBuffer& aDest,
117
                   uint32_t aLen)
118
0
{
119
0
  if (aSrc.EnsureLength(aLen) != pkix::Success) {
120
0
    return NS_ERROR_DOM_UNKNOWN_ERR;
121
0
  }
122
0
123
0
  if (!aDest.SetCapacity(aLen, mozilla::fallible)) {
124
0
    return NS_ERROR_OUT_OF_MEMORY;
125
0
  }
126
0
127
0
  for (uint32_t offset = 0; offset < aLen; ++offset) {
128
0
    uint8_t b;
129
0
    if (aSrc.Read(b) != pkix::Success) {
130
0
      return NS_ERROR_DOM_UNKNOWN_ERR;
131
0
    }
132
0
    if (!aDest.AppendElement(b, mozilla::fallible)) {
133
0
      return NS_ERROR_OUT_OF_MEMORY;
134
0
    }
135
0
  }
136
0
137
0
  return NS_OK;
138
0
}
139
140
// Format:
141
// 32 bytes: SHA256 of the RP ID
142
// 1 byte: flags (TUP & AT)
143
// 4 bytes: sign counter
144
// variable: attestation data struct
145
// variable: CBOR-format extension auth data (optional, not flagged)
146
nsresult
147
AssembleAuthenticatorData(const CryptoBuffer& rpIdHashBuf,
148
                          const uint8_t flags,
149
                          const CryptoBuffer& counterBuf,
150
                          const CryptoBuffer& attestationDataBuf,
151
                          /* out */ CryptoBuffer& authDataBuf)
152
0
{
153
0
  if (NS_WARN_IF(!authDataBuf.SetCapacity(32 + 1 + 4 + attestationDataBuf.Length(),
154
0
                                          mozilla::fallible))) {
155
0
    return NS_ERROR_OUT_OF_MEMORY;
156
0
  }
157
0
  if (rpIdHashBuf.Length() != 32 || counterBuf.Length() != 4) {
158
0
    return NS_ERROR_INVALID_ARG;
159
0
  }
160
0
161
0
  authDataBuf.AppendElements(rpIdHashBuf, mozilla::fallible);
162
0
  authDataBuf.AppendElement(flags, mozilla::fallible);
163
0
  authDataBuf.AppendElements(counterBuf, mozilla::fallible);
164
0
  authDataBuf.AppendElements(attestationDataBuf, mozilla::fallible);
165
0
  return NS_OK;
166
0
}
167
168
// attestation data struct format:
169
// - 16 bytes: AAGUID
170
// - 2 bytes: Length of Credential ID
171
// - L bytes: Credential ID
172
// - variable: CBOR-format public key
173
nsresult
174
AssembleAttestationData(const CryptoBuffer& aaguidBuf,
175
                        const CryptoBuffer& keyHandleBuf,
176
                        const CryptoBuffer& pubKeyObj,
177
                        /* out */ CryptoBuffer& attestationDataBuf)
178
0
{
179
0
  if (NS_WARN_IF(!attestationDataBuf.SetCapacity(aaguidBuf.Length() + 2 +
180
0
                                                 keyHandleBuf.Length() +
181
0
                                                 pubKeyObj.Length(),
182
0
                                                 mozilla::fallible))) {
183
0
    return NS_ERROR_OUT_OF_MEMORY;
184
0
  }
185
0
  if (keyHandleBuf.Length() > 0xFFFF) {
186
0
    return NS_ERROR_INVALID_ARG;
187
0
  }
188
0
189
0
  attestationDataBuf.AppendElements(aaguidBuf, mozilla::fallible);
190
0
  attestationDataBuf.AppendElement((keyHandleBuf.Length() >> 8) & 0xFF,
191
0
                                   mozilla::fallible);
192
0
  attestationDataBuf.AppendElement((keyHandleBuf.Length() >> 0) & 0xFF,
193
0
                                   mozilla::fallible);
194
0
  attestationDataBuf.AppendElements(keyHandleBuf, mozilla::fallible);
195
0
  attestationDataBuf.AppendElements(pubKeyObj, mozilla::fallible);
196
0
  return NS_OK;
197
0
}
198
199
nsresult
200
AssembleAttestationObject(const CryptoBuffer& aRpIdHash,
201
                          const CryptoBuffer& aPubKeyBuf,
202
                          const CryptoBuffer& aKeyHandleBuf,
203
                          const CryptoBuffer& aAttestationCertBuf,
204
                          const CryptoBuffer& aSignatureBuf,
205
                          bool aForceNoneAttestation,
206
                          /* out */ CryptoBuffer& aAttestationObjBuf)
207
0
{
208
0
  // Construct the public key object
209
0
  CryptoBuffer pubKeyObj;
210
0
  nsresult rv = CBOREncodePublicKeyObj(aPubKeyBuf, pubKeyObj);
211
0
  if (NS_FAILED(rv)) {
212
0
    return rv;
213
0
  }
214
0
215
0
  mozilla::dom::CryptoBuffer aaguidBuf;
216
0
  if (NS_WARN_IF(!aaguidBuf.SetCapacity(16, mozilla::fallible))) {
217
0
    return NS_ERROR_OUT_OF_MEMORY;
218
0
  }
219
0
220
0
  // FIDO U2F devices have no AAGUIDs, so they'll be all zeros until we add
221
0
  // support for CTAP2 devices.
222
0
  for (int i=0; i<16; i++) {
223
0
    aaguidBuf.AppendElement(0x00, mozilla::fallible);
224
0
  }
225
0
226
0
  // During create credential, counter is always 0 for U2F
227
0
  // See https://github.com/w3c/webauthn/issues/507
228
0
  mozilla::dom::CryptoBuffer counterBuf;
229
0
  if (NS_WARN_IF(!counterBuf.SetCapacity(4, mozilla::fallible))) {
230
0
    return NS_ERROR_OUT_OF_MEMORY;
231
0
  }
232
0
  counterBuf.AppendElement(0x00, mozilla::fallible);
233
0
  counterBuf.AppendElement(0x00, mozilla::fallible);
234
0
  counterBuf.AppendElement(0x00, mozilla::fallible);
235
0
  counterBuf.AppendElement(0x00, mozilla::fallible);
236
0
237
0
  // Construct the Attestation Data, which slots into the end of the
238
0
  // Authentication Data buffer.
239
0
  CryptoBuffer attDataBuf;
240
0
  rv = AssembleAttestationData(aaguidBuf, aKeyHandleBuf, pubKeyObj, attDataBuf);
241
0
  if (NS_FAILED(rv)) {
242
0
    return rv;
243
0
  }
244
0
245
0
  CryptoBuffer authDataBuf;
246
0
  // attDataBuf always contains data, so per [1] we have to set the AT flag.
247
0
  // [1] https://w3c.github.io/webauthn/#sec-authenticator-data
248
0
  const uint8_t flags = FLAG_TUP | FLAG_AT;
249
0
  rv = AssembleAuthenticatorData(aRpIdHash, flags, counterBuf, attDataBuf,
250
0
                                 authDataBuf);
251
0
  if (NS_FAILED(rv)) {
252
0
    return rv;
253
0
  }
254
0
255
0
  // Direct attestation might have been requested by the RP.
256
0
  // The user might override this and request anonymization anyway.
257
0
  if (aForceNoneAttestation) {
258
0
    rv = CBOREncodeNoneAttestationObj(authDataBuf, aAttestationObjBuf);
259
0
  } else {
260
0
    rv = CBOREncodeFidoU2FAttestationObj(authDataBuf, aAttestationCertBuf,
261
0
                                         aSignatureBuf, aAttestationObjBuf);
262
0
  }
263
0
264
0
  return rv;
265
0
}
266
267
nsresult
268
U2FDecomposeSignResponse(const CryptoBuffer& aResponse,
269
                         /* out */ uint8_t& aFlags,
270
                         /* out */ CryptoBuffer& aCounterBuf,
271
                         /* out */ CryptoBuffer& aSignatureBuf)
272
0
{
273
0
  if (aResponse.Length() < 5) {
274
0
    return NS_ERROR_INVALID_ARG;
275
0
  }
276
0
277
0
  Span<const uint8_t> rspView = MakeSpan(aResponse);
278
0
  aFlags = rspView[0];
279
0
280
0
  if (NS_WARN_IF(!aCounterBuf.AppendElements(rspView.FromTo(1, 5),
281
0
                                             mozilla::fallible))) {
282
0
    return NS_ERROR_OUT_OF_MEMORY;
283
0
  }
284
0
285
0
  if (NS_WARN_IF(!aSignatureBuf.AppendElements(rspView.From(5),
286
0
                                               mozilla::fallible))) {
287
0
    return NS_ERROR_OUT_OF_MEMORY;
288
0
  }
289
0
290
0
  return NS_OK;
291
0
}
292
293
nsresult
294
U2FDecomposeRegistrationResponse(const CryptoBuffer& aResponse,
295
                                 /* out */ CryptoBuffer& aPubKeyBuf,
296
                                 /* out */ CryptoBuffer& aKeyHandleBuf,
297
                                 /* out */ CryptoBuffer& aAttestationCertBuf,
298
                                 /* out */ CryptoBuffer& aSignatureBuf)
299
0
{
300
0
  // U2F v1.1 Format via
301
0
  // http://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html
302
0
  //
303
0
  // Bytes  Value
304
0
  // 1      0x05
305
0
  // 65     public key
306
0
  // 1      key handle length
307
0
  // *      key handle
308
0
  // ASN.1  attestation certificate
309
0
  // *      attestation signature
310
0
311
0
  pkix::Input u2fResponse;
312
0
  u2fResponse.Init(aResponse.Elements(), aResponse.Length());
313
0
314
0
  pkix::Reader input(u2fResponse);
315
0
316
0
  uint8_t b;
317
0
  if (input.Read(b) != pkix::Success) {
318
0
    return NS_ERROR_DOM_UNKNOWN_ERR;
319
0
  }
320
0
  if (b != 0x05) {
321
0
    return NS_ERROR_DOM_UNKNOWN_ERR;
322
0
  }
323
0
324
0
  nsresult rv = ReadToCryptoBuffer(input, aPubKeyBuf, 65);
325
0
  if (NS_FAILED(rv)) {
326
0
    return rv;
327
0
  }
328
0
329
0
  uint8_t handleLen;
330
0
  if (input.Read(handleLen) != pkix::Success) {
331
0
    return NS_ERROR_DOM_UNKNOWN_ERR;
332
0
  }
333
0
334
0
  rv = ReadToCryptoBuffer(input, aKeyHandleBuf, handleLen);
335
0
  if (NS_FAILED(rv)) {
336
0
    return rv;
337
0
  }
338
0
339
0
  // We have to parse the ASN.1 SEQUENCE on the outside to determine the cert's
340
0
  // length.
341
0
  pkix::Input cert;
342
0
  if (pkix::der::ExpectTagAndGetTLV(input, pkix::der::SEQUENCE, cert)
343
0
      != pkix::Success) {
344
0
    return NS_ERROR_DOM_UNKNOWN_ERR;
345
0
  }
346
0
347
0
  pkix::Reader certInput(cert);
348
0
  rv = ReadToCryptoBuffer(certInput, aAttestationCertBuf, cert.GetLength());
349
0
  if (NS_FAILED(rv)) {
350
0
    return rv;
351
0
  }
352
0
353
0
  // The remainder of u2fResponse is the signature
354
0
  pkix::Input u2fSig;
355
0
  input.SkipToEnd(u2fSig);
356
0
  pkix::Reader sigInput(u2fSig);
357
0
  rv = ReadToCryptoBuffer(sigInput, aSignatureBuf, u2fSig.GetLength());
358
0
  if (NS_FAILED(rv)) {
359
0
    return rv;
360
0
  }
361
0
362
0
  MOZ_ASSERT(input.AtEnd());
363
0
  return NS_OK;
364
0
}
365
366
nsresult
367
U2FDecomposeECKey(const CryptoBuffer& aPubKeyBuf,
368
                  /* out */ CryptoBuffer& aXcoord,
369
                  /* out */ CryptoBuffer& aYcoord)
370
0
{
371
0
  pkix::Input pubKey;
372
0
  pubKey.Init(aPubKeyBuf.Elements(), aPubKeyBuf.Length());
373
0
374
0
  pkix::Reader input(pubKey);
375
0
  uint8_t b;
376
0
  if (input.Read(b) != pkix::Success) {
377
0
    return NS_ERROR_DOM_UNKNOWN_ERR;
378
0
  }
379
0
  if (b != 0x04) {
380
0
    return NS_ERROR_DOM_UNKNOWN_ERR;
381
0
  }
382
0
383
0
  nsresult rv = ReadToCryptoBuffer(input, aXcoord, 32);
384
0
  if (NS_FAILED(rv)) {
385
0
    return rv;
386
0
  }
387
0
388
0
  rv = ReadToCryptoBuffer(input, aYcoord, 32);
389
0
  if (NS_FAILED(rv)) {
390
0
    return rv;
391
0
  }
392
0
393
0
  MOZ_ASSERT(input.AtEnd());
394
0
  return NS_OK;
395
0
}
396
397
static nsresult
398
HashCString(nsICryptoHash* aHashService,
399
            const nsACString& aIn,
400
            /* out */ CryptoBuffer& aOut)
401
0
{
402
0
  MOZ_ASSERT(aHashService);
403
0
404
0
  nsresult rv = aHashService->Init(nsICryptoHash::SHA256);
405
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
406
0
    return rv;
407
0
  }
408
0
409
0
  rv = aHashService->Update(
410
0
    reinterpret_cast<const uint8_t*>(aIn.BeginReading()), aIn.Length());
411
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
412
0
    return rv;
413
0
  }
414
0
415
0
  nsAutoCString fullHash;
416
0
  // Passing false below means we will get a binary result rather than a
417
0
  // base64-encoded string.
418
0
  rv = aHashService->Finish(false, fullHash);
419
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
420
0
    return rv;
421
0
  }
422
0
423
0
  if (NS_WARN_IF(!aOut.Assign(fullHash))) {
424
0
    return NS_ERROR_OUT_OF_MEMORY;
425
0
  }
426
0
427
0
  return NS_OK;
428
0
}
429
430
nsresult
431
HashCString(const nsACString& aIn, /* out */ CryptoBuffer& aOut)
432
0
{
433
0
  nsresult srv;
434
0
  nsCOMPtr<nsICryptoHash> hashService =
435
0
    do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
436
0
  if (NS_FAILED(srv)) {
437
0
    return srv;
438
0
  }
439
0
440
0
  srv = HashCString(hashService, aIn, aOut);
441
0
  if (NS_WARN_IF(NS_FAILED(srv))) {
442
0
    return NS_ERROR_FAILURE;
443
0
  }
444
0
445
0
  return NS_OK;
446
0
}
447
448
nsresult
449
BuildTransactionHashes(const nsCString& aRpId,
450
                       const nsCString& aClientDataJSON,
451
                       /* out */ CryptoBuffer& aRpIdHash,
452
                       /* out */ CryptoBuffer& aClientDataHash)
453
0
{
454
0
  nsresult srv;
455
0
  nsCOMPtr<nsICryptoHash> hashService =
456
0
    do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
457
0
  if (NS_FAILED(srv)) {
458
0
    return srv;
459
0
  }
460
0
461
0
  if (!aRpIdHash.SetLength(SHA256_LENGTH, fallible)) {
462
0
    return NS_ERROR_OUT_OF_MEMORY;
463
0
  }
464
0
  srv = HashCString(hashService, aRpId, aRpIdHash);
465
0
  if (NS_WARN_IF(NS_FAILED(srv))) {
466
0
    return NS_ERROR_FAILURE;
467
0
  }
468
0
469
0
  if (!aClientDataHash.SetLength(SHA256_LENGTH, fallible)) {
470
0
    return NS_ERROR_OUT_OF_MEMORY;
471
0
  }
472
0
  srv = HashCString(hashService, aClientDataJSON, aClientDataHash);
473
0
  if (NS_WARN_IF(NS_FAILED(srv))) {
474
0
    return NS_ERROR_FAILURE;
475
0
  }
476
0
477
0
  return NS_OK;
478
0
}
479
480
481
}
482
}