Skip to content

Commit ec26088

Browse files
committed
Test version format verification
1 parent 8828428 commit ec26088

File tree

4 files changed

+84
-8
lines changed

4 files changed

+84
-8
lines changed

Cargo.lock

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mullvad-update/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ workspace = true
1212

1313
[dependencies]
1414
anyhow = "1.0"
15+
json-canon = "0.1"
1516
chrono = { workspace = true, features = ["serde"] }
1617
ed25519-dalek = { version = "2.1", default-features = false }
1718
hex = { version = "0.4", default-features = false }

mullvad-update/src/deserializer.rs

+63-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,65 @@
11
//! Deserializer for version API response format
22
3+
use anyhow::Context;
34
use serde::Deserialize;
45

56
/// JSON response including signature and signed content
6-
/// Note that signature verification isn't accomplished by deserializing
7-
#[derive(Deserialize)]
7+
/// This type does not implement [serde::Deserialize] to prevent accidental deserialization without
8+
/// signature verification.
89
pub struct SignedResponse {
910
/// Signature of the canonicalized JSON of `signed`
1011
pub signature: ResponseSignature,
1112
/// Content signed by `signature`
1213
pub signed: Response,
1314
}
1415

16+
/// Helper class that leaves the signed data untouched
17+
/// Note that deserializing doesn't verify anything
18+
#[derive(serde::Deserialize)]
19+
struct PartialSignedResponse {
20+
/// Signature of the canonicalized JSON of `signed`
21+
pub signature: ResponseSignature,
22+
/// Content signed by `signature`
23+
pub signed: serde_json::Value,
24+
}
25+
26+
impl SignedResponse {
27+
/// Deserialize some bytes to JSON, and verify them, including signature and expiry.
28+
/// If successful, the deserialized data is returned.
29+
pub fn deserialize_and_verify(key: VerifyingKey, bytes: &[u8]) -> Result<Self, anyhow::Error> {
30+
let partial_data: PartialSignedResponse =
31+
serde_json::from_slice(bytes).context("Invalid version JSON")?;
32+
33+
// Check if the key matches
34+
if partial_data.signature.keyid.0 != key.0 {
35+
anyhow::bail!("Unrecognized key");
36+
}
37+
38+
// Serialize to canonical json format
39+
let canon_data = json_canon::to_vec(&partial_data.signed)
40+
.context("Failed to serialize to canonical JSON")?;
41+
42+
// Check if the data is signed by our key
43+
partial_data
44+
.signature
45+
.keyid
46+
.0
47+
.verify_strict(&canon_data, &partial_data.signature.sig.0)
48+
.context("Signature verification failed")?;
49+
50+
// Deserialize the canonical JSON to structured representation
51+
let signed_response: Response =
52+
serde_json::from_slice(&canon_data).context("Failed to deserialize response")?;
53+
54+
// FIXME: Check expiry
55+
56+
Ok(SignedResponse {
57+
signature: partial_data.signature,
58+
signed: signed_response,
59+
})
60+
}
61+
}
62+
1563
/// JSON response signature
1664
#[derive(Deserialize)]
1765
pub struct ResponseSignature {
@@ -137,10 +185,19 @@ pub struct SpecificVersionArchitectureResponse {
137185
mod test {
138186
use super::*;
139187

140-
/// Test that a valid version response is successfully deserialized
188+
/// Test that a valid signed version response is successfully deserialized and verified
141189
#[test]
142-
fn test_response_deserialization() {
143-
let _: SignedResponse =
144-
serde_json::from_str(include_str!("../test-version-response.json")).unwrap();
190+
fn test_response_deserialization_and_verification() {
191+
const TEST_PUBKEY: &str =
192+
"AEC24A08466F3D6A1EDCDB2AD3C234428AB9D991B6BEA7F53CB9F172E6CB40D8";
193+
let pubkey = hex::decode(TEST_PUBKEY).unwrap();
194+
let verifying_key =
195+
ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap();
196+
197+
SignedResponse::deserialize_and_verify(
198+
VerifyingKey(verifying_key),
199+
include_bytes!("../test-version-response.json"),
200+
)
201+
.expect("expected valid signed version metadata");
145202
}
146203
}

mullvad-update/test-version-response.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"signature": {
3-
"keyid": "8B84D57D8E94DC03D9E3A17DA77358FD8BA21D2C65B0C63B580F32A79332F727",
4-
"sig": "085672c70dffe26610e58542ee552843633cfed973abdad94c56138dbf0cd991644f2d3f27e4dda3098e08ab676e7f52627b587947ae69db1012d59a6da18e0c"
3+
"keyid": "AEC24A08466F3D6A1EDCDB2AD3C234428AB9D991B6BEA7F53CB9F172E6CB40D8",
4+
"sig": "d68ba75006ea3ac249e56849022a7d93603effe26ec0385bac42cf6675fc6e31322cae018a60428d5c670baedd46b59fa2b35a412f1ed285256c64dbafbcb905"
55
},
66
"signed": {
77
"expires": "2025-07-02T15:33:00Z",

0 commit comments

Comments
 (0)