diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/rust/origin-trial-token | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/origin-trial-token')
-rw-r--r-- | third_party/rust/origin-trial-token/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/origin-trial-token/Cargo.toml | 33 | ||||
-rw-r--r-- | third_party/rust/origin-trial-token/lib.rs | 268 | ||||
-rw-r--r-- | third_party/rust/origin-trial-token/tests.rs | 111 |
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=="); +} |