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,

View File

@@ -13,6 +13,7 @@
// limitations under the License.
use super::{Index, Storage, StorageError, StorageResult};
use alloc::vec::Vec;
use libtock::syscalls;
const DRIVER_NUMBER: usize = 0x50003;
@@ -33,8 +34,23 @@ mod allow_nr {
pub const WRITE_SLICE: usize = 0;
}
fn get_info(nr: usize) -> StorageResult<usize> {
let code = unsafe { syscalls::command(DRIVER_NUMBER, command_nr::GET_INFO, nr, 0) };
mod memop_nr {
pub const STORAGE_CNT: u32 = 12;
pub const STORAGE_PTR: u32 = 13;
pub const STORAGE_LEN: u32 = 14;
}
fn get_info(nr: usize, arg: usize) -> StorageResult<usize> {
let code = unsafe { syscalls::command(DRIVER_NUMBER, command_nr::GET_INFO, nr, arg) };
if code < 0 {
Err(StorageError::KernelError { code })
} else {
Ok(code as usize)
}
}
fn memop(nr: u32, arg: usize) -> StorageResult<usize> {
let code = unsafe { syscalls::memop(nr, arg) };
if code < 0 {
Err(StorageError::KernelError { code })
} else {
@@ -45,70 +61,56 @@ fn get_info(nr: usize) -> StorageResult<usize> {
pub struct SyscallStorage {
word_size: usize,
page_size: usize,
num_pages: usize,
max_word_writes: usize,
max_page_erases: usize,
storage: &'static mut [u8],
storage_locations: Vec<&'static [u8]>,
}
impl SyscallStorage {
/// Provides access to the embedded flash if available.
///
/// # Safety
///
/// The `storage` must be in a writeable flash region.
///
/// # Errors
///
/// Returns `BadFlash` if any of the following conditions do not hold:
/// - The word size is not a power of two.
/// - The page size is not a power of two.
/// - The page size is not a multiple of the word size.
/// - The word size is a power of two.
/// - The page size is a power of two.
/// - The page size is a multiple of the word size.
/// - The storage is page-aligned.
///
/// Returns `NotAligned` if any of the following conditions do not hold:
/// - `storage` is page-aligned.
/// - `storage.len()` is a multiple of the page size.
///
/// # Examples
///
/// ```rust
/// # extern crate ctap2;
/// # use ctap2::embedded_flash::SyscallStorage;
/// # use ctap2::embedded_flash::StorageResult;
/// # const NUM_PAGES: usize = 1;
/// # const PAGE_SIZE: usize = 1;
/// #[link_section = ".app_state"]
/// static mut STORAGE: [u8; NUM_PAGES * PAGE_SIZE] = [0xff; NUM_PAGES * PAGE_SIZE];
/// # fn foo() -> StorageResult<SyscallStorage> {
/// // This is safe because this is the only use of `STORAGE` in the whole program and this is
/// // called only once.
/// unsafe { SyscallStorage::new(&mut STORAGE) }
/// # }
/// ```
pub unsafe fn new(storage: &'static mut [u8]) -> StorageResult<SyscallStorage> {
let word_size = get_info(command_nr::get_info_nr::WORD_SIZE)?;
let page_size = get_info(command_nr::get_info_nr::PAGE_SIZE)?;
let max_word_writes = get_info(command_nr::get_info_nr::MAX_WORD_WRITES)?;
let max_page_erases = get_info(command_nr::get_info_nr::MAX_PAGE_ERASES)?;
if !word_size.is_power_of_two() || !page_size.is_power_of_two() {
return Err(StorageError::BadFlash);
}
let syscall = SyscallStorage {
word_size,
page_size,
max_word_writes,
max_page_erases,
storage,
/// Returns `OutOfBounds` the number of pages does not fit in the storage.
pub fn new(mut num_pages: usize) -> StorageResult<SyscallStorage> {
let mut syscall = SyscallStorage {
word_size: get_info(command_nr::get_info_nr::WORD_SIZE, 0)?,
page_size: get_info(command_nr::get_info_nr::PAGE_SIZE, 0)?,
num_pages,
max_word_writes: get_info(command_nr::get_info_nr::MAX_WORD_WRITES, 0)?,
max_page_erases: get_info(command_nr::get_info_nr::MAX_PAGE_ERASES, 0)?,
storage_locations: Vec::new(),
};
if !syscall.is_word_aligned(page_size) {
if !syscall.word_size.is_power_of_two()
|| !syscall.page_size.is_power_of_two()
|| !syscall.is_word_aligned(syscall.page_size)
{
return Err(StorageError::BadFlash);
}
if syscall.is_page_aligned(syscall.storage.as_ptr() as usize)
&& syscall.is_page_aligned(syscall.storage.len())
{
Ok(syscall)
} else {
Err(StorageError::NotAligned)
for i in 0..memop(memop_nr::STORAGE_CNT, 0)? {
let storage_ptr = memop(memop_nr::STORAGE_PTR, i)?;
let max_storage_len = memop(memop_nr::STORAGE_LEN, i)?;
if !syscall.is_page_aligned(storage_ptr) || !syscall.is_page_aligned(max_storage_len) {
return Err(StorageError::BadFlash);
}
let storage_len = core::cmp::min(num_pages * syscall.page_size, max_storage_len);
num_pages -= storage_len / syscall.page_size;
syscall
.storage_locations
.push(unsafe { core::slice::from_raw_parts(storage_ptr as *mut u8, storage_len) });
}
if num_pages > 0 {
// The storage locations don't have enough pages.
return Err(StorageError::OutOfBounds);
}
Ok(syscall)
}
fn is_word_aligned(&self, x: usize) -> bool {
@@ -130,7 +132,7 @@ impl Storage for SyscallStorage {
}
fn num_pages(&self) -> usize {
self.storage.len() / self.page_size
self.num_pages
}
fn max_word_writes(&self) -> usize {
@@ -142,14 +144,15 @@ impl Storage for SyscallStorage {
}
fn read_slice(&self, index: Index, length: usize) -> StorageResult<&[u8]> {
Ok(&self.storage[index.range(length, self)?])
let start = index.range(length, self)?.start;
find_slice(&self.storage_locations, start, length)
}
fn write_slice(&mut self, index: Index, value: &[u8]) -> StorageResult<()> {
if !self.is_word_aligned(index.byte) || !self.is_word_aligned(value.len()) {
return Err(StorageError::NotAligned);
}
let range = index.range(value.len(), self)?;
let ptr = self.read_slice(index, value.len())?.as_ptr() as usize;
let code = unsafe {
syscalls::allow_ptr(
DRIVER_NUMBER,
@@ -163,14 +166,8 @@ impl Storage for SyscallStorage {
if code < 0 {
return Err(StorageError::KernelError { code });
}
let code = unsafe {
syscalls::command(
DRIVER_NUMBER,
command_nr::WRITE_SLICE,
self.storage[range].as_ptr() as usize,
0,
)
};
let code =
unsafe { syscalls::command(DRIVER_NUMBER, command_nr::WRITE_SLICE, ptr, value.len()) };
if code < 0 {
return Err(StorageError::KernelError { code });
}
@@ -178,18 +175,59 @@ impl Storage for SyscallStorage {
}
fn erase_page(&mut self, page: usize) -> StorageResult<()> {
let range = Index { page, byte: 0 }.range(self.page_size(), self)?;
let code = unsafe {
syscalls::command(
DRIVER_NUMBER,
command_nr::ERASE_PAGE,
self.storage[range].as_ptr() as usize,
0,
)
};
let index = Index { page, byte: 0 };
let length = self.page_size();
let ptr = self.read_slice(index, length)?.as_ptr() as usize;
let code = unsafe { syscalls::command(DRIVER_NUMBER, command_nr::ERASE_PAGE, ptr, length) };
if code < 0 {
return Err(StorageError::KernelError { code });
}
Ok(())
}
}
fn find_slice<'a>(
slices: &'a [&'a [u8]],
mut start: usize,
length: usize,
) -> StorageResult<&'a [u8]> {
for slice in slices {
if start >= slice.len() {
start -= slice.len();
continue;
}
if start + length > slice.len() {
break;
}
return Ok(&slice[start..][..length]);
}
Err(StorageError::OutOfBounds)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_slice_ok() {
assert_eq!(
find_slice(&[&[1, 2, 3, 4]], 0, 4).ok(),
Some(&[1u8, 2, 3, 4] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 1, 2).ok(),
Some(&[2u8, 3] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 4, 2).ok(),
Some(&[5u8, 6] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 4, 0).ok(),
Some(&[] as &[u8])
);
assert!(find_slice(&[], 0, 1).is_err());
assert!(find_slice(&[&[1, 2, 3, 4], &[5, 6]], 6, 0).is_err());
assert!(find_slice(&[&[1, 2, 3, 4], &[5, 6]], 3, 2).is_err());
}
}