diff --git a/Cargo.lock b/Cargo.lock index c1171dad5d..50d4ee9dde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -535,6 +535,19 @@ dependencies = [ "serde", ] +[[package]] +name = "canonical_json" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89083fd014d71c47a718d7f4ac050864dac8587668dbe90baf9e261064c5710" +dependencies = [ + "hex", + "regex", + "serde", + "serde_json", + "thiserror 1.0.31", +] + [[package]] name = "cargo-platform" version = "0.1.2" @@ -4137,6 +4150,7 @@ version = "0.1.0" dependencies = [ "anyhow", "camino", + "canonical_json", "error-support", "expect-test", "firefox-versioning", diff --git a/components/remote_settings/Cargo.toml b/components/remote_settings/Cargo.toml index 326dc4f707..3e50b1f3b8 100644 --- a/components/remote_settings/Cargo.toml +++ b/components/remote_settings/Cargo.toml @@ -31,6 +31,7 @@ regex = "1.9" anyhow = "1.0" firefox-versioning = { path = "../support/firefox-versioning" } sha2 = "^0.10" +canonical_json = "0.5" [build-dependencies] uniffi = { version = "0.28.2", features = ["build"] } diff --git a/components/remote_settings/src/client.rs b/components/remote_settings/src/client.rs index 74bc2a2154..701a6c4215 100644 --- a/components/remote_settings/src/client.rs +++ b/components/remote_settings/src/client.rs @@ -201,6 +201,16 @@ impl RemoteSettingsClient { ) } + fn verify_signature(&self) -> Result<()> { + let mut inner = self.inner.lock(); + let collection_url = inner.api_client.collection_url(); + let timestamp = inner.storage.get_last_modified_timestamp(&collection_url)?; + let records = inner.storage.get_records(&collection_url)?; + let metadata = inner.storage.get_collection_metadata(&collection_url)?; + + Ok(()) + } + /// Downloads an attachment from [attachment_location]. NOTE: there are no guarantees about a /// maximum size, so use care when fetching potentially large attachments. pub fn get_attachment(&self, record: RemoteSettingsRecord) -> Result> { diff --git a/components/remote_settings/src/error.rs b/components/remote_settings/src/error.rs index b55b04d678..6c056e9add 100644 --- a/components/remote_settings/src/error.rs +++ b/components/remote_settings/src/error.rs @@ -49,6 +49,8 @@ pub enum Error { DatabaseError(#[from] rusqlite::Error), #[error("No attachment in given record: {0}")] RecordAttachmentMismatchError(String), + #[error("data could not be serialized: {0}")] + SerializationError(#[from] canonical_json::CanonicalJSONError), } // Define how our internal errors are handled and converted to external errors diff --git a/components/remote_settings/src/lib.rs b/components/remote_settings/src/lib.rs index 569438b75f..9629bac5e4 100644 --- a/components/remote_settings/src/lib.rs +++ b/components/remote_settings/src/lib.rs @@ -14,6 +14,7 @@ pub mod client; pub mod config; pub mod error; pub mod service; +pub mod signatures; pub mod storage; #[cfg(feature = "jexl")] diff --git a/components/remote_settings/src/signatures.rs b/components/remote_settings/src/signatures.rs new file mode 100644 index 0000000000..d81e453c1c --- /dev/null +++ b/components/remote_settings/src/signatures.rs @@ -0,0 +1,66 @@ + +use core::clone::Clone; + +use crate::{ + RemoteSettingsRecord, + Result, +}; +use canonical_json; +use serde_json::{json, Value}; + + +fn select_record_fields(value: &Value) -> Value { + if let Value::Object(map) = value { + let new_map = map + .iter() + .filter_map(|(key, v)| { + if key == "deleted" || key == "attachment" && v.is_null() { + None + } else { + Some((key.clone(), v.clone())) + } + }) + .collect(); + Value::Object(new_map) + } else { + value.clone() // Return the value as-is if it's not an object + } +} + +fn serialize_data(timestamp: u64, records: Vec) -> Result> { + let mut sorted_records = records.to_vec(); + sorted_records.sort_by_cached_key(|r| r.id.clone()); + let serialized = canonical_json::to_string(&json!({ + "data": sorted_records.into_iter().map(|r| select_record_fields(&json!(r))).collect::>(), + "last_modified": timestamp.to_string() + }))?; + let data = format!("Content-Signature:\x00{}", serialized); + Ok(data.as_bytes().to_vec()) +} + +#[cfg(test)] +mod tests { + use crate::{RemoteSettingsRecord, Attachment}; + use serde_json::json; + use super::serialize_data; + + #[test] + fn test_records_canonicaljson_serialization() { + let bytes = serialize_data(1337, vec![RemoteSettingsRecord{last_modified: 42, id: "bonjour".into(), deleted: false, attachment: None, fields: json!({"foo": "bar"}).as_object().unwrap().clone()}]).unwrap(); + let s = String::from_utf8(bytes).unwrap(); + assert_eq!(s, "Content-Signature:\u{0}{\"data\":[{\"id\":\"bonjour\",\"last_modified\":42,\"foo\":\"bar\"}],\"last_modified\":\"1337\"}"); + } + + #[test] + fn test_records_canonicaljson_serialization_with_attachment() { + let bytes = serialize_data(1337, vec![RemoteSettingsRecord{last_modified: 42, id: "bonjour".into(), deleted: true, attachment: Some(Attachment { + filename: "pix.jpg".into(), + mimetype: "image/jpeg".into(), + location: "folder/file.jpg".into(), + hash: "aabbcc".into(), + size: 1234567, + }), fields: json!({}).as_object().unwrap().clone()}]).unwrap(); + let s = String::from_utf8(bytes).unwrap(); + assert_eq!(s, "Content-Signature:\0{\"data\":[{\"id\":\"bonjour\",\"last_modified\":42,\"attachment\":{\"filename\":\"pix.jpg\",\"mimetype\":\"image/jpeg\",\"location\":\"folder/file.jpg\",\"hash\":\"aabbcc\",\"size\":1234567}}],\"last_modified\":\"1337\"}"); + } +}