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(), ¶ms); 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