srt_runtime/handshake_sm.rs
1//! Shared types for the sans-IO HSv5 handshake state machine —
2//! `draft-sharabayko-srt-01` §4.3 (Handshake Messages) / §4.3.1
3//! (Caller-Listener Handshake).
4//!
5//! This module holds the pieces [`crate::caller::CallerHandshake`] and
6//! [`crate::listener::ListenerHandshake`] share: the negotiation input
7//! ([`HandshakeConfig`]), the negotiation output ([`NegotiatedParams`]), the
8//! event type both engines emit ([`HandshakeOutput`]), and the Handshake
9//! Rejection Reason codes (§4.3, Table 7) as a typed [`RejectionReason`].
10//!
11//! This module also holds the `crypto`-feature-gated §6.1.5 Key Material
12//! Exchange helpers ([`CryptoConfig`], `build_key_material_extension`,
13//! `recover_sek`, `echo_key_material_as_response`, `verify_km_echo`) shared
14//! by [`crate::caller::CallerHandshake`] / [`crate::listener::ListenerHandshake`]
15//! — see [`CryptoConfig`]'s doc and `specs/rules/srt-crypto.md`. Congestion
16//! control beyond LiveCC packet pacing is an explicit follow-up — see the
17//! crate root docs.
18
19use alloc::string::String;
20use alloc::vec::Vec;
21
22use crate::error::{Error, Result};
23#[cfg(feature = "crypto")]
24use crate::packet::KeyMaterial;
25use crate::packet::handshake::{HS_EXT_FLAG_CONFIG, HS_EXT_FLAG_HSREQ};
26#[cfg(feature = "crypto")]
27use crate::packet::{Cipher, KmAuth, KmKeyFlag, StreamEncapsulation};
28use crate::packet::{
29 ControlPacket, EncryptionField, GroupMembershipExtension, HandshakeExtensionMessageFlags,
30 HandshakePacket, HandshakeType, HsExtMessage,
31};
32
33/// A base protocol version number of `4` — the value the Caller's INDUCTION
34/// handshake MUST always carry (`draft-sharabayko-srt-01` §4.3.1.1), kept for
35/// UDT compatibility.
36pub const HANDSHAKE_VERSION_4: u32 = 4;
37/// A base protocol version number of `5` — HSv5, used by every handshake
38/// message from the Listener's INDUCTION response onward (§4.3.1.1/§4.3.1.2).
39pub const HANDSHAKE_VERSION_5: u32 = 5;
40
41/// The `Extension Field` value the Listener echoes on its INDUCTION response
42/// so the Caller can recognise it as an SRT (not legacy UDT) party
43/// (`draft-sharabayko-srt-01` §4.3.1.1: "SRT magic code 0x4A17").
44pub const SRT_MAGIC_CODE: u16 = 0x4A17;
45
46/// The `Extension Field` value the Caller sets on its very first INDUCTION
47/// handshake (§4.3.1.1: "Extension Field: 2"). This is *not* the §3.2.1
48/// Table 3 `Extension Flags` bitmask (whose `KMREQ` bit happens to share the
49/// same numeric value) — it is a legacy UDT socket-type field (`UDT_DGRAM`)
50/// carried over because the INDUCTION handshake is version-4-shaped for UDT
51/// compatibility.
52pub const INDUCTION_LEGACY_SOCKET_TYPE: u16 = 2;
53
54/// The base wire value of the Handshake Rejection Reason codes
55/// (`draft-sharabayko-srt-01` §4.3, Table 7): a rejected connection's
56/// `Handshake Type` field carries `1000 + <code>`.
57pub const REJECTION_CODE_BASE: u32 = 1000;
58
59/// Handshake Rejection Reason (`draft-sharabayko-srt-01` §4.3, Table 7). Sent
60/// in place of a normal `Handshake Type` value (`1000 + code`, decoded via
61/// [`HandshakeType::Reserved`]) when a connection attempt is refused.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize))]
64#[non_exhaustive]
65pub enum RejectionReason {
66 /// `1000`: unknown reason.
67 Unknown,
68 /// `1001`: system function error.
69 System,
70 /// `1002`: rejected by peer.
71 Peer,
72 /// `1003`: resource allocation problem.
73 Resource,
74 /// `1004`: incorrect data in handshake.
75 Rogue,
76 /// `1005`: listener's backlog exceeded.
77 Backlog,
78 /// `1006`: internal program error.
79 Ipe,
80 /// `1007`: socket is closing.
81 Close,
82 /// `1008`: peer is an older version than the agent's minimum.
83 Version,
84 /// `1009`: rendezvous cookie collision.
85 RdvCookie,
86 /// `1010`: wrong password.
87 BadSecret,
88 /// `1011`: password required or unexpected.
89 Unsecure,
90 /// `1012`: stream flag collision.
91 MessageApi,
92 /// `1013`: incompatible congestion-controller type.
93 Congestion,
94 /// `1014`: incompatible packet filter.
95 Filter,
96 /// `1015`: incompatible group.
97 Group,
98 /// A `1000 + code` value Table 7 does not define.
99 Reserved(u32),
100}
101
102impl RejectionReason {
103 /// Decode a Table 7 wire code (the value carried in `Handshake Type`,
104 /// i.e. already `1000 + code`).
105 pub fn from_bits(v: u32) -> Self {
106 match v {
107 1000 => RejectionReason::Unknown,
108 1001 => RejectionReason::System,
109 1002 => RejectionReason::Peer,
110 1003 => RejectionReason::Resource,
111 1004 => RejectionReason::Rogue,
112 1005 => RejectionReason::Backlog,
113 1006 => RejectionReason::Ipe,
114 1007 => RejectionReason::Close,
115 1008 => RejectionReason::Version,
116 1009 => RejectionReason::RdvCookie,
117 1010 => RejectionReason::BadSecret,
118 1011 => RejectionReason::Unsecure,
119 1012 => RejectionReason::MessageApi,
120 1013 => RejectionReason::Congestion,
121 1014 => RejectionReason::Filter,
122 1015 => RejectionReason::Group,
123 other => RejectionReason::Reserved(other),
124 }
125 }
126
127 /// The wire code (`1000 + code`), as carried in `Handshake Type`.
128 pub fn to_bits(self) -> u32 {
129 match self {
130 RejectionReason::Unknown => 1000,
131 RejectionReason::System => 1001,
132 RejectionReason::Peer => 1002,
133 RejectionReason::Resource => 1003,
134 RejectionReason::Rogue => 1004,
135 RejectionReason::Backlog => 1005,
136 RejectionReason::Ipe => 1006,
137 RejectionReason::Close => 1007,
138 RejectionReason::Version => 1008,
139 RejectionReason::RdvCookie => 1009,
140 RejectionReason::BadSecret => 1010,
141 RejectionReason::Unsecure => 1011,
142 RejectionReason::MessageApi => 1012,
143 RejectionReason::Congestion => 1013,
144 RejectionReason::Filter => 1014,
145 RejectionReason::Group => 1015,
146 RejectionReason::Reserved(v) => v,
147 }
148 }
149
150 /// Spec label (Table 7).
151 pub fn name(&self) -> &'static str {
152 match self {
153 RejectionReason::Unknown => "REJ_UNKNOWN",
154 RejectionReason::System => "REJ_SYSTEM",
155 RejectionReason::Peer => "REJ_PEER",
156 RejectionReason::Resource => "REJ_RESOURCE",
157 RejectionReason::Rogue => "REJ_ROGUE",
158 RejectionReason::Backlog => "REJ_BACKLOG",
159 RejectionReason::Ipe => "REJ_IPE",
160 RejectionReason::Close => "REJ_CLOSE",
161 RejectionReason::Version => "REJ_VERSION",
162 RejectionReason::RdvCookie => "REJ_RDVCOOKIE",
163 RejectionReason::BadSecret => "REJ_BADSECRET",
164 RejectionReason::Unsecure => "REJ_UNSECURE",
165 RejectionReason::MessageApi => "REJ_MESSAGEAPI",
166 RejectionReason::Congestion => "REJ_CONGESTION",
167 RejectionReason::Filter => "REJ_FILTER",
168 RejectionReason::Group => "REJ_GROUP",
169 RejectionReason::Reserved(_) => "reserved",
170 }
171 }
172
173 /// Recover the [`RejectionReason`] a peer sent back as a `Handshake
174 /// Type`, or `None` if `ht` is a normal (non-rejection) handshake type.
175 pub fn from_handshake_type(ht: HandshakeType) -> Option<Self> {
176 match ht {
177 HandshakeType::Reserved(v) if v >= REJECTION_CODE_BASE => {
178 Some(RejectionReason::from_bits(v))
179 }
180 _ => None,
181 }
182 }
183
184 /// Encode as the `Handshake Type` value a rejection packet carries.
185 pub fn to_handshake_type(self) -> HandshakeType {
186 HandshakeType::from_bits(self.to_bits())
187 }
188}
189
190broadcast_common::impl_spec_display!(RejectionReason, Reserved);
191
192/// Opt-in §6 payload-encryption config for one side of a Caller-Listener
193/// handshake (`draft-sharabayko-srt-01` §6.1.5, Key Material Exchange —
194/// curated at `specs/rules/srt-crypto.md`). `None` on
195/// [`HandshakeConfig::crypto`] (the default) disables the encryption path
196/// entirely: no Key Material extension is sent, and a peer that sends one
197/// unexpectedly is rejected ([`RejectionReason::Unsecure`]).
198///
199/// This crate's sans-IO core never reads OS randomness — the same design
200/// choice as [`derive_cookie`]'s caller-supplied `time_bucket`/`secret`
201/// inputs, or [`crate::io`]'s `random_u64` helper for the SYN Cookie secret.
202/// [`Self::salt`] and, on the initiator, [`Self::sek`] must be freshly
203/// generated by the caller/driver (e.g. a `tokio` adapter with access to a
204/// real CSPRNG) for **every new connection** (§6.2.1: `Salt = PRNG(128)`,
205/// `SEK = PRNG(KLen)`) and never reused across connections.
206#[cfg(feature = "crypto")]
207#[cfg_attr(docsrs, doc(cfg(feature = "crypto")))]
208#[derive(Debug, Clone, PartialEq)]
209pub struct CryptoConfig {
210 /// The pre-shared secret (§6.1.4). Must be identical on both peers, or
211 /// the responder's KEK derivation recovers the wrong SEK and the RFC
212 /// 3394 wrap's integrity check fails
213 /// ([`RejectionReason::BadSecret`] on the Listener side).
214 pub passphrase: Vec<u8>,
215 /// A fresh, cryptographically random 128-bit Salt for **this
216 /// connection** (§6.2.1: `Salt = PRNG(128)`). See the struct doc for why
217 /// this crate does not generate it internally.
218 pub salt: [u8; crate::crypto::SALT_LEN],
219 /// This side's plaintext Stream Encrypting Key. **Only meaningful on the
220 /// connection initiator** (the Caller — §6.1.5: "sent by the connection
221 /// initiator ... to the responder"). Must be a fresh, cryptographically
222 /// random 16/24/32-byte value (§6.2.1: `SEK = PRNG(KLen)`) matching
223 /// [`HandshakeConfig::encryption_field`]'s advertised cipher. Ignored on
224 /// the Listener/responder side (which instead recovers the SEK by
225 /// unwrapping the initiator's Key Material) — may be left empty there.
226 pub sek: Vec<u8>,
227}
228
229/// Local configuration for one side of a Caller-Listener handshake — the
230/// values *this* engine advertises (`draft-sharabayko-srt-01` §3.2.1,
231/// §3.2.1.1, §3.2.1.3, §3.2.1.4).
232#[derive(Debug, Clone, PartialEq)]
233pub struct HandshakeConfig {
234 /// TSBPD delay in milliseconds this side requests, sent as both the
235 /// Receiver and Sender TSBPD Delay of its Handshake Extension Message
236 /// (§3.2.1.1, Figure 6). The negotiated value is the greater of the two
237 /// parties' requests (§4.3.1.2).
238 pub latency_ms: u16,
239 /// Maximum Transmission Unit Size (§3.2.1).
240 pub mtu: u32,
241 /// Maximum Flow Window Size (§3.2.1).
242 pub max_flow_window_size: u32,
243 /// Initial Packet Sequence Number this side will use for its first data
244 /// packet (§3.2.1). No data plane exists yet in this crate; callers of a
245 /// later data-plane PR are expected to set this to a real ISN.
246 pub initial_seq_number: u32,
247 /// The `SRT Version` this side reports in its Handshake Extension
248 /// Message (`major * 0x10000 + minor * 0x100 + patch`, §3.2.1.1).
249 pub srt_version: u32,
250 /// `SRT Flags` this side advertises (§3.2.1.1.1, Table 6).
251 pub flags: HandshakeExtensionMessageFlags,
252 /// Advertised cipher family and block size (§3.2.1, Table 2). Actual
253 /// key-wrap/unwrap crypto is an explicit follow-up; this field is
254 /// negotiated but not acted on in this release.
255 pub encryption_field: EncryptionField,
256 /// Stream ID to advertise (Caller only — §3.2.1.3); `None` sends no
257 /// Stream ID extension.
258 pub stream_id: Option<String>,
259 /// Group Membership to advertise (§3.2.1.4); `None` sends no Group
260 /// extension.
261 pub group: Option<GroupMembershipExtension>,
262 /// This side's own IP address, reported in every outgoing handshake
263 /// packet's `Peer IP Address` field (§3.2.1: "IPv4 or IPv6 address of
264 /// the packet's *sender*" — despite the field's name, each party reports
265 /// its own address, not its peer's).
266 pub local_ip: [u32; 4],
267 /// Number of [`crate::caller::CallerHandshake::tick`] /
268 /// [`crate::listener::ListenerHandshake::tick`] calls with no reply
269 /// before the last-sent handshake packet is retransmitted. Timeouts are
270 /// modeled as caller-driven ticks — no wall-clock lives in this crate.
271 pub retransmit_after_ticks: u32,
272 /// Maximum number of retransmissions before the handshake gives up
273 /// ([`HandshakeOutput::TimedOut`]).
274 pub max_retries: u32,
275 /// Opt-in §6 payload-encryption negotiation (`crypto` feature only).
276 /// `None` (the default) means this side neither offers nor requires
277 /// encryption. See [`CryptoConfig`] for the caller-supplied Salt/SEK
278 /// contract.
279 #[cfg(feature = "crypto")]
280 #[cfg_attr(docsrs, doc(cfg(feature = "crypto")))]
281 pub crypto: Option<CryptoConfig>,
282}
283
284impl Default for HandshakeConfig {
285 fn default() -> Self {
286 HandshakeConfig {
287 latency_ms: 120,
288 mtu: 1500,
289 max_flow_window_size: 8192,
290 initial_seq_number: 0,
291 srt_version: 0x0105_0000,
292 flags: HandshakeExtensionMessageFlags(
293 crate::packet::handshake::HS_MSG_FLAG_TSBPDSND
294 | crate::packet::handshake::HS_MSG_FLAG_TSBPDRCV
295 | crate::packet::handshake::HS_MSG_FLAG_CRYPT
296 | crate::packet::handshake::HS_MSG_FLAG_TLPKTDROP
297 | crate::packet::handshake::HS_MSG_FLAG_PERIODICNAK
298 | crate::packet::handshake::HS_MSG_FLAG_REXMITFLG,
299 ),
300 encryption_field: EncryptionField::NoEncryption,
301 stream_id: None,
302 group: None,
303 local_ip: [0, 0, 0, 0],
304 retransmit_after_ticks: 3,
305 max_retries: 5,
306 #[cfg(feature = "crypto")]
307 crypto: None,
308 }
309 }
310}
311
312/// The outcome of a Caller-Listener handshake, once both TSBPD delays and
313/// flags have been reconciled per `draft-sharabayko-srt-01` §4.3.1.2 ("The
314/// value for latency is always agreed to be the greater of those reported by
315/// each party").
316#[derive(Debug, Clone, PartialEq)]
317#[cfg_attr(feature = "serde", derive(serde::Serialize))]
318pub struct NegotiatedParams {
319 /// The negotiated base protocol version — always `5` (HSv5) for a
320 /// completed handshake in this crate.
321 pub version: u32,
322 /// The flags both sides advertised, ANDed together (only mutually
323 /// supported capabilities are considered agreed).
324 pub flags: HandshakeExtensionMessageFlags,
325 /// The agreed TSBPD latency in milliseconds: the greater of this side's
326 /// configured [`HandshakeConfig::latency_ms`] and the peer's reported
327 /// Receiver/Sender TSBPD Delay (§4.3.1.2).
328 pub latency_ms: u16,
329 /// This side's own SRT Socket ID.
330 pub own_socket_id: u32,
331 /// The peer's SRT Socket ID.
332 pub peer_socket_id: u32,
333 /// The negotiated Stream ID (§3.2.1.3), if one was advertised.
334 pub stream_id: Option<String>,
335 /// The negotiated Group Membership (§3.2.1.4), if one was advertised.
336 pub group: Option<GroupMembershipExtension>,
337 /// The negotiated Stream Encrypting Key (§6.1.5), if
338 /// [`HandshakeConfig::crypto`] was set on this side and the Key Material
339 /// exchange succeeded. `None` if encryption was not negotiated.
340 #[cfg(feature = "crypto")]
341 #[cfg_attr(docsrs, doc(cfg(feature = "crypto")))]
342 pub sek: Option<Vec<u8>>,
343 /// The Salt paired with [`Self::sek`] — both are required by
344 /// [`crate::crypto::aes_ctr_apply`] to encrypt/decrypt a data packet's
345 /// payload (§6.1.2/§6.2.2/§6.3.2).
346 #[cfg(feature = "crypto")]
347 #[cfg_attr(docsrs, doc(cfg(feature = "crypto")))]
348 pub salt: Option<[u8; crate::crypto::SALT_LEN]>,
349}
350
351/// An event produced by [`crate::caller::CallerHandshake::feed`] /
352/// [`crate::caller::CallerHandshake::start`] /
353/// [`crate::listener::ListenerHandshake::feed`] / either engine's `tick`.
354#[derive(Debug, Clone, PartialEq)]
355#[cfg_attr(feature = "serde", derive(serde::Serialize))]
356#[non_exhaustive]
357pub enum HandshakeOutput {
358 /// Send these bytes (one serialized Handshake control packet, built from
359 /// the existing [`crate::packet`] codecs) to the peer.
360 Send(Vec<u8>),
361 /// The handshake completed; here are the negotiated parameters.
362 Connected(NegotiatedParams),
363 /// The handshake was rejected — either the peer sent back an explicit
364 /// [`RejectionReason`], or a local validation check failed (in which
365 /// case a matching `Send` rejection packet is also emitted, in the same
366 /// output batch, for the Listener side).
367 Rejected(RejectionReason),
368 /// No reply arrived after [`HandshakeConfig::max_retries`]
369 /// retransmissions.
370 TimedOut,
371}
372
373/// Serializes one [`HandshakePacket`] using the existing [`ControlPacket`]
374/// codec — never hand-rolled.
375pub(crate) fn build_bytes(hp: HandshakePacket<'_>) -> Result<Vec<u8>> {
376 let pkt = ControlPacket::Handshake(hp);
377 let mut buf = alloc::vec![0u8; pkt.serialized_len()];
378 pkt.serialize_into(&mut buf)?;
379 Ok(buf)
380}
381
382/// Builds the extensions payload for a CONCLUSION-phase handshake carrying a
383/// Handshake Extension Message plus optional Stream ID / Group Membership,
384/// returning `(bytes, Extension Field flags)`.
385pub(crate) fn build_conclusion_extensions(
386 hs_ext_type: crate::packet::ExtensionType,
387 hs_msg: &HsExtMessage,
388 stream_id: Option<&str>,
389 group: Option<GroupMembershipExtension>,
390) -> Result<(Vec<u8>, u16)> {
391 use crate::packet::handshake::{build_extension_block, encode_stream_id};
392
393 let mut ext_bytes = Vec::new();
394 ext_bytes.extend(build_extension_block(hs_ext_type, &hs_msg.to_bytes())?);
395 let mut ext_flags = HS_EXT_FLAG_HSREQ;
396 if let Some(sid) = stream_id {
397 let sid_bytes = encode_stream_id(sid);
398 ext_bytes.extend(build_extension_block(
399 crate::packet::ExtensionType::Sid,
400 &sid_bytes,
401 )?);
402 ext_flags |= HS_EXT_FLAG_CONFIG;
403 }
404 if let Some(g) = group {
405 ext_bytes.extend(build_extension_block(
406 crate::packet::ExtensionType::Group,
407 &g.to_bytes(),
408 )?);
409 ext_flags |= HS_EXT_FLAG_CONFIG;
410 }
411 Ok((ext_bytes, ext_flags))
412}
413
414/// A parsed peer extension payload relevant to the Caller-Listener handshake:
415/// the mandatory Handshake Extension Message plus any optional Stream ID /
416/// Group Membership / Key Material.
417#[derive(Debug, Default)]
418pub(crate) struct ParsedPeerExtensions<'a> {
419 pub hs_msg: Option<HsExtMessage>,
420 pub stream_id: Option<String>,
421 pub group: Option<GroupMembershipExtension>,
422 /// The Key Material extension (§3.2.1.2/§3.2.2, `SRT_CMD_KMREQ` on the
423 /// Caller's CONCLUSION / `SRT_CMD_KMRSP` on the Listener's) — decoded
424 /// regardless of the `crypto` feature (wire-structure decode only,
425 /// [`KeyMaterial`] has no crypto dependency); only *acted on*
426 /// (KEK derive/unwrap/wrap) when `crypto` is enabled, see
427 /// [`crate::caller::CallerHandshake`]/[`crate::listener::ListenerHandshake`].
428 #[cfg(feature = "crypto")]
429 pub km: Option<KeyMaterial<'a>>,
430 /// Keeps the `'a` lifetime parameter meaningful when the `crypto`
431 /// feature (and with it, [`Self::km`]) is compiled out.
432 #[cfg(not(feature = "crypto"))]
433 _phantom: core::marker::PhantomData<&'a ()>,
434}
435
436/// Walks a handshake packet's extension blocks, decoding the ones this crate
437/// understands. Returns [`Error::InvalidField`] (mapped by the caller to a
438/// [`RejectionReason::Rogue`] rejection) on any decode failure, rather than
439/// panicking — extension content is untrusted peer input.
440pub(crate) fn parse_peer_extensions<'a>(
441 hp: &HandshakePacket<'a>,
442) -> Result<ParsedPeerExtensions<'a>> {
443 use crate::packet::ExtensionType;
444
445 let mut out = ParsedPeerExtensions::default();
446 for block in hp.extensions.iter() {
447 let block = block.map_err(|_| Error::InvalidField {
448 what: "handshake extensions",
449 reason: "malformed extension block",
450 })?;
451 match block.ext_type {
452 ExtensionType::HsReq | ExtensionType::HsRsp => {
453 let msg = block.as_hs_ext_message().map_err(|_| Error::InvalidField {
454 what: "handshake extension message",
455 reason: "malformed HSREQ/HSRSP contents",
456 })?;
457 out.hs_msg = Some(msg);
458 }
459 ExtensionType::Sid => {
460 let sid = block.as_stream_id().map_err(|_| Error::InvalidField {
461 what: "stream ID extension",
462 reason: "malformed or non-UTF-8 contents",
463 })?;
464 out.stream_id = Some(sid);
465 }
466 ExtensionType::Group => {
467 let g = block
468 .as_group_membership()
469 .map_err(|_| Error::InvalidField {
470 what: "group membership extension",
471 reason: "malformed contents",
472 })?;
473 out.group = Some(g);
474 }
475 #[cfg(feature = "crypto")]
476 ExtensionType::KmReq | ExtensionType::KmRsp => {
477 let km = block.as_key_material().map_err(|_| Error::InvalidField {
478 what: "key material extension",
479 reason: "malformed contents",
480 })?;
481 out.km = Some(km);
482 }
483 _ => {}
484 }
485 }
486 Ok(out)
487}
488
489// ---------------------------------------------------------------------------
490// §6.1.5 Key Material Exchange — piggybacked on the CONCLUSION extensions.
491// ---------------------------------------------------------------------------
492
493/// Build the Key Material handshake extension block (`Extension Type` +
494/// `Extension Length` + contents — ready to append after the mandatory
495/// HSREQ block) offering `crypto.sek`, wrapped under a KEK derived from
496/// `crypto.passphrase`/`crypto.salt` (`draft-sharabayko-srt-01` §6.1.5 "sent
497/// by the connection initiator ... to the responder", §6.2.1's `Wrap =
498/// AESkw(KEK, SEK)`). Always encodes `KK = Even` — this crate's convention
499/// for the handshake-negotiated initial SEK; §6.1.6's odd/even alternation
500/// only matters from the first KM Refresh onward (see [`crate::km_refresh`]).
501#[cfg(feature = "crypto")]
502pub(crate) fn build_key_material_extension(crypto: &CryptoConfig) -> Result<Vec<u8>> {
503 let kek = crate::crypto::derive_kek(&crypto.passphrase, &crypto.salt, crypto.sek.len())?;
504 let (icv, wrapped) = crate::crypto::wrap_sek(&kek, &crypto.sek)?;
505 let km = KeyMaterial {
506 kk: KmKeyFlag::Even,
507 keki: 0,
508 cipher: Cipher::AesCtr,
509 auth: KmAuth::None,
510 se: StreamEncapsulation::Unspecified,
511 salt: &crypto.salt,
512 icv,
513 x_sek: &wrapped,
514 o_sek: None,
515 };
516 let mut buf = alloc::vec![0u8; km.serialized_len()];
517 km.serialize_into(&mut buf)?;
518 crate::packet::handshake::build_extension_block(crate::packet::ExtensionType::KmReq, &buf)
519}
520
521/// A recovered/negotiated `(SEK, Salt)` pair.
522#[cfg(feature = "crypto")]
523pub(crate) type RecoveredSek = (Vec<u8>, [u8; crate::crypto::SALT_LEN]);
524
525/// Recover the SEK a peer's Key Material offered, given this side's shared
526/// passphrase (§6.1.5/§6.3.1: "the responder MUST know the passphrase ...
527/// everything else needed is extracted from the Keying Material message").
528/// Returns the recovered SEK bytes and the 16-byte Salt — both required by
529/// [`crate::crypto::aes_ctr_apply`].
530///
531/// # Errors
532/// [`Error::InvalidField`] if the Salt is not 16 bytes (the only Salt length
533/// this crate's Key Material codec accepts); otherwise propagates
534/// [`crate::crypto::unwrap_sek`]'s error (RFC 3394 wrap-integrity failure —
535/// wrong passphrase, §6.1.5's "it does not have the SEK" case).
536#[cfg(feature = "crypto")]
537pub(crate) fn recover_sek(passphrase: &[u8], km: &KeyMaterial<'_>) -> Result<RecoveredSek> {
538 if km.salt.len() != crate::crypto::SALT_LEN {
539 return Err(Error::InvalidField {
540 what: "Key Material Salt",
541 reason: "must be 16 bytes (the only Salt length this crate's codec accepts)",
542 });
543 }
544 let mut salt = [0u8; crate::crypto::SALT_LEN];
545 salt.copy_from_slice(km.salt);
546 let kek = crate::crypto::derive_kek(passphrase, &salt, km.x_sek.len())?;
547 let sek = crate::crypto::unwrap_sek(&kek, &km.icv, km.x_sek)?;
548 Ok((sek, salt))
549}
550
551/// Re-serialize a parsed Key Material message and wrap it as a `KmRsp`
552/// extension block — the responder's confirmation echo (§6.1.5: "the
553/// responder ... echoes the same KM message back to prove it derived the
554/// same SEK").
555#[cfg(feature = "crypto")]
556pub(crate) fn echo_key_material_as_response(km: &KeyMaterial<'_>) -> Result<Vec<u8>> {
557 let mut buf = alloc::vec![0u8; km.serialized_len()];
558 km.serialize_into(&mut buf)?;
559 crate::packet::handshake::build_extension_block(crate::packet::ExtensionType::KmRsp, &buf)
560}
561
562/// Verify a Listener's echoed Key Material matches what this Caller sent.
563/// Recomputed deterministically from `crypto` (the same passphrase/salt/sek
564/// always wraps to the same bytes, §6.2.1) rather than requiring the Caller
565/// to keep the original wrap bytes around.
566#[cfg(feature = "crypto")]
567pub(crate) fn verify_km_echo(crypto: &CryptoConfig, echoed: &KeyMaterial<'_>) -> bool {
568 let kek = match crate::crypto::derive_kek(&crypto.passphrase, &crypto.salt, crypto.sek.len()) {
569 Ok(k) => k,
570 Err(_) => return false,
571 };
572 let (icv, wrapped) = match crate::crypto::wrap_sek(&kek, &crypto.sek) {
573 Ok(v) => v,
574 Err(_) => return false,
575 };
576 echoed.salt == crypto.salt.as_slice() && echoed.icv == icv && echoed.x_sek == wrapped.as_slice()
577}
578
579/// The greater-of-both-parties TSBPD latency rule (§4.3.1.2).
580pub(crate) fn negotiate_latency_ms(local_latency_ms: u16, peer_msg: &HsExtMessage) -> u16 {
581 local_latency_ms
582 .max(peer_msg.receiver_tsbpd_delay_ms)
583 .max(peer_msg.sender_tsbpd_delay_ms)
584}
585
586/// A simple, deterministic, non-cryptographic mix for deriving a SYN Cookie
587/// from caller-supplied inputs (`draft-sharabayko-srt-01` §4.3.1.1: "a cookie
588/// that is crafted based on host, port and current time with 1 minute
589/// accuracy"). The draft specifies the semantic inputs, not a wire algorithm;
590/// this crate's core never reads a clock, so `time_bucket` (e.g. UNIX time /
591/// 60) must come from the caller/driver.
592pub fn derive_cookie(peer_key: u64, time_bucket: u32, secret: u64) -> u32 {
593 // A splitmix64-style avalanche over the combined inputs.
594 let mut x = peer_key ^ (u64::from(time_bucket).wrapping_mul(0x9E37_79B9_7F4A_7C15)) ^ secret;
595 x ^= x >> 30;
596 x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
597 x ^= x >> 27;
598 x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
599 x ^= x >> 31;
600 (x as u32) | 1 // never 0, so it is never confused with "no cookie yet"
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 #[test]
608 fn rejection_reason_round_trips_table_7() {
609 let all = [
610 RejectionReason::Unknown,
611 RejectionReason::System,
612 RejectionReason::Peer,
613 RejectionReason::Resource,
614 RejectionReason::Rogue,
615 RejectionReason::Backlog,
616 RejectionReason::Ipe,
617 RejectionReason::Close,
618 RejectionReason::Version,
619 RejectionReason::RdvCookie,
620 RejectionReason::BadSecret,
621 RejectionReason::Unsecure,
622 RejectionReason::MessageApi,
623 RejectionReason::Congestion,
624 RejectionReason::Filter,
625 RejectionReason::Group,
626 ];
627 for (i, r) in all.iter().enumerate() {
628 assert_eq!(r.to_bits(), 1000 + i as u32);
629 assert_eq!(RejectionReason::from_bits(r.to_bits()), *r);
630 assert_eq!(
631 RejectionReason::from_handshake_type(r.to_handshake_type()),
632 Some(*r)
633 );
634 }
635 }
636
637 #[test]
638 fn non_rejection_handshake_types_are_not_a_rejection() {
639 for ht in [
640 HandshakeType::Induction,
641 HandshakeType::Conclusion,
642 HandshakeType::Wavehand,
643 HandshakeType::Agreement,
644 HandshakeType::Done,
645 ] {
646 assert_eq!(RejectionReason::from_handshake_type(ht), None);
647 }
648 }
649
650 #[test]
651 fn derive_cookie_is_deterministic_and_nonzero() {
652 let a = derive_cookie(0x1234_5678_9ABC_DEF0, 12345, 0xDEAD_BEEF);
653 let b = derive_cookie(0x1234_5678_9ABC_DEF0, 12345, 0xDEAD_BEEF);
654 assert_eq!(a, b);
655 assert_ne!(a, 0);
656 let c = derive_cookie(0x1234_5678_9ABC_DEF1, 12345, 0xDEAD_BEEF);
657 assert_ne!(a, c);
658 }
659}