Merge branch 'master' into cred-protect

This commit is contained in:
kaczmarczyck
2020-06-04 14:10:12 +02:00
committed by GitHub
17 changed files with 668 additions and 229 deletions

View File

@@ -462,6 +462,9 @@ impl TryFrom<&cbor::Value> for CredentialProtectionPolicy {
}
// https://www.w3.org/TR/webauthn/#public-key-credential-source
//
// Note that we only use the WebAuthn definition as an example. This data-structure is not specified
// by FIDO. In particular we may choose how we serialize and deserialize it.
#[derive(Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
@@ -477,30 +480,40 @@ pub struct PublicKeyCredentialSource {
pub cred_protect_policy: Option<CredentialProtectionPolicy>,
}
// We serialize credentials for the persistent storage using CBOR maps. Each field of a credential
// is associated with a unique tag, implemented with a CBOR unsigned key.
enum PublicKeyCredentialSourceField {
CredentialId = 0,
PrivateKey = 1,
RpId = 2,
UserHandle = 3,
OtherUi = 4,
CredRandom = 5,
CredProtectPolicy = 6,
// When a field is removed, its tag should be reserved and not used for new fields. We document
// those reserved tags below.
// Reserved tags: none.
}
impl From<PublicKeyCredentialSourceField> for cbor::KeyType {
fn from(field: PublicKeyCredentialSourceField) -> cbor::KeyType {
(field as u64).into()
}
}
impl From<PublicKeyCredentialSource> for cbor::Value {
fn from(credential: PublicKeyCredentialSource) -> cbor::Value {
use PublicKeyCredentialSourceField::*;
let mut private_key = [0u8; 32];
credential.private_key.to_bytes(&mut private_key);
let other_ui = match credential.other_ui {
None => cbor_null!(),
Some(other_ui) => cbor_text!(other_ui),
};
let cred_random = match credential.cred_random {
None => cbor_null!(),
Some(cred_random) => cbor_bytes!(cred_random),
};
let cred_protect_policy = match credential.cred_protect_policy {
None => cbor_null!(),
Some(cred_protect_policy) => cbor_int!(cred_protect_policy as i64),
};
cbor_array! {
credential.credential_id,
private_key,
credential.rp_id,
credential.user_handle,
other_ui,
cred_random,
cred_protect_policy,
cbor_map_options! {
CredentialId => Some(credential.credential_id),
PrivateKey => Some(private_key.to_vec()),
RpId => Some(credential.rp_id),
UserHandle => Some(credential.user_handle),
OtherUi => credential.other_ui,
CredRandom => credential.cred_random
CredProtectPolicy => credential.cred_protect_policy.map(|p| p as i64)
}
}
}
@@ -508,34 +521,40 @@ impl From<PublicKeyCredentialSource> for cbor::Value {
impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<PublicKeyCredentialSource, Ctap2StatusCode> {
use cbor::{SimpleValue, Value};
let fields = read_array(&cbor_value)?;
if fields.len() != 7 {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR);
}
let credential_id = read_byte_string(&fields[0])?;
let private_key = read_byte_string(&fields[1])?;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
use PublicKeyCredentialSourceField::*;
let mut map = extract_map(cbor_value)?;
let credential_id = extract_byte_string(ok_or_missing(map.remove(&CredentialId.into()))?)?;
let private_key = extract_byte_string(ok_or_missing(map.remove(&PrivateKey.into()))?)?;
if private_key.len() != 32 {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR);
}
let private_key = ecdsa::SecKey::from_bytes(array_ref!(private_key, 0, 32))
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)?;
let rp_id = read_text_string(&fields[2])?;
let user_handle = read_byte_string(&fields[3])?;
let other_ui = match &fields[4] {
Value::Simple(SimpleValue::NullValue) => None,
cbor_value => Some(read_text_string(cbor_value)?),
};
let cred_random = match &fields[5] {
Value::Simple(SimpleValue::NullValue) => None,
cbor_value => Some(read_byte_string(cbor_value)?),
};
let cred_protect_policy = match &fields[6] {
Value::Simple(SimpleValue::NullValue) => None,
cbor_value => Some(CredentialProtectionPolicy::try_from(cbor_value)?),
};
let rp_id = extract_text_string(ok_or_missing(map.remove(&RpId.into()))?)?;
let user_handle = extract_byte_string(ok_or_missing(map.remove(&UserHandle.into()))?)?;
let other_ui = map
.remove(&OtherUi.into())
.map(extract_text_string)
.transpose()?;
let cred_random = map
.remove(&CredRandom.into())
.map(extract_byte_string)
.transpose()?;
let cred_protect_policy = map
.remove(&CredProtectPolicy.into())
.map(CredentialProtectionPolicy::try_from)
.transpose()?;
// We don't return whether there were unknown fields in the CBOR value. This means that
// deserialization is not injective. In particular deserialization is only an inverse of
// serialization at a given version of OpenSK. This is not a problem because:
// 1. When a field is deprecated, its tag is reserved and never reused in future versions,
// including to be reintroduced with the same semantics. In other words, removing a field
// is permanent.
// 2. OpenSK is never used with a more recent version of the storage. In particular, OpenSK
// is never rolled-back.
// As a consequence, the unknown fields are only reserved fields and don't need to be
// preserved.
Ok(PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id,
@@ -701,6 +720,13 @@ pub fn read_byte_string(cbor_value: &cbor::Value) -> Result<Vec<u8>, Ctap2Status
}
}
fn extract_byte_string(cbor_value: cbor::Value) -> Result<Vec<u8>, Ctap2StatusCode> {
match cbor_value {
cbor::Value::KeyValue(cbor::KeyType::ByteString(byte_string)) => Ok(byte_string),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
pub(super) fn read_text_string(cbor_value: &cbor::Value) -> Result<String, Ctap2StatusCode> {
match cbor_value {
cbor::Value::KeyValue(cbor::KeyType::TextString(text_string)) => {
@@ -710,6 +736,13 @@ pub(super) fn read_text_string(cbor_value: &cbor::Value) -> Result<String, Ctap2
}
}
fn extract_text_string(cbor_value: cbor::Value) -> Result<String, Ctap2StatusCode> {
match cbor_value {
cbor::Value::KeyValue(cbor::KeyType::TextString(text_string)) => Ok(text_string),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
pub(super) fn read_array(cbor_value: &cbor::Value) -> Result<&Vec<cbor::Value>, Ctap2StatusCode> {
match cbor_value {
cbor::Value::Array(array) => Ok(array),
@@ -726,6 +759,15 @@ pub(super) fn read_map(
}
}
fn extract_map(
cbor_value: cbor::Value,
) -> Result<BTreeMap<cbor::KeyType, cbor::Value>, Ctap2StatusCode> {
match cbor_value {
cbor::Value::Map(map) => Ok(map),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
pub(super) fn read_bool(cbor_value: &cbor::Value) -> Result<bool, Ctap2StatusCode> {
match cbor_value {
cbor::Value::Simple(cbor::SimpleValue::FalseValue) => Ok(false),
@@ -734,9 +776,7 @@ pub(super) fn read_bool(cbor_value: &cbor::Value) -> Result<bool, Ctap2StatusCod
}
}
pub(super) fn ok_or_missing(
value_option: Option<&cbor::Value>,
) -> Result<&cbor::Value, Ctap2StatusCode> {
pub(super) fn ok_or_missing<T>(value_option: Option<T>) -> Result<T, Ctap2StatusCode> {
value_option.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
}

View File

@@ -133,17 +133,6 @@ pub struct PersistentStore {
store: embedded_flash::Store<Storage, Config>,
}
#[cfg(feature = "ram_storage")]
const PAGE_SIZE: usize = 0x100;
#[cfg(not(feature = "ram_storage"))]
const PAGE_SIZE: usize = 0x1000;
const STORE_SIZE: usize = NUM_PAGES * PAGE_SIZE;
#[cfg(not(any(test, feature = "ram_storage")))]
#[link_section = ".app_state"]
static STORE: [u8; STORE_SIZE] = [0xff; STORE_SIZE];
impl PersistentStore {
/// Gives access to the persistent store.
///
@@ -164,19 +153,16 @@ impl PersistentStore {
#[cfg(not(any(test, feature = "ram_storage")))]
fn new_prod_storage() -> Storage {
let store = unsafe {
// Safety: The store cannot alias because this function is called only once.
core::slice::from_raw_parts_mut(STORE.as_ptr() as *mut u8, STORE_SIZE)
};
unsafe {
// Safety: The store is in a writeable flash region.
Storage::new(store).unwrap()
}
Storage::new(NUM_PAGES).unwrap()
}
#[cfg(any(test, feature = "ram_storage"))]
fn new_test_storage() -> Storage {
let store = vec![0xff; STORE_SIZE].into_boxed_slice();
#[cfg(not(test))]
const PAGE_SIZE: usize = 0x100;
#[cfg(test)]
const PAGE_SIZE: usize = 0x1000;
let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice();
let options = embedded_flash::BufferOptions {
word_size: 4,
page_size: PAGE_SIZE,