tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

OriginTrials.cpp (7857B)


      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
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "OriginTrials.h"
      8 
      9 #include <mutex>
     10 
     11 #include "ScopedNSSTypes.h"
     12 #include "js/Wrapper.h"
     13 #include "jsapi.h"
     14 #include "mozilla/Base64.h"
     15 #include "mozilla/ClearOnShutdown.h"
     16 #include "mozilla/Span.h"
     17 #include "mozilla/StaticPrefs_dom.h"
     18 #include "mozilla/dom/Document.h"
     19 #include "mozilla/dom/WebCryptoCommon.h"
     20 #include "mozilla/dom/WorkerPrivate.h"
     21 #include "mozilla/dom/WorkletThread.h"
     22 #include "nsContentUtils.h"
     23 #include "nsGlobalWindowInner.h"
     24 #include "nsIPrincipal.h"
     25 #include "nsIURI.h"
     26 #include "nsNetUtil.h"
     27 #include "nsString.h"
     28 #include "xpcpublic.h"
     29 
     30 namespace mozilla {
     31 
     32 LazyLogModule sOriginTrialsLog("OriginTrials");
     33 #define LOG(...) MOZ_LOG(sOriginTrialsLog, LogLevel::Debug, (__VA_ARGS__))
     34 
     35 // prod.pub is the EcdsaP256 public key from the production key managed in
     36 // Google Cloud. See:
     37 //
     38 //   https://github.com/mozilla/origin-trial-token/blob/main/tools/README.md#get-the-public-key
     39 //
     40 // for how to get the public key.
     41 //
     42 // See also:
     43 //
     44 //   https://github.com/mozilla/origin-trial-token/blob/main/tools/README.md#sign-a-token-using-gcloud
     45 //
     46 // for how to sign using this key.
     47 //
     48 // test.pub is the EcdsaP256 public key from this key pair:
     49 //
     50 //  * https://github.com/mozilla/origin-trial-token/blob/64f03749e2e8c58f811f67044cecc7d6955fd51a/tools/test-keys/test-ecdsa.pkcs8
     51 //  * https://github.com/mozilla/origin-trial-token/blob/64f03749e2e8c58f811f67044cecc7d6955fd51a/tools/test-keys/test-ecdsa.pub
     52 //
     53 #include "keys.inc"
     54 
     55 constexpr auto kEcAlgorithm =
     56    NS_LITERAL_STRING_FROM_CSTRING(WEBCRYPTO_NAMED_CURVE_P256);
     57 
     58 using RawKeyRef = Span<const unsigned char, sizeof(kProdKey)>;
     59 
     60 struct StaticCachedPublicKey {
     61  constexpr StaticCachedPublicKey() = default;
     62 
     63  SECKEYPublicKey* Get(const RawKeyRef aRawKey);
     64 
     65 private:
     66  std::once_flag mFlag;
     67  UniqueSECKEYPublicKey mKey;
     68 };
     69 
     70 SECKEYPublicKey* StaticCachedPublicKey::Get(const RawKeyRef aRawKey) {
     71  std::call_once(mFlag, [&] {
     72    const SECItem item{siBuffer, const_cast<unsigned char*>(aRawKey.data()),
     73                       unsigned(aRawKey.Length())};
     74    MOZ_RELEASE_ASSERT(item.data[0] == EC_POINT_FORM_UNCOMPRESSED);
     75    mKey = dom::CreateECPublicKey(&item, kEcAlgorithm);
     76    if (mKey) {
     77      // It's fine to capture [this] by pointer because we are always static.
     78      if (NS_IsMainThread()) {
     79        RunOnShutdown([this] { mKey = nullptr; });
     80      } else {
     81        NS_DispatchToMainThread(NS_NewRunnableFunction(
     82            "ClearStaticCachedPublicKey",
     83            [this] { RunOnShutdown([this] { mKey = nullptr; }); }));
     84      }
     85    }
     86  });
     87  return mKey.get();
     88 }
     89 
     90 bool VerifySignature(const uint8_t* aSignature, uintptr_t aSignatureLen,
     91                     const uint8_t* aData, uintptr_t aDataLen,
     92                     void* aUserData) {
     93  MOZ_RELEASE_ASSERT(aSignatureLen == 64);
     94  static StaticCachedPublicKey sTestKey;
     95  static StaticCachedPublicKey sProdKey;
     96 
     97  LOG("VerifySignature()\n");
     98 
     99  SECKEYPublicKey* pubKey = StaticPrefs::dom_origin_trials_test_key_enabled()
    100                                ? sTestKey.Get(Span(kTestKey))
    101                                : sProdKey.Get(Span(kProdKey));
    102  if (NS_WARN_IF(!pubKey)) {
    103    LOG("  Failed to create public key?");
    104    return false;
    105  }
    106 
    107  if (NS_WARN_IF(aDataLen > UINT_MAX)) {
    108    LOG("  Way too large data.");
    109    return false;
    110  }
    111 
    112  const SECItem signature{siBuffer, const_cast<unsigned char*>(aSignature),
    113                          unsigned(aSignatureLen)};
    114  const SECItem data{siBuffer, const_cast<unsigned char*>(aData),
    115                     unsigned(aDataLen)};
    116 
    117  // SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE
    118  const SECStatus result = PK11_VerifyWithMechanism(
    119      pubKey, CKM_ECDSA_SHA256, nullptr, &signature, &data, nullptr);
    120  if (NS_WARN_IF(result != SECSuccess)) {
    121    LOG("  Failed to verify data.");
    122    return false;
    123  }
    124  return true;
    125 }
    126 
    127 bool MatchesOrigin(const uint8_t* aOrigin, size_t aOriginLen, bool aIsSubdomain,
    128                   bool aIsThirdParty, bool aIsUsageSubset, void* aUserData) {
    129  const nsDependentCSubstring origin(reinterpret_cast<const char*>(aOrigin),
    130                                     aOriginLen);
    131 
    132  LOG("MatchesOrigin(%d, %d, %d, %s)\n", aIsThirdParty, aIsSubdomain,
    133      aIsUsageSubset, nsCString(origin).get());
    134 
    135  if (aIsThirdParty || aIsUsageSubset) {
    136    // TODO(emilio): Support third-party tokens and so on.
    137    return false;
    138  }
    139 
    140  auto* principal = static_cast<nsIPrincipal*>(aUserData);
    141  nsCOMPtr<nsIURI> originURI;
    142  if (NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(originURI), origin)))) {
    143    return false;
    144  }
    145 
    146  const bool originMatches = [&] {
    147    if (principal->IsSameOrigin(originURI)) {
    148      return true;
    149    }
    150    if (aIsSubdomain) {
    151      for (nsCOMPtr<nsIPrincipal> prin = principal->GetNextSubDomainPrincipal();
    152           prin; prin = prin->GetNextSubDomainPrincipal()) {
    153        if (prin->IsSameOrigin(originURI)) {
    154          return true;
    155        }
    156      }
    157    }
    158    return false;
    159  }();
    160 
    161  if (NS_WARN_IF(!originMatches)) {
    162    LOG("Origin doesn't match\n");
    163    return false;
    164  }
    165 
    166  return true;
    167 }
    168 
    169 void OriginTrials::UpdateFromToken(const nsAString& aBase64EncodedToken,
    170                                   nsIPrincipal* aPrincipal) {
    171  if (!StaticPrefs::dom_origin_trials_enabled()) {
    172    return;
    173  }
    174 
    175  LOG("OriginTrials::UpdateFromToken()\n");
    176 
    177  nsAutoCString decodedToken;
    178  nsresult rv = mozilla::Base64Decode(aBase64EncodedToken, decodedToken);
    179  if (NS_WARN_IF(NS_FAILED(rv))) {
    180    return;
    181  }
    182 
    183  const Span<const uint8_t> decodedTokenSpan(decodedToken);
    184  const origin_trials_ffi::OriginTrialValidationParams params{
    185      VerifySignature,
    186      MatchesOrigin,
    187      /* user_data = */ aPrincipal,
    188  };
    189  auto result = origin_trials_ffi::origin_trials_parse_and_validate_token(
    190      decodedTokenSpan.data(), decodedTokenSpan.size(), &params);
    191  if (NS_WARN_IF(!result.IsOk())) {
    192    LOG("  result = %d\n", int(result.tag));
    193    return;  // TODO(emilio): Maybe report to console or what not?
    194  }
    195  OriginTrial trial = result.AsOk().trial;
    196  LOG("  result = Ok(%d)\n", int(trial));
    197  mEnabledTrials += trial;
    198 }
    199 
    200 OriginTrials OriginTrials::FromWindow(const nsGlobalWindowInner* aWindow) {
    201  if (!aWindow) {
    202    return {};
    203  }
    204  const dom::Document* doc = aWindow->GetExtantDoc();
    205  if (!doc) {
    206    return {};
    207  }
    208  return doc->Trials();
    209 }
    210 
    211 static int32_t PrefState(OriginTrial aTrial) {
    212  switch (aTrial) {
    213    case OriginTrial::TestTrial:
    214      return StaticPrefs::dom_origin_trials_test_trial_state();
    215    case OriginTrial::CoepCredentialless:
    216      return StaticPrefs::dom_origin_trials_coep_credentialless_state();
    217    case OriginTrial::PrivateAttributionV2:
    218      return StaticPrefs::dom_origin_trials_private_attribution_state();
    219    case OriginTrial::MLS:
    220      return StaticPrefs::dom_origin_trials_mls_state();
    221    case OriginTrial::MAX:
    222      MOZ_ASSERT_UNREACHABLE("Unknown trial!");
    223      break;
    224  }
    225  return 0;
    226 }
    227 
    228 bool OriginTrials::IsEnabled(OriginTrial aTrial) const {
    229  switch (PrefState(aTrial)) {
    230    case 1:
    231      return true;
    232    case 2:
    233      return false;
    234    default:
    235      break;
    236  }
    237 
    238  return mEnabledTrials.contains(aTrial);
    239 }
    240 
    241 bool OriginTrials::IsEnabled(JSContext* aCx, JSObject* aObject,
    242                             OriginTrial aTrial) {
    243  if (nsContentUtils::ThreadsafeIsSystemCaller(aCx)) {
    244    return true;
    245  }
    246  LOG("OriginTrials::IsEnabled(%d)\n", int(aTrial));
    247  nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
    248  MOZ_ASSERT(global);
    249  return global && global->Trials().IsEnabled(aTrial);
    250 }
    251 
    252 #undef LOG
    253 
    254 }  // namespace mozilla