CTAP library move (#602)

* Moves all CTAP logic into its own library

* workflows fix test

* more coveralls workflow tests
This commit is contained in:
kaczmarczyck
2023-03-07 15:56:46 +01:00
committed by GitHub
parent 03031e6970
commit ca65902a8f
80 changed files with 412 additions and 2000 deletions

View File

@@ -0,0 +1,449 @@
// Copyright 2020-2023 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 alloc::vec::Vec;
use byteorder::{BigEndian, ByteOrder};
use core::convert::TryFrom;
use crate::api::attestation_store;
const APDU_HEADER_LEN: usize = 4;
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(non_camel_case_types, dead_code)]
pub enum ApduStatusCode {
SW_SUCCESS = 0x90_00,
/// Command successfully executed; 'XX' bytes of data are
/// available and can be requested using GET RESPONSE.
SW_GET_RESPONSE = 0x61_00,
SW_MEMERR = 0x65_01,
SW_WRONG_DATA = 0x6a_80,
SW_WRONG_LENGTH = 0x67_00,
SW_COND_USE_NOT_SATISFIED = 0x69_85,
SW_COMMAND_NOT_ALLOWED = 0x69_86,
SW_FILE_NOT_FOUND = 0x6a_82,
SW_INCORRECT_P1P2 = 0x6a_86,
/// Instruction code not supported or invalid
SW_INS_INVALID = 0x6d_00,
SW_CLA_INVALID = 0x6e_00,
SW_INTERNAL_EXCEPTION = 0x6f_00,
}
impl From<ApduStatusCode> for u16 {
fn from(code: ApduStatusCode) -> Self {
code as u16
}
}
impl From<attestation_store::Error> for ApduStatusCode {
fn from(error: attestation_store::Error) -> Self {
use attestation_store::Error;
match error {
Error::Storage => ApduStatusCode::SW_MEMERR,
Error::Internal => ApduStatusCode::SW_INTERNAL_EXCEPTION,
Error::NoSupport => ApduStatusCode::SW_INTERNAL_EXCEPTION,
}
}
}
#[allow(dead_code)]
pub enum ApduInstructions {
Select = 0xA4,
ReadBinary = 0xB0,
GetResponse = 0xC0,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[allow(dead_code)]
pub struct ApduHeader {
pub cla: u8,
pub ins: u8,
pub p1: u8,
pub p2: u8,
}
impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader {
fn from(header: &[u8; APDU_HEADER_LEN]) -> Self {
ApduHeader {
cla: header[0],
ins: header[1],
p1: header[2],
p2: header[3],
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// The APDU cases
pub enum Case {
Le1,
Lc1Data,
Lc1DataLe1,
Lc3Data,
Lc3DataLe1,
Lc3DataLe2,
Le3,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum ApduType {
Instruction,
Short(Case),
Extended(Case),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub struct Apdu {
pub header: ApduHeader,
pub lc: u16,
pub data: Vec<u8>,
pub le: u32,
pub case_type: ApduType,
}
impl TryFrom<&[u8]> for Apdu {
type Error = ApduStatusCode;
fn try_from(frame: &[u8]) -> Result<Self, ApduStatusCode> {
if frame.len() < APDU_HEADER_LEN as usize {
return Err(ApduStatusCode::SW_WRONG_DATA);
}
// +-----+-----+----+----+
// header | CLA | INS | P1 | P2 |
// +-----+-----+----+----+
let (header, payload) = frame.split_at(APDU_HEADER_LEN);
if payload.is_empty() {
// Lc is zero-bytes in length
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: 0x00,
data: Vec::new(),
le: 0x00,
case_type: ApduType::Instruction,
});
}
// Lc is not zero-bytes in length, let's figure out how long it is
let byte_0 = payload[0];
if payload.len() == 1 {
// There is only one byte in the payload, that byte cannot be Lc because that would
// entail at *least* one another byte in the payload (for the command data)
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: 0x00,
data: Vec::new(),
le: if byte_0 == 0x00 {
// Ne = 256
0x100
} else {
byte_0.into()
},
case_type: ApduType::Short(Case::Le1),
});
}
if payload.len() == 1 + (byte_0 as usize) && byte_0 != 0 {
// Lc is one-byte long and since the size specified by Lc covers the rest of the
// payload there's no Le at the end
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: byte_0.into(),
data: payload[1..].to_vec(),
case_type: ApduType::Short(Case::Lc1Data),
le: 0,
});
}
if payload.len() == 2 + (byte_0 as usize) && byte_0 != 0 {
// Lc is one-byte long and since the size specified by Lc covers the rest of the
// payload with ONE additional byte that byte must be Le
let last_byte: u32 = (*payload.last().unwrap()).into();
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: byte_0.into(),
data: payload[1..(payload.len() - 1)].to_vec(),
le: if last_byte == 0x00 { 0x100 } else { last_byte },
case_type: ApduType::Short(Case::Lc1DataLe1),
});
}
if payload.len() > 2 {
// Lc is possibly three-bytes long
let extended_apdu_lc = BigEndian::read_u16(&payload[1..3]) as usize;
if payload.len() < extended_apdu_lc + 3 {
return Err(ApduStatusCode::SW_WRONG_LENGTH);
}
let extended_apdu_le_len: usize = payload
.len()
.checked_sub(extended_apdu_lc + 3)
.ok_or(ApduStatusCode::SW_WRONG_LENGTH)?;
if extended_apdu_le_len > 3 {
return Err(ApduStatusCode::SW_WRONG_LENGTH);
}
if byte_0 == 0 && extended_apdu_le_len <= 3 {
// If first byte is zero AND the next two bytes can be parsed as a big-endian
// length that covers the rest of the block (plus few additional bytes for Le), we
// have an extended-length APDU
let last_byte: u32 = (*payload.last().unwrap()).into();
return Ok(Apdu {
header: array_ref!(header, 0, APDU_HEADER_LEN).into(),
lc: extended_apdu_lc as u16,
data: payload[3..(payload.len() - extended_apdu_le_len)].to_vec(),
le: match extended_apdu_le_len {
0 => 0,
1 => {
if last_byte == 0x00 {
0x100
} else {
last_byte
}
}
2 => {
let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]);
if le_parsed == 0x00 {
0x10000
} else {
le_parsed as u32
}
}
3 => {
let le_first_byte: u32 =
(*payload.get(payload.len() - 3).unwrap()).into();
if le_first_byte != 0x00 {
return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION);
}
let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]);
if le_parsed == 0x00 {
0x10000
} else {
le_parsed as u32
}
}
_ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION),
},
case_type: ApduType::Extended(match extended_apdu_le_len {
0 => Case::Lc3Data,
1 => Case::Lc3DataLe1,
2 => Case::Lc3DataLe2,
3 => Case::Le3,
_ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION),
}),
});
}
}
Err(ApduStatusCode::SW_INTERNAL_EXCEPTION)
}
}
#[cfg(test)]
mod test {
use super::*;
fn pass_frame(frame: &[u8]) -> Result<Apdu, ApduStatusCode> {
Apdu::try_from(frame)
}
#[test]
fn test_case_type_1() {
let frame: [u8; 4] = [0x00, 0x12, 0x00, 0x80];
let response = pass_frame(&frame);
assert!(response.is_ok());
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0x12,
p1: 0x00,
p2: 0x80,
},
lc: 0x00,
data: Vec::new(),
le: 0x00,
case_type: ApduType::Instruction,
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_2_short() {
let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x0f];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xb0,
p1: 0x00,
p2: 0x00,
},
lc: 0x00,
data: Vec::new(),
le: 0x0f,
case_type: ApduType::Short(Case::Le1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_2_short_le() {
let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x00];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xb0,
p1: 0x00,
p2: 0x00,
},
lc: 0x00,
data: Vec::new(),
le: 0x100,
case_type: ApduType::Short(Case::Le1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_3_short() {
let frame: [u8; 7] = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04];
let payload = [0xe1, 0x04];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x00,
p2: 0x0c,
},
lc: 0x02,
data: payload.to_vec(),
le: 0x00,
case_type: ApduType::Short(Case::Lc1Data),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_4_short() {
let frame: [u8; 13] = [
0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0xff,
];
let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x04,
p2: 0x00,
},
lc: 0x07,
data: payload.to_vec(),
le: 0xff,
case_type: ApduType::Short(Case::Lc1DataLe1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_case_type_4_short_le() {
let frame: [u8; 13] = [
0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00,
];
let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0xa4,
p1: 0x04,
p2: 0x00,
},
lc: 0x07,
data: payload.to_vec(),
le: 0x100,
case_type: ApduType::Short(Case::Lc1DataLe1),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_invalid_apdu_header_length() {
let frame: [u8; 3] = [0x00, 0x12, 0x00];
let response = pass_frame(&frame);
assert_eq!(Err(ApduStatusCode::SW_WRONG_DATA), response);
}
#[test]
fn test_extended_length_apdu() {
let frame: [u8; 186] = [
0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0xb1, 0x60, 0xc5, 0xb3, 0x42, 0x58, 0x6b, 0x49,
0xdb, 0x3e, 0x72, 0xd8, 0x24, 0x4b, 0xa5, 0x6c, 0x8d, 0x79, 0x2b, 0x65, 0x08, 0xe8,
0xda, 0x9b, 0x0e, 0x2b, 0xc1, 0x63, 0x0d, 0xbc, 0xf3, 0x6d, 0x66, 0xa5, 0x46, 0x72,
0xb2, 0x22, 0xc4, 0xcf, 0x95, 0xe1, 0x51, 0xed, 0x8d, 0x4d, 0x3c, 0x76, 0x7a, 0x6c,
0xc3, 0x49, 0x43, 0x59, 0x43, 0x79, 0x4e, 0x88, 0x4f, 0x3d, 0x02, 0x3a, 0x82, 0x29,
0xfd, 0x70, 0x3f, 0x8b, 0xd4, 0xff, 0xe0, 0xa8, 0x93, 0xdf, 0x1a, 0x58, 0x34, 0x16,
0xb0, 0x1b, 0x8e, 0xbc, 0xf0, 0x2d, 0xc9, 0x99, 0x8d, 0x6f, 0xe4, 0x8a, 0xb2, 0x70,
0x9a, 0x70, 0x3a, 0x27, 0x71, 0x88, 0x3c, 0x75, 0x30, 0x16, 0xfb, 0x02, 0x11, 0x4d,
0x30, 0x54, 0x6c, 0x4e, 0x8c, 0x76, 0xb2, 0xf0, 0xa8, 0x4e, 0xd6, 0x90, 0xe4, 0x40,
0x25, 0x6a, 0xdd, 0x64, 0x63, 0x3e, 0x83, 0x4f, 0x8b, 0x25, 0xcf, 0x88, 0x68, 0x80,
0x01, 0x07, 0xdb, 0xc8, 0x64, 0xf7, 0xca, 0x4f, 0xd1, 0xc7, 0x95, 0x7c, 0xe8, 0x45,
0xbc, 0xda, 0xd4, 0xef, 0x45, 0x63, 0x5a, 0x7a, 0x65, 0x3f, 0xaa, 0x22, 0x67, 0xe7,
0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb,
0xb6, 0x2b, 0x00, 0x00,
];
let payload: &[u8] = &frame[7..frame.len() - 2];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0x02,
p1: 0x03,
p2: 0x00,
},
lc: 0xb1,
data: payload.to_vec(),
le: 0x10000,
case_type: ApduType::Extended(Case::Lc3DataLe2),
};
assert_eq!(Ok(expected), response);
}
#[test]
fn test_previously_unsupported_case_type() {
let frame: [u8; 73] = [
0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x40, 0xe3, 0x8f, 0xde, 0x51, 0x3d, 0xac, 0x9d,
0x1c, 0x6e, 0x86, 0x76, 0x31, 0x40, 0x25, 0x96, 0x86, 0x4d, 0x29, 0xe8, 0x07, 0xb3,
0x56, 0x19, 0xdf, 0x4a, 0x00, 0x02, 0xae, 0x2a, 0x8c, 0x9d, 0x5a, 0xab, 0xc3, 0x4b,
0x4e, 0xb9, 0x78, 0xb9, 0x11, 0xe5, 0x52, 0x40, 0xf3, 0x45, 0x64, 0x9c, 0xd3, 0xd7,
0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d,
0xcb, 0x00, 0x00,
];
let payload: &[u8] = &frame[7..frame.len() - 2];
let response = pass_frame(&frame);
let expected = Apdu {
header: ApduHeader {
cla: 0x00,
ins: 0x01,
p1: 0x03,
p2: 0x00,
},
lc: 0x40,
data: payload.to_vec(),
le: 0x10000,
case_type: ApduType::Extended(Case::Lc3DataLe2),
};
assert_eq!(Ok(expected), response);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,489 @@
// Copyright 2020-2023 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::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorConfigParameters;
use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams};
use super::response::ResponseData;
use super::status_code::Ctap2StatusCode;
use crate::api::customization::Customization;
use crate::ctap::storage;
use crate::env::Env;
use alloc::vec;
/// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig.
fn process_enable_enterprise_attestation(
env: &mut impl Env,
) -> Result<ResponseData, Ctap2StatusCode> {
if env.customization().enterprise_attestation_mode().is_some() {
storage::enable_enterprise_attestation(env)?;
Ok(ResponseData::AuthenticatorConfig)
} else {
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
}
/// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig.
fn process_toggle_always_uv(env: &mut impl Env) -> Result<ResponseData, Ctap2StatusCode> {
storage::toggle_always_uv(env)?;
Ok(ResponseData::AuthenticatorConfig)
}
/// Processes the subcommand setMinPINLength for AuthenticatorConfig.
fn process_set_min_pin_length(
env: &mut impl Env,
params: SetMinPinLengthParams,
) -> Result<ResponseData, Ctap2StatusCode> {
let SetMinPinLengthParams {
new_min_pin_length,
min_pin_length_rp_ids,
force_change_pin,
} = params;
let store_min_pin_length = storage::min_pin_length(env)?;
let new_min_pin_length = new_min_pin_length.unwrap_or(store_min_pin_length);
if new_min_pin_length < store_min_pin_length {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
}
let mut force_change_pin = force_change_pin.unwrap_or(false);
if force_change_pin && storage::pin_hash(env)?.is_none() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET);
}
if let Some(old_length) = storage::pin_code_point_length(env)? {
force_change_pin |= new_min_pin_length > old_length;
}
if force_change_pin {
storage::force_pin_change(env)?;
}
storage::set_min_pin_length(env, new_min_pin_length)?;
if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids {
storage::set_min_pin_length_rp_ids(env, min_pin_length_rp_ids)?;
}
Ok(ResponseData::AuthenticatorConfig)
}
/// Processes the AuthenticatorConfig command.
pub fn process_config<E: Env>(
env: &mut E,
client_pin: &mut ClientPin<E>,
params: AuthenticatorConfigParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorConfigParameters {
sub_command,
sub_command_params,
pin_uv_auth_protocol,
pin_uv_auth_param,
} = params;
let enforce_uv =
!matches!(sub_command, ConfigSubCommand::ToggleAlwaysUv) && storage::has_always_uv(env)?;
if storage::pin_hash(env)?.is_some() || enforce_uv {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
// Constants are taken from the specification, section 6.11, step 4.2.
let mut config_data = vec![0xFF; 32];
config_data.extend(&[0x0D, sub_command as u8]);
if let Some(sub_command_params) = sub_command_params.clone() {
super::cbor_write(sub_command_params.into(), &mut config_data)?;
}
client_pin.verify_pin_uv_auth_token(
&config_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
client_pin.has_permission(PinPermission::AuthenticatorConfiguration)?;
}
match sub_command {
ConfigSubCommand::EnableEnterpriseAttestation => process_enable_enterprise_attestation(env),
ConfigSubCommand::ToggleAlwaysUv => process_toggle_always_uv(env),
ConfigSubCommand::SetMinPinLength => {
if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params {
process_set_min_pin_length(env, params)
} else {
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
}
}
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::api::customization::Customization;
use crate::ctap::data_formats::PinUvAuthProtocol;
use crate::ctap::pin_protocol::authenticate_pin_uv_auth_token;
use crate::env::test::TestEnv;
#[test]
fn test_process_enable_enterprise_attestation() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::EnableEnterpriseAttestation,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
if env.customization().enterprise_attestation_mode().is_some() {
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::enterprise_attestation(&mut env), Ok(true));
} else {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
}
#[test]
fn test_process_toggle_always_uv() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(storage::has_always_uv(&mut env).unwrap());
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
if env.customization().enforce_always_uv() {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
);
} else {
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(!storage::has_always_uv(&mut env).unwrap());
}
}
fn test_helper_process_toggle_always_uv_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
pin_uv_auth_protocol,
);
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
let mut config_data = vec![0xFF; 32];
config_data.extend(&[0x0D, ConfigSubCommand::ToggleAlwaysUv as u8]);
let pin_uv_auth_param =
authenticate_pin_uv_auth_token(&pin_uv_auth_token, &config_data, pin_uv_auth_protocol);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: Some(pin_uv_auth_param.clone()),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
if env.customization().enforce_always_uv() {
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED)
);
return;
}
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(storage::has_always_uv(&mut env).unwrap());
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::ToggleAlwaysUv,
sub_command_params: None,
pin_uv_auth_param: Some(pin_uv_auth_param),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert!(!storage::has_always_uv(&mut env).unwrap());
}
#[test]
fn test_process_toggle_always_uv_with_pin_v1() {
test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_toggle_always_uv_with_pin_v2() {
test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V2);
}
fn create_min_pin_config_params(
min_pin_length: u8,
min_pin_length_rp_ids: Option<Vec<String>>,
) -> AuthenticatorConfigParameters {
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(min_pin_length),
min_pin_length_rp_ids,
force_change_pin: None,
};
AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::SetMinPinLength,
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
set_min_pin_length_params,
)),
pin_uv_auth_param: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
}
}
#[test]
fn test_process_set_min_pin_length() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
// First, increase minimum PIN length from 4 to 6 without PIN auth.
let min_pin_length = 6;
let config_params = create_min_pin_config_params(min_pin_length, None);
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
// Second, increase minimum PIN length from 6 to 8 with PIN auth.
// The stored PIN or its length don't matter since we control the token.
storage::set_pin(&mut env, &[0x88; 16], 8).unwrap();
let min_pin_length = 8;
let mut config_params = create_min_pin_config_params(min_pin_length, None);
let pin_uv_auth_param = vec![
0x5C, 0x69, 0x71, 0x29, 0xBD, 0xCC, 0x53, 0xE8, 0x3C, 0x97, 0x62, 0xDD, 0x90, 0x29,
0xB2, 0xDE,
];
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
// Third, decreasing the minimum PIN length from 8 to 7 fails.
let mut config_params = create_min_pin_config_params(7, None);
let pin_uv_auth_param = vec![
0xC5, 0xEA, 0xC1, 0x5E, 0x7F, 0x80, 0x70, 0x1A, 0x4E, 0xC4, 0xAD, 0x85, 0x35, 0xD8,
0xA7, 0x71,
];
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)
);
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
}
#[test]
fn test_process_set_min_pin_length_rp_ids() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
// First, set RP IDs without PIN auth.
let min_pin_length = 6;
let min_pin_length_rp_ids = vec!["example.com".to_string()];
let config_params =
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(
storage::min_pin_length_rp_ids(&mut env),
Ok(min_pin_length_rp_ids)
);
// Second, change the RP IDs with PIN auth.
let min_pin_length = 8;
let min_pin_length_rp_ids = vec!["another.example.com".to_string()];
// The stored PIN or its length don't matter since we control the token.
storage::set_pin(&mut env, &[0x88; 16], 8).unwrap();
let mut config_params =
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
let pin_uv_auth_param = vec![
0x40, 0x51, 0x2D, 0xAC, 0x2D, 0xE2, 0x15, 0x77, 0x5C, 0xF9, 0x5B, 0x62, 0x9A, 0x2D,
0xD6, 0xDA,
];
config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone());
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(
storage::min_pin_length_rp_ids(&mut env),
Ok(min_pin_length_rp_ids.clone())
);
// Third, changing RP IDs with bad PIN auth fails.
// One PIN auth shouldn't work for different lengths.
let mut config_params =
create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone()));
config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone());
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(
storage::min_pin_length_rp_ids(&mut env),
Ok(min_pin_length_rp_ids.clone())
);
// Forth, changing RP IDs with bad PIN auth fails.
// One PIN auth shouldn't work for different RP IDs.
let mut config_params = create_min_pin_config_params(
min_pin_length,
Some(vec!["counter.example.com".to_string()]),
);
config_params.pin_uv_auth_param = Some(pin_uv_auth_param);
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(
storage::min_pin_length_rp_ids(&mut env),
Ok(min_pin_length_rp_ids)
);
}
#[test]
fn test_process_set_min_pin_length_force_pin_change_implicit() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
// Increase min PIN, force PIN change.
let min_pin_length = 6;
let mut config_params = create_min_pin_config_params(min_pin_length, None);
let pin_uv_auth_param = Some(vec![
0x81, 0x37, 0x37, 0xF3, 0xD8, 0x69, 0xBD, 0x74, 0xFE, 0x88, 0x30, 0x8C, 0xC4, 0x2E,
0xA8, 0xC8,
]);
config_params.pin_uv_auth_param = pin_uv_auth_param;
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length));
assert_eq!(storage::has_force_pin_change(&mut env), Ok(true));
}
#[test]
fn test_process_set_min_pin_length_force_pin_change_explicit() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
storage::set_pin(&mut env, &[0x88; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xE3, 0x74, 0xF4, 0x27, 0xBE, 0x7D, 0x40, 0xB5, 0x71, 0xB6, 0xB4, 0x1A, 0xD2, 0xC1,
0x53, 0xD7,
]);
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(storage::min_pin_length(&mut env).unwrap()),
min_pin_length_rp_ids: None,
force_change_pin: Some(true),
};
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::SetMinPinLength,
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
set_min_pin_length_params,
)),
pin_uv_auth_param,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(storage::has_force_pin_change(&mut env), Ok(true));
}
#[test]
fn test_process_config_vendor_prototype() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::VendorPrototype,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response = process_config(&mut env, &mut client_pin, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
}

