Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a high level persistent storage API #679

Merged
merged 5 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
kaczmarczyck marked this conversation as resolved.
Show resolved Hide resolved
/// Credentials still need keys to be identified by.
pub trait Persist {
/// Retrieves the value for a given key.
kaczmarczyck marked this conversation as resolved.
Show resolved Hide resolved
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.
kaczmarczyck marked this conversation as resolved.
Show resolved Hide resolved
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.
kaczmarczyck marked this conversation as resolved.
Show resolved Hide resolved
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
Loading