summaryrefslogtreecommitdiffstats
path: root/third_party/rust/origin-trial-token
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/origin-trial-token
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--third_party/rust/origin-trial-token/.cargo-checksum.json1
-rw-r--r--third_party/rust/origin-trial-token/Cargo.toml33
-rw-r--r--third_party/rust/origin-trial-token/lib.rs268
-rw-r--r--third_party/rust/origin-trial-token/tests.rs111
4 files changed, 413 insertions, 0 deletions
diff --git a/third_party/rust/origin-trial-token/.cargo-checksum.json b/third_party/rust/origin-trial-token/.cargo-checksum.json
new file mode 100644
index 0000000000..05af142f3d
--- /dev/null
+++ b/third_party/rust/origin-trial-token/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"27460882de26573b7e12614a45da4ba5deaec4a989e28b54b6eb4df4fd8d0164","lib.rs":"9a07944ac0bdb2c05c52e406adde9433dd9b205613186cca105ee4d443e6f414","tests.rs":"57f9ad76ff59be932c5176687bcce597764d35e7af5a3d23d59c89231b0d97ae"},"package":"94cb60fca11d2efd72ab0e0ad0298089307a15b14313178416a96476dbea4550"} \ No newline at end of file
diff --git a/third_party/rust/origin-trial-token/Cargo.toml b/third_party/rust/origin-trial-token/Cargo.toml
new file mode 100644
index 0000000000..c78becce9f
--- /dev/null
+++ b/third_party/rust/origin-trial-token/Cargo.toml
@@ -0,0 +1,33 @@
+# 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.1"
+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/mozilla/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"
diff --git a/third_party/rust/origin-trial-token/lib.rs b/third_party/rust/origin-trial-token/lib.rs
new file mode 100644
index 0000000000..8872095a1d
--- /dev/null
+++ b/third_party/rust/origin-trial-token/lib.rs
@@ -0,0 +1,268 @@
+/* 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
+ }
+
+ /// Verify the signature of this raw token.
+ pub fn verify(&self, verify_signature: impl FnOnce(&[u8; 64], &[u8]) -> bool) -> bool {
+ let signature_data = self.signature_data();
+ verify_signature(&self.signature, &signature_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> {
+ if !token.verify(verify_signature) {
+ return Err(TokenValidationError::InvalidSignature);
+ }
+ Self::from_raw_token_unverified(token)
+ }
+
+ /// Converts the token from a raw token, without verifying first.
+ pub fn from_raw_token_unverified(token: &RawToken) -> Result<Self, TokenValidationError> {
+ 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;
diff --git a/third_party/rust/origin-trial-token/tests.rs b/third_party/rust/origin-trial-token/tests.rs
new file mode 100644
index 0000000000..a402d05630
--- /dev/null
+++ b/third_party/rust/origin-trial-token/tests.rs
@@ -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==");
+}