View File

@@ -0,0 +1,494 @@
// Copyright 2022-2023 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::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt, PrivateKey};
use super::data_formats::{
CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialType,
};
use super::status_code::Ctap2StatusCode;
use super::{cbor_read, cbor_write};
use crate::api::key_store::KeyStore;
use crate::ctap::data_formats::{extract_byte_string, extract_map};
use crate::env::Env;
use alloc::string::String;
use alloc::vec::Vec;
use core::convert::{TryFrom, TryInto};
use crypto::hmac::{hmac_256, verify_hmac_256};
use crypto::sha256::Sha256;
use sk_cbor::{cbor_map_options, destructure_cbor_map};
pub const LEGACY_CREDENTIAL_ID_SIZE: usize = 112;
// CBOR credential IDs consist of
// - 1 byte : version number
// - 16 bytes: initialization vector for AES-256,
// - 192 bytes: encrypted block of the key handle cbor,
// - 32 bytes: HMAC-SHA256 over everything else.
pub const CBOR_CREDENTIAL_ID_SIZE: usize = 241;
pub const MIN_CREDENTIAL_ID_SIZE: usize = LEGACY_CREDENTIAL_ID_SIZE;
pub const MAX_CREDENTIAL_ID_SIZE: usize = CBOR_CREDENTIAL_ID_SIZE;
pub const CBOR_CREDENTIAL_ID_VERSION: u8 = 0x01;
pub const MAX_PADDING_LENGTH: u8 = 0xBF;
// Data fields that are contained in the credential ID of non-discoverable credentials.
struct CredentialSource {
private_key: PrivateKey,
rp_id_hash: [u8; 32],
cred_protect_policy: Option<CredentialProtectionPolicy>,
cred_blob: Option<Vec<u8>>,
}
// The data fields contained in the credential ID are serialized using CBOR maps.
// Each field is associated with a unique tag, implemented with a CBOR unsigned key.
enum CredentialSourceField {
PrivateKey = 0,
RpIdHash = 1,
CredProtectPolicy = 2,
CredBlob = 3,
}
impl From<CredentialSourceField> for sk_cbor::Value {
fn from(field: CredentialSourceField) -> sk_cbor::Value {
(field as u64).into()
}
}
fn decrypt_legacy_credential_id(
env: &mut impl Env,
bytes: &[u8],
) -> Result<Option<CredentialSource>, Ctap2StatusCode> {
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
let plaintext = aes256_cbc_decrypt(&aes_enc_key, bytes, true)?;
if plaintext.len() != 64 {
return Ok(None);
}
let private_key = if let Some(key) = PrivateKey::new_ecdsa_from_bytes(&plaintext[..32]) {
key
} else {
return Ok(None);
};
Ok(Some(CredentialSource {
private_key,
rp_id_hash: plaintext[32..64].try_into().unwrap(),
cred_protect_policy: None,
cred_blob: None,
}))
}
fn decrypt_cbor_credential_id(
env: &mut impl Env,
bytes: &[u8],
) -> Result<Option<CredentialSource>, Ctap2StatusCode> {
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
let mut plaintext = aes256_cbc_decrypt(&aes_enc_key, bytes, true)?;
remove_padding(&mut plaintext)?;
let cbor_credential_source = cbor_read(plaintext.as_slice())?;
destructure_cbor_map! {
let {
CredentialSourceField::PrivateKey => private_key,
CredentialSourceField::RpIdHash=> rp_id_hash,
CredentialSourceField::CredProtectPolicy => cred_protect_policy,
CredentialSourceField::CredBlob => cred_blob,
} = extract_map(cbor_credential_source)?;
}
Ok(match (private_key, rp_id_hash) {
(Some(private_key), Some(rp_id_hash)) => {
let private_key = PrivateKey::try_from(private_key)?;
let rp_id_hash = extract_byte_string(rp_id_hash)?;
if rp_id_hash.len() != 32 {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
let cred_protect_policy = cred_protect_policy
.map(CredentialProtectionPolicy::try_from)
.transpose()?;
let cred_blob = cred_blob.map(extract_byte_string).transpose()?;
Some(CredentialSource {
private_key,
rp_id_hash: rp_id_hash.try_into().unwrap(),
cred_protect_policy,
cred_blob,
})
}
_ => None,
})
}
/// Pad data to MAX_PADDING_LENGTH+1 (192) bytes using PKCS padding scheme.
/// Let N = 192 - data.len(), the PKCS padding scheme would pad N bytes of N after the data.
fn add_padding(data: &mut Vec<u8>) -> Result<(), Ctap2StatusCode> {
// The data should be between 1 to MAX_PADDING_LENGTH bytes for the padding scheme to be valid.
if data.is_empty() || data.len() > MAX_PADDING_LENGTH as usize {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
let pad_length = MAX_PADDING_LENGTH - (data.len() as u8 - 1);
data.extend(core::iter::repeat(pad_length).take(pad_length as usize));
Ok(())
}
fn remove_padding(data: &mut Vec<u8>) -> Result<(), Ctap2StatusCode> {
if data.len() != MAX_PADDING_LENGTH as usize + 1 {
// This is an internal error instead of corrupted credential ID which we should just ignore because
// we've already checked that the HMAC matched.
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
let pad_length = *data.last().unwrap();
if pad_length == 0 || pad_length > MAX_PADDING_LENGTH {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
if !data
.drain((data.len() - pad_length as usize)..)
.all(|x| x == pad_length)
{
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
Ok(())
}
/// Encrypts the given private key, relying party ID hash, and some other metadata into a credential ID.
///
/// Other information, such as a user name, are not stored. Since encrypted credential IDs are
/// stored server-side, this information is already available (unencrypted).
pub fn encrypt_to_credential_id(
env: &mut impl Env,
private_key: &PrivateKey,
rp_id_hash: &[u8; 32],
cred_protect_policy: Option<CredentialProtectionPolicy>,
cred_blob: Option<Vec<u8>>,
) -> Result<Vec<u8>, Ctap2StatusCode> {
let mut payload = Vec::new();
let cbor = cbor_map_options! {
CredentialSourceField::PrivateKey => private_key,
CredentialSourceField::RpIdHash=> rp_id_hash,
CredentialSourceField::CredProtectPolicy => cred_protect_policy,
CredentialSourceField::CredBlob => cred_blob,
};
cbor_write(cbor, &mut payload)?;
add_padding(&mut payload)?;
let aes_enc_key = crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
let encrypted_payload = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &payload, true)?;
let mut credential_id = encrypted_payload;
credential_id.insert(0, CBOR_CREDENTIAL_ID_VERSION);
let id_hmac = hmac_256::<Sha256>(
&env.key_store().key_handle_authentication()?,
&credential_id[..],
);
credential_id.extend(&id_hmac);
Ok(credential_id)
}
/// Decrypts the given credential ID into a PublicKeyCredentialSource, populating only the recorded fields.
///
/// Returns None if
/// - the format does not match any known versions, or
/// - the HMAC test fails.
///
/// For v0 (legacy U2F) the credential ID consists of:
/// - 16 bytes: initialization vector for AES-256,
/// - 32 bytes: encrypted ECDSA private key for the credential,
/// - 32 bytes: encrypted relying party ID hashed with SHA256,
/// - 32 bytes: HMAC-SHA256 over everything else.
///
/// For v1 (CBOR) the credential ID consists of:
/// - 1 byte : version number,
/// - 16 bytes: initialization vector for AES-256,
/// - 192 bytes: encrypted CBOR-encoded credential source fields,
/// - 32 bytes: HMAC-SHA256 over everything else.
pub fn decrypt_credential_id(
env: &mut impl Env,
credential_id: Vec<u8>,
rp_id_hash: &[u8],
) -> Result<Option<PublicKeyCredentialSource>, Ctap2StatusCode> {
if credential_id.len() < MIN_CREDENTIAL_ID_SIZE {
return Ok(None);
}
let hmac_message_size = credential_id.len() - 32;
if !verify_hmac_256::<Sha256>(
&env.key_store().key_handle_authentication()?,
&credential_id[..hmac_message_size],
array_ref![credential_id, hmac_message_size, 32],
) {
return Ok(None);
}
let credential_source = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE {
decrypt_legacy_credential_id(env, &credential_id[..hmac_message_size])?
} else {
match credential_id[0] {
CBOR_CREDENTIAL_ID_VERSION => {
if credential_id.len() != CBOR_CREDENTIAL_ID_SIZE {
return Ok(None);
}
decrypt_cbor_credential_id(env, &credential_id[1..hmac_message_size])?
}
_ => return Ok(None),
}
};
let credential_source = if let Some(credential_source) = credential_source {
credential_source
} else {
return Ok(None);
};
if rp_id_hash != credential_source.rp_id_hash {
return Ok(None);
}
Ok(Some(PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id,
private_key: credential_source.private_key,
rp_id: String::new(),
user_handle: Vec::new(),
user_display_name: None,
cred_protect_policy: credential_source.cred_protect_policy,
creation_order: 0,
user_name: None,
user_icon: None,
cred_blob: credential_source.cred_blob,
large_blob_key: None,
}))
}
#[cfg(test)]
mod test {
use super::*;
use crate::api::customization::Customization;
use crate::ctap::credential_id::CBOR_CREDENTIAL_ID_SIZE;
use crate::ctap::SignatureAlgorithm;
use crate::env::test::TestEnv;
use crypto::hmac::hmac_256;
const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80;
fn test_encrypt_decrypt_credential(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
let rp_id_hash = [0x55; 32];
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(private_key, decrypted_source.private_key);
}
#[test]
fn test_encrypt_decrypt_ecdsa_credential() {
test_encrypt_decrypt_credential(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_encrypt_decrypt_ed25519_credential() {
test_encrypt_decrypt_credential(SignatureAlgorithm::Eddsa);
}
#[test]
fn test_encrypt_decrypt_bad_version() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let mut encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION;
// Override the HMAC to pass the check.
encrypted_id.truncate(&encrypted_id.len() - 32);
let hmac_key = env.key_store().key_handle_authentication().unwrap();
let id_hmac = hmac_256::<Sha256>(&hmac_key, &encrypted_id[..]);
encrypted_id.extend(&id_hmac);
assert_eq!(
decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash),
Ok(None)
);
}
fn test_encrypt_decrypt_bad_hmac(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
let rp_id_hash = [0x55; 32];
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
for i in 0..encrypted_id.len() {
let mut modified_id = encrypted_id.clone();
modified_id[i] ^= 0x01;
assert_eq!(
decrypt_credential_id(&mut env, modified_id, &rp_id_hash),
Ok(None)
);
}
}
#[test]
fn test_ecdsa_encrypt_decrypt_bad_hmac() {
test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_encrypt_decrypt_bad_hmac() {
test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::Eddsa);
}
fn test_decrypt_credential_missing_blocks(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
let rp_id_hash = [0x55; 32];
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
for length in (1..CBOR_CREDENTIAL_ID_SIZE).step_by(16) {
assert_eq!(
decrypt_credential_id(&mut env, encrypted_id[..length].to_vec(), &rp_id_hash),
Ok(None)
);
}
}
#[test]
fn test_ecdsa_decrypt_credential_missing_blocks() {
test_decrypt_credential_missing_blocks(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_decrypt_credential_missing_blocks() {
test_decrypt_credential_missing_blocks(SignatureAlgorithm::Eddsa);
}
/// This is a copy of the function that genereated deprecated key handles.
fn legacy_encrypt_to_credential_id(
env: &mut impl Env,
private_key: crypto::ecdsa::SecKey,
application: &[u8; 32],
) -> Result<Vec<u8>, Ctap2StatusCode> {
let aes_enc_key =
crypto::aes256::EncryptionKey::new(&env.key_store().key_handle_encryption()?);
let mut plaintext = [0; 64];
private_key.to_bytes(array_mut_ref!(plaintext, 0, 32));
plaintext[32..64].copy_from_slice(application);
let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?;
let id_hmac = hmac_256::<Sha256>(
&env.key_store().key_handle_authentication()?,
&encrypted_id[..],
);
encrypted_id.extend(&id_hmac);
Ok(encrypted_id)
}
#[test]
fn test_encrypt_decrypt_credential_legacy() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new_ecdsa(&mut env);
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
let rp_id_hash = [0x55; 32];
let encrypted_id =
legacy_encrypt_to_credential_id(&mut env, ecdsa_key, &rp_id_hash).unwrap();
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(private_key, decrypted_source.private_key);
// Legacy credentials didn't persist credProtectPolicy info, so it should be treated as None.
assert!(decrypted_source.cred_protect_policy.is_none());
}
#[test]
fn test_encrypt_credential_size() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap();
assert_eq!(encrypted_id.len(), CBOR_CREDENTIAL_ID_SIZE);
}
#[test]
fn test_encrypt_credential_max_cbor_size() {
// The cbor encoding length is variadic and depends on size of fields. Try to put maximum length
// for each encoded field and ensure that it doesn't go over the padding size.
let mut env = TestEnv::default();
// Currently all private key types have same length when transformed to bytes.
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let cred_protect_policy = Some(CredentialProtectionPolicy::UserVerificationOptional);
let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]);
let encrypted_id = encrypt_to_credential_id(
&mut env,
&private_key,
&rp_id_hash,
cred_protect_policy,
cred_blob,
);
assert!(encrypted_id.is_ok());
}
#[test]
fn test_cred_protect_persisted() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let encrypted_id = encrypt_to_credential_id(
&mut env,
&private_key,
&rp_id_hash,
Some(CredentialProtectionPolicy::UserVerificationRequired),
None,
)
.unwrap();
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(decrypted_source.private_key, private_key);
assert_eq!(
decrypted_source.cred_protect_policy,
Some(CredentialProtectionPolicy::UserVerificationRequired)
);
}
#[test]
fn test_cred_blob_persisted() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]);
let encrypted_id =
encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, cred_blob.clone())
.unwrap();
let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(decrypted_source.private_key, private_key);
assert_eq!(decrypted_source.cred_blob, cred_blob);
}
}

View File

