summaryrefslogtreecommitdiffhomepage
path: root/mullvad-update/src
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-03-06 17:40:25 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-07 10:21:26 +0100
commita2bb3a2bfee997ca657906ec391a576327d07dfe (patch)
tree6c803656b48dfae82efb397cd8c9614aa101b694 /mullvad-update/src
parent2dc82d54771bcb4111b0611072f9d9321fd899dc (diff)
downloadmullvadvpn-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.rs14
-rw-r--r--mullvad-update/src/format/deserializer.rs29
-rw-r--r--mullvad-update/src/format/serializer.rs3
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(())
}