diff --git a/Cargo.lock b/Cargo.lock index e7d400cf8e..822504a31b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -527,15 +527,15 @@ dependencies = [ [[package]] name = "canonical_json" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89083fd014d71c47a718d7f4ac050864dac8587668dbe90baf9e261064c5710" +checksum = "15c64d9e1338e26dbd60233afe97d1f03de2227bdf33b0cfef12d59736054ec8" dependencies = [ "hex", "regex", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.20", ] [[package]] diff --git a/components/remote_settings/Cargo.toml b/components/remote_settings/Cargo.toml index 12cb09646b..f144c33ea5 100644 --- a/components/remote_settings/Cargo.toml +++ b/components/remote_settings/Cargo.toml @@ -33,7 +33,7 @@ regex = "1.9" anyhow = "1.0" firefox-versioning = { path = "../support/firefox-versioning" } sha2 = "^0.10" -canonical_json = { version = "0.5", optional = true } +canonical_json = { version = "0.7", optional = true } rc_crypto = { path = "../support/rc_crypto", optional = true } [build-dependencies] diff --git a/components/remote_settings/src/client.rs b/components/remote_settings/src/client.rs index 0eaf1d42f4..c7846cde0a 100644 --- a/components/remote_settings/src/client.rs +++ b/components/remote_settings/src/client.rs @@ -480,7 +480,9 @@ impl RemoteSettingsClient { } ); + let collection = format!("{}/{}", metadata.bucket, self.collection_name); result = signatures::verify_signature( + &collection, timestamp, records, signature.signature.as_bytes(), @@ -1571,6 +1573,34 @@ IKdcFKAt3fFrpyMhlfIKkLfmm0iDjmfmIXbDGBJw9SE= Ok(()) } + #[test] + fn test_legacy_signature_first_and_signature_with_collection_second() -> Result<()> { + // This is what the server sends: the legacy signature first (for older clients), + // and the signature of the payload that includes the collection second. + ensure_initialized(); + run_client_sync( + &[], + &[], + VALID_CERTIFICATE, + &[ + CollectionSignature { + signature: LEGACY_SIGNATURE.to_string(), + x5u: "http://mocked".into(), + mode: "p384ecdsa".into(), + }, + CollectionSignature { + signature: VALID_SIGNATURE.to_string(), + x5u: "http://mocked".into(), + mode: "p384ecdsa".into(), + }, + ], + VALID_CERT_EPOCH_SECONDS, + "main", + ) + .expect("Valid signature"); + Ok(()) + } + #[test] fn test_valid_signature_after_retry() -> Result<()> { ensure_initialized(); diff --git a/components/remote_settings/src/signatures.rs b/components/remote_settings/src/signatures.rs index 5fc030c1f5..f8054ac6fa 100644 --- a/components/remote_settings/src/signatures.rs +++ b/components/remote_settings/src/signatures.rs @@ -22,19 +22,28 @@ fn select_record_fields(value: &Value) -> Value { } /// Serialize collection data into canonical JSON. This must match the server implementation. -fn serialize_data(timestamp: u64, records: &[RemoteSettingsRecord]) -> Result> { +fn serialize_data( + collection: &str, + timestamp: u64, + records: &[RemoteSettingsRecord], +) -> 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!({ + let payload = json!({ + "collection": collection, "data": sorted_records.into_iter().map(|r| select_record_fields(&json!(r))).collect::>(), "last_modified": timestamp.to_string() - }))?; + }); + let serialized = canonical_json::to_string(&payload)?; let data = format!("Content-Signature:\x00{}", serialized); Ok(data.as_bytes().to_vec()) } -/// Verify that the timestamp and records match the signature in the metadata. +/// Verify that the collection (as `"{bid}/{cid}"`), timestamp and records match the signature +/// in the metadata. +#[allow(clippy::too_many_arguments)] pub fn verify_signature( + collection: &str, timestamp: u64, records: &[RemoteSettingsRecord], signature: &[u8], @@ -43,7 +52,7 @@ pub fn verify_signature( expected_root_hash: &str, expected_leaf_cname: &str, ) -> Result<()> { - let message = serialize_data(timestamp, records)?; + let message = serialize_data(collection, timestamp, records)?; // Check that certificate chain is valid at specific date time, and // that signature matches the input message. contentsignature::verify( @@ -66,6 +75,7 @@ mod tests { #[test] fn test_records_canonicaljson_serialization() { let bytes = serialize_data( + "main/cfr", 1337, &[RemoteSettingsRecord { last_modified: 42, @@ -77,12 +87,13 @@ mod tests { ) .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\"}"); + assert_eq!(s, "Content-Signature:\u{0}{\"collection\":\"main/cfr\",\"data\":[{\"foo\":\"bar\",\"id\":\"bonjour\",\"last_modified\":42}],\"last_modified\":\"1337\"}"); } #[test] fn test_records_canonicaljson_serialization_with_attachment() { let bytes = serialize_data( + "main/cfr", 1337, &[RemoteSettingsRecord { last_modified: 42, @@ -100,6 +111,6 @@ mod tests { ) .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\"}"); + assert_eq!(s, "Content-Signature:\0{\"collection\":\"main/cfr\",\"data\":[{\"attachment\":{\"filename\":\"pix.jpg\",\"hash\":\"aabbcc\",\"location\":\"folder/file.jpg\",\"mimetype\":\"image/jpeg\",\"size\":1234567},\"id\":\"bonjour\",\"last_modified\":42}],\"last_modified\":\"1337\"}"); } }