@@ -0,0 +1,917 @@
// Copyright 2020-2023 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::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorCredentialManagementParameters;
use super::data_formats::{
CredentialManagementSubCommand, CredentialManagementSubCommandParameters,
PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource,
PublicKeyCredentialUserEntity,
};
use super::response::{AuthenticatorCredentialManagementResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use super::{Channel, StatefulCommand, StatefulPermission};
use crate::ctap::storage;
use crate::env::Env;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crypto::sha256::Sha256;
use crypto::Hash256;
/// Generates a set with all existing RP IDs.
fn get_stored_rp_ids(env: &mut impl Env) -> Result<BTreeSet<String>, Ctap2StatusCode> {
let mut rp_set = BTreeSet::new();
let mut iter_result = Ok(());
for (_, credential) in storage::iter_credentials(env, &mut iter_result)? {
rp_set.insert(credential.rp_id);
}
iter_result?;
Ok(rp_set)
}
/// Generates the response for subcommands enumerating RPs.
fn enumerate_rps_response(
rp_id: String,
total_rps: Option<u64>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_hash = Some(Sha256::hash(rp_id.as_bytes()).to_vec());
let rp = Some(PublicKeyCredentialRpEntity {
rp_id,
rp_name: None,
rp_icon: None,
});
Ok(AuthenticatorCredentialManagementResponse {
rp,
rp_id_hash,
total_rps,
..Default::default()
})
}
/// Generates the response for subcommands enumerating credentials.
fn enumerate_credentials_response(
env: &mut impl Env,
credential: PublicKeyCredentialSource,
total_credentials: Option<u64>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let PublicKeyCredentialSource {
key_type,
credential_id,
private_key,
rp_id: _,
user_handle,
user_display_name,
cred_protect_policy,
creation_order: _,
user_name,
user_icon,
cred_blob: _,
large_blob_key,
} = credential;
let user = PublicKeyCredentialUserEntity {
user_id: user_handle,
user_name,
user_display_name,
user_icon,
};
let credential_id = PublicKeyCredentialDescriptor {
key_type,
key_id: credential_id,
transports: None, // You can set USB as a hint here.
};
let public_key = private_key.get_pub_key(env)?;
Ok(AuthenticatorCredentialManagementResponse {
user: Some(user),
credential_id: Some(credential_id),
public_key: Some(public_key),
total_credentials,
cred_protect: cred_protect_policy,
large_blob_key,
..Default::default()
})
}
/// Check if the token permissions have the correct associated RP ID.
///
/// Either no RP ID is associated, or the RP ID matches the stored credential.
fn check_rp_id_permissions<E: Env>(
env: &mut E,
client_pin: &mut ClientPin<E>,
credential_id: &[u8],
) -> Result<(), Ctap2StatusCode> {
// Pre-check a sufficient condition before calling the store.
if client_pin.has_no_rp_id_permission().is_ok() {
return Ok(());
}
let (_, credential) = storage::find_credential_item(env, credential_id)?;
client_pin.has_no_or_rp_id_permission(&credential.rp_id)
}
/// Processes the subcommand getCredsMetadata for CredentialManagement.
fn process_get_creds_metadata(
env: &mut impl Env,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
Ok(AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: Some(storage::count_credentials(env)? as u64),
max_possible_remaining_resident_credentials_count: Some(
storage::remaining_credentials(env)? as u64,
),
..Default::default()
})
}
/// Processes the subcommand enumerateRPsBegin for CredentialManagement.
fn process_enumerate_rps_begin<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
channel: Channel,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_set = get_stored_rp_ids(env)?;
let total_rps = rp_set.len();
if total_rps > 1 {
stateful_command_permission.set_command(env, StatefulCommand::EnumerateRps(1), channel);
}
// TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first()
let rp_id = rp_set
.into_iter()
.next()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
enumerate_rps_response(rp_id, Some(total_rps as u64))
}
/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement.
fn process_enumerate_rps_get_next_rp<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_index = stateful_command_permission.next_enumerate_rp(env)?;
let rp_set = get_stored_rp_ids(env)?;
// A BTreeSet is already sorted.
let rp_id = rp_set
.into_iter()
.nth(rp_id_index)
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
enumerate_rps_response(rp_id, None)
}
/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement.
fn process_enumerate_credentials_begin<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
client_pin: &mut ClientPin<E>,
sub_command_params: CredentialManagementSubCommandParameters,
channel: Channel,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_hash = sub_command_params
.rp_id_hash
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?;
let mut iter_result = Ok(());
let iter = storage::iter_credentials(env, &mut iter_result)?;
let mut rp_credentials: Vec<usize> = iter
.filter_map(|(key, credential)| {
let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes());
if cred_rp_id_hash == rp_id_hash.as_slice() {
Some(key)
} else {
None
}
})
.collect();
iter_result?;
let total_credentials = rp_credentials.len();
let current_key = rp_credentials
.pop()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
let credential = storage::get_credential(env, current_key)?;
if total_credentials > 1 {
stateful_command_permission.set_command(
env,
StatefulCommand::EnumerateCredentials(rp_credentials),
channel,
);
}
enumerate_credentials_response(env, credential, Some(total_credentials as u64))
}
/// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement.
fn process_enumerate_credentials_get_next_credential<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let credential_key = stateful_command_permission.next_enumerate_credential(env)?;
let credential = storage::get_credential(env, credential_key)?;
enumerate_credentials_response(env, credential, None)
}
/// Processes the subcommand deleteCredential for CredentialManagement.
fn process_delete_credential<E: Env>(
env: &mut E,
client_pin: &mut ClientPin<E>,
sub_command_params: CredentialManagementSubCommandParameters,
) -> Result<(), Ctap2StatusCode> {
let credential_id = sub_command_params
.credential_id
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
.key_id;
check_rp_id_permissions(env, client_pin, &credential_id)?;
storage::delete_credential(env, &credential_id)
}
/// Processes the subcommand updateUserInformation for CredentialManagement.
fn process_update_user_information<E: Env>(
env: &mut E,
client_pin: &mut ClientPin<E>,
sub_command_params: CredentialManagementSubCommandParameters,
) -> Result<(), Ctap2StatusCode> {
let credential_id = sub_command_params
.credential_id
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
.key_id;
let user = sub_command_params
.user
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
check_rp_id_permissions(env, client_pin, &credential_id)?;
storage::update_credential(env, &credential_id, user)
}
/// Processes the CredentialManagement command and all its subcommands.
pub fn process_credential_management<E: Env>(
env: &mut E,
stateful_command_permission: &mut StatefulPermission<E>,
client_pin: &mut ClientPin<E>,
cred_management_params: AuthenticatorCredentialManagementParameters,
channel: Channel,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorCredentialManagementParameters {
sub_command,
sub_command_params,
pin_uv_auth_protocol,
pin_uv_auth_param,
} = cred_management_params;
match (sub_command, stateful_command_permission.get_command(env)) {
(
CredentialManagementSubCommand::EnumerateRpsGetNextRp,
Ok(StatefulCommand::EnumerateRps(_)),
)
| (
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
Ok(StatefulCommand::EnumerateCredentials(_)),
) => (),
(_, _) => {
stateful_command_permission.clear();
}
}
match sub_command {
CredentialManagementSubCommand::GetCredsMetadata
| CredentialManagementSubCommand::EnumerateRpsBegin
| CredentialManagementSubCommand::EnumerateCredentialsBegin
| CredentialManagementSubCommand::DeleteCredential
| CredentialManagementSubCommand::UpdateUserInformation => {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
let mut management_data = vec![sub_command as u8];
if let Some(sub_command_params) = sub_command_params.clone() {
super::cbor_write(sub_command_params.into(), &mut management_data)?;
}
client_pin.verify_pin_uv_auth_token(
&management_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
// The RP ID permission is handled differently per subcommand below.
client_pin.has_permission(PinPermission::CredentialManagement)?;
}
CredentialManagementSubCommand::EnumerateRpsGetNextRp
| CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {}
}
let response = match sub_command {
CredentialManagementSubCommand::GetCredsMetadata => {
client_pin.has_no_rp_id_permission()?;
Some(process_get_creds_metadata(env)?)
}
CredentialManagementSubCommand::EnumerateRpsBegin => {
client_pin.has_no_rp_id_permission()?;
Some(process_enumerate_rps_begin(
env,
stateful_command_permission,
channel,
)?)
}
CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some(
process_enumerate_rps_get_next_rp(env, stateful_command_permission)?,
),
CredentialManagementSubCommand::EnumerateCredentialsBegin => {
Some(process_enumerate_credentials_begin(
env,
stateful_command_permission,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
channel,
)?)
}
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => Some(
process_enumerate_credentials_get_next_credential(env, stateful_command_permission)?,
),
CredentialManagementSubCommand::DeleteCredential => {
process_delete_credential(
env,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?;
None
}
CredentialManagementSubCommand::UpdateUserInformation => {
process_update_user_information(
env,
client_pin,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?;
None
}
};
Ok(ResponseData::AuthenticatorCredentialManagement(response))
}
#[cfg(test)]
mod test {
use super::super::crypto_wrapper::PrivateKey;
use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType};
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
use super::super::CtapState;
use super::*;
use crate::env::test::TestEnv;
use rng256::Rng256;
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
fn create_credential_source(env: &mut TestEnv) -> PublicKeyCredentialSource {
let private_key = PrivateKey::new_ecdsa(env);
PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: env.rng().gen_uniform_u8x32().to_vec(),
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x01],
user_display_name: Some("display_name".to_string()),
cred_protect_policy: None,
creation_order: 0,
user_name: Some("name".to_string()),
user_icon: Some("icon".to_string()),
cred_blob: None,
large_blob_key: None,
}
}
fn test_helper_process_get_creds_metadata(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
pin_uv_auth_protocol,
);
let credential_source = create_credential_source(&mut env);
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let management_data = vec![CredentialManagementSubCommand::GetCredsMetadata as u8];
let pin_uv_auth_param = authenticate_pin_uv_auth_token(
&pin_uv_auth_token,
&management_data,
pin_uv_auth_protocol,
);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
pin_uv_auth_param: Some(pin_uv_auth_param.clone()),
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let initial_capacity = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.existing_resident_credentials_count, Some(0));
response
.max_possible_remaining_resident_credentials_count
.unwrap()
}
_ => panic!("Invalid response type"),
};
storage::store_credential(&mut env, credential_source).unwrap();
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
pin_uv_auth_param: Some(pin_uv_auth_param),
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.existing_resident_credentials_count, Some(1));
assert_eq!(
response.max_possible_remaining_resident_credentials_count,
Some(initial_capacity - 1)
);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_get_creds_metadata_v1() {
test_helper_process_get_creds_metadata(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_get_creds_metadata_v2() {
test_helper_process_get_creds_metadata(PinUvAuthProtocol::V2);
}
#[test]
fn test_process_enumerate_rps_with_uv() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let credential_source1 = create_credential_source(&mut env);
let mut credential_source2 = create_credential_source(&mut env);
credential_source2.rp_id = "another.example.com".to_string();
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::store_credential(&mut env, credential_source1).unwrap();
storage::store_credential(&mut env, credential_source2).unwrap();
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
0xD0, 0xD1,
]);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let first_rp_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.total_rps, Some(2));
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
rp_id
}
_ => panic!("Invalid response type"),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let second_rp_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.total_rps, None);
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
rp_id
}
_ => panic!("Invalid response type"),
};
assert!(first_rp_id != second_rp_id);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_enumerate_rps_completeness() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let credential_source = create_credential_source(&mut env);
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
const NUM_CREDENTIALS: usize = 20;
for i in 0..NUM_CREDENTIALS {
let mut credential = credential_source.clone();
credential.rp_id = i.to_string();
storage::store_credential(&mut env, credential).unwrap();
}
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
0xD0, 0xD1,
]);
let mut rp_set = BTreeSet::new();
// This mut is just to make the test code shorter.
// The command is different on the first loop iteration.
let mut cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
for _ in 0..NUM_CREDENTIALS {
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
if rp_set.is_empty() {
assert_eq!(response.total_rps, Some(NUM_CREDENTIALS as u64));
} else {
assert_eq!(response.total_rps, None);
}
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
assert!(!rp_set.contains(&rp_id));
rp_set.insert(rp_id);
}
_ => panic!("Invalid response type"),
};
cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
}
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_enumerate_credentials_with_uv() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let credential_source1 = create_credential_source(&mut env);
let mut credential_source2 = create_credential_source(&mut env);
credential_source2.user_handle = vec![0x02];
credential_source2.user_name = Some("user2".to_string());
credential_source2.user_display_name = Some("User Two".to_string());
credential_source2.user_icon = Some("icon2".to_string());
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::store_credential(&mut env, credential_source1).unwrap();
storage::store_credential(&mut env, credential_source2).unwrap();
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28,
0x2B, 0x68,
]);
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()),
credential_id: None,
user: None,
};
// RP ID hash:
// A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let first_credential_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert!(response.user.is_some());
assert!(response.public_key.is_some());
assert_eq!(response.total_credentials, Some(2));
response.credential_id.unwrap().key_id
}
_ => panic!("Invalid response type"),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
let second_credential_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert!(response.user.is_some());
assert!(response.public_key.is_some());
assert_eq!(response.total_credentials, None);
response.credential_id.unwrap().key_id
}
_ => panic!("Invalid response type"),
};
assert!(first_credential_id != second_credential_id);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
sub_command_params: None,
pin_uv_auth_protocol: None,
pin_uv_auth_param: None,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_delete_credential() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut credential_source = create_credential_source(&mut env);
credential_source.credential_id = vec![0x1D; 32];
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::store_credential(&mut env, credential_source).unwrap();
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89,
0x9C, 0x64,
]);
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None, // You can set USB as a hint here.
};
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: None,
credential_id: Some(credential_id),
user: None,
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::DeleteCredential,
sub_command_params: Some(sub_command_params.clone()),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param: pin_uv_auth_param.clone(),
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Ok(ResponseData::AuthenticatorCredentialManagement(None))
);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::DeleteCredential,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)
);
}
#[test]
fn test_process_update_user_information() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut credential_source = create_credential_source(&mut env);
credential_source.credential_id = vec![0x1D; 32];
let mut ctap_state = CtapState::new(&mut env);
ctap_state.client_pin = client_pin;
storage::store_credential(&mut env, credential_source).unwrap();
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD,
0x9D, 0xB7,
]);
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None, // You can set USB as a hint here.
};
let new_user = PublicKeyCredentialUserEntity {
user_id: vec![0xFF],
user_name: Some("new_name".to_string()),
user_display_name: Some("new_display_name".to_string()),
user_icon: Some("new_icon".to_string()),
};
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: None,
credential_id: Some(credential_id),
user: Some(new_user),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::UpdateUserInformation,
sub_command_params: Some(sub_command_params),
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param,
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Ok(ResponseData::AuthenticatorCredentialManagement(None))
);
let updated_credential = storage::find_credential(&mut env, "example.com", &[0x1D; 32])
.unwrap()
.unwrap();
assert_eq!(updated_credential.user_handle, vec![0x01]);
assert_eq!(&updated_credential.user_name.unwrap(), "new_name");
assert_eq!(
&updated_credential.user_display_name.unwrap(),
"new_display_name"
);
assert_eq!(&updated_credential.user_icon.unwrap(), "new_icon");
}
#[test]
fn test_process_credential_management_invalid_pin_uv_auth_param() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::new(&mut env);
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1),
pin_uv_auth_param: Some(vec![0u8; 16]),
};
let cred_management_response = process_credential_management(
&mut env,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.client_pin,
cred_management_params,
DUMMY_CHANNEL,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}

View File

