diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-04-03 14:15:18 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-04-03 14:15:18 +0200 |
| commit | e6576c8e313531b79ffdcb220c5c2fcb25d24cd8 (patch) | |
| tree | d255ef990798b993c4090195427e3b5380ca8795 | |
| parent | 261888bc17023095fea48aa27543748449864436 (diff) | |
| parent | 7ac4612e478921f30207b45abde8ffc2aeeed91c (diff) | |
| download | mullvadvpn-e6576c8e313531b79ffdcb220c5c2fcb25d24cd8.tar.xz mullvadvpn-e6576c8e313531b79ffdcb220c5c2fcb25d24cd8.zip | |
Merge branch 'cleanup-downloader-pubkeys'
| -rw-r--r-- | installer-downloader/src/controller.rs | 29 | ||||
| -rw-r--r-- | mullvad-api/src/version.rs | 1 | ||||
| -rw-r--r-- | mullvad-update/meta/src/platform.rs | 63 | ||||
| -rw-r--r-- | mullvad-update/src/client/api.rs | 123 | ||||
| -rw-r--r-- | mullvad-update/src/client/snapshots/mullvad_update__client__api__test__http_version_provider.snap | 141 | ||||
| -rw-r--r-- | mullvad-update/src/defaults.rs (renamed from mullvad-update/src/keys.rs) | 13 | ||||
| -rw-r--r-- | mullvad-update/src/format/deserializer.rs | 19 | ||||
| -rw-r--r-- | mullvad-update/src/lib.rs | 2 |
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; |
