Skip to content

Commit 9d2dc9d

Browse files
fix(hid): validate CID, SEQ, and init bit on continuation packets
Per CTAP 2.2 §11.2.4, HidMessageParser must reject any continuation packet whose CID differs from the initial packet, whose SEQ does not start at 0 and increment monotonically, or whose 5th byte has the high bit set (i.e., an init packet for a different transaction). Also reject all-zero packets at the parser level. The previous code silently dropped them as a workaround for hidapi `read_timeout` returning a zeroed buffer on timeout; that workaround now belongs at the read layer, gated on bytes_read == 0. Spec refs: - CTAP 2.2 §11.2.4 USB HID message and packet structure - FIDO U2F HID Protocol v1.2 (the older base spec)
1 parent e00499c commit 9d2dc9d

1 file changed

Lines changed: 144 additions & 4 deletions

File tree

libwebauthn/src/transport/hid/framing.rs

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ use std::io::{Cursor as IOCursor, Error as IOError, ErrorKind as IOErrorKind};
33

44
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
55
use num_enum::{IntoPrimitive, TryFromPrimitive};
6-
use tracing::{debug, error};
6+
use tracing::error;
77

88
const BROADCAST_CID: u32 = 0xFFFFFFFF;
99
const PACKET_INITIAL_HEADER_SIZE: usize = 7;
1010
const PACKET_INITIAL_CMD_MASK: u8 = 0x80;
1111
const PACKET_CONT_HEADER_SIZE: usize = 5;
12+
const PACKET_CONT_SEQ_MAX: u8 = 0x7F;
1213

