diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/origin-trials | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
22 files changed, 865 insertions, 0 deletions
diff --git a/dom/origin-trials/OriginTrials.cpp b/dom/origin-trials/OriginTrials.cpp new file mode 100644 index 0000000000..9f9e6b44fe --- /dev/null +++ b/dom/origin-trials/OriginTrials.cpp @@ -0,0 +1,248 @@ +/* -*- 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 <mutex> + +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<const unsigned char, sizeof(kProdKey)>; + +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<unsigned char*>(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<unsigned char*>(aSignature), + unsigned(aSignatureLen)}; + const SECItem data{siBuffer, const_cast<unsigned char*>(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<const char*>(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<nsIPrincipal*>(aUserData); + nsCOMPtr<nsIURI> 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<nsIPrincipal> 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<const uint8_t> 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 diff --git a/dom/origin-trials/OriginTrials.h b/dom/origin-trials/OriginTrials.h new file mode 100644 index 0000000000..d1de36a202 --- /dev/null +++ b/dom/origin-trials/OriginTrials.h @@ -0,0 +1,59 @@ +/* -*- 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/. */ + +#ifndef mozilla_OriginTrials_h +#define mozilla_OriginTrials_h + +#include "mozilla/origin_trials_ffi_generated.h" +#include "mozilla/EnumSet.h" +#include "nsStringFwd.h" + +class nsIPrincipal; +class nsGlobalWindowInner; +struct JSContext; +class JSObject; + +namespace mozilla { + +using OriginTrial = origin_trials_ffi::OriginTrial; + +// A class that keeps a set of enabled trials / features for a particular +// origin. +// +// These allow sites to opt-in and provide feedback into experimental features +// before we ship it to the general public. +class OriginTrials final { + public: + using RawType = EnumSet<OriginTrial>; + + OriginTrials() = default; + + static OriginTrials FromRaw(RawType aRaw) { return OriginTrials(aRaw); } + const RawType& Raw() const { return mEnabledTrials; } + + // Parses and verifies a base64-encoded token from either a header or a meta + // tag. If the token is valid and not expired, this will enable the relevant + // feature. + void UpdateFromToken(const nsAString& aBase64EncodedToken, + nsIPrincipal* aPrincipal); + + bool IsEnabled(OriginTrial aTrial) const; + + // Checks whether a given origin trial is enabled for a given call. + static bool IsEnabled(JSContext*, JSObject*, OriginTrial); + + // Computes the currently-applying trials for our global. + static OriginTrials FromWindow(const nsGlobalWindowInner*); + + private: + explicit OriginTrials(RawType aRaw) : mEnabledTrials(aRaw) {} + + RawType mEnabledTrials; +}; + +} // namespace mozilla + +#endif diff --git a/dom/origin-trials/OriginTrialsIPCUtils.h b/dom/origin-trials/OriginTrialsIPCUtils.h new file mode 100644 index 0000000000..07885084b9 --- /dev/null +++ b/dom/origin-trials/OriginTrialsIPCUtils.h @@ -0,0 +1,45 @@ +/* -*- 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/. */ + +#ifndef mozilla_OriginTrialsIPCUtils_h +#define mozilla_OriginTrialsIPCUtils_h + +#include "mozilla/OriginTrials.h" +#include "mozilla/EnumTypeTraits.h" +#include "ipc/EnumSerializer.h" + +namespace mozilla { +template <> +struct MaxEnumValue<OriginTrial> { + static constexpr unsigned int value = + static_cast<unsigned int>(OriginTrial::MAX); +}; +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits<mozilla::OriginTrials> { + using paramType = mozilla::OriginTrials; + using RawType = mozilla::OriginTrials::RawType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.Raw()); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + RawType raw; + if (!ReadParam(aReader, &raw)) { + return false; + } + *aResult = mozilla::OriginTrials::FromRaw(raw); + return true; + } +}; + +} // namespace IPC + +#endif diff --git a/dom/origin-trials/ffi/.gitignore b/dom/origin-trials/ffi/.gitignore new file mode 100644 index 0000000000..2c96eb1b65 --- /dev/null +++ b/dom/origin-trials/ffi/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/dom/origin-trials/ffi/Cargo.toml b/dom/origin-trials/ffi/Cargo.toml new file mode 100644 index 0000000000..710b846176 --- /dev/null +++ b/dom/origin-trials/ffi/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "origin-trials-ffi" +version = "0.1.0" +edition = "2021" +authors = [ + "Emilio Cobos Álvarez <emilio@crisal.io>", +] +license = "MPL-2.0" + +[lib] +path = "lib.rs" + +[dependencies] +origin-trial-token = "0.1" diff --git a/dom/origin-trials/ffi/cbindgen.toml b/dom/origin-trials/ffi/cbindgen.toml new file mode 100644 index 0000000000..1b95a60280 --- /dev/null +++ b/dom/origin-trials/ffi/cbindgen.toml @@ -0,0 +1,18 @@ +header = """/* 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_OriginTrials_h +#error "Don't include this file directly, include mozilla/OriginTrials.h instead" +#endif +""" +include_guard = "mozilla_OriginTrials_ffi_h" +include_version = true +language = "C++" +namespaces = ["mozilla", "origin_trials_ffi"] +includes = ["mozilla/Assertions.h"] + +[enum] +derive_helper_methods = true +derive_const_casts = true +cast_assert_name = "MOZ_DIAGNOSTIC_ASSERT" diff --git a/dom/origin-trials/ffi/lib.rs b/dom/origin-trials/ffi/lib.rs new file mode 100644 index 0000000000..1745c9e790 --- /dev/null +++ b/dom/origin-trials/ffi/lib.rs @@ -0,0 +1,150 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use origin_trial_token::{RawToken, Token, TokenValidationError, Usage}; +use std::ffi::c_void; + +#[repr(u8)] +pub enum OriginTrial { + // NOTE(emilio): 0 is reserved for WebIDL usage. + TestTrial = 1, + CoepCredentialless = 2, + + MAX, +} + +impl OriginTrial { + fn from_str(s: &str) -> Option<Self> { + Some(match s { + "TestTrial" => Self::TestTrial, + "CoepCredentialless" => Self::CoepCredentialless, + _ => return None, + }) + } +} + +#[repr(u8)] +pub enum OriginTrialResult { + Ok { trial: OriginTrial }, + BufferTooSmall, + MismatchedPayloadSize { expected: usize, actual: usize }, + InvalidSignature, + UnknownVersion, + UnsupportedThirdPartyToken, + UnexpectedUsageInNonThirdPartyToken, + MalformedPayload, + ExpiredToken, + UnknownTrial, + OriginMismatch, +} + +impl OriginTrialResult { + fn from_error(e: TokenValidationError) -> Self { + match e { + TokenValidationError::BufferTooSmall => OriginTrialResult::BufferTooSmall, + TokenValidationError::MismatchedPayloadSize { expected, actual } => { + OriginTrialResult::MismatchedPayloadSize { expected, actual } + } + TokenValidationError::InvalidSignature => OriginTrialResult::InvalidSignature, + TokenValidationError::UnknownVersion => OriginTrialResult::UnknownVersion, + TokenValidationError::UnsupportedThirdPartyToken => { + OriginTrialResult::UnsupportedThirdPartyToken + } + TokenValidationError::UnexpectedUsageInNonThirdPartyToken => { + OriginTrialResult::UnexpectedUsageInNonThirdPartyToken + } + TokenValidationError::MalformedPayload(..) => OriginTrialResult::MalformedPayload, + } + } +} + +/// A struct that allows you to configure how validation on works, and pass +/// state to the signature verification. +#[repr(C)] +pub struct OriginTrialValidationParams { + /// Verify a given signature against the signed data. + pub verify_signature: extern "C" fn( + signature: *const u8, + signature_len: usize, + data: *const u8, + data_len: usize, + user_data: *mut c_void, + ) -> bool, + + /// Returns whether a given origin, which is passed as the first two + /// arguments, and guaranteed to be valid UTF-8, passes the validation for a + /// given invocation. + pub matches_origin: extern "C" fn( + origin: *const u8, + len: usize, + is_subdomain: bool, + is_third_party: bool, + is_usage_subset: bool, + user_data: *mut c_void, + ) -> bool, + + /// A pointer with user-supplied data that will be passed down to the + /// other functions in this method. + pub user_data: *mut c_void, +} + +#[no_mangle] +pub unsafe extern "C" fn origin_trials_parse_and_validate_token( + bytes: *const u8, + len: usize, + params: &OriginTrialValidationParams, +) -> OriginTrialResult { + let slice = std::slice::from_raw_parts(bytes, len); + let raw_token = match RawToken::from_buffer(slice) { + Ok(token) => token, + Err(e) => return OriginTrialResult::from_error(e), + }; + + // Verifying the token is usually more expensive than the early-outs here. + let token = match Token::from_raw_token_unverified(raw_token) { + Ok(token) => token, + Err(e) => return OriginTrialResult::from_error(e), + }; + + if token.is_expired() { + return OriginTrialResult::ExpiredToken; + } + + let trial = match OriginTrial::from_str(token.feature()) { + Some(t) => t, + None => return OriginTrialResult::UnknownTrial, + }; + + let is_usage_subset = match token.usage { + Usage::None => false, + Usage::Subset => true, + }; + + if !(params.matches_origin)( + token.origin.as_ptr(), + token.origin.len(), + token.is_subdomain, + token.is_third_party, + is_usage_subset, + params.user_data, + ) { + return OriginTrialResult::OriginMismatch; + } + + let valid_signature = raw_token.verify(|signature, data| { + (params.verify_signature)( + signature.as_ptr(), + signature.len(), + data.as_ptr(), + data.len(), + params.user_data, + ) + }); + + if !valid_signature { + return OriginTrialResult::InvalidSignature; + } + + OriginTrialResult::Ok { trial } +} diff --git a/dom/origin-trials/gen-keys.py b/dom/origin-trials/gen-keys.py new file mode 100644 index 0000000000..6d00d695cf --- /dev/null +++ b/dom/origin-trials/gen-keys.py @@ -0,0 +1,42 @@ +# 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/. + +import sys + +from pyasn1.codec.der import decoder +from pyasn1.type import univ +from pyasn1_modules import pem + + +def public_key_to_string(file, name): + out = "static const unsigned char " + name + "[65] = { " + with open(file) as f: + substrate = pem.readPemFromFile( + f, "-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----" + ) + key = decoder.decode(substrate) + ident = key[0][0] + assert ident[0] == univ.ObjectIdentifier( + "1.2.840.10045.2.1" + ), "should be an ECPublicKey" + assert ident[1] == univ.ObjectIdentifier( + "1.2.840.10045.3.1.7" + ), "should be a EcdsaP256 key" + bits = key[0][1] + assert isinstance(bits, univ.BitString), "Should be a bit string" + assert len(bits) == 520, "Should be 520 bits (65 bytes)" + for byte in bits.asOctets(): + out += hex(byte) + ", " + out += "};" + return out + + +def generate(output, test_key, prod_key): + output.write(public_key_to_string(test_key, "kTestKey")) + output.write("\n\n") + output.write(public_key_to_string(prod_key, "kProdKey")) + + +if __name__ == "__main__": + generate(sys.stdout, *sys.argv[1:]) diff --git a/dom/origin-trials/moz.build b/dom/origin-trials/moz.build new file mode 100644 index 0000000000..693fd62281 --- /dev/null +++ b/dom/origin-trials/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +if CONFIG["COMPILE_ENVIRONMENT"]: + EXPORTS.mozilla += [ + "!origin_trials_ffi_generated.h", + ] + + CbindgenHeader( + "origin_trials_ffi_generated.h", + inputs=["ffi"], + ) + + GeneratedFile( + "keys.inc", + inputs=["test.pub", "prod.pub"], + script="gen-keys.py", + entry_point="generate", + ) + +MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.toml"] + +EXPORTS.mozilla += [ + "OriginTrials.h", + "OriginTrialsIPCUtils.h", +] + +UNIFIED_SOURCES += [ + "OriginTrials.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/origin-trials/prod.pub b/dom/origin-trials/prod.pub new file mode 100644 index 0000000000..1aee02676c --- /dev/null +++ b/dom/origin-trials/prod.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUb7TDvEyuLXntd4S0ERgv5YWLCiO +1VQI9gJ4nvVF/mD8OmFI6S42bKmzsgq0RVwEHJUAFpaWalhgJmfo3T9rTw== +-----END PUBLIC KEY----- diff --git a/dom/origin-trials/test.pub b/dom/origin-trials/test.pub new file mode 100644 index 0000000000..157bef8d05 --- /dev/null +++ b/dom/origin-trials/test.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESq52ZCSgVcRmDkMyTB2FCGNqk9Qd +D/y0LHc95ofc60bNz4jQ4zmP5Re4yNfT3DJ8TwiLYRndwAJfESBrRM8qZA== +-----END PUBLIC KEY----- diff --git a/dom/origin-trials/tests/mochitest/common.js b/dom/origin-trials/tests/mochitest/common.js new file mode 100644 index 0000000000..a38a975ea7 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/common.js @@ -0,0 +1,114 @@ +/* import-globals-from ../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */ + +// This would be a bit nicer with `self`, but Worklet doesn't have that, so +// `globalThis` it is, see https://github.com/whatwg/html/issues/7696 +function workerReply(port) { + port.postMessage({ + testTrialInterfaceExposed: !!globalThis.TestTrialInterface, + }); +} + +if ( + globalThis.SharedWorkerGlobalScope && + globalThis instanceof globalThis.SharedWorkerGlobalScope +) { + globalThis.addEventListener("connect", function (e) { + const port = e.ports[0]; + workerReply(port); + }); +} else if ( + globalThis.WorkerGlobalScope && + globalThis instanceof globalThis.WorkerGlobalScope +) { + workerReply(globalThis); +} else if ( + globalThis.WorkletGlobalScope && + globalThis instanceof globalThis.WorkletGlobalScope +) { + class Processor extends AudioWorkletProcessor { + constructor() { + super(); + this.port.start(); + workerReply(this.port); + } + + process(inputs, outputs, parameters) { + // Do nothing, output silence + return true; + } + } + registerProcessor("test-processor", Processor); +} + +function assertTestTrialActive(shouldBeActive) { + add_task(async function () { + info("Main thread test: " + document.URL); + is( + !!navigator.testTrialGatedAttribute, + shouldBeActive, + "Should match active status for Navigator.testTrialControlledAttribute" + ); + is( + !!self.TestTrialInterface, + shouldBeActive, + "Should match active status for TestTrialInterface" + ); + if (shouldBeActive) { + ok( + new self.TestTrialInterface(), + "Should be able to construct interface" + ); + } + + function promiseWorkerWorkletMessage(target, context) { + info(`promiseWorkerWorkletMessage(${context})`); + return new Promise(resolve => { + target.addEventListener( + "message", + function (e) { + is( + e.data.testTrialInterfaceExposed, + shouldBeActive, + "Should work as expected in " + context + ); + info(`got ${context} message`); + resolve(); + }, + { once: true } + ); + }); + } + + { + info("Worker test"); + const worker = new Worker("common.js"); + await promiseWorkerWorkletMessage(worker, "worker"); + worker.terminate(); + } + + { + info("SharedWorker test"); + // We want a unique worker per page since the trial state depends on the + // creator document. + const worker = new SharedWorker("common.js", document.URL); + const promise = promiseWorkerWorkletMessage(worker.port, "shared worker"); + worker.port.start(); + await promise; + } + + { + info("AudioWorklet test"); + const audioContext = new AudioContext(); + await audioContext.audioWorklet.addModule("common.js"); + audioContext.resume(); + const workletNode = new AudioWorkletNode(audioContext, "test-processor"); + const promise = promiseWorkerWorkletMessage(workletNode.port, "worklet"); + workletNode.port.start(); + await promise; + await audioContext.close(); + } + + // FIXME(emilio): Add more tests. + // * Stuff hanging off Window or Document (bug 1757935). + }); +} diff --git a/dom/origin-trials/tests/mochitest/file_subdomain_bad_frame.html b/dom/origin-trials/tests/mochitest/file_subdomain_bad_frame.html new file mode 100644 index 0000000000..fe5dae7bc1 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/file_subdomain_bad_frame.html @@ -0,0 +1,8 @@ +<!doctype html> +<!-- Created with: mktoken --origin 'https://example.com' --feature TestTrial --expiry '01 Jan 3000 01:00:00 +0100' --sign test-keys/test-ecdsa.pkcs8 --> +<meta http-equiv="origin-trial" content="A2BnUbfkkbKU8524UJACNw8UH6czqkmu+hkn9EhLesuleoBBPuthuTU8DCu7H80TgWmfCROkcZpBtXnEn7jYmhMAAABLeyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIiwiZmVhdHVyZSI6IlRlc3RUcmlhbCIsImV4cGlyeSI6MzI1MDM2ODAwMDB9"> +<script> + parent.postMessage({ + testTrialInterfaceExposed: !!globalThis.TestTrialInterface, + }, "https://example.com"); +</script> diff --git a/dom/origin-trials/tests/mochitest/file_subdomain_good_frame.html b/dom/origin-trials/tests/mochitest/file_subdomain_good_frame.html new file mode 100644 index 0000000000..a5cbb13db6 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/file_subdomain_good_frame.html @@ -0,0 +1,8 @@ +<!doctype html> +<!-- Created with: mktoken --origin 'https://example.com' --subdomain --feature TestTrial --expiry '01 Jan 3000 01:00:00 +0100' --sign test-keys/test-ecdsa.pkcs8 --> +<meta http-equiv="origin-trial" content="A/RnDeJo/c3P5otcVhRW7z00y2KtbiAH18HjGpZiBBdkvlFlafDSitJMb3SXavmnz7HXJyljoPsoa7nLN3XJLNYAAABeeyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIiwiZmVhdHVyZSI6IlRlc3RUcmlhbCIsImV4cGlyeSI6MzI1MDM2ODAwMDAsImlzU3ViZG9tYWluIjp0cnVlfQ=="> +<script> + parent.postMessage({ + testTrialInterfaceExposed: !!globalThis.TestTrialInterface, + }, "https://example.com"); +</script> diff --git a/dom/origin-trials/tests/mochitest/mochitest.toml b/dom/origin-trials/tests/mochitest/mochitest.toml new file mode 100644 index 0000000000..1c7bedb697 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/mochitest.toml @@ -0,0 +1,39 @@ +[DEFAULT] +prefs = [ + "dom.origin-trials.enabled=true", + "dom.origin-trials.test-key.enabled=true", + "browser.tabs.remote.coep.credentialless=false", +] +support-files = [ + "test_header_simple.html^headers^", + "common.js", +] +# * Test interfaces only exposed on DEBUG builds. +# * xorigin tests run in example.org rather than example.com, so token +# verification fails, expectedly. +skip-if = [ + "!debug", + "xorigin", # AudioWorklet requires secure context +] + +scheme = "https" + +["test_expired_token.html"] + +["test_header_simple.html"] + +["test_meta_simple.html"] + +["test_subdomain.html"] +support-files = [ + "file_subdomain_good_frame.html", + "file_subdomain_bad_frame.html", +] +skip-if = [ + "http3", + "http2", +] + +["test_trial_hidden.html"] + +["test_wrong_origin.html"] diff --git a/dom/origin-trials/tests/mochitest/test_expired_token.html b/dom/origin-trials/tests/mochitest/test_expired_token.html new file mode 100644 index 0000000000..48bd4d1215 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/test_expired_token.html @@ -0,0 +1,8 @@ +<!doctype html> +<!-- Created with: mktoken --origin 'https://example.com' --feature TestTrial --expiry '01 Jan 2000 01:00:00 +0100' --sign test-keys/test-ecdsa.pkcs8 --> +<meta http-equiv="origin-trial" content="A41+wSMhPQeR9B+AofdiFzheyZVF+gP4ubTNzrt6v8Qcjv68j1eINNFCxVe5/vdy4cO9dGDkwd9eizsib70RgAQAAABJeyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIiwiZmVhdHVyZSI6IlRlc3RUcmlhbCIsImV4cGlyeSI6OTQ2Njg0ODAwfQ=="> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="common.js"></script> +<script> + assertTestTrialActive(false); +</script> diff --git a/dom/origin-trials/tests/mochitest/test_header_simple.html b/dom/origin-trials/tests/mochitest/test_header_simple.html new file mode 100644 index 0000000000..2c378d3524 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/test_header_simple.html @@ -0,0 +1,6 @@ +<!doctype html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="common.js"></script> +<script> + assertTestTrialActive(true); +</script> diff --git a/dom/origin-trials/tests/mochitest/test_header_simple.html^headers^ b/dom/origin-trials/tests/mochitest/test_header_simple.html^headers^ new file mode 100644 index 0000000000..2765b5bd11 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/test_header_simple.html^headers^ @@ -0,0 +1 @@ +Origin-Trial: AyGdETIKWLLqe+chG57f74gZcjYSfbdYAapEq7DA49E6CmaYaPmaoXh/4tAe5XJJJdwwpFVal7hz/irC+Wvp1HgAAABLeyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIiwiZmVhdHVyZSI6IlRlc3RUcmlhbCIsImV4cGlyeSI6MzI1MDM2ODAwMDB9 diff --git a/dom/origin-trials/tests/mochitest/test_meta_simple.html b/dom/origin-trials/tests/mochitest/test_meta_simple.html new file mode 100644 index 0000000000..8b853a8774 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/test_meta_simple.html @@ -0,0 +1,13 @@ +<!doctype html> +<!-- Created with: mktoken --origin 'https://example.com' --feature TestTrial --expiry 'Wed, 01 Jan 3000 01:00:00 +0100' --sign test-keys/test-ecdsa.pkcs8 --> +<meta http-equiv="origin-trial" content="AyGdETIKWLLqe+chG57f74gZcjYSfbdYAapEq7DA49E6CmaYaPmaoXh/4tAe5XJJJdwwpFVal7hz/irC+Wvp1HgAAABLeyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIiwiZmVhdHVyZSI6IlRlc3RUcmlhbCIsImV4cGlyeSI6MzI1MDM2ODAwMDB9"> +<!-- Created with: mktoken --origin 'https://example.com' --feature CoepCredentialless --expiry 'Wed, 01 Jan 3000 01:00:00 +0100' --sign test-keys/test-ecdsa.pkcs8 --> +<meta http-equiv="origin-trial" content="Az+DK2Kczk8Xz1cAlD+TkvPZmuM2uJZ2CFefbp2hLuCU9FbUqxWTyQ2tEYr50r0syKELcOZLAPaABw8aYTLHn5YAAABUeyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIiwiZmVhdHVyZSI6IkNvZXBDcmVkZW50aWFsbGVzcyIsImV4cGlyeSI6MzI1MDM2ODAwMDB9"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="common.js"></script> +<script> + assertTestTrialActive(true); + add_task(function() { + ok(!!SpecialPowers.DOMWindowUtils.isCoepCredentialless(), "CoepCredentialless trial works."); + }); +</script> diff --git a/dom/origin-trials/tests/mochitest/test_subdomain.html b/dom/origin-trials/tests/mochitest/test_subdomain.html new file mode 100644 index 0000000000..3814e1e95b --- /dev/null +++ b/dom/origin-trials/tests/mochitest/test_subdomain.html @@ -0,0 +1,28 @@ +<!doctype html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<iframe></iframe> +<script> + async function testFrame(file, expectEnabled) { + let reply = new Promise(resolve => { + window.addEventListener("message", function(e) { + resolve(e.data); + }, { once: true }); + }); + + let iframe = document.querySelector("iframe"); + let load = new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + + iframe.src = "https://www.example.com/" + location.pathname.replace("test_subdomain.html", file); + info("loading " + iframe.src); + await load; + let data = await reply; + is(data.testTrialInterfaceExposed, expectEnabled); + } + + add_task(async function test_subdomain() { + await testFrame("file_subdomain_good_frame.html", true); + await testFrame("file_subdomain_bad_frame.html", false); + }); +</script> diff --git a/dom/origin-trials/tests/mochitest/test_trial_hidden.html b/dom/origin-trials/tests/mochitest/test_trial_hidden.html new file mode 100644 index 0000000000..90f7f8da0a --- /dev/null +++ b/dom/origin-trials/tests/mochitest/test_trial_hidden.html @@ -0,0 +1,6 @@ +<!doctype html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="common.js"></script> +<script> + assertTestTrialActive(false); +</script> diff --git a/dom/origin-trials/tests/mochitest/test_wrong_origin.html b/dom/origin-trials/tests/mochitest/test_wrong_origin.html new file mode 100644 index 0000000000..adb3a0e900 --- /dev/null +++ b/dom/origin-trials/tests/mochitest/test_wrong_origin.html @@ -0,0 +1,8 @@ +<!doctype html> +<!-- Created with: mktoken --origin 'https://not-example.com' --feature TestTrial --expiry '01 Jan 3000 01:00:00 +0100' --sign test-keys/test-ecdsa.pkcs8 --> +<meta http-equiv="origin-trial" content="A1nUsa3CwtYj28syX2jYUogdrg+ZsjjNfAvmdg3SGybXxaJFbNq7i8AmY6Fo3OUe6Xvza3R0YYfaGTqM0TOU2OAAAABPeyJvcmlnaW4iOiJodHRwczovL25vdC1leGFtcGxlLmNvbSIsImZlYXR1cmUiOiJUZXN0VHJpYWwiLCJleHBpcnkiOjMyNTAzNjgwMDAwfQ=="> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="common.js"></script> +<script> + assertTestTrialActive(false); +</script> |