Initial commit
This commit is contained in:
483
src/ctap/command.rs
Normal file
483
src/ctap/command.rs
Normal file
@@ -0,0 +1,483 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::data_formats::{
|
||||
ok_or_missing, read_array, read_byte_string, read_integer, read_map, read_text_string,
|
||||
read_unsigned, ClientPinSubCommand, CoseKey, Extensions, GetAssertionOptions,
|
||||
MakeCredentialOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity,
|
||||
PublicKeyCredentialType, PublicKeyCredentialUserEntity,
|
||||
};
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryFrom;
|
||||
|
||||
// CTAP specification (version 20190130) section 6.1
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
|
||||
pub enum Command {
|
||||
AuthenticatorMakeCredential(AuthenticatorMakeCredentialParameters),
|
||||
AuthenticatorGetAssertion(AuthenticatorGetAssertionParameters),
|
||||
AuthenticatorGetInfo,
|
||||
AuthenticatorClientPin(AuthenticatorClientPinParameters),
|
||||
AuthenticatorReset,
|
||||
AuthenticatorGetNextAssertion,
|
||||
// TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts)
|
||||
}
|
||||
|
||||
impl From<cbor::reader::DecoderError> for Ctap2StatusCode {
|
||||
fn from(_: cbor::reader::DecoderError) -> Self {
|
||||
Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this `allow(dead_code)` once the constants are used.
|
||||
#[allow(dead_code)]
|
||||
impl Command {
|
||||
const AUTHENTICATOR_MAKE_CREDENTIAL: u8 = 0x01;
|
||||
const AUTHENTICATOR_GET_ASSERTION: u8 = 0x02;
|
||||
const AUTHENTICATOR_GET_INFO: u8 = 0x04;
|
||||
const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06;
|
||||
const AUTHENTICATOR_RESET: u8 = 0x07;
|
||||
// TODO(kaczmarczyck) use or remove those constants
|
||||
const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08;
|
||||
const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09;
|
||||
const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0;
|
||||
const AUTHENTICATOR_SELECTION: u8 = 0xB0;
|
||||
const AUTHENTICATOR_CONFIG: u8 = 0xC0;
|
||||
const AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40;
|
||||
const AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF;
|
||||
|
||||
pub fn deserialize(bytes: &[u8]) -> Result<Command, Ctap2StatusCode> {
|
||||
if bytes.is_empty() {
|
||||
// The error to return is not specified, missing parameter seems to fit best.
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER);
|
||||
}
|
||||
|
||||
let command_value = bytes[0];
|
||||
match command_value {
|
||||
Command::AUTHENTICATOR_MAKE_CREDENTIAL => {
|
||||
let decoded_cbor = cbor::read(&bytes[1..])?;
|
||||
Ok(Command::AuthenticatorMakeCredential(
|
||||
AuthenticatorMakeCredentialParameters::try_from(decoded_cbor)?,
|
||||
))
|
||||
}
|
||||
Command::AUTHENTICATOR_GET_ASSERTION => {
|
||||
let decoded_cbor = cbor::read(&bytes[1..])?;
|
||||
Ok(Command::AuthenticatorGetAssertion(
|
||||
AuthenticatorGetAssertionParameters::try_from(decoded_cbor)?,
|
||||
))
|
||||
}
|
||||
Command::AUTHENTICATOR_GET_INFO => {
|
||||
// Parameters are ignored.
|
||||
Ok(Command::AuthenticatorGetInfo)
|
||||
}
|
||||
Command::AUTHENTICATOR_CLIENT_PIN => {
|
||||
let decoded_cbor = cbor::read(&bytes[1..])?;
|
||||
Ok(Command::AuthenticatorClientPin(
|
||||
AuthenticatorClientPinParameters::try_from(decoded_cbor)?,
|
||||
))
|
||||
}
|
||||
Command::AUTHENTICATOR_RESET => {
|
||||
// Parameters are ignored.
|
||||
Ok(Command::AuthenticatorReset)
|
||||
}
|
||||
Command::AUTHENTICATOR_GET_NEXT_ASSERTION => {
|
||||
// Parameters are ignored.
|
||||
Ok(Command::AuthenticatorGetNextAssertion)
|
||||
}
|
||||
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
|
||||
pub struct AuthenticatorMakeCredentialParameters {
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub rp: PublicKeyCredentialRpEntity,
|
||||
pub user: PublicKeyCredentialUserEntity,
|
||||
pub pub_key_cred_params: Vec<(PublicKeyCredentialType, i64)>,
|
||||
pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
|
||||
pub extensions: Option<Extensions>,
|
||||
// Even though options are optional, we can use the default if not present.
|
||||
pub options: MakeCredentialOptions,
|
||||
pub pin_uv_auth_param: Option<Vec<u8>>,
|
||||
pub pin_uv_auth_protocol: Option<u64>,
|
||||
}
|
||||
|
||||
impl TryFrom<cbor::Value> for AuthenticatorMakeCredentialParameters {
|
||||
type Error = Ctap2StatusCode;
|
||||
|
||||
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
|
||||
let param_map = read_map(&cbor_value)?;
|
||||
|
||||
let client_data_hash = read_byte_string(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
|
||||
|
||||
let rp = PublicKeyCredentialRpEntity::try_from(ok_or_missing(
|
||||
param_map.get(&cbor_unsigned!(2)),
|
||||
)?)?;
|
||||
|
||||
let user = PublicKeyCredentialUserEntity::try_from(ok_or_missing(
|
||||
param_map.get(&cbor_unsigned!(3)),
|
||||
)?)?;
|
||||
|
||||
let cred_param_vec = read_array(ok_or_missing(param_map.get(&cbor_unsigned!(4)))?)?;
|
||||
let mut pub_key_cred_params = vec![];
|
||||
for cred_param_map_value in cred_param_vec {
|
||||
let cred_param_map = read_map(cred_param_map_value)?;
|
||||
let cred_type = PublicKeyCredentialType::try_from(ok_or_missing(
|
||||
cred_param_map.get(&cbor_text!("type")),
|
||||
)?)?;
|
||||
let alg = read_integer(ok_or_missing(cred_param_map.get(&cbor_text!("alg")))?)?;
|
||||
pub_key_cred_params.push((cred_type, alg));
|
||||
}
|
||||
|
||||
let exclude_list = match param_map.get(&cbor_unsigned!(5)) {
|
||||
Some(entry) => {
|
||||
let exclude_list_vec = read_array(entry)?;
|
||||
let mut exclude_list = vec![];
|
||||
for exclude_list_value in exclude_list_vec {
|
||||
exclude_list.push(PublicKeyCredentialDescriptor::try_from(exclude_list_value)?);
|
||||
}
|
||||
Some(exclude_list)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let extensions = param_map
|
||||
.get(&cbor_unsigned!(6))
|
||||
.map(Extensions::try_from)
|
||||
.transpose()?;
|
||||
|
||||
let options = match param_map.get(&cbor_unsigned!(7)) {
|
||||
Some(entry) => MakeCredentialOptions::try_from(entry)?,
|
||||
None => MakeCredentialOptions {
|
||||
rk: false,
|
||||
uv: false,
|
||||
},
|
||||
};
|
||||
|
||||
let pin_uv_auth_param = param_map
|
||||
.get(&cbor_unsigned!(8))
|
||||
.map(read_byte_string)
|
||||
.transpose()?;
|
||||
|
||||
let pin_uv_auth_protocol = param_map
|
||||
.get(&cbor_unsigned!(9))
|
||||
.map(read_unsigned)
|
||||
.transpose()?;
|
||||
|
||||
Ok(AuthenticatorMakeCredentialParameters {
|
||||
client_data_hash,
|
||||
rp,
|
||||
user,
|
||||
pub_key_cred_params,
|
||||
exclude_list,
|
||||
extensions,
|
||||
options,
|
||||
pin_uv_auth_param,
|
||||
pin_uv_auth_protocol,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
|
||||
pub struct AuthenticatorGetAssertionParameters {
|
||||
pub rp_id: String,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
|
||||
pub extensions: Option<Extensions>,
|
||||
// Even though options are optional, we can use the default if not present.
|
||||
pub options: GetAssertionOptions,
|
||||
pub pin_uv_auth_param: Option<Vec<u8>>,
|
||||
pub pin_uv_auth_protocol: Option<u64>,
|
||||
}
|
||||
|
||||
impl TryFrom<cbor::Value> for AuthenticatorGetAssertionParameters {
|
||||
type Error = Ctap2StatusCode;
|
||||
|
||||
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
|
||||
let param_map = read_map(&cbor_value)?;
|
||||
|
||||
let rp_id = read_text_string(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
|
||||
|
||||
let client_data_hash = read_byte_string(ok_or_missing(param_map.get(&cbor_unsigned!(2)))?)?;
|
||||
|
||||
let allow_list = match param_map.get(&cbor_unsigned!(3)) {
|
||||
Some(entry) => {
|
||||
let allow_list_vec = read_array(entry)?;
|
||||
let mut allow_list = vec![];
|
||||
for allow_list_value in allow_list_vec {
|
||||
allow_list.push(PublicKeyCredentialDescriptor::try_from(allow_list_value)?);
|
||||
}
|
||||
Some(allow_list)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let extensions = param_map
|
||||
.get(&cbor_unsigned!(4))
|
||||
.map(Extensions::try_from)
|
||||
.transpose()?;
|
||||
|
||||
let options = match param_map.get(&cbor_unsigned!(5)) {
|
||||
Some(entry) => GetAssertionOptions::try_from(entry)?,
|
||||
None => GetAssertionOptions {
|
||||
up: true,
|
||||
uv: false,
|
||||
},
|
||||
};
|
||||
|
||||
let pin_uv_auth_param = param_map
|
||||
.get(&cbor_unsigned!(6))
|
||||
.map(read_byte_string)
|
||||
.transpose()?;
|
||||
|
||||
let pin_uv_auth_protocol = param_map
|
||||
.get(&cbor_unsigned!(7))
|
||||
.map(read_unsigned)
|
||||
.transpose()?;
|
||||
|
||||
Ok(AuthenticatorGetAssertionParameters {
|
||||
rp_id,
|
||||
client_data_hash,
|
||||
allow_list,
|
||||
extensions,
|
||||
options,
|
||||
pin_uv_auth_param,
|
||||
pin_uv_auth_protocol,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
|
||||
pub struct AuthenticatorClientPinParameters {
|
||||
pub pin_protocol: u64,
|
||||
pub sub_command: ClientPinSubCommand,
|
||||
pub key_agreement: Option<CoseKey>,
|
||||
pub pin_auth: Option<Vec<u8>>,
|
||||
pub new_pin_enc: Option<Vec<u8>>,
|
||||
pub pin_hash_enc: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
|
||||
type Error = Ctap2StatusCode;
|
||||
|
||||
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
|
||||
let param_map = read_map(&cbor_value)?;
|
||||
|
||||
let pin_protocol = read_unsigned(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
|
||||
|
||||
let sub_command =
|
||||
ClientPinSubCommand::try_from(ok_or_missing(param_map.get(&cbor_unsigned!(2)))?)?;
|
||||
|
||||
let key_agreement = param_map
|
||||
.get(&cbor_unsigned!(3))
|
||||
.map(read_map)
|
||||
.transpose()?
|
||||
.map(|x| CoseKey(x.clone()));
|
||||
|
||||
let pin_auth = param_map
|
||||
.get(&cbor_unsigned!(4))
|
||||
.map(read_byte_string)
|
||||
.transpose()?;
|
||||
|
||||
let new_pin_enc = param_map
|
||||
.get(&cbor_unsigned!(5))
|
||||
.map(read_byte_string)
|
||||
.transpose()?;
|
||||
|
||||
let pin_hash_enc = param_map
|
||||
.get(&cbor_unsigned!(6))
|
||||
.map(read_byte_string)
|
||||
.transpose()?;
|
||||
|
||||
Ok(AuthenticatorClientPinParameters {
|
||||
pin_protocol,
|
||||
sub_command,
|
||||
key_agreement,
|
||||
pin_auth,
|
||||
new_pin_enc,
|
||||
pin_hash_enc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::data_formats::{
|
||||
AuthenticatorTransport, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
|
||||
};
|
||||
use super::*;
|
||||
use alloc::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn test_from_cbor_make_credential_parameters() {
|
||||
let cbor_value = cbor_map! {
|
||||
1 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
|
||||
2 => cbor_map! {
|
||||
"id" => "example.com",
|
||||
"name" => "Example",
|
||||
"icon" => "example.com/icon.png",
|
||||
},
|
||||
3 => cbor_map! {
|
||||
"id" => vec![0x1D, 0x1D, 0x1D, 0x1D],
|
||||
"name" => "foo",
|
||||
"displayName" => "bar",
|
||||
"icon" => "example.com/foo/icon.png",
|
||||
},
|
||||
4 => cbor_array![ cbor_map! {
|
||||
"type" => "public-key",
|
||||
"alg" => -7
|
||||
} ],
|
||||
5 => cbor_array![],
|
||||
8 => vec![0x12, 0x34],
|
||||
9 => 1,
|
||||
};
|
||||
let returned_make_credential_parameters =
|
||||
AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap();
|
||||
|
||||
let client_data_hash = vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
||||
0x0E, 0x0F,
|
||||
];
|
||||
let rp = PublicKeyCredentialRpEntity {
|
||||
rp_id: "example.com".to_string(),
|
||||
rp_name: Some("Example".to_string()),
|
||||
rp_icon: Some("example.com/icon.png".to_string()),
|
||||
};
|
||||
let user = PublicKeyCredentialUserEntity {
|
||||
user_id: vec![0x1D, 0x1D, 0x1D, 0x1D],
|
||||
user_name: Some("foo".to_string()),
|
||||
user_display_name: Some("bar".to_string()),
|
||||
user_icon: Some("example.com/foo/icon.png".to_string()),
|
||||
};
|
||||
let pub_key_cred_param = (PublicKeyCredentialType::PublicKey, -7);
|
||||
let options = MakeCredentialOptions {
|
||||
rk: false,
|
||||
uv: false,
|
||||
};
|
||||
let expected_make_credential_parameters = AuthenticatorMakeCredentialParameters {
|
||||
client_data_hash,
|
||||
rp,
|
||||
user,
|
||||
pub_key_cred_params: vec![pub_key_cred_param],
|
||||
exclude_list: Some(vec![]),
|
||||
extensions: None,
|
||||
options,
|
||||
pin_uv_auth_param: Some(vec![0x12, 0x34]),
|
||||
pin_uv_auth_protocol: Some(1),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
returned_make_credential_parameters,
|
||||
expected_make_credential_parameters
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_cbor_get_assertion_parameters() {
|
||||
let cbor_value = cbor_map! {
|
||||
1 => "example.com",
|
||||
2 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
|
||||
3 => cbor_array![ cbor_map! {
|
||||
"type" => "public-key",
|
||||
"id" => vec![0x2D, 0x2D, 0x2D, 0x2D],
|
||||
"transports" => cbor_array!["usb"],
|
||||
} ],
|
||||
6 => vec![0x12, 0x34],
|
||||
7 => 1,
|
||||
};
|
||||
let returned_get_assertion_parameters =
|
||||
AuthenticatorGetAssertionParameters::try_from(cbor_value).unwrap();
|
||||
|
||||
let rp_id = "example.com".to_string();
|
||||
let client_data_hash = vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
||||
0x0E, 0x0F,
|
||||
];
|
||||
let pub_key_cred_descriptor = PublicKeyCredentialDescriptor {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
key_id: vec![0x2D, 0x2D, 0x2D, 0x2D],
|
||||
transports: Some(vec![AuthenticatorTransport::Usb]),
|
||||
};
|
||||
let options = GetAssertionOptions {
|
||||
up: true,
|
||||
uv: false,
|
||||
};
|
||||
let expected_get_assertion_parameters = AuthenticatorGetAssertionParameters {
|
||||
rp_id,
|
||||
client_data_hash,
|
||||
allow_list: Some(vec![pub_key_cred_descriptor]),
|
||||
extensions: None,
|
||||
options,
|
||||
pin_uv_auth_param: Some(vec![0x12, 0x34]),
|
||||
pin_uv_auth_protocol: Some(1),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
returned_get_assertion_parameters,
|
||||
expected_get_assertion_parameters
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_cbor_client_pin_parameters() {
|
||||
let cbor_value = cbor_map! {
|
||||
1 => 1,
|
||||
2 => ClientPinSubCommand::GetPinRetries,
|
||||
3 => cbor_map!{},
|
||||
4 => vec! [0xBB],
|
||||
5 => vec! [0xCC],
|
||||
6 => vec! [0xDD],
|
||||
};
|
||||
let returned_pin_protocol_parameters =
|
||||
AuthenticatorClientPinParameters::try_from(cbor_value).unwrap();
|
||||
|
||||
let expected_pin_protocol_parameters = AuthenticatorClientPinParameters {
|
||||
pin_protocol: 1,
|
||||
sub_command: ClientPinSubCommand::GetPinRetries,
|
||||
key_agreement: Some(CoseKey(BTreeMap::new())),
|
||||
pin_auth: Some(vec![0xBB]),
|
||||
new_pin_enc: Some(vec![0xCC]),
|
||||
pin_hash_enc: Some(vec![0xDD]),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
returned_pin_protocol_parameters,
|
||||
expected_pin_protocol_parameters
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_get_info() {
|
||||
let cbor_bytes = [Command::AUTHENTICATOR_GET_INFO];
|
||||
let command = Command::deserialize(&cbor_bytes);
|
||||
assert_eq!(command, Ok(Command::AuthenticatorGetInfo));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_reset() {
|
||||
// Adding some random bytes to see if they are ignored.
|
||||
let cbor_bytes = [Command::AUTHENTICATOR_RESET, 0xAB, 0xCD, 0xEF];
|
||||
let command = Command::deserialize(&cbor_bytes);
|
||||
assert_eq!(command, Ok(Command::AuthenticatorReset));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_get_next_assertion() {
|
||||
let cbor_bytes = [Command::AUTHENTICATOR_GET_NEXT_ASSERTION];
|
||||
let command = Command::deserialize(&cbor_bytes);
|
||||
assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion));
|
||||
}
|
||||
}
|
||||
675
src/ctap/ctap1.rs
Normal file
675
src/ctap/ctap1.rs
Normal file
@@ -0,0 +1,675 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::hid::ChannelID;
|
||||
use super::key_material::{ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY};
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use super::CtapState;
|
||||
use crate::timer::ClockValue;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::Into;
|
||||
use core::convert::TryFrom;
|
||||
use crypto::rng256::Rng256;
|
||||
|
||||
// The specification referenced in this file is at:
|
||||
// https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf
|
||||
|
||||
// status codes specification (version 20170411) section 3.3
|
||||
#[allow(non_camel_case_types)]
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
|
||||
pub enum Ctap1StatusCode {
|
||||
SW_NO_ERROR = 0x9000,
|
||||
SW_CONDITIONS_NOT_SATISFIED = 0x6985,
|
||||
SW_WRONG_DATA = 0x6A80,
|
||||
SW_WRONG_LENGTH = 0x6700,
|
||||
SW_CLA_NOT_SUPPORTED = 0x6E00,
|
||||
SW_INS_NOT_SUPPORTED = 0x6D00,
|
||||
SW_VENDOR_KEY_HANDLE_TOO_LONG = 0xF000,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for Ctap1StatusCode {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u16) -> Result<Ctap1StatusCode, ()> {
|
||||
match value {
|
||||
0x9000 => Ok(Ctap1StatusCode::SW_NO_ERROR),
|
||||
0x6985 => Ok(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED),
|
||||
0x6A80 => Ok(Ctap1StatusCode::SW_WRONG_DATA),
|
||||
0x6700 => Ok(Ctap1StatusCode::SW_WRONG_LENGTH),
|
||||
0x6E00 => Ok(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED),
|
||||
0x6D00 => Ok(Ctap1StatusCode::SW_INS_NOT_SUPPORTED),
|
||||
0xF000 => Ok(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<u16> for Ctap1StatusCode {
|
||||
fn into(self) -> u16 {
|
||||
self as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug))]
|
||||
#[derive(PartialEq)]
|
||||
pub enum Ctap1Flags {
|
||||
CheckOnly = 0x07,
|
||||
EnforceUpAndSign = 0x03,
|
||||
DontEnforceUpAndSign = 0x08,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Ctap1Flags {
|
||||
type Error = Ctap1StatusCode;
|
||||
|
||||
fn try_from(value: u8) -> Result<Ctap1Flags, Ctap1StatusCode> {
|
||||
match value {
|
||||
0x07 => Ok(Ctap1Flags::CheckOnly),
|
||||
0x03 => Ok(Ctap1Flags::EnforceUpAndSign),
|
||||
0x08 => Ok(Ctap1Flags::DontEnforceUpAndSign),
|
||||
_ => Err(Ctap1StatusCode::SW_WRONG_DATA),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<u8> for Ctap1Flags {
|
||||
fn into(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
|
||||
// TODO: remove #allow when https://github.com/rust-lang/rust/issues/64362 is fixed
|
||||
enum U2fCommand {
|
||||
#[allow(dead_code)]
|
||||
Register {
|
||||
challenge: [u8; 32],
|
||||
application: [u8; 32],
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
Authenticate {
|
||||
challenge: [u8; 32],
|
||||
application: [u8; 32],
|
||||
key_handle: Vec<u8>,
|
||||
flags: Ctap1Flags,
|
||||
},
|
||||
Version,
|
||||
#[allow(dead_code)]
|
||||
VendorSpecific {
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for U2fCommand {
|
||||
type Error = Ctap1StatusCode;
|
||||
|
||||
fn try_from(message: &[u8]) -> Result<Self, Ctap1StatusCode> {
|
||||
if message.len() < Ctap1Command::APDU_HEADER_LEN as usize {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_DATA);
|
||||
}
|
||||
|
||||
let (apdu, payload) = message.split_at(Ctap1Command::APDU_HEADER_LEN as usize);
|
||||
|
||||
// ISO7816 APDU Header format. Each cell is 1 byte. Note that the CTAP flavor always
|
||||
// encodes the length on 3 bytes and doesn't use the field "Le" (Length Expected).
|
||||
// We keep the 2 byte of "Le" for the packet length in mind, but always ignore its value.
|
||||
// Lc is using big-endian encoding
|
||||
// +-----+-----+----+----+-----+-----+-----+
|
||||
// | CLA | INS | P1 | P2 | Lc1 | Lc2 | Lc3 |
|
||||
// +-----+-----+----+----+-----+-----+-----+
|
||||
if apdu[0] != Ctap1Command::CTAP1_CLA {
|
||||
return Err(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
let lc = (((apdu[4] as u32) << 16) | ((apdu[5] as u32) << 8) | (apdu[6] as u32)) as usize;
|
||||
|
||||
// Since there is always request data, the expected length is either omitted or
|
||||
// encoded in 2 bytes.
|
||||
if lc != payload.len() && lc + 2 != payload.len() {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
|
||||
match apdu[1] {
|
||||
// U2F raw message format specification, Section 4.1
|
||||
// +-----------------+-------------------+
|
||||
// + Challenge (32B) | Application (32B) |
|
||||
// +-----------------+-------------------+
|
||||
Ctap1Command::U2F_REGISTER => {
|
||||
if lc != 64 {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
Ok(Self::Register {
|
||||
challenge: *array_ref!(payload, 0, 32),
|
||||
application: *array_ref!(payload, 32, 32),
|
||||
})
|
||||
}
|
||||
|
||||
// U2F raw message format specification, Section 5.1
|
||||
// +-----------------+-------------------+---------------------+------------+
|
||||
// + Challenge (32B) | Application (32B) | key handle len (1B) | key handle |
|
||||
// +-----------------+-------------------+---------------------+------------+
|
||||
Ctap1Command::U2F_AUTHENTICATE => {
|
||||
if lc < 65 {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
let handle_length = payload[64] as usize;
|
||||
if lc != 65 + handle_length {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
let flag = Ctap1Flags::try_from(apdu[2])?;
|
||||
Ok(Self::Authenticate {
|
||||
challenge: *array_ref!(payload, 0, 32),
|
||||
application: *array_ref!(payload, 32, 32),
|
||||
key_handle: payload[65..lc].to_vec(),
|
||||
flags: flag,
|
||||
})
|
||||
}
|
||||
|
||||
// U2F raw message format specification, Section 6.1
|
||||
Ctap1Command::U2F_VERSION => {
|
||||
if lc != 0 {
|
||||
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
|
||||
}
|
||||
Ok(Self::Version)
|
||||
}
|
||||
|
||||
// For Vendor specific command.
|
||||
Ctap1Command::VENDOR_SPECIFIC_FIRST..=Ctap1Command::VENDOR_SPECIFIC_LAST => {
|
||||
Ok(Self::VendorSpecific {
|
||||
payload: payload.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
_ => Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ctap1Command {}
|
||||
|
||||
impl Ctap1Command {
|
||||
const APDU_HEADER_LEN: u32 = 7; // CLA + INS + P1 + P2 + LC1-3
|
||||
|
||||
const CTAP1_CLA: u8 = 0;
|
||||
// This byte is used in Register, but only serves backwards compatibility.
|
||||
const LEGACY_BYTE: u8 = 0x05;
|
||||
// This byte is hardcoded into the specification of Authenticate.
|
||||
const USER_PRESENCE_INDICATOR_BYTE: u8 = 0x01;
|
||||
|
||||
// CTAP1/U2F commands
|
||||
// U2F raw message format specification 1.2 (version 20170411)
|
||||
const U2F_REGISTER: u8 = 0x01;
|
||||
const U2F_AUTHENTICATE: u8 = 0x02;
|
||||
const U2F_VERSION: u8 = 0x03;
|
||||
const VENDOR_SPECIFIC_FIRST: u8 = 0x40;
|
||||
const VENDOR_SPECIFIC_LAST: u8 = 0xBF;
|
||||
|
||||
pub fn process_command<R, CheckUserPresence>(
|
||||
message: &[u8],
|
||||
ctap_state: &mut CtapState<R, CheckUserPresence>,
|
||||
clock_value: ClockValue,
|
||||
) -> Result<Vec<u8>, Ctap1StatusCode>
|
||||
where
|
||||
R: Rng256,
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
let command = U2fCommand::try_from(message)?;
|
||||
match command {
|
||||
U2fCommand::Register {
|
||||
challenge,
|
||||
application,
|
||||
} => {
|
||||
if !ctap_state.u2f_up_state.consume_up(clock_value) {
|
||||
return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
Ctap1Command::process_register(challenge, application, ctap_state)
|
||||
}
|
||||
|
||||
U2fCommand::Authenticate {
|
||||
challenge,
|
||||
application,
|
||||
key_handle,
|
||||
flags,
|
||||
} => {
|
||||
// The order is important due to side effects of checking user presence.
|
||||
if flags == Ctap1Flags::EnforceUpAndSign
|
||||
&& !ctap_state.u2f_up_state.consume_up(clock_value)
|
||||
{
|
||||
return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
Ctap1Command::process_authenticate(
|
||||
challenge,
|
||||
application,
|
||||
key_handle,
|
||||
flags,
|
||||
ctap_state,
|
||||
)
|
||||
}
|
||||
|
||||
// U2F raw message format specification (version 20170411) section 6.3
|
||||
U2fCommand::Version => Ok(Vec::<u8>::from(super::U2F_VERSION_STRING)),
|
||||
|
||||
// TODO: should we return an error instead such as SW_INS_NOT_SUPPORTED?
|
||||
U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_NO_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
// U2F raw message format specification (version 20170411) section 4.3
|
||||
// In case of success we need to send back the following reply
|
||||
// (excluding ISO7816 success code)
|
||||
// +------+--------------------+---------------------+------------+------------+------+
|
||||
// + 0x05 | User pub key (65B) | key handle len (1B) | key handle | X.509 Cert | Sign |
|
||||
// +------+--------------------+---------------------+------------+------------+------+
|
||||
//
|
||||
// Where Sign is an ECDSA signature over the following structure:
|
||||
// +------+-------------------+-----------------+------------+--------------------+
|
||||
// + 0x00 | application (32B) | challenge (32B) | key handle | User pub key (65B) |
|
||||
// +------+-------------------+-----------------+------------+--------------------+
|
||||
fn process_register<R, CheckUserPresence>(
|
||||
challenge: [u8; 32],
|
||||
application: [u8; 32],
|
||||
ctap_state: &mut CtapState<R, CheckUserPresence>,
|
||||
) -> Result<Vec<u8>, Ctap1StatusCode>
|
||||
where
|
||||
R: Rng256,
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
let sk = crypto::ecdsa::SecKey::gensk(ctap_state.rng);
|
||||
let pk = sk.genpk();
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
if key_handle.len() > 0xFF {
|
||||
// This is just being defensive with unreachable code.
|
||||
return Err(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG);
|
||||
}
|
||||
|
||||
let mut response =
|
||||
Vec::with_capacity(105 + key_handle.len() + ATTESTATION_CERTIFICATE.len());
|
||||
response.push(Ctap1Command::LEGACY_BYTE);
|
||||
let user_pk = pk.to_uncompressed();
|
||||
response.extend_from_slice(&user_pk);
|
||||
response.push(key_handle.len() as u8);
|
||||
response.extend(key_handle.clone());
|
||||
response.extend_from_slice(&ATTESTATION_CERTIFICATE);
|
||||
|
||||
// The first byte is reserved.
|
||||
let mut signature_data = Vec::with_capacity(66 + key_handle.len());
|
||||
signature_data.push(0x00);
|
||||
signature_data.extend(&application);
|
||||
signature_data.extend(&challenge);
|
||||
signature_data.extend(key_handle);
|
||||
signature_data.extend_from_slice(&user_pk);
|
||||
|
||||
let attestation_key = crypto::ecdsa::SecKey::from_bytes(&ATTESTATION_PRIVATE_KEY).unwrap();
|
||||
let signature = attestation_key.sign_rfc6979::<crypto::sha256::Sha256>(&signature_data);
|
||||
|
||||
response.extend(signature.to_asn1_der());
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
// U2F raw message format specification (version 20170411) section 5.4
|
||||
// In case of success we need to send back the following reply
|
||||
// (excluding ISO7816 success code)
|
||||
// +---------+--------------+-----------+
|
||||
// + UP (1B) | Counter (4B) | Signature |
|
||||
// +---------+--------------+-----------+
|
||||
// UP only has 2 defined values:
|
||||
// - 0x00: user presence was not verified
|
||||
// - 0x01: user presence was verified
|
||||
//
|
||||
// Where Signature is an ECDSA signature over the following structure:
|
||||
// +-------------------+---------+--------------+-----------------+
|
||||
// + application (32B) | UP (1B) | Counter (4B) | challenge (32B) |
|
||||
// +-------------------+---------+--------------+-----------------+
|
||||
fn process_authenticate<R, CheckUserPresence>(
|
||||
challenge: [u8; 32],
|
||||
application: [u8; 32],
|
||||
key_handle: Vec<u8>,
|
||||
flags: Ctap1Flags,
|
||||
ctap_state: &mut CtapState<R, CheckUserPresence>,
|
||||
) -> Result<Vec<u8>, Ctap1StatusCode>
|
||||
where
|
||||
R: Rng256,
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
let credential_source = ctap_state.decrypt_credential_source(key_handle, &application);
|
||||
if let Some(credential_source) = credential_source {
|
||||
if flags == Ctap1Flags::CheckOnly {
|
||||
return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
ctap_state.increment_global_signature_counter();
|
||||
let mut signature_data = ctap_state
|
||||
.generate_auth_data(&application, Ctap1Command::USER_PRESENCE_INDICATOR_BYTE);
|
||||
signature_data.extend(&challenge);
|
||||
let signature = credential_source
|
||||
.private_key
|
||||
.sign_rfc6979::<crypto::sha256::Sha256>(&signature_data);
|
||||
|
||||
let mut response = signature_data[application.len()..application.len() + 5].to_vec();
|
||||
response.extend(signature.to_asn1_der());
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(Ctap1StatusCode::SW_WRONG_DATA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::{ENCRYPTED_CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER};
|
||||
use super::*;
|
||||
use crypto::rng256::ThreadRng256;
|
||||
use crypto::Hash256;
|
||||
|
||||
const CLOCK_FREQUENCY_HZ: usize = 32768;
|
||||
const START_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ);
|
||||
const TIMEOUT_CLOCK_VALUE: ClockValue = ClockValue::new(
|
||||
(30001 * CLOCK_FREQUENCY_HZ as isize) / 1000,
|
||||
CLOCK_FREQUENCY_HZ,
|
||||
);
|
||||
|
||||
fn create_register_message(application: &[u8; 32]) -> Vec<u8> {
|
||||
let mut message = vec![
|
||||
Ctap1Command::CTAP1_CLA,
|
||||
Ctap1Command::U2F_REGISTER,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x40,
|
||||
];
|
||||
let challenge = [0x0C; 32];
|
||||
message.extend(&challenge);
|
||||
message.extend(application);
|
||||
message
|
||||
}
|
||||
|
||||
fn create_authenticate_message(
|
||||
application: &[u8; 32],
|
||||
flags: Ctap1Flags,
|
||||
key_handle: &Vec<u8>,
|
||||
) -> Vec<u8> {
|
||||
let mut message = vec![
|
||||
Ctap1Command::CTAP1_CLA,
|
||||
Ctap1Command::U2F_AUTHENTICATE,
|
||||
flags.into(),
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
65 + ENCRYPTED_CREDENTIAL_ID_SIZE as u8,
|
||||
];
|
||||
let challenge = [0x0C; 32];
|
||||
message.extend(&challenge);
|
||||
message.extend(application);
|
||||
message.push(ENCRYPTED_CREDENTIAL_ID_SIZE as u8);
|
||||
message.extend(key_handle);
|
||||
message
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_register() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let application = [0x0A; 32];
|
||||
let message = create_register_message(&application);
|
||||
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
|
||||
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
|
||||
let response =
|
||||
Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap();
|
||||
|
||||
assert_eq!(response[0], Ctap1Command::LEGACY_BYTE);
|
||||
assert_eq!(response[66], ENCRYPTED_CREDENTIAL_ID_SIZE as u8);
|
||||
assert!(ctap_state
|
||||
.decrypt_credential_source(
|
||||
response[67..67 + ENCRYPTED_CREDENTIAL_ID_SIZE].to_vec(),
|
||||
&application
|
||||
)
|
||||
.is_some());
|
||||
const CERT_START: usize = 67 + ENCRYPTED_CREDENTIAL_ID_SIZE;
|
||||
assert_eq!(
|
||||
&response[CERT_START..CERT_START + ATTESTATION_CERTIFICATE.len()],
|
||||
&ATTESTATION_CERTIFICATE[..]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_register_bad_message() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let application = [0x0A; 32];
|
||||
let message = create_register_message(&application);
|
||||
let response = Ctap1Command::process_command(
|
||||
&message[..message.len() - 1],
|
||||
&mut ctap_state,
|
||||
START_CLOCK_VALUE,
|
||||
);
|
||||
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_register_without_up() {
|
||||
let application = [0x0A; 32];
|
||||
let message = create_register_message(&application);
|
||||
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
|
||||
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
|
||||
let response =
|
||||
Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_rp() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
let application = [0x55; 32];
|
||||
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_length() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
let mut message =
|
||||
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
|
||||
message.push(0x00);
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
|
||||
|
||||
// Two extra zeros are okay, they could encode the expected response length.
|
||||
message.push(0x00);
|
||||
message.push(0x00);
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_cla() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
let mut message =
|
||||
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
message[0] = 0xEE;
|
||||
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_ins() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
let mut message =
|
||||
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
message[1] = 0xEE;
|
||||
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_check_only_wrong_flags() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
let mut message =
|
||||
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
|
||||
message[2] = 0xEE;
|
||||
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_enforce() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
let message =
|
||||
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
|
||||
|
||||
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
|
||||
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
|
||||
let response =
|
||||
Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap();
|
||||
assert_eq!(response[0], 0x01);
|
||||
if USE_SIGNATURE_COUNTER {
|
||||
assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x01]);
|
||||
} else {
|
||||
assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x00]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_dont_enforce() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let sk = crypto::ecdsa::SecKey::gensk(&mut rng);
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
let rp_id = "example.com";
|
||||
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
|
||||
let key_handle = ctap_state.encrypt_key_handle(sk, &application);
|
||||
let message = create_authenticate_message(
|
||||
&application,
|
||||
Ctap1Flags::DontEnforceUpAndSign,
|
||||
&key_handle,
|
||||
);
|
||||
|
||||
let response =
|
||||
Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE).unwrap();
|
||||
assert_eq!(response[0], 0x01);
|
||||
if USE_SIGNATURE_COUNTER {
|
||||
assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x01]);
|
||||
} else {
|
||||
assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x00]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_bad_key_handle() {
|
||||
let application = [0x0A; 32];
|
||||
let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE];
|
||||
let message =
|
||||
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
|
||||
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
|
||||
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
|
||||
let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_authenticate_without_up() {
|
||||
let application = [0x0A; 32];
|
||||
let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE];
|
||||
let message =
|
||||
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
|
||||
|
||||
let mut rng = ThreadRng256 {};
|
||||
let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1");
|
||||
let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence);
|
||||
|
||||
ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE);
|
||||
ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE);
|
||||
let response =
|
||||
Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE);
|
||||
assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED));
|
||||
}
|
||||
}
|
||||
1020
src/ctap/data_formats.rs
Normal file
1020
src/ctap/data_formats.rs
Normal file
File diff suppressed because it is too large
Load Diff
596
src/ctap/hid/mod.rs
Normal file
596
src/ctap/hid/mod.rs
Normal file
@@ -0,0 +1,596 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod receive;
|
||||
mod send;
|
||||
|
||||
use self::receive::MessageAssembler;
|
||||
use self::send::HidPacketIterator;
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
use super::ctap1;
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use super::timed_permission::TimedPermission;
|
||||
use super::CtapState;
|
||||
use crate::timer::{ClockValue, Duration, Timestamp};
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
use core::fmt::Write;
|
||||
use crypto::rng256::Rng256;
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
use libtock::console::Console;
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1
|
||||
// TODO: Channel allocation, section 8.1.3?
|
||||
// TODO: Transaction timeout, section 8.1.5.2
|
||||
|
||||
pub type HidPacket = [u8; 64];
|
||||
pub type ChannelID = [u8; 4];
|
||||
|
||||
pub enum ProcessedPacket<'a> {
|
||||
InitPacket {
|
||||
cmd: u8,
|
||||
len: usize,
|
||||
data: &'a [u8; 57],
|
||||
},
|
||||
ContinuationPacket {
|
||||
seq: u8,
|
||||
data: &'a [u8; 59],
|
||||
},
|
||||
}
|
||||
|
||||
// An assembled CTAPHID command.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
// Channel ID.
|
||||
pub cid: ChannelID,
|
||||
// Command.
|
||||
pub cmd: u8,
|
||||
// Bytes of the message.
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct CtapHid {
|
||||
assembler: MessageAssembler,
|
||||
// The specification (version 20190130) only requires unique CIDs ; the allocation algorithm is
|
||||
// vendor specific.
|
||||
// We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are
|
||||
// allocated.
|
||||
// In packets, the ids are then encoded with the native endianness (with the
|
||||
// u32::to/from_ne_bytes methods).
|
||||
allocated_cids: usize,
|
||||
pub wink_permission: TimedPermission,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum KeepaliveStatus {
|
||||
Processing,
|
||||
UpNeeded,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
// TODO(kaczmarczyck) disable the warning in the end
|
||||
impl CtapHid {
|
||||
// CTAP specification (version 20190130) section 8.1.3
|
||||
const CHANNEL_RESERVED: ChannelID = [0, 0, 0, 0];
|
||||
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
||||
const TYPE_INIT_BIT: u8 = 0x80;
|
||||
const PACKET_TYPE_MASK: u8 = 0x80;
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.9
|
||||
const COMMAND_PING: u8 = 0x01;
|
||||
const COMMAND_MSG: u8 = 0x03;
|
||||
const COMMAND_INIT: u8 = 0x06;
|
||||
const COMMAND_CBOR: u8 = 0x10;
|
||||
pub const COMMAND_CANCEL: u8 = 0x11;
|
||||
const COMMAND_KEEPALIVE: u8 = 0x3B;
|
||||
const COMMAND_ERROR: u8 = 0x3F;
|
||||
// TODO: optional lock command
|
||||
const COMMAND_LOCK: u8 = 0x04;
|
||||
const COMMAND_WINK: u8 = 0x08;
|
||||
const COMMAND_VENDOR_FIRST: u8 = 0x40;
|
||||
const COMMAND_VENDOR_LAST: u8 = 0x7F;
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.6
|
||||
const ERR_INVALID_CMD: u8 = 0x01;
|
||||
const ERR_INVALID_PAR: u8 = 0x02;
|
||||
const ERR_INVALID_LEN: u8 = 0x03;
|
||||
const ERR_INVALID_SEQ: u8 = 0x04;
|
||||
const ERR_MSG_TIMEOUT: u8 = 0x05;
|
||||
const ERR_CHANNEL_BUSY: u8 = 0x06;
|
||||
const ERR_LOCK_REQUIRED: u8 = 0x0A;
|
||||
const ERR_INVALID_CHANNEL: u8 = 0x0B;
|
||||
const ERR_OTHER: u8 = 0x7F;
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.3
|
||||
const PROTOCOL_VERSION: u8 = 2;
|
||||
|
||||
// The device version number is vendor-defined. For now we define them to be zero.
|
||||
// TODO: Update with device version?
|
||||
const DEVICE_VERSION_MAJOR: u8 = 0;
|
||||
const DEVICE_VERSION_MINOR: u8 = 0;
|
||||
const DEVICE_VERSION_BUILD: u8 = 0;
|
||||
|
||||
const CAPABILITY_WINK: u8 = 0x01;
|
||||
const CAPABILITY_CBOR: u8 = 0x04;
|
||||
const CAPABILITY_NMSG: u8 = 0x08;
|
||||
// Capabilitites currently supported by this device.
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
const CAPABILITIES: u8 = CtapHid::CAPABILITY_WINK | CtapHid::CAPABILITY_CBOR;
|
||||
#[cfg(not(feature = "with_ctap1"))]
|
||||
const CAPABILITIES: u8 =
|
||||
CtapHid::CAPABILITY_WINK | CtapHid::CAPABILITY_CBOR | CtapHid::CAPABILITY_NMSG;
|
||||
|
||||
// TODO: Is this timeout duration specified?
|
||||
const TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(100);
|
||||
const WINK_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(5000);
|
||||
|
||||
pub fn new() -> CtapHid {
|
||||
CtapHid {
|
||||
assembler: MessageAssembler::new(),
|
||||
allocated_cids: 0,
|
||||
wink_permission: TimedPermission::waiting(),
|
||||
}
|
||||
}
|
||||
|
||||
// Process an incoming USB HID packet, and optionally returns a list of outgoing packets to
|
||||
// send as a reply.
|
||||
pub fn process_hid_packet<R, CheckUserPresence>(
|
||||
&mut self,
|
||||
packet: &HidPacket,
|
||||
clock_value: ClockValue,
|
||||
ctap_state: &mut CtapState<R, CheckUserPresence>,
|
||||
) -> HidPacketIterator
|
||||
where
|
||||
R: Rng256,
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
// TODO: Send COMMAND_KEEPALIVE every 100ms?
|
||||
match self
|
||||
.assembler
|
||||
.parse_packet(packet, Timestamp::<isize>::from_clock_value(clock_value))
|
||||
{
|
||||
Ok(Some(message)) => {
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
writeln!(&mut Console::new(), "Received message: {:02x?}", message).unwrap();
|
||||
|
||||
let cid = message.cid;
|
||||
if !self.has_valid_channel(&message) {
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
writeln!(&mut Console::new(), "Invalid channel: {:02x?}", cid).unwrap();
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL);
|
||||
}
|
||||
// If another command arrives, stop winking to prevent accidential button touches.
|
||||
self.wink_permission = TimedPermission::waiting();
|
||||
|
||||
match message.cmd {
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.1
|
||||
CtapHid::COMMAND_MSG => {
|
||||
// If we don't have CTAP1 backward compatibilty, this command in invalid.
|
||||
#[cfg(not(feature = "with_ctap1"))]
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD);
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
match ctap1::Ctap1Command::process_command(
|
||||
&message.payload,
|
||||
ctap_state,
|
||||
clock_value,
|
||||
) {
|
||||
Ok(payload) => CtapHid::ctap1_success_message(cid, &payload),
|
||||
Err(ctap1_status_code) => {
|
||||
CtapHid::ctap1_error_message(cid, ctap1_status_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.2
|
||||
CtapHid::COMMAND_CBOR => {
|
||||
// CTAP specification (version 20190130) section 8.1.5.1
|
||||
// Each transaction is atomic, so we process the command directly here and
|
||||
// don't handle any other packet in the meantime.
|
||||
// TODO: Send keep-alive packets in the meantime.
|
||||
let response = ctap_state.process_command(&message.payload, cid);
|
||||
if let Some(iterator) = CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_CBOR,
|
||||
payload: response,
|
||||
}) {
|
||||
iterator
|
||||
} else {
|
||||
// Handle the case of a payload > 7609 bytes.
|
||||
// Although this shouldn't happen if the FIDO2 commands are implemented
|
||||
// correctly, we reply with a vendor specific code instead of silently
|
||||
// ignoring the error.
|
||||
//
|
||||
// The error payload that we send instead is 1 <= 7609 bytes, so it is
|
||||
// safe to unwrap() the result.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_CBOR,
|
||||
payload: vec![
|
||||
Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG as u8,
|
||||
],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.3
|
||||
CtapHid::COMMAND_INIT => {
|
||||
if cid == CtapHid::CHANNEL_BROADCAST {
|
||||
if message.payload.len() != 8 {
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN);
|
||||
}
|
||||
|
||||
// TODO: Prevent allocating 2^32 channels.
|
||||
self.allocated_cids += 1;
|
||||
let allocated_cid = (self.allocated_cids as u32).to_ne_bytes();
|
||||
|
||||
let mut payload = vec![0; 17];
|
||||
payload[..8].copy_from_slice(&message.payload);
|
||||
payload[8..12].copy_from_slice(&allocated_cid);
|
||||
payload[12] = CtapHid::PROTOCOL_VERSION;
|
||||
payload[13] = CtapHid::DEVICE_VERSION_MAJOR;
|
||||
payload[14] = CtapHid::DEVICE_VERSION_MINOR;
|
||||
payload[15] = CtapHid::DEVICE_VERSION_BUILD;
|
||||
payload[16] = CtapHid::CAPABILITIES;
|
||||
|
||||
// This unwrap is safe because the payload length is 17 <= 7609 bytes.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload,
|
||||
})
|
||||
.unwrap()
|
||||
} else {
|
||||
// Sync the channel and discard the current transaction.
|
||||
// TODO: The specification (version 20190130) wording isn't clear about
|
||||
// the payload format in this case.
|
||||
//
|
||||
// This unwrap is safe because the payload length is 0 <= 7609 bytes.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload: vec![],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.4
|
||||
CtapHid::COMMAND_PING => {
|
||||
// Pong the same message.
|
||||
// This unwrap is safe because if we could parse the incoming message, it's
|
||||
// payload length must be <= 7609 bytes.
|
||||
CtapHid::split_message(message).unwrap()
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.5
|
||||
CtapHid::COMMAND_CANCEL => {
|
||||
// Authenticators MUST NOT reply to this message.
|
||||
// CANCEL is handled during user presence checks in main.
|
||||
HidPacketIterator::none()
|
||||
}
|
||||
// Optional commands
|
||||
// CTAP specification (version 20190130) section 8.1.9.2.1
|
||||
CtapHid::COMMAND_WINK => {
|
||||
if !message.payload.is_empty() {
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN);
|
||||
}
|
||||
self.wink_permission =
|
||||
TimedPermission::granted(clock_value, CtapHid::WINK_TIMEOUT_DURATION);
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_WINK,
|
||||
payload: vec![],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.2.2
|
||||
// TODO: implement LOCK
|
||||
_ => {
|
||||
// Unknown or unsupported command.
|
||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Waiting for more packets to assemble the message, nothing to send for now.
|
||||
HidPacketIterator::none()
|
||||
}
|
||||
Err((cid, error)) => {
|
||||
if !self.is_allocated_channel(cid) {
|
||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL)
|
||||
} else {
|
||||
match error {
|
||||
receive::Error::UnexpectedChannel => {
|
||||
CtapHid::error_message(cid, CtapHid::ERR_CHANNEL_BUSY)
|
||||
}
|
||||
receive::Error::UnexpectedInit => {
|
||||
// TODO: Should we send another error code in this case?
|
||||
// Technically, we were expecting a sequence number and got another
|
||||
// byte, although the command/seqnum bit has higher-level semantics
|
||||
// than sequence numbers.
|
||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ)
|
||||
}
|
||||
receive::Error::UnexpectedContinuation => {
|
||||
// CTAP specification (version 20190130) section 8.1.5.4
|
||||
// Spurious continuation packets will be ignored.
|
||||
HidPacketIterator::none()
|
||||
}
|
||||
receive::Error::UnexpectedSeq => {
|
||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ)
|
||||
}
|
||||
receive::Error::Timeout => {
|
||||
CtapHid::error_message(cid, CtapHid::ERR_MSG_TIMEOUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_valid_channel(&self, message: &Message) -> bool {
|
||||
match message.cid {
|
||||
// Only INIT commands use the broadcast channel.
|
||||
CtapHid::CHANNEL_BROADCAST => message.cmd == CtapHid::COMMAND_INIT,
|
||||
// Check that the channel is allocated.
|
||||
_ => self.is_allocated_channel(message.cid),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_allocated_channel(&self, cid: ChannelID) -> bool {
|
||||
cid != CtapHid::CHANNEL_RESERVED && u32::from_ne_bytes(cid) as usize <= self.allocated_cids
|
||||
}
|
||||
|
||||
fn error_message(cid: ChannelID, error_code: u8) -> HidPacketIterator {
|
||||
// This unwrap is safe because the payload length is 1 <= 7609 bytes.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_ERROR,
|
||||
payload: vec![error_code],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn process_single_packet(packet: &HidPacket) -> (&ChannelID, ProcessedPacket) {
|
||||
let (cid, rest) = array_refs![packet, 4, 60];
|
||||
if rest[0] & CtapHid::PACKET_TYPE_MASK != 0 {
|
||||
let cmd = rest[0] & !CtapHid::PACKET_TYPE_MASK;
|
||||
let len = (rest[1] as usize) << 8 | (rest[2] as usize);
|
||||
(
|
||||
cid,
|
||||
ProcessedPacket::InitPacket {
|
||||
cmd,
|
||||
len,
|
||||
data: array_ref!(rest, 3, 57),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
cid,
|
||||
ProcessedPacket::ContinuationPacket {
|
||||
seq: rest[0],
|
||||
data: array_ref!(rest, 1, 59),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn split_message(message: Message) -> Option<HidPacketIterator> {
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
writeln!(&mut Console::new(), "Sending message: {:02x?}", message).unwrap();
|
||||
HidPacketIterator::new(message)
|
||||
}
|
||||
|
||||
pub fn keepalive(cid: ChannelID, status: KeepaliveStatus) -> HidPacketIterator {
|
||||
let status_code = match status {
|
||||
KeepaliveStatus::Processing => 1,
|
||||
KeepaliveStatus::UpNeeded => 2,
|
||||
};
|
||||
// This unwrap is safe because the payload length is 1 <= 7609 bytes.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_KEEPALIVE,
|
||||
payload: vec![status_code],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
fn ctap1_error_message(
|
||||
cid: ChannelID,
|
||||
error_code: ctap1::Ctap1StatusCode,
|
||||
) -> HidPacketIterator {
|
||||
// This unwrap is safe because the payload length is 2 <= 7609 bytes
|
||||
let code: u16 = error_code.into();
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_MSG,
|
||||
payload: code.to_be_bytes().to_vec(),
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> HidPacketIterator {
|
||||
let mut response = payload.to_vec();
|
||||
let code: u16 = ctap1::Ctap1StatusCode::SW_NO_ERROR.into();
|
||||
response.extend_from_slice(&code.to_be_bytes());
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_MSG,
|
||||
payload: response,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crypto::rng256::ThreadRng256;
|
||||
|
||||
const CLOCK_FREQUENCY_HZ: usize = 32768;
|
||||
// Except for tests for timeouts (done in ctap1.rs), transactions are time independant.
|
||||
const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ);
|
||||
const DUMMY_TIMESTAMP: Timestamp<isize> = Timestamp::from_ms(0);
|
||||
|
||||
fn process_messages<CheckUserPresence>(
|
||||
ctap_hid: &mut CtapHid,
|
||||
ctap_state: &mut CtapState<ThreadRng256, CheckUserPresence>,
|
||||
request: Vec<Message>,
|
||||
) -> Option<Vec<Message>>
|
||||
where
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
let mut result = Vec::new();
|
||||
let mut assembler_reply = MessageAssembler::new();
|
||||
for msg_request in request {
|
||||
for pkt_request in HidPacketIterator::new(msg_request).unwrap() {
|
||||
for pkt_reply in
|
||||
ctap_hid.process_hid_packet(&pkt_request, DUMMY_CLOCK_VALUE, ctap_state)
|
||||
{
|
||||
match assembler_reply.parse_packet(&pkt_reply, DUMMY_TIMESTAMP) {
|
||||
Ok(Some(message)) => result.push(message),
|
||||
Ok(None) => (),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn cid_from_init<CheckUserPresence>(
|
||||
ctap_hid: &mut CtapHid,
|
||||
ctap_state: &mut CtapState<ThreadRng256, CheckUserPresence>,
|
||||
) -> ChannelID
|
||||
where
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
let nonce = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
|
||||
let reply = process_messages(
|
||||
ctap_hid,
|
||||
ctap_state,
|
||||
vec![Message {
|
||||
cid: CtapHid::CHANNEL_BROADCAST,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload: nonce.clone(),
|
||||
}],
|
||||
);
|
||||
|
||||
let mut cid_in_payload: ChannelID = Default::default();
|
||||
if let Some(messages) = reply {
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(messages[0].payload.len() >= 12);
|
||||
assert_eq!(nonce, &messages[0].payload[..8]);
|
||||
cid_in_payload.copy_from_slice(&messages[0].payload[8..12]);
|
||||
} else {
|
||||
panic!("The init process was not successful to generate a valid channel ID.")
|
||||
}
|
||||
cid_in_payload
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_assemble() {
|
||||
for payload_len in 0..7609 {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![0xFF; payload_len],
|
||||
};
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut assembler = MessageAssembler::new();
|
||||
for packet in HidPacketIterator::new(message.clone()).unwrap() {
|
||||
match assembler.parse_packet(&packet, DUMMY_TIMESTAMP) {
|
||||
Ok(Some(msg)) => messages.push(msg),
|
||||
Ok(None) => (),
|
||||
Err(_) => panic!("Couldn't assemble packet: {:02x?}", &packet as &[u8]),
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(messages, vec![message]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_init() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let user_immediately_present = |_| Ok(());
|
||||
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
|
||||
let mut ctap_hid = CtapHid::new();
|
||||
|
||||
let reply = process_messages(
|
||||
&mut ctap_hid,
|
||||
&mut ctap_state,
|
||||
vec![Message {
|
||||
cid: CtapHid::CHANNEL_BROADCAST,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0],
|
||||
}],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
reply,
|
||||
Some(vec![Message {
|
||||
cid: CtapHid::CHANNEL_BROADCAST,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload: vec![
|
||||
0x12, // Nonce
|
||||
0x34,
|
||||
0x56,
|
||||
0x78,
|
||||
0x9A,
|
||||
0xBC,
|
||||
0xDE,
|
||||
0xF0,
|
||||
0x01, // Allocated CID
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02, // Protocol version
|
||||
0x00, // Device version
|
||||
0x00,
|
||||
0x00,
|
||||
CtapHid::CAPABILITIES
|
||||
]
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_ping() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let user_immediately_present = |_| Ok(());
|
||||
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
|
||||
let mut ctap_hid = CtapHid::new();
|
||||
let cid = cid_from_init(&mut ctap_hid, &mut ctap_state);
|
||||
|
||||
let reply = process_messages(
|
||||
&mut ctap_hid,
|
||||
&mut ctap_state,
|
||||
vec![Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_PING,
|
||||
payload: vec![0x99, 0x99],
|
||||
}],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
reply,
|
||||
Some(vec![Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_PING,
|
||||
payload: vec![0x99, 0x99]
|
||||
}])
|
||||
);
|
||||
}
|
||||
}
|
||||
590
src/ctap/hid/receive.rs
Normal file
590
src/ctap/hid/receive.rs
Normal file
@@ -0,0 +1,590 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{ChannelID, CtapHid, HidPacket, Message, ProcessedPacket};
|
||||
use crate::timer::Timestamp;
|
||||
use alloc::vec::Vec;
|
||||
use core::mem::swap;
|
||||
|
||||
// A structure to assemble CTAPHID commands from a series of incoming USB HID packets.
|
||||
pub struct MessageAssembler {
|
||||
// Whether this is waiting to receive an initialization packet.
|
||||
idle: bool,
|
||||
// Current channel ID.
|
||||
cid: ChannelID,
|
||||
// Timestamp of the last packet received on the current channel.
|
||||
last_timestamp: Timestamp<isize>,
|
||||
// Current command.
|
||||
cmd: u8,
|
||||
// Sequence number expected for the next packet.
|
||||
seq: u8,
|
||||
// Number of bytes left to fill the current message.
|
||||
remaining_payload_len: usize,
|
||||
// Buffer for the current payload.
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Error {
|
||||
// Expected a continuation packet on a specific channel, got a packet on another channel.
|
||||
UnexpectedChannel,
|
||||
// Expected a continuation packet, got an init packet.
|
||||
UnexpectedInit,
|
||||
// Expected an init packet, got a continuation packet.
|
||||
UnexpectedContinuation,
|
||||
// Expected a continuation packet with a specific sequence number, got another sequence number.
|
||||
UnexpectedSeq,
|
||||
// This packet arrived after a timeout.
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl MessageAssembler {
|
||||
pub fn new() -> MessageAssembler {
|
||||
MessageAssembler {
|
||||
idle: true,
|
||||
cid: [0, 0, 0, 0],
|
||||
last_timestamp: Timestamp::from_ms(0),
|
||||
cmd: 0,
|
||||
seq: 0,
|
||||
remaining_payload_len: 0,
|
||||
payload: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Resets the message assembler to the idle state.
|
||||
// The caller can reset the assembler for example due to a timeout.
|
||||
pub fn reset(&mut self) {
|
||||
self.idle = true;
|
||||
self.cid = [0, 0, 0, 0];
|
||||
self.last_timestamp = Timestamp::from_ms(0);
|
||||
self.cmd = 0;
|
||||
self.seq = 0;
|
||||
self.remaining_payload_len = 0;
|
||||
self.payload.clear();
|
||||
}
|
||||
|
||||
// Returns:
|
||||
// - An Ok() result if the packet was parsed correctly. This contains either Some(Vec<u8>) if a
|
||||
// full message was assembled after this packet, or None if more packets are needed to fill the
|
||||
// message.
|
||||
// - An Err() result if there was a parsing error.
|
||||
// TODO: Implement timeouts. For example, have the caller pass us a timestamp of when this
|
||||
// packet was received.
|
||||
pub fn parse_packet(
|
||||
&mut self,
|
||||
packet: &HidPacket,
|
||||
timestamp: Timestamp<isize>,
|
||||
) -> Result<Option<Message>, (ChannelID, Error)> {
|
||||
// TODO: Support non-full-speed devices (i.e. packet len != 64)? This isn't recommended by
|
||||
// section 8.8.1
|
||||
let (cid, processed_packet) = CtapHid::process_single_packet(&packet);
|
||||
|
||||
if !self.idle && timestamp - self.last_timestamp >= CtapHid::TIMEOUT_DURATION {
|
||||
// The current channel timed out.
|
||||
// Save the channel ID and reset the state.
|
||||
let current_cid = self.cid;
|
||||
self.reset();
|
||||
|
||||
// If the packet is from the timed-out channel, send back a timeout error.
|
||||
// Otherwise, proceed with processing the packet.
|
||||
if *cid == current_cid {
|
||||
return Err((*cid, Error::Timeout));
|
||||
}
|
||||
}
|
||||
|
||||
if self.idle {
|
||||
// Expecting an initialization packet.
|
||||
match processed_packet {
|
||||
ProcessedPacket::InitPacket { cmd, len, data } => {
|
||||
Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp))
|
||||
}
|
||||
ProcessedPacket::ContinuationPacket { .. } => {
|
||||
// CTAP specification (version 20190130) section 8.1.5.4
|
||||
// Spurious continuation packets will be ignored.
|
||||
Err((*cid, Error::UnexpectedContinuation))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Expecting a continuation packet from the current channel.
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.5.1
|
||||
// Reject packets from other channels.
|
||||
if *cid != self.cid {
|
||||
return Err((*cid, Error::UnexpectedChannel));
|
||||
}
|
||||
|
||||
match processed_packet {
|
||||
// Unexpected initialization packet.
|
||||
ProcessedPacket::InitPacket { cmd, len, data } => {
|
||||
self.reset();
|
||||
if cmd == CtapHid::COMMAND_INIT {
|
||||
Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp))
|
||||
} else {
|
||||
Err((*cid, Error::UnexpectedInit))
|
||||
}
|
||||
}
|
||||
ProcessedPacket::ContinuationPacket { seq, data } => {
|
||||
if seq != self.seq {
|
||||
// Reject packets with the wrong sequence number.
|
||||
self.reset();
|
||||
Err((*cid, Error::UnexpectedSeq))
|
||||
} else {
|
||||
// Update the last timestamp.
|
||||
self.last_timestamp = timestamp;
|
||||
// Increment the sequence number for the next packet.
|
||||
self.seq += 1;
|
||||
Ok(self.append_payload(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_init_packet(
|
||||
&mut self,
|
||||
cid: ChannelID,
|
||||
cmd: u8,
|
||||
len: usize,
|
||||
data: &[u8],
|
||||
timestamp: Timestamp<isize>,
|
||||
) -> Option<Message> {
|
||||
// TODO: Should invalid commands/payload lengths be rejected early, i.e. as soon as the
|
||||
// initialization packet is received, or should we build a message and then catch the
|
||||
// error?
|
||||
// The specification (version 20190130) isn't clear on this point.
|
||||
self.cid = cid;
|
||||
self.last_timestamp = timestamp;
|
||||
self.cmd = cmd;
|
||||
self.seq = 0;
|
||||
self.remaining_payload_len = len;
|
||||
self.append_payload(data)
|
||||
}
|
||||
|
||||
fn append_payload(&mut self, data: &[u8]) -> Option<Message> {
|
||||
if data.len() < self.remaining_payload_len {
|
||||
self.payload.extend_from_slice(data);
|
||||
self.idle = false;
|
||||
self.remaining_payload_len -= data.len();
|
||||
None
|
||||
} else {
|
||||
self.payload
|
||||
.extend_from_slice(&data[..self.remaining_payload_len]);
|
||||
self.idle = true;
|
||||
let mut payload = Vec::new();
|
||||
swap(&mut self.payload, &mut payload);
|
||||
Some(Message {
|
||||
cid: self.cid,
|
||||
cmd: self.cmd,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::timer::Duration;
|
||||
|
||||
// Except for tests that exercise timeouts, all packets are synchronized at the same dummy
|
||||
// timestamp.
|
||||
const DUMMY_TIMESTAMP: Timestamp<isize> = Timestamp::from_ms(0);
|
||||
|
||||
fn byte_extend(bytes: &[u8], padding: u8) -> HidPacket {
|
||||
let len = bytes.len();
|
||||
assert!(len <= 64);
|
||||
let mut result = [0; 64];
|
||||
result[..len].copy_from_slice(bytes);
|
||||
for byte in result[len..].iter_mut() {
|
||||
*byte = padding;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn zero_extend(bytes: &[u8]) -> HidPacket {
|
||||
byte_extend(bytes, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_payload() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_packet() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![0x00; 0x10]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonzero_padding() {
|
||||
// CTAP specification (version 20190130) section 8.1.4
|
||||
// It is written that "Unused bytes SHOULD be set to zero", so we test that non-zero
|
||||
// padding is accepted as well.
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10], 0xFF),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![0xFF; 0x10]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_packets() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x40]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_three_packets() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x80]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_packets() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
for seq in 0..0x7F {
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, seq]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x1DB9]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_messages() {
|
||||
// Check that after yielding a message, the assembler is ready to process new messages.
|
||||
let mut assembler = MessageAssembler::new();
|
||||
for i in 0..10 {
|
||||
// Introduce some variability in the messages.
|
||||
let cmd = 2 * i;
|
||||
let byte = 3 * i;
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80 | cmd, 0x00, 0x80], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x00], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x01], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd,
|
||||
payload: vec![byte; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channel_switch() {
|
||||
// Check that the assembler can process messages from multiple channels, sequentially.
|
||||
let mut assembler = MessageAssembler::new();
|
||||
for i in 0..10 {
|
||||
// Introduce some variability in the messages.
|
||||
let cid = 0x78 + i;
|
||||
let cmd = 2 * i;
|
||||
let byte = 3 * i;
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x80 | cmd, 0x00, 0x80], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x00], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x01], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, cid],
|
||||
cmd,
|
||||
payload: vec![byte; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_channel() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
|
||||
// Check that many sorts of packets on another channel are ignored.
|
||||
for cmd in 0..=0xFF {
|
||||
for byte in 0..=0xFF {
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x9A, cmd, 0x00], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x9A], Error::UnexpectedChannel))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x40]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spurious_continuation_packets() {
|
||||
// CTAP specification (version 20190130) section 8.1.5.4
|
||||
// Spurious continuation packets appearing without a prior initialization packet will be
|
||||
// ignored.
|
||||
let mut assembler = MessageAssembler::new();
|
||||
for i in 0..0x80 {
|
||||
// Some legit packet.
|
||||
let byte = 2 * i;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![byte; 0x10]
|
||||
}))
|
||||
);
|
||||
|
||||
// Spurious continuation packet.
|
||||
let seq = i;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, seq]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], Error::UnexpectedContinuation))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_init() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], Error::UnexpectedInit))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_seq() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], Error::UnexpectedSeq))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timed_out_packet() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00]),
|
||||
DUMMY_TIMESTAMP + CtapHid::TIMEOUT_DURATION
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], Error::Timeout))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_just_in_time_packets() {
|
||||
let mut timestamp = DUMMY_TIMESTAMP;
|
||||
// Delay between each packet is just below the threshold.
|
||||
let delay = CtapHid::TIMEOUT_DURATION - Duration::from_ms(1);
|
||||
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
|
||||
timestamp
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
for seq in 0..0x7F {
|
||||
timestamp += delay;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&zero_extend(&[0x12, 0x34, 0x56, 0x78, seq]), timestamp),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
timestamp += delay;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F]), timestamp),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x1DB9]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: more tests
|
||||
}
|
||||
296
src/ctap/hid/send.rs
Normal file
296
src/ctap/hid/send.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{CtapHid, HidPacket, Message};
|
||||
|
||||
pub struct HidPacketIterator(Option<MessageSplitter>);
|
||||
|
||||
impl HidPacketIterator {
|
||||
pub fn new(message: Message) -> Option<HidPacketIterator> {
|
||||
let splitter = MessageSplitter::new(message);
|
||||
if splitter.is_some() {
|
||||
Some(HidPacketIterator(splitter))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn none() -> HidPacketIterator {
|
||||
HidPacketIterator(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for HidPacketIterator {
|
||||
type Item = HidPacket;
|
||||
|
||||
fn next(&mut self) -> Option<HidPacket> {
|
||||
match &mut self.0 {
|
||||
Some(splitter) => splitter.next(),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MessageSplitter {
|
||||
message: Message,
|
||||
packet: HidPacket,
|
||||
seq: Option<u8>,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl MessageSplitter {
|
||||
// Try to split this message into an iterator of HID packets. This fails if the message is too
|
||||
// long to fit into a sequence of HID packets (which is limited to 7609 bytes).
|
||||
pub fn new(message: Message) -> Option<MessageSplitter> {
|
||||
if message.payload.len() > 7609 {
|
||||
None
|
||||
} else {
|
||||
// Cache the CID, as it is constant for all packets in this message.
|
||||
let mut packet = [0; 64];
|
||||
packet[..4].copy_from_slice(&message.cid);
|
||||
|
||||
Some(MessageSplitter {
|
||||
message,
|
||||
packet,
|
||||
seq: None,
|
||||
i: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Copy as many bytes as possible from data to dst, and return how many bytes are copied.
|
||||
// Contrary to copy_from_slice, this doesn't require slices of the same length.
|
||||
// All unused bytes in dst are set to zero, as if the data was padded with zeros to match.
|
||||
fn consume_data(dst: &mut [u8], data: &[u8]) -> usize {
|
||||
let dst_len = dst.len();
|
||||
let data_len = data.len();
|
||||
|
||||
if data_len <= dst_len {
|
||||
// data fits in dst, copy all the bytes.
|
||||
dst[..data_len].copy_from_slice(data);
|
||||
for byte in dst[data_len..].iter_mut() {
|
||||
*byte = 0;
|
||||
}
|
||||
data_len
|
||||
} else {
|
||||
// Fill all of dst.
|
||||
dst.copy_from_slice(&data[..dst_len]);
|
||||
dst_len
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for MessageSplitter {
|
||||
type Item = HidPacket;
|
||||
|
||||
fn next(&mut self) -> Option<HidPacket> {
|
||||
let payload_len = self.message.payload.len();
|
||||
match self.seq {
|
||||
None => {
|
||||
// First, send an initialization packet.
|
||||
self.packet[4] = self.message.cmd | CtapHid::TYPE_INIT_BIT;
|
||||
self.packet[5] = (payload_len >> 8) as u8;
|
||||
self.packet[6] = payload_len as u8;
|
||||
|
||||
self.seq = Some(0);
|
||||
self.i =
|
||||
MessageSplitter::consume_data(&mut self.packet[7..], &self.message.payload);
|
||||
Some(self.packet)
|
||||
}
|
||||
Some(seq) => {
|
||||
// Send the next continuation packet, if any.
|
||||
if self.i < payload_len {
|
||||
self.packet[4] = seq;
|
||||
self.seq = Some(seq + 1);
|
||||
self.i += MessageSplitter::consume_data(
|
||||
&mut self.packet[5..],
|
||||
&self.message.payload[self.i..],
|
||||
);
|
||||
Some(self.packet)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn assert_packet_output_equality(message: Message, expected_packets: Vec<HidPacket>) {
|
||||
let packets: Vec<HidPacket> = HidPacketIterator::new(message).unwrap().collect();
|
||||
assert_eq!(packets.len(), expected_packets.len());
|
||||
for (packet, expected_packet) in packets.iter().zip(expected_packets.iter()) {
|
||||
assert_eq!(packet as &[u8], expected_packet as &[u8]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_single_packet() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x4C,
|
||||
payload: vec![0xAA, 0xBB],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![[
|
||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x02, 0xAA, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_big_single_packet() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x4C,
|
||||
payload: vec![0xAA; 64 - 7],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![[
|
||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x39, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
]];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_two_packets() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x4C,
|
||||
payload: vec![0xAA; 64 - 7 + 1],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x3A, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
],
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
],
|
||||
];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_two_full_packets() {
|
||||
let mut payload = vec![0xAA; 64 - 7];
|
||||
payload.extend(vec![0xBB; 64 - 5]);
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x4C,
|
||||
payload,
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x74, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
],
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x00, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
],
|
||||
];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_max_packets() {
|
||||
let mut payload = vec![0xFF; 64 - 7];
|
||||
for i in 0..128 {
|
||||
payload.extend(vec![i + 1; 64 - 5]);
|
||||
}
|
||||
|
||||
// Sanity check for the length of the payload.
|
||||
assert_eq!((64 - 7) + 128 * (64 - 5), 0x1db9);
|
||||
assert_eq!(7609, 0x1db9);
|
||||
assert_eq!(payload.len(), 0x1db9);
|
||||
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0xAB,
|
||||
payload,
|
||||
};
|
||||
|
||||
let mut expected_packets = Vec::new();
|
||||
expected_packets.push([
|
||||
0x12, 0x34, 0x56, 0x78, 0xAB, 0x1D, 0xB9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]);
|
||||
for i in 0..128 {
|
||||
let mut packet: HidPacket = [0; 64];
|
||||
packet[0] = 0x12;
|
||||
packet[1] = 0x34;
|
||||
packet[2] = 0x56;
|
||||
packet[3] = 0x78;
|
||||
packet[4] = i;
|
||||
for byte in packet.iter_mut().skip(5) {
|
||||
*byte = i + 1;
|
||||
}
|
||||
expected_packets.push(packet);
|
||||
}
|
||||
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_payload_one_too_large() {
|
||||
let payload = vec![0xFF; (64 - 7) + 128 * (64 - 5) + 1];
|
||||
assert_eq!(payload.len(), 0x1dba);
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0xAB,
|
||||
payload,
|
||||
};
|
||||
assert!(HidPacketIterator::new(message).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_payload_way_too_large() {
|
||||
// Check that overflow of u16 doesn't bypass the size limit.
|
||||
let payload = vec![0xFF; 0x10000];
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0xAB,
|
||||
payload,
|
||||
};
|
||||
assert!(HidPacketIterator::new(message).is_none());
|
||||
}
|
||||
|
||||
// TODO(kaczmarczyck) implement and test limits (maximum bytes and packets)
|
||||
}
|
||||
1297
src/ctap/mod.rs
Normal file
1297
src/ctap/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
267
src/ctap/response.rs
Normal file
267
src/ctap/response.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::data_formats::{
|
||||
CoseKey, PackedAttestationStatement, PublicKeyCredentialDescriptor,
|
||||
PublicKeyCredentialUserEntity,
|
||||
};
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
|
||||
pub enum ResponseData {
|
||||
AuthenticatorMakeCredential(AuthenticatorMakeCredentialResponse),
|
||||
AuthenticatorGetAssertion(AuthenticatorGetAssertionResponse),
|
||||
AuthenticatorGetNextAssertion(AuthenticatorGetAssertionResponse),
|
||||
AuthenticatorGetInfo(AuthenticatorGetInfoResponse),
|
||||
AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>),
|
||||
AuthenticatorReset,
|
||||
}
|
||||
|
||||
impl From<ResponseData> for Option<cbor::Value> {
|
||||
fn from(response: ResponseData) -> Self {
|
||||
match response {
|
||||
ResponseData::AuthenticatorMakeCredential(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorGetInfo(data) => Some(data.into()),
|
||||
ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()),
|
||||
ResponseData::AuthenticatorClientPin(None) => None,
|
||||
ResponseData::AuthenticatorReset => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
|
||||
pub struct AuthenticatorMakeCredentialResponse {
|
||||
pub fmt: String,
|
||||
pub auth_data: Vec<u8>,
|
||||
pub att_stmt: PackedAttestationStatement,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorMakeCredentialResponse> for cbor::Value {
|
||||
fn from(make_credential_response: AuthenticatorMakeCredentialResponse) -> Self {
|
||||
let AuthenticatorMakeCredentialResponse {
|
||||
fmt,
|
||||
auth_data,
|
||||
att_stmt,
|
||||
} = make_credential_response;
|
||||
|
||||
cbor_map_options! {
|
||||
1 => fmt,
|
||||
2 => auth_data,
|
||||
3 => att_stmt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
|
||||
pub struct AuthenticatorGetAssertionResponse {
|
||||
pub credential: Option<PublicKeyCredentialDescriptor>,
|
||||
pub auth_data: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
pub user: Option<PublicKeyCredentialUserEntity>,
|
||||
pub number_of_credentials: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorGetAssertionResponse> for cbor::Value {
|
||||
fn from(get_assertion_response: AuthenticatorGetAssertionResponse) -> Self {
|
||||
let AuthenticatorGetAssertionResponse {
|
||||
credential,
|
||||
auth_data,
|
||||
signature,
|
||||
user,
|
||||
number_of_credentials,
|
||||
} = get_assertion_response;
|
||||
|
||||
cbor_map_options! {
|
||||
1 => credential,
|
||||
2 => auth_data,
|
||||
3 => signature,
|
||||
4 => user,
|
||||
5 => number_of_credentials,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
|
||||
pub struct AuthenticatorGetInfoResponse {
|
||||
// TODO(kaczmarczyck) add fields from 2.1
|
||||
pub versions: Vec<String>,
|
||||
pub extensions: Option<Vec<String>>,
|
||||
pub aaguid: [u8; 16],
|
||||
pub options: Option<BTreeMap<String, bool>>,
|
||||
pub max_msg_size: Option<u64>,
|
||||
pub pin_protocols: Option<Vec<u64>>,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorGetInfoResponse> for cbor::Value {
|
||||
fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self {
|
||||
let AuthenticatorGetInfoResponse {
|
||||
versions,
|
||||
extensions,
|
||||
aaguid,
|
||||
options,
|
||||
max_msg_size,
|
||||
pin_protocols,
|
||||
} = get_info_response;
|
||||
|
||||
let options_cbor: Option<cbor::Value> = options.map(|options| {
|
||||
let option_map: BTreeMap<_, _> = options
|
||||
.into_iter()
|
||||
.map(|(key, value)| (cbor_text!(key), cbor_bool!(value)))
|
||||
.collect();
|
||||
cbor_map_btree!(option_map)
|
||||
});
|
||||
|
||||
cbor_map_options! {
|
||||
1 => cbor_array_vec!(versions),
|
||||
2 => extensions.map(|vec| cbor_array_vec!(vec)),
|
||||
3 => &aaguid,
|
||||
4 => options_cbor,
|
||||
5 => max_msg_size,
|
||||
6 => pin_protocols.map(|vec| cbor_array_vec!(vec)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
|
||||
pub struct AuthenticatorClientPinResponse {
|
||||
pub key_agreement: Option<CoseKey>,
|
||||
pub pin_token: Option<Vec<u8>>,
|
||||
pub retries: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorClientPinResponse> for cbor::Value {
|
||||
fn from(client_pin_response: AuthenticatorClientPinResponse) -> Self {
|
||||
let AuthenticatorClientPinResponse {
|
||||
key_agreement,
|
||||
pin_token,
|
||||
retries,
|
||||
} = client_pin_response;
|
||||
|
||||
cbor_map_options! {
|
||||
1 => key_agreement.map(|cose_key| cbor_map_btree!(cose_key.0)),
|
||||
2 => pin_token,
|
||||
3 => retries,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::data_formats::PackedAttestationStatement;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_make_credential_into_cbor() {
|
||||
let certificate: cbor::values::KeyType = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]];
|
||||
let att_stmt = PackedAttestationStatement {
|
||||
alg: 1,
|
||||
sig: vec![0x55, 0x55, 0x55, 0x55],
|
||||
x5c: Some(vec![vec![0x5C, 0x5C, 0x5C, 0x5C]]),
|
||||
ecdaa_key_id: Some(vec![0xEC, 0xDA, 0x1D]),
|
||||
};
|
||||
let cbor_packed_attestation_statement = cbor_map! {
|
||||
"alg" => 1,
|
||||
"sig" => vec![0x55, 0x55, 0x55, 0x55],
|
||||
"x5c" => cbor_array_vec![vec![certificate]],
|
||||
"ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D],
|
||||
};
|
||||
|
||||
let make_credential_response = AuthenticatorMakeCredentialResponse {
|
||||
fmt: "packed".to_string(),
|
||||
auth_data: vec![0xAD],
|
||||
att_stmt,
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorMakeCredential(make_credential_response).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
1 => "packed",
|
||||
2 => vec![0xAD],
|
||||
3 => cbor_packed_attestation_statement,
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_assertion_into_cbor() {
|
||||
let get_assertion_response = AuthenticatorGetAssertionResponse {
|
||||
credential: None,
|
||||
auth_data: vec![0xAD],
|
||||
signature: vec![0x51],
|
||||
user: None,
|
||||
number_of_credentials: None,
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorGetAssertion(get_assertion_response).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
2 => vec![0xAD],
|
||||
3 => vec![0x51],
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info_into_cbor() {
|
||||
let get_info_response = AuthenticatorGetInfoResponse {
|
||||
versions: vec!["FIDO_2_0".to_string()],
|
||||
extensions: None,
|
||||
aaguid: [0x00; 16],
|
||||
options: None,
|
||||
max_msg_size: None,
|
||||
pin_protocols: None,
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorGetInfo(get_info_response).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
1 => cbor_array_vec![vec!["FIDO_2_0"]],
|
||||
3 => vec![0x00; 16],
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_used_client_pin_into_cbor() {
|
||||
let client_pin_response = AuthenticatorClientPinResponse {
|
||||
key_agreement: None,
|
||||
pin_token: Some(vec![70]),
|
||||
retries: None,
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into();
|
||||
let expected_cbor = cbor_map_options! {
|
||||
2 => vec![70],
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_client_pin_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorClientPin(None).into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_into_cbor() {
|
||||
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorReset.into();
|
||||
assert_eq!(response_cbor, None);
|
||||
}
|
||||
}
|
||||
71
src/ctap/status_code.rs
Normal file
71
src/ctap/status_code.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// CTAP specification (version 20190130) section 6.3
|
||||
// For now, only the CTAP2 codes are here, the CTAP1 are not included.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Ctap2StatusCode {
|
||||
CTAP2_OK = 0x00,
|
||||
CTAP1_ERR_INVALID_COMMAND = 0x01,
|
||||
CTAP1_ERR_INVALID_PARAMETER = 0x02,
|
||||
CTAP1_ERR_INVALID_LENGTH = 0x03,
|
||||
CTAP1_ERR_INVALID_SEQ = 0x04,
|
||||
CTAP1_ERR_TIMEOUT = 0x05,
|
||||
CTAP1_ERR_CHANNEL_BUSY = 0x06,
|
||||
CTAP1_ERR_LOCK_REQUIRED = 0x0A,
|
||||
CTAP1_ERR_INVALID_CHANNEL = 0x0B,
|
||||
CTAP2_ERR_CBOR_UNEXPECTED_TYPE = 0x11,
|
||||
CTAP2_ERR_INVALID_CBOR = 0x12,
|
||||
CTAP2_ERR_MISSING_PARAMETER = 0x14,
|
||||
CTAP2_ERR_LIMIT_EXCEEDED = 0x15,
|
||||
CTAP2_ERR_UNSUPPORTED_EXTENSION = 0x16,
|
||||
CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19,
|
||||
CTAP2_ERR_PROCESSING = 0x21,
|
||||
CTAP2_ERR_INVALID_CREDENTIAL = 0x22,
|
||||
CTAP2_ERR_USER_ACTION_PENDING = 0x23,
|
||||
CTAP2_ERR_OPERATION_PENDING = 0x24,
|
||||
CTAP2_ERR_NO_OPERATIONS = 0x25,
|
||||
CTAP2_ERR_UNSUPPORTED_ALGORITHM = 0x26,
|
||||
CTAP2_ERR_OPERATION_DENIED = 0x27,
|
||||
CTAP2_ERR_KEY_STORE_FULL = 0x28,
|
||||
CTAP2_ERR_NO_OPERATION_PENDING = 0x2A,
|
||||
CTAP2_ERR_UNSUPPORTED_OPTION = 0x2B,
|
||||
CTAP2_ERR_INVALID_OPTION = 0x2C,
|
||||
CTAP2_ERR_KEEPALIVE_CANCEL = 0x2D,
|
||||
CTAP2_ERR_NO_CREDENTIALS = 0x2E,
|
||||
CTAP2_ERR_USER_ACTION_TIMEOUT = 0x2F,
|
||||
CTAP2_ERR_NOT_ALLOWED = 0x30,
|
||||
CTAP2_ERR_PIN_INVALID = 0x31,
|
||||
CTAP2_ERR_PIN_BLOCKED = 0x32,
|
||||
CTAP2_ERR_PIN_AUTH_INVALID = 0x33,
|
||||
CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34,
|
||||
CTAP2_ERR_PIN_NOT_SET = 0x35,
|
||||
CTAP2_ERR_PIN_REQUIRED = 0x36,
|
||||
CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37,
|
||||
CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38,
|
||||
CTAP2_ERR_REQUEST_TOO_LARGE = 0x39,
|
||||
CTAP2_ERR_ACTION_TIMEOUT = 0x3A,
|
||||
CTAP2_ERR_UP_REQUIRED = 0x3B,
|
||||
CTAP2_ERR_UV_BLOCKED = 0x3C,
|
||||
CTAP1_ERR_OTHER = 0x7F,
|
||||
CTAP2_ERR_SPEC_LAST = 0xDF,
|
||||
CTAP2_ERR_EXTENSION_FIRST = 0xE0,
|
||||
CTAP2_ERR_EXTENSION_LAST = 0xEF,
|
||||
// CTAP2_ERR_VENDOR_FIRST = 0xF0,
|
||||
CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG = 0xF0,
|
||||
CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR = 0xF1,
|
||||
CTAP2_ERR_VENDOR_LAST = 0xFF,
|
||||
}
|
||||
682
src/ctap/storage.rs
Normal file
682
src/ctap/storage.rs
Normal file
@@ -0,0 +1,682 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::crypto::rng256::Rng256;
|
||||
use crate::ctap::data_formats::PublicKeyCredentialSource;
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::ctap::PIN_AUTH_LENGTH;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryInto;
|
||||
use ctap2::embedded_flash::{self, StoreConfig, StoreEntry, StoreError, StoreIndex};
|
||||
|
||||
#[cfg(test)]
|
||||
type Storage = embedded_flash::BufferStorage;
|
||||
#[cfg(not(test))]
|
||||
type Storage = embedded_flash::SyscallStorage;
|
||||
|
||||
// Those constants may be modified before compilation to tune the behavior of the key.
|
||||
//
|
||||
// The number of pages should be at least 2 and at most what the flash can hold. There should be no
|
||||
// reason to put a small number here, except that the latency of flash operations depends on the
|
||||
// number of pages. This will improve in the future. Currently, using 20 pages gives 65ms per
|
||||
// operation. The rule of thumb is 3.5ms per additional page.
|
||||
//
|
||||
// Limiting the number of residential keys permits to ensure a minimum number of counter increments.
|
||||
// Let:
|
||||
// - P the number of pages (NUM_PAGES)
|
||||
// - K the maximum number of residential keys (MAX_SUPPORTED_RESIDENTIAL_KEYS)
|
||||
// - S the maximum size of a residential key (about 500)
|
||||
// - C the number of erase cycles (10000)
|
||||
// - I the minimum number of counter increments
|
||||
//
|
||||
// We have: I = ((P - 1) * 4092 - K * S) / 12 * C
|
||||
//
|
||||
// With P=20 and K=150, we have I > 2M which is enough for 500 increments per day for 10 years.
|
||||
const NUM_PAGES: usize = 20;
|
||||
const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150;
|
||||
|
||||
// List of tags. They should all be unique. And there should be less than NUM_TAGS.
|
||||
const TAG_CREDENTIAL: usize = 0;
|
||||
const GLOBAL_SIGNATURE_COUNTER: usize = 1;
|
||||
const MASTER_KEYS: usize = 2;
|
||||
const PIN_HASH: usize = 3;
|
||||
const PIN_RETRIES: usize = 4;
|
||||
const NUM_TAGS: usize = 5;
|
||||
|
||||
const MAX_PIN_RETRIES: u8 = 6;
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum Key {
|
||||
// TODO(cretin): Test whether this doesn't consume too much memory. Otherwise, we can use less
|
||||
// keys. Either only a simple enum value for all credentials, or group by rp_id.
|
||||
Credential {
|
||||
rp_id: Option<String>,
|
||||
credential_id: Option<Vec<u8>>,
|
||||
user_handle: Option<Vec<u8>>,
|
||||
},
|
||||
GlobalSignatureCounter,
|
||||
MasterKeys,
|
||||
PinHash,
|
||||
PinRetries,
|
||||
}
|
||||
|
||||
pub struct MasterKeys<'a> {
|
||||
pub encryption: &'a [u8; 32],
|
||||
pub hmac: &'a [u8; 32],
|
||||
}
|
||||
|
||||
struct Config;
|
||||
|
||||
impl StoreConfig for Config {
|
||||
type Key = Key;
|
||||
|
||||
fn num_tags(&self) -> usize {
|
||||
NUM_TAGS
|
||||
}
|
||||
|
||||
fn keys(&self, entry: StoreEntry, mut add: impl FnMut(Key)) {
|
||||
match entry.tag {
|
||||
TAG_CREDENTIAL => {
|
||||
let credential = match deserialize_credential(entry.data) {
|
||||
None => {
|
||||
debug_assert!(false);
|
||||
return;
|
||||
}
|
||||
Some(credential) => credential,
|
||||
};
|
||||
add(Key::Credential {
|
||||
rp_id: Some(credential.rp_id.clone()),
|
||||
credential_id: Some(credential.credential_id),
|
||||
user_handle: None,
|
||||
});
|
||||
add(Key::Credential {
|
||||
rp_id: Some(credential.rp_id.clone()),
|
||||
credential_id: None,
|
||||
user_handle: None,
|
||||
});
|
||||
add(Key::Credential {
|
||||
rp_id: Some(credential.rp_id),
|
||||
credential_id: None,
|
||||
user_handle: Some(credential.user_handle),
|
||||
});
|
||||
add(Key::Credential {
|
||||
rp_id: None,
|
||||
credential_id: None,
|
||||
user_handle: None,
|
||||
});
|
||||
}
|
||||
GLOBAL_SIGNATURE_COUNTER => add(Key::GlobalSignatureCounter),
|
||||
MASTER_KEYS => add(Key::MasterKeys),
|
||||
PIN_HASH => add(Key::PinHash),
|
||||
PIN_RETRIES => add(Key::PinRetries),
|
||||
_ => debug_assert!(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PersistentStore {
|
||||
store: embedded_flash::Store<Storage, Config>,
|
||||
}
|
||||
|
||||
const PAGE_SIZE: usize = 0x1000;
|
||||
const STORE_SIZE: usize = NUM_PAGES * PAGE_SIZE;
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[link_section = ".app_state"]
|
||||
static STORE: [u8; STORE_SIZE] = [0xff; STORE_SIZE];
|
||||
|
||||
impl PersistentStore {
|
||||
/// Gives access to the persistent store.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This should be at most one instance of persistent store per program lifetime.
|
||||
pub fn new(rng: &mut impl Rng256) -> PersistentStore {
|
||||
#[cfg(not(test))]
|
||||
let storage = PersistentStore::new_prod_storage();
|
||||
#[cfg(test)]
|
||||
let storage = PersistentStore::new_test_storage();
|
||||
let mut store = PersistentStore {
|
||||
store: embedded_flash::Store::new(storage, Config).unwrap(),
|
||||
};
|
||||
store.init(rng);
|
||||
store
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn new_prod_storage() -> Storage {
|
||||
let store = unsafe {
|
||||
// Safety: The store cannot alias because this function is called only once.
|
||||
core::slice::from_raw_parts_mut(STORE.as_ptr() as *mut u8, STORE_SIZE)
|
||||
};
|
||||
unsafe {
|
||||
// Safety: The store is in a writeable flash region.
|
||||
Storage::new(store).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn new_test_storage() -> Storage {
|
||||
let store = vec![0xff; STORE_SIZE].into_boxed_slice();
|
||||
let options = embedded_flash::BufferOptions {
|
||||
word_size: 4,
|
||||
page_size: PAGE_SIZE,
|
||||
max_word_writes: 2,
|
||||
max_page_erases: 10000,
|
||||
strict_write: true,
|
||||
};
|
||||
Storage::new(store, options)
|
||||
}
|
||||
|
||||
fn init(&mut self, rng: &mut impl Rng256) {
|
||||
if self.store.find_one(&Key::MasterKeys).is_none() {
|
||||
let master_encryption_key = rng.gen_uniform_u8x32();
|
||||
let master_hmac_key = rng.gen_uniform_u8x32();
|
||||
let mut master_keys = Vec::with_capacity(64);
|
||||
master_keys.extend_from_slice(&master_encryption_key);
|
||||
master_keys.extend_from_slice(&master_hmac_key);
|
||||
self.store
|
||||
.insert(StoreEntry {
|
||||
tag: MASTER_KEYS,
|
||||
data: &master_keys,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
if self.store.find_one(&Key::PinRetries).is_none() {
|
||||
self.store
|
||||
.insert(StoreEntry {
|
||||
tag: PIN_RETRIES,
|
||||
data: &[MAX_PIN_RETRIES],
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_credential(
|
||||
&self,
|
||||
rp_id: &str,
|
||||
credential_id: &[u8],
|
||||
) -> Option<PublicKeyCredentialSource> {
|
||||
let key = Key::Credential {
|
||||
rp_id: Some(rp_id.into()),
|
||||
credential_id: Some(credential_id.into()),
|
||||
user_handle: None,
|
||||
};
|
||||
let (_, entry) = self.store.find_one(&key)?;
|
||||
debug_assert_eq!(entry.tag, TAG_CREDENTIAL);
|
||||
let result = deserialize_credential(entry.data);
|
||||
debug_assert!(result.is_some());
|
||||
result
|
||||
}
|
||||
|
||||
pub fn store_credential(
|
||||
&mut self,
|
||||
credential: PublicKeyCredentialSource,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
let key = Key::Credential {
|
||||
rp_id: Some(credential.rp_id.clone()),
|
||||
credential_id: None,
|
||||
user_handle: Some(credential.user_handle.clone()),
|
||||
};
|
||||
let old_entry = self.store.find_one(&key);
|
||||
if old_entry.is_none() && self.count_credentials() >= MAX_SUPPORTED_RESIDENTIAL_KEYS {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL);
|
||||
}
|
||||
let credential = serialize_credential(credential)?;
|
||||
let new_entry = StoreEntry {
|
||||
tag: TAG_CREDENTIAL,
|
||||
data: &credential,
|
||||
};
|
||||
match old_entry {
|
||||
None => self.store.insert(new_entry)?,
|
||||
Some((index, old_entry)) => {
|
||||
debug_assert_eq!(old_entry.tag, TAG_CREDENTIAL);
|
||||
self.store.replace(index, new_entry)?
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn filter_credential(&self, rp_id: &str) -> Vec<PublicKeyCredentialSource> {
|
||||
self.store
|
||||
.find_all(&Key::Credential {
|
||||
rp_id: Some(rp_id.into()),
|
||||
credential_id: None,
|
||||
user_handle: None,
|
||||
})
|
||||
.filter_map(|(_, entry)| {
|
||||
debug_assert_eq!(entry.tag, TAG_CREDENTIAL);
|
||||
let credential = deserialize_credential(entry.data);
|
||||
debug_assert!(credential.is_some());
|
||||
credential
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn count_credentials(&self) -> usize {
|
||||
self.store
|
||||
.find_all(&Key::Credential {
|
||||
rp_id: None,
|
||||
credential_id: None,
|
||||
user_handle: None,
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn global_signature_counter(&self) -> u32 {
|
||||
self.store
|
||||
.find_one(&Key::GlobalSignatureCounter)
|
||||
.map_or(0, |(_, entry)| {
|
||||
u32::from_ne_bytes(*array_ref!(entry.data, 0, 4))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn incr_global_signature_counter(&mut self) {
|
||||
let mut buffer = [0; core::mem::size_of::<u32>()];
|
||||
match self.store.find_one(&Key::GlobalSignatureCounter) {
|
||||
None => {
|
||||
buffer.copy_from_slice(&1u32.to_ne_bytes());
|
||||
self.store
|
||||
.insert(StoreEntry {
|
||||
tag: GLOBAL_SIGNATURE_COUNTER,
|
||||
data: &buffer,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
Some((index, entry)) => {
|
||||
let value = u32::from_ne_bytes(*array_ref!(entry.data, 0, 4));
|
||||
// In hopes that servers handle the wrapping gracefully.
|
||||
buffer.copy_from_slice(&value.wrapping_add(1).to_ne_bytes());
|
||||
self.store
|
||||
.replace(
|
||||
index,
|
||||
StoreEntry {
|
||||
tag: GLOBAL_SIGNATURE_COUNTER,
|
||||
data: &buffer,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn master_keys(&self) -> MasterKeys {
|
||||
// We have as invariant that there is always exactly one MasterKeys entry in the store.
|
||||
let (_, entry) = self.store.find_one(&Key::MasterKeys).unwrap();
|
||||
let data = entry.data;
|
||||
// And this entry is well formed: the encryption key followed by the hmac key.
|
||||
let encryption = array_ref!(data, 0, 32);
|
||||
let hmac = array_ref!(data, 32, 32);
|
||||
MasterKeys { encryption, hmac }
|
||||
}
|
||||
|
||||
pub fn pin_hash(&self) -> Option<&[u8; PIN_AUTH_LENGTH]> {
|
||||
self.store
|
||||
.find_one(&Key::PinHash)
|
||||
.map(|(_, entry)| array_ref!(entry.data, 0, PIN_AUTH_LENGTH))
|
||||
}
|
||||
|
||||
pub fn set_pin_hash(&mut self, pin_hash: &[u8; PIN_AUTH_LENGTH]) {
|
||||
let entry = StoreEntry {
|
||||
tag: PIN_HASH,
|
||||
data: pin_hash,
|
||||
};
|
||||
match self.store.find_one(&Key::PinHash) {
|
||||
None => self.store.insert(entry).unwrap(),
|
||||
Some((index, _)) => {
|
||||
self.store.replace(index, entry).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pin_retries_entry(&self) -> (StoreIndex, u8) {
|
||||
let (index, entry) = self.store.find_one(&Key::PinRetries).unwrap();
|
||||
let data = entry.data;
|
||||
debug_assert_eq!(data.len(), 1);
|
||||
(index, data[0])
|
||||
}
|
||||
|
||||
pub fn pin_retries(&self) -> u8 {
|
||||
self.pin_retries_entry().1
|
||||
}
|
||||
|
||||
pub fn decr_pin_retries(&mut self) {
|
||||
let (index, old_value) = self.pin_retries_entry();
|
||||
let new_value = old_value.saturating_sub(1);
|
||||
self.store
|
||||
.replace(
|
||||
index,
|
||||
StoreEntry {
|
||||
tag: PIN_RETRIES,
|
||||
data: &[new_value],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn reset_pin_retries(&mut self) {
|
||||
let (index, _) = self.pin_retries_entry();
|
||||
self.store
|
||||
.replace(
|
||||
index,
|
||||
StoreEntry {
|
||||
tag: PIN_RETRIES,
|
||||
data: &[MAX_PIN_RETRIES],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, rng: &mut impl Rng256) {
|
||||
loop {
|
||||
let index = {
|
||||
let mut iter = self.store.iter();
|
||||
match iter.next() {
|
||||
None => break,
|
||||
Some((index, _)) => index,
|
||||
}
|
||||
};
|
||||
self.store.delete(index).unwrap();
|
||||
}
|
||||
self.init(rng);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoreError> for Ctap2StatusCode {
|
||||
fn from(error: StoreError) -> Ctap2StatusCode {
|
||||
match error {
|
||||
StoreError::StoreFull => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL,
|
||||
StoreError::InvalidTag => unreachable!(),
|
||||
StoreError::InvalidPrecondition => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_credential(data: &[u8]) -> Option<PublicKeyCredentialSource> {
|
||||
let cbor = cbor::read(data).ok()?;
|
||||
cbor.try_into().ok()
|
||||
}
|
||||
|
||||
fn serialize_credential(credential: PublicKeyCredentialSource) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
let mut data = Vec::new();
|
||||
if cbor::write(credential.into(), &mut data) {
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CREDENTIAL)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::crypto;
|
||||
use crate::crypto::rng256::{Rng256, ThreadRng256};
|
||||
use crate::ctap::data_formats::{PublicKeyCredentialSource, PublicKeyCredentialType};
|
||||
|
||||
fn create_credential_source(
|
||||
rng: &mut ThreadRng256,
|
||||
rp_id: &str,
|
||||
user_handle: Vec<u8>,
|
||||
) -> PublicKeyCredentialSource {
|
||||
let private_key = crypto::ecdsa::SecKey::gensk(rng);
|
||||
PublicKeyCredentialSource {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
credential_id: rng.gen_uniform_u8x32().to_vec(),
|
||||
private_key,
|
||||
rp_id: String::from(rp_id),
|
||||
user_handle,
|
||||
other_ui: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_overhead() {
|
||||
// nRF52840 NVMC
|
||||
const WORD_SIZE: usize = 4;
|
||||
const PAGE_SIZE: usize = 0x1000;
|
||||
const NUM_PAGES: usize = 100;
|
||||
let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice();
|
||||
let options = embedded_flash::BufferOptions {
|
||||
word_size: WORD_SIZE,
|
||||
page_size: PAGE_SIZE,
|
||||
max_word_writes: 2,
|
||||
max_page_erases: 10000,
|
||||
strict_write: true,
|
||||
};
|
||||
let storage = Storage::new(store, options);
|
||||
let store = embedded_flash::Store::new(storage, Config).unwrap();
|
||||
// We can replace 3 bytes with minimal overhead.
|
||||
assert_eq!(store.replace_len(0), 2 * WORD_SIZE);
|
||||
assert_eq!(store.replace_len(3), 2 * WORD_SIZE);
|
||||
assert_eq!(store.replace_len(4), 3 * WORD_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_store() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
assert_eq!(persistent_store.count_credentials(), 0);
|
||||
let credential_source = create_credential_source(&mut rng, "example.com", vec![]);
|
||||
assert!(persistent_store.store_credential(credential_source).is_ok());
|
||||
assert!(persistent_store.count_credentials() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::assertions_on_constants)]
|
||||
fn test_fill_store() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
assert_eq!(persistent_store.count_credentials(), 0);
|
||||
|
||||
// To make this test work for bigger storages, implement better int -> Vec conversion.
|
||||
assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256);
|
||||
for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS {
|
||||
let credential_source =
|
||||
create_credential_source(&mut rng, "example.com", vec![i as u8]);
|
||||
assert!(persistent_store.store_credential(credential_source).is_ok());
|
||||
assert_eq!(persistent_store.count_credentials(), i + 1);
|
||||
}
|
||||
let credential_source = create_credential_source(
|
||||
&mut rng,
|
||||
"example.com",
|
||||
vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8],
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_store.store_credential(credential_source),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL)
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_store.count_credentials(),
|
||||
MAX_SUPPORTED_RESIDENTIAL_KEYS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::assertions_on_constants)]
|
||||
fn test_overwrite() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
assert_eq!(persistent_store.count_credentials(), 0);
|
||||
// These should have different IDs.
|
||||
let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]);
|
||||
let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x00]);
|
||||
let expected_credential = credential_source1.clone();
|
||||
|
||||
assert!(persistent_store
|
||||
.store_credential(credential_source0)
|
||||
.is_ok());
|
||||
assert!(persistent_store
|
||||
.store_credential(credential_source1)
|
||||
.is_ok());
|
||||
assert_eq!(persistent_store.count_credentials(), 1);
|
||||
assert_eq!(
|
||||
&persistent_store.filter_credential("example.com"),
|
||||
&[expected_credential]
|
||||
);
|
||||
|
||||
// To make this test work for bigger storages, implement better int -> Vec conversion.
|
||||
assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256);
|
||||
for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS {
|
||||
let credential_source =
|
||||
create_credential_source(&mut rng, "example.com", vec![i as u8]);
|
||||
assert!(persistent_store.store_credential(credential_source).is_ok());
|
||||
assert_eq!(persistent_store.count_credentials(), i + 1);
|
||||
}
|
||||
let credential_source = create_credential_source(
|
||||
&mut rng,
|
||||
"example.com",
|
||||
vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8],
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_store.store_credential(credential_source),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL)
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_store.count_credentials(),
|
||||
MAX_SUPPORTED_RESIDENTIAL_KEYS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
assert_eq!(persistent_store.count_credentials(), 0);
|
||||
let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]);
|
||||
let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x01]);
|
||||
let credential_source2 =
|
||||
create_credential_source(&mut rng, "another.example.com", vec![0x02]);
|
||||
let id0 = credential_source0.credential_id.clone();
|
||||
let id1 = credential_source1.credential_id.clone();
|
||||
assert!(persistent_store
|
||||
.store_credential(credential_source0)
|
||||
.is_ok());
|
||||
assert!(persistent_store
|
||||
.store_credential(credential_source1)
|
||||
.is_ok());
|
||||
assert!(persistent_store
|
||||
.store_credential(credential_source2)
|
||||
.is_ok());
|
||||
|
||||
let filtered_credentials = persistent_store.filter_credential("example.com");
|
||||
assert_eq!(filtered_credentials.len(), 2);
|
||||
assert!(
|
||||
(filtered_credentials[0].credential_id == id0
|
||||
&& filtered_credentials[1].credential_id == id1)
|
||||
|| (filtered_credentials[1].credential_id == id0
|
||||
&& filtered_credentials[0].credential_id == id1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
assert_eq!(persistent_store.count_credentials(), 0);
|
||||
let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]);
|
||||
let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x01]);
|
||||
let id0 = credential_source0.credential_id.clone();
|
||||
let key0 = credential_source0.private_key.clone();
|
||||
assert!(persistent_store
|
||||
.store_credential(credential_source0)
|
||||
.is_ok());
|
||||
assert!(persistent_store
|
||||
.store_credential(credential_source1)
|
||||
.is_ok());
|
||||
|
||||
let no_credential = persistent_store.find_credential("another.example.com", &id0);
|
||||
assert_eq!(no_credential, None);
|
||||
let found_credential = persistent_store.find_credential("example.com", &id0);
|
||||
let expected_credential = PublicKeyCredentialSource {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
credential_id: id0,
|
||||
private_key: key0,
|
||||
rp_id: String::from("example.com"),
|
||||
user_handle: vec![0x00],
|
||||
other_ui: None,
|
||||
};
|
||||
assert_eq!(found_credential, Some(expected_credential));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_master_keys() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
|
||||
// Master keys stay the same between resets.
|
||||
let master_keys_1 = persistent_store.master_keys();
|
||||
let master_keys_2 = persistent_store.master_keys();
|
||||
assert_eq!(master_keys_2.encryption, master_keys_1.encryption);
|
||||
assert_eq!(master_keys_2.hmac, master_keys_1.hmac);
|
||||
|
||||
// Master keys change after reset. This test may fail if the random generator produces the
|
||||
// same keys.
|
||||
let master_encryption_key = master_keys_1.encryption.to_vec();
|
||||
let master_hmac_key = master_keys_1.hmac.to_vec();
|
||||
persistent_store.reset(&mut rng);
|
||||
let master_keys_3 = persistent_store.master_keys();
|
||||
assert!(master_keys_3.encryption as &[u8] != &master_encryption_key[..]);
|
||||
assert!(master_keys_3.hmac as &[u8] != &master_hmac_key[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pin_hash() {
|
||||
use crate::ctap::PIN_AUTH_LENGTH;
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
|
||||
// Pin hash is initially not set.
|
||||
assert!(persistent_store.pin_hash().is_none());
|
||||
|
||||
// Setting the pin hash sets the pin hash.
|
||||
let random_data = rng.gen_uniform_u8x32();
|
||||
assert_eq!(random_data.len(), 2 * PIN_AUTH_LENGTH);
|
||||
let pin_hash_1 = array_ref!(random_data, 0, PIN_AUTH_LENGTH);
|
||||
let pin_hash_2 = array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH);
|
||||
persistent_store.set_pin_hash(&pin_hash_1);
|
||||
assert_eq!(persistent_store.pin_hash(), Some(pin_hash_1));
|
||||
assert_eq!(persistent_store.pin_hash(), Some(pin_hash_1));
|
||||
persistent_store.set_pin_hash(&pin_hash_2);
|
||||
assert_eq!(persistent_store.pin_hash(), Some(pin_hash_2));
|
||||
assert_eq!(persistent_store.pin_hash(), Some(pin_hash_2));
|
||||
|
||||
// Resetting the storage resets the pin hash.
|
||||
persistent_store.reset(&mut rng);
|
||||
assert!(persistent_store.pin_hash().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pin_retries() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
|
||||
// The pin retries is initially at the maximum.
|
||||
assert_eq!(persistent_store.pin_retries(), MAX_PIN_RETRIES);
|
||||
|
||||
// Decrementing the pin retries decrements the pin retries.
|
||||
for pin_retries in (0..MAX_PIN_RETRIES).rev() {
|
||||
persistent_store.decr_pin_retries();
|
||||
assert_eq!(persistent_store.pin_retries(), pin_retries);
|
||||
}
|
||||
|
||||
// Decrementing the pin retries after zero does not modify the pin retries.
|
||||
persistent_store.decr_pin_retries();
|
||||
assert_eq!(persistent_store.pin_retries(), 0);
|
||||
|
||||
// Resetting the pin retries resets the pin retries.
|
||||
persistent_store.reset_pin_retries();
|
||||
assert_eq!(persistent_store.pin_retries(), MAX_PIN_RETRIES);
|
||||
}
|
||||
}
|
||||
188
src/ctap/timed_permission.rs
Normal file
188
src/ctap/timed_permission.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::timer::{ClockValue, Duration};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum TimedPermission {
|
||||
Waiting,
|
||||
Granted(ClockValue),
|
||||
}
|
||||
|
||||
impl TimedPermission {
|
||||
pub fn waiting() -> TimedPermission {
|
||||
TimedPermission::Waiting
|
||||
}
|
||||
|
||||
pub fn granted(now: ClockValue, grant_duration: Duration<isize>) -> TimedPermission {
|
||||
TimedPermission::Granted(now.wrapping_add(grant_duration))
|
||||
}
|
||||
|
||||
// Checks if the timeout is not reached, false for differing ClockValue frequencies.
|
||||
pub fn is_granted(&self, now: ClockValue) -> bool {
|
||||
if let TimedPermission::Granted(timeout) = self {
|
||||
if let Some(remaining_duration) = timeout.wrapping_sub(now) {
|
||||
return remaining_duration > Duration::from_ms(0);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Consumes the state and returns the current new permission state at time "now".
|
||||
// Returns a new state for differing ClockValue frequencies.
|
||||
pub fn check_expiration(self, now: ClockValue) -> TimedPermission {
|
||||
if let TimedPermission::Granted(timeout) = self {
|
||||
if let Some(remaining_duration) = timeout.wrapping_sub(now) {
|
||||
if remaining_duration > Duration::from_ms(0) {
|
||||
return TimedPermission::Granted(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
TimedPermission::Waiting
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
#[derive(Debug)]
|
||||
pub struct U2fUserPresenceState {
|
||||
// If user presence was recently requested, its timeout is saved here.
|
||||
needs_up: TimedPermission,
|
||||
// Button touch timeouts, while user presence is requested, are saved here.
|
||||
has_up: TimedPermission,
|
||||
// This is the timeout duration of user presence requests.
|
||||
request_duration: Duration<isize>,
|
||||
// This is the timeout duration of button touches.
|
||||
presence_duration: Duration<isize>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
impl U2fUserPresenceState {
|
||||
pub fn new(
|
||||
request_duration: Duration<isize>,
|
||||
presence_duration: Duration<isize>,
|
||||
) -> U2fUserPresenceState {
|
||||
U2fUserPresenceState {
|
||||
needs_up: TimedPermission::Waiting,
|
||||
has_up: TimedPermission::Waiting,
|
||||
request_duration,
|
||||
presence_duration,
|
||||
}
|
||||
}
|
||||
|
||||
// Granting user presence is ignored if it needs activation, but waits. Also cleans up.
|
||||
pub fn grant_up(&mut self, now: ClockValue) {
|
||||
self.check_expiration(now);
|
||||
if self.needs_up.is_granted(now) {
|
||||
self.needs_up = TimedPermission::Waiting;
|
||||
self.has_up = TimedPermission::granted(now, self.presence_duration);
|
||||
}
|
||||
}
|
||||
|
||||
// This marks user presence as needed or uses it up if already granted. Also cleans up.
|
||||
pub fn consume_up(&mut self, now: ClockValue) -> bool {
|
||||
self.check_expiration(now);
|
||||
if self.has_up.is_granted(now) {
|
||||
self.has_up = TimedPermission::Waiting;
|
||||
true
|
||||
} else {
|
||||
self.needs_up = TimedPermission::granted(now, self.request_duration);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Returns if user presence was requested. Also cleans up.
|
||||
pub fn is_up_needed(&mut self, now: ClockValue) -> bool {
|
||||
self.check_expiration(now);
|
||||
self.needs_up.is_granted(now)
|
||||
}
|
||||
|
||||
// If you don't regularly call any other function, not cleaning up leads to overflow problems.
|
||||
pub fn check_expiration(&mut self, now: ClockValue) {
|
||||
self.needs_up = self.needs_up.check_expiration(now);
|
||||
self.has_up = self.has_up.check_expiration(now);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use core::isize;
|
||||
|
||||
const CLOCK_FREQUENCY_HZ: usize = 32768;
|
||||
const ZERO: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ);
|
||||
const BIG_POSITIVE: ClockValue = ClockValue::new(isize::MAX / 1000 - 1, CLOCK_FREQUENCY_HZ);
|
||||
const NEGATIVE: ClockValue = ClockValue::new(-1, CLOCK_FREQUENCY_HZ);
|
||||
const SMALL_NEGATIVE: ClockValue = ClockValue::new(isize::MIN / 1000 + 1, CLOCK_FREQUENCY_HZ);
|
||||
const REQUEST_DURATION: Duration<isize> = Duration::from_ms(1000);
|
||||
const PRESENCE_DURATION: Duration<isize> = Duration::from_ms(1000);
|
||||
|
||||
fn grant_up_when_needed(start_time: ClockValue) {
|
||||
let mut u2f_state = U2fUserPresenceState::new(REQUEST_DURATION, PRESENCE_DURATION);
|
||||
assert!(!u2f_state.consume_up(start_time));
|
||||
assert!(u2f_state.is_up_needed(start_time));
|
||||
u2f_state.grant_up(start_time);
|
||||
assert!(u2f_state.consume_up(start_time));
|
||||
assert!(!u2f_state.consume_up(start_time));
|
||||
}
|
||||
|
||||
fn need_up_timeout(start_time: ClockValue) {
|
||||
let mut u2f_state = U2fUserPresenceState::new(REQUEST_DURATION, PRESENCE_DURATION);
|
||||
assert!(!u2f_state.consume_up(start_time));
|
||||
assert!(u2f_state.is_up_needed(start_time));
|
||||
// The timeout excludes equality, so it should be over at this instant.
|
||||
assert!(!u2f_state.is_up_needed(start_time.wrapping_add(REQUEST_DURATION)));
|
||||
}
|
||||
|
||||
fn grant_up_timeout(start_time: ClockValue) {
|
||||
let mut u2f_state = U2fUserPresenceState::new(REQUEST_DURATION, PRESENCE_DURATION);
|
||||
assert!(!u2f_state.consume_up(start_time));
|
||||
assert!(u2f_state.is_up_needed(start_time));
|
||||
u2f_state.grant_up(start_time);
|
||||
// The timeout excludes equality, so it should be over at this instant.
|
||||
assert!(!u2f_state.consume_up(start_time.wrapping_add(PRESENCE_DURATION)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grant_up_timeout() {
|
||||
grant_up_timeout(ZERO);
|
||||
grant_up_timeout(BIG_POSITIVE);
|
||||
grant_up_timeout(NEGATIVE);
|
||||
grant_up_timeout(SMALL_NEGATIVE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_need_up_timeout() {
|
||||
need_up_timeout(ZERO);
|
||||
need_up_timeout(BIG_POSITIVE);
|
||||
need_up_timeout(NEGATIVE);
|
||||
need_up_timeout(SMALL_NEGATIVE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grant_up_when_needed() {
|
||||
grant_up_when_needed(ZERO);
|
||||
grant_up_when_needed(BIG_POSITIVE);
|
||||
grant_up_when_needed(NEGATIVE);
|
||||
grant_up_when_needed(SMALL_NEGATIVE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grant_up_without_need() {
|
||||
let mut u2f_state = U2fUserPresenceState::new(REQUEST_DURATION, PRESENCE_DURATION);
|
||||
u2f_state.grant_up(ZERO);
|
||||
assert!(!u2f_state.is_up_needed(ZERO));
|
||||
assert!(!u2f_state.consume_up(ZERO));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user