introduces Transport and Channel (#444)
This commit is contained in:
@@ -28,7 +28,7 @@ use ctap2::ctap::hid::{
|
||||
ChannelID, CtapHidCommand, HidPacket, HidPacketIterator, Message, MessageAssembler,
|
||||
};
|
||||
use ctap2::env::test::TestEnv;
|
||||
use ctap2::Ctap;
|
||||
use ctap2::{Ctap, Transport};
|
||||
|
||||
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
||||
|
||||
@@ -72,7 +72,9 @@ fn initialize(ctap: &mut Ctap<TestEnv>) -> ChannelID {
|
||||
let mut assembler_reply = MessageAssembler::new();
|
||||
let mut result_cid: ChannelID = Default::default();
|
||||
for pkt_request in HidPacketIterator::new(message).unwrap() {
|
||||
for pkt_reply in ctap.process_hid_packet(&pkt_request, CtapInstant::new(0)) {
|
||||
for pkt_reply in
|
||||
ctap.process_hid_packet(&pkt_request, Transport::MainHid, CtapInstant::new(0))
|
||||
{
|
||||
if let Ok(Some(result)) = assembler_reply.parse_packet(&pkt_reply, CtapInstant::new(0))
|
||||
{
|
||||
result_cid.copy_from_slice(&result.payload[8..12]);
|
||||
@@ -111,7 +113,9 @@ fn process_message(data: &[u8], ctap: &mut Ctap<TestEnv>) {
|
||||
if let Some(hid_packet_iterator) = HidPacketIterator::new(message) {
|
||||
let mut assembler_reply = MessageAssembler::new();
|
||||
for pkt_request in hid_packet_iterator {
|
||||
for pkt_reply in ctap.process_hid_packet(&pkt_request, CtapInstant::new(0)) {
|
||||
for pkt_reply in
|
||||
ctap.process_hid_packet(&pkt_request, Transport::MainHid, CtapInstant::new(0))
|
||||
{
|
||||
// Only checks for assembling crashes, not for semantics.
|
||||
let _ = assembler_reply.parse_packet(&pkt_reply, CtapInstant::new(0));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::ctap::hid::ChannelID;
|
||||
use crate::ctap::hid::{
|
||||
CtapHid, CtapHidCommand, CtapHidError, HidPacket, HidPacketIterator, Message,
|
||||
};
|
||||
use crate::ctap::TimedPermission;
|
||||
use crate::ctap::{Channel, TimedPermission};
|
||||
use crate::env::Env;
|
||||
use embedded_time::duration::Milliseconds;
|
||||
|
||||
@@ -91,7 +91,8 @@ impl MainHid {
|
||||
// Each transaction is atomic, so we process the command directly here and
|
||||
// don't handle any other packet in the meantime.
|
||||
// TODO: Send "Processing" type keep-alive packets in the meantime.
|
||||
let response = ctap_state.process_command(env, &message.payload, cid, now);
|
||||
let response =
|
||||
ctap_state.process_command(env, &message.payload, Channel::MainHid(cid), now);
|
||||
Message {
|
||||
cid,
|
||||
cmd: CtapHidCommand::Cbor,
|
||||
|
||||
191
src/ctap/mod.rs
191
src/ctap/mod.rs
@@ -125,6 +125,36 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa
|
||||
alg: SignatureAlgorithm::ES256,
|
||||
};
|
||||
|
||||
/// Transports supported by OpenSK.
|
||||
///
|
||||
/// An OpenSK library user annotates incoming data with this data type.
|
||||
///
|
||||
/// The difference between this data type and `AuthenticatorTransport` is that the latter
|
||||
/// corresponds to the communication defined in the CTAP specification. This data type describes
|
||||
/// the hardware path a packet took.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Transport {
|
||||
/// Corresponds to CTAP's USB transport.
|
||||
MainHid,
|
||||
/// No equivalent in CTAP, used for communication outside the specification.
|
||||
VendorHid,
|
||||
}
|
||||
|
||||
/// Communication channels between authenticator and client.
|
||||
///
|
||||
/// From OpenSK's perspective, a channel represents a client. When we receive data from a new
|
||||
/// channel, we have to assume it's a new client.
|
||||
///
|
||||
/// For HID, communication channels coincide with the channel ID. NFC and HID transports are unique
|
||||
/// channels themselves.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Channel {
|
||||
/// Corresponds to CTAP's USB transport.
|
||||
MainHid(ChannelID),
|
||||
/// No equivalent in CTAP, used for communication outside the specification.
|
||||
VendorHid(ChannelID),
|
||||
}
|
||||
|
||||
// Helpers to perform CBOR read/write while respecting CTAP2 nesting limits.
|
||||
pub fn cbor_read(encoded_cbor: &[u8]) -> Result<cbor::Value, Ctap2StatusCode> {
|
||||
cbor::reader::read_nested(encoded_cbor, Some(MAX_CBOR_NESTING_DEPTH))
|
||||
@@ -448,7 +478,7 @@ impl CtapState {
|
||||
&mut self,
|
||||
env: &mut impl Env,
|
||||
command_cbor: &[u8],
|
||||
cid: ChannelID,
|
||||
channel: Channel,
|
||||
now: CtapInstant,
|
||||
) -> Vec<u8> {
|
||||
let cmd = Command::deserialize(command_cbor);
|
||||
@@ -481,10 +511,10 @@ impl CtapState {
|
||||
}
|
||||
let response = match command {
|
||||
Command::AuthenticatorMakeCredential(params) => {
|
||||
self.process_make_credential(env, params, cid)
|
||||
self.process_make_credential(env, params, channel)
|
||||
}
|
||||
Command::AuthenticatorGetAssertion(params) => {
|
||||
self.process_get_assertion(env, params, cid, now)
|
||||
self.process_get_assertion(env, params, channel, now)
|
||||
}
|
||||
Command::AuthenticatorGetNextAssertion => {
|
||||
self.process_get_next_assertion(env, now)
|
||||
@@ -493,7 +523,7 @@ impl CtapState {
|
||||
Command::AuthenticatorClientPin(params) => {
|
||||
self.client_pin.process_command(env, params, now)
|
||||
}
|
||||
Command::AuthenticatorReset => self.process_reset(env, cid, now),
|
||||
Command::AuthenticatorReset => self.process_reset(env, channel, now),
|
||||
Command::AuthenticatorCredentialManagement(params) => {
|
||||
process_credential_management(
|
||||
env,
|
||||
@@ -503,7 +533,7 @@ impl CtapState {
|
||||
now,
|
||||
)
|
||||
}
|
||||
Command::AuthenticatorSelection => self.process_selection(env, cid),
|
||||
Command::AuthenticatorSelection => self.process_selection(env, channel),
|
||||
Command::AuthenticatorLargeBlobs(params) => {
|
||||
self.large_blobs
|
||||
.process_command(env, &mut self.client_pin, params)
|
||||
@@ -513,7 +543,7 @@ impl CtapState {
|
||||
}
|
||||
// Vendor specific commands
|
||||
Command::AuthenticatorVendorConfigure(params) => {
|
||||
self.process_vendor_configure(env, params, cid)
|
||||
self.process_vendor_configure(env, params, channel)
|
||||
}
|
||||
Command::AuthenticatorVendorUpgrade(params) => {
|
||||
self.process_vendor_upgrade(env, params)
|
||||
@@ -546,12 +576,12 @@ impl CtapState {
|
||||
env: &mut impl Env,
|
||||
pin_uv_auth_param: &Option<Vec<u8>>,
|
||||
pin_uv_auth_protocol: Option<PinUvAuthProtocol>,
|
||||
cid: ChannelID,
|
||||
channel: Channel,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
if let Some(auth_param) = &pin_uv_auth_param {
|
||||
// This case was added in FIDO 2.1.
|
||||
if auth_param.is_empty() {
|
||||
env.user_presence().check(cid)?;
|
||||
env.user_presence().check(channel)?;
|
||||
if storage::pin_hash(env)?.is_none() {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET);
|
||||
} else {
|
||||
@@ -567,7 +597,7 @@ impl CtapState {
|
||||
&mut self,
|
||||
env: &mut impl Env,
|
||||
make_credential_params: AuthenticatorMakeCredentialParameters,
|
||||
cid: ChannelID,
|
||||
channel: Channel,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
let AuthenticatorMakeCredentialParameters {
|
||||
client_data_hash,
|
||||
@@ -582,7 +612,7 @@ impl CtapState {
|
||||
enterprise_attestation,
|
||||
} = make_credential_params;
|
||||
|
||||
self.pin_uv_auth_precheck(env, &pin_uv_auth_param, pin_uv_auth_protocol, cid)?;
|
||||
self.pin_uv_auth_precheck(env, &pin_uv_auth_param, pin_uv_auth_protocol, channel)?;
|
||||
|
||||
if !pub_key_cred_params.contains(&ES256_CRED_PARAM) {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
|
||||
@@ -659,13 +689,13 @@ impl CtapState {
|
||||
{
|
||||
// Perform this check, so bad actors can't brute force exclude_list
|
||||
// without user interaction.
|
||||
let _ = env.user_presence().check(cid);
|
||||
let _ = env.user_presence().check(channel);
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env.user_presence().check(cid)?;
|
||||
env.user_presence().check(channel)?;
|
||||
self.client_pin.clear_token_flags();
|
||||
|
||||
let mut cred_protect_policy = extensions.cred_protect;
|
||||
@@ -923,7 +953,7 @@ impl CtapState {
|
||||
&mut self,
|
||||
env: &mut impl Env,
|
||||
get_assertion_params: AuthenticatorGetAssertionParameters,
|
||||
cid: ChannelID,
|
||||
channel: Channel,
|
||||
now: CtapInstant,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
let AuthenticatorGetAssertionParameters {
|
||||
@@ -936,7 +966,7 @@ impl CtapState {
|
||||
pin_uv_auth_protocol,
|
||||
} = get_assertion_params;
|
||||
|
||||
self.pin_uv_auth_precheck(env, &pin_uv_auth_param, pin_uv_auth_protocol, cid)?;
|
||||
self.pin_uv_auth_precheck(env, &pin_uv_auth_param, pin_uv_auth_protocol, channel)?;
|
||||
|
||||
if extensions.hmac_secret.is_some() && !options.up {
|
||||
// The extension is actually supported, but we need user presence.
|
||||
@@ -1024,7 +1054,7 @@ impl CtapState {
|
||||
|
||||
// This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0.
|
||||
if options.up {
|
||||
env.user_presence().check(cid)?;
|
||||
env.user_presence().check(channel)?;
|
||||
self.client_pin.clear_token_flags();
|
||||
}
|
||||
|
||||
@@ -1134,7 +1164,7 @@ impl CtapState {
|
||||
fn process_reset(
|
||||
&mut self,
|
||||
env: &mut impl Env,
|
||||
cid: ChannelID,
|
||||
channel: Channel,
|
||||
now: CtapInstant,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
self.stateful_command_permission
|
||||
@@ -1143,7 +1173,7 @@ impl CtapState {
|
||||
StatefulCommand::Reset => (),
|
||||
_ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED),
|
||||
}
|
||||
env.user_presence().check(cid)?;
|
||||
env.user_presence().check(channel)?;
|
||||
|
||||
storage::reset(env)?;
|
||||
self.client_pin.reset(env.rng());
|
||||
@@ -1157,9 +1187,9 @@ impl CtapState {
|
||||
fn process_selection(
|
||||
&self,
|
||||
env: &mut impl Env,
|
||||
cid: ChannelID,
|
||||
channel: Channel,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
env.user_presence().check(cid)?;
|
||||
env.user_presence().check(channel)?;
|
||||
Ok(ResponseData::AuthenticatorSelection)
|
||||
}
|
||||
|
||||
@@ -1167,10 +1197,10 @@ impl CtapState {
|
||||
&mut self,
|
||||
env: &mut impl Env,
|
||||
params: AuthenticatorVendorConfigureParameters,
|
||||
cid: ChannelID,
|
||||
channel: Channel,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
if params.attestation_material.is_some() || params.lockdown {
|
||||
env.user_presence().check(cid)?;
|
||||
env.user_presence().check(channel)?;
|
||||
}
|
||||
|
||||
// Sanity checks
|
||||
@@ -1336,7 +1366,7 @@ mod test {
|
||||
// keep-alive packets to.
|
||||
// In tests where we define a dummy user-presence check that immediately returns, the channel
|
||||
// ID is irrelevant, so we pass this (dummy but valid) value.
|
||||
const DUMMY_CHANNEL_ID: ChannelID = [0x12, 0x34, 0x56, 0x78];
|
||||
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
|
||||
|
||||
fn check_make_response(
|
||||
make_credential_response: Result<ResponseData, Ctap2StatusCode>,
|
||||
@@ -1385,7 +1415,7 @@ mod test {
|
||||
let mut env = TestEnv::new();
|
||||
let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0));
|
||||
let info_reponse =
|
||||
ctap_state.process_command(&mut env, &[0x04], DUMMY_CHANNEL_ID, CtapInstant::new(0));
|
||||
ctap_state.process_command(&mut env, &[0x04], DUMMY_CHANNEL, CtapInstant::new(0));
|
||||
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => cbor_array_vec![vec![
|
||||
@@ -1499,7 +1529,7 @@ mod test {
|
||||
|
||||
let make_credential_params = create_minimal_make_credential_parameters();
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
|
||||
check_make_response(
|
||||
make_credential_response,
|
||||
@@ -1518,7 +1548,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.options.rk = false;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
|
||||
check_make_response(
|
||||
make_credential_response,
|
||||
@@ -1537,7 +1567,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.pub_key_cred_params = vec![];
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
@@ -1571,7 +1601,7 @@ mod test {
|
||||
assert!(storage::store_credential(&mut env, excluded_credential_source).is_ok());
|
||||
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED)
|
||||
@@ -1587,7 +1617,7 @@ mod test {
|
||||
let make_credential_params =
|
||||
create_make_credential_parameters_with_cred_protect_policy(test_policy);
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert!(make_credential_response.is_ok());
|
||||
|
||||
let mut iter_result = Ok(());
|
||||
@@ -1601,7 +1631,7 @@ mod test {
|
||||
let make_credential_params =
|
||||
create_make_credential_parameters_with_exclude_list(&credential_id);
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED)
|
||||
@@ -1611,7 +1641,7 @@ mod test {
|
||||
let make_credential_params =
|
||||
create_make_credential_parameters_with_cred_protect_policy(test_policy);
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert!(make_credential_response.is_ok());
|
||||
|
||||
let mut iter_result = Ok(());
|
||||
@@ -1625,7 +1655,7 @@ mod test {
|
||||
let make_credential_params =
|
||||
create_make_credential_parameters_with_exclude_list(&credential_id);
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert!(make_credential_response.is_ok());
|
||||
}
|
||||
|
||||
@@ -1642,7 +1672,7 @@ mod test {
|
||||
make_credential_params.options.rk = false;
|
||||
make_credential_params.extensions = extensions;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
|
||||
let expected_extension_cbor = [
|
||||
0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5,
|
||||
@@ -1668,7 +1698,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.extensions = extensions;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
|
||||
let expected_extension_cbor = [
|
||||
0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5,
|
||||
@@ -1695,7 +1725,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.extensions = extensions;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
check_make_response(
|
||||
make_credential_response,
|
||||
0x41,
|
||||
@@ -1717,7 +1747,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.extensions = extensions;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
let expected_extension_cbor = [
|
||||
0xA1, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68,
|
||||
0x04,
|
||||
@@ -1743,7 +1773,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.extensions = extensions;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
let expected_extension_cbor = [
|
||||
0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF5,
|
||||
];
|
||||
@@ -1775,7 +1805,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.extensions = extensions;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
let expected_extension_cbor = [
|
||||
0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF4,
|
||||
];
|
||||
@@ -1807,7 +1837,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.extensions = extensions;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
let large_blob_key = match make_credential_response.unwrap() {
|
||||
ResponseData::AuthenticatorMakeCredential(make_credential_response) => {
|
||||
make_credential_response.large_blob_key.unwrap()
|
||||
@@ -1850,7 +1880,7 @@ mod test {
|
||||
let make_credential_response = ctap_state.process_make_credential(
|
||||
&mut env,
|
||||
make_credential_params.clone(),
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
|
||||
check_make_response(
|
||||
@@ -1862,7 +1892,7 @@ mod test {
|
||||
);
|
||||
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
@@ -1888,7 +1918,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.options.rk = false;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
|
||||
check_make_response(
|
||||
make_credential_response,
|
||||
@@ -1907,7 +1937,7 @@ mod test {
|
||||
|
||||
let make_credential_params = create_minimal_make_credential_parameters();
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)
|
||||
@@ -1922,7 +1952,7 @@ mod test {
|
||||
storage::toggle_always_uv(&mut env).unwrap();
|
||||
let make_credential_params = create_minimal_make_credential_parameters();
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)
|
||||
@@ -1933,7 +1963,7 @@ mod test {
|
||||
make_credential_params.pin_uv_auth_param = Some(vec![0xA4; 16]);
|
||||
make_credential_params.pin_uv_auth_protocol = Some(PinUvAuthProtocol::V1);
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||
@@ -1949,7 +1979,7 @@ mod test {
|
||||
|
||||
let make_credential_params = create_minimal_make_credential_parameters();
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
|
||||
assert_eq!(
|
||||
make_credential_response,
|
||||
@@ -2044,7 +2074,7 @@ mod test {
|
||||
|
||||
let make_credential_params = create_minimal_make_credential_parameters();
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
|
||||
let get_assertion_params = AuthenticatorGetAssertionParameters {
|
||||
@@ -2062,7 +2092,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
let signature_counter = storage::global_signature_counter(&mut env).unwrap();
|
||||
@@ -2135,7 +2165,7 @@ mod test {
|
||||
make_credential_params.options.rk = false;
|
||||
make_credential_params.extensions = make_extensions;
|
||||
let make_credential_response =
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID);
|
||||
ctap_state.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL);
|
||||
assert!(make_credential_response.is_ok());
|
||||
let credential_id = match make_credential_response.unwrap() {
|
||||
ResponseData::AuthenticatorMakeCredential(make_credential_response) => {
|
||||
@@ -2171,7 +2201,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
assert!(get_assertion_response.is_ok());
|
||||
@@ -2201,7 +2231,7 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.extensions = make_extensions;
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
|
||||
let client_pin_params = AuthenticatorClientPinParameters {
|
||||
@@ -2227,7 +2257,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
assert!(get_assertion_response.is_ok());
|
||||
@@ -2288,7 +2318,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2311,7 +2341,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
let signature_counter = storage::global_signature_counter(&mut env).unwrap();
|
||||
@@ -2348,7 +2378,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2399,7 +2429,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
let signature_counter = storage::global_signature_counter(&mut env).unwrap();
|
||||
@@ -2457,7 +2487,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
let large_blob_key = match get_assertion_response.unwrap() {
|
||||
@@ -2489,7 +2519,7 @@ mod test {
|
||||
};
|
||||
make_credential_params.user = user1.clone();
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
let user2 = PublicKeyCredentialUserEntity {
|
||||
@@ -2500,7 +2530,7 @@ mod test {
|
||||
};
|
||||
make_credential_params.user = user2.clone();
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
|
||||
ctap_state.client_pin = client_pin;
|
||||
@@ -2528,7 +2558,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
let signature_counter = storage::global_signature_counter(&mut env).unwrap();
|
||||
@@ -2581,7 +2611,7 @@ mod test {
|
||||
make_credential_params.user.user_display_name = Some("removed".to_string());
|
||||
make_credential_params.user.user_icon = Some("removed".to_string());
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.user.user_id = vec![0x02];
|
||||
@@ -2589,7 +2619,7 @@ mod test {
|
||||
make_credential_params.user.user_display_name = Some("removed".to_string());
|
||||
make_credential_params.user.user_icon = Some("removed".to_string());
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.user.user_id = vec![0x03];
|
||||
@@ -2597,7 +2627,7 @@ mod test {
|
||||
make_credential_params.user.user_display_name = Some("removed".to_string());
|
||||
make_credential_params.user.user_icon = Some("removed".to_string());
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
|
||||
let get_assertion_params = AuthenticatorGetAssertionParameters {
|
||||
@@ -2615,7 +2645,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
let signature_counter = storage::global_signature_counter(&mut env).unwrap();
|
||||
@@ -2657,12 +2687,12 @@ mod test {
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.user.user_id = vec![0x01];
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
let mut make_credential_params = create_minimal_make_credential_parameters();
|
||||
make_credential_params.user.user_id = vec![0x02];
|
||||
assert!(ctap_state
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL_ID)
|
||||
.process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL)
|
||||
.is_ok());
|
||||
|
||||
let get_assertion_params = AuthenticatorGetAssertionParameters {
|
||||
@@ -2680,7 +2710,7 @@ mod test {
|
||||
let get_assertion_response = ctap_state.process_get_assertion(
|
||||
&mut env,
|
||||
get_assertion_params,
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
assert!(get_assertion_response.is_ok());
|
||||
@@ -2698,12 +2728,7 @@ mod test {
|
||||
4 => cbor_array![ES256_CRED_PARAM],
|
||||
};
|
||||
assert!(cbor_write(cbor_value, &mut command_cbor).is_ok());
|
||||
ctap_state.process_command(
|
||||
&mut env,
|
||||
&command_cbor,
|
||||
DUMMY_CHANNEL_ID,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
ctap_state.process_command(&mut env, &command_cbor, DUMMY_CHANNEL, CtapInstant::new(0));
|
||||
|
||||
let get_assertion_response =
|
||||
ctap_state.process_get_next_assertion(&mut env, CtapInstant::new(0));
|
||||
@@ -2738,7 +2763,7 @@ mod test {
|
||||
assert!(storage::count_credentials(&mut env).unwrap() > 0);
|
||||
|
||||
let reset_reponse =
|
||||
ctap_state.process_command(&mut env, &[0x07], DUMMY_CHANNEL_ID, CtapInstant::new(0));
|
||||
ctap_state.process_command(&mut env, &[0x07], DUMMY_CHANNEL, CtapInstant::new(0));
|
||||
let expected_response = vec![0x00];
|
||||
assert_eq!(reset_reponse, expected_response);
|
||||
assert!(storage::count_credentials(&mut env).unwrap() == 0);
|
||||
@@ -2751,8 +2776,7 @@ mod test {
|
||||
.set(|_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL));
|
||||
let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0));
|
||||
|
||||
let reset_reponse =
|
||||
ctap_state.process_reset(&mut env, DUMMY_CHANNEL_ID, CtapInstant::new(0));
|
||||
let reset_reponse = ctap_state.process_reset(&mut env, DUMMY_CHANNEL, CtapInstant::new(0));
|
||||
|
||||
assert_eq!(
|
||||
reset_reponse,
|
||||
@@ -2766,10 +2790,9 @@ mod test {
|
||||
let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0));
|
||||
|
||||
// This is a GetNextAssertion command.
|
||||
ctap_state.process_command(&mut env, &[0x08], DUMMY_CHANNEL_ID, CtapInstant::new(0));
|
||||
ctap_state.process_command(&mut env, &[0x08], DUMMY_CHANNEL, CtapInstant::new(0));
|
||||
|
||||
let reset_reponse =
|
||||
ctap_state.process_reset(&mut env, DUMMY_CHANNEL_ID, CtapInstant::new(0));
|
||||
let reset_reponse = ctap_state.process_reset(&mut env, DUMMY_CHANNEL, CtapInstant::new(0));
|
||||
assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
@@ -2782,7 +2805,7 @@ mod test {
|
||||
let reponse = ctap_state.process_command(
|
||||
&mut env,
|
||||
&[0x0A, 0xA1, 0x01, 0x18, 0xEE],
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
CtapInstant::new(0),
|
||||
);
|
||||
let expected_response = vec![Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND as u8];
|
||||
@@ -2796,7 +2819,7 @@ mod test {
|
||||
|
||||
// This command does not exist.
|
||||
let reponse =
|
||||
ctap_state.process_command(&mut env, &[0xDF], DUMMY_CHANNEL_ID, CtapInstant::new(0));
|
||||
ctap_state.process_command(&mut env, &[0xDF], DUMMY_CHANNEL, CtapInstant::new(0));
|
||||
let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8];
|
||||
assert_eq!(reponse, expected_response);
|
||||
}
|
||||
@@ -2871,7 +2894,7 @@ mod test {
|
||||
lockdown: false,
|
||||
attestation_material: None,
|
||||
},
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
@@ -2895,7 +2918,7 @@ mod test {
|
||||
private_key: dummy_key,
|
||||
}),
|
||||
},
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
@@ -2926,7 +2949,7 @@ mod test {
|
||||
private_key: other_dummy_key,
|
||||
}),
|
||||
},
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
@@ -2953,7 +2976,7 @@ mod test {
|
||||
lockdown: true,
|
||||
attestation_material: None,
|
||||
},
|
||||
DUMMY_CHANNEL_ID,
|
||||
DUMMY_CHANNEL,
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
|
||||
4
src/env/mod.rs
vendored
4
src/env/mod.rs
vendored
@@ -1,7 +1,7 @@
|
||||
use crate::api::firmware_protection::FirmwareProtection;
|
||||
use crate::api::upgrade_storage::UpgradeStorage;
|
||||
use crate::ctap::hid::ChannelID;
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::ctap::Channel;
|
||||
use crypto::rng256::Rng256;
|
||||
use persistent_store::{Storage, Store};
|
||||
|
||||
@@ -13,7 +13,7 @@ pub trait UserPresence {
|
||||
/// Blocks for user presence.
|
||||
///
|
||||
/// Returns an error in case of timeout or keepalive error.
|
||||
fn check(&mut self, cid: ChannelID) -> Result<(), Ctap2StatusCode>;
|
||||
fn check(&mut self, channel: Channel) -> Result<(), Ctap2StatusCode>;
|
||||
}
|
||||
|
||||
/// Describes what CTAP needs to function.
|
||||
|
||||
10
src/env/test/mod.rs
vendored
10
src/env/test/mod.rs
vendored
@@ -1,7 +1,7 @@
|
||||
use self::upgrade_storage::BufferUpgradeStorage;
|
||||
use crate::api::firmware_protection::FirmwareProtection;
|
||||
use crate::ctap::hid::ChannelID;
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::ctap::Channel;
|
||||
use crate::env::{Env, UserPresence};
|
||||
use crypto::rng256::ThreadRng256;
|
||||
use persistent_store::{BufferOptions, BufferStorage, Store};
|
||||
@@ -16,7 +16,7 @@ pub struct TestEnv {
|
||||
}
|
||||
|
||||
pub struct TestUserPresence {
|
||||
check: Box<dyn Fn(ChannelID) -> Result<(), Ctap2StatusCode>>,
|
||||
check: Box<dyn Fn(Channel) -> Result<(), Ctap2StatusCode>>,
|
||||
}
|
||||
|
||||
pub struct TestWrite;
|
||||
@@ -65,14 +65,14 @@ impl TestEnv {
|
||||
}
|
||||
|
||||
impl TestUserPresence {
|
||||
pub fn set(&mut self, check: impl Fn(ChannelID) -> Result<(), Ctap2StatusCode> + 'static) {
|
||||
pub fn set(&mut self, check: impl Fn(Channel) -> Result<(), Ctap2StatusCode> + 'static) {
|
||||
self.check = Box::new(check);
|
||||
}
|
||||
}
|
||||
|
||||
impl UserPresence for TestUserPresence {
|
||||
fn check(&mut self, cid: ChannelID) -> Result<(), Ctap2StatusCode> {
|
||||
(self.check)(cid)
|
||||
fn check(&mut self, channel: Channel) -> Result<(), Ctap2StatusCode> {
|
||||
(self.check)(channel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
src/env/tock/mod.rs
vendored
8
src/env/tock/mod.rs
vendored
@@ -2,6 +2,7 @@ pub use self::storage::{TockStorage, TockUpgradeStorage};
|
||||
use crate::api::firmware_protection::FirmwareProtection;
|
||||
use crate::ctap::hid::{ChannelID, CtapHid, CtapHidCommand, KeepaliveStatus, ProcessedPacket};
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::ctap::Channel;
|
||||
use crate::env::{Env, UserPresence};
|
||||
use core::cell::Cell;
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -54,8 +55,11 @@ pub fn take_storage() -> StorageResult<TockStorage> {
|
||||
}
|
||||
|
||||
impl UserPresence for TockEnv {
|
||||
fn check(&mut self, cid: ChannelID) -> Result<(), Ctap2StatusCode> {
|
||||
check_user_presence(self, cid)
|
||||
fn check(&mut self, channel: Channel) -> Result<(), Ctap2StatusCode> {
|
||||
match channel {
|
||||
Channel::MainHid(cid) => check_user_presence(self, cid),
|
||||
Channel::VendorHid(cid) => check_user_presence(self, cid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
src/lib.rs
11
src/lib.rs
@@ -21,6 +21,7 @@ extern crate arrayref;
|
||||
use crate::ctap::hid::{HidPacket, HidPacketIterator};
|
||||
use crate::ctap::main_hid::MainHid;
|
||||
use crate::ctap::CtapState;
|
||||
pub use crate::ctap::Transport;
|
||||
use crate::env::Env;
|
||||
use clock::CtapInstant;
|
||||
|
||||
@@ -78,10 +79,16 @@ impl<E: Env> Ctap<E> {
|
||||
pub fn process_hid_packet(
|
||||
&mut self,
|
||||
packet: &HidPacket,
|
||||
transport: Transport,
|
||||
now: CtapInstant,
|
||||
) -> HidPacketIterator {
|
||||
self.hid
|
||||
.process_hid_packet(&mut self.env, packet, now, &mut self.state)
|
||||
match transport {
|
||||
Transport::MainHid => {
|
||||
self.hid
|
||||
.process_hid_packet(&mut self.env, packet, now, &mut self.state)
|
||||
}
|
||||
Transport::VendorHid => HidPacketIterator::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_timeouts(&mut self, now: CtapInstant) {
|
||||
|
||||
@@ -34,6 +34,7 @@ use ctap2::clock::{new_clock, Clock, ClockInt, KEEPALIVE_DELAY};
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
use ctap2::env::tock::blink_leds;
|
||||
use ctap2::env::tock::{switch_off_leds, wink_leds, TockEnv, KEEPALIVE_DELAY_TOCK};
|
||||
use ctap2::Transport;
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
use embedded_time::duration::Microseconds;
|
||||
use embedded_time::duration::Milliseconds;
|
||||
@@ -120,7 +121,7 @@ fn main() {
|
||||
ctap.update_timeouts(now);
|
||||
|
||||
if has_packet {
|
||||
let reply = ctap.process_hid_packet(&pkt_request, now);
|
||||
let reply = ctap.process_hid_packet(&pkt_request, Transport::MainHid, now);
|
||||
// This block handles sending packets.
|
||||
for mut pkt_reply in reply {
|
||||
let status = usb_ctap_hid::send_or_recv_with_timeout(&mut pkt_reply, SEND_TIMEOUT);
|
||||
|
||||
Reference in New Issue
Block a user