зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1755956
- Add initial boilerplate for origin trials. r=smaug,hsivonen
This patch imports and implements all the infrastructure for origin trial tokens, minus the crypto stuff / token verification. We still don't hook it anywhere. The intended setup for now would be to have the `OriginTrials` object hanging off the `Document` (or global perhaps, not sure yet). That has a self-descriptive API to enable trials (UpdateFromToken), and check enabledness status (IsEnabled). There are some tests in the origin-trial-token crate (third_party/rust/origin-trial-token/tests.rs). No test for the DOM code yet because this isn't hooked into yet. Differential Revision: https://phabricator.services.mozilla.com/D139033
This commit is contained in:
Родитель
e0e12f120a
Коммит
5c525da4a0
|
@ -2058,6 +2058,7 @@ dependencies = [
|
|||
"netwerk_helper",
|
||||
"nserror",
|
||||
"nsstring",
|
||||
"origin-trials-ffi",
|
||||
"prefs_parser",
|
||||
"processtools",
|
||||
"profiler_helper",
|
||||
|
@ -3712,6 +3713,23 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "origin-trial-token"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a4bbc9a3f0da0355296ec1884b80c3730be9c4013b736840cb460b698012a4d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "origin-trials-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"origin-trial-token",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.0"
|
||||
|
|
|
@ -41,6 +41,7 @@ exclude = [
|
|||
"xpcom/rust/gkrust_utils",
|
||||
"tools/lint/test/files/clippy",
|
||||
"tools/fuzzing/rust",
|
||||
"dom/origin-trials/ffi",
|
||||
]
|
||||
|
||||
# Use the new dependency resolver to reduce some of the platform-specific dependencies.
|
||||
|
|
|
@ -109,6 +109,7 @@ DIRS += [
|
|||
"localstorage",
|
||||
"prio",
|
||||
"l10n",
|
||||
"origin-trials",
|
||||
]
|
||||
|
||||
if CONFIG["MOZ_DOM_STREAMS"]:
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/* -*- 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 "nsString.h"
|
||||
#include "nsIPrincipal.h"
|
||||
#include "nsIURI.h"
|
||||
#include "nsNetUtil.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
bool VerifySignature(const uint8_t* aSignature, uintptr_t aSignatureLen,
|
||||
const uint8_t* aData, uintptr_t aDataLen,
|
||||
void* aUserData) {
|
||||
MOZ_RELEASE_ASSERT(aSignatureLen == 64);
|
||||
// TODO(emilio): Implement.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MatchesOrigin(const uint8_t* aOrigin, size_t aOriginLen, bool aIsSubdomain,
|
||||
bool aIsThirdParty, bool aIsUsageSubset, void* aUserData) {
|
||||
if (aIsThirdParty || aIsSubdomain || aIsUsageSubset) {
|
||||
// TODO(emilio): Support third-party tokens and so on.
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* principal = static_cast<nsIPrincipal*>(aUserData);
|
||||
nsDependentCSubstring origin(reinterpret_cast<const char*>(aOrigin),
|
||||
aOriginLen);
|
||||
nsCOMPtr<nsIURI> originURI;
|
||||
if (NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(originURI), origin)))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return principal->IsSameOrigin(originURI);
|
||||
}
|
||||
|
||||
void OriginTrials::UpdateFromToken(const nsAString& aBase64EncodedToken,
|
||||
nsIPrincipal* aPrincipal) {
|
||||
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 (!result.IsSuccess()) {
|
||||
return; // TODO(emilio): Maybe report to console or what not?
|
||||
}
|
||||
OriginTrial trial = result.AsSuccess().trial;
|
||||
mEnabledTrials += trial;
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
|
@ -0,0 +1,42 @@
|
|||
/* -*- 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;
|
||||
|
||||
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 {
|
||||
// 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 {
|
||||
return mEnabledTrials.contains(aTrial);
|
||||
}
|
||||
|
||||
private:
|
||||
EnumSet<OriginTrial> mEnabledTrials;
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
#endif
|
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
Cargo.lock
|
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
Cargo.lock
|
|
@ -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"
|
|
@ -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"
|
|
@ -0,0 +1,131 @@
|
|||
/* 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::{Token, TokenValidationError, Usage};
|
||||
use std::ffi::c_void;
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum OriginTrial {
|
||||
TestTrial,
|
||||
}
|
||||
|
||||
impl OriginTrial {
|
||||
fn from_str(s: &str) -> Option<Self> {
|
||||
return Some(match s {
|
||||
"TestTrial" => Self::TestTrial,
|
||||
_ => return None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum OriginTrialResult {
|
||||
Success { trial: OriginTrial },
|
||||
BufferTooSmall,
|
||||
MismatchedPayloadSize { expected: usize, actual: usize },
|
||||
InvalidSignature,
|
||||
UnknownVersion,
|
||||
UnsupportedThirdPartyToken,
|
||||
UnexpectedUsageInNonThirdPartyToken,
|
||||
MalformedPayload,
|
||||
ExpiredToken,
|
||||
UnknownTrial,
|
||||
OriginMismatch,
|
||||
}
|
||||
|
||||
/// 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 token = Token::from_buffer(slice, |signature, data| {
|
||||
(params.verify_signature)(
|
||||
signature.as_ptr(),
|
||||
signature.len(),
|
||||
data.as_ptr(),
|
||||
data.len(),
|
||||
params.user_data,
|
||||
)
|
||||
});
|
||||
|
||||
let token = match token {
|
||||
Ok(token) => token,
|
||||
Err(e) => {
|
||||
return 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,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
OriginTrialResult::Success { trial }
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# -*- 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"],
|
||||
)
|
||||
|
||||
EXPORTS.mozilla += [
|
||||
"OriginTrials.h",
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
"OriginTrials.cpp",
|
||||
]
|
||||
|
||||
FINAL_LIBRARY = "xul"
|
|
@ -0,0 +1 @@
|
|||
{"files":{"Cargo.toml":"a623e84593c3b41a613035b84d1f2731bae649dc7fddb17e2828314190e809f1","lib.rs":"8eeadedf37345718ebf9cf6209bf37ca162a88231e60975234ea6ee4fa1ac16d","tests.rs":"57f9ad76ff59be932c5176687bcce597764d35e7af5a3d23d59c89231b0d97ae"},"package":"1a4bbc9a3f0da0355296ec1884b80c3730be9c4013b736840cb460b698012a4d"}
|
|
@ -0,0 +1,31 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "origin-trial-token"
|
||||
version = "0.1.0"
|
||||
authors = ["Emilio Cobos Álvarez <emilio@crisal.io>"]
|
||||
description = "An implementation of the Chrome Origin Trial token format"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/emilio/origin-trial-token"
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0"
|
||||
[dev-dependencies.base64]
|
||||
version = "0.13"
|
|
@ -0,0 +1,258 @@
|
|||
/* 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/. */
|
||||
|
||||
//! Implements a simple processor for
|
||||
//! https://github.com/chromium/chromium/blob/d7da0240cae77824d1eda25745c4022757499131/third_party/blink/public/common/origin_trials/origin_trials_token_structure.md
|
||||
//!
|
||||
//! This crate intentionally leaves the cryptography to the caller. See the
|
||||
//! tools/ directory for example usages.
|
||||
|
||||
|
||||
/// Latest version as documented.
|
||||
pub const LATEST_VERSION: u8 = 3;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct RawToken {
|
||||
version: u8,
|
||||
signature: [u8; 64],
|
||||
payload_length: [u8; 4],
|
||||
/// Payload is an slice of payload_length bytes, and has to be verified
|
||||
/// before returning it to the caller.
|
||||
payload: [u8; 0],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TokenValidationError {
|
||||
BufferTooSmall,
|
||||
MismatchedPayloadSize { expected: usize, actual: usize },
|
||||
InvalidSignature,
|
||||
UnknownVersion,
|
||||
UnsupportedThirdPartyToken,
|
||||
UnexpectedUsageInNonThirdPartyToken,
|
||||
MalformedPayload(serde_json::Error),
|
||||
}
|
||||
|
||||
impl RawToken {
|
||||
const HEADER_SIZE: usize = std::mem::size_of::<Self>();
|
||||
|
||||
#[inline]
|
||||
pub fn version(&self) -> u8 {
|
||||
self.version
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn signature(&self) -> &[u8; 64] {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn payload_length(&self) -> usize {
|
||||
u32::from_be_bytes(self.payload_length) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_buffer(&self) -> &[u8] {
|
||||
let buffer_size = Self::HEADER_SIZE + self.payload_length();
|
||||
unsafe { std::slice::from_raw_parts(self as *const _ as *const u8, buffer_size) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
let len = self.payload_length();
|
||||
unsafe { std::slice::from_raw_parts(self.payload.as_ptr(), len) }
|
||||
}
|
||||
|
||||
/// Returns a RawToken from a raw buffer.
|
||||
pub fn from_buffer<'a>(buffer: &'a [u8]) -> Result<&'a Self, TokenValidationError> {
|
||||
if buffer.len() <= Self::HEADER_SIZE {
|
||||
return Err(TokenValidationError::BufferTooSmall);
|
||||
}
|
||||
assert_eq!(
|
||||
std::mem::align_of::<Self>(),
|
||||
1,
|
||||
"RawToken is a view over the buffer"
|
||||
);
|
||||
let raw_token = unsafe { &*(buffer.as_ptr() as *const Self) };
|
||||
let payload = &buffer[Self::HEADER_SIZE..];
|
||||
let expected = raw_token.payload_length();
|
||||
let actual = payload.len();
|
||||
if expected != actual {
|
||||
return Err(TokenValidationError::MismatchedPayloadSize { expected, actual });
|
||||
}
|
||||
Ok(raw_token)
|
||||
}
|
||||
|
||||
/// The data to verify the signature in this raw token.
|
||||
fn signature_data(&self) -> Vec<u8> {
|
||||
Self::raw_signature_data(self.version, self.payload())
|
||||
}
|
||||
|
||||
/// The data to sign or verify given a payload and a version.
|
||||
fn raw_signature_data(version: u8, payload: &[u8]) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(payload.len() + 5);
|
||||
data.push(version);
|
||||
data.extend((payload.len() as u32).to_be_bytes());
|
||||
data.extend(payload);
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Usage {
|
||||
#[serde(rename = "")]
|
||||
None,
|
||||
Subset,
|
||||
}
|
||||
|
||||
impl Usage {
|
||||
fn is_none(&self) -> bool {
|
||||
*self == Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Usage {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_false(t: &bool) -> bool {
|
||||
*t == false
|
||||
}
|
||||
|
||||
/// An already decoded and maybe-verified token.
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token {
|
||||
pub origin: String,
|
||||
pub feature: String,
|
||||
pub expiry: u64, // Timestamp. Seconds since epoch.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub is_subdomain: bool,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub is_third_party: bool,
|
||||
#[serde(default, skip_serializing_if = "Usage::is_none")]
|
||||
pub usage: Usage,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
#[inline]
|
||||
pub fn origin(&self) -> &str {
|
||||
&self.origin
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn feature(&self) -> &str {
|
||||
&self.feature
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn expiry_since_unix_epoch(&self) -> std::time::Duration {
|
||||
std::time::Duration::from_secs(self.expiry)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn expiry_time(&self) -> Option<std::time::SystemTime> {
|
||||
std::time::UNIX_EPOCH.checked_add(self.expiry_since_unix_epoch())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_expired(&self) -> bool {
|
||||
let now_duration = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("System time before epoch?");
|
||||
now_duration >= self.expiry_since_unix_epoch()
|
||||
}
|
||||
|
||||
/// Most high-level function: For a given buffer, tries to parse it and
|
||||
/// verify it as a token.
|
||||
pub fn from_buffer(
|
||||
buffer: &[u8],
|
||||
verify_signature: impl FnOnce(&[u8; 64], &[u8]) -> bool,
|
||||
) -> Result<Self, TokenValidationError> {
|
||||
Self::from_raw_token(RawToken::from_buffer(buffer)?, verify_signature)
|
||||
}
|
||||
|
||||
/// Validates a RawToken's signature and converts the token if valid.
|
||||
pub fn from_raw_token(
|
||||
token: &RawToken,
|
||||
verify_signature: impl FnOnce(&[u8; 64], &[u8]) -> bool,
|
||||
) -> Result<Self, TokenValidationError> {
|
||||
let signature_data = token.signature_data();
|
||||
if !verify_signature(&token.signature, &signature_data) {
|
||||
return Err(TokenValidationError::InvalidSignature);
|
||||
}
|
||||
Self::from_payload(token.version, token.payload())
|
||||
}
|
||||
|
||||
/// Converts the token from a raw payload, version pair.
|
||||
pub fn from_payload(version: u8, payload: &[u8]) -> Result<Self, TokenValidationError> {
|
||||
if version != 2 && version != 3 {
|
||||
assert_ne!(version, LATEST_VERSION);
|
||||
return Err(TokenValidationError::UnknownVersion);
|
||||
}
|
||||
|
||||
let token: Token = match serde_json::from_slice(payload) {
|
||||
Ok(t) => t,
|
||||
Err(e) => return Err(TokenValidationError::MalformedPayload(e)),
|
||||
};
|
||||
|
||||
// Third-party tokens are not supported in version 2.
|
||||
if token.is_third_party {
|
||||
if version == 2 {
|
||||
return Err(TokenValidationError::UnsupportedThirdPartyToken);
|
||||
}
|
||||
} else if !token.usage.is_none() {
|
||||
return Err(TokenValidationError::UnexpectedUsageInNonThirdPartyToken);
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Converts the token to a raw payload.
|
||||
pub fn to_payload(&self) -> Vec<u8> {
|
||||
serde_json::to_string(self)
|
||||
.expect("Should always be able to turn a token into a payload")
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
/// Converts the token to the data that should be signed.
|
||||
pub fn to_signature_data(&self) -> Vec<u8> {
|
||||
RawToken::raw_signature_data(LATEST_VERSION, &self.to_payload())
|
||||
}
|
||||
|
||||
/// Turns the token into a fully signed token.
|
||||
pub fn to_signed_token(&self, sign: impl FnOnce(&[u8]) -> [u8; 64]) -> Vec<u8> {
|
||||
self.to_signed_token_with_payload(sign, &self.to_payload())
|
||||
}
|
||||
|
||||
/// DO NOT EXPOSE: This is intended for testing only. We need to test with
|
||||
/// the original payload so that the tokens match, but we assert
|
||||
/// that self.to_payload() and payload are equivalent.
|
||||
fn to_signed_token_with_payload(
|
||||
&self,
|
||||
sign: impl FnOnce(&[u8]) -> [u8; 64],
|
||||
payload: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let signature_data_with_payload = RawToken::raw_signature_data(LATEST_VERSION, &payload);
|
||||
let signature = sign(&signature_data_with_payload);
|
||||
|
||||
let mut buffer = Vec::with_capacity(1 + signature.len() + 4 + payload.len());
|
||||
buffer.push(LATEST_VERSION);
|
||||
buffer.extend(signature);
|
||||
buffer.extend((payload.len() as u32).to_be_bytes());
|
||||
buffer.extend(payload);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
let token = Self::from_buffer(&buffer, |_, _| true).expect("Creating malformed token?");
|
||||
assert_eq!(self, &token, "Token differs after deserialization?");
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
|
@ -0,0 +1,111 @@
|
|||
/* 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 super::*;
|
||||
|
||||
fn mock_verify(_signature: &[u8; 64], _data: &[u8]) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// We'd like to just assert_eq!(original_payload, our_payload), but our JSON
|
||||
/// serialization format is different (we don't have spaces after commas or
|
||||
/// colons), so we need to do this instead.
|
||||
fn assert_payloads_equivalent(our_payload: &[u8], original_payload: &[u8]) {
|
||||
// Per the above we expect our payload to always be smaller than the
|
||||
// original.
|
||||
assert!(our_payload.len() <= original_payload.len());
|
||||
|
||||
let our_value: serde_json::Value = serde_json::from_slice(our_payload).unwrap();
|
||||
let original_value: serde_json::Value = serde_json::from_slice(original_payload).unwrap();
|
||||
if our_value == original_value {
|
||||
return;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
std::str::from_utf8(our_payload).unwrap(),
|
||||
std::str::from_utf8(original_payload).unwrap(),
|
||||
"Mismatched payloads"
|
||||
);
|
||||
}
|
||||
|
||||
fn test_roundtrip(payload: &[u8], token: &Token, base64: &[u8]) {
|
||||
let binary = base64::decode(base64).unwrap();
|
||||
let raw_token = RawToken::from_buffer(&binary).unwrap();
|
||||
let from_binary_token = Token::from_raw_token(&raw_token, mock_verify).unwrap();
|
||||
assert_eq!(&from_binary_token, token);
|
||||
|
||||
// LMAO, payload in the documentation and the examples have members out of
|
||||
// order so this doesn't hold.
|
||||
// assert_eq!(std::str::from_utf8(raw_token.payload()).unwrap(), std::str::from_utf8(payload).unwrap());
|
||||
|
||||
let our_payload = from_binary_token.to_payload();
|
||||
assert_payloads_equivalent(&our_payload, payload);
|
||||
assert_payloads_equivalent(&our_payload, raw_token.payload());
|
||||
|
||||
let signed = from_binary_token
|
||||
.to_signed_token_with_payload(|_data| raw_token.signature.clone(), raw_token.payload());
|
||||
assert_eq!(binary, signed);
|
||||
|
||||
let new_base64 = base64::encode(signed);
|
||||
assert_eq!(new_base64, std::str::from_utf8(base64).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
// The one from the example.
|
||||
let payload =
|
||||
r#"{"origin": "https://example.com:443", "feature": "Frobulate", "expiry": 1609459199}"#;
|
||||
let token = Token::from_payload(LATEST_VERSION, payload.as_bytes()).unwrap();
|
||||
assert_eq!(token.origin, "https://example.com:443");
|
||||
assert_eq!(token.feature, "Frobulate");
|
||||
assert_eq!(token.expiry, 1609459199);
|
||||
assert_eq!(token.is_subdomain, false);
|
||||
assert_eq!(token.is_third_party, false);
|
||||
assert!(token.usage.is_none());
|
||||
|
||||
test_roundtrip(payload.as_bytes(), &token, b"A9YTk5WLM0uhXPj2OE/dEj8mEdWbcWOvCyWMNdRFiCZpBRuynxJMx1i/SO5pRT7UhoCSDTieoh9qOCMHsc2y5w4AAABTeyJvcmlnaW4iOiAiaHR0cHM6Ly9leGFtcGxlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5IjogMTYwOTQ1OTE5OX0=");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subdomain() {
|
||||
// The one from the example.
|
||||
let payload = r#"{"origin": "https://example.com:443", "isSubdomain": true, "feature": "Frobulate", "expiry": 1609459199}"#;
|
||||
let token = Token::from_payload(LATEST_VERSION, payload.as_bytes()).unwrap();
|
||||
assert_eq!(token.origin, "https://example.com:443");
|
||||
assert_eq!(token.feature, "Frobulate");
|
||||
assert_eq!(token.expiry, 1609459199);
|
||||
assert_eq!(token.is_subdomain, true);
|
||||
assert_eq!(token.is_third_party, false);
|
||||
assert!(token.usage.is_none());
|
||||
|
||||
test_roundtrip(payload.as_bytes(), &token, b"AzHieSb3NXHXhJ1zvxNcmUeR351wzlXwJK7pYM8MCFfNenvonZi30kS0GOKWUleIyats/2aTB1HoiCmLWIvG5AgAAABoeyJvcmlnaW4iOiAiaHR0cHM6Ly9leGFtcGxlLmNvbTo0NDMiLCAiaXNTdWJkb21haW4iOiB0cnVlLCAiZmVhdHVyZSI6ICJGcm9idWxhdGUiLCAiZXhwaXJ5IjogMTYwOTQ1OTE5OX0=");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_party() {
|
||||
let payload = r#"{"origin": "https://thirdparty.com:443", "feature": "Frobulate", "expiry": 1609459199, "isThirdParty": true}"#;
|
||||
let token = Token::from_payload(LATEST_VERSION, payload.as_bytes()).unwrap();
|
||||
assert_eq!(token.origin, "https://thirdparty.com:443");
|
||||
assert_eq!(token.feature, "Frobulate");
|
||||
assert_eq!(token.expiry, 1609459199);
|
||||
assert_eq!(token.is_subdomain, false);
|
||||
assert_eq!(token.is_third_party, true);
|
||||
assert!(token.usage.is_none());
|
||||
|
||||
test_roundtrip(payload.as_bytes(), &token, b"Ax8UsCU9EUBRj8PZG147cOO7VqR86BF13TSu6w2wRqixzJ+fEUULvOQimXwWl1ETYCfAZMlvvAqoFYB8HxrsZA4AAABseyJvcmlnaW4iOiAiaHR0cHM6Ly90aGlyZHBhcnR5LmNvbTo0NDMiLCAiaXNUaGlyZFBhcnR5IjogdHJ1ZSwgImZlYXR1cmUiOiAiRnJvYnVsYXRlIiwgImV4cGlyeSI6IDE2MDk0NTkxOTl9");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_party_usage_restriction() {
|
||||
let payload = r#"{"origin": "https://thirdparty.com:443", "feature": "Frobulate", "expiry": 1609459199, "isThirdParty": true, "usage": "subset"}"#;
|
||||
let token = Token::from_payload(LATEST_VERSION, payload.as_bytes()).unwrap();
|
||||
assert_eq!(token.origin, "https://thirdparty.com:443");
|
||||
assert_eq!(token.feature, "Frobulate");
|
||||
assert_eq!(token.expiry, 1609459199);
|
||||
assert_eq!(token.is_subdomain, false);
|
||||
assert_eq!(token.is_third_party, true);
|
||||
assert_eq!(token.usage, Usage::Subset);
|
||||
|
||||
test_roundtrip(payload.as_bytes(), &token, b"AzEs7XzQG5ktWF/puroSU5RzxPEdEUUhqwXtL2hItZoJU0bghKwbsTKVghkR95GHSfINTBnxwRBnFVfYGJLm8AUAAAB/eyJvcmlnaW4iOiAiaHR0cHM6Ly90aGlyZHBhcnR5LmNvbTo0NDMiLCAiaXNUaGlyZFBhcnR5IjogdHJ1ZSwgInVzYWdlIjogInN1YnNldCIsICJmZWF0dXJlIjogIkZyb2J1bGF0ZSIsICJleHBpcnkiOiAxNjA5NDU5MTk5fQ==");
|
||||
}
|
|
@ -58,6 +58,7 @@ fluent-langneg-ffi = { path = "../../../../intl/locale/rust/fluent-langneg-ffi"
|
|||
rust_minidump_writer_linux = { path = "../../../crashreporter/rust_minidump_writer_linux", optional = true }
|
||||
gecko-profiler = { path = "../../../../tools/profiler/rust-api"}
|
||||
midir_impl = { path = "../../../../dom/midi/midir_impl", optional = true }
|
||||
origin-trials-ffi = { path = "../../../../dom/origin-trials/ffi" }
|
||||
|
||||
# Note: `modern_sqlite` means rusqlite's bindings file be for a sqlite with
|
||||
# version less than or equal to what we link to. This isn't a problem because we
|
||||
|
|
|
@ -87,6 +87,8 @@ extern crate rust_minidump_writer_linux;
|
|||
#[cfg(feature = "webmidi_midir_impl")]
|
||||
extern crate midir_impl;
|
||||
|
||||
extern crate origin_trials_ffi;
|
||||
|
||||
extern crate log;
|
||||
use log::info;
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче