diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-02-20 14:43:17 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-03-05 23:32:11 +0100 |
| commit | 20aa2428753e87102073fb16ea21c30b13ead36a (patch) | |
| tree | 1fd3d12458f9b8c1eb72d43301321ffe60aeca88 | |
| parent | 6b56ceaee3d03b6b186c9014a1ebc93b23197b2b (diff) | |
| download | mullvadvpn-20aa2428753e87102073fb16ea21c30b13ead36a.tar.xz mullvadvpn-20aa2428753e87102073fb16ea21c30b13ead36a.zip | |
Support multiple signatures
| -rw-r--r-- | mullvad-update/src/format/deserializer.rs | 73 | ||||
| -rw-r--r-- | mullvad-update/src/format/key.rs | 9 | ||||
| -rw-r--r-- | mullvad-update/src/format/mod.rs | 20 | ||||
| -rw-r--r-- | mullvad-update/src/format/serializer.rs | 13 | ||||
| -rw-r--r-- | mullvad-update/test-version-response.json | 11 |
5 files changed, 99 insertions, 27 deletions
diff --git a/mullvad-update/src/format/deserializer.rs b/mullvad-update/src/format/deserializer.rs index 0cde83bf1b..28291dde00 100644 --- a/mullvad-update/src/format/deserializer.rs +++ b/mullvad-update/src/format/deserializer.rs @@ -4,7 +4,7 @@ use anyhow::Context; use super::key::*; use super::Response; -use super::{PartialSignedResponse, SignedResponse}; +use super::{PartialSignedResponse, ResponseSignature, SignedResponse}; impl SignedResponse { /// Deserialize some bytes to JSON, and verify them, including signature and expiry. @@ -26,7 +26,7 @@ impl SignedResponse { let signed = serde_json::from_value(partial_data.signed) .context("Failed to deserialize response")?; Ok(Self { - signature: partial_data.signature, + signatures: partial_data.signatures, signed, }) } @@ -64,7 +64,7 @@ impl SignedResponse { } Ok(SignedResponse { - signature: partial_data.signature, + signatures: partial_data.signatures, signed: signed_response, }) } @@ -82,24 +82,26 @@ pub(super) fn deserialize_and_verify( serde_json::from_slice(bytes).context("Invalid version JSON")?; // Check if the key matches - if partial_data.signature.keyid.0 != key.0 { + let Some(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), + // Ignore all non-matching key + _ => None, + }) else { 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) + key.0 + .verify_strict(&canon_data, &sig.0) .context("Signature verification failed")?; Ok(PartialSignedResponse { - signature: partial_data.signature, + signatures: partial_data.signatures, signed: partial_data.signed, }) } @@ -147,4 +149,53 @@ mod test { ) .expect_err("expected rejected version number"); } + + /// Test that invalid key types deserialized to "other" + #[test] + fn test_response_unknown_keytypes() { + //let secret = "F6631A59EBBF8AADEAC64CC30A08A83FC7283F39DE53B7F1BFBA6BE52663DC94"; + let pubkey = "8F735E412015D8976079E5FA0E090100A43A34937CCFC3A2341219E30291DD39"; + let fakesig = "08954286A9284718B83CAADA5DF8A9A9DF0CE569F8EFF669D8C2A2E5945C809C465C38168E2F6018461DD8801DBFC74126A2ED9102F99A49F6DD54722C9B3605"; + let value = serde_json::json!({ + "signatures": [ + { + "keytype": "ed25519", + "keyid": pubkey, + "sig": fakesig, + }, + { + "keytype": "new shiny key", + "keyid": "test 1", + "sig": "test 2", + } + ], + "signed": { + "metadata_expiry": "3000-01-01T00:00:00Z", + "metadata_version": 0, + "releases": [] + } + }); + + let bytes = serde_json::to_vec(&value).expect("serialize should succeed"); + + let response = SignedResponse::deserialize_and_verify_insecure(&bytes) + .expect("deserialization failed"); + + let expected_key = VerifyingKey::from_hex(pubkey).unwrap(); + let expected_sig = Signature::from_hex(&fakesig).unwrap(); + + // Ed25519 key + assert!( + matches!(&response.signatures[0], ResponseSignature::Ed25519 { keyid, sig } if keyid == &expected_key && sig == &expected_sig), + "unexpected response sig: {:?}", + response.signatures[0] + ); + + // Unrecognized key type + assert!( + matches!(&response.signatures[1], ResponseSignature::Other { keyid, sig } if keyid == "test 1" && sig == "test 2"), + "expected unrecognized key: {:?}", + response.signatures[1] + ); + } } diff --git a/mullvad-update/src/format/key.rs b/mullvad-update/src/format/key.rs index 0033ec30a1..a951aff709 100644 --- a/mullvad-update/src/format/key.rs +++ b/mullvad-update/src/format/key.rs @@ -111,9 +111,16 @@ impl Serialize for VerifyingKey { } /// ed25519 signature -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct Signature(pub ed25519_dalek::Signature); +impl Signature { + pub fn from_hex(s: &str) -> anyhow::Result<Self> { + let bytes = bytes_from_hex::<{ ed25519_dalek::SIGNATURE_LENGTH }>(s)?; + Ok(Self(ed25519_dalek::Signature::from_bytes(&bytes))) + } +} + impl<'de> Deserialize<'de> for Signature { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where diff --git a/mullvad-update/src/format/mod.rs b/mullvad-update/src/format/mod.rs index edbe24ab4f..fc706f4357 100644 --- a/mullvad-update/src/format/mod.rs +++ b/mullvad-update/src/format/mod.rs @@ -23,8 +23,8 @@ pub mod serializer; /// signature verification. #[derive(Debug, Serialize)] pub struct SignedResponse { - /// Signature of the canonicalized JSON of `signed` - pub signature: ResponseSignature, + /// Signatures of the canonicalized JSON of `signed` + pub signatures: Vec<ResponseSignature>, /// Content signed by `signature` pub signed: Response, } @@ -33,8 +33,8 @@ pub struct SignedResponse { /// Note that deserializing doesn't verify anything #[derive(Deserialize, Serialize)] struct PartialSignedResponse { - /// Signature of the canonicalized JSON of `signed` - pub signature: ResponseSignature, + /// Signatures of the canonicalized JSON of `signed` + pub signatures: Vec<ResponseSignature>, /// Content signed by `signature` pub signed: serde_json::Value, } @@ -102,9 +102,15 @@ pub enum Architecture { /// JSON response signature #[derive(Debug, Deserialize, Serialize)] -pub struct ResponseSignature { - pub keyid: key::VerifyingKey, - pub sig: key::Signature, +#[serde(tag = "keytype")] +#[serde(rename_all = "lowercase")] +pub enum ResponseSignature { + Ed25519 { + keyid: key::VerifyingKey, + sig: key::Signature, + }, + #[serde(untagged)] + Other { keyid: String, sig: String }, } #[cfg(test)] diff --git a/mullvad-update/src/format/serializer.rs b/mullvad-update/src/format/serializer.rs index 6698c9282f..46c4c8cb7a 100644 --- a/mullvad-update/src/format/serializer.rs +++ b/mullvad-update/src/format/serializer.rs @@ -34,7 +34,7 @@ impl SignedResponse { let response: Response = serde_json::from_value(partial_signed.signed)?; Ok(SignedResponse { - signature: partial_signed.signature, + signatures: partial_signed.signatures, signed: response, }) } @@ -58,10 +58,10 @@ fn sign<T: Serialize>( // Attach signature Ok(PartialSignedResponse { - signature: ResponseSignature { + signatures: vec![ResponseSignature::Ed25519 { keyid: key.pubkey(), sig, - }, + }], // Attach now-signed data signed, }) @@ -86,7 +86,12 @@ mod test { // Verify that we can deserialize and verify the data let partial = sign(&key, &data).context("Signing failed")?; - assert_eq!(partial.signature.keyid, pubkey); + assert!( + matches!(&partial.signatures[0], ResponseSignature::Ed25519 { + keyid, + .. + } if keyid == &pubkey) + ); let bytes = serde_json::to_vec(&partial)?; diff --git a/mullvad-update/test-version-response.json b/mullvad-update/test-version-response.json index d4d72698e6..b6466e48c2 100644 --- a/mullvad-update/test-version-response.json +++ b/mullvad-update/test-version-response.json @@ -1,8 +1,11 @@ { - "signature": { - "keyid": "bb4ef63ffdcc6bd5a19c30cd23b9de03099407a04463418f17ae338b98aa09d4", - "sig": "253ec37846dcd909bfc5119c0e0d06535767e179eb8b4465015eaa95f4bed362c8c9186311192c987871722bf7d319d44e4f04eaf79c269820bc13ff1a901f0b" - }, + "signatures": [ + { + "keytype": "ed25519", + "keyid": "bb4ef63ffdcc6bd5a19c30cd23b9de03099407a04463418f17ae338b98aa09d4", + "sig": "253ec37846dcd909bfc5119c0e0d06535767e179eb8b4465015eaa95f4bed362c8c9186311192c987871722bf7d319d44e4f04eaf79c269820bc13ff1a901f0b" + } + ], "signed": { "metadata_version": 0, "metadata_expiry": "2025-07-02T15:33:00Z", |
