summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-03-07 10:22:00 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-07 10:22:00 +0100
commitcae71a342274fbe39a39283d6a833e8671b017c8 (patch)
tree1a5bc61db884b201dadae5b554cf13d75fdaeab8
parent2dc82d54771bcb4111b0611072f9d9321fd899dc (diff)
parent54dec579af9a86b2478dfbfe5d9ca34c1d4edbeb (diff)
downloadmullvadvpn-cae71a342274fbe39a39283d6a833e8671b017c8.tar.xz
mullvadvpn-cae71a342274fbe39a39283d6a833e8671b017c8.zip
Merge branch 'multiple-verifying-keys'
-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.rs10
-rw-r--r--mullvad-update/src/client/api.rs11
-rw-r--r--mullvad-update/src/format/deserializer.rs29
-rw-r--r--mullvad-update/src/format/key.rs1
-rw-r--r--mullvad-update/src/format/mod.rs1
-rw-r--r--mullvad-update/src/format/serializer.rs54
12 files changed, 102 insertions, 23 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..8f8bb45cb9 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)
@@ -229,12 +230,11 @@ impl Platform {
println!("Verifying signature of {}...", signed_path.display());
let bytes = fs::read(signed_path).await.context("Failed to read file")?;
- // TODO: Actual key
- let public_key =
- key::VerifyingKey::from_hex(include_str!("../../test-pubkey")).expect("Invalid pubkey");
+ let public_key = key::VerifyingKey::from_hex(include_str!("../../stagemole-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..b3428854ab 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,10 @@ 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 =
+ 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 +141,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/key.rs b/mullvad-update/src/format/key.rs
index 4add53fab4..58994bac05 100644
--- a/mullvad-update/src/format/key.rs
+++ b/mullvad-update/src/format/key.rs
@@ -72,6 +72,7 @@ impl Serialize for SecretKey {
/// ed25519 verifying key
#[derive(Debug, PartialEq, Eq)]
+#[cfg_attr(test, derive(Clone))]
pub struct VerifyingKey(pub ed25519_dalek::VerifyingKey);
impl VerifyingKey {
diff --git a/mullvad-update/src/format/mod.rs b/mullvad-update/src/format/mod.rs
index e48c7c68d5..1e2b88bd5f 100644
--- a/mullvad-update/src/format/mod.rs
+++ b/mullvad-update/src/format/mod.rs
@@ -34,6 +34,7 @@ pub struct SignedResponse {
/// Helper type that leaves the signed data untouched
/// Note that deserializing doesn't verify anything
#[derive(Deserialize, Serialize)]
+#[cfg_attr(test, derive(Debug))]
struct PartialSignedResponse {
/// Signatures of the canonicalized JSON of `signed`
pub signatures: Vec<ResponseSignature>,
diff --git a/mullvad-update/src/format/serializer.rs b/mullvad-update/src/format/serializer.rs
index 46c4c8cb7a..ce6ebf9559 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,58 @@ mod test {
let bytes = serde_json::to_vec(&partial)?;
- deserialize_and_verify(&pubkey, &bytes)?;
+ deserialize_and_verify(&vec1![pubkey.clone()], &bytes)?;
+
+ // Verify that an irrelevant key is ignored
+ let invalid_key = key::SecretKey::generate();
+ let invalid_pubkey = invalid_key.pubkey();
+
+ deserialize_and_verify(&vec1![pubkey.clone(), invalid_pubkey.clone()], &bytes)?;
+
+ // Wrong public key only fails
+ deserialize_and_verify(&vec1![invalid_pubkey], &bytes).unwrap_err();
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_sign_multiple() -> anyhow::Result<()> {
+ // Generate keys and data
+ let key = key::SecretKey::generate();
+ let pubkey = key.pubkey();
+
+ let key2 = key::SecretKey::generate();
+ let pubkey2 = key2.pubkey();
+
+ let invalid_key = key::SecretKey::generate();
+ let invalid_pubkey = invalid_key.pubkey();
+
+ let data = json!({
+ "stuff": "I can prove that I wrote this"
+ });
+
+ // Sign with two keys
+ let mut partial = sign(&key, &data).context("Signing failed")?;
+ let partial2 = sign(&key2, &data).context("Signing failed")?;
+ partial.signatures.extend(partial2.signatures);
+
+ let bytes = serde_json::to_vec(&partial)?;
+
+ // Accept either (or both) keys
+ deserialize_and_verify(&vec1![pubkey.clone(), pubkey2.clone()], &bytes)?;
+ deserialize_and_verify(&vec1![pubkey2.clone()], &bytes)?;
+ deserialize_and_verify(&vec1![pubkey.clone()], &bytes)?;
+
+ // Ignore irrelevant key
+ deserialize_and_verify(
+ &vec1![pubkey.clone(), pubkey2.clone(), invalid_pubkey.clone()],
+ &bytes,
+ )?;
+ deserialize_and_verify(&vec1![pubkey2, invalid_pubkey.clone()], &bytes)?;
+ deserialize_and_verify(&vec1![invalid_pubkey.clone(), pubkey], &bytes)?;
+
+ // Using wrong public key fails
+ deserialize_and_verify(&vec1![invalid_pubkey], &bytes).unwrap_err();
Ok(())
}