@@ -0,0 +1,428 @@
// Copyright 2021-2023 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::api::key_store::KeyStore;
use crate::ctap::data_formats::{extract_array, extract_byte_string, CoseKey, SignatureAlgorithm};
use crate::ctap::status_code::Ctap2StatusCode;
use crate::env::Env;
use alloc::vec;
use alloc::vec::Vec;
use core::convert::TryFrom;
use crypto::cbc::{cbc_decrypt, cbc_encrypt};
use crypto::ecdsa;
use crypto::sha256::Sha256;
use rng256::Rng256;
use sk_cbor as cbor;
use sk_cbor::{cbor_array, cbor_bytes, cbor_int};
/// Wraps the AES256-CBC encryption to match what we need in CTAP.
pub fn aes256_cbc_encrypt(
rng: &mut dyn Rng256,
aes_enc_key: &crypto::aes256::EncryptionKey,
plaintext: &[u8],
embeds_iv: bool,
) -> Result<Vec<u8>, Ctap2StatusCode> {
if plaintext.len() % 16 != 0 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
// The extra 1 capacity is because encrypt_key_handle adds a version number.
let mut ciphertext = Vec::with_capacity(plaintext.len() + 16 * embeds_iv as usize + 1);
let iv = if embeds_iv {
let random_bytes = rng.gen_uniform_u8x32();
ciphertext.extend_from_slice(&random_bytes[..16]);
*array_ref!(ciphertext, 0, 16)
} else {
[0u8; 16]
};
let start = ciphertext.len();
ciphertext.extend_from_slice(plaintext);
cbc_encrypt(aes_enc_key, iv, &mut ciphertext[start..]);
Ok(ciphertext)
}
/// Wraps the AES256-CBC decryption to match what we need in CTAP.
pub fn aes256_cbc_decrypt(
aes_enc_key: &crypto::aes256::EncryptionKey,
ciphertext: &[u8],
embeds_iv: bool,
) -> Result<Vec<u8>, Ctap2StatusCode> {
if ciphertext.len() % 16 != 0 || (embeds_iv && ciphertext.is_empty()) {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let (iv, ciphertext) = if embeds_iv {
let (iv, ciphertext) = ciphertext.split_at(16);
(*array_ref!(iv, 0, 16), ciphertext)
} else {
([0u8; 16], ciphertext)
};
let mut plaintext = ciphertext.to_vec();
let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key);
cbc_decrypt(&aes_dec_key, iv, &mut plaintext);
Ok(plaintext)
}
/// An asymmetric private key that can sign messages.
#[derive(Clone, Debug)]
// We shouldn't compare private keys in prod without constant-time operations.
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum PrivateKey {
// We store the seed instead of the key since we can't get the seed back from the key. We could
// store both if we believe deriving the key is done more than once and costly.
Ecdsa([u8; 32]),
#[cfg(feature = "ed25519")]
Ed25519(ed25519_compact::SecretKey),
}
impl PrivateKey {
/// Creates a new private key for the given algorithm.
///
/// # Panics
///
/// Panics if the algorithm is [`SignatureAlgorithm::Unknown`].
pub fn new(env: &mut impl Env, alg: SignatureAlgorithm) -> Self {
match alg {
SignatureAlgorithm::Es256 => {
PrivateKey::Ecdsa(env.key_store().generate_ecdsa_seed().unwrap())
}
#[cfg(feature = "ed25519")]
SignatureAlgorithm::Eddsa => {
let bytes = env.rng().gen_uniform_u8x32();
Self::new_ed25519_from_bytes(&bytes).unwrap()
}
SignatureAlgorithm::Unknown => unreachable!(),
}
}
/// Creates a new ecdsa private key.
pub fn new_ecdsa(env: &mut impl Env) -> PrivateKey {
Self::new(env, SignatureAlgorithm::Es256)
}
/// Helper function that creates a private key of type ECDSA.
///
/// This function is public for legacy credential source parsing only.
pub fn new_ecdsa_from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != 32 {
return None;
}
Some(PrivateKey::Ecdsa(*array_ref!(bytes, 0, 32)))
}
#[cfg(feature = "ed25519")]
pub fn new_ed25519_from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != 32 {
return None;
}
let seed = ed25519_compact::Seed::from_slice(bytes).unwrap();
Some(Self::Ed25519(ed25519_compact::KeyPair::from_seed(seed).sk))
}
/// Returns the ECDSA private key.
pub fn ecdsa_key(&self, env: &mut impl Env) -> Result<ecdsa::SecKey, Ctap2StatusCode> {
match self {
PrivateKey::Ecdsa(seed) => ecdsa_key_from_seed(env, seed),
#[allow(unreachable_patterns)]
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}
/// Returns the corresponding public key.
pub fn get_pub_key(&self, env: &mut impl Env) -> Result<CoseKey, Ctap2StatusCode> {
Ok(match self {
PrivateKey::Ecdsa(ecdsa_seed) => {
CoseKey::from(ecdsa_key_from_seed(env, ecdsa_seed)?.genpk())
}
#[cfg(feature = "ed25519")]
PrivateKey::Ed25519(ed25519_key) => CoseKey::from(ed25519_key.public_key()),
})
}
/// Returns the encoded signature for a given message.
pub fn sign_and_encode(
&self,
env: &mut impl Env,
message: &[u8],
) -> Result<Vec<u8>, Ctap2StatusCode> {
Ok(match self {
PrivateKey::Ecdsa(ecdsa_seed) => ecdsa_key_from_seed(env, ecdsa_seed)?
.sign_rfc6979::<Sha256>(message)
.to_asn1_der(),
#[cfg(feature = "ed25519")]
PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message, None).to_vec(),
})
}
/// The associated COSE signature algorithm identifier.
pub fn signature_algorithm(&self) -> SignatureAlgorithm {
match self {
PrivateKey::Ecdsa(_) => SignatureAlgorithm::Es256,
#[cfg(feature = "ed25519")]
PrivateKey::Ed25519(_) => SignatureAlgorithm::Eddsa,
}
}
/// Writes the key bytes.
pub fn to_bytes(&self) -> Vec<u8> {
match self {
PrivateKey::Ecdsa(ecdsa_seed) => ecdsa_seed.to_vec(),
#[cfg(feature = "ed25519")]
PrivateKey::Ed25519(ed25519_key) => ed25519_key.seed().to_vec(),
}
}
}
fn ecdsa_key_from_seed(
env: &mut impl Env,
seed: &[u8; 32],
) -> Result<ecdsa::SecKey, Ctap2StatusCode> {
let ecdsa_bytes = env.key_store().derive_ecdsa(seed)?;
Ok(ecdsa::SecKey::from_bytes(&ecdsa_bytes).unwrap())
}
impl From<&PrivateKey> for cbor::Value {
fn from(private_key: &PrivateKey) -> Self {
cbor_array![
cbor_int!(private_key.signature_algorithm() as i64),
cbor_bytes!(private_key.to_bytes()),
]
}
}
impl TryFrom<cbor::Value> for PrivateKey {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let mut array = extract_array(cbor_value)?;
if array.len() != 2 {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR);
}
let key_bytes = extract_byte_string(array.pop().unwrap())?;
match SignatureAlgorithm::try_from(array.pop().unwrap())? {
SignatureAlgorithm::Es256 => PrivateKey::new_ecdsa_from_bytes(&key_bytes)
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
#[cfg(feature = "ed25519")]
SignatureAlgorithm::Eddsa => PrivateKey::new_ed25519_from_bytes(&key_bytes)
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
#[test]
fn test_encrypt_decrypt_with_iv() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypt_decrypt_without_iv() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap();
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, false).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_correct_iv_usage() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let mut ciphertext_no_iv =
aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap();
let mut ciphertext_with_iv = vec![0u8; 16];
ciphertext_with_iv.append(&mut ciphertext_no_iv);
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext_with_iv, true).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_iv_manipulation_property() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let mut ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
let mut expected_plaintext = plaintext;
for i in 0..16 {
ciphertext[i] ^= 0xBB;
expected_plaintext[i] ^= 0xBB;
}
let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap();
assert_eq!(decrypted, expected_plaintext);
}
#[test]
fn test_chaining() {
let mut env = TestEnv::default();
let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext1 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
let ciphertext2 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap();
assert_eq!(ciphertext1.len(), 80);
assert_eq!(ciphertext2.len(), 80);
// The ciphertext should mutate in all blocks with a different IV.
let block_iter1 = ciphertext1.chunks_exact(16);
let block_iter2 = ciphertext2.chunks_exact(16);
for (block1, block2) in block_iter1.zip(block_iter2) {
assert_ne!(block1, block2);
}
}
#[test]
fn test_new_ecdsa_from_bytes() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let key_bytes = private_key.to_bytes();
assert_eq!(
PrivateKey::new_ecdsa_from_bytes(&key_bytes),
Some(private_key)
);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_new_ed25519_from_bytes() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Eddsa);
let key_bytes = private_key.to_bytes();
assert_eq!(
PrivateKey::new_ed25519_from_bytes(&key_bytes),
Some(private_key)
);
}
#[test]
fn test_new_ecdsa_from_bytes_wrong_length() {
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 16]), None);
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 31]), None);
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 33]), None);
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 64]), None);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_new_ed25519_from_bytes_wrong_length() {
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 16]), None);
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 31]), None);
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 33]), None);
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 64]), None);
}
#[test]
fn test_private_key_get_pub_key() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new_ecdsa(&mut env);
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
let public_key = ecdsa_key.genpk();
assert_eq!(
private_key.get_pub_key(&mut env),
Ok(CoseKey::from(public_key))
);
}
#[test]
fn test_private_key_sign_and_encode() {
let mut env = TestEnv::default();
let message = [0x5A; 32];
let private_key = PrivateKey::new_ecdsa(&mut env);
let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap();
let signature = ecdsa_key.sign_rfc6979::<Sha256>(&message).to_asn1_der();
assert_eq!(
private_key.sign_and_encode(&mut env, &message),
Ok(signature)
);
}
fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
assert_eq!(private_key.signature_algorithm(), signature_algorithm);
}
#[test]
fn test_ecdsa_private_key_signature_algorithm() {
test_private_key_signature_algorithm(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_private_key_signature_algorithm() {
test_private_key_signature_algorithm(SignatureAlgorithm::Eddsa);
}
fn test_private_key_from_to_cbor(signature_algorithm: SignatureAlgorithm) {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, signature_algorithm);
let cbor = cbor::Value::from(&private_key);
assert_eq!(PrivateKey::try_from(cbor), Ok(private_key),);
}
#[test]
fn test_ecdsa_private_key_from_to_cbor() {
test_private_key_from_to_cbor(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_private_key_from_to_cbor() {
test_private_key_from_to_cbor(SignatureAlgorithm::Eddsa);
}
fn test_private_key_from_bad_cbor(signature_algorithm: SignatureAlgorithm) {
let cbor = cbor_array![
cbor_int!(signature_algorithm as i64),
cbor_bytes!(vec![0x88; 32]),
// The array is too long.
cbor_int!(0),
];
assert_eq!(
PrivateKey::try_from(cbor),
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
);
}
#[test]
fn test_ecdsa_private_key_from_bad_cbor() {
test_private_key_from_bad_cbor(SignatureAlgorithm::Es256);
}
#[test]
#[cfg(feature = "ed25519")]
fn test_ed25519_private_key_from_bad_cbor() {
test_private_key_from_bad_cbor(SignatureAlgorithm::Eddsa);
}
#[test]
fn test_private_key_from_bad_cbor_unsupported_algo() {
let cbor = cbor_array![
// This algorithms doesn't exist.
cbor_int!(-1),
cbor_bytes!(vec![0x88; 32]),
];
assert_eq!(
PrivateKey::try_from(cbor),
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
);
}
}

View File

@@ -0,0 +1,702 @@
// Copyright 2019-2023 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::apdu::{Apdu, ApduStatusCode};
use super::credential_id::{decrypt_credential_id, encrypt_to_credential_id};
use super::crypto_wrapper::PrivateKey;
use super::CtapState;
use crate::api::attestation_store::{self, Attestation, AttestationStore};
use crate::env::Env;
use alloc::vec::Vec;
use arrayref::array_ref;
use core::convert::TryFrom;
// For now, they're the same thing with apdu.rs containing the authoritative definition
pub type Ctap1StatusCode = ApduStatusCode;
// 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
#[derive(Clone, Debug, PartialEq, Eq)]
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 From<Ctap1Flags> for u8 {
fn from(flags: Ctap1Flags) -> u8 {
flags as u8
}
}
#[derive(Debug, PartialEq, Eq)]
enum U2fCommand {
Register {
challenge: [u8; 32],
application: [u8; 32],
},
Authenticate {
challenge: [u8; 32],
application: [u8; 32],
key_handle: Vec<u8>,
flags: Ctap1Flags,
},
Version,
VendorSpecific {
payload: Vec<u8>,
},
}
impl TryFrom<&[u8]> for U2fCommand {
type Error = Ctap1StatusCode;
fn try_from(message: &[u8]) -> Result<Self, Ctap1StatusCode> {
let apdu: Apdu = match Apdu::try_from(message) {
Ok(apdu) => apdu,
Err(apdu_status_code) => return Err(apdu_status_code),
};
let lc = apdu.lc 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.header.cla != Ctap1Command::CTAP1_CLA {
return Err(Ctap1StatusCode::SW_CLA_INVALID);
}
// Since there is always request data, the expected length is either omitted or
// encoded in 2 bytes.
if lc != apdu.data.len() && lc + 2 != apdu.data.len() {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
match apdu.header.ins {
// 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!(apdu.data, 0, 32),
application: *array_ref!(apdu.data, 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 = apdu.data[64] as usize;
if lc != 65 + handle_length {
return Err(Ctap1StatusCode::SW_WRONG_LENGTH);
}
let flag = Ctap1Flags::try_from(apdu.header.p1)?;
Ok(Self::Authenticate {
challenge: *array_ref!(apdu.data, 0, 32),
application: *array_ref!(apdu.data, 32, 32),
key_handle: apdu.data[65..].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: apdu.data.to_vec(),
})
}
_ => Err(Ctap1StatusCode::SW_INS_INVALID),
}
}
}
pub struct Ctap1Command {}
impl Ctap1Command {
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<E: Env>(
env: &mut E,
message: &[u8],
ctap_state: &mut CtapState<E>,
) -> Result<Vec<u8>, Ctap1StatusCode> {
if !ctap_state
.allows_ctap1(env)
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?
{
return Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED);
}
let command = U2fCommand::try_from(message)?;
match command {
U2fCommand::Register {
challenge,
application,
} => {
if !ctap_state.u2f_up_state.consume_up(env) {
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
}
Ctap1Command::process_register(env, challenge, application)
}
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(env)
{
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
}
Ctap1Command::process_authenticate(
env,
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_SUCCESS),
}
}
// 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(
env: &mut impl Env,
challenge: [u8; 32],
application: [u8; 32],
) -> Result<Vec<u8>, Ctap1StatusCode> {
let private_key = PrivateKey::new_ecdsa(env);
let sk = private_key
.ecdsa_key(env)
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let pk = sk.genpk();
let key_handle = encrypt_to_credential_id(env, &private_key, &application, None, None)
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
if key_handle.len() > 0xFF {
// This is just being defensive with unreachable code.
return Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION);
}
let Attestation {
private_key,
certificate,
} = env
.attestation_store()
.get(&attestation_store::Id::Batch)?
.ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let mut response = Vec::with_capacity(105 + key_handle.len() + 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(&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(&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<E: Env>(
env: &mut E,
challenge: [u8; 32],
application: [u8; 32],
key_handle: Vec<u8>,
flags: Ctap1Flags,
ctap_state: &mut CtapState<E>,
) -> Result<Vec<u8>, Ctap1StatusCode> {
let credential_source = decrypt_credential_id(env, key_handle, &application)
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
if let Some(credential_source) = credential_source {
let ecdsa_key = credential_source
.private_key
.ecdsa_key(env)
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
if flags == Ctap1Flags::CheckOnly {
return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED);
}
ctap_state
.increment_global_signature_counter(env)
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
let mut signature_data = ctap_state
.generate_auth_data(
env,
&application,
Ctap1Command::USER_PRESENCE_INDICATOR_BYTE,
)
.map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?;
signature_data.extend(&challenge);
let signature = ecdsa_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::credential_id::CBOR_CREDENTIAL_ID_SIZE;
use super::super::data_formats::SignatureAlgorithm;
use super::super::TOUCH_TIMEOUT_MS;
use super::*;
use crate::api::customization::Customization;
use crate::ctap::storage;
use crate::env::test::TestEnv;
use crypto::Hash256;
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: &[u8],
) -> Vec<u8> {
let mut message = vec![
Ctap1Command::CTAP1_CLA,
Ctap1Command::U2F_AUTHENTICATE,
flags.into(),
0x00,
0x00,
];
message.extend(&(65 + CBOR_CREDENTIAL_ID_SIZE as u16).to_be_bytes());
let challenge = [0x0C; 32];
message.extend(&challenge);
message.extend(application);
message.push(CBOR_CREDENTIAL_ID_SIZE as u8);
message.extend(key_handle);
message
}
#[test]
fn test_process_allowed() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
storage::toggle_always_uv(&mut env).unwrap();
let application = [0x0A; 32];
let message = create_register_message(&application);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED));
}
#[test]
fn test_process_register() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
let application = [0x0A; 32];
let message = create_register_message(&application);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
// Certificate and private key are missing
assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));
let attestation = Attestation {
private_key: [0x41; 32],
certificate: vec![0x99; 100],
};
env.attestation_store()
.set(&attestation_store::Id::Batch, Some(&attestation))
.unwrap();
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
assert_eq!(response[0], Ctap1Command::LEGACY_BYTE);
assert_eq!(response[66], CBOR_CREDENTIAL_ID_SIZE as u8);
assert!(decrypt_credential_id(
&mut env,
response[67..67 + CBOR_CREDENTIAL_ID_SIZE].to_vec(),
&application,
)
.unwrap()
.is_some());
const CERT_START: usize = 67 + CBOR_CREDENTIAL_ID_SIZE;
assert_eq!(
&response[CERT_START..][..attestation.certificate.len()],
&attestation.certificate
);
}
#[test]
fn test_process_register_bad_message() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
let application = [0x0A; 32];
let message = create_register_message(&application);
let response =
Ctap1Command::process_command(&mut env, &message[..message.len() - 1], &mut ctap_state);
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 env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
env.clock().advance(TOUCH_TIMEOUT_MS);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
}
#[test]
fn test_process_authenticate_check_only() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
}
#[test]
fn test_process_authenticate_check_only_wrong_rp() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let application = [0x55; 32];
let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
}
#[test]
fn test_process_authenticate_check_only_wrong_length() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let mut message = create_authenticate_message(
&application,
Ctap1Flags::DontEnforceUpAndSign,
&key_handle,
);
message.push(0x00);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert!(response.is_ok());
message.push(0x00);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert!(response.is_ok());
message.push(0x00);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert!(response.is_ok());
message.push(0x00);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH));
}
#[test]
fn test_process_authenticate_check_only_wrong_cla() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let mut message =
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
message[0] = 0xEE;
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_CLA_INVALID));
}
#[test]
fn test_process_authenticate_check_only_wrong_ins() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let mut message =
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
message[1] = 0xEE;
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_INS_INVALID));
}
#[test]
fn test_process_authenticate_check_only_wrong_flags() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let mut message =
create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle);
message[2] = 0xEE;
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
}
fn check_signature_counter(env: &mut impl Env, response: &[u8; 4], signature_counter: u32) {
if env.customization().use_signature_counter() {
assert_eq!(u32::from_be_bytes(*response), signature_counter);
} else {
assert_eq!(response, &[0x00, 0x00, 0x00, 0x00]);
}
}
#[test]
fn test_process_authenticate_enforce() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let message =
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
assert_eq!(response[0], 0x01);
let global_signature_counter = storage::global_signature_counter(&mut env).unwrap();
check_signature_counter(
&mut env,
array_ref!(response, 1, 4),
global_signature_counter,
);
}
#[test]
fn test_process_authenticate_dont_enforce() {
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let mut ctap_state = CtapState::new(&mut env);
let rp_id = "example.com";
let application = crypto::sha256::Sha256::hash(rp_id.as_bytes());
let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap();
let message = create_authenticate_message(
&application,
Ctap1Flags::DontEnforceUpAndSign,
&key_handle,
);
env.clock().advance(TOUCH_TIMEOUT_MS);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap();
assert_eq!(response[0], 0x01);
let global_signature_counter = storage::global_signature_counter(&mut env).unwrap();
check_signature_counter(
&mut env,
array_ref!(response, 1, 4),
global_signature_counter,
);
}
#[test]
fn test_process_authenticate_bad_key_handle() {
let application = [0x0A; 32];
let key_handle = vec![0x00; CBOR_CREDENTIAL_ID_SIZE];
let message =
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA));
}
#[test]
fn test_process_authenticate_without_up() {
let application = [0x0A; 32];
let key_handle = vec![0x00; CBOR_CREDENTIAL_ID_SIZE];
let message =
create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle);
let mut env = TestEnv::default();
env.user_presence()
.set(|| panic!("Unexpected user presence check in CTAP1"));
let mut ctap_state = CtapState::new(&mut env);
ctap_state.u2f_up_state.consume_up(&mut env);
ctap_state.u2f_up_state.grant_up(&mut env);
env.clock().advance(TOUCH_TIMEOUT_MS);
let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state);
assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,622 @@
// Copyright 2019-2023 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;
// Implementation details must be public for testing (in particular fuzzing).
#[cfg(feature = "std")]
pub use self::receive::MessageAssembler;
#[cfg(not(feature = "std"))]
use self::receive::MessageAssembler;
pub use self::send::HidPacketIterator;
use super::status_code::Ctap2StatusCode;
#[cfg(test)]
use crate::env::test::TestEnv;
use crate::env::Env;
use alloc::vec;
use alloc::vec::Vec;
use arrayref::{array_ref, array_refs};
#[cfg(test)]
use enum_iterator::IntoEnumIterator;
// We implement CTAP 2.1 from 2021-06-15. Please see section
// 11.2. USB Human Interface Device (USB HID)
const CHANNEL_RESERVED: ChannelID = [0, 0, 0, 0];
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
const PACKET_TYPE_MASK: u8 = 0x80;
// See section 11.2.9.1.3. CTAPHID_INIT (0x06).
const PROTOCOL_VERSION: u8 = 2;
// The device version number is vendor-defined.
const DEVICE_VERSION_MAJOR: u8 = 1;
const DEVICE_VERSION_MINOR: u8 = 0;
const DEVICE_VERSION_BUILD: u8 = 0;
pub type HidPacket = [u8; 64];
pub type ChannelID = [u8; 4];
/// CTAPHID commands
///
/// See section 11.2.9. of FIDO 2.1 (2021-06-15).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(test, derive(IntoEnumIterator))]
pub enum CtapHidCommand {
Ping = 0x01,
Msg = 0x03,
// Lock is optional and may be used in the future.
Lock = 0x04,
Init = 0x06,
Wink = 0x08,
Cbor = 0x10,
Cancel = 0x11,
Keepalive = 0x3B,
Error = 0x3F,
// The vendor range starts here, going from 0x40 to 0x7F.
}
impl From<u8> for CtapHidCommand {
fn from(cmd: u8) -> Self {
match cmd {
x if x == CtapHidCommand::Ping as u8 => CtapHidCommand::Ping,
x if x == CtapHidCommand::Msg as u8 => CtapHidCommand::Msg,
x if x == CtapHidCommand::Lock as u8 => CtapHidCommand::Lock,
x if x == CtapHidCommand::Init as u8 => CtapHidCommand::Init,
x if x == CtapHidCommand::Wink as u8 => CtapHidCommand::Wink,
x if x == CtapHidCommand::Cbor as u8 => CtapHidCommand::Cbor,
x if x == CtapHidCommand::Cancel as u8 => CtapHidCommand::Cancel,
x if x == CtapHidCommand::Keepalive as u8 => CtapHidCommand::Keepalive,
// This includes the actual error code 0x3F. Error is not used for incoming packets in
// the specification, so we can safely reuse it for unknown bytes.
_ => CtapHidCommand::Error,
}
}
}
/// CTAPHID errors
///
/// See section 11.2.9.1.6. of FIDO 2.1 (2021-06-15).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CtapHidError {
/// The command in the request is invalid.
InvalidCmd = 0x01,
/// A parameter in the request is invalid.
_InvalidPar = 0x02,
/// The length of a message is too big.
InvalidLen = 0x03,
/// Expected a continuation packet with a specific sequence number, got another sequence number.
///
/// This error code is also used if we expect a continuation packet, and receive an init
/// packet. We interpreted it as invalid seq number 0.
InvalidSeq = 0x04,
/// This packet arrived after a timeout.
MsgTimeout = 0x05,
/// A packet arrived on one channel while another is busy.
ChannelBusy = 0x06,
/// Command requires channel lock.
_LockRequired = 0x0A,
/// The requested channel ID is invalid.
InvalidChannel = 0x0B,
/// Unspecified error.
_Other = 0x7F,
/// This error is silently ignored.
UnexpectedContinuation,
}
/// Describes the structure of a parsed HID packet.
///
/// A packet is either an Init or a Continuation packet.
#[derive(Clone, Debug, PartialEq, Eq)]
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: CtapHidCommand,
// Bytes of the message.
pub payload: Vec<u8>,
}
/// A keepalive packet reports the reason why a command does not finish.
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeepaliveStatus {
Processing = 0x01,
UpNeeded = 0x02,
}
/// Holds all state for receiving and sending HID packets.
///
/// This includes
/// - state from not fully processed messages,
/// - all allocated channels.
///
/// To process a packet and receive the response, call `parse_packet`. If you didn't receive any
/// message or preprocessing discarded it, stop. Else process the message further, by handling the
/// commands:
///
/// - PING (optional)
/// - MSG
/// - WINK
/// - CBOR
///
/// To get packets to send from your processed message, call `split_message`. Summary:
///
/// 1. `HidPacket` -> `Option<Message>`
/// 2. `Option<Message>` -> `Message`
/// 3. `Message` -> `Message`
/// 4. `Message` -> `HidPacketIterator`
///
/// These steps correspond to:
///
/// 1. `parse_packet` assembles the message and preprocesses all pure HID commands and errors.
/// 2. If you didn't receive any message or preprocessing discarded it, stop.
/// 3. Handles all CTAP protocol interactions.
/// 4. `split_message` creates packets out of the response message.
pub struct CtapHid<E: Env> {
assembler: MessageAssembler<E>,
// The specification 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 ID encoding is Big Endian to match what is used throughout CTAP (with the
// u32::to/from_be_bytes methods).
// TODO(kaczmarczyck) We might want to limit or timeout open channels.
allocated_cids: usize,
capabilities: u8,
}
impl<E: Env> CtapHid<E> {
pub const CAPABILITY_WINK: u8 = 0x01;
pub const CAPABILITY_CBOR: u8 = 0x04;
#[cfg(any(not(feature = "with_ctap1"), feature = "vendor_hid"))]
pub const CAPABILITY_NMSG: u8 = 0x08;
/// Creates a new CTAP HID packet parser.
///
/// The capabilities passed in are reported to the client in Init.
pub fn new(capabilities: u8) -> CtapHid<E> {
Self {
assembler: MessageAssembler::default(),
allocated_cids: 0,
capabilities,
}
}
/// Parses a packet, and preprocesses some messages and errors.
///
/// The preprocessed commands are:
/// - INIT
/// - CANCEL
/// - ERROR
/// - Unknown and unexpected commands like KEEPALIVE
/// - LOCK is not implemented and currently treated like an unknown command
///
/// Commands that may still be processed:
/// - PING
/// - MSG
/// - WINK
/// - CBOR
///
/// You may ignore PING, it's behaving correctly by default (input == output).
/// Ignoring the others is incorrect behavior. You have to at least replace them with an error
/// message:
/// `Self::error_message(message.cid, CtapHidError::InvalidCmd)`
pub fn parse_packet(&mut self, env: &mut E, packet: &HidPacket) -> Option<Message> {
match self.assembler.parse_packet(env, packet) {
Ok(Some(message)) => {
debug_ctap!(env, "Received message: {:02x?}", message);
self.preprocess_message(message)
}
Ok(None) => {
// Waiting for more packets to assemble the message, nothing to send for now.
None
}
Err((cid, error)) => {
if matches!(error, CtapHidError::UnexpectedContinuation) {
None
} else if !self.is_allocated_channel(cid) {
Some(Self::error_message(cid, CtapHidError::InvalidChannel))
} else {
Some(Self::error_message(cid, error))
}
}
}
}
/// Processes HID-only commands of a message and returns an outgoing message if necessary.
///
/// The preprocessed commands are:
/// - INIT
/// - CANCEL
/// - ERROR
/// - Unknown and unexpected commands like KEEPALIVE
/// - LOCK is not implemented and currently treated like an unknown command
fn preprocess_message(&mut self, message: Message) -> Option<Message> {
let cid = message.cid;
if !self.has_valid_channel(&message) {
return Some(Self::error_message(cid, CtapHidError::InvalidChannel));
}
match message.cmd {
CtapHidCommand::Msg => Some(message),
CtapHidCommand::Cbor => Some(message),
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.3.
CtapHidCommand::Init => {
if message.payload.len() != 8 {
return Some(Self::error_message(cid, CtapHidError::InvalidLen));
}
let new_cid = if cid == CHANNEL_BROADCAST {
// TODO: Prevent allocating 2^32 channels.
self.allocated_cids += 1;
(self.allocated_cids as u32).to_be_bytes()
} else {
// Sync the channel and discard the current transaction.
cid
};
let mut payload = vec![0; 17];
payload[..8].copy_from_slice(&message.payload);
payload[8..12].copy_from_slice(&new_cid);
payload[12] = PROTOCOL_VERSION;
payload[13] = DEVICE_VERSION_MAJOR;
payload[14] = DEVICE_VERSION_MINOR;
payload[15] = DEVICE_VERSION_BUILD;
payload[16] = self.capabilities;
Some(Message {
cid,
cmd: CtapHidCommand::Init,
payload,
})
}
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.4.
CtapHidCommand::Ping => {
// Pong the same message.
Some(message)
}
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.5.
CtapHidCommand::Cancel => {
// Authenticators MUST NOT reply to this message.
// CANCEL is handled during user presence checks in main.
None
}
CtapHidCommand::Wink => Some(message),
_ => {
// Unknown or unsupported command.
Some(Self::error_message(cid, CtapHidError::InvalidCmd))
}
}
}
fn has_valid_channel(&self, message: &Message) -> bool {
match message.cid {
// Only INIT commands use the broadcast channel.
CHANNEL_BROADCAST => message.cmd == CtapHidCommand::Init,
// Check that the channel is allocated.
_ => self.is_allocated_channel(message.cid),
}
}
fn is_allocated_channel(&self, cid: ChannelID) -> bool {
cid != CHANNEL_RESERVED && u32::from_be_bytes(cid) as usize <= self.allocated_cids
}
pub fn error_message(cid: ChannelID, error_code: CtapHidError) -> Message {
Message {
cid,
cmd: CtapHidCommand::Error,
payload: vec![error_code as u8],
}
}
/// Helper function to parse a raw packet.
pub fn process_single_packet(packet: &HidPacket) -> (ChannelID, ProcessedPacket) {
let (&cid, rest) = array_refs![packet, 4, 60];
if rest[0] & PACKET_TYPE_MASK != 0 {
let cmd = rest[0] & !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),
},
)
}
}
/// Splits the message and unwraps the result.
///
/// Unwrapping handles the case of payload lengths > 7609 bytes. All responses are fixed
/// length, with the exception of:
/// - PING, but here output equals the (validated) input,
/// - CBOR, where long responses are conceivable.
///
/// Long CBOR responses should not happen, but we might not catch all edge cases, like for
/// example long user names that are part of the output of an assertion. These cases should be
/// correctly handled by the CTAP implementation. It is therefore an internal error from the
/// HID perspective.
pub fn split_message(message: Message) -> HidPacketIterator {
let cid = message.cid;
HidPacketIterator::new(message).unwrap_or_else(|| {
// The error payload is 1 <= 7609 bytes, so unwrap() is safe.
HidPacketIterator::new(Message {
cid,
cmd: CtapHidCommand::Cbor,
payload: vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8],
})
.unwrap()
})
}
/// Generates the HID response packets for a keepalive status.
pub fn keepalive(cid: ChannelID, status: KeepaliveStatus) -> HidPacketIterator {
Self::split_message(Message {
cid,
cmd: CtapHidCommand::Keepalive,
payload: vec![status as u8],
})
}
#[cfg(test)]
pub fn new_initialized() -> (Self, ChannelID) {
(
Self {
assembler: MessageAssembler::default(),
allocated_cids: 1,
capabilities: 0x0D,
},
[0x00, 0x00, 0x00, 0x01],
)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_split_assemble() {
let mut env = TestEnv::default();
for payload_len in 0..7609 {
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0xFF; payload_len],
};
let mut messages = Vec::new();
let mut assembler = MessageAssembler::<TestEnv>::default();
for packet in HidPacketIterator::new(message.clone()).unwrap() {
match assembler.parse_packet(&mut env, &packet) {
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_spurious_continuation_packet() {
let mut env = TestEnv::default();
let mut ctap_hid = CtapHid::<TestEnv>::new(0x0D);
let mut packet = [0x00; 64];
packet[0..7].copy_from_slice(&[0xC1, 0xC1, 0xC1, 0xC1, 0x00, 0x51, 0x51]);
// Continuation packets are silently ignored.
assert_eq!(ctap_hid.parse_packet(&mut env, &packet), None);
}
#[test]
fn test_command_init() {
let mut ctap_hid = CtapHid::<TestEnv>::new(0x0D);
let init_message = Message {
cid: CHANNEL_BROADCAST,
cmd: CtapHidCommand::Init,
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0],
};
let reply = ctap_hid.preprocess_message(init_message);
assert_eq!(
reply,
Some(Message {
cid: CHANNEL_BROADCAST,
cmd: CtapHidCommand::Init,
payload: vec![
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, // Nonce
0x00, 0x00, 0x00, 0x01, // Allocated CID
0x02, // Protocol version
0x01, 0x00, 0x00, // Device version
0x0D, // Capabilities
]
})
);
}
#[test]
fn test_command_init_for_sync() {
let mut env = TestEnv::default();
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
// Ping packet with a length longer than one packet.
let mut packet1 = [0x51; 64];
packet1[..4].copy_from_slice(&cid);
packet1[4..7].copy_from_slice(&[0x81, 0x02, 0x00]);
// Init packet on the same channel.
let mut packet2 = [0x00; 64];
packet2[..4].copy_from_slice(&cid);
packet2[4..15].copy_from_slice(&[
0x86, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
]);
assert_eq!(ctap_hid.parse_packet(&mut env, &packet1), None);
assert_eq!(
ctap_hid.parse_packet(&mut env, &packet2),
Some(Message {
cid,
cmd: CtapHidCommand::Init,
payload: vec![
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, // Nonce
cid[0], cid[1], cid[2], cid[3], // Allocated CID
0x02, // Protocol version
0x01, 0x00, 0x00, // Device version
0x0D, // Capabilities
]
})
);
}
#[test]
fn test_command_ping() {
let mut env = TestEnv::default();
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
let mut ping_packet = [0x00; 64];
ping_packet[..4].copy_from_slice(&cid);
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
assert_eq!(
ctap_hid.parse_packet(&mut env, &ping_packet),
Some(Message {
cid,
cmd: CtapHidCommand::Ping,
payload: vec![0x99, 0x99]
})
);
}
#[test]
fn test_command_cancel() {
let mut env = TestEnv::default();
let (mut ctap_hid, cid) = CtapHid::<TestEnv>::new_initialized();
let mut cancel_packet = [0x00; 64];
cancel_packet[..4].copy_from_slice(&cid);
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
let response = ctap_hid.parse_packet(&mut env, &cancel_packet);
assert_eq!(response, None);
}
#[test]
fn test_split_message() {
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x99, 0x99],
};
let mut response = CtapHid::<TestEnv>::split_message(message);
let mut expected_packet = [0x00; 64];
expected_packet[..9]
.copy_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x02, 0x99, 0x99]);
assert_eq!(response.next(), Some(expected_packet));
assert_eq!(response.next(), None);
}
#[test]
fn test_split_message_too_large() {
let payload = vec![0xFF; 7609 + 1];
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload,
};
let mut response = CtapHid::<TestEnv>::split_message(message);
let mut expected_packet = [0x00; 64];
expected_packet[..8].copy_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x01, 0xF2]);
assert_eq!(response.next(), Some(expected_packet));
assert_eq!(response.next(), None);
}
#[test]
fn test_keepalive() {
for &status in [KeepaliveStatus::Processing, KeepaliveStatus::UpNeeded].iter() {
let cid = [0x12, 0x34, 0x56, 0x78];
let mut response = CtapHid::<TestEnv>::keepalive(cid, status);
let mut expected_packet = [0x00; 64];
expected_packet[..8].copy_from_slice(&[
0x12,
0x34,
0x56,
0x78,
0xBB,
0x00,
0x01,
status as u8,
]);
assert_eq!(response.next(), Some(expected_packet));
assert_eq!(response.next(), None);
}
}
#[test]
fn test_process_single_packet() {
let cid = [0x12, 0x34, 0x56, 0x78];
let mut packet = [0x00; 64];
packet[..4].copy_from_slice(&cid);
packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
let (processed_cid, processed_packet) = CtapHid::<TestEnv>::process_single_packet(&packet);
assert_eq!(processed_cid, cid);
let expected_packet = ProcessedPacket::InitPacket {
cmd: CtapHidCommand::Ping as u8,
len: 2,
data: array_ref!(packet, 7, 57),
};
assert_eq!(processed_packet, expected_packet);
}
#[test]
fn test_from_ctap_hid_command() {
// 0x3E is unassigned.
assert_eq!(CtapHidCommand::from(0x3E), CtapHidCommand::Error);
for command in CtapHidCommand::into_enum_iter() {
assert_eq!(CtapHidCommand::from(command as u8), command);
}
}
#[test]
fn test_error_message() {
let cid = [0x12, 0x34, 0x56, 0x78];
assert_eq!(
CtapHid::<TestEnv>::error_message(cid, CtapHidError::InvalidCmd),
Message {
cid,
cmd: CtapHidCommand::Error,
payload: vec![0x01],
}
);
}
}

