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