Initial commit

This commit is contained in:
Jean-Michel Picod
2020-01-28 15:09:10 +01:00
commit f91d2fd3db
90 changed files with 31123 additions and 0 deletions

483
src/ctap/command.rs Normal file
View File

@@ -0,0 +1,483 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::data_formats::{
ok_or_missing, read_array, read_byte_string, read_integer, read_map, read_text_string,
read_unsigned, ClientPinSubCommand, CoseKey, Extensions, GetAssertionOptions,
MakeCredentialOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity,
PublicKeyCredentialType, PublicKeyCredentialUserEntity,
};
use super::status_code::Ctap2StatusCode;
use alloc::string::String;
use alloc::vec::Vec;
use core::convert::TryFrom;
// CTAP specification (version 20190130) section 6.1
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub enum Command {
AuthenticatorMakeCredential(AuthenticatorMakeCredentialParameters),
AuthenticatorGetAssertion(AuthenticatorGetAssertionParameters),
AuthenticatorGetInfo,
AuthenticatorClientPin(AuthenticatorClientPinParameters),
AuthenticatorReset,
AuthenticatorGetNextAssertion,
// TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts)
}
impl From<cbor::reader::DecoderError> for Ctap2StatusCode {
fn from(_: cbor::reader::DecoderError) -> Self {
Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR
}
}
// TODO: Remove this `allow(dead_code)` once the constants are used.
#[allow(dead_code)]
impl Command {
const AUTHENTICATOR_MAKE_CREDENTIAL: u8 = 0x01;
const AUTHENTICATOR_GET_ASSERTION: u8 = 0x02;
const AUTHENTICATOR_GET_INFO: u8 = 0x04;
const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06;
const AUTHENTICATOR_RESET: u8 = 0x07;
// TODO(kaczmarczyck) use or remove those constants
const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08;
const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09;
const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0;
const AUTHENTICATOR_SELECTION: u8 = 0xB0;
const AUTHENTICATOR_CONFIG: u8 = 0xC0;
const AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40;
const AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF;
pub fn deserialize(bytes: &[u8]) -> Result<Command, Ctap2StatusCode> {
if bytes.is_empty() {
// The error to return is not specified, missing parameter seems to fit best.
return Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER);
}
let command_value = bytes[0];
match command_value {
Command::AUTHENTICATOR_MAKE_CREDENTIAL => {
let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorMakeCredential(
AuthenticatorMakeCredentialParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_GET_ASSERTION => {
let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorGetAssertion(
AuthenticatorGetAssertionParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_GET_INFO => {
// Parameters are ignored.
Ok(Command::AuthenticatorGetInfo)
}
Command::AUTHENTICATOR_CLIENT_PIN => {
let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorClientPin(
AuthenticatorClientPinParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_RESET => {
// Parameters are ignored.
Ok(Command::AuthenticatorReset)
}
Command::AUTHENTICATOR_GET_NEXT_ASSERTION => {
// Parameters are ignored.
Ok(Command::AuthenticatorGetNextAssertion)
}
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND),
}
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorMakeCredentialParameters {
pub client_data_hash: Vec<u8>,
pub rp: PublicKeyCredentialRpEntity,
pub user: PublicKeyCredentialUserEntity,
pub pub_key_cred_params: Vec<(PublicKeyCredentialType, i64)>,
pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
pub extensions: Option<Extensions>,
// Even though options are optional, we can use the default if not present.
pub options: MakeCredentialOptions,
pub pin_uv_auth_param: Option<Vec<u8>>,
pub pin_uv_auth_protocol: Option<u64>,
}
impl TryFrom<cbor::Value> for AuthenticatorMakeCredentialParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let param_map = read_map(&cbor_value)?;
let client_data_hash = read_byte_string(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
let rp = PublicKeyCredentialRpEntity::try_from(ok_or_missing(
param_map.get(&cbor_unsigned!(2)),
)?)?;
let user = PublicKeyCredentialUserEntity::try_from(ok_or_missing(
param_map.get(&cbor_unsigned!(3)),
)?)?;
let cred_param_vec = read_array(ok_or_missing(param_map.get(&cbor_unsigned!(4)))?)?;
let mut pub_key_cred_params = vec![];
for cred_param_map_value in cred_param_vec {
let cred_param_map = read_map(cred_param_map_value)?;
let cred_type = PublicKeyCredentialType::try_from(ok_or_missing(
cred_param_map.get(&cbor_text!("type")),
)?)?;
let alg = read_integer(ok_or_missing(cred_param_map.get(&cbor_text!("alg")))?)?;
pub_key_cred_params.push((cred_type, alg));
}
let exclude_list = match param_map.get(&cbor_unsigned!(5)) {
Some(entry) => {
let exclude_list_vec = read_array(entry)?;
let mut exclude_list = vec![];
for exclude_list_value in exclude_list_vec {
exclude_list.push(PublicKeyCredentialDescriptor::try_from(exclude_list_value)?);
}
Some(exclude_list)
}
None => None,
};
let extensions = param_map
.get(&cbor_unsigned!(6))
.map(Extensions::try_from)
.transpose()?;
let options = match param_map.get(&cbor_unsigned!(7)) {
Some(entry) => MakeCredentialOptions::try_from(entry)?,
None => MakeCredentialOptions {
rk: false,
uv: false,
},
};
let pin_uv_auth_param = param_map
.get(&cbor_unsigned!(8))
.map(read_byte_string)
.transpose()?;
let pin_uv_auth_protocol = param_map
.get(&cbor_unsigned!(9))
.map(read_unsigned)
.transpose()?;
Ok(AuthenticatorMakeCredentialParameters {
client_data_hash,
rp,
user,
pub_key_cred_params,
exclude_list,
extensions,
options,
pin_uv_auth_param,
pin_uv_auth_protocol,
})
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorGetAssertionParameters {
pub rp_id: String,
pub client_data_hash: Vec<u8>,
pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
pub extensions: Option<Extensions>,
// Even though options are optional, we can use the default if not present.
pub options: GetAssertionOptions,
pub pin_uv_auth_param: Option<Vec<u8>>,
pub pin_uv_auth_protocol: Option<u64>,
}
impl TryFrom<cbor::Value> for AuthenticatorGetAssertionParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let param_map = read_map(&cbor_value)?;
let rp_id = read_text_string(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
let client_data_hash = read_byte_string(ok_or_missing(param_map.get(&cbor_unsigned!(2)))?)?;
let allow_list = match param_map.get(&cbor_unsigned!(3)) {
Some(entry) => {
let allow_list_vec = read_array(entry)?;
let mut allow_list = vec![];
for allow_list_value in allow_list_vec {
allow_list.push(PublicKeyCredentialDescriptor::try_from(allow_list_value)?);
}
Some(allow_list)
}
None => None,
};
let extensions = param_map
.get(&cbor_unsigned!(4))
.map(Extensions::try_from)
.transpose()?;
let options = match param_map.get(&cbor_unsigned!(5)) {
Some(entry) => GetAssertionOptions::try_from(entry)?,
None => GetAssertionOptions {
up: true,
uv: false,
},
};
let pin_uv_auth_param = param_map
.get(&cbor_unsigned!(6))
.map(read_byte_string)
.transpose()?;
let pin_uv_auth_protocol = param_map
.get(&cbor_unsigned!(7))
.map(read_unsigned)
.transpose()?;
Ok(AuthenticatorGetAssertionParameters {
rp_id,
client_data_hash,
allow_list,
extensions,
options,
pin_uv_auth_param,
pin_uv_auth_protocol,
})
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorClientPinParameters {
pub pin_protocol: u64,
pub sub_command: ClientPinSubCommand,
pub key_agreement: Option<CoseKey>,
pub pin_auth: Option<Vec<u8>>,
pub new_pin_enc: Option<Vec<u8>>,
pub pin_hash_enc: Option<Vec<u8>>,
}
impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let param_map = read_map(&cbor_value)?;
let pin_protocol = read_unsigned(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
let sub_command =
ClientPinSubCommand::try_from(ok_or_missing(param_map.get(&cbor_unsigned!(2)))?)?;
let key_agreement = param_map
.get(&cbor_unsigned!(3))
.map(read_map)
.transpose()?
.map(|x| CoseKey(x.clone()));
let pin_auth = param_map
.get(&cbor_unsigned!(4))
.map(read_byte_string)
.transpose()?;
let new_pin_enc = param_map
.get(&cbor_unsigned!(5))
.map(read_byte_string)
.transpose()?;
let pin_hash_enc = param_map
.get(&cbor_unsigned!(6))
.map(read_byte_string)
.transpose()?;
Ok(AuthenticatorClientPinParameters {
pin_protocol,
sub_command,
key_agreement,
pin_auth,
new_pin_enc,
pin_hash_enc,
})
}
}
#[cfg(test)]
mod test {
use super::super::data_formats::{
AuthenticatorTransport, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
};
use super::*;
use alloc::collections::BTreeMap;
#[test]
fn test_from_cbor_make_credential_parameters() {
let cbor_value = cbor_map! {
1 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
2 => cbor_map! {
"id" => "example.com",
"name" => "Example",
"icon" => "example.com/icon.png",
},
3 => cbor_map! {
"id" => vec![0x1D, 0x1D, 0x1D, 0x1D],
"name" => "foo",
"displayName" => "bar",
"icon" => "example.com/foo/icon.png",
},
4 => cbor_array![ cbor_map! {
"type" => "public-key",
"alg" => -7
} ],
5 => cbor_array![],
8 => vec![0x12, 0x34],
9 => 1,
};
let returned_make_credential_parameters =
AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap();
let client_data_hash = vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
0x0E, 0x0F,
];
let rp = PublicKeyCredentialRpEntity {
rp_id: "example.com".to_string(),
rp_name: Some("Example".to_string()),
rp_icon: Some("example.com/icon.png".to_string()),
};
let user = PublicKeyCredentialUserEntity {
user_id: vec![0x1D, 0x1D, 0x1D, 0x1D],
user_name: Some("foo".to_string()),
user_display_name: Some("bar".to_string()),
user_icon: Some("example.com/foo/icon.png".to_string()),
};
let pub_key_cred_param = (PublicKeyCredentialType::PublicKey, -7);
let options = MakeCredentialOptions {
rk: false,
uv: false,
};
let expected_make_credential_parameters = AuthenticatorMakeCredentialParameters {
client_data_hash,
rp,
user,
pub_key_cred_params: vec![pub_key_cred_param],
exclude_list: Some(vec![]),
extensions: None,
options,
pin_uv_auth_param: Some(vec![0x12, 0x34]),
pin_uv_auth_protocol: Some(1),
};
assert_eq!(
returned_make_credential_parameters,
expected_make_credential_parameters
);
}
#[test]
fn test_from_cbor_get_assertion_parameters() {
let cbor_value = cbor_map! {
1 => "example.com",
2 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
3 => cbor_array![ cbor_map! {
"type" => "public-key",
"id" => vec![0x2D, 0x2D, 0x2D, 0x2D],
"transports" => cbor_array!["usb"],
} ],
6 => vec![0x12, 0x34],
7 => 1,
};
let returned_get_assertion_parameters =
AuthenticatorGetAssertionParameters::try_from(cbor_value).unwrap();
let rp_id = "example.com".to_string();
let client_data_hash = vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
0x0E, 0x0F,
];
let pub_key_cred_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x2D, 0x2D, 0x2D, 0x2D],
transports: Some(vec![AuthenticatorTransport::Usb]),
};
let options = GetAssertionOptions {
up: true,
uv: false,
};
let expected_get_assertion_parameters = AuthenticatorGetAssertionParameters {
rp_id,
client_data_hash,
allow_list: Some(vec![pub_key_cred_descriptor]),
extensions: None,
options,
pin_uv_auth_param: Some(vec![0x12, 0x34]),
pin_uv_auth_protocol: Some(1),
};
assert_eq!(
returned_get_assertion_parameters,
expected_get_assertion_parameters
);
}
#[test]
fn test_from_cbor_client_pin_parameters() {
let cbor_value = cbor_map! {
1 => 1,
2 => ClientPinSubCommand::GetPinRetries,
3 => cbor_map!{},
4 => vec! [0xBB],
5 => vec! [0xCC],
6 => vec! [0xDD],
};
let returned_pin_protocol_parameters =
AuthenticatorClientPinParameters::try_from(cbor_value).unwrap();
let expected_pin_protocol_parameters = AuthenticatorClientPinParameters {
pin_protocol: 1,
sub_command: ClientPinSubCommand::GetPinRetries,
key_agreement: Some(CoseKey(BTreeMap::new())),
pin_auth: Some(vec![0xBB]),
new_pin_enc: Some(vec![0xCC]),
pin_hash_enc: Some(vec![0xDD]),
};
assert_eq!(
returned_pin_protocol_parameters,
expected_pin_protocol_parameters
);
}
#[test]
fn test_deserialize_get_info() {
let cbor_bytes = [Command::AUTHENTICATOR_GET_INFO];
let command = Command::deserialize(&cbor_bytes);
assert_eq!(command, Ok(Command::AuthenticatorGetInfo));
}
#[test]
fn test_deserialize_reset() {
// Adding some random bytes to see if they are ignored.
let cbor_bytes = [Command::AUTHENTICATOR_RESET, 0xAB, 0xCD, 0xEF];
let command = Command::deserialize(&cbor_bytes);
assert_eq!(command, Ok(Command::AuthenticatorReset));
}
#[test]
fn test_deserialize_get_next_assertion() {
let cbor_bytes = [Command::AUTHENTICATOR_GET_NEXT_ASSERTION];
let command = Command::deserialize(&cbor_bytes);
assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion));
}
}