View File

@@ -0,0 +1,594 @@
// Copyright 2019-2023 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, CtapHidCommand, CtapHidError, HidPacket, Message, ProcessedPacket,
};
use crate::api::clock::Clock;
use crate::api::customization::Customization;
use crate::env::Env;
use alloc::vec::Vec;
use core::mem::swap;
// TODO: Is this timeout duration specified?
const TIMEOUT_DURATION_MS: usize = 100;
/// A structure to assemble CTAPHID commands from a series of incoming USB HID packets.
pub struct MessageAssembler<E: Env> {
// 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.
timer: <E::Clock as Clock>::Timer,
// 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>,
}
impl<E: Env> Default for MessageAssembler<E> {
fn default() -> MessageAssembler<E> {
MessageAssembler {
idle: true,
cid: [0, 0, 0, 0],
timer: <E::Clock as Clock>::Timer::default(),
cmd: 0,
seq: 0,
remaining_payload_len: 0,
payload: Vec::new(),
}
}
}
impl<E: Env> MessageAssembler<E> {
// Resets the message assembler to the idle state.
// The caller can reset the assembler for example due to a timeout.
fn reset(&mut self) {
self.idle = true;
self.cid = [0, 0, 0, 0];
self.timer = <E::Clock as Clock>::Timer::default();
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.
pub fn parse_packet(
&mut self,
env: &mut E,
packet: &HidPacket,
) -> Result<Option<Message>, (ChannelID, CtapHidError)> {
// 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::<E>::process_single_packet(packet);
if !self.idle && env.clock().is_elapsed(&self.timer) {
// 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, CtapHidError::MsgTimeout));
}
}
if self.idle {
// Expecting an initialization packet.
match processed_packet {
ProcessedPacket::InitPacket { cmd, len, data } => {
self.parse_init_packet(env, cid, cmd, len, data)
}
ProcessedPacket::ContinuationPacket { .. } => {
// CTAP specification (version 20190130) section 8.1.5.4
// Spurious continuation packets will be ignored.
Err((cid, CtapHidError::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, CtapHidError::ChannelBusy));
}
match processed_packet {
// Unexpected initialization packet.
ProcessedPacket::InitPacket { cmd, len, data } => {
self.reset();
if cmd == CtapHidCommand::Init as u8 {
self.parse_init_packet(env, cid, cmd, len, data)
} else {
Err((cid, CtapHidError::InvalidSeq))
}
}
ProcessedPacket::ContinuationPacket { seq, data } => {
if seq != self.seq {
// Reject packets with the wrong sequence number.
self.reset();
Err((cid, CtapHidError::InvalidSeq))
} else {
// Update the last timestamp.
self.timer = env.clock().make_timer(TIMEOUT_DURATION_MS);
// Increment the sequence number for the next packet.
self.seq += 1;
Ok(self.append_payload(data))
}
}
}
}
}
fn parse_init_packet(
&mut self,
env: &mut E,
cid: ChannelID,
cmd: u8,
len: usize,
data: &[u8],
) -> Result<Option<Message>, (ChannelID, CtapHidError)> {
// Reject invalid lengths early to reduce the risk of running out of memory.
// TODO: also reject invalid commands early?
if len > env.customization().max_msg_size() {
return Err((cid, CtapHidError::InvalidLen));
}
self.cid = cid;
self.timer = env.clock().make_timer(TIMEOUT_DURATION_MS);
self.cmd = cmd;
self.seq = 0;
self.remaining_payload_len = len;
Ok(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: CtapHidCommand::from(self.cmd),
payload,
})
}
}
}
#[cfg(test)]
mod test {
use crate::env::test::TestEnv;
use super::*;
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 env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x90])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![]
}))
);
}
#[test]
fn test_one_packet() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x10]),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0x00; 0x10]
}))
);
}
#[test]
fn test_nonzero_padding() {
let mut env = TestEnv::default();
// 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::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x10], 0xFF),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Cbor,
payload: vec![0xFF; 0x10]
}))
);
}
#[test]
fn test_two_packets() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x40]
}))
);
}
#[test]
fn test_three_packets() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x80]),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x80]
}))
);
}
#[test]
fn test_max_packets() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
),
Ok(None)
);
for seq in 0..0x7F {
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
Ok(None)
);
}
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x1DB9]
}))
);
}
#[test]
fn test_multiple_messages() {
let mut env = TestEnv::default();
// Check that after yielding a message, the assembler is ready to process new messages.
let mut assembler = MessageAssembler::default();
for i in 0..10 {
// Introduce some variability in the messages.
let cmd = CtapHidCommand::from(i + 1);
let byte = 3 * i;
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(
&[0x12, 0x34, 0x56, 0x78, 0x80 | cmd as u8, 0x00, 0x80],
byte
),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x00], byte),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x01], byte),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd,
payload: vec![byte; 0x80]
}))
);
}
}
#[test]
fn test_channel_switch() {
let mut env = TestEnv::default();
// Check that the assembler can process messages from multiple channels, sequentially.
let mut assembler = MessageAssembler::default();
for i in 0..10 {
// Introduce some variability in the messages.
let cid = 0x78 + i;
let cmd = CtapHidCommand::from(i + 1);
let byte = 3 * i;
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x80 | cmd as u8, 0x00, 0x80], byte),
),
Ok(None)
);
assert_eq!(
assembler
.parse_packet(&mut env, &byte_extend(&[0x12, 0x34, 0x56, cid, 0x00], byte)),
Ok(None)
);
assert_eq!(
assembler
.parse_packet(&mut env, &byte_extend(&[0x12, 0x34, 0x56, cid, 0x01], byte)),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, cid],
cmd,
payload: vec![byte; 0x80]
}))
);
}
}
#[test]
fn test_unexpected_channel() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
// Check that many sorts of packets on another channel are ignored.
for i in 0..=0xFF {
let cmd = CtapHidCommand::from(i);
for byte in 0..=0xFF {
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x9A, cmd as u8, 0x00], byte),
),
Err(([0x12, 0x34, 0x56, 0x9A], CtapHidError::ChannelBusy))
);
}
}
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x40]
}))
);
}
#[test]
fn test_spurious_continuation_packets() {
let mut env = TestEnv::default();
// 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::default();
for i in 0..0x80 {
// Some legit packet.
let byte = 2 * i;
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x10], byte),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![byte; 0x10]
}))
);
// Spurious continuation packet.
let seq = i;
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
Err((
[0x12, 0x34, 0x56, 0x78],
CtapHidError::UnexpectedContinuation
))
);
}
}
#[test]
fn test_unexpected_init() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80])),
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::InvalidSeq))
);
}
#[test]
fn test_unexpected_seq() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01])),
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::InvalidSeq))
);
}
#[test]
fn test_timed_out_packet() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
),
Ok(None)
);
env.clock().advance(TIMEOUT_DURATION_MS);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00])),
Err(([0x12, 0x34, 0x56, 0x78], CtapHidError::MsgTimeout))
);
}
#[test]
fn test_just_in_time_packets() {
let mut env = TestEnv::default();
// Delay between each packet is just below the threshold.
let delay = TIMEOUT_DURATION_MS - 1;
let mut assembler = MessageAssembler::default();
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
),
Ok(None)
);
for seq in 0..0x7F {
env.clock().advance(delay);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, seq])),
Ok(None)
);
}
env.clock().advance(delay);
assert_eq!(
assembler.parse_packet(&mut env, &zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F])),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Ping,
payload: vec![0x00; 0x1DB9]
}))
);
}
#[test]
fn test_init_sync() {
let mut env = TestEnv::default();
let mut assembler = MessageAssembler::default();
// Ping packet with a length longer than one packet.
assert_eq!(
assembler.parse_packet(
&mut env,
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x02, 0x00], 0x51),
),
Ok(None)
);
// Init packet on the same channel.
assert_eq!(
assembler.parse_packet(
&mut env,
&zero_extend(&[
0x12, 0x34, 0x56, 0x78, 0x86, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
0xDE, 0xF0
]),
),
Ok(Some(Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Init,
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]
}))
);
}
// TODO: more tests
}

View File

