Skip to content

Commit 5c205ce

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 1f05a58 commit 5c205ce

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 {
@@ -292,4 +345,91 @@ mod tests {
292345
assert_eq!(msg.cmd, HidCommand::Msg);
293346
assert_eq!(msg.payload, vec![0x0A, 0x0B, 0x0C, 0x0D, 0x0E]);
294347
}
348+
349+
#[test]
350+
fn parse_continuation_with_wrong_cid_is_rejected() {
351+
let mut parser = HidMessageParser::new();
352+
assert_eq!(
353+
parser
354+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x05, 0x0A])
355+
.unwrap(),
356+
HidMessageParserState::MorePacketsExpected
357+
);
358+
// Continuation from a different channel.
359+
let err = parser
360+
.update(&[0xD0, 0xD1, 0xD2, 0xD3, 0x00, 0x0B, 0x0C])
361+
.unwrap_err();
362+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
363+
}
364+
365+
#[test]
366+
fn parse_continuation_with_non_zero_first_seq_is_rejected() {
367+
let mut parser = HidMessageParser::new();
368+
assert_eq!(
369+
parser
370+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x05, 0x0A])
371+
.unwrap(),
372+
HidMessageParserState::MorePacketsExpected
373+
);
374+
// First continuation must have SEQ=0.
375+
let err = parser
376+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x01, 0x0B, 0x0C])
377+
.unwrap_err();
378+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
379+
}
380+
381+
#[test]
382+
fn parse_continuation_with_non_monotonic_seq_is_rejected() {
383+
let mut parser = HidMessageParser::new();
384+
assert_eq!(
385+
parser
386+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x07, 0x0A])
387+
.unwrap(),
388+
HidMessageParserState::MorePacketsExpected
389+
);
390+
assert_eq!(
391+
parser
392+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x00, 0x0B, 0x0C])
393+
.unwrap(),
394+
HidMessageParserState::MorePacketsExpected
395+
);
396+
// Skipping SEQ=1 and jumping to SEQ=2 is not allowed.
397+
let err = parser
398+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x02, 0x0D, 0x0E])
399+
.unwrap_err();
400+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
401+
}
402+
403+
#[test]
404+
fn parse_init_packet_after_init_is_rejected() {
405+
let mut parser = HidMessageParser::new();
406+
assert_eq!(
407+
parser
408+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x05, 0x0A])
409+
.unwrap(),
410+
HidMessageParserState::MorePacketsExpected
411+
);
412+
// Another init packet (high bit set on byte 4) for a new transaction.
413+
let err = parser
414+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x83, 0x00, 0x05, 0x0B])
415+
.unwrap_err();
416+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
417+
}
418+
419+
#[test]
420+
fn parse_all_zero_packet_is_rejected() {
421+
let mut parser = HidMessageParser::new();
422+
let err = parser.update(&[0u8; 64]).unwrap_err();
423+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
424+
}
425+
426+
#[test]
427+
fn parse_first_packet_must_be_init_packet() {
428+
// High bit of byte 4 cleared means continuation; not allowed first.
429+
let mut parser = HidMessageParser::new();
430+
let err = parser
431+
.update(&[0xC0, 0xC1, 0xC2, 0xC3, 0x00, 0x00, 0x05, 0x0A])
432+
.unwrap_err();
433+
assert_eq!(err.kind(), IOErrorKind::InvalidData);
434+
}
295435
}

0 commit comments

Comments
 (0)