/* -*- 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 "OriginTrials.h" #include "mozilla/Base64.h" #include "mozilla/Span.h" #include "nsString.h" #include "nsIPrincipal.h" #include "nsIURI.h" #include "nsNetUtil.h" #include "nsContentUtils.h" #include "xpcpublic.h" #include "jsapi.h" #include "js/Wrapper.h" #include "nsGlobalWindowInner.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkletThread.h" #include "mozilla/dom/WebCryptoCommon.h" #include "mozilla/StaticPrefs_dom.h" #include "ScopedNSSTypes.h" #include namespace mozilla { LazyLogModule sOriginTrialsLog("OriginTrials"); #define LOG(...) MOZ_LOG(sOriginTrialsLog, LogLevel::Debug, (__VA_ARGS__)) // prod.pub is the EcdsaP256 public key from the production key managed in // Google Cloud. See: // // https://github.com/mozilla/origin-trial-token/blob/main/tools/README.md#get-the-public-key // // for how to get the public key. // // See also: // // https://github.com/mozilla/origin-trial-token/blob/main/tools/README.md#sign-a-token-using-gcloud // // for how to sign using this key. // // test.pub is the EcdsaP256 public key from this key pair: // // * https://github.com/mozilla/origin-trial-token/blob/64f03749e2e8c58f811f67044cecc7d6955fd51a/tools/test-keys/test-ecdsa.pkcs8 // * https://github.com/mozilla/origin-trial-token/blob/64f03749e2e8c58f811f67044cecc7d6955fd51a/tools/test-keys/test-ecdsa.pub // #include "keys.inc" constexpr auto kEcAlgorithm = NS_LITERAL_STRING_FROM_CSTRING(WEBCRYPTO_NAMED_CURVE_P256); using RawKeyRef = Span; struct StaticCachedPublicKey { constexpr StaticCachedPublicKey() = default; SECKEYPublicKey* Get(const RawKeyRef aRawKey); private: std::once_flag mFlag; UniqueSECKEYPublicKey mKey; }; SECKEYPublicKey* StaticCachedPublicKey::Get(const RawKeyRef aRawKey) { std::call_once(mFlag, [&] { const SECItem item{siBuffer, const_cast(aRawKey.data()), unsigned(aRawKey.Length())}; MOZ_RELEASE_ASSERT(item.data[0] == EC_POINT_FORM_UNCOMPRESSED); mKey = dom::CreateECPublicKey(&item, kEcAlgorithm); if (mKey) { // It's fine to capture [this] by pointer because we are always static. if (NS_IsMainThread()) { RunOnShutdown([this] { mKey = nullptr; }); } else { NS_DispatchToMainThread(NS_NewRunnableFunction( "ClearStaticCachedPublicKey", [this] { RunOnShutdown([this] { mKey = nullptr; }); })); } } }); return mKey.get(); } bool VerifySignature(const uint8_t* aSignature, uintptr_t aSignatureLen, const uint8_t* aData, uintptr_t aDataLen, void* aUserData) { MOZ_RELEASE_ASSERT(aSignatureLen == 64); static StaticCachedPublicKey sTestKey; static StaticCachedPublicKey sProdKey; LOG("VerifySignature()\n"); SECKEYPublicKey* pubKey = StaticPrefs::dom_origin_trials_test_key_enabled() ? sTestKey.Get(Span(kTestKey)) : sProdKey.Get(Span(kProdKey)); if (NS_WARN_IF(!pubKey)) { LOG(" Failed to create public key?"); return false; } if (NS_WARN_IF(aDataLen > UINT_MAX)) { LOG(" Way too large data."); return false; } const SECItem signature{siBuffer, const_cast(aSignature), unsigned(aSignatureLen)}; const SECItem data{siBuffer, const_cast(aData), unsigned(aDataLen)}; // SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE const SECStatus result = PK11_VerifyWithMechanism( pubKey, CKM_ECDSA_SHA256, nullptr, &signature, &data, nullptr); if (NS_WARN_IF(result != SECSuccess)) { LOG(" Failed to verify data."); return false; } return true; } bool MatchesOrigin(const uint8_t* aOrigin, size_t aOriginLen, bool aIsSubdomain, bool aIsThirdParty, bool aIsUsageSubset, void* aUserData) { const nsDependentCSubstring origin(reinterpret_cast(aOrigin), aOriginLen); LOG("MatchesOrigin(%d, %d, %d, %s)\n", aIsThirdParty, aIsSubdomain, aIsUsageSubset, nsCString(origin).get()); if (aIsThirdParty || aIsUsageSubset) { // TODO(emilio): Support third-party tokens and so on. return false; } auto* principal = static_cast(aUserData); nsCOMPtr originURI; if (NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(originURI), origin)))) { return false; } const bool originMatches = [&] { if (principal->IsSameOrigin(originURI)) { return true; } if (aIsSubdomain) { for (nsCOMPtr prin = principal->GetNextSubDomainPrincipal(); prin; prin = prin->GetNextSubDomainPrincipal()) { if (prin->IsSameOrigin(originURI)) { return true; } } } return false; }(); if (NS_WARN_IF(!originMatches)) { LOG("Origin doesn't match\n"); return false; } return true; } void OriginTrials::UpdateFromToken(const nsAString& aBase64EncodedToken, nsIPrincipal* aPrincipal) { if (!StaticPrefs::dom_origin_trials_enabled()) { return; } LOG("OriginTrials::UpdateFromToken()\n"); nsAutoCString decodedToken; nsresult rv = mozilla::Base64Decode(aBase64EncodedToken, decodedToken); if (NS_WARN_IF(NS_FAILED(rv))) { return; } const Span decodedTokenSpan(decodedToken); const origin_trials_ffi::OriginTrialValidationParams params{ VerifySignature, MatchesOrigin, /* user_data = */ aPrincipal, }; auto result = origin_trials_ffi::origin_trials_parse_and_validate_token( decodedTokenSpan.data(), decodedTokenSpan.size(), ¶ms); if (NS_WARN_IF(!result.IsOk())) { LOG(" result = %d\n", int(result.tag)); return; // TODO(emilio): Maybe report to console or what not? } OriginTrial trial = result.AsOk().trial; LOG(" result = Ok(%d)\n", int(trial)); mEnabledTrials += trial; } OriginTrials OriginTrials::FromWindow(const nsGlobalWindowInner* aWindow) { if (!aWindow) { return {}; } const dom::Document* doc = aWindow->GetExtantDoc(); if (!doc) { return {}; } return doc->Trials(); } static int32_t PrefState(OriginTrial aTrial) { switch (aTrial) { case OriginTrial::TestTrial: return StaticPrefs::dom_origin_trials_test_trial_state(); case OriginTrial::CoepCredentialless: return StaticPrefs::dom_origin_trials_coep_credentialless_state(); case OriginTrial::MAX: MOZ_ASSERT_UNREACHABLE("Unknown trial!"); break; } return 0; } bool OriginTrials::IsEnabled(OriginTrial aTrial) const { switch (PrefState(aTrial)) { case 1: return true; case 2: return false; default: break; } return mEnabledTrials.contains(aTrial); } bool OriginTrials::IsEnabled(JSContext* aCx, JSObject* aObject, OriginTrial aTrial) { if (nsContentUtils::ThreadsafeIsSystemCaller(aCx)) { return true; } LOG("OriginTrials::IsEnabled(%d)\n", int(aTrial)); nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); MOZ_ASSERT(global); return global && global->Trials().IsEnabled(aTrial); } #undef LOG } // namespace mozilla