diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-02-20 13:04:09 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-03-05 23:32:11 +0100 |
| commit | 6b56ceaee3d03b6b186c9014a1ebc93b23197b2b (patch) | |
| tree | 02c2cb790398c8e0e76b8c8aba3688f92a7bd37f | |
| parent | 147a28c9e0244c0c4ca24fdd30f720616f2fb3b1 (diff) | |
| download | mullvadvpn-6b56ceaee3d03b6b186c9014a1ebc93b23197b2b.tar.xz mullvadvpn-6b56ceaee3d03b6b186c9014a1ebc93b23197b2b.zip | |
Add version counter to metadata and rename expiry field
| -rw-r--r-- | installer-downloader/src/controller.rs | 4 | ||||
| -rw-r--r-- | mullvad-update/src/api.rs | 15 | ||||
| -rw-r--r-- | mullvad-update/src/format/deserializer.rs | 39 | ||||
| -rw-r--r-- | mullvad-update/src/format/mod.rs | 4 | ||||
| -rw-r--r-- | mullvad-update/src/format/serializer.rs | 2 | ||||
| -rw-r--r-- | mullvad-update/src/version.rs | 5 | ||||
| -rw-r--r-- | mullvad-update/test-pubkey | 1 | ||||
| -rw-r--r-- | mullvad-update/test-version-response.json | 18 | ||||
| -rw-r--r-- | mullvad-update/update-testdata.sh | 19 |
9 files changed, 81 insertions, 26 deletions
diff --git a/installer-downloader/src/controller.rs b/installer-downloader/src/controller.rs index 5021785fb5..c4383dff7d 100644 --- a/installer-downloader/src/controller.rs +++ b/installer-downloader/src/controller.rs @@ -53,7 +53,7 @@ pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T) { type DirProvider = TempDirProvider; // Version info provider to use - const TEST_PUBKEY: &str = "4d35f5376f1f58c41b2a0ee4600ae7811eace354f100227e853994deef38942d"; + const TEST_PUBKEY: &str = include_str!("../../mullvad-update/test-pubkey"); let verifying_key = mullvad_update::format::key::VerifyingKey::from_hex(TEST_PUBKEY).expect("valid key"); let version_provider = HttpVersionInfoProvider { @@ -139,6 +139,8 @@ where architecture: VersionArchitecture::X86, // For the downloader, the rollout version is always preferred rollout: 1., + // The downloader allows any version + lowest_metadata_version: 0, }; let err = match version_provider.get_version_info(version_params).await { diff --git a/mullvad-update/src/api.rs b/mullvad-update/src/api.rs index 864c0745a2..8695daa215 100644 --- a/mullvad-update/src/api.rs +++ b/mullvad-update/src/api.rs @@ -26,8 +26,11 @@ pub struct HttpVersionInfoProvider { impl VersionInfoProvider for HttpVersionInfoProvider { async fn get_version_info(&self, params: VersionParameters) -> anyhow::Result<VersionInfo> { let raw_json = Self::get(&self.url, self.pinned_certificate.clone()).await?; - let response = - format::SignedResponse::deserialize_and_verify(&self.verifying_key, &raw_json)?; + let response = format::SignedResponse::deserialize_and_verify( + &self.verifying_key, + &raw_json, + params.lowest_metadata_version, + )?; VersionInfo::try_from_response(¶ms, response.signed) } @@ -99,10 +102,9 @@ 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 = crate::format::key::VerifyingKey::from_hex( - "4d35f5376f1f58c41b2a0ee4600ae7811eace354f100227e853994deef38942d", - ) - .expect("valid key"); + let verifying_key = + crate::format::key::VerifyingKey::from_hex(include_str!("../test-pubkey")) + .expect("valid key"); // Start HTTP server let mut server = mockito::Server::new_async().await; @@ -118,6 +120,7 @@ mod test { let params = VersionParameters { architecture: VersionArchitecture::X86, rollout: 1., + lowest_metadata_version: 0, }; let info_provider = HttpVersionInfoProvider { url, diff --git a/mullvad-update/src/format/deserializer.rs b/mullvad-update/src/format/deserializer.rs index 3c19376ecb..0cde83bf1b 100644 --- a/mullvad-update/src/format/deserializer.rs +++ b/mullvad-update/src/format/deserializer.rs @@ -9,8 +9,12 @@ use super::{PartialSignedResponse, SignedResponse}; 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, bytes: &[u8]) -> Result<Self, anyhow::Error> { - Self::deserialize_and_verify_at_time(key, bytes, chrono::Utc::now()) + pub fn deserialize_and_verify( + key: &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) } /// This method is used for testing, and skips all verification. @@ -33,6 +37,7 @@ impl SignedResponse { key: &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)?; @@ -42,10 +47,19 @@ impl SignedResponse { .context("Failed to deserialize response")?; // Reject time if the data has expired - if current_time >= signed_response.expires { + if current_time >= signed_response.metadata_expiry { anyhow::bail!( "Version metadata has expired: valid until {}", - signed_response.expires + signed_response.metadata_expiry + ); + } + + // Reject data if the version counter is below `min_metadata_version` + if signed_response.metadata_version < min_metadata_version { + anyhow::bail!( + "Version metadata is too old: {}, must be at least {}", + signed_response.metadata_version, + min_metadata_version, ); } @@ -99,9 +113,7 @@ mod test { /// Test that a valid signed version response is successfully deserialized and verified #[test] fn test_response_deserialization_and_verification() { - const TEST_PUBKEY: &str = - "4d35f5376f1f58c41b2a0ee4600ae7811eace354f100227e853994deef38942d"; - let pubkey = hex::decode(TEST_PUBKEY).unwrap(); + let pubkey = hex::decode(include_str!("../../test-pubkey")).unwrap(); let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap(); @@ -110,6 +122,8 @@ mod test { include_bytes!("../../test-version-response.json"), // It's 1970 again chrono::DateTime::UNIX_EPOCH, + // Accept any version + 0, ) .expect("expected valid signed version metadata"); @@ -119,7 +133,18 @@ mod test { include_bytes!("../../test-version-response.json"), // In the year 3000 chrono::DateTime::from_str("3000-01-01T00:00:00Z").unwrap(), + // Accept any version + 0, ) .expect_err("expected expired version metadata"); + + // Reject expired version number + SignedResponse::deserialize_and_verify_at_time( + &VerifyingKey(verifying_key), + include_bytes!("../../test-version-response.json"), + chrono::DateTime::UNIX_EPOCH, + usize::MAX, + ) + .expect_err("expected rejected version number"); } } diff --git a/mullvad-update/src/format/mod.rs b/mullvad-update/src/format/mod.rs index eaa6a6de00..edbe24ab4f 100644 --- a/mullvad-update/src/format/mod.rs +++ b/mullvad-update/src/format/mod.rs @@ -44,8 +44,10 @@ struct PartialSignedResponse { #[serde(deny_unknown_fields)] #[cfg_attr(test, derive(Clone))] pub struct Response { + /// Version counter + pub metadata_version: usize, /// When the signature expires - pub expires: chrono::DateTime<chrono::Utc>, + pub metadata_expiry: chrono::DateTime<chrono::Utc>, /// Available app releases pub releases: Vec<Release>, } diff --git a/mullvad-update/src/format/serializer.rs b/mullvad-update/src/format/serializer.rs index 4c40037e2d..6698c9282f 100644 --- a/mullvad-update/src/format/serializer.rs +++ b/mullvad-update/src/format/serializer.rs @@ -22,7 +22,7 @@ use super::{key, PartialSignedResponse, Response, ResponseSignature, SignedRespo impl SignedResponse { pub fn sign(key: key::SecretKey, response: Response) -> anyhow::Result<SignedResponse> { // Refuse to sign expired data - if response.expires < chrono::Utc::now() { + if response.metadata_expiry < chrono::Utc::now() { anyhow::bail!("Signing failed since the data has expired"); } diff --git a/mullvad-update/src/version.rs b/mullvad-update/src/version.rs index c62e0c8113..5317cf8d49 100644 --- a/mullvad-update/src/version.rs +++ b/mullvad-update/src/version.rs @@ -18,6 +18,9 @@ pub struct VersionParameters { pub architecture: VersionArchitecture, /// Rollout threshold. Any version in the response below this threshold will be ignored pub rollout: f32, + /// Lowest allowed `metadata_version` in the version data + /// Typically the current version plus 1 + pub lowest_metadata_version: usize, } /// Installer architecture @@ -173,6 +176,7 @@ mod test { let params = VersionParameters { architecture: VersionArchitecture::X86, rollout: 1., + lowest_metadata_version: 0, }; // Expect: The available latest versions for X86, where the rollout is 1. @@ -193,6 +197,7 @@ mod test { let params = VersionParameters { architecture: VersionArchitecture::Arm64, rollout: 0.01, + lowest_metadata_version: 0, }; let info = VersionInfo::try_from_response(¶ms, response.signed)?; diff --git a/mullvad-update/test-pubkey b/mullvad-update/test-pubkey new file mode 100644 index 0000000000..f5b25b1f24 --- /dev/null +++ b/mullvad-update/test-pubkey @@ -0,0 +1 @@ +BB4EF63FFDCC6BD5A19C30CD23B9DE03099407A04463418F17AE338B98AA09D4
\ No newline at end of file diff --git a/mullvad-update/test-version-response.json b/mullvad-update/test-version-response.json index 0484159d5b..d4d72698e6 100644 --- a/mullvad-update/test-version-response.json +++ b/mullvad-update/test-version-response.json @@ -1,10 +1,11 @@ { "signature": { - "keyid": "4d35f5376f1f58c41b2a0ee4600ae7811eace354f100227e853994deef38942d", - "sig": "7dc4f2d491b972d98ead6a252022dd5cbe2d3829ae28f174129ee94bcd3d1329d19db90d46251c81d75e04e49db29ae950899bcb4e6cf7f64c3fedec3ee0ee08" + "keyid": "bb4ef63ffdcc6bd5a19c30cd23b9de03099407a04463418f17ae338b98aa09d4", + "sig": "253ec37846dcd909bfc5119c0e0d06535767e179eb8b4465015eaa95f4bed362c8c9186311192c987871722bf7d319d44e4f04eaf79c269820bc13ff1a901f0b" }, "signed": { - "expires": "2025-07-02T15:33:00Z", + "metadata_version": 0, + "metadata_expiry": "2025-07-02T15:33:00Z", "releases": [ { "version": "2025.2", @@ -26,8 +27,7 @@ "size": 104146312, "sha256": "AFD8098A1FF89D69A243EC4E2E946CF5FBF8D1C10998230D6C8FC0A5C9C39541" } - ], - "rollout": 1.0 + ] }, { "version": "2025.3", @@ -50,7 +50,7 @@ "sha256": "AFD8098A1FF89D69A243EC4E2E946CF5FBF8D1C10998230D6C8FC0A5C9C39541" } ], - "rollout": 0.3 + "rollout": 0.5 }, { "version": "2025.1-beta1", @@ -72,8 +72,7 @@ "size": 111488248, "sha256": "82948D3DB5B869EE5F0D246DB557A81B72B68DFDDD2267872B7B8A5B19A05444" } - ], - "rollout": 1.0 + ] }, { "version": "2025.3-beta1", @@ -95,8 +94,7 @@ "size": 111488248, "sha256": "82948D3DB5B869EE5F0D246DB557A81B72B68DFDDD2267872B7B8A5B19A05444" } - ], - "rollout": 1.0 + ] } ] } diff --git a/mullvad-update/update-testdata.sh b/mullvad-update/update-testdata.sh new file mode 100644 index 0000000000..17ac62d9ea --- /dev/null +++ b/mullvad-update/update-testdata.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +# Update test-version-response from + +secret="a459c1ee4f128780592b61454786cb289b38034a3ac1c7860e6e62187ac6e9a9" +#secret=$(cargo r --bin mullvad-version-metadata --features sign generate-key) +pubkey="BB4EF63FFDCC6BD5A19C30CD23B9DE03099407A04463418F17AE338B98AA09D4" + +echo "secret: $secret" +echo "pubkey: $pubkey" + +cargo r --bin mullvad-version-metadata --features sign sign --file ./unsigned-response.json --secret $secret > test-version-response.json + +echo -n "$pubkey" > test-pubkey |