@@ -0,0 +1,316 @@
// Copyright 2019-2023 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::{HidPacket, Message};
const TYPE_INIT_BIT: u8 = 0x80;
/// Iterator for HID packets.
///
/// The `new` constructor splits the CTAP `Message` into `HidPacket`s for sending over USB.
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)
}
pub fn has_data(&self) -> bool {
if let Some(ms) = &self.0 {
ms.finished()
} else {
false
}
}
}
impl Iterator for HidPacketIterator {
type Item = HidPacket;
fn next(&mut self) -> Option<HidPacket> {
match &mut self.0 {
Some(splitter) => splitter.next(),
None => None,
}
}
}
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.
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
}
}
// Is there more data to iterate over?
fn finished(&self) -> bool {
let payload_len = self.message.payload.len();
match self.seq {
None => true,
Some(_) => self.i < payload_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 as u8 | 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::super::CtapHidCommand;
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: CtapHidCommand::Cbor,
payload: vec![0xAA, 0xBB],
};
let expected_packets: Vec<HidPacket> = vec![[
0x12, 0x34, 0x56, 0x78, 0x90, 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: CtapHidCommand::Cbor,
payload: vec![0xAA; 64 - 7],
};
let expected_packets: Vec<HidPacket> = vec![[
0x12, 0x34, 0x56, 0x78, 0x90, 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: CtapHidCommand::Cbor,
payload: vec![0xAA; 64 - 7 + 1],
};
let expected_packets: Vec<HidPacket> = vec![
[
0x12, 0x34, 0x56, 0x78, 0x90, 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: CtapHidCommand::Cbor,
payload,
};
let expected_packets: Vec<HidPacket> = vec![
[
0x12, 0x34, 0x56, 0x78, 0x90, 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!(payload.len(), 0x1db9);
let message = Message {
cid: [0x12, 0x34, 0x56, 0x78],
cmd: CtapHidCommand::Msg,
payload,
};
let mut expected_packets: Vec<HidPacket> = vec![[
0x12, 0x34, 0x56, 0x78, 0x83, 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: CtapHidCommand::Msg,
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: CtapHidCommand::Msg,
payload,
};
assert!(HidPacketIterator::new(message).is_none());
}
}

View File

@@ -0,0 +1,459 @@
// Copyright 2020-2021 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::client_pin::{ClientPin, PinPermission};
use super::command::AuthenticatorLargeBlobsParameters;
use super::response::{AuthenticatorLargeBlobsResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use crate::api::customization::Customization;
use crate::ctap::storage;
use crate::env::Env;
use alloc::vec;
use alloc::vec::Vec;
use byteorder::{ByteOrder, LittleEndian};
use crypto::sha256::Sha256;
use crypto::Hash256;
/// The length of the truncated hash that as appended to the large blob data.
const TRUNCATED_HASH_LEN: usize = 16;
pub struct LargeBlobs {
buffer: Vec<u8>,
expected_length: usize,
expected_next_offset: usize,
}
/// Implements the logic for the AuthenticatorLargeBlobs command and keeps its state.
impl LargeBlobs {
pub fn new() -> LargeBlobs {
LargeBlobs {
buffer: Vec::new(),
expected_length: 0,
expected_next_offset: 0,
}
}
/// Process the large blob command.
pub fn process_command<E: Env>(
&mut self,
env: &mut E,
client_pin: &mut ClientPin<E>,
large_blobs_params: AuthenticatorLargeBlobsParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorLargeBlobsParameters {
get,
set,
offset,
length,
pin_uv_auth_param,
pin_uv_auth_protocol,
} = large_blobs_params;
let max_fragment_size = env.customization().max_msg_size() - 64;
if let Some(get) = get {
if get > max_fragment_size || offset.checked_add(get).is_none() {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
}
let config = storage::get_large_blob_array(env, offset, get)?;
return Ok(ResponseData::AuthenticatorLargeBlobs(Some(
AuthenticatorLargeBlobsResponse { config },
)));
}
if let Some(mut set) = set {
if set.len() > max_fragment_size {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
}
if offset == 0 {
self.expected_length =
length.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
if self.expected_length > env.customization().max_large_blob_array_size() {
return Err(Ctap2StatusCode::CTAP2_ERR_LARGE_BLOB_STORAGE_FULL);
}
self.expected_next_offset = 0;
}
if offset != self.expected_next_offset {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ);
}
if storage::pin_hash(env)?.is_some() || storage::has_always_uv(env)? {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_uv_auth_protocol =
pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
let mut large_blob_data = vec![0xFF; 32];
large_blob_data.extend(&[0x0C, 0x00]);
let mut offset_bytes = [0u8; 4];
LittleEndian::write_u32(&mut offset_bytes, offset as u32);
large_blob_data.extend(&offset_bytes);
large_blob_data.extend(&Sha256::hash(set.as_slice()));
client_pin.verify_pin_uv_auth_token(
&large_blob_data,
&pin_uv_auth_param,
pin_uv_auth_protocol,
)?;
client_pin.has_permission(PinPermission::LargeBlobWrite)?;
}
if offset + set.len() > self.expected_length {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if offset == 0 {
self.buffer = Vec::with_capacity(self.expected_length);
}
self.buffer.append(&mut set);
self.expected_next_offset = self.buffer.len();
if self.expected_next_offset == self.expected_length {
self.expected_length = 0;
self.expected_next_offset = 0;
// Must be a positive number.
let buffer_hash_index = self.buffer.len() - TRUNCATED_HASH_LEN;
if Sha256::hash(&self.buffer[..buffer_hash_index])[..TRUNCATED_HASH_LEN]
!= self.buffer[buffer_hash_index..]
{
self.buffer = Vec::new();
return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE);
}
storage::commit_large_blob_array(env, &self.buffer)?;
self.buffer = Vec::new();
}
return Ok(ResponseData::AuthenticatorLargeBlobs(None));
}
// This should be unreachable, since the command has either get or set.
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
}
#[cfg(test)]
mod test {
use super::super::data_formats::PinUvAuthProtocol;
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
use super::*;
use crate::env::test::TestEnv;
#[test]
fn test_process_command_get_empty() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
let large_blob = vec![
0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5,
0x7A, 0x6D, 0x3C,
];
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: Some(large_blob.len()),
set: None,
offset: 0,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
match large_blobs_response.unwrap() {
ResponseData::AuthenticatorLargeBlobs(Some(response)) => {
assert_eq!(response.config, large_blob);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_command_commit_and_get() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
offset: BLOB_LEN / 2,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: Some(BLOB_LEN),
set: None,
offset: 0,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
match large_blobs_response.unwrap() {
ResponseData::AuthenticatorLargeBlobs(Some(response)) => {
assert_eq!(response.config, large_blob);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_command_commit_unexpected_offset() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
// The offset is 1 too big.
offset: BLOB_LEN / 2 + 1,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ),
);
}
#[test]
fn test_process_command_commit_unexpected_length() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 200;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[..BLOB_LEN / 2].to_vec()),
offset: 0,
// The length is 1 too small.
length: Some(BLOB_LEN - 1),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob[BLOB_LEN / 2..].to_vec()),
offset: BLOB_LEN / 2,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
);
}
#[test]
fn test_process_command_commit_end_offset_overflow() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: Some(1),
set: None,
offset: usize::MAX,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
assert_eq!(
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH),
);
}
#[test]
fn test_process_command_commit_unexpected_hash() {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
PinUvAuthProtocol::V1,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 20;
// This blob does not have an appropriate hash.
let large_blob = vec![0x1B; BLOB_LEN];
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob.to_vec()),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE),
);
}
fn test_helper_process_command_commit_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::default();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pin_uv_auth_token = [0x55; 32];
let mut client_pin = ClientPin::<TestEnv>::new_test(
&mut env,
key_agreement_key,
pin_uv_auth_token,
pin_uv_auth_protocol,
);
let mut large_blobs = LargeBlobs::new();
const BLOB_LEN: usize = 20;
const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN;
let mut large_blob = vec![0x1B; DATA_LEN];
large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]);
storage::set_pin(&mut env, &[0u8; 16], 4).unwrap();
let mut large_blob_data = vec![0xFF; 32];
// Command constant and offset bytes.
large_blob_data.extend(&[0x0C, 0x00, 0x00, 0x00, 0x00, 0x00]);
large_blob_data.extend(&Sha256::hash(&large_blob));
let pin_uv_auth_param = authenticate_pin_uv_auth_token(
&pin_uv_auth_token,
&large_blob_data,
pin_uv_auth_protocol,
);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param: Some(pin_uv_auth_param),
pin_uv_auth_protocol: Some(pin_uv_auth_protocol),
};
let large_blobs_response =
large_blobs.process_command(&mut env, &mut client_pin, large_blobs_params);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
}
#[test]
fn test_process_command_commit_with_pin_v1() {
test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_command_commit_with_pin_v2() {
test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V2);
}
}

View File

