summaryrefslogtreecommitdiffhomepage
path: root/mullvad-update/src/format
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-03-06 17:40:25 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-07 10:21:26 +0100
commita2bb3a2bfee997ca657906ec391a576327d07dfe (patch)
tree6c803656b48dfae82efb397cd8c9614aa101b694 /mullvad-update/src/format
parent2dc82d54771bcb4111b0611072f9d9321fd899dc (diff)
downloadmullvadvpn-a2bb3a2bfee997ca657906ec391a576327d07dfe.tar.xz
mullvadvpn-a2bb3a2bfee997ca657906ec391a576327d07dfe.zip
Support multiple verifying keys in mullvad-update
Diffstat (limited to 'mullvad-update/src/format')
-rw-r--r--mullvad-update/src/format/deserializer.rs29
-rw-r--r--mullvad-update/src/format/serializer.rs3
2 files changed, 20 insertions, 12 deletions
diff --git a/mullvad-update/src/format/deserializer.rs b/mullvad-update/src/format/deserializer.rs
index dec1e64f47..3b370c2ff5 100644
--- a/mullvad-update/src/format/deserializer.rs
+++ b/mullvad-update/src/format/deserializer.rs
@@ -1,6 +1,7 @@
//! Deserializer and verifier of version metadata
use anyhow::Context;
+use vec1::Vec1;
use super::key::*;
use super::Response;
@@ -10,11 +11,11 @@ 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,
+ keys: &Vec1<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)
+ Self::deserialize_and_verify_at_time(keys, bytes, chrono::Utc::now(), min_metadata_version)
}
/// This method is used mostly for testing, and skips all verification.
@@ -33,13 +34,13 @@ impl SignedResponse {
/// Deserialize some bytes to JSON, and verify them, including signature and expiry.
/// If successful, the deserialized data is returned.
fn deserialize_and_verify_at_time(
- key: &VerifyingKey,
+ keys: &Vec1<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)?;
+ let partial_data = deserialize_and_verify(keys, bytes)?;
// Deserialize the canonical JSON to structured representation
let signed_response: Response = serde_json::from_value(partial_data.signed)
@@ -74,16 +75,20 @@ impl SignedResponse {
///
/// On success, this returns verified data and signature
pub(super) fn deserialize_and_verify(
- key: &VerifyingKey,
+ keys: &Vec1<VerifyingKey>,
bytes: &[u8],
) -> anyhow::Result<PartialSignedResponse> {
let partial_data: PartialSignedResponse =
serde_json::from_slice(bytes).context("Invalid version JSON")?;
- // Check if the key matches
- let Some(sig) = partial_data.signatures.iter().find_map(|sig| match sig {
+ let valid_keys: Vec<_> = keys.into_iter().map(|k| k.0).collect();
+
+ // Check if one of the keys matches
+ let Some((key, sig)) = partial_data.signatures.iter().find_map(|sig| match sig {
// Check if ed25519 key matches
- ResponseSignature::Ed25519 { keyid, sig } if keyid.0 == key.0 => Some(sig),
+ ResponseSignature::Ed25519 { keyid, sig } if valid_keys.contains(&keyid.0) => {
+ Some((keyid, sig))
+ }
// Ignore all non-matching key
_ => None,
}) else {
@@ -109,6 +114,8 @@ pub(super) fn deserialize_and_verify(
mod test {
use std::str::FromStr;
+ use vec1::vec1;
+
use super::*;
/// Test that a valid signed version response is successfully deserialized and verified
@@ -119,7 +126,7 @@ mod test {
ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap();
SignedResponse::deserialize_and_verify_at_time(
- &VerifyingKey(verifying_key),
+ &vec1![VerifyingKey(verifying_key)],
include_bytes!("../../test-version-response.json"),
// It's 1970 again
chrono::DateTime::UNIX_EPOCH,
@@ -130,7 +137,7 @@ mod test {
// Reject expired data
SignedResponse::deserialize_and_verify_at_time(
- &VerifyingKey(verifying_key),
+ &vec1![VerifyingKey(verifying_key)],
include_bytes!("../../test-version-response.json"),
// In the year 3000
chrono::DateTime::from_str("3000-01-01T00:00:00Z").unwrap(),
@@ -141,7 +148,7 @@ mod test {
// Reject expired version number
SignedResponse::deserialize_and_verify_at_time(
- &VerifyingKey(verifying_key),
+ &vec1![VerifyingKey(verifying_key)],
include_bytes!("../../test-version-response.json"),
chrono::DateTime::UNIX_EPOCH,
usize::MAX,
diff --git a/mullvad-update/src/format/serializer.rs b/mullvad-update/src/format/serializer.rs
index 46c4c8cb7a..ca98b7ba26 100644
--- a/mullvad-update/src/format/serializer.rs
+++ b/mullvad-update/src/format/serializer.rs
@@ -72,6 +72,7 @@ mod test {
use super::*;
use crate::format::deserializer::deserialize_and_verify;
use serde_json::json;
+ use vec1::vec1;
#[test]
fn test_sign() -> anyhow::Result<()> {
@@ -95,7 +96,7 @@ mod test {
let bytes = serde_json::to_vec(&partial)?;
- deserialize_and_verify(&pubkey, &bytes)?;
+ deserialize_and_verify(&vec1![pubkey], &bytes)?;
Ok(())
}