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
93/// Why a handshake did not complete.
94#[derive(Debug, PartialEq, Eq)]
95pub enum LinkHandshakeError {
96    /// A recorded public key that is not an Ed25519 point.
97    InvalidIdentity,
98    /// The peer's message did not authenticate against the expected static
99    /// key: a third machine, or a key derived some other way.
100    PeerNotTheExpectedMachine,
101    /// A message or buffer outside Noise's bounds.
102    Framing(String),
103    /// The handshake pattern or keys could not be set up.
104    Setup(String),
105    /// Transport was requested before the handshake finished.
106    NotFinished,
107}
108
109impl std::fmt::Display for LinkHandshakeError {
110    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            Self::InvalidIdentity => formatter.write_str("machine identity is not an Ed25519 key"),
113            Self::PeerNotTheExpectedMachine => {
114                formatter.write_str("peer is not the machine this link was opened with")
115            }
116            Self::Framing(detail) => write!(formatter, "noise framing refused: {detail}"),
117            Self::Setup(detail) => write!(formatter, "noise setup refused: {detail}"),
118            Self::NotFinished => formatter.write_str("handshake has not finished"),
119        }
120    }
121}
122
123impl std::error::Error for LinkHandshakeError {}
124
125fn classify(error: &snow::Error) -> LinkHandshakeError {
126    match error {
127        snow::Error::Decrypt => LinkHandshakeError::PeerNotTheExpectedMachine,
128        snow::Error::Input => LinkHandshakeError::Framing("input outside bounds".to_owned()),
129        other => LinkHandshakeError::Framing(other.to_string()),
130    }
131}
132
133/// One end of a link handshake in progress.
134pub struct LinkHandshake {
135    state: snow::HandshakeState,
136    peer: MachineIdentity,
137}
138
139impl std::fmt::Debug for LinkHandshake {
140    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        formatter
142            .debug_struct("LinkHandshake")
143            .field("peer", &self.peer)
144            .field("finished", &self.state.is_handshake_finished())
145            .finish()
146    }
147}
148
149impl LinkHandshake {
150    fn build(
151        local: &LocalMachine,
152        peer: MachineIdentity,
153        initiator: bool,
154    ) -> Result<Self, LinkHandshakeError> {
155        let params: snow::params::NoiseParams = LINK_NOISE_PATTERN
156            .parse()
157            .map_err(|error: snow::Error| LinkHandshakeError::Setup(error.to_string()))?;
158        let secret = local.x25519_secret();
159        let remote = peer.x25519_public()?;
160        let builder = snow::Builder::new(params)
161            .local_private_key(&secret)
162            .map_err(|error| LinkHandshakeError::Setup(error.to_string()))?
163            .remote_public_key(&remote)
164            .map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
165        let state = if initiator {
166            builder.build_initiator()
167        } else {
168            builder.build_responder()
169        }
170        .map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
171        Ok(Self { state, peer })
172    }
173
174    /// The end that sends the first message, expecting `peer` to answer.
175    ///
176    /// # Errors
177    /// A peer key that is not an Ed25519 point, or a Noise setup refusal.
178    pub fn initiator(
179        local: &LocalMachine,
180        peer: MachineIdentity,
181    ) -> Result<Self, LinkHandshakeError> {
182        Self::build(local, peer, true)
183    }
184
185    /// The end that waits for the first message, expecting it from `peer`.
186    ///
187    /// # Errors
188    /// A peer key that is not an Ed25519 point, or a Noise setup refusal.
189    pub fn responder(
190        local: &LocalMachine,
191        peer: MachineIdentity,
192    ) -> Result<Self, LinkHandshakeError> {
193        Self::build(local, peer, false)
194    }
195
196    /// Writes the next handshake message into `out`, returning its length.
197    ///
198    /// # Errors
199    /// A buffer too small, or a message out of turn.
200    pub fn write(&mut self, out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
201        self.state
202            .write_message(&[], out)
203            .map_err(|error| classify(&error))
204    }
205
206    /// Reads the peer's handshake message.
207    ///
208    /// # Errors
209    /// `PeerNotTheExpectedMachine` when the message does not authenticate
210    /// against the static key this end expected; framing otherwise.
211    pub fn read(&mut self, message: &[u8]) -> Result<(), LinkHandshakeError> {
212        let mut payload = [0_u8; NOISE_OVERHEAD];
213        self.state
214            .read_message(message, &mut payload)
215            .map(|_| ())
216            .map_err(|error| classify(&error))
217    }
218
219    /// Whether both messages of `KK` have been exchanged.
220    #[must_use]
221    pub fn is_finished(&self) -> bool {
222        self.state.is_handshake_finished()
223    }
224
225    /// The transport the finished handshake keyed, bound to the proved peer.
226    ///
227    /// # Errors
228    /// `NotFinished` before both messages were exchanged.
229    pub fn finish(self) -> Result<LinkTransport, LinkHandshakeError> {
230        if !self.state.is_handshake_finished() {
231            return Err(LinkHandshakeError::NotFinished);
232        }
233        let transport = self
234            .state
235            .into_transport_mode()
236            .map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
237        Ok(LinkTransport {
238            transport,
239            peer: self.peer,
240        })
241    }
242}
243
244/// A keyed link between two proved machines.
245pub struct LinkTransport {
246    transport: snow::TransportState,
247    peer: MachineIdentity,
248}
249
250impl std::fmt::Debug for LinkTransport {
251    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        formatter
253            .debug_struct("LinkTransport")
254            .field("peer", &self.peer)
255            .finish_non_exhaustive()
256    }
257}
258
259impl LinkTransport {
260    /// The nonce Noise will use to seal the next transport message.
261    #[must_use]
262    pub fn sending_nonce(&self) -> u64 {
263        self.transport.sending_nonce()
264    }
265
266    /// Selects the explicit nonce of the next received transport message.
267    pub fn set_receiving_nonce(&mut self, nonce: u64) {
268        self.transport.set_receiving_nonce(nonce);
269    }
270
271    /// The machine the handshake proved on the other end.
272    #[must_use]
273    pub const fn peer(&self) -> &MachineIdentity {
274        &self.peer
275    }
276
277    /// Seals one frame for the peer, returning the bytes written to `out`.
278    ///
279    /// # Errors
280    /// A payload or buffer outside Noise's bounds.
281    pub fn seal(&mut self, payload: &[u8], out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
282        self.transport
283            .write_message(payload, out)
284            .map_err(|error| classify(&error))
285    }
286
287    /// Opens one frame from the peer, returning the bytes written to `out`.
288    ///
289    /// # Errors
290    /// `PeerNotTheExpectedMachine` when the frame was not sealed by the keyed
291    /// peer; framing otherwise.
292    pub fn open(&mut self, frame: &[u8], out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
293        self.transport
294            .read_message(frame, out)
295            .map_err(|error| classify(&error))
296    }
297}