Skip to content

Commit cae71a3

Browse files
committed
Merge branch 'multiple-verifying-keys'
2 parents 2dc82d5 + 54dec57 commit cae71a3

File tree

12 files changed

+102
-23
lines changed

12 files changed

+102
-23
lines changed

Cargo.lock

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

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ ipnetwork = "0.20"
122122
tun = { version = "0.5.5", features = ["async"] }
123123
socket2 = "0.5.7"
124124

125+
vec1 = "1.12"
126+
125127
# Test dependencies
126128
proptest = "1.4"
127129
insta = { version = "1.42", features = ["yaml"] }

installer-downloader/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ rand = { version = "0.8.5" }
2626
reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls"] }
2727
serde = { workspace = true, features = ["derive"] }
2828
tokio = { workspace = true, features = ["rt-multi-thread", "fs"] }
29+
vec1 = { workspace = true }
2930

3031
talpid-platform-metadata = { path = "../talpid-platform-metadata" }
3132
mullvad-update = { path = "../mullvad-update", features = ["client"] }

installer-downloader/src/controller.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use tokio::{
1919
sync::{mpsc, oneshot},
2020
task::JoinHandle,
2121
};
22+
use vec1::vec1;
2223

2324
/// ed25519 pubkey used to verify metadata from the Mullvad (stagemole) API
2425
const VERSION_PROVIDER_PUBKEY: &str = include_str!("../../mullvad-update/stagemole-pubkey");
@@ -60,7 +61,7 @@ pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T, environ
6061
let version_provider = HttpVersionInfoProvider {
6162
url: get_metadata_url(),
6263
pinned_certificate: Some(cert),
63-
verifying_key,
64+
verifying_keys: vec1![verifying_key],
6465
};
6566

