Skip to content

Commit

Permalink
Adds a high level persistent storage API
Browse files Browse the repository at this point in the history
  • Loading branch information
kaczmarczyck committed Apr 2, 2024
1 parent 4339600 commit 02d66ba
Show file tree
Hide file tree
Showing 9 changed files with 512 additions and 210 deletions.
1 change: 1 addition & 0 deletions libraries/opensk/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod crypto;
pub mod customization;
pub mod firmware_protection;
pub mod key_store;
pub mod persist;
pub mod private_key;
pub mod rng;
pub mod user_presence;
367 changes: 367 additions & 0 deletions libraries/opensk/src/api/persist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,367 @@
// Copyright 2024 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 keys;

use crate::ctap::status_code::{Ctap2StatusCode, CtapResult};
use crate::ctap::PIN_AUTH_LENGTH;
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::cmp;

pub type PersistIter<'a> = Box<dyn Iterator<Item = CtapResult<usize>> + 'a>;
pub type PersistCredentialIter<'a> = Box<dyn Iterator<Item = CtapResult<(usize, Vec<u8>)>> + 'a>;

/// Stores data that persists across reboots.
///
/// This trait might get appended to with new versions of CTAP.
///
/// To implement this trait, you have 2 options:
/// - Implement all high level functions with default implementations,
/// calling `unimplemented!` in the key-value accessors.
/// When we update this trait in a new version, OpenSK will panic when calling any new functions.
/// - Implement the key-value accessors, and special case as many default implemented high level
/// functions as desired.
/// When the trait gets extended, new features will silently work.
/// Credentials still need keys to be identified by.
pub trait Persist {
/// Retrieves the value for a given key.
fn find(&self, key: usize) -> CtapResult<Option<Vec<u8>>>;

/// Inserts the value at the given key.
///
/// Values up to a length of 1023 Byte must be supported.
fn insert(&mut self, key: usize, value: &[u8]) -> CtapResult<()>;

/// Removes a key, if present.
fn remove(&mut self, key: usize) -> CtapResult<()>;

/// Iterator for all present keys.
fn iter(&self) -> CtapResult<PersistIter<'_>>;

/// Checks consistency on boot, and if necessary fixes or initializes problems.
fn init(&mut self) -> CtapResult<()> {
if self.find(keys::RESET_COMPLETION)?.is_some() {
self.reset()?;
}
// TODO don't forget to call, add other init functionality, e.g. from KeyStore
Ok(())
}

/// Returns the byte array representation of a stored credential.
fn credential_bytes(&self, key: usize) -> CtapResult<Vec<u8>> {
if !keys::CREDENTIALS.contains(&key) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
self.find(key)?
.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)
}

/// Writes a credential at the given key.
fn write_credential_bytes(&mut self, key: usize, value: &[u8]) -> CtapResult<()> {
if !keys::CREDENTIALS.contains(&key) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
self.insert(key, value)
}

/// Removes a credential at the given key.
fn remove_credential(&mut self, key: usize) -> CtapResult<()> {
if !keys::CREDENTIALS.contains(&key) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
self.remove(key)
}

/// Iterates all stored credentials.
fn iter_credentials(&self) -> CtapResult<PersistCredentialIter<'_>> {
Ok(Box::new(self.iter()?.filter_map(move |key| match key {
Ok(k) => {
if keys::CREDENTIALS.contains(&k) {
match self.find(k) {
Ok(Some(v)) => Some(Ok((k, v))),
Ok(None) => None,
Err(e) => Some(Err(e)),
}
} else {
None
}
}
Err(e) => Some(Err(e)),
})))
}

/// Returns a key where a new credential can be inserted.
fn free_credential_key(&self) -> CtapResult<usize> {
for key in keys::CREDENTIALS {
if self.find(key)?.is_none() {
return Ok(key);
}
}
Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL)
}

