Skip to main content

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}