6667
AppController::initialize::<_, Downloader<T>, _, DirProvider>(

mullvad-update/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ workspace = true
1313
[features]
1414
default = []
1515
sign = ["rand", "clap"]
16-
client = ["async-trait", "reqwest", "sha2", "tokio", "thiserror"]
16+
client = ["async-trait", "reqwest", "sha2", "tokio", "thiserror", "vec1"]
1717

1818
[dependencies]
1919
anyhow = { workspace = true }
@@ -28,6 +28,7 @@ async-trait = { version = "0.1", optional = true }
2828
reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls"], optional = true }
2929
sha2 = { version = "0.10", optional = true }
3030
tokio = { workspace = true, features = ["rt-multi-thread", "fs", "process", "macros"], optional = true }
31+
vec1 = { workspace = true, optional = true }
3132

3233
mullvad-version = { path = "../mullvad-version", features = ["serde"] }
3334

mullvad-update/meta/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ serde = { workspace = true }
2222
sha2 = "0.10"
2323
tokio = { version = "1", features = ["full"] }
2424
toml = "0.8"
25+
vec1 = { workspace = true }
2526

2627
mullvad-version = { path = "../../mullvad-version", features = ["serde"] }
2728
mullvad-update = { path = "../", features = ["client", "sign"] }

mullvad-update/meta/src/platform.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::{
1212
str::FromStr,
1313
};
1414
use tokio::{fs, io};
15+
use vec1::vec1;
1516

1617
use crate::{
1718
artifacts,
@@ -128,7 +129,7 @@ impl Platform {
128129
// TODO: pin
129130
pinned_certificate: None,
130131
url,
131-
verifying_key,
132+
verifying_keys: vec1![verifying_key],
132133
};
133134
let response = version_provider
134135
.get_versions(crate::MIN_VERIFY_METADATA_VERSION)
@@ -229,12 +230,11 @@ impl Platform {
229230
println!("Verifying signature of {}...", signed_path.display());
230231
let bytes = fs::read(signed_path).await.context("Failed to read file")?;
231232

232-
// TODO: Actual key
233-
let public_key =
234-
key::VerifyingKey::from_hex(include_str!("../../test-pubkey")).expect("Invalid pubkey");
233+
let public_key = key::VerifyingKey::from_hex(include_str!("../../stagemole-pubkey"))
234+
.expect("Invalid pubkey");
235235

236236
format::SignedResponse::deserialize_and_verify(
237-
&public_key,
237+
&vec1![public_key],
238238
&bytes,
239239
crate::MIN_VERIFY_METADATA_VERSION,
240240
)

mullvad-update/src/client/api.rs

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! This module implements fetching of information about app versions
22
33
use anyhow::Context;
4+
use vec1::Vec1;
45

56
use crate::format;
67
use crate::version::{VersionInfo, VersionParameters};
@@ -19,7 +20,7 @@ pub struct HttpVersionInfoProvider {
1920
/// Accepted root certificate. Defaults are used unless specified
2021
pub pinned_certificate: Option<reqwest::Certificate>,
2122
/// Key to use for verifying the response
22-
pub verifying_key: format::key::VerifyingKey,
23+
pub verifying_keys: Vec1<format::key::VerifyingKey>,
2324
}
2425

2526
#[async_trait::async_trait]
@@ -41,7 +42,7 @@ impl HttpVersionInfoProvider {
4142
) -> anyhow::Result<format::SignedResponse> {
4243
let raw_json = Self::get(&self.url, self.pinned_certificate.clone()).await?;
4344
let response = format::SignedResponse::deserialize_and_verify(
44-
&self.verifying_key,
45+
&self.verifying_keys,
4546
&raw_json,
4647
lowest_metadata_version,
4748
)?;
@@ -101,6 +102,7 @@ impl HttpVersionInfoProvider {
101102
#[cfg(test)]
102103
mod test {
103104
use insta::assert_yaml_snapshot;
105+
use vec1::vec1;
104106

105107
use crate::version::VersionArchitecture;
106108

@@ -115,9 +117,10 @@ mod test {
115117
/// We're not testing the correctness of [version] here, only the HTTP client
116118
#[tokio::test]
117119
async fn test_http_version_provider() -> anyhow::Result<()> {
118-
let verifying_key =
120+
let valid_key =
119121
crate::format::key::VerifyingKey::from_hex(include_str!("../../test-pubkey"))
120122
.expect("valid key");
123+
let verifying_keys = vec1![valid_key];
121124

122125
// Start HTTP server
123126
let mut server = mockito::Server::new_async().await;
@@ -138,7 +141,7 @@ mod test {
138141
let info_provider = HttpVersionInfoProvider {
139142
url,
140143
pinned_certificate: None,
141-
verifying_key,
144+
verifying_keys,
142145
};
143146

144147
let info = info_provider

mullvad-update/src/format/deserializer.rs

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Deserializer and verifier of version metadata
22
33
use anyhow::Context;
4+
use vec1::Vec1;
45

56
use super::key::*;
67
use super::Response;
@@ -10,11 +11,11 @@ impl SignedResponse {
1011
/// Deserialize some bytes to JSON, and verify them, including signature and expiry.
1112
/// If successful, the deserialized data is returned.
1213
pub fn deserialize_and_verify(
13-
key: &VerifyingKey,
14+
keys: &Vec1<VerifyingKey>,
1415
bytes: &[u8],
1516
min_metadata_version: usize,
1617
) -> Result<Self, anyhow::Error> {
17-
Self::deserialize_and_verify_at_time(key, bytes, chrono::Utc::now(), min_metadata_version)
18+
Self::deserialize_and_verify_at_time(keys, bytes, chrono::Utc::now(), min_metadata_version)
1819
}
1920

2021
/// This method is used mostly for testing, and skips all verification.
@@ -33,13 +34,13 @@ impl SignedResponse {
3334
/// Deserialize some bytes to JSON, and verify them, including signature and expiry.
3435
/// If successful, the deserialized data is returned.
3536
fn deserialize_and_verify_at_time(
36-
key: &VerifyingKey,
37+
keys: &Vec1<VerifyingKey>,
3738
bytes: &[u8],
3839
current_time: chrono::DateTime<chrono::Utc>,
3940
min_metadata_version: usize,
4041
) -> Result<Self, anyhow::Error> {
4142
// Deserialize and verify signature
42-
let partial_data = deserialize_and_verify(key, bytes)?;
43+
let partial_data = deserialize_and_verify(keys, bytes)?;
4344

4445
// Deserialize the canonical JSON to structured representation
4546
let signed_response: Response = serde_json::from_value(partial_data.signed)
@@ -74,16 +75,20 @@ impl SignedResponse {
7475
///
7576
/// On success, this returns verified data and signature
7677
pub(super) fn deserialize_and_verify(
77-
key: &VerifyingKey,
78+
keys: &Vec1<VerifyingKey>,
7879
bytes: &[u8],
7980
) -> anyhow::Result<PartialSignedResponse> {
8081
let partial_data: PartialSignedResponse =
8182
serde_json::from_slice(bytes).context("Invalid version JSON")?;
8283

83-
// Check if the key matches
84-
let Some(sig) = partial_data.signatures.iter().find_map(|sig| match sig {
84+
let valid_keys: Vec<_> = keys.into_iter().map(|k| k.0).collect();
85+
86+
// Check if one of the keys matches
87+
let Some((key, sig)) = partial_data.signatures.iter().find_map(|sig| match sig {
8588
// Check if ed25519 key matches
86-
ResponseSignature::Ed25519 { keyid, sig } if keyid.0 == key.0 => Some(sig),
89+
ResponseSignature::Ed25519 { keyid, sig } if valid_keys.contains(&keyid.0) => {
90+
Some((keyid, sig))
91+
}
8792
// Ignore all non-matching key
8893
_ => None,
8994
}) else {
@@ -109,6 +114,8 @@ pub(super) fn deserialize_and_verify(
109114
mod test {
110115
use std::str::FromStr;
111116

117+
use vec1::vec1;
118+
112119
use super::*;
113120

114121
/// Test that a valid signed version response is successfully deserialized and verified
@@ -119,7 +126,7 @@ mod test {
119126
ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap();
120127

121128
SignedResponse::deserialize_and_verify_at_time(
122-
&VerifyingKey(verifying_key),
129+
&vec1![VerifyingKey(verifying_key)],
123130
include_bytes!("../../test-version-response.json"),
124131
// It's 1970 again
125132
chrono::DateTime::UNIX_EPOCH,
@@ -130,7 +137,7 @@ mod test {
130137

131138
// Reject expired data
132139
SignedResponse::deserialize_and_verify_at_time(
133-
&VerifyingKey(verifying_key),
140+
&vec1![VerifyingKey(verifying_key)],
134141
include_bytes!("../../test-version-response.json"),
135142
// In the year 3000
136143
chrono::DateTime::from_str("3000-01-01T00:00:00Z").unwrap(),
@@ -141,7 +148,7 @@ mod test {
141148

142149
// Reject expired version number
143150
SignedResponse::deserialize_and_verify_at_time(
144-
&VerifyingKey(verifying_key),
151+
&vec1![VerifyingKey(verifying_key)],
145152
include_bytes!("../../test-version-response.json"),
146153
chrono::DateTime::UNIX_EPOCH,
147154
usize::MAX,

mullvad-update/src/format/key.rs

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ impl Serialize for SecretKey {
7272

7373
/// ed25519 verifying key
7474
#[derive(Debug, PartialEq, Eq)]
75+
#[cfg_attr(test, derive(Clone))]
7576
pub struct VerifyingKey(pub ed25519_dalek::VerifyingKey);
7677

7778
impl VerifyingKey {

mullvad-update/src/format/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub struct SignedResponse {
3434
/// Helper type that leaves the signed data untouched
3535
/// Note that deserializing doesn't verify anything
3636
#[derive(Deserialize, Serialize)]
37+
#[cfg_attr(test, derive(Debug))]
3738
struct PartialSignedResponse {
3839
/// Signatures of the canonicalized JSON of `signed`
3940
pub signatures: Vec<ResponseSignature>,

mullvad-update/src/format/serializer.rs

+53-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ mod test {
7272
use super::*;
7373
use crate::format::deserializer::deserialize_and_verify;
7474
use serde_json::json;
75+
use vec1::vec1;
7576

7677
#[test]
7778
fn test_sign() -> anyhow::Result<()> {
@@ -95,7 +96,58 @@ mod test {
9596

9697
let bytes = serde_json::to_vec(&partial)?;
9798

98-
deserialize_and_verify(&pubkey, &bytes)?;
99+
deserialize_and_verify(&vec1![pubkey.clone()], &bytes)?;
100+
101+
// Verify that an irrelevant key is ignored
102+
let invalid_key = key::SecretKey::generate();
103+
let invalid_pubkey = invalid_key.pubkey();
104+
105+
deserialize_and_verify(&vec1![pubkey.clone(), invalid_pubkey.clone()], &bytes)?;
106+
107+
// Wrong public key only fails
108+
deserialize_and_verify(&vec1![invalid_pubkey], &bytes).unwrap_err();
109+
110+
Ok(())
111+
}
112+
113+
#[test]
114+
fn test_sign_multiple() -> anyhow::Result<()> {
115+
// Generate keys and data
116+
let key = key::SecretKey::generate();
117+
let pubkey = key.pubkey();
118+
119+
let key2 = key::SecretKey::generate();
120+
let pubkey2 = key2.pubkey();
121+
122+
let invalid_key = key::SecretKey::generate();
123+
let invalid_pubkey = invalid_key.pubkey();
124+
125+
let data = json!({
126+
"stuff": "I can prove that I wrote this"
127+
});
128+
129+
// Sign with two keys
130+
let mut partial = sign(&key, &data).context("Signing failed")?;
131+
let partial2 = sign(&key2, &data).context("Signing failed")?;
132+
partial.signatures.extend(partial2.signatures);
133+
134+
let bytes = serde_json::to_vec(&partial)?;
135+
136+
// Accept either (or both) keys
137+
deserialize_and_verify(&vec1![pubkey.clone(), pubkey2.clone()], &bytes)?;
138+
deserialize_and_verify(&vec1![pubkey2.clone()], &bytes)?;
139+
deserialize_and_verify(&vec1![pubkey.clone()], &bytes)?;
140+
141+
// Ignore irrelevant key
142+
deserialize_and_verify(
143+
&vec1![pubkey.clone(), pubkey2.clone(), invalid_pubkey.clone()],
144+
&bytes,
145+
)?;
146+
deserialize_and_verify(&vec1![pubkey2, invalid_pubkey.clone()], &bytes)?;
147+
deserialize_and_verify(&vec1![invalid_pubkey.clone(), pubkey], &bytes)?;
148+
149+
// Using wrong public key fails
150+
deserialize_and_verify(&vec1![invalid_pubkey], &bytes).unwrap_err();
99151

100152
Ok(())
101153
}

0 commit comments

Comments
 (0)