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/src | |
| parent | 2dc82d54771bcb4111b0611072f9d9321fd899dc (diff) | |
| download | mullvadvpn-a2bb3a2bfee997ca657906ec391a576327d07dfe.tar.xz mullvadvpn-a2bb3a2bfee997ca657906ec391a576327d07dfe.zip | |
Support multiple verifying keys in mullvad-update
Diffstat (limited to 'mullvad-update/src')
| -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 |
3 files changed, 28 insertions, 18 deletions
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(()) } |