@@ -0,0 +1,215 @@
// Copyright 2022 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::api::clock::Clock;
#[cfg(feature = "with_ctap1")]
use crate::ctap::ctap1;
#[cfg(feature = "with_ctap1")]
use crate::ctap::hid::ChannelID;
use crate::ctap::hid::{
CtapHid, CtapHidCommand, CtapHidError, HidPacket, HidPacketIterator, Message,
};
use crate::ctap::{Channel, CtapState};
use crate::env::Env;
const WINK_TIMEOUT_DURATION_MS: usize = 5000;
/// Implements the standard CTAP command processing for HID.
pub struct MainHid<E: Env> {
hid: CtapHid<E>,
wink_permission: <E::Clock as Clock>::Timer,
}
impl<E: Env> Default for MainHid<E> {
/// Instantiates a HID handler for CTAP1, CTAP2 and Wink.
fn default() -> Self {
#[cfg(feature = "with_ctap1")]
let capabilities = CtapHid::<E>::CAPABILITY_WINK | CtapHid::<E>::CAPABILITY_CBOR;
#[cfg(not(feature = "with_ctap1"))]
let capabilities = CtapHid::<E>::CAPABILITY_WINK
| CtapHid::<E>::CAPABILITY_CBOR
| CtapHid::<E>::CAPABILITY_NMSG;
let hid = CtapHid::new(capabilities);
let wink_permission = <E::Clock as Clock>::Timer::default();
MainHid {
hid,
wink_permission,
}
}
}
impl<E: Env> MainHid<E> {
/// Processes an incoming USB HID packet, and returns an iterator for all outgoing packets.
pub fn process_hid_packet(
&mut self,
env: &mut E,
packet: &HidPacket,
ctap_state: &mut CtapState<E>,
) -> HidPacketIterator {
if let Some(message) = self.hid.parse_packet(env, packet) {
let processed_message = self.process_message(env, message, ctap_state);
debug_ctap!(env, "Sending message: {:02x?}", processed_message);
CtapHid::<E>::split_message(processed_message)
} else {
HidPacketIterator::none()
}
}
/// Processes a message's commands that affect the protocol outside HID.
pub fn process_message(
&mut self,
env: &mut E,
message: Message,
ctap_state: &mut CtapState<E>,
) -> Message {
// If another command arrives, stop winking to prevent accidential button touches.
self.wink_permission = <E::Clock as Clock>::Timer::default();
let cid = message.cid;
match message.cmd {
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.1.
CtapHidCommand::Msg => {
// If we don't have CTAP1 backward compatibilty, this command is invalid.
#[cfg(not(feature = "with_ctap1"))]
return CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd);
#[cfg(feature = "with_ctap1")]
match ctap1::Ctap1Command::process_command(env, &message.payload, ctap_state) {
Ok(payload) => Self::ctap1_success_message(cid, &payload),
Err(ctap1_status_code) => Self::ctap1_error_message(cid, ctap1_status_code),
}
}
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.2.
CtapHidCommand::Cbor => {
// Each transaction is atomic, so we process the command directly here and
// don't handle any other packet in the meantime.
// TODO: Send "Processing" type keep-alive packets in the meantime.
let response =
ctap_state.process_command(env, &message.payload, Channel::MainHid(cid));
Message {
cid,
cmd: CtapHidCommand::Cbor,
payload: response,
}
}
// CTAP 2.1 from 2021-06-15, section 11.2.9.2.1.
CtapHidCommand::Wink => {
if message.payload.is_empty() {
self.wink_permission = env.clock().make_timer(WINK_TIMEOUT_DURATION_MS);
// The response is empty like the request.
message
} else {
CtapHid::<E>::error_message(cid, CtapHidError::InvalidLen)
}
}
// All other commands have already been processed, keep them as is.
_ => message,
}
}
/// Returns whether a wink permission is currently granted.
pub fn should_wink(&self, env: &mut E) -> bool {
!env.clock().is_elapsed(&self.wink_permission)
}
#[cfg(feature = "with_ctap1")]
fn ctap1_error_message(cid: ChannelID, error_code: ctap1::Ctap1StatusCode) -> Message {
let code: u16 = error_code.into();
Message {
cid,
cmd: CtapHidCommand::Msg,
payload: code.to_be_bytes().to_vec(),
}
}
#[cfg(feature = "with_ctap1")]
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> Message {
let mut response = payload.to_vec();
let code: u16 = ctap1::Ctap1StatusCode::SW_SUCCESS.into();
response.extend_from_slice(&code.to_be_bytes());
Message {
cid,
cmd: CtapHidCommand::Msg,
payload: response,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ctap::hid::ChannelID;
use crate::env::test::TestEnv;
fn new_initialized() -> (MainHid<TestEnv>, ChannelID) {
let (hid, cid) = CtapHid::new_initialized();
let wink_permission = <<TestEnv as Env>::Clock as Clock>::Timer::default();
(
MainHid::<TestEnv> {
hid,
wink_permission,
},
cid,
)
}
#[test]
fn test_process_hid_packet() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut main_hid, cid) = new_initialized();
let mut ping_packet = [0x00; 64];
ping_packet[..4].copy_from_slice(&cid);
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
let mut response = main_hid.process_hid_packet(&mut env, &ping_packet, &mut ctap_state);
assert_eq!(response.next(), Some(ping_packet));
assert_eq!(response.next(), None);
}
#[test]
fn test_process_hid_packet_empty() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut main_hid, cid) = new_initialized();
let mut cancel_packet = [0x00; 64];
cancel_packet[..4].copy_from_slice(&cid);
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
let mut response = main_hid.process_hid_packet(&mut env, &cancel_packet, &mut ctap_state);
assert_eq!(response.next(), None);
}
#[test]
fn test_wink() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut main_hid, cid) = new_initialized();
assert!(!main_hid.should_wink(&mut env));
let mut wink_packet = [0x00; 64];
wink_packet[..4].copy_from_slice(&cid);
wink_packet[4..7].copy_from_slice(&[0x88, 0x00, 0x00]);
let mut response = main_hid.process_hid_packet(&mut env, &wink_packet, &mut ctap_state);
assert_eq!(response.next(), Some(wink_packet));
assert_eq!(response.next(), None);
assert!(main_hid.should_wink(&mut env));
env.clock().advance(WINK_TIMEOUT_DURATION_MS);
assert!(!main_hid.should_wink(&mut env));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,408 @@
// Copyright 2021 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::ctap::client_pin::PIN_TOKEN_LENGTH;
use crate::ctap::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt};
use crate::ctap::data_formats::{CoseKey, PinUvAuthProtocol};
use crate::ctap::status_code::Ctap2StatusCode;
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::convert::TryInto;
use crypto::hkdf::hkdf_empty_salt_256;
#[cfg(test)]
use crypto::hmac::hmac_256;
use crypto::hmac::{verify_hmac_256, verify_hmac_256_first_128bits};
use crypto::sha256::Sha256;
use crypto::Hash256;
use rng256::Rng256;
/// Implements common functions between existing PIN protocols for handshakes.
pub struct PinProtocol {
key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
}
impl PinProtocol {
/// This process is run by the authenticator at power-on.
///
/// This function implements "initialize" from the specification.
pub fn new(rng: &mut impl Rng256) -> PinProtocol {
let key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
let pin_uv_auth_token = rng.gen_uniform_u8x32();
PinProtocol {
key_agreement_key,
pin_uv_auth_token,
}
}
/// Generates a fresh public key.
pub fn regenerate(&mut self, rng: &mut impl Rng256) {
self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
}
/// Generates a fresh pinUvAuthToken.
pub fn reset_pin_uv_auth_token(&mut self, rng: &mut impl Rng256) {
self.pin_uv_auth_token = rng.gen_uniform_u8x32();
}
/// Returns the authenticators public key as a CoseKey structure.
pub fn get_public_key(&self) -> CoseKey {
CoseKey::from(self.key_agreement_key.genpk())
}
/// Processes the peer's encapsulated CoseKey and returns the shared secret.
pub fn decapsulate(
&self,
peer_cose_key: CoseKey,
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Result<Box<dyn SharedSecret>, Ctap2StatusCode> {
let pk: crypto::ecdh::PubKey = CoseKey::try_into(peer_cose_key)?;
let handshake = self.key_agreement_key.exchange_x(&pk);
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => Ok(Box::new(SharedSecretV1::new(handshake))),
PinUvAuthProtocol::V2 => Ok(Box::new(SharedSecretV2::new(handshake))),
}
}
/// Getter for pinUvAuthToken.
pub fn get_pin_uv_auth_token(&self) -> &[u8; PIN_TOKEN_LENGTH] {
&self.pin_uv_auth_token
}
/// This is used for debugging to inject key material.
#[cfg(test)]
pub fn new_test(
key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
) -> PinProtocol {
PinProtocol {
key_agreement_key,
pin_uv_auth_token,
}
}
}
/// Authenticates the pinUvAuthToken for the given PIN protocol.
#[cfg(test)]
pub fn authenticate_pin_uv_auth_token(
token: &[u8; PIN_TOKEN_LENGTH],
message: &[u8],
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Vec<u8> {
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => hmac_256::<Sha256>(token, message)[..16].to_vec(),
PinUvAuthProtocol::V2 => hmac_256::<Sha256>(token, message).to_vec(),
}
}
/// Verifies the pinUvAuthToken for the given PIN protocol.
pub fn verify_pin_uv_auth_token(
token: &[u8; PIN_TOKEN_LENGTH],
message: &[u8],
signature: &[u8],
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Result<(), Ctap2StatusCode> {
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => verify_v1(token, message, signature),
PinUvAuthProtocol::V2 => verify_v2(token, message, signature),
}
}
pub trait SharedSecret {
/// Returns the encrypted plaintext.
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode>;
/// Returns the decrypted ciphertext.
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode>;
/// Verifies that the signature is a valid MAC for the given message.
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode>;
/// Creates a signature that matches verify.
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8>;
}
fn verify_v1(key: &[u8; 32], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
if signature.len() != 16 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if verify_hmac_256_first_128bits::<Sha256>(key, message, array_ref![signature, 0, 16]) {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
fn verify_v2(key: &[u8; 32], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
if signature.len() != 32 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if verify_hmac_256::<Sha256>(key, message, array_ref![signature, 0, 32]) {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
pub struct SharedSecretV1 {
common_secret: [u8; 32],
aes_enc_key: crypto::aes256::EncryptionKey,
}
impl SharedSecretV1 {
/// Creates a new shared secret from the handshake result.
fn new(handshake: [u8; 32]) -> SharedSecretV1 {
let common_secret = Sha256::hash(&handshake);
let aes_enc_key = crypto::aes256::EncryptionKey::new(&common_secret);
SharedSecretV1 {
common_secret,
aes_enc_key,
}
}
}
impl SharedSecret for SharedSecretV1 {
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, false)
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, false)
}
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
verify_v1(&self.common_secret, message, signature)
}
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8> {
hmac_256::<Sha256>(&self.common_secret, message)[..16].to_vec()
}
}
pub struct SharedSecretV2 {
aes_enc_key: crypto::aes256::EncryptionKey,
hmac_key: [u8; 32],
}
impl SharedSecretV2 {
/// Creates a new shared secret from the handshake result.
fn new(handshake: [u8; 32]) -> SharedSecretV2 {
let aes_key = hkdf_empty_salt_256::<Sha256>(&handshake, b"CTAP2 AES key");
SharedSecretV2 {
aes_enc_key: crypto::aes256::EncryptionKey::new(&aes_key),
hmac_key: hkdf_empty_salt_256::<Sha256>(&handshake, b"CTAP2 HMAC key"),
}
}
}
impl SharedSecret for SharedSecretV2 {
fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, true)
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Ctap2StatusCode> {
aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, true)
}
fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> {
verify_v2(&self.hmac_key, message, signature)
}
#[cfg(test)]
fn authenticate(&self, message: &[u8]) -> Vec<u8> {
hmac_256::<Sha256>(&self.hmac_key, message).to_vec()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
#[test]
fn test_pin_protocol_public_key() {
let mut env = TestEnv::default();
let mut pin_protocol = PinProtocol::new(env.rng());
let public_key = pin_protocol.get_public_key();
pin_protocol.regenerate(env.rng());
let new_public_key = pin_protocol.get_public_key();
assert_ne!(public_key, new_public_key);
}
#[test]
fn test_pin_protocol_pin_uv_auth_token() {
let mut env = TestEnv::default();
let mut pin_protocol = PinProtocol::new(env.rng());
let token = *pin_protocol.get_pin_uv_auth_token();
pin_protocol.reset_pin_uv_auth_token(env.rng());
let new_token = pin_protocol.get_pin_uv_auth_token();
assert_ne!(&token, new_token);
}
#[test]
fn test_shared_secret_v1_encrypt_decrypt() {
let mut env = TestEnv::default();
let shared_secret = SharedSecretV1::new([0x55; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret.encrypt(env.rng(), &plaintext).unwrap();
assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext));
}
#[test]
fn test_shared_secret_v1_authenticate_verify() {
let shared_secret = SharedSecretV1::new([0x55; 32]);
let message = [0xAA; 32];
let signature = shared_secret.authenticate(&message);
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
}
#[test]
fn test_shared_secret_v1_verify() {
let shared_secret = SharedSecretV1::new([0x55; 32]);
let message = [0xAA];
let signature = [
0x8B, 0x60, 0x15, 0x7D, 0xF3, 0x44, 0x82, 0x2E, 0x54, 0x34, 0x7A, 0x01, 0xFB, 0x02,
0x48, 0xA6,
];
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
assert_eq!(
shared_secret.verify(&[0xBB], &signature),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
shared_secret.verify(&message, &[0x12; 16]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_shared_secret_v2_encrypt_decrypt() {
let mut env = TestEnv::default();
let shared_secret = SharedSecretV2::new([0x55; 32]);
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret.encrypt(env.rng(), &plaintext).unwrap();
assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext));
}
#[test]
fn test_shared_secret_v2_authenticate_verify() {
let shared_secret = SharedSecretV2::new([0x55; 32]);
let message = [0xAA; 32];
let signature = shared_secret.authenticate(&message);
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
}
#[test]
fn test_shared_secret_v2_verify() {
let shared_secret = SharedSecretV2::new([0x55; 32]);
let message = [0xAA];
let signature = [
0xC0, 0x3F, 0x2A, 0x22, 0x5C, 0xC3, 0x4E, 0x05, 0xC1, 0x0E, 0x72, 0x9C, 0x8D, 0xD5,
0x7D, 0xE5, 0x98, 0x9C, 0x68, 0x15, 0xEC, 0xE2, 0x3A, 0x95, 0xD5, 0x90, 0xE1, 0xE9,
0x3F, 0xF0, 0x1A, 0xAF,
];
assert_eq!(shared_secret.verify(&message, &signature), Ok(()));
assert_eq!(
shared_secret.verify(&[0xBB], &signature),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
shared_secret.verify(&message, &[0x12; 32]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_decapsulate_symmetric() {
let mut env = TestEnv::default();
let pin_protocol1 = PinProtocol::new(env.rng());
let pin_protocol2 = PinProtocol::new(env.rng());
for &protocol in &[PinUvAuthProtocol::V1, PinUvAuthProtocol::V2] {
let shared_secret1 = pin_protocol1
.decapsulate(pin_protocol2.get_public_key(), protocol)
.unwrap();
let shared_secret2 = pin_protocol2
.decapsulate(pin_protocol1.get_public_key(), protocol)
.unwrap();
let plaintext = vec![0xAA; 64];
let ciphertext = shared_secret1.encrypt(env.rng(), &plaintext).unwrap();
assert_eq!(plaintext, shared_secret2.decrypt(&ciphertext).unwrap());
}
}
#[test]
fn test_verify_pin_uv_auth_token_v1() {
let token = [0x91; PIN_TOKEN_LENGTH];
let message = [0xAA];
let signature = [
0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50,
0x49, 0x68,
];
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V1),
Ok(())
);
assert_eq!(
verify_pin_uv_auth_token(
&[0x12; PIN_TOKEN_LENGTH],
&message,
&signature,
PinUvAuthProtocol::V1
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V1),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &[0x12; 16], PinUvAuthProtocol::V1),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_verify_pin_uv_auth_token_v2() {
let token = [0x91; PIN_TOKEN_LENGTH];
let message = [0xAA];
let signature = [
0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50,
0x49, 0x68, 0x94, 0x90, 0x20, 0x53, 0x0F, 0xA3, 0xD2, 0x7A, 0x9F, 0xFD, 0xFA, 0x62,
0x36, 0x93, 0xF7, 0x84,
];
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V2),
Ok(())
);
assert_eq!(
verify_pin_uv_auth_token(
&[0x12; PIN_TOKEN_LENGTH],
&message,
&signature,
PinUvAuthProtocol::V2
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V2),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
verify_pin_uv_auth_token(&token, &message, &[0x12; 32], PinUvAuthProtocol::V2),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}

View File

@@ -0,0 +1,688 @@
// Copyright 2019-2021 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::{
AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
};
use alloc::string::String;
use alloc::vec::Vec;
use sk_cbor as cbor;
use sk_cbor::{
cbor_array_vec, cbor_bool, cbor_int, cbor_map_collection, cbor_map_options, cbor_text,
};
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
pub enum ResponseData {
AuthenticatorMakeCredential(AuthenticatorMakeCredentialResponse),
AuthenticatorGetAssertion(AuthenticatorGetAssertionResponse),
AuthenticatorGetNextAssertion(AuthenticatorGetAssertionResponse),
AuthenticatorGetInfo(AuthenticatorGetInfoResponse),
AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>),
AuthenticatorReset,
AuthenticatorCredentialManagement(Option<AuthenticatorCredentialManagementResponse>),
AuthenticatorSelection,
AuthenticatorLargeBlobs(Option<AuthenticatorLargeBlobsResponse>),
AuthenticatorConfig,
AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse),
AuthenticatorVendorUpgrade,
AuthenticatorVendorUpgradeInfo(AuthenticatorVendorUpgradeInfoResponse),
}
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(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorReset => None,
ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorSelection => None,
ResponseData::AuthenticatorLargeBlobs(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorConfig => None,
ResponseData::AuthenticatorVendorConfigure(data) => Some(data.into()),
ResponseData::AuthenticatorVendorUpgrade => None,
ResponseData::AuthenticatorVendorUpgradeInfo(data) => Some(data.into()),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorMakeCredentialResponse {
pub fmt: String,
pub auth_data: Vec<u8>,
pub att_stmt: PackedAttestationStatement,
pub ep_att: Option<bool>,
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorMakeCredentialResponse> for cbor::Value {
fn from(make_credential_response: AuthenticatorMakeCredentialResponse) -> Self {
let AuthenticatorMakeCredentialResponse {
fmt,
auth_data,
att_stmt,
ep_att,
large_blob_key,
} = make_credential_response;
cbor_map_options! {
0x01 => fmt,
0x02 => auth_data,
0x03 => att_stmt,
0x04 => ep_att,
0x05 => large_blob_key,
}
}
}
#[derive(Debug, PartialEq, Eq)]
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>,
// 0x06: userSelected missing as we don't support displays.
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorGetAssertionResponse> for cbor::Value {
fn from(get_assertion_response: AuthenticatorGetAssertionResponse) -> Self {
let AuthenticatorGetAssertionResponse {
credential,
auth_data,
signature,
user,
number_of_credentials,
large_blob_key,
} = get_assertion_response;
cbor_map_options! {
0x01 => credential,
0x02 => auth_data,
0x03 => signature,
0x04 => user,
0x05 => number_of_credentials,
0x07 => large_blob_key,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorGetInfoResponse {
pub versions: Vec<String>,
pub extensions: Option<Vec<String>>,
pub aaguid: [u8; 16],
pub options: Option<Vec<(String, bool)>>,
pub max_msg_size: Option<u64>,
pub pin_protocols: Option<Vec<u64>>,
pub max_credential_count_in_list: Option<u64>,
pub max_credential_id_length: Option<u64>,
pub transports: Option<Vec<AuthenticatorTransport>>,
pub algorithms: Option<Vec<PublicKeyCredentialParameter>>,
pub max_serialized_large_blob_array: Option<u64>,
pub force_pin_change: Option<bool>,
pub min_pin_length: u8,
pub firmware_version: Option<u64>,
pub max_cred_blob_length: Option<u64>,
pub max_rp_ids_for_set_min_pin_length: Option<u64>,
// Missing response fields as they are only relevant for internal UV:
// - 0x11: preferredPlatformUvAttempts
// - 0x12: uvModality
// Add them when your hardware supports any kind of user verification within
// the boundary of the device, e.g. fingerprint or built-in keyboard.
pub certifications: Option<Vec<(String, i64)>>,
pub remaining_discoverable_credentials: Option<u64>,
// - 0x15: vendorPrototypeConfigCommands missing as we don't support it.
}
impl From<AuthenticatorGetInfoResponse> for cbor::Value {
fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self {
let AuthenticatorGetInfoResponse {
versions,
extensions,
aaguid,
options,
max_msg_size,
pin_protocols,
max_credential_count_in_list,
max_credential_id_length,
transports,
algorithms,
max_serialized_large_blob_array,
force_pin_change,
min_pin_length,
firmware_version,
max_cred_blob_length,
max_rp_ids_for_set_min_pin_length,
certifications,
remaining_discoverable_credentials,
} = get_info_response;
let options_cbor: Option<cbor::Value> = options.map(|options| {
let options_map: Vec<(_, _)> = options
.into_iter()
.map(|(key, value)| (cbor_text!(key), cbor_bool!(value)))
.collect();
cbor_map_collection!(options_map)
});
let certifications_cbor: Option<cbor::Value> = certifications.map(|certifications| {
let certifications_map: Vec<(_, _)> = certifications
.into_iter()
.map(|(key, value)| (cbor_text!(key), cbor_int!(value)))
.collect();
cbor_map_collection!(certifications_map)
});
cbor_map_options! {
0x01 => cbor_array_vec!(versions),
0x02 => extensions.map(|vec| cbor_array_vec!(vec)),
0x03 => &aaguid,
0x04 => options_cbor,
0x05 => max_msg_size,
0x06 => pin_protocols.map(|vec| cbor_array_vec!(vec)),
0x07 => max_credential_count_in_list,
0x08 => max_credential_id_length,
0x09 => transports.map(|vec| cbor_array_vec!(vec)),
0x0A => algorithms.map(|vec| cbor_array_vec!(vec)),
0x0B => max_serialized_large_blob_array,
0x0C => force_pin_change,
0x0D => min_pin_length as u64,
0x0E => firmware_version,
0x0F => max_cred_blob_length,
0x10 => max_rp_ids_for_set_min_pin_length,
0x13 => certifications_cbor,
0x14 => remaining_discoverable_credentials,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorClientPinResponse {
pub key_agreement: Option<CoseKey>,
pub pin_uv_auth_token: Option<Vec<u8>>,
pub retries: Option<u64>,
pub power_cycle_state: Option<bool>,
// - 0x05: uvRetries missing as we don't support internal UV.
}
impl From<AuthenticatorClientPinResponse> for cbor::Value {
fn from(client_pin_response: AuthenticatorClientPinResponse) -> Self {
let AuthenticatorClientPinResponse {
key_agreement,
pin_uv_auth_token,
retries,
power_cycle_state,
} = client_pin_response;
cbor_map_options! {
0x01 => key_agreement.map(cbor::Value::from),
0x02 => pin_uv_auth_token,
0x03 => retries,
0x04 => power_cycle_state,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorLargeBlobsResponse {
pub config: Vec<u8>,
}
impl From<AuthenticatorLargeBlobsResponse> for cbor::Value {
fn from(platform_large_blobs_response: AuthenticatorLargeBlobsResponse) -> Self {
let AuthenticatorLargeBlobsResponse { config } = platform_large_blobs_response;
cbor_map_options! {
0x01 => config,
}
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct AuthenticatorCredentialManagementResponse {
pub existing_resident_credentials_count: Option<u64>,
pub max_possible_remaining_resident_credentials_count: Option<u64>,
pub rp: Option<PublicKeyCredentialRpEntity>,
pub rp_id_hash: Option<Vec<u8>>,
pub total_rps: Option<u64>,
pub user: Option<PublicKeyCredentialUserEntity>,
pub credential_id: Option<PublicKeyCredentialDescriptor>,
pub public_key: Option<CoseKey>,
pub total_credentials: Option<u64>,
pub cred_protect: Option<CredentialProtectionPolicy>,
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorCredentialManagementResponse> for cbor::Value {
fn from(cred_management_response: AuthenticatorCredentialManagementResponse) -> Self {
let AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count,
max_possible_remaining_resident_credentials_count,
rp,
rp_id_hash,
total_rps,
user,
credential_id,
public_key,
total_credentials,
cred_protect,
large_blob_key,
} = cred_management_response;
cbor_map_options! {
0x01 => existing_resident_credentials_count,
0x02 => max_possible_remaining_resident_credentials_count,
0x03 => rp,
0x04 => rp_id_hash,
0x05 => total_rps,
0x06 => user,
0x07 => credential_id,
0x08 => public_key.map(cbor::Value::from),
0x09 => total_credentials,
0x0A => cred_protect,
0x0B => large_blob_key,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorVendorConfigureResponse {
pub cert_programmed: bool,
pub pkey_programmed: bool,
}
impl From<AuthenticatorVendorConfigureResponse> for cbor::Value {
fn from(vendor_response: AuthenticatorVendorConfigureResponse) -> Self {
let AuthenticatorVendorConfigureResponse {
cert_programmed,
pkey_programmed,
} = vendor_response;
cbor_map_options! {
0x01 => cert_programmed,
0x02 => pkey_programmed,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AuthenticatorVendorUpgradeInfoResponse {
pub info: u32,
}
impl From<AuthenticatorVendorUpgradeInfoResponse> for cbor::Value {
fn from(vendor_upgrade_info_response: AuthenticatorVendorUpgradeInfoResponse) -> Self {
let AuthenticatorVendorUpgradeInfoResponse { info } = vendor_upgrade_info_response;
cbor_map_options! {
0x01 => info as u64,
}
}
}
#[cfg(test)]
mod test {
use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType};
use super::super::ES256_CRED_PARAM;
use super::*;
use crate::env::test::TestEnv;
use cbor::{cbor_array, cbor_bytes, cbor_map};
#[test]
fn test_make_credential_into_cbor() {
let certificate = 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![certificate],
"ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D],
};
let make_credential_response = AuthenticatorMakeCredentialResponse {
fmt: "packed".to_string(),
auth_data: vec![0xAD],
att_stmt,
ep_att: Some(true),
large_blob_key: Some(vec![0x1B]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorMakeCredential(make_credential_response).into();
let expected_cbor = cbor_map_options! {
0x01 => "packed",
0x02 => vec![0xAD],
0x03 => cbor_packed_attestation_statement,
0x04 => true,
0x05 => vec![0x1B],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_get_assertion_into_cbor() {
let pub_key_cred_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x2D, 0x2D, 0x2D, 0x2D],
transports: Some(vec![AuthenticatorTransport::Usb]),
};
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 get_assertion_response = AuthenticatorGetAssertionResponse {
credential: Some(pub_key_cred_descriptor),
auth_data: vec![0xAD],
signature: vec![0x51],
user: Some(user),
number_of_credentials: Some(2),
large_blob_key: Some(vec![0x1B]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetAssertion(get_assertion_response).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor_map! {
"id" => vec![0x2D, 0x2D, 0x2D, 0x2D],
"type" => "public-key",
"transports" => cbor_array!["usb"],
},
0x02 => vec![0xAD],
0x03 => vec![0x51],
0x04 => cbor_map! {
"id" => vec![0x1D, 0x1D, 0x1D, 0x1D],
"icon" => "example.com/foo/icon.png".to_string(),
"name" => "foo".to_string(),
"displayName" => "bar".to_string(),
},
0x05 => 2,
0x07 => vec![0x1B],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_get_info_into_cbor() {
let versions = vec!["FIDO_2_0".to_string()];
let get_info_response = AuthenticatorGetInfoResponse {
versions: versions.clone(),
extensions: None,
aaguid: [0x00; 16],
options: None,
max_msg_size: None,
pin_protocols: None,
max_credential_count_in_list: None,
max_credential_id_length: None,
transports: None,
algorithms: None,
max_serialized_large_blob_array: None,
force_pin_change: None,
min_pin_length: 4,
firmware_version: None,
max_cred_blob_length: None,
max_rp_ids_for_set_min_pin_length: None,
certifications: None,
remaining_discoverable_credentials: None,
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetInfo(get_info_response).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor_array_vec![versions],
0x03 => vec![0x00; 16],
0x0D => 4,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_get_info_optionals_into_cbor() {
let get_info_response = AuthenticatorGetInfoResponse {
versions: vec!["FIDO_2_0".to_string()],
extensions: Some(vec!["extension".to_string()]),
aaguid: [0x00; 16],
options: Some(vec![(String::from("rk"), true)]),
max_msg_size: Some(1024),
pin_protocols: Some(vec![1]),
max_credential_count_in_list: Some(20),
max_credential_id_length: Some(256),
transports: Some(vec![AuthenticatorTransport::Usb]),
algorithms: Some(vec![ES256_CRED_PARAM]),
max_serialized_large_blob_array: Some(1024),
force_pin_change: Some(false),
min_pin_length: 4,
firmware_version: Some(0),
max_cred_blob_length: Some(1024),
max_rp_ids_for_set_min_pin_length: Some(8),
certifications: Some(vec![(String::from("example-cert"), 1)]),
remaining_discoverable_credentials: Some(150),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetInfo(get_info_response).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor_array!["FIDO_2_0"],
0x02 => cbor_array!["extension"],
0x03 => vec![0x00; 16],
0x04 => cbor_map! {"rk" => true},
0x05 => 1024,
0x06 => cbor_array![1],
0x07 => 20,
0x08 => 256,
0x09 => cbor_array!["usb"],
0x0A => cbor_array![ES256_CRED_PARAM],
0x0B => 1024,
0x0C => false,
0x0D => 4,
0x0E => 0,
0x0F => 1024,
0x10 => 8,
0x13 => cbor_map! {"example-cert" => 1},
0x14 => 150,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_used_client_pin_into_cbor() {
let mut env = TestEnv::default();
let sk = crypto::ecdh::SecKey::gensk(env.rng());
let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
let client_pin_response = AuthenticatorClientPinResponse {
key_agreement: Some(cose_key.clone()),
pin_uv_auth_token: Some(vec![70]),
retries: Some(8),
power_cycle_state: Some(false),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => cbor::Value::from(cose_key),
0x02 => vec![70],
0x03 => 8,
0x04 => false,
};
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);
}
#[test]
fn test_used_credential_management_into_cbor() {
let cred_management_response = AuthenticatorCredentialManagementResponse::default();
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
let expected_cbor = cbor_map_options! {};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_used_credential_management_optionals_into_cbor() {
let mut env = TestEnv::default();
let sk = crypto::ecdh::SecKey::gensk(env.rng());
let rp = PublicKeyCredentialRpEntity {
rp_id: String::from("example.com"),
rp_name: None,
rp_icon: None,
};
let user = PublicKeyCredentialUserEntity {
user_id: vec![0xFA, 0xB1, 0xA2],
user_name: None,
user_display_name: None,
user_icon: None,
};
let cred_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None,
};
let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
let cred_management_response = AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: Some(100),
max_possible_remaining_resident_credentials_count: Some(96),
rp: Some(rp.clone()),
rp_id_hash: Some(vec![0x1D; 32]),
total_rps: Some(3),
user: Some(user.clone()),
credential_id: Some(cred_descriptor.clone()),
public_key: Some(cose_key.clone()),
total_credentials: Some(2),
cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional),
large_blob_key: Some(vec![0xBB; 64]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => 100,
0x02 => 96,
0x03 => rp,
0x04 => vec![0x1D; 32],
0x05 => 3,
0x06 => user,
0x07 => cred_descriptor,
0x08 => cbor::Value::from(cose_key),
0x09 => 2,
0x0A => 0x01,
0x0B => vec![0xBB; 64],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_empty_credential_management_into_cbor() {
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(None).into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_selection_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorSelection.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_large_blobs_into_cbor() {
let large_blobs_response = AuthenticatorLargeBlobsResponse { config: vec![0xC0] };
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorLargeBlobs(Some(large_blobs_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => vec![0xC0],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_empty_large_blobs_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorLargeBlobs(None).into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_config_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorConfig.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_vendor_response_into_cbor() {
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse {
cert_programmed: true,
pkey_programmed: false,
})
.into();
assert_eq!(
response_cbor,
Some(cbor_map_options! {
0x01 => true,
0x02 => false,
})
);
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorVendorConfigure(AuthenticatorVendorConfigureResponse {
cert_programmed: false,
pkey_programmed: true,
})
.into();
assert_eq!(
response_cbor,
Some(cbor_map_options! {
0x01 => false,
0x02 => true,
})
);
}
#[test]
fn test_vendor_upgrade_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorVendorUpgrade.into();
assert_eq!(response_cbor, None);
}
#[test]
fn test_vendor_upgrade_info_into_cbor() {
let vendor_upgrade_info_response =
AuthenticatorVendorUpgradeInfoResponse { info: 0x00060000 };
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorVendorUpgradeInfo(vendor_upgrade_info_response).into();
let expected_cbor = cbor_map! {
0x01 => 0x00060000,
};
assert_eq!(response_cbor, Some(expected_cbor));
}
}

View File

@@ -0,0 +1,113 @@
// Copyright 2019-2021 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::api::user_presence::UserPresenceError;
use crate::api::{attestation_store, key_store};
// 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(Copy, Clone, 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_FP_DATABASE_FULL = 0x17,
CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18,
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_PUAT_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,
CTAP2_ERR_INTEGRITY_FAILURE = 0x3D,
CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E,
CTAP2_ERR_UV_INVALID = 0x3F,
CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40,
CTAP1_ERR_OTHER = 0x7F,
_CTAP2_ERR_SPEC_LAST = 0xDF,
_CTAP2_ERR_EXTENSION_FIRST = 0xE0,
_CTAP2_ERR_EXTENSION_LAST = 0xEF,
_CTAP2_ERR_VENDOR_FIRST = 0xF0,
/// An internal invariant is broken.
///
/// This type of error is unexpected and the current state is undefined.
CTAP2_ERR_VENDOR_INTERNAL_ERROR = 0xF2,
/// The hardware is malfunctioning.
///
/// It may be possible that some of those errors are actually internal errors.
CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3,
_CTAP2_ERR_VENDOR_LAST = 0xFF,
}
impl From<UserPresenceError> for Ctap2StatusCode {
fn from(user_presence_error: UserPresenceError) -> Self {
match user_presence_error {
UserPresenceError::Timeout => Self::CTAP2_ERR_USER_ACTION_TIMEOUT,
UserPresenceError::Declined => Self::CTAP2_ERR_OPERATION_DENIED,
UserPresenceError::Canceled => Self::CTAP2_ERR_KEEPALIVE_CANCEL,
}
}
}
impl From<key_store::Error> for Ctap2StatusCode {
fn from(_: key_store::Error) -> Self {
Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR
}
}
impl From<attestation_store::Error> for Ctap2StatusCode {
fn from(error: attestation_store::Error) -> Self {
use attestation_store::Error;
match error {
Error::Storage => Self::CTAP2_ERR_VENDOR_HARDWARE_FAILURE,
Error::Internal => Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
Error::NoSupport => Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
// Copyright 2019-2020 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.
/// Number of keys that persist the CTAP reset command.
pub const NUM_PERSISTENT_KEYS: usize = 20;
/// Defines a key given its name and value or range of values.
macro_rules! make_key {
($(#[$doc: meta])* $name: ident = $key: literal..$end: literal) => {
$(#[$doc])* pub const $name: core::ops::Range<usize> = $key..$end;
};
($(#[$doc: meta])* $name: ident = $key: literal) => {
$(#[$doc])* pub const $name: usize = $key;
};
}
/// Returns the range of values of a key given its value description.
#[cfg(test)]
macro_rules! make_range {
($key: literal..$end: literal) => {
$key..$end
};
($key: literal) => {
$key..$key + 1
};
}
/// Helper to define keys as a partial partition of a range.
macro_rules! make_partition {
($range: expr,
$(
$(#[$doc: meta])*
$name: ident = $key: literal $(.. $end: literal)?;
)*) => {
$(
make_key!($(#[$doc])* $name = $key $(.. $end)?);
)*
#[cfg(test)]
const KEY_RANGE: core::ops::Range<usize> = $range;
#[cfg(test)]
const ALL_KEYS: &[core::ops::Range<usize>] = &[$(make_range!($key $(.. $end)?)),*];
};
}
make_partition! {
// We reserve 0 and 2048+ for possible migration purposes. We add persistent entries starting
// from 1 and going up. We add non-persistent entries starting from 2047 and going down. This
// way, we don't commit to a fixed number of persistent keys.
1..2048,
// WARNING: Keys should not be deleted but prefixed with `_` to avoid accidentally reusing them.
/// Reserved for the attestation store implementation of the environment.
_RESERVED_ATTESTATION_STORE = 1..3;
/// Used for the AAGUID before, but deprecated.
_AAGUID = 3;
// This is the persistent key limit:
// - When adding a (persistent) key above this message, make sure its value is smaller than
// NUM_PERSISTENT_KEYS.
// - When adding a (non-persistent) key below this message, make sure its value is bigger or
// equal than NUM_PERSISTENT_KEYS.
/// Reserved for future credential-related objects.
///
/// In particular, additional credentials could be added there by reducing the lower bound of
/// the credential range below as well as the upper bound of this range in a similar manner.
_RESERVED_CREDENTIALS = 1000..1700;
/// The credentials.
///
/// Depending on `Customization::max_supported_resident_keys()`, only a prefix of those keys is used.
/// Each board may configure `Customization::max_supported_resident_keys()` depending on the
/// storage size.
CREDENTIALS = 1700..2000;
/// Storage for the serialized large blob array.
///
/// The stored large blob can be too big for one key, so it has to be sharded.
LARGE_BLOB_SHARDS = 2000..2004;
/// If this entry exists and is empty, alwaysUv is enabled.
ALWAYS_UV = 2038;
/// If this entry exists and is empty, enterprise attestation is enabled.
ENTERPRISE_ATTESTATION = 2039;
/// If this entry exists and is empty, the PIN needs to be changed.
FORCE_PIN_CHANGE = 2040;
/// The secret of the CredRandom feature.
CRED_RANDOM_SECRET = 2041;
/// List of RP IDs allowed to read the minimum PIN length.
MIN_PIN_LENGTH_RP_IDS = 2042;
/// The minimum PIN length.
///
/// If the entry is absent, the minimum PIN length is `Customization::default_min_pin_length()`.
MIN_PIN_LENGTH = 2043;
/// The number of PIN retries.
///
/// If the entry is absent, the number of PIN retries is `Customization::max_pin_retries()`.
PIN_RETRIES = 2044;
/// The PIN hash and length.
///
/// If the entry is absent, there is no PIN set. The first byte represents
/// the length, the following are an array with the hash.
PIN_PROPERTIES = 2045;
/// Reserved for the key store implementation of the environment.
_RESERVED_KEY_STORE = 2046;
/// The global signature counter.
///
/// If the entry is absent, the counter is 0.
GLOBAL_SIGNATURE_COUNTER = 2047;
}
#[cfg(test)]
mod test {
use super::*;
use crate::api::customization::Customization;
use crate::env::test::TestEnv;
use crate::env::Env;
#[test]
fn enough_credentials() {
let env = TestEnv::default();
assert!(
env.customization().max_supported_resident_keys()
<= CREDENTIALS.end - CREDENTIALS.start
);
}
#[test]
fn keys_are_disjoint() {
// Check that keys are in the range.
for keys in ALL_KEYS {
assert!(KEY_RANGE.start <= keys.start && keys.end <= KEY_RANGE.end);
}
// Check that keys are assigned at most once, essentially partitioning the range.
for key in KEY_RANGE {
assert!(ALL_KEYS.iter().filter(|keys| keys.contains(&key)).count() <= 1);
}
}
}

View File

@@ -0,0 +1,275 @@
// Copyright 2021 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::api::clock::Clock;
use crate::ctap::client_pin::PinPermission;
use crate::ctap::status_code::Ctap2StatusCode;
use crate::env::Env;
use alloc::string::String;
use crypto::sha256::Sha256;
use crypto::Hash256;
/// Timeout for auth tokens.
///
/// This usage time limit is correct for USB, BLE, and internal.
/// NFC only allows 19.8 seconds.
/// TODO(#15) multiplex over transports, add NFC
const INITIAL_USAGE_TIME_LIMIT_MS: usize = 30000;
/// Implements pinUvAuthToken state from section 6.5.2.1.
///
/// The userPresent flag is omitted as the only way to set it to true is
/// built-in user verification. Therefore, we never cache user presence.
///
/// This implementation does not use a rolling timer.
pub struct PinUvAuthTokenState<E: Env> {
// Relies on the fact that all permissions are represented by powers of two.
permissions_set: u8,
permissions_rp_id: Option<String>,
usage_timer: <E::Clock as Clock>::Timer,
user_verified: bool,
in_use: bool,
}
impl<E: Env> PinUvAuthTokenState<E> {
/// Creates a pinUvAuthToken state without permissions.
pub fn new() -> Self {
PinUvAuthTokenState {
permissions_set: 0,
permissions_rp_id: None,
usage_timer: <E::Clock as Clock>::Timer::default(),
user_verified: false,
in_use: false,
}
}
/// Returns whether the pinUvAuthToken is active.
pub fn is_in_use(&self) -> bool {
self.in_use
}
/// Checks if the permission is granted.
pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> {
if permission as u8 & self.permissions_set != 0 {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
/// Checks if there is no associated permissions RPID.
pub fn has_no_permissions_rp_id(&self) -> Result<(), Ctap2StatusCode> {
if self.permissions_rp_id.is_some() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
Ok(())
}
/// Checks if the permissions RPID is associated.
pub fn has_permissions_rp_id(&self, rp_id: &str) -> Result<(), Ctap2StatusCode> {
match &self.permissions_rp_id {
Some(p) if rp_id == p => Ok(()),
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
}
}
/// Checks if the permissions RPID's association matches the hash.
pub fn has_permissions_rp_id_hash(&self, rp_id_hash: &[u8]) -> Result<(), Ctap2StatusCode> {
match &self.permissions_rp_id {
Some(p) if rp_id_hash == Sha256::hash(p.as_bytes()) => Ok(()),
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
}
}
/// Sets the permissions, represented as bits in a byte.
pub fn set_permissions(&mut self, permissions: u8) {
self.permissions_set = permissions;
}
/// Sets the permissions RPID.
pub fn set_permissions_rp_id(&mut self, permissions_rp_id: Option<String>) {
self.permissions_rp_id = permissions_rp_id;
}
/// Sets the default permissions.
///
/// Allows MakeCredential and GetAssertion, without specifying a RP ID.
pub fn set_default_permissions(&mut self) {
self.set_permissions(0x03);
self.set_permissions_rp_id(None);
}
/// Starts the timer for pinUvAuthToken usage.
pub fn begin_using_pin_uv_auth_token(&mut self, env: &mut E) {
self.user_verified = true;
self.usage_timer = env.clock().make_timer(INITIAL_USAGE_TIME_LIMIT_MS);
self.in_use = true;
}
/// Updates the usage timer, and disables the pinUvAuthToken on timeout.
pub fn pin_uv_auth_token_usage_timer_observer(&mut self, env: &mut E) {
if !self.in_use {
return;
}
if env.clock().is_elapsed(&self.usage_timer) {
self.stop_using_pin_uv_auth_token();
}
}
/// Returns whether the user is verified.
pub fn get_user_verified_flag_value(&self) -> bool {
self.in_use && self.user_verified
}
/// Consumes the user verification.
pub fn clear_user_verified_flag(&mut self) {
self.user_verified = false;
}
/// Clears all permissions except Large Blob Write.
pub fn clear_pin_uv_auth_token_permissions_except_lbw(&mut self) {
self.permissions_set &= PinPermission::LargeBlobWrite as u8;
}
/// Resets to the initial state.
pub fn stop_using_pin_uv_auth_token(&mut self) {
self.permissions_rp_id = None;
self.permissions_set = 0;
self.usage_timer = <E::Clock as Clock>::Timer::default();
self.user_verified = false;
self.in_use = false;
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
use enum_iterator::IntoEnumIterator;
#[test]
fn test_observer() {
let mut env = TestEnv::default();
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
token_state.begin_using_pin_uv_auth_token(&mut env);
assert!(token_state.is_in_use());
env.clock().advance(100);
token_state.pin_uv_auth_token_usage_timer_observer(&mut env);
assert!(token_state.is_in_use());
env.clock().advance(INITIAL_USAGE_TIME_LIMIT_MS);
token_state.pin_uv_auth_token_usage_timer_observer(&mut env);
assert!(!token_state.is_in_use());
}
#[test]
fn test_stop() {
let mut env = TestEnv::default();
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
token_state.begin_using_pin_uv_auth_token(&mut env);
assert!(token_state.is_in_use());
token_state.stop_using_pin_uv_auth_token();
assert!(!token_state.is_in_use());
}
#[test]
fn test_permissions() {
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
token_state.set_permissions(0xFF);
for permission in PinPermission::into_enum_iter() {
assert_eq!(token_state.has_permission(permission), Ok(()));
}
token_state.clear_pin_uv_auth_token_permissions_except_lbw();
assert_eq!(
token_state.has_permission(PinPermission::CredentialManagement),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permission(PinPermission::LargeBlobWrite),
Ok(())
);
token_state.stop_using_pin_uv_auth_token();
for permission in PinPermission::into_enum_iter() {
assert_eq!(
token_state.has_permission(permission),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}
#[test]
fn test_permissions_rp_id_none() {
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
let example_hash = Sha256::hash(b"example.com");
token_state.set_permissions_rp_id(None);
assert_eq!(token_state.has_no_permissions_rp_id(), Ok(()));
assert_eq!(
token_state.has_permissions_rp_id("example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_permissions_rp_id_some() {
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
let example_hash = Sha256::hash(b"example.com");
token_state.set_permissions_rp_id(Some(String::from("example.com")));
assert_eq!(
token_state.has_no_permissions_rp_id(),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(token_state.has_permissions_rp_id("example.com"), Ok(()));
assert_eq!(
token_state.has_permissions_rp_id("another.example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Ok(())
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&[0x1D; 32]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
token_state.stop_using_pin_uv_auth_token();
assert_eq!(
token_state.has_permissions_rp_id("example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
token_state.has_permissions_rp_id_hash(&example_hash),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_user_verified_flag() {
let mut env = TestEnv::default();
let mut token_state = PinUvAuthTokenState::<TestEnv>::new();
assert!(!token_state.get_user_verified_flag_value());
token_state.begin_using_pin_uv_auth_token(&mut env);
assert!(token_state.get_user_verified_flag_value());
token_state.clear_user_verified_flag();
assert!(!token_state.get_user_verified_flag_value());
token_state.begin_using_pin_uv_auth_token(&mut env);
assert!(token_state.get_user_verified_flag_value());
token_state.stop_using_pin_uv_auth_token();
assert!(!token_state.get_user_verified_flag_value());
}
}

View File

@@ -0,0 +1,137 @@
// Copyright 2019-2021 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 super::TOUCH_TIMEOUT_MS;
use crate::api::clock::Clock;
use crate::env::Env;
const U2F_UP_PROMPT_TIMEOUT_MS: usize = 10000;
pub struct U2fUserPresenceState<E: Env> {
/// If user presence was recently requested, its timeout is saved here.
needs_up: <E::Clock as Clock>::Timer,
/// Button touch timeouts, while user presence is requested, are saved here.
has_up: <E::Clock as Clock>::Timer,
}
impl<E: Env> U2fUserPresenceState<E> {
pub fn new() -> U2fUserPresenceState<E> {
U2fUserPresenceState {
needs_up: <E::Clock as Clock>::Timer::default(),
has_up: <E::Clock as Clock>::Timer::default(),
}
}
/// Allows consuming user presence until timeout, if it was needed.
///
/// If user presence was not requested, granting user presence does nothing.
pub fn grant_up(&mut self, env: &mut E) {
if !env.clock().is_elapsed(&self.needs_up) {
self.needs_up = <E::Clock as Clock>::Timer::default();
self.has_up = env.clock().make_timer(TOUCH_TIMEOUT_MS);
}
}
/// Returns whether user presence was granted within the timeout and not yet consumed.
pub fn consume_up(&mut self, env: &mut E) -> bool {
if !env.clock().is_elapsed(&self.has_up) {
self.has_up = <E::Clock as Clock>::Timer::default();
true
} else {
self.needs_up = env.clock().make_timer(U2F_UP_PROMPT_TIMEOUT_MS);
false
}
}
/// Returns whether user presence was requested.
///
/// This function doesn't represent interaction with the environment, and does not change the
/// state, i.e. neither grants nor consumes user presence.
pub fn is_up_needed(&mut self, env: &mut E) -> bool {
!env.clock().is_elapsed(&self.needs_up)
}
}
#[cfg(feature = "with_ctap1")]
#[cfg(test)]
mod test {
use super::*;
use crate::env::test::TestEnv;
fn big_positive() -> usize {
1000000
}
fn grant_up_when_needed(env: &mut TestEnv) {
let mut u2f_state = U2fUserPresenceState::new();
assert!(!u2f_state.consume_up(env));
assert!(u2f_state.is_up_needed(env));
u2f_state.grant_up(env);
assert!(u2f_state.consume_up(env));
assert!(!u2f_state.consume_up(env));
}
fn need_up_timeout(env: &mut TestEnv) {
let mut u2f_state = U2fUserPresenceState::new();
assert!(!u2f_state.consume_up(env));
assert!(u2f_state.is_up_needed(env));
env.clock().advance(U2F_UP_PROMPT_TIMEOUT_MS);
// The timeout excludes equality, so it should be over at this instant.
assert!(!u2f_state.is_up_needed(env));
}
fn grant_up_timeout(env: &mut TestEnv) {
let mut u2f_state = U2fUserPresenceState::new();
assert!(!u2f_state.consume_up(env));
assert!(u2f_state.is_up_needed(env));
u2f_state.grant_up(env);
env.clock().advance(TOUCH_TIMEOUT_MS);
// The timeout excludes equality, so it should be over at this instant.
assert!(!u2f_state.consume_up(env));
}
#[test]
fn test_grant_up_timeout() {
let mut env = TestEnv::default();
grant_up_timeout(&mut env);
env.clock().advance(big_positive());
grant_up_timeout(&mut env);
}
#[test]
fn test_need_up_timeout() {
let mut env = TestEnv::default();
need_up_timeout(&mut env);
env.clock().advance(big_positive());
need_up_timeout(&mut env);
}
#[test]
fn test_grant_up_when_needed() {
let mut env = TestEnv::default();
grant_up_when_needed(&mut env);
env.clock().advance(big_positive());
grant_up_when_needed(&mut env);
}
#[test]
fn test_grant_up_without_need() {
let mut env = TestEnv::default();
let mut u2f_state = U2fUserPresenceState::new();
u2f_state.grant_up(&mut env);
assert!(!u2f_state.is_up_needed(&mut env));
assert!(!u2f_state.consume_up(&mut env));
}
}

View File

@@ -0,0 +1,153 @@
// Copyright 2022 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::ctap::hid::{
CtapHid, CtapHidCommand, CtapHidError, HidPacket, HidPacketIterator, Message,
};
use crate::ctap::{Channel, CtapState};
use crate::env::Env;
/// Implements the non-standard command processing for HID.
///
/// Outside of the pure HID commands like INIT, only PING and CBOR commands are allowed.
pub struct VendorHid<E: Env> {
hid: CtapHid<E>,
}
impl<E: Env> Default for VendorHid<E> {
/// Instantiates a HID handler for CTAP1, CTAP2 and Wink.
fn default() -> Self {
let hid = CtapHid::<E>::new(CtapHid::<E>::CAPABILITY_CBOR | CtapHid::<E>::CAPABILITY_NMSG);
VendorHid { hid }
}
}
impl<E: Env> VendorHid<E> {
/// Processes an incoming USB HID packet, and returns an iterator for all outgoing packets.
pub fn process_hid_packet(
&mut self,
env: &mut E,
packet: &HidPacket,
ctap_state: &mut CtapState<E>,
) -> HidPacketIterator {
if let Some(message) = self.hid.parse_packet(env, packet) {
let processed_message = self.process_message(env, message, ctap_state);
debug_ctap!(
env,
"Sending message through the second usage page: {:02x?}",
processed_message
);
CtapHid::<E>::split_message(processed_message)
} else {
HidPacketIterator::none()
}
}
/// Processes a message's commands that affect the protocol outside HID.
pub fn process_message(
&mut self,
env: &mut E,
message: Message,
ctap_state: &mut CtapState<E>,
) -> Message {
let cid = message.cid;
match message.cmd {
// There are no custom CTAP1 commands.
CtapHidCommand::Msg => CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd),
// The CTAP2 processing function multiplexes internally.
CtapHidCommand::Cbor => {
let response =
ctap_state.process_command(env, &message.payload, Channel::VendorHid(cid));
Message {
cid,
cmd: CtapHidCommand::Cbor,
payload: response,
}
}
// Call Wink over the main HID.
CtapHidCommand::Wink => CtapHid::<E>::error_message(cid, CtapHidError::InvalidCmd),
// All other commands have already been processed, keep them as is.
_ => message,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ctap::hid::ChannelID;
use crate::env::test::TestEnv;
fn new_initialized() -> (VendorHid<TestEnv>, ChannelID) {
let (hid, cid) = CtapHid::new_initialized();
(VendorHid::<TestEnv> { hid }, cid)
}
#[test]
fn test_process_hid_packet() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut vendor_hid, cid) = new_initialized();
let mut ping_packet = [0x00; 64];
ping_packet[..4].copy_from_slice(&cid);
ping_packet[4..9].copy_from_slice(&[0x81, 0x00, 0x02, 0x99, 0x99]);
let mut response = vendor_hid.process_hid_packet(&mut env, &ping_packet, &mut ctap_state);
assert_eq!(response.next(), Some(ping_packet));
assert_eq!(response.next(), None);
}
#[test]
fn test_process_hid_packet_empty() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut vendor_hid, cid) = new_initialized();
let mut cancel_packet = [0x00; 64];
cancel_packet[..4].copy_from_slice(&cid);
cancel_packet[4..7].copy_from_slice(&[0x91, 0x00, 0x00]);
let mut response = vendor_hid.process_hid_packet(&mut env, &cancel_packet, &mut ctap_state);
assert_eq!(response.next(), None);
}
#[test]
fn test_blocked_commands() {
let mut env = TestEnv::default();
let mut ctap_state = CtapState::<TestEnv>::new(&mut env);
let (mut vendor_hid, cid) = new_initialized();
// Usually longer, but we don't parse them anyway.
let mut msg_packet = [0x00; 64];
msg_packet[..4].copy_from_slice(&cid);
msg_packet[4..7].copy_from_slice(&[0x83, 0x00, 0x00]);
let mut wink_packet = [0x00; 64];
wink_packet[..4].copy_from_slice(&cid);
wink_packet[4..7].copy_from_slice(&[0x88, 0x00, 0x00]);
let mut error_packet = [0x00; 64];
error_packet[..4].copy_from_slice(&cid);
error_packet[4..8].copy_from_slice(&[0xBF, 0x00, 0x01, 0x01]);
let mut response = vendor_hid.process_hid_packet(&mut env, &msg_packet, &mut ctap_state);
assert_eq!(response.next(), Some(error_packet));
assert_eq!(response.next(), None);
let mut response = vendor_hid.process_hid_packet(&mut env, &wink_packet, &mut ctap_state);
assert_eq!(response.next(), Some(error_packet));
assert_eq!(response.next(), None);
}
}