diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-02-12 21:01:17 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-03-05 23:31:59 +0100 |
| commit | b2f54826a9b2951ecf56c7794f341124b0220762 (patch) | |
| tree | e92661cfea90ae1982017ca4899a4547d5f552e5 | |
| parent | 504bb2a2c6ffbad4523a38990fc834541b0826ad (diff) | |
| download | mullvadvpn-b2f54826a9b2951ecf56c7794f341124b0220762.tar.xz mullvadvpn-b2f54826a9b2951ecf56c7794f341124b0220762.zip | |
Update version response format
| -rw-r--r-- | mullvad-update/src/api.rs | 145 | ||||
| -rw-r--r-- | mullvad-update/src/format/deserializer.rs | 2 | ||||
| -rw-r--r-- | mullvad-update/src/format/mod.rs | 57 | ||||
| -rw-r--r-- | mullvad-update/test-version-response.json | 57 |
4 files changed, 149 insertions, 112 deletions
diff --git a/mullvad-update/src/api.rs b/mullvad-update/src/api.rs index c2f5fefecc..253c4fc922 100644 --- a/mullvad-update/src/api.rs +++ b/mullvad-update/src/api.rs @@ -13,14 +13,8 @@ pub struct VersionParameters { pub rollout: f32, } -/// Architecture to retrieve data for -#[derive(Debug, Clone, Copy)] -pub enum VersionArchitecture { - /// x86-64 architecture - X86, - /// ARM64 architecture - Arm64, -} +/// Installer architecture +pub type VersionArchitecture = format::Architecture; /// See [module-level](self) docs. #[async_trait::async_trait] @@ -34,7 +28,8 @@ pub trait VersionInfoProvider { pub struct VersionInfo { /// Stable version info pub stable: Version, - /// Beta version info + /// Beta version info (if available and newer than `stable`). + /// If latest stable version is newer, this will be `None`. pub beta: Option<Version>, } @@ -63,7 +58,7 @@ impl VersionInfoProvider for ApiVersionInfoProvider { use format::*; const TEST_PUBKEY: &str = - "f4c262705b4ae8088bc8173889f779f77563edfd7de3b0ac4aa0e554a6896404"; + "4d35f5376f1f58c41b2a0ee4600ae7811eace354f100227e853994deef38942d"; let pubkey = hex::decode(TEST_PUBKEY).unwrap(); let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap(); @@ -73,68 +68,110 @@ impl VersionInfoProvider for ApiVersionInfoProvider { include_bytes!("../test-version-response.json"), )?; - VersionInfo::try_from_signed_response(¶ms, response) + VersionInfo::try_from_response(¶ms, response.signed) } } +/// Helper used to lift the relevant installer out of the array in [format::Release] +#[derive(Clone)] +struct IntermediateVersion { + version: mullvad_version::Version, + changelog: String, + installer: format::Installer, +} + impl VersionInfo { /// Convert signed response data to public version type /// NOTE: `response` is assumed to be verified and untampered. It is not verified. - fn try_from_signed_response( + fn try_from_response( params: &VersionParameters, - response: format::SignedResponse, + response: format::Response, ) -> anyhow::Result<Self> { - let stable = Version::try_from_signed_response(params, response.signed.stable)?; - let beta = response - .signed - .beta - .map(|response| Version::try_from_signed_response(params, response)) - .transpose() - .context("Failed to parse beta version")?; + let mut releases: Vec<_> = response + .releases + .into_iter() + // Filter out releases that are not rolled out to us + .filter(|release| release.rollout >= params.rollout) + // Include only installers for the requested architecture + .flat_map(|release| { + release + .installers + .into_iter() + .filter(|installer| params.architecture == installer.architecture) + // Map each artifact to a [IntermediateVersion] + .map(move |installer| { + IntermediateVersion { + version: release.version.clone(), + changelog: release.changelog.clone(), + installer, + } + }) + }) + .collect(); - Ok(Self { stable, beta }) - } -} + // Sort releases by version + releases.sort_by(|a, b| mullvad_version::Version::version_ordering(&a.version, &b.version)); -impl Version { - /// Convert response data to public version type - fn try_from_signed_response( - params: &VersionParameters, - response: format::VersionResponse, - ) -> anyhow::Result<Self> { - // Check if the rollout version is acceptable according to threshold - if let Some(next) = response.next { - if next.rollout >= params.rollout { - // Use the version being rolled out - return Self::try_for_arch(params, next.version); - } + // Fail if there are duplicate versions + // Important! This must occur after sorting + if let Some(dup_version) = Self::find_duplicate_version(&releases) { + anyhow::bail!("API response contains at least one duplicated version: {dup_version}"); } - // Return the version not being rolled out - Self::try_for_arch(params, response.current) + // Find latest stable version + let stable = releases + .iter() + .rfind(|release| release.version.is_stable() && !release.version.is_dev()); + let Some(stable) = stable.cloned() else { + anyhow::bail!("No stable version found"); + }; + + // Find the latest beta version + let beta = releases + .iter() + // Find most recent beta version + .rfind(|release| release.version.beta().is_some() && !release.version.is_dev()) + // If the latest beta version is older than latest stable, dispose of it + .filter(|release| release.version.version_ordering(&stable.version).is_gt()) + .cloned(); + + Ok(Self { + stable: Version::try_from(stable)?, + beta: beta.map(|beta| Version::try_from(beta)).transpose()?, + }) } - /// Convert version response to the public version type for a given architecture - /// This may fail if the current architecture isn't included in the response - fn try_for_arch( - params: &VersionParameters, - response: format::SpecificVersionResponse, - ) -> anyhow::Result<Self> { - let installer = match params.architecture { - VersionArchitecture::X86 => response.installers.x86, - VersionArchitecture::Arm64 => response.installers.arm64, - }; - let installer = installer.context("Installer missing for architecture")?; - let sha256 = hex::decode(installer.sha256) + /// Returns the first duplicated version found in `releases`. + /// `None` is returned if there are no duplicates. + /// NOTE: `releases` MUST be sorted + fn find_duplicate_version( + releases: &[IntermediateVersion], + ) -> Option<&mullvad_version::Version> { + releases + .windows(2) + .find(|pair| { + mullvad_version::Version::version_ordering(&pair[0].version, &pair[1].version) + .is_eq() + }) + .map(|pair| &pair[0].version) + } +} + +impl TryFrom<IntermediateVersion> for Version { + type Error = anyhow::Error; + + fn try_from(version: IntermediateVersion) -> Result<Self, Self::Error> { + // Convert hex checksum to bytes + let sha256 = hex::decode(version.installer.sha256) .context("Invalid checksum hex")? .try_into() .map_err(|_| anyhow::anyhow!("Invalid checksum length"))?; - Ok(Self { - changelog: response.changelog, - version: response.version, - urls: installer.urls, - size: installer.size, + Ok(Version { + version: version.version, + size: version.installer.size, + urls: version.installer.urls, + changelog: version.changelog, sha256, }) } diff --git a/mullvad-update/src/format/deserializer.rs b/mullvad-update/src/format/deserializer.rs index 2647bbe212..6c86f7c01b 100644 --- a/mullvad-update/src/format/deserializer.rs +++ b/mullvad-update/src/format/deserializer.rs @@ -102,7 +102,7 @@ mod test { #[test] fn test_response_deserialization_and_verification() { const TEST_PUBKEY: &str = - "f4c262705b4ae8088bc8173889f779f77563edfd7de3b0ac4aa0e554a6896404"; + "4d35f5376f1f58c41b2a0ee4600ae7811eace354f100227e853994deef38942d"; let pubkey = hex::decode(TEST_PUBKEY).unwrap(); let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap(); diff --git a/mullvad-update/src/format/mod.rs b/mullvad-update/src/format/mod.rs index d1957f42ca..01e4618ddf 100644 --- a/mullvad-update/src/format/mod.rs +++ b/mullvad-update/src/format/mod.rs @@ -45,49 +45,34 @@ struct PartialSignedResponse { pub struct Response { /// When the signature expires pub expires: chrono::DateTime<chrono::Utc>, - /// Stable version response - pub stable: VersionResponse, - /// Beta version response - pub beta: Option<VersionResponse>, + /// Available app releases + pub releases: Vec<Release>, } +/// App release #[derive(Debug, Deserialize, Serialize)] -pub struct VersionResponse { - /// The current version in this channel - pub current: SpecificVersionResponse, - /// The version being rolled out in this channel - pub next: Option<NextSpecificVersionResponse>, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct NextSpecificVersionResponse { - /// The percentage of users that should receive the new version. - pub rollout: f32, - #[serde(flatten)] - pub version: SpecificVersionResponse, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct SpecificVersionResponse { +pub struct Release { /// Mullvad app version pub version: mullvad_version::Version, /// Changelog entries pub changelog: String, /// Installer details for different architectures - pub installers: SpecificVersionArchitectureResponses, + pub installers: Vec<Installer>, + /// Fraction of users that should receive the new version + #[serde(default = "default_rollout")] + pub rollout: f32, } -/// Version details for supported architectures -#[derive(Debug, Deserialize, Serialize)] -pub struct SpecificVersionArchitectureResponses { - /// Details for x86 installer - pub x86: Option<SpecificVersionArchitectureResponse>, - /// Details for ARM64 installer - pub arm64: Option<SpecificVersionArchitectureResponse>, +/// By default, rollout includes all users +fn default_rollout() -> f32 { + 1. } -#[derive(Debug, Deserialize, Serialize)] -pub struct SpecificVersionArchitectureResponse { +/// App installer +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Installer { + /// Installer architecture + pub architecture: Architecture, /// Mirrors that host the artifact pub urls: Vec<String>, /// Size of the installer, in bytes @@ -96,6 +81,16 @@ pub struct SpecificVersionArchitectureResponse { pub sha256: String, } +/// Installer architecture +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum Architecture { + /// x86-64 architecture + X86, + /// ARM64 architecture + Arm64, +} + /// JSON response signature #[derive(Debug, Deserialize, Serialize)] pub struct ResponseSignature { diff --git a/mullvad-update/test-version-response.json b/mullvad-update/test-version-response.json index bfa17dc08d..bc5a89adc1 100644 --- a/mullvad-update/test-version-response.json +++ b/mullvad-update/test-version-response.json @@ -1,75 +1,80 @@ { "signature": { - "keyid": "f4c262705b4ae8088bc8173889f779f77563edfd7de3b0ac4aa0e554a6896404", - "sig": "692ff231a4e12698b86c91c5c3e43dd8781db8022e7910ff5a0f640e7516ae55b1cef216b7e40774d3eae4c1b29129ce78587900779bec51ca51db274af0eb0d" + "keyid": "4d35f5376f1f58c41b2a0ee4600ae7811eace354f100227e853994deef38942d", + "sig": "0537b9fc1f458592a3330e369ef521b2e29dacad2a422cbae37ff7ddd400a5381b063c5f3056e9e3db6235d128128d95b7c54bf305eb2f3bd250d722baa2a504" }, "signed": { "expires": "2025-07-02T15:33:00Z", - "stable": { - "current": { + "releases": [ + { "version": "2025.2", "changelog": "[macos] Adding support for quicfuscator\n[windows] Less bugs", - "installers": { - "x86": { + "installers": [ + { + "architecture": "x86", "urls": [ "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2.exe" ], "size": 101384672, "sha256": "F4B25713D13F2819A300F2FFA94D967463AAEEA0D357FBD7479A281154BA0460" }, - "arm64": { + { + "architecture": "arm64", "urls": [ "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2_arm64.exe" ], "size": 104146312, "sha256": "AFD8098A1FF89D69A243EC4E2E946CF5FBF8D1C10998230D6C8FC0A5C9C39541" } - } + ], + "rollout": 1.0 }, - "next": { - "rollout": 0.3, - "version": "2025.1", + { + "version": "2025.3", "changelog": "[macos] Adding support for quicfuscator\n[windows] Less bugs", - "installers": { - "x86": { + "installers": [ + { + "architecture": "x86", "urls": [ "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2.exe" ], "size": 101384672, "sha256": "F4B25713D13F2819A300F2FFA94D967463AAEEA0D357FBD7479A281154BA0460" }, - "arm64": { + { + "architecture": "arm64", "urls": [ "https://releases.mullvad.net/desktop/releases/2025.2/MullvadVPN-2025.2_arm64.exe" ], "size": 104146312, "sha256": "AFD8098A1FF89D69A243EC4E2E946CF5FBF8D1C10998230D6C8FC0A5C9C39541" } - } - } - }, - "beta": { - "current": { + ], + "rollout": 0.3 + }, + { "version": "2025.1-beta1", "changelog": "[macos] Adding support for quicfuscator\n[windows] Less bugs", - "installers": { - "x86": { + "installers": [ + { + "architecture": "x86", "urls": [ "https://releases.mullvad.net/desktop/releases/2025.3-beta1/MullvadVPN-2025.3-beta1_x64.exe" ], "size": 106297504, "sha256": "0c569aa0912eb93605a85073447d42ba0ca612361bef78ef04ef038e80b15403" }, - "arm64": { + { + "architecture": "arm64", "urls": [ "https://releases.mullvad.net/desktop/releases/2025.3-beta1/MullvadVPN-2025.3-beta1_arm64.exe" ], "size": 111488248, "sha256": "82948D3DB5B869EE5F0D246DB557A81B72B68DFDDD2267872B7B8A5B19A05444" } - } - }, - "next": null - } + ], + "rollout": 1.0 + } + ] } } |
