Skip to main content

liminal_server/
link_handshake.rs

1//! The link handshake: a Noise `KK` exchange whose static keys are the two
2//! machines' Lys identities, through lys-core's X25519 derivation and never a
3//! second one (ACCESS-002 R1).
4//!
5//! `KK` means each side knows the other's static key before the first byte:
6//! the importer's and exporter's Lys-recorded Ed25519 keys, mapped to their
7//! Montgomery form. A third machine, whose static key is not the one the peer
8//! expected, fails at the first message; so does a key derived any other way
9//! from the same identity, because it is simply a different key.
10
11use ed25519_dalek::VerifyingKey;
12use lys_core::keys::Ed25519Identity;
13
14/// The Noise pattern and primitives every link uses; both ends must agree.
15pub const LINK_NOISE_PATTERN: &str = "Noise_KK_25519_ChaChaPoly_BLAKE2s";
16/// Bytes a handshake message or transport frame may need above its payload.
17pub const NOISE_OVERHEAD: usize = 96;
18/// The largest frame Noise carries.
19pub const NOISE_MAX_MESSAGE: usize = 65_535;
20
21/// A machine as Lys records it: its Ed25519 public key.
22#[derive(Clone, Copy, PartialEq, Eq)]
23pub struct MachineIdentity {
24    ed25519_public: [u8; 32],
25}
26
27impl std::fmt::Debug for MachineIdentity {
28    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        formatter
30            .debug_struct("MachineIdentity")
31            .field("ed25519_public", &"32 bytes")
32            .finish()
33    }
34}
35
36impl MachineIdentity {
37    /// A machine from the Ed25519 public key Lys recorded for it.
38    #[must_use]
39    pub const fn new(ed25519_public: [u8; 32]) -> Self {
40        Self { ed25519_public }
41    }
42
43    /// The recorded Ed25519 public key.
44    #[must_use]
45    pub const fn ed25519_public(&self) -> &[u8; 32] {
46        &self.ed25519_public
47    }
48
49    /// The machine's X25519 static public key: the Montgomery form of its
50    /// Ed25519 point, the same map lys-core applies to its own identity.
51    ///
52    /// # Errors
53    /// The recorded bytes are not a valid Ed25519 public key.
54    pub fn x25519_public(&self) -> Result<[u8; 32], LinkHandshakeError> {
55        VerifyingKey::from_bytes(&self.ed25519_public)
56            .map(|key| key.to_montgomery().to_bytes())
57            .map_err(|_| LinkHandshakeError::InvalidIdentity)
58    }
59}
60
61/// This machine, holding the Lys identity whose derivation keys the link.
62pub struct LocalMachine {
63    identity: Ed25519Identity,
64}
65
66impl std::fmt::Debug for LocalMachine {
67    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        formatter
69            .debug_struct("LocalMachine")
70            .field("identity", &self.machine())
71            .finish()
72    }
73}
74
75impl LocalMachine {
76    /// Wraps the Lys identity this machine signs with.
77    #[must_use]
78    pub const fn new(identity: Ed25519Identity) -> Self {
79        Self { identity }
80    }
81
82    /// This machine as Lys records it.
83    #[must_use]
84    pub fn machine(&self) -> MachineIdentity {
85        MachineIdentity::new(self.identity.public_key_bytes())
86    }
87
88    fn x25519_secret(&self) -> [u8; 32] {
89        self.identity.x25519_static_secret().to_bytes()
90    }
91
92    /// Signs the issuer's exact pass request as current certificate-standing proof.
93    /// The nonce is fresh for this open and the issuer rejects its reuse.
94    ///
95    /// # Errors
96    /// Randomness or the wall clock is unavailable.
97    pub(crate) fn certificate_proof(
98        &self,
99        agent: &str,
100        path: &str,
101        body: &[u8],
102    ) -> Result<String, crate::link::certificate::CertificateRefusal> {
103        use crate::link::certificate::CertificateRefusal;
104        use std::io::Read;
105        let mut nonce = [0; 16];
106        std::fs::File::open("/dev/urandom")
107            .and_then(|mut file| file.read_exact(&mut nonce))
108            .map_err(|_| CertificateRefusal::StatusUnavailable)?;
109        let at = std::time::SystemTime::now()
110            .duration_since(std::time::UNIX_EPOCH)
111            .map_err(|_| CertificateRefusal::StatusUnavailable)?;
112        let at =
113            u64::try_from(at.as_millis()).map_err(|_| CertificateRefusal::StatusUnavailable)?;
114        Ok(crate::link_pass::cose::header(
115            &self.identity,
116            agent,
117            path,
118            body,
119            at,
120            &hex::encode(nonce),
121        ))
122    }
123}
124
125/// Why a handshake did not complete.
126#[derive(Debug, PartialEq, Eq)]
127pub enum LinkHandshakeError {
128    /// A recorded public key that is not an Ed25519 point.
129    InvalidIdentity,
130    /// The peer's message did not authenticate against the expected static
131    /// key: a third machine, or a key derived some other way.
132    PeerNotTheExpectedMachine,
133    /// A message or buffer outside Noise's bounds.
134    Framing(String),
135    /// The handshake pattern or keys could not be set up.
136    Setup(String),
137    /// Transport was requested before the handshake finished.
138    NotFinished,
139}
140
141impl std::fmt::Display for LinkHandshakeError {
142    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Self::InvalidIdentity => formatter.write_str("machine identity is not an Ed25519 key"),
145            Self::PeerNotTheExpectedMachine => {
146                formatter.write_str("peer is not the machine this link was opened with")
147            }
148            Self::Framing(detail) => write!(formatter, "noise framing refused: {detail}"),
149            Self::Setup(detail) => write!(formatter, "noise setup refused: {detail}"),
150            Self::NotFinished => formatter.write_str("handshake has not finished"),
151        }
152    }
153}
154
155impl std::error::Error for LinkHandshakeError {}
156
157fn classify(error: &snow::Error) -> LinkHandshakeError {
158    match error {
159        snow::Error::Decrypt => LinkHandshakeError::PeerNotTheExpectedMachine,
160        snow::Error::Input => LinkHandshakeError::Framing("input outside bounds".to_owned()),
161        other => LinkHandshakeError::Framing(other.to_string()),
162    }
163}
164
165/// One end of a link handshake in progress.
166pub struct LinkHandshake {
167    state: snow::HandshakeState,
168    peer: MachineIdentity,
169}
170
171impl std::fmt::Debug for LinkHandshake {
172    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        formatter
174            .debug_struct("LinkHandshake")
175            .field("peer", &self.peer)
176            .field("finished", &self.state.is_handshake_finished())
177            .finish()
178    }
179}
180
181impl LinkHandshake {
182    fn build(
183        local: &LocalMachine,
184        peer: MachineIdentity,
185        initiator: bool,
186    ) -> Result<Self, LinkHandshakeError> {
187        let params: snow::params::NoiseParams = LINK_NOISE_PATTERN
188            .parse()
189            .map_err(|error: snow::Error| LinkHandshakeError::Setup(error.to_string()))?;
190        let secret = local.x25519_secret();
191        let remote = peer.x25519_public()?;
192        let builder = snow::Builder::new(params)
193            .local_private_key(&secret)
194            .map_err(|error| LinkHandshakeError::Setup(error.to_string()))?
195            .remote_public_key(&remote)
196            .map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
197        let state = if initiator {
198            builder.build_initiator()
199        } else {
200            builder.build_responder()
201        }
202        .map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
203        Ok(Self { state, peer })
204    }
205
206    /// The end that sends the first message, expecting `peer` to answer.
207    ///
208    /// # Errors
209    /// A peer key that is not an Ed25519 point, or a Noise setup refusal.
210    pub fn initiator(
211        local: &LocalMachine,
212        peer: MachineIdentity,
213    ) -> Result<Self, LinkHandshakeError> {
214        Self::build(local, peer, true)
215    }
216
217    /// The end that waits for the first message, expecting it from `peer`.
218    ///
219    /// # Errors
220    /// A peer key that is not an Ed25519 point, or a Noise setup refusal.
221    pub fn responder(
222        local: &LocalMachine,
223        peer: MachineIdentity,
224    ) -> Result<Self, LinkHandshakeError> {
225        Self::build(local, peer, false)
226    }
227
228    /// Writes the next handshake message into `out`, returning its length.
229    ///
230    /// # Errors
231    /// A buffer too small, or a message out of turn.
232    pub fn write(&mut self, out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
233        self.state
234            .write_message(&[], out)
235            .map_err(|error| classify(&error))
236    }
237
238    /// Reads the peer's handshake message.
239    ///
240    /// # Errors
241    /// `PeerNotTheExpectedMachine` when the message does not authenticate
242    /// against the static key this end expected; framing otherwise.
243    pub fn read(&mut self, message: &[u8]) -> Result<(), LinkHandshakeError> {
244        let mut payload = [0_u8; NOISE_OVERHEAD];
245        self.state
246            .read_message(message, &mut payload)
247            .map(|_| ())
248            .map_err(|error| classify(&error))
249    }
250
251    /// Whether both messages of `KK` have been exchanged.
252    #[must_use]
253    pub fn is_finished(&self) -> bool {
254        self.state.is_handshake_finished()
255    }
256
257    /// The transport the finished handshake keyed, bound to the proved peer.
258    ///
259    /// # Errors
260    /// `NotFinished` before both messages were exchanged.
261    pub fn finish(self) -> Result<LinkTransport, LinkHandshakeError> {
262        if !self.state.is_handshake_finished() {
263            return Err(LinkHandshakeError::NotFinished);
264        }
265        let transport = self
266            .state
267            .into_transport_mode()
268            .map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
269        Ok(LinkTransport {
270            transport,
271            peer: self.peer,
272        })
273    }
274}
275
276/// A keyed link between two proved machines.
277pub struct LinkTransport {
278    transport: snow::TransportState,
279    peer: MachineIdentity,
280}
281
282impl std::fmt::Debug for LinkTransport {
283    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        formatter
285            .debug_struct("LinkTransport")
286            .field("peer", &self.peer)
287            .finish_non_exhaustive()
288    }
289}
290
291impl LinkTransport {
292    /// The nonce Noise will use to seal the next transport message.
293    #[must_use]
294    pub fn sending_nonce(&self) -> u64 {
295        self.transport.sending_nonce()
296    }
297
298    /// Selects the explicit nonce of the next received transport message.
299    pub fn set_receiving_nonce(&mut self, nonce: u64) {
300        self.transport.set_receiving_nonce(nonce);
301    }
302
303    /// The machine the handshake proved on the other end.
304    #[must_use]
305    pub const fn peer(&self) -> &MachineIdentity {
306        &self.peer
307    }
308
309    /// Seals one frame for the peer, returning the bytes written to `out`.
310    ///
311    /// # Errors
312    /// A payload or buffer outside Noise's bounds.
313    pub fn seal(&mut self, payload: &[u8], out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
314        self.transport
315            .write_message(payload, out)
316            .map_err(|error| classify(&error))
317    }
318
319    /// Opens one frame from the peer, returning the bytes written to `out`.
320    ///
321    /// # Errors
322    /// `PeerNotTheExpectedMachine` when the frame was not sealed by the keyed
323    /// peer; framing otherwise.
324    pub fn open(&mut self, frame: &[u8], out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
325        self.transport
326            .read_message(frame, out)
327            .map_err(|error| classify(&error))
328    }
329}