zoe_wire_protocol/challenge.rs
1use crate::{Signature, VerifyingKey};
2use forward_compatible_enum::ForwardCompatibleEnum;
3use serde::{Deserialize, Serialize};
4
5#[cfg(feature = "server")]
6pub mod server;
7
8#[cfg(feature = "client")]
9pub mod client;
10
11/// Default challenge timeout in seconds
12#[cfg(feature = "server")]
13const DEFAULT_CHALLENGE_TIMEOUT_SECS: u64 = 30;
14
15/// Maximum size for challenge messages (to prevent DoS)
16#[cfg(any(feature = "client", feature = "server"))]
17const MAX_PACKAGE_SIZE: usize = 1024 * 1024; // Should be enough for challenge data
18
19/// Forward-compatible challenge system for connection-level authentication
20///
21/// The Zoe protocol uses a challenge-response handshake immediately after QUIC connection
22/// establishment to verify possession of cryptographic private keys. This happens before
23/// any service streams are created, ensuring all connections have verified credentials.
24///
25/// ## Protocol Flow
26///
27/// 1. **QUIC Connection**: Client connects using ML-DSA-44 mutual TLS
28/// 2. **Challenge Phase**: Server sends `ZoeChallenge` on first bi-directional stream
29/// 3. **Response Phase**: Client responds with `ZoeChallengeResponse`
30/// 4. **Verification**: Server verifies proofs and sends `ZoeChallengeResult`
31/// 5. **Service Phase**: Normal service streams can now be established
32///
33/// ## Wire Format
34///
35/// All challenge messages are serialized using postcard format for compact binary encoding.
36///
37/// ### Challenge Message (Server → Client)
38/// ```text
39/// | Field | Type | Description |
40/// |---------------------|-------------------|--------------------------------|
41/// | challenge_type | u8 | Forward-compatible enum tag |
42/// | challenge_data | Vec<u8> | Serialized challenge content |
43/// ```
44///
45/// ### Response Message (Client → Server)
46/// ```text
47/// | Field | Type | Description |
48/// |---------------------|-------------------|--------------------------------|
49/// | response_type | u8 | Forward-compatible enum tag |
50/// | response_data | Vec<u8> | Serialized response content |
51/// ```
52///
53/// ### Result Message (Server → Client)
54/// ```text
55/// | Field | Type | Description |
56/// |---------------------|-------------------|--------------------------------|
57/// | result_type | u8 | Forward-compatible enum tag |
58/// | result_data | Vec<u8> | Serialized result content |
59/// ```
60///
61/// ## Security Properties
62///
63/// - **Replay Protection**: Each challenge includes a unique nonce
64/// - **Server Binding**: Signatures include server's public key
65/// - **Time Bounds**: Challenges have expiration timestamps
66/// - **Connection Scoped**: Verified keys are tied to specific QUIC connections
67/// - **Forward Secrecy**: New challenges generated for each connection
68///
69/// ## Example Usage
70///
71/// ```rust
72/// use zoe_wire_protocol::{ZoeChallenge, ZoeChallengeResponse, KeyChallenge};
73///
74/// // Server sends challenge
75/// let challenge = ZoeChallenge::Key(KeyChallenge {
76/// nonce: generate_nonce(),
77/// server_public_key: server_key.to_bytes().to_vec(),
78/// expires_at: current_time() + 30,
79/// });
80///
81/// // Client creates multiple key proofs
82/// let response = ZoeChallengeResponse::Key(KeyResponse {
83/// key_proofs: vec![
84/// KeyProof { public_key: key1_bytes, signature: sig1_bytes },
85/// KeyProof { public_key: key2_bytes, signature: sig2_bytes },
86/// ],
87/// });
88/// ```
89#[derive(Debug, Clone, ForwardCompatibleEnum)]
90pub enum ZoeChallenge {
91 /// Multi-key ML-DSA challenge allowing clients to prove multiple private keys
92 ///
93 /// This challenge type allows clients to prove possession of multiple ML-DSA
94 /// private keys in a single handshake round-trip. This is useful for:
95 ///
96 /// - **Role-based authentication**: Different keys for personal, work, admin roles
97 /// - **Key rotation**: Proving both old and new keys during transition periods
98 /// - **Delegation**: Proving keys for multiple identities or organizations
99 /// - **Batch verification**: Efficient verification of multiple keys at once
100 ///
101 /// The client must sign `(nonce || server_public_key)` with each private key
102 /// they wish to prove possession of.
103 #[discriminant(1)]
104 Key(Box<KeyChallenge>),
105
106 /// Unknown challenge type for forward compatibility
107 Unknown { discriminant: u32, data: Vec<u8> },
108}
109
110#[derive(Debug, Clone, ForwardCompatibleEnum)]
111pub enum ZoeChallengeRejection {
112 /// The challenge has been rejected. The connection will be closed. A message
113 /// might be provided to explain why and what to do before trying again.
114 #[discriminant(30)]
115 GenericRejection(Option<String>),
116
117 /// The client failed to respond to the challenge with the valied answer.
118 #[discriminant(31)]
119 ChallengeFailed,
120
121 /// The client failed to respond to the challenge on time
122 #[discriminant(32)]
123 ChallengeExpired,
124
125 /// The client failed to respond to the challenge with a valid complete answer.
126 #[discriminant(33)]
127 ChallengeIncomplete,
128
129 /// The client has been blocked by the server. Any fruther requests from this
130 /// client will be rejected.
131 #[discriminant(40)]
132 Blocked,
133
134 /// Unknown rejection type for forward compatibility
135 Unknown { discriminant: u32, data: Vec<u8> },
136}
137
138#[derive(Debug, Clone, ForwardCompatibleEnum)]
139pub enum ZoeChallengeWarning {
140 /// The connection is throttled, a reason to display to the user might be provided
141 #[discriminant(20)]
142 ConnectionThrottled(String),
143
144 /// The user is close to the quota limit on this server. This should probably be
145 /// prompted to the user as uploading data might be rejected by the server.
146 #[discriminant(33)]
147 QuotaLimitInReach(String),
148
149 /// The user is out of quota on this server. This should probably be prompted
150 /// to the user as uploading data might be rejected by the server.
151 #[discriminant(34)]
152 OutOfQuota(String),
153
154 /// The user has reached the limits (other than quota) on this server. This
155 /// should probably be prompted to the user as uploading data might be rejected
156 /// by the server.
157 #[discriminant(35)]
158 UserHasLimitedResource(String),
159
160 /// The best ngeotiated protocol version is deprecated. The client should
161 /// prompt the user about this server soon not supporting this client anymore
162 /// and thus the client needs to be upgraded.
163 #[discriminant(41)]
164 ProtocolVersionDeprecated(String),
165
166 /// The server has detected a potential breach of the security policy. The
167 /// client should prompt the user about this and ask for confirmation to
168 /// continue.
169 #[discriminant(42)]
170 PotentialBreach(String),
171
172 /// The server has detected a weak crypto algorithm being used. The client
173 /// should prompt the user about this and ask for confirmation to continue.
174 #[discriminant(43)]
175 WeakCrypto(String),
176
177 /// THe server has a generic warning to show to the user. This should be shown
178 /// to the user by the client.
179 #[discriminant(50)]
180 GenericServerWarning(String),
181
182 /// The server is under high load and might be throttling the user connection,
183 #[discriminant(51)]
184 ServerUnderLoad(String),
185
186 /// Unknown warning type for forward compatibility
187 Unknown { discriminant: u32, data: Vec<u8> },
188}
189
190/// Forward-compatible result system for challenge verification
191///
192/// After verifying challenge responses, the server sends back results indicating
193/// which proofs succeeded or failed. This allows clients to understand the
194/// verification status and take appropriate action.
195#[derive(Debug, Clone, ForwardCompatibleEnum)]
196pub enum ZoeChallengeResult {
197 /// The challanges have been accepted.
198 #[discriminant(20)]
199 Accepted,
200
201 /// This challenge has been accepted, but there is another challenge to come
202 /// and perform. After this read for another ZoeChallenge.
203 #[discriminant(30)]
204 Next,
205
206 /// The server wants to warn the client and potentially the user about a
207 /// possible issue. The client should consider the warning and maybe
208 /// inform the user about it. But other than that it should continue by
209 /// waiting for another result. Can be sent multiple times for different
210 /// problems.
211 #[discriminant(31)]
212 Warning(ZoeChallengeWarning),
213
214 /// The challenge has been rejected The connection will be closed. A message
215 /// might be provided to explain why and what to do before trying again.
216 #[discriminant(40)]
217 Rejected(ZoeChallengeRejection),
218
219 /// An error occured (on the server side). The connection will be closed.
220 /// The error should probably be shown to the user to allow them to figure
221 /// out what went wrong before trying again.
222 #[discriminant(50)]
223 Error(String),
224
225 /// Unknown result type for forward compatibility
226 Unknown { discriminant: u32, data: Vec<u8> },
227}
228
229/// Challenge for proving possession of multiple ML-DSA private keys
230///
231/// This challenge is sent by the server immediately after QUIC connection
232/// establishment. The client must respond by proving possession of one or
233/// more ML-DSA private keys.
234///
235/// ## Security Considerations
236///
237/// - **Nonce**: Must be cryptographically random and unique per challenge
238/// - **Server Key**: Binds the signature to this specific server
239/// - **Expiration**: Prevents replay attacks and limits challenge lifetime
240/// - **Key Encoding**: ML-DSA public keys should use the standard encoding
241///
242/// ## Wire Size
243///
244/// Approximate serialized size: ~80 bytes
245/// - nonce: 32 bytes
246/// - server_public_key: ~1312 bytes (ML-DSA-44)
247/// - expires_at: 8 bytes
248/// - overhead: ~8 bytes (postcard encoding)
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct KeyChallenge {
251 /// Cryptographically random nonce that must be included in signatures
252 ///
253 /// This 32-byte nonce provides replay protection by ensuring each challenge
254 /// is unique. Clients must include this exact nonce when constructing
255 /// their signature data.
256 pub nonce: [u8; 32],
257
258 /// Server's ML-DSA-44 public key that must be included in signatures
259 ///
260 /// Including the server's public key in the signature data prevents
261 /// signature replay attacks across different servers. This should be
262 /// the same ML-DSA-44 key used in the server's TLS certificate.
263 pub signature: Signature,
264
265 /// Unix timestamp when this challenge expires
266 ///
267 /// Challenges have a limited lifetime (typically 30-60 seconds) to prevent
268 /// replay attacks. Clients must respond before this timestamp or the
269 /// challenge will be rejected.
270 pub expires_at: u64,
271}
272
273/// Response containing proofs of ML-DSA private key possession
274///
275/// The client responds to an `KeyChallenge` by providing one or more
276/// key proofs. Each proof demonstrates possession of a specific ML-DSA private key.
277///
278/// ## Proof Construction
279///
280/// For each key the client wishes to prove:
281///
282/// 1. Construct signature data: `nonce || server_public_key`
283/// 2. Sign the data using the ML-DSA private key
284/// 3. Create a `KeyProof` with the public key and signature
285///
286/// ## Wire Size
287///
288/// Approximate serialized size per key proof: ~2500 bytes
289/// - public_key: ~1312 bytes (ML-DSA-65 public key)
290/// - signature: ~2420 bytes (ML-DSA-65 signature)
291/// - overhead: ~8 bytes (postcard encoding)
292///
293/// Total message size scales linearly with number of keys being proven.
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct KeyResponse {
296 /// List of key proofs - one for each ML-DSA key being proven
297 ///
298 /// The client can prove multiple keys in a single response. Each proof
299 /// is verified independently by the server. At least one proof must
300 /// succeed for the handshake to complete successfully.
301 ///
302 /// ## Ordering
303 ///
304 /// The order of proofs in this vector corresponds to the indices used
305 /// in `KeyResult.failed_indices` for error reporting.
306 pub key_proofs: Vec<KeyProof>,
307}
308
309/// Cryptographic proof of ML-DSA private key possession
310///
311/// Each proof consists of a public key and a signature that demonstrates
312/// the client possesses the corresponding private key. The signature is
313/// computed over challenge-specific data to prevent replay attacks.
314///
315/// ## Verification Process
316///
317/// The server verifies each proof by:
318///
319/// 1. Decoding the ML-DSA public key from `public_key`
320/// 2. Reconstructing signature data: `nonce || server_public_key`
321/// 3. Verifying the signature using the public key and signature data
322/// 4. Adding successfully verified keys to the connection's verified set
323///
324/// ## Key Encoding
325///
326/// ML-DSA public keys must be encoded using the standard ML-DSA encoding:
327/// - ML-DSA-44: 1312 bytes
328/// - ML-DSA-65: 1952 bytes
329/// - ML-DSA-87: 2592 bytes
330///
331/// This implementation uses ML-DSA-65 (security level 3, ~192-bit security).
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct KeyProof {
334 /// Encoded ML-DSA public key being proven
335 ///
336 /// This should be the result of calling `verifying_key.encode()` on
337 /// an ML-DSA verifying key. The encoding includes all necessary
338 /// information to reconstruct the public key for verification.
339 pub public_key: VerifyingKey,
340
341 /// ML-DSA signature over (nonce || server_public_key)
342 ///
343 /// This signature proves possession of the private key corresponding
344 /// to `public_key`. It must be computed over the exact concatenation
345 /// of the challenge nonce and server public key.
346 ///
347 /// Signature sizes:
348 /// - ML-DSA-44: ~2420 bytes
349 /// - ML-DSA-65: ~3309 bytes
350 /// - ML-DSA-87: ~4627 bytes
351 pub signature: Signature,
352}
353
354/// Result of ML-DSA multi-key challenge verification
355///
356/// After verifying all key proofs in a response, the server sends back
357/// this result indicating which proofs succeeded or failed. This allows
358/// the client to understand their verification status.
359///
360/// ## Success Criteria
361///
362/// The handshake is considered successful if at least one key proof is valid.
363/// Even if some proofs fail, the connection can continue with the successfully
364/// verified keys.
365///
366/// ## Error Handling
367///
368/// If all proofs fail, the server should close the connection. Clients can
369/// use the failure information to:
370/// - Log which specific keys were rejected
371/// - Retry the connection with different keys
372/// - Debug key or signature generation issues
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub enum KeyResult {
375 /// All key proofs were successfully verified
376 ///
377 /// This is the ideal case where every key the client attempted to prove
378 /// was successfully verified. All provided keys are now available for
379 /// use in message authentication on this connection.
380 AllValid,
381
382 /// Some key proofs failed verification
383 ///
384 /// Contains the indices (into the original `key_proofs` vector) of proofs
385 /// that failed verification. The connection continues with the successfully
386 /// verified keys.
387 ///
388 /// Common failure reasons:
389 /// - Invalid signature (wrong private key used)
390 /// - Malformed public key encoding
391 /// - Signature over wrong data (incorrect nonce/server key)
392 /// - Expired challenge (client took too long to respond)
393 PartialFailure {
394 /// Zero-based indices of failed key proofs
395 ///
396 /// These indices correspond to positions in the original
397 /// `KeyResponse.key_proofs` vector that failed verification.
398 failed_indices: Vec<usize>,
399 },
400
401 /// All key proofs failed verification
402 ///
403 /// No keys were successfully verified. The server will typically close
404 /// the connection after sending this result. Clients should not attempt
405 /// to establish service streams after receiving this result.
406 AllFailed,
407}
408
409impl KeyResult {
410 /// Check if the handshake was successful (at least one key verified)
411 ///
412 /// Returns `true` if at least one key was successfully verified,
413 /// `false` if all keys failed verification.
414 ///
415 /// # Example
416 ///
417 /// ```rust
418 /// use zoe_wire_protocol::KeyResult;
419 ///
420 /// let result = KeyResult::PartialFailure {
421 /// failed_indices: vec![1, 3]
422 /// };
423 /// assert!(result.is_successful());
424 ///
425 /// let result = KeyResult::AllFailed;
426 /// assert!(!result.is_successful());
427 /// ```
428 pub fn is_successful(&self) -> bool {
429 !matches!(self, KeyResult::AllFailed)
430 }
431
432 /// Get the number of failed key proofs
433 ///
434 /// Returns the count of key proofs that failed verification.
435 /// For `AllValid`, returns 0. For `AllFailed`, the count depends
436 /// on how many keys were originally submitted.
437 ///
438 /// # Parameters
439 ///
440 /// * `total_keys` - Total number of keys that were submitted (needed for AllFailed case)
441 ///
442 /// # Example
443 ///
444 /// ```rust
445 /// use zoe_wire_protocol::KeyResult;
446 ///
447 /// let result = KeyResult::PartialFailure {
448 /// failed_indices: vec![1, 3]
449 /// };
450 /// assert_eq!(result.failed_count(5), 2);
451 ///
452 /// let result = KeyResult::AllFailed;
453 /// assert_eq!(result.failed_count(3), 3);
454 /// ```
455 pub fn failed_count(&self, total_keys: usize) -> usize {
456 match self {
457 KeyResult::AllValid => 0,
458 KeyResult::PartialFailure { failed_indices } => failed_indices.len(),
459 KeyResult::AllFailed => total_keys,
460 }
461 }
462
463 /// Get the number of successfully verified keys
464 ///
465 /// Returns the count of key proofs that passed verification.
466 ///
467 /// # Parameters
468 ///
469 /// * `total_keys` - Total number of keys that were submitted
470 ///
471 /// # Example
472 ///
473 /// ```rust
474 /// use zoe_wire_protocol::KeyResult;
475 ///
476 /// let result = KeyResult::PartialFailure {
477 /// failed_indices: vec![1]
478 /// };
479 /// assert_eq!(result.success_count(3), 2);
480 ///
481 /// let result = KeyResult::AllValid;
482 /// assert_eq!(result.success_count(5), 5);
483 /// ```
484 pub fn success_count(&self, total_keys: usize) -> usize {
485 total_keys - self.failed_count(total_keys)
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::ConnectionInfo;
493 use std::collections::HashSet;
494
495 #[test]
496 fn test_ml_dsa_result_success_check() {
497 assert!(KeyResult::AllValid.is_successful());
498 assert!(KeyResult::PartialFailure {
499 failed_indices: vec![1]
500 }
501 .is_successful());
502 assert!(!KeyResult::AllFailed.is_successful());
503 }
504
505 #[test]
506 fn test_ml_dsa_result_counts() {
507 let result = KeyResult::PartialFailure {
508 failed_indices: vec![0, 2],
509 };
510 assert_eq!(result.failed_count(5), 2);
511 assert_eq!(result.success_count(5), 3);
512
513 let result = KeyResult::AllValid;
514 assert_eq!(result.failed_count(3), 0);
515 assert_eq!(result.success_count(3), 3);
516
517 let result = KeyResult::AllFailed;
518 assert_eq!(result.failed_count(4), 4);
519 assert_eq!(result.success_count(4), 0);
520 }
521
522 #[test]
523 fn test_connection_info_key_verification() {
524 let keypair1 = {
525 use crate::KeyPair;
526 KeyPair::generate_ed25519(&mut rand::thread_rng())
527 };
528 let keypair2 = {
529 use crate::KeyPair;
530 KeyPair::generate_ml_dsa65(&mut rand::thread_rng())
531 };
532
533 let mut verified_keys = HashSet::new();
534 verified_keys.insert(keypair1.public_key());
535 verified_keys.insert(keypair2.public_key());
536
537 let transport_key = {
538 use crate::KeyPair;
539 KeyPair::generate(&mut rand::thread_rng()).public_key()
540 };
541 let connection_info = ConnectionInfo::with_verified_keys(
542 transport_key,
543 verified_keys,
544 "127.0.0.1:8080".parse().unwrap(),
545 );
546
547 assert!(connection_info.has_verified_key(&keypair1.public_key()));
548 assert!(connection_info.has_verified_key(&keypair2.public_key()));
549
550 assert_eq!(connection_info.verified_key_count(), 2);
551 }
552}