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}