/// Returns the global signature counter.
fn global_signature_counter(&self) -> CtapResult<u32> {
const INITIAL_SIGNATURE_COUNTER: u32 = 1;
match self.find(keys::GLOBAL_SIGNATURE_COUNTER)? {
None => Ok(INITIAL_SIGNATURE_COUNTER),
Some(value) if value.len() == 4 => Ok(u32::from_ne_bytes(*array_ref!(&value, 0, 4))),
Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}

/// Increments the global signature counter.
fn incr_global_signature_counter(&mut self, increment: u32) -> CtapResult<()> {
let old_value = self.global_signature_counter()?;
// In hopes that servers handle the wrapping gracefully.
let new_value = old_value.wrapping_add(increment);
self.insert(keys::GLOBAL_SIGNATURE_COUNTER, &new_value.to_ne_bytes())
}

/// Returns the PIN hash if defined.
fn pin_hash(&self) -> CtapResult<Option<[u8; PIN_AUTH_LENGTH]>> {
let pin_properties = match self.find(keys::PIN_PROPERTIES)? {
None => return Ok(None),
Some(pin_properties) => pin_properties,
};
if pin_properties.len() != 1 + PIN_AUTH_LENGTH {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
Ok(Some(*array_ref![pin_properties, 1, PIN_AUTH_LENGTH]))
}

/// Returns the length of the currently set PIN if defined.
#[cfg(feature = "config_command")]
fn pin_code_point_length(&self) -> CtapResult<Option<u8>> {
let pin_properties = match self.find(keys::PIN_PROPERTIES)? {
None => return Ok(None),
Some(pin_properties) => pin_properties,
};
if pin_properties.is_empty() {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
Ok(Some(pin_properties[0]))
}

/// Sets the PIN hash and length.
///
/// If it was already defined, it is updated.
fn set_pin(
&mut self,
pin_hash: &[u8; PIN_AUTH_LENGTH],
pin_code_point_length: u8,
) -> CtapResult<()> {
let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH];
pin_properties[0] = pin_code_point_length;
pin_properties[1..].clone_from_slice(pin_hash);
self.insert(keys::PIN_PROPERTIES, &pin_properties[..])?;
// If power fails between these 2 transactions, PIN has to be set again.
self.remove(keys::FORCE_PIN_CHANGE)
}

/// Returns the number of failed PIN attempts.
fn pin_fails(&self) -> CtapResult<u8> {
match self.find(keys::PIN_RETRIES)? {
None => Ok(0),
Some(value) if value.len() == 1 => Ok(value[0]),
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}

/// Decrements the number of remaining PIN retries.
fn incr_pin_fails(&mut self) -> CtapResult<()> {
let old_value = self.pin_fails()?;
let new_value = old_value.saturating_add(1);
self.insert(keys::PIN_RETRIES, &[new_value])
}

/// Resets the number of remaining PIN retries.
fn reset_pin_retries(&mut self) -> CtapResult<()> {
self.remove(keys::PIN_RETRIES)
}

/// Returns the minimum PIN length, if stored.
fn min_pin_length(&self) -> CtapResult<Option<u8>> {
match self.find(keys::MIN_PIN_LENGTH)? {
None => Ok(None),
Some(value) if value.len() == 1 => Ok(Some(value[0])),
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}

/// Sets the minimum PIN length.
#[cfg(feature = "config_command")]
fn set_min_pin_length(&mut self, min_pin_length: u8) -> CtapResult<()> {
self.insert(keys::MIN_PIN_LENGTH, &[min_pin_length])
}

/// Returns the list of RP IDs that may read the minimum PIN length.
fn min_pin_length_rp_ids_bytes(&self) -> CtapResult<Vec<u8>> {
Ok(self
.find(keys::MIN_PIN_LENGTH_RP_IDS)?
.unwrap_or(Vec::new()))
}

/// Sets the list of RP IDs that may read the minimum PIN length.
#[cfg(feature = "config_command")]
fn set_min_pin_length_rp_ids(&mut self, min_pin_length_rp_ids_bytes: &[u8]) -> CtapResult<()> {
self.insert(keys::MIN_PIN_LENGTH_RP_IDS, min_pin_length_rp_ids_bytes)
}

/// Reads the byte vector stored as the serialized large blobs array.
///
/// If too few bytes exist at that offset, return the maximum number
/// available. This includes cases of offset being beyond the stored array.
///
/// If no large blob is committed to the store, get responds as if an empty
/// CBOR array (0x80) was written, together with the 16 byte prefix of its
/// SHA256, to a total length of 17 byte (which is the shortest legitimate
/// large blob entry possible).
fn get_large_blob_array(
&self,
mut offset: usize,
byte_count: usize,
) -> CtapResult<Option<Vec<u8>>> {
let mut result = Vec::with_capacity(byte_count);
for key in keys::LARGE_BLOB_SHARDS {
if offset >= VALUE_LENGTH {
offset = offset.saturating_sub(VALUE_LENGTH);
continue;
}
let end = offset.saturating_add(byte_count - result.len());
let end = cmp::min(end, VALUE_LENGTH);
let value = self.find(key)?.unwrap_or(Vec::new());
if key == keys::LARGE_BLOB_SHARDS.start && value.is_empty() {
return Ok(None);
}
let end = cmp::min(end, value.len());
if end < offset {
return Ok(Some(result));
}
result.extend(&value[offset..end]);
offset = offset.saturating_sub(VALUE_LENGTH);
}
Ok(Some(result))
}

/// Sets a byte vector as the serialized large blobs array.
fn commit_large_blob_array(&mut self, large_blob_array: &[u8]) -> CtapResult<()> {
debug_assert!(large_blob_array.len() <= keys::LARGE_BLOB_SHARDS.len() * VALUE_LENGTH);
let mut offset = 0;
for key in keys::LARGE_BLOB_SHARDS {
let cur_len = cmp::min(large_blob_array.len().saturating_sub(offset), VALUE_LENGTH);
let slice = &large_blob_array[offset..][..cur_len];
if slice.is_empty() {
self.remove(key)?;
} else {
self.insert(key, slice)?;
}
offset += cur_len;
}
Ok(())
}

/// Resets persistent data, consistent with a CTAP reset.
///
/// In particular, entries that are persistent across factory reset are not removed.
fn reset(&mut self) -> CtapResult<()> {
self.insert(keys::RESET_COMPLETION, &[])?;
let mut removed_keys = Vec::new();
for key in self.iter()? {
let key = key?;
if key >= keys::NUM_PERSISTENT_KEYS && key != keys::RESET_COMPLETION {
removed_keys.push(key);
}
}
for key in removed_keys {
self.remove(key)?;
}
self.remove(keys::RESET_COMPLETION)
}

/// Returns whether the PIN needs to be changed before its next usage.
fn has_force_pin_change(&self) -> CtapResult<bool> {
match self.find(keys::FORCE_PIN_CHANGE)? {
None => Ok(false),
Some(value) if value.is_empty() => Ok(true),
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}

/// Marks the PIN as outdated with respect to the new PIN policy.
#[cfg(feature = "config_command")]
fn force_pin_change(&mut self) -> CtapResult<()> {
self.insert(keys::FORCE_PIN_CHANGE, &[])
}

/// Returns whether enterprise attestation is enabled.
#[cfg(feature = "config_command")]
fn enterprise_attestation(&self) -> CtapResult<bool> {
match self.find(keys::ENTERPRISE_ATTESTATION)? {
None => Ok(false),
Some(value) if value.is_empty() => Ok(true),
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}

/// Marks enterprise attestation as enabled.
#[cfg(feature = "config_command")]
fn enable_enterprise_attestation(&mut self) -> CtapResult<()> {
// TODO
// if self.attestation_store_get(&attestation_store::Id::Enterprise)?.is_none() {
// return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
// }
self.insert(keys::ENTERPRISE_ATTESTATION, &[])
}

/// Returns whether alwaysUv is enabled.
fn has_always_uv(&self) -> CtapResult<bool> {
match self.find(keys::ALWAYS_UV)? {
None => Ok(false),
Some(value) if value.is_empty() => Ok(true),
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
}
}

/// Enables alwaysUv, when disabled, and vice versa.
#[cfg(feature = "config_command")]
fn toggle_always_uv(&mut self) -> CtapResult<()> {
if self.has_always_uv()? {
Ok(self.remove(keys::ALWAYS_UV)?)
} else {
Ok(self.insert(keys::ALWAYS_UV, &[])?)
}
}
}

const VALUE_LENGTH: usize = 1023;

#[cfg(test)]
mod test {
use super::*;
use crate::api::customization::Customization;
use crate::env::test::TestEnv;
use crate::env::Env;

#[test]
fn test_max_large_blob_array_size() {
let env = TestEnv::default();

assert!(
env.customization().max_large_blob_array_size()
<= VALUE_LENGTH * keys::LARGE_BLOB_SHARDS.len()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ make_partition! {
// - When adding a (non-persistent) key below this message, make sure its value is bigger or
// equal than NUM_PERSISTENT_KEYS.

/// Used to make sure that a Reset command completes once started.
RESET_COMPLETION = 20;

/// Reserved for future credential-related objects.
///
/// In particular, additional credentials could be added there by reducing the lower bound of
Expand Down
Loading

0 comments on commit 02d66ba

Please sign in to comment.