diff options
| -rw-r--r-- | Cargo.lock | 9 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | installer-downloader/Cargo.toml | 1 | ||||
| -rw-r--r-- | installer-downloader/src/controller.rs | 3 | ||||
| -rw-r--r-- | mullvad-update/Cargo.toml | 3 | ||||
| -rw-r--r-- | mullvad-update/meta/Cargo.toml | 1 | ||||
| -rw-r--r-- | mullvad-update/meta/src/platform.rs | 5 | ||||
| -rw-r--r-- | mullvad-update/src/client/api.rs | 14 | ||||
| -rw-r--r-- | mullvad-update/src/format/deserializer.rs | 29 | ||||
| -rw-r--r-- | mullvad-update/src/format/serializer.rs | 3 |
10 files changed, 48 insertions, 22 deletions
diff --git a/Cargo.lock b/Cargo.lock index 4f88e6e8e7..a0c5623f67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,6 +2036,7 @@ dependencies = [ "serde", "talpid-platform-metadata", "tokio", + "vec1", "windows-sys 0.52.0", "winres", ] @@ -2457,6 +2458,7 @@ dependencies = [ "sha2", "tokio", "toml 0.8.19", + "vec1", ] [[package]] @@ -2906,6 +2908,7 @@ dependencies = [ "sha2", "thiserror 2.0.9", "tokio", + "vec1", ] [[package]] @@ -5612,6 +5615,12 @@ dependencies = [ ] [[package]] +name = "vec1" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab68b56840f69efb0fefbe3ab6661499217ffdc58e2eef7c3f6f69835386322" + +[[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 5dae521483..5c38824b47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,6 +122,8 @@ ipnetwork = "0.20" tun = { version = "0.5.5", features = ["async"] } socket2 = "0.5.7" +vec1 = "1.12" + # Test dependencies proptest = "1.4" insta = { version = "1.42", features = ["yaml"] } diff --git a/installer-downloader/Cargo.toml b/installer-downloader/Cargo.toml index 1062853668..e71fa3e71e 100644 --- a/installer-downloader/Cargo.toml +++ b/installer-downloader/Cargo.toml @@ -26,6 +26,7 @@ rand = { version = "0.8.5" } reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls"] } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["rt-multi-thread", "fs"] } +vec1 = { workspace = true } talpid-platform-metadata = { path = "../talpid-platform-metadata" } mullvad-update = { path = "../mullvad-update", features = ["client"] } diff --git a/installer-downloader/src/controller.rs b/installer-downloader/src/controller.rs index c01222c9b3..fd9c931756 100644 --- a/installer-downloader/src/controller.rs +++ b/installer-downloader/src/controller.rs @@ -19,6 +19,7 @@ use tokio::{ sync::{mpsc, oneshot}, task::JoinHandle, }; +use vec1::vec1; /// ed25519 pubkey used to verify metadata from the Mullvad (stagemole) API const VERSION_PROVIDER_PUBKEY: &str = include_str!("../../mullvad-update/stagemole-pubkey"); @@ -60,7 +61,7 @@ pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T, environ let version_provider = HttpVersionInfoProvider { url: get_metadata_url(), pinned_certificate: Some(cert), - verifying_key, + verifying_keys: vec1![verifying_key], }; AppController::initialize::<_, Downloader<T>, _, DirProvider>( diff --git a/mullvad-update/Cargo.toml b/mullvad-update/Cargo.toml index 128a635194..e295de3c1f 100644 --- a/mullvad-update/Cargo.toml +++ b/mullvad-update/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [features] default = [] sign = ["rand", "clap"] -client = ["async-trait", "reqwest", "sha2", "tokio", "thiserror"] +client = ["async-trait", "reqwest", "sha2", "tokio", "thiserror", "vec1"] [dependencies] anyhow = { workspace = true } @@ -28,6 +28,7 @@ async-trait = { version = "0.1", optional = true } reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls"], optional = true } sha2 = { version = "0.10", optional = true } tokio = { workspace = true, features = ["rt-multi-thread", "fs", "process", "macros"], optional = true } +vec1 = { workspace = true, optional = true } mullvad-version = { path = "../mullvad-version", features = ["serde"] } diff --git a/mullvad-update/meta/Cargo.toml b/mullvad-update/meta/Cargo.toml index 7eba726e95..932b5780b2 100644 --- a/mullvad-update/meta/Cargo.toml +++ b/mullvad-update/meta/Cargo.toml @@ -22,6 +22,7 @@ serde = { workspace = true } sha2 = "0.10" tokio = { version = "1", features = ["full"] } toml = "0.8" +vec1 = { workspace = true } mullvad-version = { path = "../../mullvad-version", features = ["serde"] } mullvad-update = { path = "../", features = ["client", "sign"] } diff --git a/mullvad-update/meta/src/platform.rs b/mullvad-update/meta/src/platform.rs index bc6c5bec2d..1fb3ef6dda 100644 --- a/mullvad-update/meta/src/platform.rs +++ b/mullvad-update/meta/src/platform.rs @@ -12,6 +12,7 @@ use std::{ str::FromStr, }; use tokio::{fs, io}; +use vec1::vec1; use crate::{ artifacts, @@ -128,7 +129,7 @@ impl Platform { // TODO: pin pinned_certificate: None, url, - verifying_key, + verifying_keys: vec1![verifying_key], }; let response = version_provider .get_versions(crate::MIN_VERIFY_METADATA_VERSION) @@ -234,7 +235,7 @@ impl Platform { key::VerifyingKey::from_hex(include_str!("../../test-pubkey")).expect("Invalid pubkey"); format::SignedResponse::deserialize_and_verify( - &public_key, + &vec1![public_key], &bytes, crate::MIN_VERIFY_METADATA_VERSION, ) diff --git a/mullvad-update/src/client/api.rs b/mullvad-update/src/client/api.rs index 62b550603f..d3e4ea1790 100644 --- a/mullvad-update/src/client/api.rs +++ b/mullvad-update/src/client/api.rs @@ -1,6 +1,7 @@ //! This module implements fetching of information about app versions use anyhow::Context; +use vec1::Vec1; use crate::format; use crate::version::{VersionInfo, VersionParameters}; @@ -19,7 +20,7 @@ pub struct HttpVersionInfoProvider { /// Accepted root certificate. Defaults are used unless specified pub pinned_certificate: Option<reqwest::Certificate>, /// Key to use for verifying the response - pub verifying_key: format::key::VerifyingKey, + pub verifying_keys: Vec1<format::key::VerifyingKey>, } #[async_trait::async_trait] @@ -41,7 +42,7 @@ impl HttpVersionInfoProvider { ) -> anyhow::Result<format::SignedResponse> { let raw_json = Self::get(&self.url, self.pinned_certificate.clone()).await?; let response = format::SignedResponse::deserialize_and_verify( - &self.verifying_key, + &self.verifying_keys, &raw_json, lowest_metadata_version, )?; @@ -101,6 +102,7 @@ impl HttpVersionInfoProvider { #[cfg(test)] mod test { use insta::assert_yaml_snapshot; + use vec1::vec1; use crate::version::VersionArchitecture; @@ -115,9 +117,9 @@ mod test { /// We're not testing the correctness of [version] here, only the HTTP client #[tokio::test] async fn test_http_version_provider() -> anyhow::Result<()> { - let verifying_key = - crate::format::key::VerifyingKey::from_hex(include_str!("../../test-pubkey")) - .expect("valid key"); + let valid_key = crate::format::key::VerifyingKey::from_hex(include_str!("../../test-pubkey")) + .expect("valid key"); + let verifying_keys = vec1![valid_key]; // Start HTTP server let mut server = mockito::Server::new_async().await; @@ -138,7 +140,7 @@ mod test { let info_provider = HttpVersionInfoProvider { url, pinned_certificate: None, - verifying_key, + verifying_keys, }; let info = info_provider diff --git a/mullvad-update/src/format/deserializer.rs b/mullvad-update/src/format/deserializer.rs index dec1e64f47..3b370c2ff5 100644 --- a/mullvad-update/src/format/deserializer.rs +++ b/mullvad-update/src/format/deserializer.rs @@ -1,6 +1,7 @@ //! Deserializer and verifier of version metadata use anyhow::Context; +use vec1::Vec1; use super::key::*; use super::Response; @@ -10,11 +11,11 @@ impl SignedResponse { /// Deserialize some bytes to JSON, and verify them, including signature and expiry. /// If successful, the deserialized data is returned. pub fn deserialize_and_verify( - key: &VerifyingKey, + keys: &Vec1<VerifyingKey>, bytes: &[u8], min_metadata_version: usize, ) -> Result<Self, anyhow::Error> { - Self::deserialize_and_verify_at_time(key, bytes, chrono::Utc::now(), min_metadata_version) + Self::deserialize_and_verify_at_time(keys, bytes, chrono::Utc::now(), min_metadata_version) } /// This method is used mostly for testing, and skips all verification. @@ -33,13 +34,13 @@ impl SignedResponse { /// Deserialize some bytes to JSON, and verify them, including signature and expiry. /// If successful, the deserialized data is returned. fn deserialize_and_verify_at_time( - key: &VerifyingKey, + keys: &Vec1<VerifyingKey>, bytes: &[u8], current_time: chrono::DateTime<chrono::Utc>, min_metadata_version: usize, ) -> Result<Self, anyhow::Error> { // Deserialize and verify signature - let partial_data = deserialize_and_verify(key, bytes)?; + let partial_data = deserialize_and_verify(keys, bytes)?; // Deserialize the canonical JSON to structured representation let signed_response: Response = serde_json::from_value(partial_data.signed) @@ -74,16 +75,20 @@ impl SignedResponse { /// /// On success, this returns verified data and signature pub(super) fn deserialize_and_verify( - key: &VerifyingKey, + keys: &Vec1<VerifyingKey>, bytes: &[u8], ) -> anyhow::Result<PartialSignedResponse> { let partial_data: PartialSignedResponse = serde_json::from_slice(bytes).context("Invalid version JSON")?; - // Check if the key matches - let Some(sig) = partial_data.signatures.iter().find_map(|sig| match sig { + let valid_keys: Vec<_> = keys.into_iter().map(|k| k.0).collect(); + + // Check if one of the keys matches + let Some((key, sig)) = partial_data.signatures.iter().find_map(|sig| match sig { // Check if ed25519 key matches - ResponseSignature::Ed25519 { keyid, sig } if keyid.0 == key.0 => Some(sig), + ResponseSignature::Ed25519 { keyid, sig } if valid_keys.contains(&keyid.0) => { + Some((keyid, sig)) + } // Ignore all non-matching key _ => None, }) else { @@ -109,6 +114,8 @@ pub(super) fn deserialize_and_verify( mod test { use std::str::FromStr; + use vec1::vec1; + use super::*; /// Test that a valid signed version response is successfully deserialized and verified @@ -119,7 +126,7 @@ mod test { ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap(); SignedResponse::deserialize_and_verify_at_time( - &VerifyingKey(verifying_key), + &vec1![VerifyingKey(verifying_key)], include_bytes!("../../test-version-response.json"), // It's 1970 again chrono::DateTime::UNIX_EPOCH, @@ -130,7 +137,7 @@ mod test { // Reject expired data SignedResponse::deserialize_and_verify_at_time( - &VerifyingKey(verifying_key), + &vec1![VerifyingKey(verifying_key)], include_bytes!("../../test-version-response.json"), // In the year 3000 chrono::DateTime::from_str("3000-01-01T00:00:00Z").unwrap(), @@ -141,7 +148,7 @@ mod test { // Reject expired version number SignedResponse::deserialize_and_verify_at_time( - &VerifyingKey(verifying_key), + &vec1![VerifyingKey(verifying_key)], include_bytes!("../../test-version-response.json"), chrono::DateTime::UNIX_EPOCH, usize::MAX, diff --git a/mullvad-update/src/format/serializer.rs b/mullvad-update/src/format/serializer.rs index 46c4c8cb7a..ca98b7ba26 100644 --- a/mullvad-update/src/format/serializer.rs +++ b/mullvad-update/src/format/serializer.rs @@ -72,6 +72,7 @@ mod test { use super::*; use crate::format::deserializer::deserialize_and_verify; use serde_json::json; + use vec1::vec1; #[test] fn test_sign() -> anyhow::Result<()> { @@ -95,7 +96,7 @@ mod test { let bytes = serde_json::to_vec(&partial)?; - deserialize_and_verify(&pubkey, &bytes)?; + deserialize_and_verify(&vec1![pubkey], &bytes)?; Ok(()) } |