1314
#[derive(Debug, IntoPrimitive, TryFromPrimitive, Copy, Clone, PartialEq)]
1415
#[repr(u8)]
@@ -121,17 +122,69 @@ impl HidMessageParser {
121122
if (self.packets.is_empty() && packet.len() < PACKET_INITIAL_HEADER_SIZE)
122123
|| packet.len() < PACKET_CONT_HEADER_SIZE + 1
123124
{
124-
error!("Packet length in invalid");
125+
error!("Packet length is invalid");
125126
return Err(IOError::new(
126127
IOErrorKind::InvalidInput,
127128
"Packet length is invalid",
128129
));
129130
}
131+
132+
// CID 0x00000000 is reserved (CTAP 2.2 §11.2.4); reject all-zero frames.
130133
if packet.iter().all(|&b| b == 0) {
131-
debug!("Received unexpected packet of all zeroes, ignoring"); // ?!
134+
error!("Received all-zero packet, rejecting");
135+
return Err(IOError::new(
136+
IOErrorKind::InvalidData,
137+
"All-zero packet is not a valid CTAPHID frame",
138+
));
139+
}
140+
141+
if self.packets.is_empty() {
142+
// First packet must be an initialization packet: high bit of
143+
// byte 4 set (CTAP 2.2 §11.2.4).
144+
if packet[4] & PACKET_INITIAL_CMD_MASK == 0 {
145+
error!("First packet is not an initialization packet");
146+
return Err(IOError::new(
147+
IOErrorKind::InvalidData,
148+
"First packet must be an initialization packet",
149+
));
150+
}
132151
} else {
133-
self.packets.push(Vec::from(packet));
152+
// Continuation packets: same CID as the initial packet, SEQ has
153+
// high bit cleared, SEQ starts at 0 and increments monotonically.
154+
let initial = &self.packets[0];
155+
if packet[..4] != initial[..4] {
156+
error!("Continuation packet CID does not match initial packet");
157+
return Err(IOError::new(
158+
IOErrorKind::InvalidData,
159+
"Continuation packet CID mismatch",
160+
));
161+
}
162+
let seq = packet[4];
163+
if seq & PACKET_INITIAL_CMD_MASK != 0 {
164+
error!(seq, "Unexpected init packet during continuation");
165+
return Err(IOError::new(
166+
IOErrorKind::InvalidData,
167+
"Unexpected initialization packet during continuation",
168+
));
169+
}
170+
let expected_seq = (self.packets.len() - 1) as u8;
171+
if expected_seq > PACKET_CONT_SEQ_MAX {
172+
error!(seq, "Continuation count exceeds maximum SEQ");
173+
return Err(IOError::new(
174+
IOErrorKind::InvalidData,
175+
"Too many continuation packets",
176+
));
177+
}
178+
if seq != expected_seq {
179+
error!(seq, expected_seq, "Out-of-order continuation SEQ");
180+
return Err(IOError::new(
181+
IOErrorKind::InvalidData,
182+
"Out-of-order continuation SEQ",
183+
));
184+
}
134185
}
186+
187+
self.packets.push(Vec::from(packet));
135188
if self.more_packets_needed() {
136189
Ok(HidMessageParserState::MorePacketsExpected)
137190
} else {
@@ -299,4 +352,91 @@ mod tests {
299352
assert_eq!(msg.cmd, HidCommand::Msg);
300353
assert_eq!(msg.payload, vec![0x0A, 0x0B, 0x0C, 0x0D, 0x0E]);
301354
}
355+
356+
#[test]
357+
fn parse_continuation_with_wrong_cid_is_rejected() {
358+
let mut parser = HidMessageParser::new();
359+
assert_eq!(
360+
parser
361+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x05, 0x0A])
362+
.unwrap(),
363+
HidMessageParserState::MorePacketsExpected
364+
);
365+
// Continuation from a different channel.
366+
let err = parser
367+
.update(&[0xD0, 0xD1, 0xD2, 0xD3, 0x00, 0x0B, 0x0C])
368+
.unwrap_err();
369+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
370+
}
371+
372+
#[test]
373+
fn parse_continuation_with_non_zero_first_seq_is_rejected() {
374+
let mut parser = HidMessageParser::new();
375+
assert_eq!(
376+
parser
377+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x05, 0x0A])
378+
.unwrap(),
379+
HidMessageParserState::MorePacketsExpected
380+
);
381+
// First continuation must have SEQ=0.
382+
let err = parser
383+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x01, 0x0B, 0x0C])
384+
.unwrap_err();
385+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
386+
}
387+
388+
#[test]
389+
fn parse_continuation_with_non_monotonic_seq_is_rejected() {
390+
let mut parser = HidMessageParser::new();
391+
assert_eq!(
392+
parser
393+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x07, 0x0A])
394+
.unwrap(),
395+
HidMessageParserState::MorePacketsExpected
396+
);
397+
assert_eq!(
398+
parser
399+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x00, 0x0B, 0x0C])
400+
.unwrap(),
401+
HidMessageParserState::MorePacketsExpected
402+
);
403+
// Skipping SEQ=1 and jumping to SEQ=2 is not allowed.
404+
let err = parser
405+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x02, 0x0D, 0x0E])
406+
.unwrap_err();
407+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
408+
}
409+
410+
#[test]
411+
fn parse_init_packet_after_init_is_rejected() {
412+
let mut parser = HidMessageParser::new();
413+
assert_eq!(
414+
parser
415+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x05, 0x0A])
416+
.unwrap(),
417+
HidMessageParserState::MorePacketsExpected
418+
);
419+
// Another init packet (high bit set on byte 4) for a new transaction.
420+
let err = parser
421+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x05, 0x0B])
422+
.unwrap_err();
423+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
424+
}
425+
426+
#[test]
427+
fn parse_all_zero_packet_is_rejected() {
428+
let mut parser = HidMessageParser::new();
429+
let err = parser.update(&[0u8; 64]).unwrap_err();
430+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
431+
}
432+
433+
#[test]
434+
fn parse_first_packet_must_be_init_packet() {
435+
// High bit of byte 4 cleared means continuation; not allowed first.
436+
let mut parser = HidMessageParser::new();
437+
let err = parser
438+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x00, 0x00, 0x05, 0x0A])
439+
.unwrap_err();
440+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
441+
}
302442
}

0 commit comments

Comments
 (0)