summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-04-03 14:15:18 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-04-03 14:15:18 +0200
commite6576c8e313531b79ffdcb220c5c2fcb25d24cd8 (patch)
treed255ef990798b993c4090195427e3b5380ca8795
parent261888bc17023095fea48aa27543748449864436 (diff)
parent7ac4612e478921f30207b45abde8ffc2aeeed91c (diff)
downloadmullvadvpn-e6576c8e313531b79ffdcb220c5c2fcb25d24cd8.tar.xz
mullvadvpn-e6576c8e313531b79ffdcb220c5c2fcb25d24cd8.zip
Merge branch 'cleanup-downloader-pubkeys'
-rw-r--r--installer-downloader/src/controller.rs29
-rw-r--r--mullvad-api/src/version.rs1
-rw-r--r--mullvad-update/meta/src/platform.rs63
-rw-r--r--mullvad-update/src/client/api.rs123
-rw-r--r--mullvad-update/src/client/snapshots/mullvad_update__client__api__test__http_version_provider.snap141
-rw-r--r--mullvad-update/src/defaults.rs (renamed from mullvad-update/src/keys.rs)13
-rw-r--r--mullvad-update/src/format/deserializer.rs19
-rw-r--r--mullvad-update/src/lib.rs2
8 files changed, 220 insertions, 171 deletions
diff --git a/installer-downloader/src/controller.rs b/installer-downloader/src/controller.rs
index 2e2b44e351..4ad8c44d47 100644
--- a/installer-downloader/src/controller.rs
+++ b/installer-downloader/src/controller.rs
@@ -9,7 +9,7 @@ use crate::{
};
use mullvad_update::{
- api::{HttpVersionInfoProvider, VersionInfoProvider},
+ api::{HttpVersionInfoProvider, MetaRepositoryPlatform, VersionInfoProvider},
app::{self, AppDownloader, HttpAppDownloader},
version::{Version, VersionInfo, VersionParameters},
};
@@ -20,13 +20,6 @@ use tokio::{
task::JoinHandle,
};
-/// Pinned root certificate used when fetching version metadata
-const PINNED_CERTIFICATE: &[u8] = include_bytes!("../../mullvad-api/le_root_cert.pem");
-
-/// Base URL for pulling metadata. Actual JSON files should be stored at `<base
-/// url>/<platform>.json`
-const META_REPOSITORY_URL: &str = "https://api.mullvad.net/app/releases/";
-
/// Actions handled by an async worker task in [ActionMessageHandler].
enum TaskMessage {
BeginDownload,
@@ -49,12 +42,8 @@ pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T, environ
// Directory provider to use
type DirProvider = crate::temp::TempDirProvider;
- let cert = reqwest::Certificate::from_pem(PINNED_CERTIFICATE).expect("invalid cert");
- let version_provider = HttpVersionInfoProvider {
- url: get_metadata_url(),
- pinned_certificate: Some(cert),
- verifying_keys: mullvad_update::keys::TRUSTED_METADATA_SIGNING_PUBKEYS.clone(),
- };
+ let platform = MetaRepositoryPlatform::current().expect("current platform must be supported");
+ let version_provider = HttpVersionInfoProvider::from(platform);
AppController::initialize::<_, Downloader<T>, _, DirProvider>(
delegate,
@@ -63,18 +52,6 @@ pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T, environ
)
}
-/// JSON files should be stored at `<base url>/<platform>.json`.
-fn get_metadata_url() -> String {
- const PLATFORM: &str = if cfg!(target_os = "windows") {
- "windows"
- } else if cfg!(target_os = "macos") {
- "macos"
- } else {
- panic!("Unsupported platform")
- };
- format!("{META_REPOSITORY_URL}/{PLATFORM}.json")
-}
-
impl AppController {
/// Initialize [AppController] using the provided delegate.
///
diff --git a/mullvad-api/src/version.rs b/mullvad-api/src/version.rs
index 8218b94701..ef68525802 100644
--- a/mullvad-api/src/version.rs
+++ b/mullvad-api/src/version.rs
@@ -67,7 +67,6 @@ impl AppVersionProxy {
let bytes = response.body_with_max_size(Self::SIZE_LIMIT).await?;
let response = mullvad_update::format::SignedResponse::deserialize_and_verify(
- &mullvad_update::keys::TRUSTED_METADATA_SIGNING_PUBKEYS,
&bytes,
lowest_metadata_version,
)
diff --git a/mullvad-update/meta/src/platform.rs b/mullvad-update/meta/src/platform.rs
index 2368c643d1..06ad8a7185 100644
--- a/mullvad-update/meta/src/platform.rs
+++ b/mullvad-update/meta/src/platform.rs
@@ -2,7 +2,7 @@
use anyhow::{anyhow, bail, Context};
use mullvad_update::{
- api::HttpVersionInfoProvider,
+ api::{HttpVersionInfoProvider, MetaRepositoryPlatform},
format::{self, key},
};
use std::{
@@ -10,7 +10,6 @@ use std::{
fmt,
path::{Path, PathBuf},
str::FromStr,
- sync::LazyLock,
};
use tokio::{fs, io};
@@ -19,16 +18,6 @@ use crate::{
io_util::{create_dir_and_write, wait_for_confirm},
};
-/// Base URL for metadata found with `meta pull`.
-/// Actual JSON files should be stored at `<base url>/<platform>.json`.
-const META_REPOSITORY_URL: &str = "https://releases.mullvad.net/desktop/metadata/";
-
-/// TLS certificate to pin to for `meta pull`.
-static PINNED_CERTIFICATE: LazyLock<reqwest::Certificate> = LazyLock::new(|| {
- const CERT_BYTES: &[u8] = include_bytes!("../../../mullvad-api/le_root_cert.pem");
- reqwest::Certificate::from_pem(CERT_BYTES).expect("invalid cert")
-});
-
#[derive(Clone, Copy)]
pub enum Platform {
Windows,
@@ -81,11 +70,6 @@ impl Platform {
Path::new("signed").join(self.local_filename())
}
- /// URL that stores the latest published metadata
- pub fn published_url(&self) -> String {
- format!("{META_REPOSITORY_URL}/{}", self.published_filename())
- }
-
/// Expected artifacts in `artifacts/` directory
pub fn artifact_filenames(&self, version: &mullvad_version::Version) -> Artifacts {
let artifacts_dir = Path::new("artifacts");
@@ -105,14 +89,6 @@ impl Platform {
}
}
- fn published_filename(&self) -> &str {
- match self {
- Platform::Windows => "windows.json",
- Platform::Linux => "linux.json",
- Platform::Macos => "macos.json",
- }
- }
-
fn local_filename(&self) -> &str {
match self {
Platform::Windows => "windows.json",
@@ -123,19 +99,16 @@ impl Platform {
/// Pull latest metadata from repository and store it in `signed/`
pub async fn pull(&self, assume_yes: bool) -> anyhow::Result<()> {
- let url = self.published_url();
+ let platform = MetaRepositoryPlatform::from(*self);
- println!("Pulling {self} metadata from {url}...");
+ println!("Pulling {self} metadata from {}...", platform.url());
- let version_provider = HttpVersionInfoProvider {
- pinned_certificate: Some(PINNED_CERTIFICATE.clone()),
- url,
- verifying_keys: mullvad_update::keys::TRUSTED_METADATA_SIGNING_PUBKEYS.clone(),
- };
- let response = version_provider
- .get_versions(crate::MIN_VERIFY_METADATA_VERSION)
- .await
- .context("Failed to retrieve versions")?;
+ let response = HttpVersionInfoProvider::get_versions_for_platform(
+ platform,
+ crate::MIN_VERIFY_METADATA_VERSION,
+ )
+ .await
+ .context("Failed to retrieve versions")?;
let json = serde_json::to_string_pretty(&response)
.context("Failed to serialize updated metadata")?;
@@ -231,12 +204,8 @@ impl Platform {
println!("Verifying signature of {}...", signed_path.display());
let bytes = fs::read(signed_path).await.context("Failed to read file")?;
- format::SignedResponse::deserialize_and_verify(
- &mullvad_update::keys::TRUSTED_METADATA_SIGNING_PUBKEYS,
- &bytes,
- crate::MIN_VERIFY_METADATA_VERSION,
- )
- .context("Failed to verify metadata for {platform}: {error}")?;
+ format::SignedResponse::deserialize_and_verify(&bytes, crate::MIN_VERIFY_METADATA_VERSION)
+ .context("Failed to verify metadata for {platform}: {error}")?;
Ok(())
}
@@ -428,6 +397,16 @@ impl Platform {
}
}
+impl From<Platform> for MetaRepositoryPlatform {
+ fn from(platform: Platform) -> Self {
+ match platform {
+ Platform::Windows => MetaRepositoryPlatform::Windows,
+ Platform::Linux => MetaRepositoryPlatform::Linux,
+ Platform::Macos => MetaRepositoryPlatform::Macos,
+ }
+ }
+}
+
/// Print release info:
/// Version: 2025.3 (arm, x86) (50%)
/// <Changelog>
diff --git a/mullvad-update/src/client/api.rs b/mullvad-update/src/client/api.rs
index b3428854ab..c4953177b3 100644
--- a/mullvad-update/src/client/api.rs
+++ b/mullvad-update/src/client/api.rs
@@ -1,11 +1,52 @@
//! This module implements fetching of information about app versions
use anyhow::Context;
+#[cfg(test)]
use vec1::Vec1;
use crate::format;
use crate::version::{VersionInfo, VersionParameters};
+/// Available platforms in the default metadata repository
+#[derive(Debug, Clone, Copy)]
+pub enum MetaRepositoryPlatform {
+ Windows,
+ Linux,
+ Macos,
+}
+
+impl MetaRepositoryPlatform {
+ /// Return the current platform
+ pub fn current() -> Option<Self> {
+ if cfg!(target_os = "windows") {
+ Some(Self::Windows)
+ } else if cfg!(target_os = "linux") {
+ Some(Self::Linux)
+ } else if cfg!(target_os = "macos") {
+ Some(Self::Macos)
+ } else {
+ None
+ }
+ }
+
+ /// Return complete URL used for the metadata
+ pub fn url(&self) -> String {
+ format!(
+ "{}/{}",
+ crate::defaults::META_REPOSITORY_URL,
+ self.filename()
+ )
+ }
+
+ fn filename(&self) -> &str {
+ match self {
+ MetaRepositoryPlatform::Windows => "windows.json",
+ MetaRepositoryPlatform::Linux => "linux.json",
+ MetaRepositoryPlatform::Macos => "macos.json",
+ }
+ }
+}
+
/// See [module-level](self) docs.
#[async_trait::async_trait]
pub trait VersionInfoProvider {
@@ -16,11 +57,9 @@ pub trait VersionInfoProvider {
/// Obtain version data using a GET request
pub struct HttpVersionInfoProvider {
/// Endpoint for GET request
- pub url: String,
+ url: String,
/// Accepted root certificate. Defaults are used unless specified
- pub pinned_certificate: Option<reqwest::Certificate>,
- /// Key to use for verifying the response
- pub verifying_keys: Vec1<format::key::VerifyingKey>,
+ pinned_certificate: Option<reqwest::Certificate>,
}
#[async_trait::async_trait]
@@ -31,22 +70,72 @@ impl VersionInfoProvider for HttpVersionInfoProvider {
}
}
+impl From<MetaRepositoryPlatform> for HttpVersionInfoProvider {
+ /// Construct an [HttpVersionInfoProvider] for the given platform using reasonable defaults.
+ ///
+ /// By default, `pinned_certificate` will be set to the LE root certificate.
+ fn from(platform: MetaRepositoryPlatform) -> Self {
+ HttpVersionInfoProvider {
+ url: platform.url(),
+ pinned_certificate: Some(crate::defaults::PINNED_CERTIFICATE.clone()),
+ }
+ }
+}
+
impl HttpVersionInfoProvider {
/// Maximum size of the GET response, in bytes
const SIZE_LIMIT: usize = 1024 * 1024;
- /// Download and verify signed data
- pub async fn get_versions(
+ /// Retrieve version metadata for the given platform using reasonable defaults.
+ ///
+ /// By default, `pinned_certificate` will be set to the LE root certificate, and
+ /// `verifying_keys` will be set to the keys in `trusted-metadata-signing-keys`.
+ pub async fn get_versions_for_platform(
+ platform: MetaRepositoryPlatform,
+ lowest_metadata_version: usize,
+ ) -> anyhow::Result<format::SignedResponse> {
+ HttpVersionInfoProvider::from(platform)
+ .get_versions(lowest_metadata_version)
+ .await
+ }
+
+ /// Download and verify signed data with sane defaults
+ ///
+ /// By default, `pinned_certificate` will be set to the LE root certificate, and
+ /// and the keys in `trusted-metadata-signing-keys` will be used for verification.
+ async fn get_versions(
&self,
lowest_metadata_version: usize,
) -> anyhow::Result<format::SignedResponse> {
+ self.get_versions_inner(|raw_json| {
+ format::SignedResponse::deserialize_and_verify(raw_json, lowest_metadata_version)
+ })
+ .await
+ }
+
+ /// Download and verify signed data with the given keys
+ #[cfg(test)]
+ async fn get_versions_with_keys(
+ &self,
+ lowest_metadata_version: usize,
+ verifying_keys: &Vec1<format::key::VerifyingKey>,
+ ) -> anyhow::Result<format::SignedResponse> {
+ self.get_versions_inner(|raw_json| {
+ format::SignedResponse::deserialize_and_verify_with_keys(
+ verifying_keys,
+ raw_json,
+ lowest_metadata_version,
+ )
+ })
+ .await
+ }
+
+ async fn get_versions_inner(
+ &self,
+ deserialize_fn: impl FnOnce(&[u8]) -> anyhow::Result<format::SignedResponse>,
+ ) -> 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_keys,
- &raw_json,
- lowest_metadata_version,
- )?;
- Ok(response)
+ deserialize_fn(&raw_json)
}
/// Perform a simple GET request, with a size limit, and return it as bytes
@@ -104,8 +193,6 @@ mod test {
use insta::assert_yaml_snapshot;
use vec1::vec1;
- use crate::version::VersionArchitecture;
-
use super::*;
// These tests rely on `insta` for snapshot testing. If they fail due to snapshot assertions,
@@ -133,19 +220,13 @@ mod test {
let url = format!("{}/version", server.url());
// Construct query and provider
- let params = VersionParameters {
- architecture: VersionArchitecture::X86,
- rollout: 1.,
- lowest_metadata_version: 0,
- };
let info_provider = HttpVersionInfoProvider {
url,
pinned_certificate: None,
- verifying_keys,
};
let info = info_provider
- .get_version_info(params)
+ .get_versions_with_keys(0, &verifying_keys)
.await
.context("Expected valid version info")?;
diff --git a/mullvad-update/src/client/snapshots/mullvad_update__client__api__test__http_version_provider.snap b/mullvad-update/src/client/snapshots/mullvad_update__client__api__test__http_version_provider.snap
index 1cb23ff5e5..a7266769fc 100644
--- a/mullvad-update/src/client/snapshots/mullvad_update__client__api__test__http_version_provider.snap
+++ b/mullvad-update/src/client/snapshots/mullvad_update__client__api__test__http_version_provider.snap
@@ -1,83 +1,66 @@
---
-source: mullvad-update/src/api.rs
+source: mullvad-update/src/client/api.rs
expression: info
snapshot_kind: text
---
-stable:
- version: "2025.2"
- urls:
- - "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2.exe"
- size: 101384672
- changelog: "[macos] Adding support for quicfuscator\n[windows] Less bugs"
- sha256:
- - 244
- - 178
- - 87
- - 19
- - 209
- - 63
- - 40
- - 25
- - 163
- - 0
- - 242
- - 255
- - 169
- - 77
- - 150
- - 116
- - 99
- - 170
- - 238
- - 160
- - 211
- - 87
- - 251
- - 215
- - 71
- - 154
- - 40
- - 17
- - 84
- - 186
- - 4
- - 96
-beta:
- version: 2025.3-beta1
- urls:
- - "https://releases.mullvad.net/desktop/releases/2025.3-beta1/MullvadVPN-2025.3-beta1_x64.exe"
- size: 106297504
- changelog: "[macos] Adding support for quicfuscator\n[windows] Less bugs"
- sha256:
- - 12
- - 86
- - 154
- - 160
- - 145
- - 46
- - 185
- - 54
- - 5
- - 168
- - 80
- - 115
- - 68
- - 125
- - 66
- - 186
- - 12
- - 166
- - 18
- - 54
- - 27
- - 239
- - 120
- - 239
- - 4
- - 239
- - 3
- - 142
- - 128
- - 177
- - 84
- - 3
+signatures:
+ - keytype: ed25519
+ keyid: bb4ef63ffdcc6bd5a19c30cd23b9de03099407a04463418f17ae338b98aa09d4
+ sig: 253ec37846dcd909bfc5119c0e0d06535767e179eb8b4465015eaa95f4bed362c8c9186311192c987871722bf7d319d44e4f04eaf79c269820bc13ff1a901f0b
+signed:
+ metadata_version: 0
+ metadata_expiry: "2025-07-02T15:33:00Z"
+ releases:
+ - version: "2025.2"
+ changelog: "[macos] Adding support for quicfuscator\n[windows] Less bugs"
+ installers:
+ - architecture: x86
+ urls:
+ - "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2.exe"
+ size: 101384672
+ sha256: F4B25713D13F2819A300F2FFA94D967463AAEEA0D357FBD7479A281154BA0460
+ - architecture: arm64
+ urls:
+ - "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2_arm64.exe"
+ size: 104146312
+ sha256: AFD8098A1FF89D69A243EC4E2E946CF5FBF8D1C10998230D6C8FC0A5C9C39541
+ - version: "2025.3"
+ changelog: "[macos] Adding support for quicfuscator\n[windows] Less bugs"
+ installers:
+ - architecture: x86
+ urls:
+ - "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2.exe"
+ size: 101384672
+ sha256: F4B25713D13F2819A300F2FFA94D967463AAEEA0D357FBD7479A281154BA0460
+ - architecture: arm64
+ urls:
+ - "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2_arm64.exe"
+ size: 104146312
+ sha256: AFD8098A1FF89D69A243EC4E2E946CF5FBF8D1C10998230D6C8FC0A5C9C39541
+ rollout: 0.5
+ - version: 2025.1-beta1
+ changelog: "[macos] Adding support for quicfuscator\n[windows] Less bugs"
+ installers:
+ - architecture: x86
+ urls:
+ - "https://releases.mullvad.net/desktop/releases/2025.3-beta1/MullvadVPN-2025.3-beta1_x64.exe"
+ size: 106297504
+ sha256: 0c569aa0912eb93605a85073447d42ba0ca612361bef78ef04ef038e80b15403
+ - architecture: arm64
+ urls:
+ - "https://releases.mullvad.net/desktop/releases/2025.3-beta1/MullvadVPN-2025.3-beta1_arm64.exe"
+ size: 111488248
+ sha256: 82948D3DB5B869EE5F0D246DB557A81B72B68DFDDD2267872B7B8A5B19A05444
+ - version: 2025.3-beta1
+ changelog: "[macos] Adding support for quicfuscator\n[windows] Less bugs"
+ installers:
+ - architecture: x86
+ urls:
+ - "https://releases.mullvad.net/desktop/releases/2025.3-beta1/MullvadVPN-2025.3-beta1_x64.exe"
+ size: 106297504
+ sha256: 0c569aa0912eb93605a85073447d42ba0ca612361bef78ef04ef038e80b15403
+ - architecture: arm64
+ urls:
+ - "https://releases.mullvad.net/desktop/releases/2025.3-beta1/MullvadVPN-2025.3-beta1_arm64.exe"
+ size: 111488248
+ sha256: 82948D3DB5B869EE5F0D246DB557A81B72B68DFDDD2267872B7B8A5B19A05444
diff --git a/mullvad-update/src/keys.rs b/mullvad-update/src/defaults.rs
index 9b0abf5c6b..7d6ba5f172 100644
--- a/mullvad-update/src/keys.rs
+++ b/mullvad-update/src/defaults.rs
@@ -1,9 +1,20 @@
-//! Keys that may be used for verifying data
+//! Default keys and certificates that may be used for verifying data
use crate::format::key::VerifyingKey;
use std::sync::LazyLock;
use vec1::Vec1;
+/// Default repository URL for version metadata
+#[cfg(feature = "client")]
+pub const META_REPOSITORY_URL: &str = "https://api.mullvad.net/app/releases/";
+
+/// Default TLS certificate to pin to
+#[cfg(feature = "client")]
+pub static PINNED_CERTIFICATE: LazyLock<reqwest::Certificate> = LazyLock::new(|| {
+ const CERT_BYTES: &[u8] = include_bytes!("../../mullvad-api/le_root_cert.pem");
+ reqwest::Certificate::from_pem(CERT_BYTES).expect("invalid cert")
+});
+
/// Pubkeys used to verify metadata from the Mullvad API (production)
pub static TRUSTED_METADATA_SIGNING_PUBKEYS: LazyLock<Vec1<VerifyingKey>> =
LazyLock::new(|| parse_keys(include_str!("../trusted-metadata-signing-pubkeys")));
diff --git a/mullvad-update/src/format/deserializer.rs b/mullvad-update/src/format/deserializer.rs
index c47de65fe3..0aa7343f62 100644
--- a/mullvad-update/src/format/deserializer.rs
+++ b/mullvad-update/src/format/deserializer.rs
@@ -10,7 +10,24 @@ use super::{PartialSignedResponse, ResponseSignature, SignedResponse};
impl SignedResponse {
/// Deserialize some bytes to JSON, and verify them, including signature and expiry.
/// If successful, the deserialized data is returned.
+ ///
+ /// This uses the keys in `trusted-metadata-signing-pubkeys`
pub fn deserialize_and_verify(
+ bytes: &[u8],
+ min_metadata_version: usize,
+ ) -> Result<Self, anyhow::Error> {
+ Self::deserialize_and_verify_with_keys(
+ &crate::defaults::TRUSTED_METADATA_SIGNING_PUBKEYS,
+ bytes,
+ min_metadata_version,
+ )
+ }
+
+ /// Deserialize some bytes to JSON, and verify them, including signature and expiry.
+ /// If successful, the deserialized data is returned.
+ ///
+ /// This is typically only used for testing. Prefer [deserialize_and_verify].
+ pub(crate) fn deserialize_and_verify_with_keys(
keys: &Vec1<VerifyingKey>,
bytes: &[u8],
min_metadata_version: usize,
@@ -33,6 +50,8 @@ impl SignedResponse {
/// Deserialize some bytes to JSON, and verify them, including signature and expiry.
/// If successful, the deserialized data is returned.
+ ///
+ /// This is typically only used for testing. Prefer [deserialize_and_verify].
fn deserialize_and_verify_at_time(
keys: &Vec1<VerifyingKey>,
bytes: &[u8],
diff --git a/mullvad-update/src/lib.rs b/mullvad-update/src/lib.rs
index 88e78034a7..bfce66e120 100644
--- a/mullvad-update/src/lib.rs
+++ b/mullvad-update/src/lib.rs
@@ -6,7 +6,7 @@ mod client;
#[cfg(feature = "client")]
pub use client::*;
-pub mod keys;
+mod defaults;
pub mod version;