adds the new command AuthenticatorLargeBlobs

This commit is contained in:
Fabian Kaczmarczyck
2021-01-06 13:39:59 +01:00
parent d87d35847a
commit b2c8c5a128
8 changed files with 914 additions and 9 deletions

View File

@@ -22,6 +22,7 @@ use super::data_formats::{
}; };
use super::key_material; use super::key_material;
use super::status_code::Ctap2StatusCode; use super::status_code::Ctap2StatusCode;
use super::storage::MAX_LARGE_BLOB_ARRAY_SIZE;
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use arrayref::array_ref; use arrayref::array_ref;
@@ -33,6 +34,9 @@ use core::convert::TryFrom;
// You might also want to set the max credential size in process_get_info then. // You might also want to set the max credential size in process_get_info then.
pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option<usize> = None; pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option<usize> = None;
// This constant is a consequence of the structure of messages.
const MIN_LARGE_BLOB_LEN: usize = 17;
// CTAP specification (version 20190130) section 6.1 // CTAP specification (version 20190130) section 6.1
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub enum Command { pub enum Command {
@@ -44,8 +48,8 @@ pub enum Command {
AuthenticatorGetNextAssertion, AuthenticatorGetNextAssertion,
AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters), AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters),
AuthenticatorSelection, AuthenticatorSelection,
AuthenticatorLargeBlobs(AuthenticatorLargeBlobsParameters),
AuthenticatorConfig(AuthenticatorConfigParameters), AuthenticatorConfig(AuthenticatorConfigParameters),
// TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts)
// Vendor specific commands // Vendor specific commands
AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters),
} }
@@ -56,8 +60,6 @@ impl From<cbor::reader::DecoderError> for Ctap2StatusCode {
} }
} }
// TODO: Remove this `allow(dead_code)` once the constants are used.
#[allow(dead_code)]
impl Command { impl Command {
const AUTHENTICATOR_MAKE_CREDENTIAL: u8 = 0x01; const AUTHENTICATOR_MAKE_CREDENTIAL: u8 = 0x01;
const AUTHENTICATOR_GET_ASSERTION: u8 = 0x02; const AUTHENTICATOR_GET_ASSERTION: u8 = 0x02;
@@ -65,8 +67,8 @@ impl Command {
const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06; const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06;
const AUTHENTICATOR_RESET: u8 = 0x07; const AUTHENTICATOR_RESET: u8 = 0x07;
const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08;
// TODO(kaczmarczyck) use or remove those constants // Implement Bio Enrollment when your hardware supports biometrics.
const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; const _AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09;
const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0x0A; const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0x0A;
const AUTHENTICATOR_SELECTION: u8 = 0x0B; const AUTHENTICATOR_SELECTION: u8 = 0x0B;
const AUTHENTICATOR_LARGE_BLOBS: u8 = 0x0C; const AUTHENTICATOR_LARGE_BLOBS: u8 = 0x0C;
@@ -123,6 +125,12 @@ impl Command {
// Parameters are ignored. // Parameters are ignored.
Ok(Command::AuthenticatorSelection) Ok(Command::AuthenticatorSelection)
} }
Command::AUTHENTICATOR_LARGE_BLOBS => {
let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorLargeBlobs(
AuthenticatorLargeBlobsParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_CONFIG => { Command::AUTHENTICATOR_CONFIG => {
let decoded_cbor = cbor::read(&bytes[1..])?; let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorConfig( Ok(Command::AuthenticatorConfig(
@@ -351,6 +359,81 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
} }
} }
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorLargeBlobsParameters {
pub get: Option<usize>,
pub set: Option<Vec<u8>>,
pub offset: usize,
pub length: Option<usize>,
pub pin_uv_auth_param: Option<Vec<u8>>,
pub pin_uv_auth_protocol: Option<u64>,
}
impl TryFrom<cbor::Value> for AuthenticatorLargeBlobsParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
1 => get,
2 => set,
3 => offset,
4 => length,
5 => pin_uv_auth_param,
6 => pin_uv_auth_protocol,
} = extract_map(cbor_value)?;
}
// careful: some missing parameters here are CTAP1_ERR_INVALID_PARAMETER
let get = get.map(extract_unsigned).transpose()?.map(|u| u as usize);
let set = set.map(extract_byte_string).transpose()?;
let offset =
extract_unsigned(offset.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?)? as usize;
let length = length
.map(extract_unsigned)
.transpose()?
.map(|u| u as usize);
let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?;
let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?;
if get.is_none() && set.is_none() {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if get.is_some() && set.is_some() {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if get.is_some()
&& (length.is_some() || pin_uv_auth_param.is_some() || pin_uv_auth_protocol.is_some())
{
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if set.is_some() && offset == 0 {
match length {
None => return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
Some(len) if len > MAX_LARGE_BLOB_ARRAY_SIZE => {
return Err(Ctap2StatusCode::CTAP2_ERR_LARGE_BLOB_STORAGE_FULL)
}
Some(len) if len < MIN_LARGE_BLOB_LEN => {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
Some(_) => (),
}
}
if set.is_some() && offset != 0 && length.is_some() {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
Ok(AuthenticatorLargeBlobsParameters {
get,
set,
offset,
length,
pin_uv_auth_param,
pin_uv_auth_protocol,
})
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorConfigParameters { pub struct AuthenticatorConfigParameters {
pub sub_command: ConfigSubCommand, pub sub_command: ConfigSubCommand,
@@ -698,6 +781,149 @@ mod test {
assert_eq!(command, Ok(Command::AuthenticatorSelection)); assert_eq!(command, Ok(Command::AuthenticatorSelection));
} }
#[test]
fn test_from_cbor_large_blobs_parameters() {
// successful get
let cbor_value = cbor_map! {
1 => 2,
3 => 4,
};
let returned_large_blobs_parameters =
AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap();
let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters {
get: Some(2),
set: None,
offset: 4,
length: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
assert_eq!(
returned_large_blobs_parameters,
expected_large_blobs_parameters
);
// successful first set
let cbor_value = cbor_map! {
2 => vec! [0x5E],
3 => 0,
4 => MIN_LARGE_BLOB_LEN as u64,
5 => vec! [0xA9],
6 => 1,
};
let returned_large_blobs_parameters =
AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap();
let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(vec![0x5E]),
offset: 0,
length: Some(MIN_LARGE_BLOB_LEN),
pin_uv_auth_param: Some(vec![0xA9]),
pin_uv_auth_protocol: Some(1),
};
assert_eq!(
returned_large_blobs_parameters,
expected_large_blobs_parameters
);
// successful next set
let cbor_value = cbor_map! {
2 => vec! [0x5E],
3 => 1,
5 => vec! [0xA9],
6 => 1,
};
let returned_large_blobs_parameters =
AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap();
let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(vec![0x5E]),
offset: 1,
length: None,
pin_uv_auth_param: Some(vec![0xA9]),
pin_uv_auth_protocol: Some(1),
};
assert_eq!(
returned_large_blobs_parameters,
expected_large_blobs_parameters
);
// failing with neither get nor set
let cbor_value = cbor_map! {
3 => 4,
5 => vec! [0xA9],
6 => 1,
};
assert_eq!(
AuthenticatorLargeBlobsParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// failing with get and set
let cbor_value = cbor_map! {
1 => 2,
2 => vec! [0x5E],
3 => 4,
5 => vec! [0xA9],
6 => 1,
};
assert_eq!(
AuthenticatorLargeBlobsParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// failing with get and length
let cbor_value = cbor_map! {
1 => 2,
3 => 4,
4 => MIN_LARGE_BLOB_LEN as u64,
5 => vec! [0xA9],
6 => 1,
};
assert_eq!(
AuthenticatorLargeBlobsParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// failing with zero offset and no length present
let cbor_value = cbor_map! {
2 => vec! [0x5E],
3 => 0,
5 => vec! [0xA9],
6 => 1,
};
assert_eq!(
AuthenticatorLargeBlobsParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// failing with length smaller than minimum
let cbor_value = cbor_map! {
2 => vec! [0x5E],
3 => 0,
4 => MIN_LARGE_BLOB_LEN as u64 - 1,
5 => vec! [0xA9],
6 => 1,
};
assert_eq!(
AuthenticatorLargeBlobsParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// failing with non-zero offset and length present
let cbor_value = cbor_map! {
2 => vec! [0x5E],
3 => 4,
4 => MIN_LARGE_BLOB_LEN as u64,
5 => vec! [0xA9],
6 => 1,
};
assert_eq!(
AuthenticatorLargeBlobsParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
#[test] #[test]
fn test_vendor_configure() { fn test_vendor_configure() {
// Incomplete command // Incomplete command

View File

@@ -100,7 +100,7 @@ fn enumerate_credentials_response(
public_key: Some(public_key), public_key: Some(public_key),
total_credentials, total_credentials,
cred_protect: cred_protect_policy, cred_protect: cred_protect_policy,
// TODO(kaczmarczyck) add when largeBlobKey is implemented // TODO(kaczmarczyck) add when largeBlobKey extension is implemented
large_blob_key: None, large_blob_key: None,
..Default::default() ..Default::default()
}) })

422
src/ctap/large_blobs.rs Normal file
View File

@@ -0,0 +1,422 @@
// 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::check_pin_uv_auth_protocol;
use super::command::AuthenticatorLargeBlobsParameters;
use super::pin_protocol_v1::{PinPermission, PinProtocolV1};
use super::response::{AuthenticatorLargeBlobsResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore;
use alloc::vec;
use alloc::vec::Vec;
use byteorder::{ByteOrder, LittleEndian};
use crypto::sha256::Sha256;
use crypto::Hash256;
/// This is maximum message size supported by the authenticator. 1024 is the default.
/// Increasing this values can speed up commands with longer responses, but lead to
/// packets dropping or unexpected failures.
pub const MAX_MSG_SIZE: usize = 1024;
/// 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(
&mut self,
persistent_store: &mut PersistentStore,
pin_protocol_v1: &mut PinProtocolV1,
large_blobs_params: AuthenticatorLargeBlobsParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorLargeBlobsParameters {
get,
set,
offset,
length,
pin_uv_auth_param,
pin_uv_auth_protocol,
} = large_blobs_params;
const MAX_FRAGMENT_LENGTH: usize = MAX_MSG_SIZE - 64;
if let Some(get) = get {
if get > MAX_FRAGMENT_LENGTH {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
}
let config = persistent_store.get_large_blob_array(get, offset)?;
return Ok(ResponseData::AuthenticatorLargeBlobs(Some(
AuthenticatorLargeBlobsResponse { config },
)));
}
if let Some(mut set) = set {
if set.len() > MAX_FRAGMENT_LENGTH {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH);
}
if offset == 0 {
// Checks for offset and length are already done in command.
self.expected_length =
length.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
self.expected_next_offset = 0;
}
if offset != self.expected_next_offset {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ);
}
if persistent_store.pin_hash()?.is_some() {
let pin_uv_auth_param =
pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
// TODO(kaczmarczyck) Error codes for PIN protocol differ across commands.
// Change to Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED for None?
check_pin_uv_auth_protocol(pin_uv_auth_protocol)?;
pin_protocol_v1.has_permission(PinPermission::LargeBlobWrite)?;
let mut message = vec![0xFF; 32];
message.extend(&[0x0C, 0x00]);
let mut offset_bytes = [0u8; 4];
LittleEndian::write_u32(&mut offset_bytes, offset as u32);
message.extend(&offset_bytes);
message.extend(&Sha256::hash(set.as_slice()));
if !pin_protocol_v1.verify_pin_auth_token(&message, &pin_uv_auth_param) {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
}
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);
}
persistent_store.commit_large_blob_array(&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::*;
use crypto::rng256::ThreadRng256;
#[test]
fn test_process_command_get_empty() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
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 persistent_store,
&mut pin_protocol_v1,
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 rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
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 persistent_store,
&mut pin_protocol_v1,
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 persistent_store,
&mut pin_protocol_v1,
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 persistent_store,
&mut pin_protocol_v1,
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 rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
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 persistent_store,
&mut pin_protocol_v1,
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 persistent_store,
&mut pin_protocol_v1,
large_blobs_params,
);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ),
);
}
#[test]
fn test_process_command_commit_unexpected_length() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
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 persistent_store,
&mut pin_protocol_v1,
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 persistent_store,
&mut pin_protocol_v1,
large_blobs_params,
);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
);
}
#[test]
fn test_process_command_commit_unexpected_hash() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
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 persistent_store,
&mut pin_protocol_v1,
large_blobs_params,
);
assert_eq!(
large_blobs_response,
Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE),
);
}
#[test]
fn test_process_command_commit_with_pin() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
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]);
persistent_store.set_pin(&[0u8; 16], 4).unwrap();
let pin_uv_auth_param = Some(vec![
0x68, 0x0C, 0x3F, 0x6A, 0x62, 0x47, 0xE6, 0x7C, 0x23, 0x1F, 0x79, 0xE3, 0xDC, 0x6D,
0xC3, 0xDE,
]);
let large_blobs_params = AuthenticatorLargeBlobsParameters {
get: None,
set: Some(large_blob),
offset: 0,
length: Some(BLOB_LEN),
pin_uv_auth_param,
pin_uv_auth_protocol: Some(1),
};
let large_blobs_response = large_blobs.process_command(
&mut persistent_store,
&mut pin_protocol_v1,
large_blobs_params,
);
assert_eq!(
large_blobs_response,
Ok(ResponseData::AuthenticatorLargeBlobs(None))
);
}
}

