alpine/
discovery.rs

1use std::net::SocketAddr;
2
3use ed25519_dalek::{Signature, Signer, Verifier, VerifyingKey};
4use rand::{rngs::OsRng, RngCore};
5use thiserror::Error;
6use tokio::net::UdpSocket;
7
8use crate::messages::{CapabilitySet, DiscoveryReply, DiscoveryRequest, MessageType};
9
10#[derive(Debug, Error)]
11pub enum DiscoveryError {
12    #[error("socket error: {0}")]
13    Io(String),
14    #[error("decode error: {0}")]
15    Decode(String),
16    #[error("signature invalid")]
17    InvalidSignature,
18    #[error("nonce mismatch")]
19    NonceMismatch,
20    #[error("unsupported version")]
21    UnsupportedVersion,
22}
23
24/// Controller-side discovery helper.
25pub struct DiscoveryClient;
26
27impl DiscoveryClient {
28    pub async fn broadcast(
29        socket: &UdpSocket,
30        broadcast: SocketAddr,
31        requested: Vec<String>,
32    ) -> Result<Vec<u8>, DiscoveryError> {
33        let mut nonce = vec![0u8; 32];
34        OsRng.fill_bytes(&mut nonce);
35        let request = DiscoveryRequest::new(requested, nonce.clone());
36        let bytes =
37            serde_cbor::to_vec(&request).map_err(|e| DiscoveryError::Decode(e.to_string()))?;
38        socket
39            .send_to(&bytes, broadcast)
40            .await
41            .map_err(|e| DiscoveryError::Io(e.to_string()))?;
42        Ok(nonce)
43    }
44
45    pub async fn recv_reply(
46        socket: &UdpSocket,
47        expected_nonce: &[u8],
48        verifier: &VerifyingKey,
49    ) -> Result<DiscoveryReply, DiscoveryError> {
50        let mut buf = vec![0u8; 2048];
51        let (len, _) = socket
52            .recv_from(&mut buf)
53            .await
54            .map_err(|e| DiscoveryError::Io(e.to_string()))?;
55        let reply: DiscoveryReply = serde_cbor::from_slice(&buf[..len])
56            .map_err(|e| DiscoveryError::Decode(e.to_string()))?;
57        verify_reply(&reply, expected_nonce, verifier)?;
58        Ok(reply)
59    }
60}
61
62/// Device-side responder skeleton.
63pub struct DiscoveryResponder {
64    pub identity: crate::messages::DeviceIdentity,
65    pub mac_address: String,
66    pub capabilities: CapabilitySet,
67    pub signer: ed25519_dalek::SigningKey,
68}
69
70impl DiscoveryResponder {
71    pub fn reply(&self, server_nonce: Vec<u8>, client_nonce: &[u8]) -> DiscoveryReply {
72        let mut data = server_nonce.clone();
73        data.extend_from_slice(client_nonce);
74        let signature = self.signer.sign(&data).to_vec();
75        let pubkey = self.signer.verifying_key().to_bytes().to_vec();
76        DiscoveryReply::new(
77            &self.identity,
78            self.mac_address.clone(),
79            server_nonce,
80            self.capabilities.clone(),
81            signature,
82            pubkey,
83            Vec::new(),
84            false,
85        )
86    }
87}
88
89fn verify_reply(
90    reply: &DiscoveryReply,
91    expected_client_nonce: &[u8],
92    verifier: &VerifyingKey,
93) -> Result<(), DiscoveryError> {
94    if reply.message_type != MessageType::AlpineDiscoverReply {
95        return Err(DiscoveryError::UnsupportedVersion);
96    }
97    if reply.alpine_version != crate::messages::ALPINE_VERSION {
98        return Err(DiscoveryError::UnsupportedVersion);
99    }
100
101    // Signature is taken over server_nonce || client_nonce to bind request/response.
102    let mut data = reply.server_nonce.clone();
103    data.extend_from_slice(expected_client_nonce);
104    let sig =
105        Signature::from_slice(&reply.signature).map_err(|_| DiscoveryError::InvalidSignature)?;
106    verifier
107        .verify(&data, &sig)
108        .map_err(|_| DiscoveryError::InvalidSignature)?;
109    Ok(())
110}