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