View File

@@ -21,6 +21,7 @@ mod ctap1;
pub mod data_formats; pub mod data_formats;
pub mod hid; pub mod hid;
mod key_material; mod key_material;
mod large_blobs;
mod pin_protocol_v1; mod pin_protocol_v1;
pub mod response; pub mod response;
pub mod status_code; pub mod status_code;
@@ -41,6 +42,7 @@ use self::data_formats::{
SignatureAlgorithm, SignatureAlgorithm,
}; };
use self::hid::ChannelID; use self::hid::ChannelID;
use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE};
use self::pin_protocol_v1::{PinPermission, PinProtocolV1}; use self::pin_protocol_v1::{PinPermission, PinProtocolV1};
use self::response::{ use self::response::{
AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse,
@@ -293,6 +295,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(
pub u2f_up_state: U2fUserPresenceState, pub u2f_up_state: U2fUserPresenceState,
// The state initializes to Reset and its timeout, and never goes back to Reset. // The state initializes to Reset and its timeout, and never goes back to Reset.
stateful_command_permission: StatefulPermission, stateful_command_permission: StatefulPermission,
large_blobs: LargeBlobs,
} }
impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence>
@@ -318,6 +321,7 @@ where
Duration::from_ms(TOUCH_TIMEOUT_MS), Duration::from_ms(TOUCH_TIMEOUT_MS),
), ),
stateful_command_permission: StatefulPermission::new_reset(now), stateful_command_permission: StatefulPermission::new_reset(now),
large_blobs: LargeBlobs::new(),
} }
} }
@@ -484,12 +488,16 @@ where
) )
} }
Command::AuthenticatorSelection => self.process_selection(cid), Command::AuthenticatorSelection => self.process_selection(cid),
Command::AuthenticatorLargeBlobs(params) => self.large_blobs.process_command(
&mut self.persistent_store,
&mut self.pin_protocol_v1,
params,
),
Command::AuthenticatorConfig(params) => process_config( Command::AuthenticatorConfig(params) => process_config(
&mut self.persistent_store, &mut self.persistent_store,
&mut self.pin_protocol_v1, &mut self.pin_protocol_v1,
params, params,
), ),
// TODO(kaczmarczyck) implement FIDO 2.1 commands
// Vendor specific commands // Vendor specific commands
Command::AuthenticatorVendorConfigure(params) => { Command::AuthenticatorVendorConfigure(params) => {
self.process_vendor_configure(params, cid) self.process_vendor_configure(params, cid)
@@ -1026,7 +1034,7 @@ where
]), ]),
aaguid: self.persistent_store.aaguid()?, aaguid: self.persistent_store.aaguid()?,
options: Some(options_map), options: Some(options_map),
max_msg_size: Some(1024), max_msg_size: Some(MAX_MSG_SIZE as u64),
pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]),
max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64),
max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64),

