diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-03-06 17:40:25 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-03-07 10:21:26 +0100 |
| commit | a2bb3a2bfee997ca657906ec391a576327d07dfe (patch) | |
| tree | 6c803656b48dfae82efb397cd8c9614aa101b694 /mullvad-update | |
| parent | 2dc82d54771bcb4111b0611072f9d9321fd899dc (diff) | |
| download | mullvadvpn-a2bb3a2bfee997ca657906ec391a576327d07dfe.tar.xz mullvadvpn-a2bb3a2bfee997ca657906ec391a576327d07dfe.zip | |
Support multiple verifying keys in mullvad-update
Diffstat (limited to 'mullvad-update')
| -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 |
6 files changed, 34 insertions, 21 deletions
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(()) } |
