phantom_protocol/transport/
path_validation_codec.rs1use crate::transport::path::PATH_CHALLENGE_LEN;
40use crate::transport::types::{PacketFlags, PacketHeader, PhantomPacket, SessionId, StreamId};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum PathValidationKind {
47 Challenge,
48 Response,
49}
50
51pub fn build_path_validation_packet(
58 session_id: SessionId,
59 path_id: u8,
60 packet_number: u64,
61 payload: [u8; PATH_CHALLENGE_LEN],
62) -> PhantomPacket {
63 let stream_id: StreamId = 0;
64 let header = PacketHeader::new(
65 session_id,
66 stream_id,
67 packet_number,
68 PacketFlags::new(PacketFlags::PATH_VALIDATION),
69 )
70 .with_path_id(path_id);
71 PhantomPacket::new(header, payload.to_vec())
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct ParsedPathValidation {
78 pub path_id: u8,
79 pub payload: [u8; PATH_CHALLENGE_LEN],
80}
81
82pub fn parse_path_validation(
92 packet: &PhantomPacket,
93) -> Result<Option<ParsedPathValidation>, PathValidationParseError> {
94 if !packet.header.flags.contains(PacketFlags::PATH_VALIDATION) {
95 return Ok(None);
96 }
97 if packet.payload.len() != PATH_CHALLENGE_LEN {
98 return Err(PathValidationParseError::WrongPayloadLength {
99 got: packet.payload.len(),
100 });
101 }
102 let mut buf = [0u8; PATH_CHALLENGE_LEN];
103 buf.copy_from_slice(&packet.payload);
104 Ok(Some(ParsedPathValidation {
105 path_id: packet.header.path_id,
106 payload: buf,
107 }))
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum PathValidationParseError {
113 WrongPayloadLength { got: usize },
115}
116
117impl std::fmt::Display for PathValidationParseError {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 Self::WrongPayloadLength { got } => write!(
121 f,
122 "PATH_VALIDATION payload length is {}, expected {}",
123 got, PATH_CHALLENGE_LEN
124 ),
125 }
126 }
127}
128
129impl std::error::Error for PathValidationParseError {}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 fn fixed_session_id() -> SessionId {
136 SessionId::from_bytes([0x42; 32])
137 }
138
139 #[test]
140 fn build_round_trip_preserves_path_id_and_payload() {
141 let payload = [0xAA; PATH_CHALLENGE_LEN];
142 let v2 = build_path_validation_packet(fixed_session_id(), 7, 42, payload);
143 assert_eq!(v2.header.path_id, 7);
144 assert!(v2.header.flags.contains(PacketFlags::PATH_VALIDATION));
145 assert_eq!(v2.header.stream_id, 0u16);
146 assert_eq!(v2.header.packet_number, 42u64);
147 assert_eq!(v2.payload, payload.to_vec());
148 }
149
150 #[test]
151 fn parse_path_validation_returns_payload_on_match() {
152 let payload = [0xCC; PATH_CHALLENGE_LEN];
153 let v2 = build_path_validation_packet(fixed_session_id(), 3, 1, payload);
154 let parsed = parse_path_validation(&v2).expect("ok").expect("some");
155 assert_eq!(parsed.path_id, 3);
156 assert_eq!(parsed.payload, payload);
157 }
158
159 #[test]
160 fn parse_returns_none_when_flag_missing() {
161 let header = PacketHeader::new(
162 fixed_session_id(),
163 0u16,
164 0u64,
165 PacketFlags::new(PacketFlags::ENCRYPTED), );
167 let p = PhantomPacket::new(header, vec![0u8; PATH_CHALLENGE_LEN]);
168 let parsed = parse_path_validation(&p).expect("no error");
169 assert!(parsed.is_none());
170 }
171
172 #[test]
173 fn parse_errors_on_wrong_payload_length() {
174 let header = PacketHeader::new(
175 fixed_session_id(),
176 0u16,
177 0u64,
178 PacketFlags::new(PacketFlags::PATH_VALIDATION),
179 );
180 let p = PhantomPacket::new(header, vec![0u8; 16]); let err = parse_path_validation(&p).expect_err("err");
182 assert_eq!(
183 err,
184 PathValidationParseError::WrongPayloadLength { got: 16 }
185 );
186 }
187
188 #[test]
189 fn challenge_and_response_are_wire_identical() {
190 let payload = [0x55; PATH_CHALLENGE_LEN];
194 let a = build_path_validation_packet(fixed_session_id(), 1, 5, payload);
195 let b = build_path_validation_packet(fixed_session_id(), 1, 5, payload);
196
197 let buf_a = a.to_wire();
198 let buf_b = b.to_wire();
199 assert_eq!(buf_a, buf_b);
200 }
201
202 #[test]
203 fn kind_enum_round_trips_for_documentation() {
204 assert_ne!(PathValidationKind::Challenge, PathValidationKind::Response);
208 }
209
210 #[test]
215 fn full_challenge_response_round_trip_via_codec() {
216 use crate::transport::path::{PathRegistry, PathStateKind, RegistrationResult};
217
218 let side_a = PathRegistry::new();
222 let side_b = PathRegistry::new();
223 let path_id: u8 = 5;
224
225 assert_eq!(side_a.register(path_id), RegistrationResult::Created);
227 let challenge = side_a.issue_challenge(path_id).expect("challenge issued");
228 let session_id = fixed_session_id();
229
230 let outgoing = build_path_validation_packet(session_id, path_id, 0, challenge);
234 let buf = outgoing.to_wire();
235 let v2 = PhantomPacket::from_wire(&buf).expect("deserialize");
236 let parsed = parse_path_validation(&v2)
237 .expect("ok")
238 .expect("flag matched");
239 assert_eq!(parsed.path_id, path_id);
240 assert_eq!(parsed.payload, challenge);
241
242 let response = build_path_validation_packet(session_id, path_id, 0, parsed.payload);
245 let buf2 = response.to_wire();
246 let v2_echoed = PhantomPacket::from_wire(&buf2).expect("deserialize");
247 let echoed_parsed = parse_path_validation(&v2_echoed)
248 .expect("ok")
249 .expect("flag matched");
250
251 let accepted = side_a.verify_response(echoed_parsed.path_id, &echoed_parsed.payload);
253 assert!(accepted, "responder's echo must validate");
254 assert_eq!(side_a.state(path_id), Some(PathStateKind::Validated));
255
256 let _ = side_b;
259 }
260
261 #[test]
262 fn tampered_response_fails_validation() {
263 use crate::transport::path::{PathRegistry, PathStateKind};
264
265 let validator = PathRegistry::new();
266 validator.register(2);
267 let challenge = validator.issue_challenge(2).expect("challenge");
268
269 let mut tampered = challenge;
271 tampered[7] ^= 0xFF;
272 let v2 = build_path_validation_packet(fixed_session_id(), 2, 0, tampered);
273 let parsed = parse_path_validation(&v2).unwrap().unwrap();
274
275 assert!(!validator.verify_response(parsed.path_id, &parsed.payload));
276 assert_eq!(validator.state(2), Some(PathStateKind::Failed));
277 }
278}