Skip to content

Commit d3768b9

Browse files
authored
Merge pull request #197 from freedomofpress/step5
protocol §5 key retrieval
2 parents cf8da6f + f4e28a1 commit d3768b9

File tree

9 files changed

+216
-140
lines changed

9 files changed

+216
-140
lines changed

docs/protocol.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@ the first sender.
375375

376376
### 5. Sender fetches keys and verifies their authenticity <!-- Figure 3(b) as of b1e4d41 -->
377377

378+
Senders must fetch recipient keys from the server. For each journalist, the server MUST select one key bundle at random and then delete it; a given key bundle MUST NOT be returned more than once.
379+
378380
Given:
379381

380382
| | Anyone |

securedrop-protocol/bench/src/submit.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ use rand_chacha::ChaCha20Rng;
33
use rand_core::SeedableRng;
44

55
use securedrop_protocol_minimal::{
6-
journalist::JournalistClient, keys::FPFKeyPair, messages::core::SourceJournalistKeyResponse,
7-
server::Server, source::SourceClient,
6+
journalist::JournalistClient, keys::FPFKeyPair, messages::core::KeyResponse, server::Server,
7+
source::SourceClient,
88
};
99

10-
fn setup_test_environment() -> (SourceClient, Vec<SourceJournalistKeyResponse>) {
10+
fn setup_test_environment() -> (SourceClient, Vec<KeyResponse>) {
1111
// 1. Create server with newsroom and journalist
1212
let mut server = Server::new();
1313

@@ -16,9 +16,6 @@ fn setup_test_environment() -> (SourceClient, Vec<SourceJournalistKeyResponse>)
1616
.create_newsroom_setup_request(ChaCha20Rng::seed_from_u64(666))
1717
.expect("Can create newsroom setup request");
1818

19-
// Store the newsroom verifying key before moving the request
20-
let newsroom_verifying_key = newsroom_setup_request.newsroom_verifying_key;
21-
2219
// Simulate FPF signing
2320
let fpf_keypair =
2421
FPFKeyPair::new(ChaCha20Rng::seed_from_u64(666)).expect("FPF key generation failed");
@@ -51,24 +48,24 @@ fn setup_test_environment() -> (SourceClient, Vec<SourceJournalistKeyResponse>)
5148
let mut source = SourceClient::from_passphrase(&[1u8; 32]);
5249

5350
// 6. Source fetches newsroom keys
54-
let newsroom_key_request = source.fetch_newsroom_keys();
55-
let newsroom_key_response = server.handle_source_newsroom_key_request(newsroom_key_request);
51+
let newsroom_key_request = source.newsroom_key_request();
52+
let newsroom_key_response = server.handle_newsroom_key_request(newsroom_key_request);
5653

5754
// Source handles and verifies the newsroom key response
5855
source
5956
.handle_newsroom_key_response(&newsroom_key_response, &fpf_keypair.verifying_key())
6057
.expect("Newsroom key response should be valid");
6158

6259
// 7. Source fetches journalist keys
63-
let journalist_key_request = source.fetch_journalist_keys();
64-
let journalist_key_responses = server.handle_source_journalist_key_request(
60+
let journalist_key_request = source.request_keys();
61+
let journalist_key_responses = server.handle_key_request(
6562
journalist_key_request,
6663
&mut ChaCha20Rng::seed_from_u64(666),
6764
);
6865

6966
// Source handles and verifies the journalist key response
7067
source
71-
.handle_journalist_key_response(&journalist_key_responses[0], &newsroom_verifying_key)
68+
.handle_key_response(&journalist_key_responses[0])
7269
.expect("Journalist key response should be valid");
7370

7471
(source, journalist_key_responses)

securedrop-protocol/protocol-minimal/src/api.rs

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//!
1111
//! Key verification follows a chain of trust:
1212
//! 1. The FPF signing key is a trust anchor (pre-distributed out of band).
13-
//! 2. The newsroom's verifying key is signed by FPF. (This is not yet verified by `handle_journalist_key_response()`.)
13+
//! 2. The newsroom's verifying key is signed by FPF and stored by [`Api::handle_newsroom_key_response`].
1414
//! 3. Each journalist's signing key is signed by the newsroom.
1515
//! 4. Each journalist's key bundles are self-signed.
1616
@@ -20,8 +20,8 @@ use crate::{
2020
encrypt_decrypt::{encrypt, solve_fetch_challenges},
2121
messages::{
2222
core::{
23-
MessageChallengeFetchRequest, MessageFetchRequest, SourceJournalistKeyRequest,
24-
SourceJournalistKeyResponse, SourceNewsroomKeyRequest, SourceNewsroomKeyResponse,
23+
KeyRequest, KeyResponse, MessageChallengeFetchRequest, MessageFetchRequest,
24+
NewsroomKeyRequest, NewsroomKeyResponse,
2525
},
2626
setup::{JournalistEphemeralKeyRequest, JournalistSetupRequest},
2727
},
@@ -38,26 +38,29 @@ use uuid::Uuid;
3838
/// [`set_newsroom_verifying_key`](Api::set_newsroom_verifying_key).
3939
/// All other methods have default implementations.
4040
pub trait Api {
41-
/// Returns the stored newsroom verifying key, if one has been verified.
41+
/// Returns the stored newsroom verifying key.
4242
fn newsroom_verifying_key(&self) -> Option<&VerifyingKey>;
4343

44-
/// Stores a verified newsroom verifying key.
44+
/// Stores a newsroom verifying key.
45+
///
46+
/// The caller is responsible for ensuring the key has been verified before
47+
/// storing it. Prefer [`handle_newsroom_key_response`](Api::handle_newsroom_key_response),
48+
/// which verifies the FPF signature before storing.
4549
fn set_newsroom_verifying_key(&mut self, key: VerifyingKey);
4650

47-
/// Creates a request to fetch the newsroom's public keys from the server.
51+
/// Creates a `NewsroomKeyRequest` to fetch the newsroom's public keys from the server.
4852
///
4953
/// This is the first part of step 5 in the protocol spec.
50-
fn fetch_newsroom_keys(&self) -> SourceNewsroomKeyRequest {
51-
SourceNewsroomKeyRequest {}
54+
fn newsroom_key_request(&self) -> NewsroomKeyRequest {
55+
NewsroomKeyRequest {}
5256
}
5357

54-
/// Creates a request to fetch journalist public keys from the server.
58+
/// Creates a `RequestKeys` request (step 5 in the spec).
5559
///
56-
/// This is the second part of step 5 in the protocol spec. The server
57-
/// responds with long-term keys and a one-time ephemeral key bundle
58-
/// for each available journalist.
59-
fn fetch_journalist_keys(&self) -> SourceJournalistKeyRequest {
60-
SourceJournalistKeyRequest {}
60+
/// The server responds with long-term keys and a one-time ephemeral key
61+
/// bundle for each available journalist.
62+
fn request_keys(&self) -> KeyRequest {
63+
KeyRequest {}
6164
}
6265

6366
/// Creates a request to fetch encrypted message IDs from the server.
@@ -133,15 +136,15 @@ pub trait Api {
133136
/// Returns an error if the FPF signature is invalid.
134137
fn handle_newsroom_key_response(
135138
&mut self,
136-
response: &SourceNewsroomKeyResponse,
139+
response: &NewsroomKeyResponse,
137140
fpf_verifying_key: &VerifyingKey,
138141
) -> Result<(), Error> {
139-
let newsroom_vk_bytes = response.newsroom_verifying_key.into_bytes();
142+
let newsroom_vk_bytes = response.newsroom_verifying_key().into_bytes();
140143
fpf_verifying_key
141-
.verify(&newsroom_vk_bytes, &response.fpf_sig)
144+
.verify(&newsroom_vk_bytes, response.fpf_sig())
142145
.map_err(|_| anyhow::anyhow!("invalid FPF signature on newsroom verifying key"))?;
143146

144-
self.set_newsroom_verifying_key(response.newsroom_verifying_key);
147+
self.set_newsroom_verifying_key(*response.newsroom_verifying_key());
145148
Ok(())
146149
}
147150

@@ -154,32 +157,36 @@ pub trait Api {
154157
///
155158
/// # Errors
156159
///
157-
/// Returns an error if any signature check fails.
158-
fn handle_journalist_key_response(
159-
&self,
160-
response: &SourceJournalistKeyResponse,
161-
newsroom_verifying_key: &VerifyingKey,
162-
) -> Result<(), Error> {
160+
/// Returns an error if:
161+
/// - The newsroom verifying key has not been set (call [`handle_newsroom_key_response`](Api::handle_newsroom_key_response) first).
162+
/// - Any of the three signature checks fail.
163+
fn handle_key_response(&self, response: &KeyResponse) -> Result<(), Error> {
164+
let newsroom_verifying_key = self.newsroom_verifying_key().ok_or_else(|| {
165+
anyhow::anyhow!(
166+
"newsroom verifying key not set; call handle_newsroom_key_response first"
167+
)
168+
})?;
169+
163170
// 1. Verify newsroom signature on journalist's verifying key.
164171
newsroom_verifying_key
165172
.verify(
166-
&response.journalist.verifying_key().into_bytes(),
167-
&response.nr_signature,
173+
&response.journalist().verifying_key().into_bytes(),
174+
response.nr_signature(),
168175
)
169176
.map_err(|_| anyhow::anyhow!("invalid newsroom signature on journalist signing key"))?;
170177

171178
// 2. Verify journalist's self-signature on long-term key bundle.
172-
let vk = response.journalist.verifying_key();
179+
let vk = response.journalist().verifying_key();
173180
vk.verify(
174-
response.journalist.signed_keybytes().as_bytes(),
175-
response.journalist.self_signature(),
181+
response.journalist().signed_keybytes().as_bytes(),
182+
response.journalist().self_signature(),
176183
)
177184
.map_err(|_| anyhow::anyhow!("invalid journalist self-signature on long-term keys"))?;
178185

179186
// 3. Verify journalist's self-signature on one-time ephemeral key bundle.
180187
vk.verify(
181-
&response.journalist.ephemeral_bundle().as_bytes(),
182-
response.journalist.ephemeral_signature(),
188+
&response.journalist().ephemeral_bundle().as_bytes(),
189+
response.journalist().ephemeral_signature(),
183190
)
184191
.map_err(|_| anyhow::anyhow!("invalid journalist self-signature on one-time keys"))?;
185192

securedrop-protocol/protocol-minimal/src/journalist.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,20 @@ pub struct Journalist {
3030
fetch_key: DhFetchKeyPair,
3131
message_keys: Vec<SignedMessageKeyBundle>,
3232
reply_key: DhAkemKeyPair,
33+
reply_mlkem: MlKem768KeyPair,
3334
self_signature: Signature<JournalistLongTermKey>,
3435
signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
3536
session_storage: SessionStorage,
3637
}
3738

3839
// Public-facing representation of a journalist
3940
// used to send them a message
41+
#[derive(Debug)]
4042
pub struct JournalistPublicView {
4143
vk: VerifyingKey,
4244
fetch_pk: DHPublicKey,
4345
dhakem_pk_reply: DhAkemPublicKey,
46+
mlkem_pk_reply: MLKEM768PublicKey,
4447
signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
4548
selfsig: Signature<JournalistLongTermKey>,
4649
kb: SignedKeyBundlePublic,
@@ -51,6 +54,7 @@ impl JournalistPublicView {
5154
vk: VerifyingKey,
5255
fetch: DHPublicKey,
5356
dhakem: DhAkemPublicKey,
57+
mlkem: MLKEM768PublicKey,
5458
selfsig: Signature<JournalistLongTermKey>,
5559
signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
5660
kb: SignedKeyBundlePublic,
@@ -59,6 +63,7 @@ impl JournalistPublicView {
5963
vk,
6064
fetch_pk: fetch,
6165
dhakem_pk_reply: dhakem,
66+
mlkem_pk_reply: mlkem,
6267
selfsig,
6368
signed_longterm_key_bytes,
6469
kb,
@@ -166,6 +171,7 @@ impl Enrollable for Journalist {
166171
self.signing_key.pk,
167172
self.fetch_key.pk.clone(),
168173
self.reply_key.pk.clone(),
174+
self.reply_mlkem.pk.clone(),
169175
),
170176
}
171177
}
@@ -194,8 +200,13 @@ impl Journalist {
194200
let (sk_reply, pk_reply) =
195201
generate_dh_akem_keypair(&mut *rng).expect("DH-AKEM Keygen (Reply) failed");
196202

203+
let (sk_reply_mlkem, pk_reply_mlkem) =
204+
generate_mlkem768_keypair(&mut *rng).expect("ML-KEM Keygen (Reply) failed");
205+
197206
// Self-sign long-term pubkeys (for enrollment).
198-
let selfsigned_pubkeys = SignedLongtermPubKeyBytes::from_keys(&pk_reply, &pk_fetch);
207+
// pk_J^APKE = pk_J^APKE(DHKEM) || pk_J^APKE(ML-KEM) per spec key table.
208+
let selfsigned_pubkeys =
209+
SignedLongtermPubKeyBytes::from_keys(&pk_reply, &pk_reply_mlkem, &pk_fetch);
199210
let self_signature: Signature<JournalistLongTermKey> =
200211
signing_key.sign(selfsigned_pubkeys.as_bytes());
201212

@@ -250,6 +261,10 @@ impl Journalist {
250261
sk: sk_reply,
251262
pk: pk_reply,
252263
},
264+
reply_mlkem: KeyPair {
265+
sk: sk_reply_mlkem,
266+
pk: pk_reply_mlkem,
267+
},
253268
message_keys: key_bundles,
254269
self_signature,
255270
signed_longterm_key_bytes: selfsigned_pubkeys,
@@ -263,6 +278,7 @@ impl Journalist {
263278
self.signing_key.pk,
264279
self.fetch_key.pk.clone(),
265280
self.reply_key.pk.clone(),
281+
self.reply_mlkem.pk.clone(),
266282
self.self_signature,
267283
self.signed_longterm_key_bytes.clone(),
268284
(kb.bundle.public(), kb.selfsig),

securedrop-protocol/protocol-minimal/src/keys.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,25 @@ pub(crate) struct SignedMessageKeyBundle {
107107
}
108108

109109
#[derive(Debug, Clone)]
110-
pub struct SignedLongtermPubKeyBytes(pub [u8; LEN_DHKEM_ENCAPS_KEY + LEN_DH_ITEM]);
110+
pub struct SignedLongtermPubKeyBytes(
111+
pub [u8; LEN_DHKEM_ENCAPS_KEY + LEN_MLKEM_ENCAPS_KEY + LEN_DH_ITEM],
112+
);
111113

112114
impl SignedLongtermPubKeyBytes {
113115
/// Serialize long-term public keys into the canonical byte encoding.
114116
///
115-
/// Byte layout (per spec §3.1): `pk_J^APKE || pk_J^fetch`
116-
pub(crate) fn from_keys(reply_dhakem: &DhAkemPublicKey, fetch_pk: &DHPublicKey) -> Self {
117-
let mut pubkey_bytes = [0u8; LEN_DHKEM_ENCAPS_KEY + LEN_DH_ITEM];
117+
/// Byte layout (per spec §3.1): `pk_J^APKE(DHKEM) || pk_J^APKE(ML-KEM) || pk_J^fetch`
118+
pub(crate) fn from_keys(
119+
reply_dhakem: &DhAkemPublicKey,
120+
reply_mlkem: &MLKEM768PublicKey,
121+
fetch_pk: &DHPublicKey,
122+
) -> Self {
123+
let mut pubkey_bytes = [0u8; LEN_DHKEM_ENCAPS_KEY + LEN_MLKEM_ENCAPS_KEY + LEN_DH_ITEM];
118124
pubkey_bytes[0..LEN_DHKEM_ENCAPS_KEY].copy_from_slice(reply_dhakem.as_bytes());
119-
pubkey_bytes[LEN_DHKEM_ENCAPS_KEY..].copy_from_slice(&fetch_pk.into_bytes());
125+
pubkey_bytes[LEN_DHKEM_ENCAPS_KEY..LEN_DHKEM_ENCAPS_KEY + LEN_MLKEM_ENCAPS_KEY]
126+
.copy_from_slice(reply_mlkem.as_bytes());
127+
pubkey_bytes[LEN_DHKEM_ENCAPS_KEY + LEN_MLKEM_ENCAPS_KEY..]
128+
.copy_from_slice(&fetch_pk.into_bytes());
120129

121130
Self(pubkey_bytes)
122131
}
@@ -131,7 +140,12 @@ impl SignedLongtermPubKeyBytes {
131140
pub struct Enrollment {
132141
pub bundle: SignedLongtermPubKeyBytes,
133142
pub selfsig: Signature<JournalistLongTermKey>,
134-
pub keys: (VerifyingKey, DHPublicKey, DhAkemPublicKey),
143+
pub keys: (
144+
VerifyingKey,
145+
DHPublicKey,
146+
DhAkemPublicKey,
147+
MLKEM768PublicKey,
148+
),
135149
}
136150

137151
// in memory session storage

0 commit comments

Comments
 (0)