summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock9
-rw-r--r--Cargo.toml2
-rw-r--r--installer-downloader/Cargo.toml1
-rw-r--r--installer-downloader/src/controller.rs3
-rw-r--r--mullvad-update/Cargo.toml3
-rw-r--r--mullvad-update/meta/Cargo.toml1
-rw-r--r--mullvad-update/meta/src/platform.rs5
-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
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(())
}