Skip to main content

peat_protocol/security/
authenticator.rs

1//! Device authenticator for challenge-response authentication.
2
3use super::device_id::DeviceId;
4use super::error::SecurityError;
5use super::keypair::DeviceKeypair;
6use super::{CHALLENGE_NONCE_SIZE, DEFAULT_CHALLENGE_TIMEOUT_SECS};
7use peat_schema::security::v1::{Challenge, SignedChallengeResponse};
8use rand_core::{OsRng, RngCore};
9use std::collections::HashMap;
10use std::sync::RwLock;
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13/// Device authenticator manages challenge-response authentication.
14///
15/// # Overview
16///
17/// The authenticator uses Ed25519 signatures for mutual authentication:
18/// 1. Generate a challenge with random nonce and timestamp
19/// 2. Peer signs the challenge and returns their public key
20/// 3. Verify signature and cache the verified peer identity
21///
22/// # Example
23///
24/// ```ignore
25/// use peat_protocol::security::{DeviceKeypair, DeviceAuthenticator};
26///
27/// let keypair = DeviceKeypair::generate();
28/// let authenticator = DeviceAuthenticator::new(keypair);
29///
30/// // Generate challenge for peer
31/// let challenge = authenticator.generate_challenge();
32///
33/// // Peer creates response
34/// let response = peer_authenticator.respond_to_challenge(&challenge)?;
35///
36/// // Verify response
37/// let peer_id = authenticator.verify_response(&response)?;
38/// println!("Authenticated peer: {}", peer_id);
39/// ```
40pub struct DeviceAuthenticator {
41    /// This device's keypair
42    keypair: DeviceKeypair,
43
44    /// Verified peers cache
45    verified_peers: RwLock<HashMap<DeviceId, VerifiedPeer>>,
46
47    /// Challenge timeout duration
48    challenge_timeout: Duration,
49}
50
51/// A verified peer's identity
52#[derive(Debug, Clone)]
53pub struct VerifiedPeer {
54    /// The peer's device ID
55    pub device_id: DeviceId,
56
57    /// The peer's public key bytes
58    pub public_key: [u8; 32],
59
60    /// When this peer was verified
61    pub verified_at: SystemTime,
62}
63
64impl DeviceAuthenticator {
65    /// Create a new authenticator with the given keypair.
66    pub fn new(keypair: DeviceKeypair) -> Self {
67        Self::with_timeout(keypair, Duration::from_secs(DEFAULT_CHALLENGE_TIMEOUT_SECS))
68    }
69
70    /// Create an authenticator with a custom challenge timeout.
71    pub fn with_timeout(keypair: DeviceKeypair, challenge_timeout: Duration) -> Self {
72        DeviceAuthenticator {
73            keypair,
74            verified_peers: RwLock::new(HashMap::new()),
75            challenge_timeout,
76        }
77    }
78
79    /// Get this device's ID.
80    pub fn device_id(&self) -> DeviceId {
81        self.keypair.device_id()
82    }
83
84    /// Get this device's public key bytes.
85    pub fn public_key_bytes(&self) -> [u8; 32] {
86        self.keypair.public_key_bytes()
87    }
88
89    /// Generate a challenge for authenticating a peer.
90    ///
91    /// The challenge contains:
92    /// - Random 32-byte nonce
93    /// - Current timestamp
94    /// - This device's ID
95    /// - Expiration timestamp
96    pub fn generate_challenge(&self) -> Challenge {
97        let mut nonce = [0u8; CHALLENGE_NONCE_SIZE];
98        OsRng.fill_bytes(&mut nonce);
99
100        let now = SystemTime::now()
101            .duration_since(UNIX_EPOCH)
102            .unwrap_or_default();
103
104        let expires = now + self.challenge_timeout;
105
106        Challenge {
107            nonce: nonce.to_vec(),
108            timestamp: Some(peat_schema::common::v1::Timestamp {
109                seconds: now.as_secs(),
110                nanos: now.subsec_nanos(),
111            }),
112            challenger_id: self.device_id().to_hex(),
113            expires_at: Some(peat_schema::common::v1::Timestamp {
114                seconds: expires.as_secs(),
115                nanos: expires.subsec_nanos(),
116            }),
117        }
118    }
119
120    /// Create a signed response to a challenge.
121    ///
122    /// Signs the challenge data with this device's private key.
123    pub fn respond_to_challenge(
124        &self,
125        challenge: &Challenge,
126    ) -> Result<SignedChallengeResponse, SecurityError> {
127        // Check challenge hasn't expired
128        self.check_challenge_expiry(challenge)?;
129
130        // Create message to sign: nonce || challenger_id || timestamp
131        let message = self.create_challenge_message(challenge);
132
133        // Sign the message
134        let signature = self.keypair.sign(&message);
135
136        Ok(SignedChallengeResponse {
137            challenge_nonce: challenge.nonce.clone(),
138            public_key: self.keypair.public_key_bytes().to_vec(),
139            signature: signature.to_bytes().to_vec(),
140            timestamp: Some(peat_schema::common::v1::Timestamp {
141                seconds: SystemTime::now()
142                    .duration_since(UNIX_EPOCH)
143                    .unwrap_or_default()
144                    .as_secs(),
145                nanos: 0,
146            }),
147            device_type: 0,       // DEVICE_TYPE_UNSPECIFIED for MVP
148            certificates: vec![], // Empty for MVP (no X.509 chain)
149        })
150    }
151
152    /// Verify a peer's challenge response.
153    ///
154    /// On success, caches the peer's identity and returns their DeviceId.
155    pub fn verify_response(
156        &self,
157        response: &SignedChallengeResponse,
158    ) -> Result<DeviceId, SecurityError> {
159        // Parse public key
160        let public_key = DeviceKeypair::verifying_key_from_bytes(&response.public_key)?;
161
162        // Derive device ID from public key
163        let peer_device_id = DeviceId::from_public_key(&public_key);
164
165        // Recreate the message that should have been signed
166        // Note: In a full implementation, we would look up the original challenge
167        // For MVP, we verify the signature is valid for the provided nonce
168        let mut message = response.challenge_nonce.clone();
169        message.extend_from_slice(self.device_id().to_hex().as_bytes());
170        // Append timestamp if available
171        if let Some(ts) = &response.timestamp {
172            message.extend_from_slice(&ts.seconds.to_le_bytes());
173        }
174
175        // Parse and verify signature
176        let signature = DeviceKeypair::signature_from_bytes(&response.signature)?;
177        DeviceKeypair::verify_with_key(&public_key, &message, &signature)?;
178
179        // Cache the verified peer
180        let verified_peer = VerifiedPeer {
181            device_id: peer_device_id,
182            public_key: public_key.to_bytes(),
183            verified_at: SystemTime::now(),
184        };
185
186        self.verified_peers
187            .write()
188            .map_err(|e| SecurityError::Internal(format!("lock poisoned: {}", e)))?
189            .insert(peer_device_id, verified_peer);
190
191        Ok(peer_device_id)
192    }
193
194    /// Check if a peer is verified.
195    pub fn is_verified(&self, device_id: &DeviceId) -> bool {
196        self.verified_peers
197            .read()
198            .map(|cache| cache.contains_key(device_id))
199            .unwrap_or(false)
200    }
201
202    /// Get a verified peer's info.
203    pub fn get_verified_peer(&self, device_id: &DeviceId) -> Option<VerifiedPeer> {
204        self.verified_peers
205            .read()
206            .ok()
207            .and_then(|cache| cache.get(device_id).cloned())
208    }
209
210    /// Remove a peer from the verified cache.
211    pub fn remove_peer(&self, device_id: &DeviceId) {
212        if let Ok(mut cache) = self.verified_peers.write() {
213            cache.remove(device_id);
214        }
215    }
216
217    /// Clear all verified peers.
218    pub fn clear_verified_peers(&self) {
219        if let Ok(mut cache) = self.verified_peers.write() {
220            cache.clear();
221        }
222    }
223
224    /// Get number of verified peers.
225    pub fn verified_peer_count(&self) -> usize {
226        self.verified_peers
227            .read()
228            .map(|cache| cache.len())
229            .unwrap_or(0)
230    }
231
232    /// Create the message bytes that should be signed for a challenge.
233    fn create_challenge_message(&self, challenge: &Challenge) -> Vec<u8> {
234        let mut message = challenge.nonce.clone();
235        message.extend_from_slice(challenge.challenger_id.as_bytes());
236        if let Some(ts) = &challenge.timestamp {
237            message.extend_from_slice(&ts.seconds.to_le_bytes());
238        }
239        message
240    }
241
242    /// Check if a challenge has expired.
243    fn check_challenge_expiry(&self, challenge: &Challenge) -> Result<(), SecurityError> {
244        if let Some(expires) = &challenge.expires_at {
245            let now = SystemTime::now()
246                .duration_since(UNIX_EPOCH)
247                .unwrap_or_default();
248
249            if now.as_secs() > expires.seconds {
250                return Err(SecurityError::ChallengeExpired(expires.seconds));
251            }
252        }
253        Ok(())
254    }
255}
256
257impl std::fmt::Debug for DeviceAuthenticator {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        f.debug_struct("DeviceAuthenticator")
260            .field("device_id", &self.device_id())
261            .field("verified_peer_count", &self.verified_peer_count())
262            .field("challenge_timeout", &self.challenge_timeout)
263            .finish()
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    fn create_test_authenticator() -> DeviceAuthenticator {
272        let keypair = DeviceKeypair::generate();
273        DeviceAuthenticator::new(keypair)
274    }
275
276    #[test]
277    fn test_generate_challenge() {
278        let auth = create_test_authenticator();
279        let challenge = auth.generate_challenge();
280
281        assert_eq!(challenge.nonce.len(), CHALLENGE_NONCE_SIZE);
282        assert!(!challenge.challenger_id.is_empty());
283        assert!(challenge.timestamp.is_some());
284        assert!(challenge.expires_at.is_some());
285    }
286
287    #[test]
288    fn test_challenge_nonce_unique() {
289        let auth = create_test_authenticator();
290        let c1 = auth.generate_challenge();
291        let c2 = auth.generate_challenge();
292
293        assert_ne!(c1.nonce, c2.nonce);
294    }
295
296    #[test]
297    fn test_respond_to_challenge() {
298        let auth1 = create_test_authenticator();
299        let auth2 = create_test_authenticator();
300
301        let challenge = auth1.generate_challenge();
302        let response = auth2.respond_to_challenge(&challenge).unwrap();
303
304        assert_eq!(response.public_key.len(), 32);
305        assert_eq!(response.signature.len(), 64);
306        assert_eq!(response.challenge_nonce, challenge.nonce);
307    }
308
309    #[test]
310    fn test_full_authentication_flow() {
311        let auth1 = create_test_authenticator();
312        let auth2 = create_test_authenticator();
313
314        // Auth1 generates challenge for Auth2
315        let challenge = auth1.generate_challenge();
316
317        // Auth2 responds
318        let response = auth2.respond_to_challenge(&challenge).unwrap();
319
320        // Auth1 verifies
321        let peer_id = auth1.verify_response(&response).unwrap();
322
323        // Peer ID should match Auth2's device ID
324        assert_eq!(peer_id, auth2.device_id());
325
326        // Peer should now be in verified cache
327        assert!(auth1.is_verified(&peer_id));
328    }
329
330    #[test]
331    fn test_expired_challenge_rejected() {
332        let auth = create_test_authenticator();
333
334        // Create a challenge with expiration in the past
335        let mut challenge = auth.generate_challenge();
336        challenge.expires_at = Some(peat_schema::common::v1::Timestamp {
337            seconds: 0, // Way in the past
338            nanos: 0,
339        });
340
341        let result = auth.respond_to_challenge(&challenge);
342        assert!(matches!(result, Err(SecurityError::ChallengeExpired(_))));
343    }
344
345    #[test]
346    fn test_invalid_signature_rejected() {
347        let auth1 = create_test_authenticator();
348        let auth2 = create_test_authenticator();
349
350        let challenge = auth1.generate_challenge();
351        let mut response = auth2.respond_to_challenge(&challenge).unwrap();
352
353        // Corrupt the signature
354        response.signature[0] ^= 0xFF;
355
356        let result = auth1.verify_response(&response);
357        assert!(matches!(result, Err(SecurityError::InvalidSignature(_))));
358    }
359
360    #[test]
361    fn test_remove_peer() {
362        let auth1 = create_test_authenticator();
363        let auth2 = create_test_authenticator();
364
365        // Authenticate
366        let challenge = auth1.generate_challenge();
367        let response = auth2.respond_to_challenge(&challenge).unwrap();
368        let peer_id = auth1.verify_response(&response).unwrap();
369
370        assert!(auth1.is_verified(&peer_id));
371
372        // Remove
373        auth1.remove_peer(&peer_id);
374        assert!(!auth1.is_verified(&peer_id));
375    }
376
377    #[test]
378    fn test_clear_verified_peers() {
379        let auth1 = create_test_authenticator();
380        let auth2 = create_test_authenticator();
381        let auth3 = create_test_authenticator();
382
383        // Authenticate two peers
384        let c1 = auth1.generate_challenge();
385        let r1 = auth2.respond_to_challenge(&c1).unwrap();
386        auth1.verify_response(&r1).unwrap();
387
388        let c2 = auth1.generate_challenge();
389        let r2 = auth3.respond_to_challenge(&c2).unwrap();
390        auth1.verify_response(&r2).unwrap();
391
392        assert_eq!(auth1.verified_peer_count(), 2);
393
394        auth1.clear_verified_peers();
395        assert_eq!(auth1.verified_peer_count(), 0);
396    }
397}