View File

@@ -162,7 +162,7 @@ pub enum PinPermission {
GetAssertion = 0x02, GetAssertion = 0x02,
CredentialManagement = 0x04, CredentialManagement = 0x04,
BioEnrollment = 0x08, BioEnrollment = 0x08,
PlatformConfiguration = 0x10, LargeBlobWrite = 0x10,
AuthenticatorConfiguration = 0x20, AuthenticatorConfiguration = 0x20,
} }

View File

@@ -33,6 +33,7 @@ pub enum ResponseData {
AuthenticatorReset, AuthenticatorReset,
AuthenticatorCredentialManagement(Option<AuthenticatorCredentialManagementResponse>), AuthenticatorCredentialManagement(Option<AuthenticatorCredentialManagementResponse>),
AuthenticatorSelection, AuthenticatorSelection,
AuthenticatorLargeBlobs(Option<AuthenticatorLargeBlobsResponse>),
// TODO(kaczmarczyck) dummy, extend // TODO(kaczmarczyck) dummy, extend
AuthenticatorConfig, AuthenticatorConfig,
AuthenticatorVendor(AuthenticatorVendorResponse), AuthenticatorVendor(AuthenticatorVendorResponse),
@@ -49,6 +50,7 @@ impl From<ResponseData> for Option<cbor::Value> {
ResponseData::AuthenticatorReset => None, ResponseData::AuthenticatorReset => None,
ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()), ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorSelection => None,
ResponseData::AuthenticatorLargeBlobs(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorConfig => None, ResponseData::AuthenticatorConfig => None,
ResponseData::AuthenticatorVendor(data) => Some(data.into()), ResponseData::AuthenticatorVendor(data) => Some(data.into()),
} }
@@ -204,6 +206,22 @@ impl From<AuthenticatorClientPinResponse> for cbor::Value {
} }
} }
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
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(Default)] #[derive(Default)]
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
@@ -510,6 +528,23 @@ mod test {
assert_eq!(response_cbor, None); 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] #[test]
fn test_config_into_cbor() { fn test_config_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorConfig.into(); let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorConfig.into();

View File

@@ -28,6 +28,7 @@ use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
use arrayref::array_ref; use arrayref::array_ref;
use cbor::cbor_array_vec; use cbor::cbor_array_vec;
use core::cmp;
use core::convert::TryInto; use core::convert::TryInto;
use crypto::rng256::Rng256; use crypto::rng256::Rng256;
use persistent_store::StoreUpdate; use persistent_store::StoreUpdate;
@@ -59,6 +60,9 @@ const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec<String> = Vec::new();
// This constant is an attempt to limit storage requirements. If you don't set it to 0, // This constant is an attempt to limit storage requirements. If you don't set it to 0,
// the stored strings can still be unbounded, but that is true for all RP IDs. // the stored strings can still be unbounded, but that is true for all RP IDs.
pub const MAX_RP_IDS_LENGTH: usize = 8; pub const MAX_RP_IDS_LENGTH: usize = 8;
const SHARD_SIZE: usize = 128;
pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize =
SHARD_SIZE * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start);
/// Wrapper for master keys. /// Wrapper for master keys.
pub struct MasterKeys { pub struct MasterKeys {
@@ -467,6 +471,70 @@ impl PersistentStore {
)?) )?)
} }
/// Reads the byte vector stored as the serialized large blobs array.
///
/// If more data is requested than stored, return as many bytes as possible.
pub fn get_large_blob_array(
&self,
mut byte_count: usize,
mut offset: usize,
) -> Result<Vec<u8>, Ctap2StatusCode> {
if self.store.find(key::LARGE_BLOB_SHARDS.start)?.is_none() {
return Ok(vec![
0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5,
0x7a, 0x6d, 0x3c,
]);
}
let mut output = Vec::with_capacity(byte_count);
while byte_count > 0 {
let shard = offset / SHARD_SIZE;
let shard_offset = offset % SHARD_SIZE;
let shard_length = cmp::min(SHARD_SIZE - shard_offset, byte_count);
let shard_key = key::LARGE_BLOB_SHARDS.start + shard;
if !key::LARGE_BLOB_SHARDS.contains(&shard_key) {
// This request should have been caught at application level.
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
let shard_entry = self.store.find(shard_key)?.unwrap_or_default();
if shard_entry.len() < shard_offset + shard_length {
output.extend(&shard_entry[..]);
return Ok(output);
}
output.extend(&shard_entry[shard_offset..shard_offset + shard_length]);
offset += shard_length;
byte_count -= shard_length;
}
Ok(output)
}
/// Sets a byte vector as the serialized large blobs array.
pub fn commit_large_blob_array(
&mut self,
large_blob_array: &[u8],
) -> Result<(), Ctap2StatusCode> {
let mut large_blob_index = 0;
let mut shard_key = key::LARGE_BLOB_SHARDS.start;
while large_blob_index < large_blob_array.len() {
if !key::LARGE_BLOB_SHARDS.contains(&shard_key) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index);
self.store.insert(
shard_key,
&large_blob_array[large_blob_index..large_blob_index + shard_length],
)?;
large_blob_index += shard_length;
shard_key += 1;
}
// The length is not stored, so overwrite old entries explicitly.
for key in shard_key..key::LARGE_BLOB_SHARDS.end {
// Assuming the store optimizes out unnecessary writes.
self.store.remove(key)?;
}
Ok(())
}
/// Returns the attestation private key if defined. /// Returns the attestation private key if defined.
pub fn attestation_private_key( pub fn attestation_private_key(
&self, &self,
@@ -1144,6 +1212,147 @@ mod test {
assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids); assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids);
} }
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_max_large_blob_array_size() {
assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024);
}
#[test]
fn test_commit_get_large_blob_array_1_shard() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let large_blob_array = vec![0xC0; 1];
assert!(persistent_store
.commit_large_blob_array(&large_blob_array)
.is_ok());
let restored_large_blob_array = persistent_store.get_large_blob_array(1, 0).unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
let large_blob_array = vec![0xC0; SHARD_SIZE];
assert!(persistent_store
.commit_large_blob_array(&large_blob_array)
.is_ok());
let restored_large_blob_array = persistent_store
.get_large_blob_array(SHARD_SIZE, 0)
.unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
let restored_large_blob_array = persistent_store
.get_large_blob_array(SHARD_SIZE + 1, 0)
.unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
}
#[test]
fn test_commit_get_large_blob_array_2_shards() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let large_blob_array = vec![0xC0; SHARD_SIZE + 1];
assert!(persistent_store
.commit_large_blob_array(&large_blob_array)
.is_ok());
let restored_large_blob_array = persistent_store
.get_large_blob_array(SHARD_SIZE, 0)
.unwrap();
assert_eq!(
large_blob_array[..SHARD_SIZE],
restored_large_blob_array[..]
);
let restored_large_blob_array = persistent_store
.get_large_blob_array(SHARD_SIZE + 1, 0)
.unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
let large_blob_array = vec![0xC0; 2 * SHARD_SIZE];
assert!(persistent_store
.commit_large_blob_array(&large_blob_array)
.is_ok());
let restored_large_blob_array = persistent_store
.get_large_blob_array(2 * SHARD_SIZE, 0)
.unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
let restored_large_blob_array = persistent_store
.get_large_blob_array(2 * SHARD_SIZE + 1, 0)
.unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
}
#[test]
fn test_commit_get_large_blob_array_3_shards() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let mut large_blob_array = vec![0x11; SHARD_SIZE];
large_blob_array.extend([0x22; SHARD_SIZE].iter());
large_blob_array.extend([0x33; 1].iter());
assert!(persistent_store
.commit_large_blob_array(&large_blob_array)
.is_ok());
let restored_large_blob_array = persistent_store
.get_large_blob_array(2 * SHARD_SIZE + 1, 0)
.unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
let restored_large_blob_array = persistent_store
.get_large_blob_array(3 * SHARD_SIZE, 0)
.unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
let shard1 = persistent_store
.get_large_blob_array(SHARD_SIZE, 0)
.unwrap();
let shard2 = persistent_store
.get_large_blob_array(SHARD_SIZE, SHARD_SIZE)
.unwrap();
let shard3 = persistent_store
.get_large_blob_array(1, 2 * SHARD_SIZE)
.unwrap();
assert_eq!(large_blob_array[..SHARD_SIZE], shard1[..]);
assert_eq!(large_blob_array[SHARD_SIZE..2 * SHARD_SIZE], shard2[..]);
assert_eq!(large_blob_array[2 * SHARD_SIZE..], shard3[..]);
let shard12 = persistent_store
.get_large_blob_array(2, SHARD_SIZE - 1)
.unwrap();
let shard23 = persistent_store
.get_large_blob_array(2, 2 * SHARD_SIZE - 1)
.unwrap();
assert_eq!(vec![0x11, 0x22], shard12);
assert_eq!(vec![0x22, 0x33], shard23);
}
#[test]
fn test_commit_get_large_blob_array_overwrite() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let large_blob_array = vec![0x11; SHARD_SIZE + 1];
assert!(persistent_store
.commit_large_blob_array(&large_blob_array)
.is_ok());
let large_blob_array = vec![0x22; SHARD_SIZE];
assert!(persistent_store
.commit_large_blob_array(&large_blob_array)
.is_ok());
let restored_large_blob_array = persistent_store
.get_large_blob_array(SHARD_SIZE + 1, 0)
.unwrap();
assert_eq!(large_blob_array, restored_large_blob_array);
let restored_large_blob_array = persistent_store
.get_large_blob_array(1, SHARD_SIZE)
.unwrap();
assert_eq!(Vec::<u8>::new(), restored_large_blob_array);
assert!(persistent_store.commit_large_blob_array(&[]).is_ok());
let restored_large_blob_array = persistent_store
.get_large_blob_array(SHARD_SIZE + 1, 0)
.unwrap();
let empty_blob_array = vec![
0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5,
0x7a, 0x6d, 0x3c,
];
assert_eq!(empty_blob_array, restored_large_blob_array);
}
#[test] #[test]
fn test_global_signature_counter() { fn test_global_signature_counter() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};

View File

@@ -88,6 +88,11 @@ make_partition! {
/// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size. /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size.
CREDENTIALS = 1700..2000; 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..2016;
/// If this entry exists and equals 1, the PIN needs to be changed. /// If this entry exists and equals 1, the PIN needs to be changed.
FORCE_PIN_CHANGE = 2040; FORCE_PIN_CHANGE = 2040;