summaryrefslogtreecommitdiffstats
path: root/dom/origin-trials
diff options
context:
space:
mode:
Diffstat (limited to 'dom/origin-trials')
-rw-r--r--dom/origin-trials/OriginTrials.cpp248
-rw-r--r--dom/origin-trials/OriginTrials.h59
-rw-r--r--dom/origin-trials/OriginTrialsIPCUtils.h45
-rw-r--r--dom/origin-trials/ffi/.gitignore2
-rw-r--r--dom/origin-trials/ffi/Cargo.toml14
-rw-r--r--dom/origin-trials/ffi/cbindgen.toml18
-rw-r--r--dom/origin-trials/ffi/lib.rs150
-rw-r--r--dom/origin-trials/gen-keys.py42
-rw-r--r--dom/origin-trials/moz.build40
-rw-r--r--dom/origin-trials/prod.pub4
-rw-r--r--dom/origin-trials/test.pub4
-rw-r--r--dom/origin-trials/tests/mochitest/common.js114
-rw-r--r--dom/origin-trials/tests/mochitest/file_subdomain_bad_frame.html8
-rw-r--r--dom/origin-trials/tests/mochitest/file_subdomain_good_frame.html8
-rw-r--r--dom/origin-trials/tests/mochitest/mochitest.ini26
-rw-r--r--dom/origin-trials/tests/mochitest/test_expired_token.html8
-rw-r--r--dom/origin-trials/tests/mochitest/test_header_simple.html6
-rw-r--r--dom/origin-trials/tests/mochitest/test_header_simple.html^headers^1
-rw-r--r--dom/origin-trials/tests/mochitest/test_meta_simple.html13
-rw-r--r--dom/origin-trials/tests/mochitest/test_subdomain.html28
-rw-r--r--dom/origin-trials/tests/mochitest/test_trial_hidden.html6
-rw-r--r--dom/origin-trials/tests/mochitest/test_wrong_origin.html8
22 files changed, 852 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(), &params);
+ 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..bc8b1840e7
--- /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.ini"]
+
+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.ini b/dom/origin-trials/tests/mochitest/mochitest.ini
new file mode 100644
index 0000000000..c05d2cc55d
--- /dev/null
+++ b/dom/origin-trials/tests/mochitest/mochitest.ini
@@ -0,0 +1,26 @@
+[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_meta_simple.html]
+[test_header_simple.html]
+[test_trial_hidden.html]
+[test_expired_token.html]
+[test_wrong_origin.html]
+[test_subdomain.html]
+support-files =
+ file_subdomain_good_frame.html
+ file_subdomain_bad_frame.html
+skip-if =
+ http3
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>