remote_settings/
signatures.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use core::clone::Clone;
6
7use crate::{RemoteSettingsRecord, Result};
8use rc_crypto::contentsignature;
9use serde_json::{json, Value};
10
11/// Remove `deleted` and `attachment` fields if they are null.
12fn select_record_fields(value: &Value) -> Value {
13    match value {
14        Value::Object(map) => Value::Object(
15            map.iter()
16                .filter(|(key, v)| !(*key == "deleted" || (*key == "attachment" && v.is_null())))
17                .map(|(key, v)| (key.clone(), v.clone()))
18                .collect(),
19        ),
20        _ => value.clone(), // Return the value as-is if it's not an object
21    }
22}
23
24/// Serialize collection data into canonical JSON. This must match the server implementation.
25fn serialize_data(timestamp: u64, records: &[RemoteSettingsRecord]) -> Result<Vec<u8>> {
26    let mut sorted_records = records.to_vec();
27    sorted_records.sort_by_cached_key(|r| r.id.clone());
28    let serialized = canonical_json::to_string(&json!({
29        "data": sorted_records.into_iter().map(|r| select_record_fields(&json!(r))).collect::<Vec<Value>>(),
30        "last_modified": timestamp.to_string()
31    }))?;
32    let data = format!("Content-Signature:\x00{}", serialized);
33    Ok(data.as_bytes().to_vec())
34}
35
36/// Verify that the timestamp and records match the signature in the metadata.
37pub fn verify_signature(
38    timestamp: u64,
39    records: &[RemoteSettingsRecord],
40    signature: &[u8],
41    cert_chain_bytes: &[u8],
42    epoch_seconds: u64,
43    expected_root_hash: &str,
44    expected_leaf_cname: &str,
45) -> Result<()> {
46    let message = serialize_data(timestamp, records)?;
47    // Check that certificate chain is valid at specific date time, and
48    // that signature matches the input message.
49    contentsignature::verify(
50        &message,
51        signature,
52        cert_chain_bytes,
53        epoch_seconds,
54        expected_root_hash,
55        expected_leaf_cname,
56    )?;
57    Ok(())
58}
59
60#[cfg(test)]
61mod tests {
62    use super::serialize_data;
63    use crate::{Attachment, RemoteSettingsRecord};
64    use serde_json::json;
65
66    #[test]
67    fn test_records_canonicaljson_serialization() {
68        let bytes = serialize_data(
69            1337,
70            &vec![RemoteSettingsRecord {
71                last_modified: 42,
72                id: "bonjour".into(),
73                deleted: false,
74                attachment: None,
75                fields: json!({"foo": "bar"}).as_object().unwrap().clone(),
76            }],
77        )
78        .unwrap();
79        let s = String::from_utf8(bytes).unwrap();
80        assert_eq!(s, "Content-Signature:\u{0}{\"data\":[{\"id\":\"bonjour\",\"last_modified\":42,\"foo\":\"bar\"}],\"last_modified\":\"1337\"}");
81    }
82
83    #[test]
84    fn test_records_canonicaljson_serialization_with_attachment() {
85        let bytes = serialize_data(
86            1337,
87            &vec![RemoteSettingsRecord {
88                last_modified: 42,
89                id: "bonjour".into(),
90                deleted: true,
91                attachment: Some(Attachment {
92                    filename: "pix.jpg".into(),
93                    mimetype: "image/jpeg".into(),
94                    location: "folder/file.jpg".into(),
95                    hash: "aabbcc".into(),
96                    size: 1234567,
97                }),
98                fields: json!({}).as_object().unwrap().clone(),
99            }],
100        )
101        .unwrap();
102        let s = String::from_utf8(bytes).unwrap();
103        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\"}");
104    }
105}