Skip to content

Commit

Permalink
Refactors HID connections to support busy response
Browse files Browse the repository at this point in the history
The main intended difference is that OpenSK now responds with ERR_CHANNEL_BUSY
when it receives a packet while waiting for touch in in a CTAP2 command.

To support this more elegantly, we changed `HidConnection` to have a
`send` and `recv` instead of `send_and_maybe_recv` for a cleaner API.
This also resolves an older TODO to respond to incoming channels on
the other USB endpoint.

It doesn't process the incoming traffic correctly, but unblock the host
at least, and tells it to come back later. We still only allow one
active channel at the same time.

We now don't need the `TRANSMIT_AND_RECEIVE` syscall anymore. I didn't
remove it from the Tock pack for simplicity, but cleaned up
libtock-drivers.

The Env API changes from having one connection per endpoint, to having
one send function that takes an endpoint, and a receive function that
receives on all endpoints at the same time.
The `HidConnection` already received on all endpoints before, which was
inconsistent with the existance of, e.g., `VendorHidConnection` being
able to receive on the main endpoint.

Since we now have a cleaner send and receive API, we use this in
`src/main.rs` and simplify it a bit, while making it more consistent
with other calls to the HID API.

I found an additional inaccuracy in our implementation while
refactoring: We want to send keepalives every 100 ms. To do so, we first
wait for a button callback for 100 ms. Then we send the keepalive and
check if a cancel packet appeared. What should have happened instead is
that we listen for HID packets and button presses at the same time
during these 100 ms. If nothing happens that stops the loop, we send the
keepalive.

The old implementation would, in some implementations, wait 200 ms for
each keepalive: First 100 for touch, then 100 for `send_and_maybe_receive`.

The new implementation can loop faster in case there is a lot of unrelated
HID traffic. To make the touch timeout last 30 seconds, we introduce an
extra timer and loop until time is up. This might still make us blink
the LEDs too fast, but that is already the case in the main loop, and
generally would need a bigger refactoring.
The only simple workaround to make the LEDs slightly more precise is to
wait for touch and packets until it is time to flip the LEDs, and if we
return too early within some thresholds, wait again. If we care enough,
we can fix that in a later PR.

Not sure how to make the test work that I removed in ctap/mod.rs. I'd
need to advance the time while the loop is running. Setting a user
presence function that advances time seems hard with the Rust borrowing
rules.
  • Loading branch information
kaczmarczyck committed Aug 30, 2024
1 parent 7da88b0 commit 214b004
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 394 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion libraries/opensk/src/api/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ pub struct SendOrRecvError;
pub type SendOrRecvResult = Result<SendOrRecvStatus, SendOrRecvError>;

pub trait HidConnection {
fn send_and_maybe_recv(&mut self, buf: &mut [u8; 64], timeout_ms: usize) -> SendOrRecvResult;
fn send(&mut self, buf: &[u8; 64], endpoint: UsbEndpoint) -> SendOrRecvResult;
fn recv(&mut self, buf: &mut [u8; 64], timeout_ms: usize) -> SendOrRecvResult;
}

#[cfg(test)]
Expand Down
8 changes: 7 additions & 1 deletion libraries/opensk/src/api/user_presence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::api::connection::UsbEndpoint;

