summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-02-12 21:01:17 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-05 23:31:59 +0100
commitb2f54826a9b2951ecf56c7794f341124b0220762 (patch)
treee92661cfea90ae1982017ca4899a4547d5f552e5
parent504bb2a2c6ffbad4523a38990fc834541b0826ad (diff)
downloadmullvadvpn-b2f54826a9b2951ecf56c7794f341124b0220762.tar.xz
mullvadvpn-b2f54826a9b2951ecf56c7794f341124b0220762.zip
Update version response format
-rw-r--r--mullvad-update/src/api.rs145
-rw-r--r--mullvad-update/src/format/deserializer.rs2
-rw-r--r--mullvad-update/src/format/mod.rs57
-rw-r--r--mullvad-update/test-version-response.json57
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(&params, response)
+ VersionInfo::try_from_response(&params, 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
+ }
+ ]
}
}