summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-02-20 13:04:09 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-05 23:32:11 +0100
commit6b56ceaee3d03b6b186c9014a1ebc93b23197b2b (patch)
tree02c2cb790398c8e0e76b8c8aba3688f92a7bd37f
parent147a28c9e0244c0c4ca24fdd30f720616f2fb3b1 (diff)
downloadmullvadvpn-6b56ceaee3d03b6b186c9014a1ebc93b23197b2b.tar.xz
mullvadvpn-6b56ceaee3d03b6b186c9014a1ebc93b23197b2b.zip
Add version counter to metadata and rename expiry field
-rw-r--r--installer-downloader/src/controller.rs4
-rw-r--r--mullvad-update/src/api.rs15
-rw-r--r--mullvad-update/src/format/deserializer.rs39
-rw-r--r--mullvad-update/src/format/mod.rs4
-rw-r--r--mullvad-update/src/format/serializer.rs2
-rw-r--r--mullvad-update/src/version.rs5
-rw-r--r--mullvad-update/test-pubkey1
-rw-r--r--mullvad-update/test-version-response.json18
-rw-r--r--mullvad-update/update-testdata.sh19
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(&params, 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(&params, 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