Skip to content

Commit

Permalink
Test version format verification
Browse files Browse the repository at this point in the history
  • Loading branch information
dlon committed Feb 7, 2025
1 parent e1f0bbe commit 580f7b1
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 8 deletions.
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mullvad-update/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ workspace = true

[dependencies]
anyhow = "1.0"
json-canon = "0.1"
chrono = { workspace = true, features = ["serde"] }
ed25519-dalek = { version = "2.1", default-features = false }
hex = { version = "0.4", default-features = false }
Expand Down
87 changes: 81 additions & 6 deletions mullvad-update/src/deserializer.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,81 @@
//! Deserializer for version API response format
use anyhow::Context;
use serde::Deserialize;

/// JSON response including signature and signed content
/// Note that signature verification isn't accomplished by deserializing
#[derive(Deserialize)]
/// This type does not implement [serde::Deserialize] to prevent accidental deserialization without
/// signature verification.
pub struct SignedResponse {
/// Signature of the canonicalized JSON of `signed`
pub signature: ResponseSignature,
/// Content signed by `signature`
pub signed: Response,
}

/// Helper class that leaves the signed data untouched
/// Note that deserializing doesn't verify anything
#[derive(serde::Deserialize)]
struct PartialSignedResponse {
/// Signature of the canonicalized JSON of `signed`
pub signature: ResponseSignature,
/// Content signed by `signature`
pub signed: serde_json::Value,
}

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())
}

/// 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,
bytes: &[u8],
current_time: chrono::DateTime<chrono::Utc>,
) -> Result<Self, anyhow::Error> {
let partial_data: PartialSignedResponse =
serde_json::from_slice(bytes).context("Invalid version JSON")?;

// Check if the key matches
if partial_data.signature.keyid.0 != key.0 {
anyhow::bail!("Unrecognized key");
}

// Serialize to canonical json format
let canon_data = json_canon::to_vec(&partial_data.signed)
.context("Failed to serialize to canonical JSON")?;

// Check if the data is signed by our key
partial_data
.signature
.keyid
.0
.verify_strict(&canon_data, &partial_data.signature.sig.0)
.context("Signature verification failed")?;

// Deserialize the canonical JSON to structured representation
let signed_response: Response =
serde_json::from_slice(&canon_data).context("Failed to deserialize response")?;

// Reject time if the data has expired
if current_time >= signed_response.expires {
anyhow::bail!(
"Version metadata has expired: valid until {}",
signed_response.expires
);
}

Ok(SignedResponse {
signature: partial_data.signature,
signed: signed_response,
})
}
}

/// JSON response signature
#[derive(Deserialize)]
pub struct ResponseSignature {
Expand Down Expand Up @@ -137,10 +201,21 @@ pub struct SpecificVersionArchitectureResponse {
mod test {
use super::*;

/// Test that a valid version response is successfully deserialized
/// Test that a valid signed version response is successfully deserialized and verified
#[test]
fn test_response_deserialization() {
let _: SignedResponse =
serde_json::from_str(include_str!("../test-version-response.json")).unwrap();
fn test_response_deserialization_and_verification() {
const TEST_PUBKEY: &str =
"AEC24A08466F3D6A1EDCDB2AD3C234428AB9D991B6BEA7F53CB9F172E6CB40D8";
let pubkey = hex::decode(TEST_PUBKEY).unwrap();
let verifying_key =
ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap();

SignedResponse::deserialize_and_verify_at_time(
VerifyingKey(verifying_key),
include_bytes!("../test-version-response.json"),
// It's 1970 again
chrono::DateTime::UNIX_EPOCH,
)
.expect("expected valid signed version metadata");
}
}
4 changes: 2 additions & 2 deletions mullvad-update/test-version-response.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"signature": {
"keyid": "8B84D57D8E94DC03D9E3A17DA77358FD8BA21D2C65B0C63B580F32A79332F727",
"sig": "085672c70dffe26610e58542ee552843633cfed973abdad94c56138dbf0cd991644f2d3f27e4dda3098e08ab676e7f52627b587947ae69db1012d59a6da18e0c"
"keyid": "AEC24A08466F3D6A1EDCDB2AD3C234428AB9D991B6BEA7F53CB9F172E6CB40D8",
"sig": "d68ba75006ea3ac249e56849022a7d93603effe26ec0385bac42cf6675fc6e31322cae018a60428d5c670baedd46b59fa2b35a412f1ed285256c64dbafbcb905"
},
"signed": {
"expires": "2025-07-02T15:33:00Z",
Expand Down

0 comments on commit 580f7b1

Please sign in to comment.