#[derive(Debug)]
pub enum UserPresenceError {
/// User explicitly declined user presence check.
Expand All @@ -24,7 +26,10 @@ pub enum UserPresenceError {
Fail,
}

pub type UserPresenceResult = Result<(), UserPresenceError>;
pub type UserPresenceResult = (
Result<(), UserPresenceError>,
Option<([u8; 64], UsbEndpoint)>,
);

pub trait UserPresence {
/// Initializes for a user presence check.
Expand All @@ -35,6 +40,7 @@ pub trait UserPresence {
/// Waits until user presence is confirmed, rejected, or the given timeout expires.
///
/// Must be called between calls to [`Self::check_init`] and [`Self::check_complete`].
/// Additionally returns a packet if one was received during the wait.
fn wait_with_timeout(&mut self, timeout_ms: usize) -> UserPresenceResult;

/// Finalizes a user presence check.
Expand Down
24 changes: 24 additions & 0 deletions libraries/opensk/src/ctap/hid/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,11 @@ impl<E: Env> CtapHid<E> {
})
}

/// Generates a the HID response packets for a busy error.
pub fn busy_error(cid: ChannelID) -> HidPacketIterator {
Self::split_message(Self::error_message(cid, CtapHidError::ChannelBusy))
}

#[cfg(test)]
pub fn new_initialized() -> (Self, ChannelID) {
(
Expand Down Expand Up @@ -633,6 +638,25 @@ mod test {
}
}

#[test]
fn test_busy_error() {
let cid = [0x12, 0x34, 0x56, 0x78];
let mut response = CtapHid::<TestEnv>::busy_error(cid);
let mut expected_packet = [0x00; 64];
expected_packet[..8].copy_from_slice(&[
0x12,
0x34,
0x56,
0x78,
0xBF,
0x00,
0x01,
CtapHidError::ChannelBusy as u8,
]);
assert_eq!(response.next(), Some(expected_packet));
assert_eq!(response.next(), None);
}

#[test]
fn test_process_single_packet() {
let cid = [0x12, 0x34, 0x56, 0x78];
Expand Down
180 changes: 64 additions & 116 deletions libraries/opensk/src/ctap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ use self::data_formats::{
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource,
PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm,
};
use self::hid::{ChannelID, CtapHid, CtapHidCommand, KeepaliveStatus, ProcessedPacket};
use self::hid::{
ChannelID, CtapHid, CtapHidCommand, HidPacketIterator, KeepaliveStatus, ProcessedPacket,
};
use self::large_blobs::LargeBlobState;
use self::response::{
AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse,
Expand Down Expand Up @@ -149,11 +151,11 @@ pub enum Transport {
}

impl Transport {
pub fn hid_connection<E: Env>(self, env: &mut E) -> &mut E::HidConnection {
pub fn usb_endpoint(self) -> UsbEndpoint {
match self {
Transport::MainHid => env.main_hid_connection(),
Transport::MainHid => UsbEndpoint::MainHid,
#[cfg(feature = "vendor_hid")]
Transport::VendorHid => env.vendor_hid_connection(),
Transport::VendorHid => UsbEndpoint::VendorHid,
}
}
}
Expand Down Expand Up @@ -255,120 +257,80 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str {
}
}

// Sends keepalive packet during user presence checking. If user agent replies with CANCEL response,
// returns Err(UserPresenceError::Canceled).
fn send_keepalive_up_needed<E: Env>(
env: &mut E,
channel: Channel,
timeout_ms: usize,
) -> Result<(), UserPresenceError> {
let (cid, transport) = match channel {
Channel::MainHid(cid) => (cid, Transport::MainHid),
#[cfg(feature = "vendor_hid")]
Channel::VendorHid(cid) => (cid, Transport::VendorHid),
};
let keepalive_msg = CtapHid::<E>::keepalive(cid, KeepaliveStatus::UpNeeded);
for mut pkt in keepalive_msg {
let ctap_hid_connection = transport.hid_connection(env);
match ctap_hid_connection.send_and_maybe_recv(&mut pkt, timeout_ms) {
/// Send non-critical packets using fire-and-forget.
fn send_packets<E: Env>(env: &mut E, endpoint: UsbEndpoint, packets: HidPacketIterator) {
for pkt in packets {
match env.hid_connection().send(&pkt, endpoint) {
Ok(SendOrRecvStatus::Timeout) => {
debug_ctap!(env, "Sending a KEEPALIVE packet timed out");
// The client is likely unresponsive, but let's retry.
}
Err(_) => panic!("Error sending KEEPALIVE packet"),
Ok(SendOrRecvStatus::Sent) => {
debug_ctap!(env, "Sent KEEPALIVE packet");
}
Ok(SendOrRecvStatus::Received(endpoint)) => {
let rx_transport = match endpoint {
UsbEndpoint::MainHid => Transport::MainHid,
#[cfg(feature = "vendor_hid")]
UsbEndpoint::VendorHid => Transport::VendorHid,
};
if rx_transport != transport {
debug_ctap!(
env,
"Received a packet on transport {:?} while sending a KEEPALIVE packet on transport {:?}",
rx_transport, transport
);
// Ignore this packet.
// TODO(liamjm): Support receiving packets on both interfaces.
continue;
}

// We only parse one packet, because we only care about CANCEL.
let (received_cid, processed_packet) = CtapHid::<E>::process_single_packet(&pkt);
if received_cid != cid {
debug_ctap!(
env,
"Received a packet on channel ID {:?} while sending a KEEPALIVE packet",
received_cid,
);
return Ok(());
}
match processed_packet {
ProcessedPacket::InitPacket { cmd, .. } => {
if cmd == CtapHidCommand::Cancel as u8 {
// We ignore the payload, we can't answer with an error code anyway.
debug_ctap!(env, "User presence check cancelled");
return Err(UserPresenceError::Canceled);
} else {
debug_ctap!(
env,
"Discarded packet with command {} received while sending a KEEPALIVE packet",
cmd,
);
}
}
ProcessedPacket::ContinuationPacket { .. } => {
debug_ctap!(
env,
"Discarded continuation packet received while sending a KEEPALIVE packet",
);
}
}
debug_ctap!(env, "Timeout sending packet");
}
Ok(SendOrRecvStatus::Sent) => (),
_ => panic!("Error sending packet"),
}
}
Ok(())
}

/// Blocks for user presence.
///
/// Returns an error in case of timeout, user declining presence request, or keepalive error.
pub fn check_user_presence<E: Env>(env: &mut E, channel: Channel) -> CtapResult<()> {
env.user_presence().check_init();
let loop_timer = env.clock().make_timer(TOUCH_TIMEOUT_MS);

// The timeout is N times the keepalive delay.
const TIMEOUT_ITERATIONS: usize = TOUCH_TIMEOUT_MS / KEEPALIVE_DELAY_MS;
let (cid, transport) = match channel {
Channel::MainHid(cid) => (cid, Transport::MainHid),
#[cfg(feature = "vendor_hid")]
Channel::VendorHid(cid) => (cid, Transport::VendorHid),
};
let endpoint = transport.usb_endpoint();

// All fallible functions are called without '?' operator to always reach
// check_complete(...) cleanup function.

let mut result = Err(UserPresenceError::Timeout);
for i in 0..=TIMEOUT_ITERATIONS {
// First presence check is made without timeout. That way Env implementation may return
// user presence check result immediately to client, without sending any keepalive packets.
result = env
.user_presence()
.wait_with_timeout(if i == 0 { 0 } else { KEEPALIVE_DELAY_MS });
if !matches!(result, Err(UserPresenceError::Timeout)) {
break;
while !env.clock().is_elapsed(&loop_timer) {
let (status, data) = env.user_presence().wait_with_timeout(KEEPALIVE_DELAY_MS);
if let Some((packet, rx_endpoint)) = data {
let (received_cid, processed_packet) = CtapHid::<E>::process_single_packet(&packet);
if rx_endpoint != endpoint || received_cid != cid {
debug_ctap!(
env,
"Received an unrelated packet on endpoint {:?} while sending a KEEPALIVE packet",
rx_endpoint,
);
let busy_error = CtapHid::<E>::busy_error(received_cid);
send_packets(env, rx_endpoint, busy_error);
}
match processed_packet {
ProcessedPacket::InitPacket { cmd, .. } => {
if cmd == CtapHidCommand::Cancel as u8 {
// Ignored, CANCEL specification says: "the authenticator MUST NOT reply"
debug_ctap!(env, "User presence check cancelled");
return Err(UserPresenceError::Canceled.into());
} else {
// Ignored. A client shouldn't try to talk to us on this channel yet.
debug_ctap!(
env,
"Discarded packet with command {} received while sending a KEEPALIVE packet",
cmd,
);
}
}
// Ignored. We likely ignore the init packet before as well.
ProcessedPacket::ContinuationPacket { .. } => {
debug_ctap!(
env,
"Discarded continuation packet received while sending a KEEPALIVE packet",
);
}
}
}
// TODO: this may take arbitrary time. Next wait's delay should be adjusted
// accordingly, so that all wait_with_timeout invocations are separated by
// equal time intervals. That way token indicators, such as LEDs, will blink
// with a consistent pattern.
let keepalive_result = send_keepalive_up_needed(env, channel, KEEPALIVE_DELAY_MS);
if keepalive_result.is_err() {
debug_ctap!(
env,
"Sending keepalive failed with error {:?}",
keepalive_result.as_ref().unwrap_err()
);
result = keepalive_result;
if !matches!(status, Err(UserPresenceError::Timeout)) {
result = status;
break;
}
let keepalive_msg = CtapHid::<E>::keepalive(cid, KeepaliveStatus::UpNeeded);
send_packets(env, endpoint, keepalive_msg);
}

env.user_presence().check_complete();
Expand Down Expand Up @@ -2290,7 +2252,8 @@ mod test {
#[test]
fn test_process_make_credential_cancelled() {
let mut env = TestEnv::default();
env.user_presence().set(|| Err(UserPresenceError::Canceled));
env.user_presence()
.set(|| (Err(UserPresenceError::Canceled), None));
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);

let make_credential_params = create_minimal_make_credential_parameters();
Expand Down Expand Up @@ -3170,7 +3133,8 @@ mod test {
#[test]
fn test_process_reset_cancelled() {
let mut env = TestEnv::default();
env.user_presence().set(|| Err(UserPresenceError::Canceled));
env.user_presence()
.set(|| (Err(UserPresenceError::Canceled), None));
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);

let reset_reponse = ctap_state.process_reset(&mut env, DUMMY_CHANNEL);
Expand Down Expand Up @@ -3396,22 +3360,6 @@ mod test {
assert!(matches!(response, Ok(_)));
}

#[test]
fn test_check_user_presence_timeout() {
// This will always return timeout.
fn user_presence_timeout() -> UserPresenceResult {
Err(UserPresenceError::Timeout)
}

let mut env = TestEnv::default();
env.user_presence().set(user_presence_timeout);
let response = check_user_presence(&mut env, DUMMY_CHANNEL);
assert!(matches!(
response,
Err(Ctap2StatusCode::CTAP2_ERR_USER_ACTION_TIMEOUT)
));
}

#[test]
fn test_channel_interleaving() {
let mut env = TestEnv::default();
Expand Down
8 changes: 2 additions & 6 deletions libraries/opensk/src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,8 @@ pub trait Env {

fn customization(&self) -> &Self::Customization;

/// I/O connection for sending packets implementing CTAP HID protocol.
fn main_hid_connection(&mut self) -> &mut Self::HidConnection;

/// I/O connection for sending packets implementing vendor extensions to CTAP HID protocol.
#[cfg(feature = "vendor_hid")]
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection;
/// I/O connection for sending HID packets.
fn hid_connection(&mut self) -> &mut Self::HidConnection;

/// Indicates that the last power cycle was not caused by user action.
fn boots_after_soft_reset(&self) -> bool;
Expand Down
20 changes: 10 additions & 10 deletions libraries/opensk/src/env/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

use crate::api::clock::Clock;
use crate::api::connection::{HidConnection, SendOrRecvResult, SendOrRecvStatus};
use crate::api::connection::{HidConnection, SendOrRecvResult, SendOrRecvStatus, UsbEndpoint};
use crate::api::crypto::software_crypto::SoftwareCrypto;
use crate::api::customization::DEFAULT_CUSTOMIZATION;
use crate::api::key_store;
Expand Down Expand Up @@ -128,17 +128,22 @@ impl Persist for TestEnv {
}

impl HidConnection for TestEnv {
fn send_and_maybe_recv(&mut self, _buf: &mut [u8; 64], _timeout_ms: usize) -> SendOrRecvResult {
// TODO: Implement I/O from canned requests/responses for integration testing.
// TODO: Implement I/O from canned requests/responses for integration testing.

fn send(&mut self, _buf: &[u8; 64], _endpoint: UsbEndpoint) -> SendOrRecvResult {
Ok(SendOrRecvStatus::Sent)
}

fn recv(&mut self, _buf: &mut [u8; 64], _timeout_ms: usize) -> SendOrRecvResult {
Ok(SendOrRecvStatus::Received(UsbEndpoint::MainHid))
}
}

impl Default for TestEnv {
fn default() -> Self {
let rng = StdRng::seed_from_u64(0);
let user_presence = TestUserPresence {
check: Box::new(|| Ok(())),
check: Box::new(|| (Ok(()), None)),
};
let storage = new_storage();
let store = Store::new(storage).ok().unwrap();
Expand Down Expand Up @@ -224,12 +229,7 @@ impl Env for TestEnv {
&self.customization
}

fn main_hid_connection(&mut self) -> &mut Self::HidConnection {
self
}

#[cfg(feature = "vendor_hid")]
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection {
fn hid_connection(&mut self) -> &mut Self {
self
}

Expand Down
Loading

0 comments on commit 214b004

Please sign in to comment.