#![no_std]
#![forbid(unsafe_code)]
#[cfg(test)]
#[macro_use]
extern crate lazy_static;
extern crate alloc;
extern crate base64;
extern crate cbor;
extern crate crypto;
mod der;
#[macro_use]
mod macros;
mod passkeys;
mod pin;
mod recovery_key_store;
mod spki;
#[cfg(fuzzing)]
pub use recovery_key_store::fuzzing::{x509_parse, xml_parse};
#[cfg(fuzzing)]
pub use spki::parse as spki_parse;
use alloc::collections::{btree_map, BTreeMap};
use alloc::string::String;
use alloc::vec::Vec;
use alloc::{format, vec};
use bytes::Bytes;
use cbor::{cbor, MapKey, MapKeyRef, MapLookupKey, Value};
#[derive(Clone)]
pub enum ClientState {
Initial,
Explicit(StateData),
}
#[derive(Debug)]
pub enum StateUpdate {
Major(StateData),
Minor(StateData),
None,
}
#[derive(Clone, Debug)]
pub struct StateData {
pub transparent: Vec<u8>,
pub confidential: Vec<u8>,
}
#[derive(Debug)]
pub enum Error {
TransparentDataCBORError(cbor::Error),
ConfidentialDataCBORError(cbor::Error),
UnknownClient,
UnknownKey,
SignatureVerificationFailed,
Str(&'static str),
CBORError(cbor::Error),
}
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct MetricsUpdate {
pub bad_request: u32,
pub unknown_client: u32,
pub missing_uv_key: u32,
pub missing_key: u32,
pub cannot_parse_public_key: u32,
pub signature_verification_failed: u32,
pub error_result: u32,
pub missing_uv_key_with_deferred_bit: u32,
pub missing_uv_key_without_deferred_bit: u32,
pub missing_uv_key_with_hw_key_present: u32,
pub missing_uv_and_hw_key: u32,
pub debug_success: u32,
pub debug_dump: u32,
pub passkeys_create: u32,
pub passkeys_assert: u32,
pub passkeys_wrap_pin: u32,
pub device_register: u32,
pub device_add_uv_key: u32,
pub device_forget: u32,
pub keys_genpair: u32,
pub keys_wrap: u32,
pub recovery_key_store_wrap: u32,
pub recovery_key_store_wrap_as_member: u32,
pub recovery_key_store_wrap_pin_and_secret: u32,
pub recovery_key_store_rewrap: u32,
pub device_auth_keys_wrap: u32,
}
#[derive(Clone)]
pub struct DeviceAuthorizationKey {
pub version: i64,
pub key: Vec<u8>,
}
#[derive(Clone)]
pub struct ExternalContext {
pub current_time_epoch_millis: i64,
pub client_device_identifier: Vec<u8>,
pub is_reauthenticated: bool,
pub device_authorization_keys: Vec<DeviceAuthorizationKey>,
}
const OK: &str = "ok";
const ERR: &str = "err";
pub(crate) const KEY_PURPOSE_SECURITY_DOMAIN_SECRET: &str = "security domain secret";
pub(crate) const KEY_PURPOSE_DEVICE_AUTHORIZATION_KEY_PREFIX: &str = "device authorization key v_";
map_keys! {
AUTH_LEVEL, AUTH_LEVEL_KEY = "auth_level",
CMD, CMD_KEY = "cmd",
COHORT_PUBLIC_KEY, COHORT_PUBLIC_KEY_KEY = "cohort_public_key",
CERT_XML_SERIAL_NUMBER, CERT_XML_SERIAL_NUMBER_KEY = "cert_xml_serial_number",
COUNTER_ID, COUNTER_ID_KEY = "counter_id",
CREATE_NEW_VAULT, CREATE_NEW_VAULT_KEY = "create_new_vault",
DEVICE_ID, DEVICE_ID_KEY = "device_id",
DEVICES, DEVICES_KEY = "devices",
DEVICE_AUTH_KEYS, DEVICE_AUTH_KEYS_KEY = "wrapped_device_auth_keys",
ENCODED_REQUESTS, ENCODED_REQUESTS_KEY = "encoded_requests",
EXTERNAL_DEVICE_IDENTIFIER, EXTERNAL_DEVICE_IDENTIFIER_KEY = "ext_device_id",
KEY, KEY_KEY = "key",
LAST_USED, LAST_USED_KEY = "last_used",
PIN_ATTEMPTS, PIN_ATTEMPTS_KEY = "pin_attempts",
PRIV_KEY, PRIV_KEY_KEY = "priv_key",
PUB_KEY, PUB_KEY_KEY = "pub_key",
PUB_KEYS, PUB_KEYS_KEY = "pub_keys",
PURPOSE, PURPOSE_KEY = "purpose",
REGISTER_TIME, REGISTER_TIME_KEY = "register_time",
SECRET, SECRET_KEY = "secret",
SIG, SIG_KEY = "sig",
TO, TO_KEY = "to",
UV_KEY_PENDING, UV_KEY_PENDING_KEY = "uv_key_pending",
VAULT_HANDLE_WITHOUT_TYPE, VAULT_HANDLE_WITHOUT_TYPE_KEY = "vault_handle_without_type",
WRAPPED_PIN_DATA, WRAPPED_PIN_DATA_KEY = "wrapped_pin_data",
WRAPPED_SECRET, WRAPPED_SECRET_KEY = "wrapped_secret",
WRAPPING_KEYS, WRAPPING_KEYS_KEY = "wrapping_keys",
}
const LARGE_NONCE_LEN: usize = 24;
const AES256_KEY_LEN: usize = 32;
const GCM_OVERHEAD: usize = 16;
fn get_key_and_nonce(
wrapping_key: &[u8],
nonce: &[u8; LARGE_NONCE_LEN],
) -> ([u8; 32], [u8; crypto::NONCE_LEN]) {
static_assertions::const_assert!(LARGE_NONCE_LEN == 2 * crypto::NONCE_LEN);
let (key_nonce, gcm_nonce) = nonce.split_at(LARGE_NONCE_LEN - crypto::NONCE_LEN);
let mut gcm_key = [0u8; AES256_KEY_LEN];
crypto::hkdf_sha256(
wrapping_key,
key_nonce,
b"derive wrapping key",
&mut gcm_key,
)
.unwrap();
(gcm_key, gcm_nonce.try_into().unwrap())
}
fn wrap(wrapping_key: &[u8], data: &[u8], purpose: &str) -> Vec<u8> {
let mut nonce = [0u8; LARGE_NONCE_LEN];
crypto::rand_bytes(&mut nonce);
let (gcm_key, gcm_nonce) = get_key_and_nonce(wrapping_key, &nonce);
let mut ciphertext = Vec::with_capacity(data.len() + GCM_OVERHEAD + LARGE_NONCE_LEN);
ciphertext.extend_from_slice(data);
crypto::aes_256_gcm_seal_in_place(&gcm_key, &gcm_nonce, purpose.as_bytes(), &mut ciphertext);
let mut nonce = nonce.to_vec();
nonce.extend_from_slice(&ciphertext);
nonce
}
fn unwrap(wrapping_key: &[u8], data: &[u8], purpose: &str) -> Result<Vec<u8>, RequestError> {
if data.len() < LARGE_NONCE_LEN {
return debug("wrapped data too small");
}
let (nonce_slice, ciphertext) = data.split_at(LARGE_NONCE_LEN);
let (gcm_key, gcm_nonce) = get_key_and_nonce(wrapping_key, nonce_slice.try_into().unwrap());
crypto::aes_256_gcm_open_in_place(
&gcm_key,
&gcm_nonce,
purpose.as_bytes(),
Vec::from(ciphertext),
)
.map_err(|_| RequestError::Debug("decryption failed"))
}
fn open_aes_256_gcm(key: &[u8; 32], nonce_and_ciphertext: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
if nonce_and_ciphertext.len() < crypto::NONCE_LEN {
return None;
}
let (nonce, ciphertext) = nonce_and_ciphertext.split_at(crypto::NONCE_LEN);
let nonce: [u8; crypto::NONCE_LEN] = nonce.try_into().unwrap();
crypto::aes_256_gcm_open_in_place(key, &nonce, aad, ciphertext.to_vec()).ok()
}
enum SourceOfSecret {
Wrapped,
Direct,
}
fn get_secret_from_request(
state: &DirtyFlag<ParsedState>,
request: &BTreeMap<MapKey, Value>,
device_id: &[u8],
) -> Result<([u8; 32], SourceOfSecret), RequestError> {
let (secret, source) =
if let Some(Value::Bytestring(wrapped_secret)) = request.get(WRAPPED_SECRET_KEY) {
if request.get(SECRET_KEY).is_some() {
return debug("both wrapped and unwrapped secret provided");
} else {
(
state.unwrap(
device_id,
wrapped_secret,
KEY_PURPOSE_SECURITY_DOMAIN_SECRET,
)?,
SourceOfSecret::Wrapped,
)
}
} else if let Some(Value::Bytestring(secret)) = request.get(SECRET_KEY) {
(secret.to_vec(), SourceOfSecret::Direct)
} else {
return debug("must provide secret or wrapped secret");
};
let secret = secret
.as_slice()
.try_into()
.map_err(|_| RequestError::Debug("wrong length secret"))?;
Ok((secret, source))
}
pub struct PINState {
attempts: i64,
}
pub struct ParsedState {
transparent: BTreeMap<MapKey, Value>,
confidential: BTreeMap<MapKey, Value>,
}
impl ParsedState {
fn serialize(self: ParsedState) -> StateData {
StateData {
transparent: Value::Map(self.transparent).to_bytes(),
confidential: Value::Map(self.confidential).to_bytes(),
}
}
fn get_device(&self, device_id: &[u8]) -> Option<&BTreeMap<MapKey, Value>> {
let Some(Value::Map(devices)) = self.transparent.get(DEVICES_KEY) else {
return None;
};
let Some(Value::Map(client)) =
devices.get(&MapKeyRef::Slice(device_id) as &dyn MapLookupKey)
else {
return None;
};
Some(client)
}
fn get_mut_device(&mut self, device_id: &[u8]) -> Option<&mut BTreeMap<MapKey, Value>> {
let Some(Value::Map(devices)) = self.transparent.get_mut(DEVICES_KEY) else {
return None;
};
let Some(Value::Map(client)) =
devices.get_mut(&MapKeyRef::Slice(device_id) as &dyn MapLookupKey)
else {
return None;
};
Some(client)
}
fn get_device_entry(
&mut self,
device_id: Vec<u8>,
) -> Result<btree_map::Entry<'_, MapKey, Value>, RequestError> {
let Some(Value::Map(devices)) = self.transparent.get_mut(DEVICES_KEY) else {
return debug("malformed transparent data");
};
Ok(devices.entry(MapKey::Bytestring(device_id)))
}
fn wrapping_key(&self, device_id: &[u8]) -> Result<&[u8], RequestError> {
let Some(Value::Map(wrapping_keys)) = self.confidential.get(WRAPPING_KEYS_KEY) else {
return debug("malformed confidential data");
};
let Some(Value::Bytestring(wrapping_key)) =
wrapping_keys.get(&MapKeyRef::Slice(device_id) as &dyn MapLookupKey)
else {
return debug("missing wrapping key");
};
Ok(wrapping_key)
}
fn wrap(&self, device_id: &[u8], data: &[u8], purpose: &str) -> Result<Vec<u8>, RequestError> {
let wrapping_key = self.wrapping_key(device_id)?;
Ok(wrap(wrapping_key, data, purpose))
}
fn unwrap(
&self,
device_id: &[u8],
data: &[u8],
purpose: &str,
) -> Result<Vec<u8>, RequestError> {
let wrapping_key = self.wrapping_key(device_id)?;
unwrap(wrapping_key, data, purpose)
}
fn get_pin_state(&self, device_id: &[u8]) -> Result<PINState, RequestError> {
let device = self
.get_device(device_id)
.ok_or(RequestError::Debug("unknown device"))?;
let attempts = match device.get(PIN_ATTEMPTS_KEY) {
Some(Value::Int(attempts)) => *attempts,
_ => 0,
};
Ok(PINState { attempts })
}
fn set_pin_state(&mut self, device_id: &[u8], pin_state: PINState) -> Result<(), RequestError> {
let device = self
.get_mut_device(device_id)
.ok_or(RequestError::Debug("unknown device"))?;
if pin_state.attempts == 0 {
device.remove(PIN_ATTEMPTS_KEY);
} else {
device.insert(PIN_ATTEMPTS.into(), Value::Int(pin_state.attempts));
}
Ok(())
}
}
impl Default for ParsedState {
fn default() -> ParsedState {
let confidential = BTreeMap::from([(
MapKey::String(String::from(WRAPPING_KEYS)),
Value::Map(BTreeMap::new()),
)]);
ParsedState {
transparent: BTreeMap::from([(
MapKey::String(String::from(DEVICES)),
Value::Map(BTreeMap::new()),
)]),
confidential,
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
enum AuthLevel {
Software,
Hardware,
UserVerification,
SoftwareUserVerification,
}
impl AuthLevel {
fn as_str(&self) -> &'static str {
match self {
AuthLevel::Software => "sw",
AuthLevel::Hardware => "hw",
AuthLevel::UserVerification => "uv",
AuthLevel::SoftwareUserVerification => "swuv",
}
}
}
impl core::str::FromStr for AuthLevel {
type Err = ();
fn from_str(s: &str) -> Result<AuthLevel, ()> {
match s {
"sw" => Ok(AuthLevel::Software),
"hw" => Ok(AuthLevel::Hardware),
"uv" => Ok(AuthLevel::UserVerification),
"swuv" => Ok(AuthLevel::SoftwareUserVerification),
_ => Err(()),
}
}
}
#[derive(Copy, Clone)]
enum Reauth {
None,
Done,
}
#[derive(Copy, Clone)]
enum OneTimeUV {
Consumed,
None,
}
enum Authentication {
None,
Device(Vec<u8>, AuthLevel, OneTimeUV, Reauth),
NewlyRegistered(Vec<u8>),
}
impl ClientState {
fn parse(self: ClientState) -> Result<ParsedState, Error> {
match self {
ClientState::Initial => Ok(ParsedState::default()),
ClientState::Explicit(data) => {
let Value::Map(transparent) =
cbor::parse(data.transparent).map_err(Error::TransparentDataCBORError)?
else {
return Err(Error::Str("transparent data isn't a map"));
};
let Value::Map(confidential) =
cbor::parse(data.confidential).map_err(Error::ConfidentialDataCBORError)?
else {
return Err(Error::Str("confidential data isn't a map"));
};
Ok(ParsedState {
transparent,
confidential,
})
}
}
}
}
struct DirtyFlag<'a, T> {
_contents: &'a mut T,
changed: bool,
minor_change: bool,
}
impl<'a, T> core::ops::Deref for DirtyFlag<'a, T> {
type Target = T;
fn deref(&self) -> &T {
self._contents
}
}
impl<'a, T> DirtyFlag<'a, T> {
fn new(r: &'a mut T) -> Self {
DirtyFlag {
_contents: r,
changed: false,
minor_change: false,
}
}
fn get_mut(&mut self) -> &mut T where {
self.changed = true;
self._contents
}
fn get_mut_for_minor_change(&mut self) -> &mut T {
self.minor_change = true;
self._contents
}
}
pub fn process_client_msg(
state: ClientState,
metrics: &mut MetricsUpdate,
mut ext_ctx: ExternalContext,
handshake_hash: &[u8],
client_msg: Vec<u8>,
) -> Result<(Value, StateUpdate), Error> {
let mut state = state.parse()?;
let Value::Map(client_msg) = cbor::parse(client_msg).map_err(Error::CBORError)? else {
metrics.bad_request += 1;
return Err(Error::Str("request structure was not a map"));
};
let Some(Value::Bytestring(encoded_requests)) = client_msg.get(ENCODED_REQUESTS_KEY) else {
metrics.bad_request += 1;
return Err(Error::Str("encoded_requests must be given"));
};
let Value::Array(requests) =
cbor::parse_bytes(encoded_requests.clone()).map_err(Error::CBORError)?
else {
metrics.bad_request += 1;
return Err(Error::Str("encoded_requests must be an array"));
};
let mut auth = Authentication::None;
if let Some(device_id) = client_msg.get(DEVICE_ID_KEY) {
let Value::Bytestring(device_id) = device_id else {
metrics.bad_request += 1;
return Err(Error::Str("device_id must be a bytestring"));
};
let device_id = device_id.to_vec();
let Some(Value::String(auth_level)) = client_msg.get(AUTH_LEVEL_KEY) else {
metrics.bad_request += 1;
return Err(Error::Str("auth_level must be given"));
};
let auth_level: AuthLevel = auth_level
.parse()
.map_err(|_| Error::Str("unrecognised authentication level"))?;
let Some(Value::Bytestring(sig)) = client_msg.get(SIG_KEY) else {
metrics.bad_request += 1;
return Err(Error::Str("signature must be given"));
};
let Some(client) = state.get_device(&device_id) else {
metrics.unknown_client += 1;
return Err(Error::UnknownClient);
};
let Some(Value::Map(pub_keys)) = client.get(PUB_KEYS_KEY) else {
return Err(Error::Str("client is missing pub_keys"));
};
let auth_level = if pub_keys
.get(&MapKeyRef::Str(auth_level.as_str()) as &dyn MapLookupKey)
.is_some()
{
auth_level
} else {
match auth_level {
AuthLevel::Software => AuthLevel::Hardware,
_ => auth_level,
}
};
let Some(Value::Bytestring(pub_key)) =
pub_keys.get(&MapKeyRef::Str(auth_level.as_str()) as &dyn MapLookupKey)
else {
if auth_level == AuthLevel::UserVerification {
metrics.missing_uv_key += 1;
match client
.get(UV_KEY_PENDING_KEY)
.unwrap_or(&Value::Boolean(false))
{
Value::Boolean(true) => metrics.missing_uv_key_with_deferred_bit += 1,
_ => metrics.missing_uv_key_without_deferred_bit += 1,
}
match pub_keys.get(&MapKeyRef::Str("hw") as &dyn MapLookupKey) {
Some(_) => metrics.missing_uv_key_with_hw_key_present += 1,
None => metrics.missing_uv_and_hw_key += 1,
}
} else {
metrics.missing_key += 1;
}
return Err(Error::UnknownKey);
};
let Some((pub_key_type, pub_key)) = spki::parse(pub_key) else {
metrics.cannot_parse_public_key += 1;
return Err(Error::Str("cannot parse registered public key"));
};
let encoded_requests_hash = crypto::sha256(encoded_requests);
let signed_message = [handshake_hash, encoded_requests_hash.as_ref()].concat();
if !match pub_key_type {
spki::PublicKeyType::P256 => crypto::ecdsa_verify(pub_key, &signed_message, sig),
spki::PublicKeyType::RSA => crypto::rsa_verify(pub_key, &signed_message, sig),
} {
metrics.signature_verification_failed += 1;
return Err(Error::SignatureVerificationFailed);
}
auth = Authentication::Device(
device_id,
auth_level,
OneTimeUV::None,
if ext_ctx.is_reauthenticated {
Reauth::Done
} else {
Reauth::None
},
);
}
let mut state_with_dirty_flag = DirtyFlag::new(&mut state);
let mut results = Vec::<Value>::with_capacity(requests.len());
for request in requests {
let Value::Map(request) = request else {
metrics.bad_request += 1;
return Err(Error::Str("each request must be a map"));
};
match do_request(
&ext_ctx,
metrics,
&mut auth,
&mut state_with_dirty_flag,
request,
) {
Ok(result) => results.push(Value::Map(BTreeMap::from([(
MapKey::String(String::from(OK)),
result,
)]))),
Err(error) => {
metrics.error_result += 1;
results.push(Value::Map(BTreeMap::from([(
MapKey::String(String::from(ERR)),
error.to_cbor(),
)])));
break;
}
}
}
let has_major_update = state_with_dirty_flag.changed;
let has_minor_update = state_with_dirty_flag.minor_change
|| match auth {
Authentication::Device(device_id, _, _, _) => {
if let Some(device) = state.get_mut_device(&device_id) {
device.insert(
MapKey::String(String::from(LAST_USED)),
Value::Int(ext_ctx.current_time_epoch_millis),
);
device.insert(
MapKey::String(String::from(EXTERNAL_DEVICE_IDENTIFIER)),
Value::Bytestring(Bytes::from(core::mem::take(
&mut ext_ctx.client_device_identifier,
))),
);
true
} else {
false
}
}
_ => false,
};
let update = if has_major_update {
StateUpdate::Major(state.serialize())
} else if has_minor_update {
StateUpdate::Minor(state.serialize())
} else {
StateUpdate::None
};
Ok((Value::Array(results), update))
}
#[derive(Debug, PartialEq)]
enum RequestError {
NoSupportedAlgorithm,
Duplicate,
IncorrectPIN,
PINLocked,
RecoveryKeyStoreDowngrade,
CohortNotYetDeprecated,
Debug(&'static str),
}
impl RequestError {
fn to_cbor(&self) -> Value {
match self {
RequestError::NoSupportedAlgorithm => Value::Int(1),
RequestError::Duplicate => Value::Int(2),
RequestError::IncorrectPIN => Value::Int(3),
RequestError::PINLocked => Value::Int(4),
RequestError::RecoveryKeyStoreDowngrade => Value::Int(6),
RequestError::CohortNotYetDeprecated => Value::Int(7),
RequestError::Debug(s) => Value::String(String::from(*s)),
}
}
}
fn debug<T>(msg: &'static str) -> Result<T, RequestError> {
Err(RequestError::Debug(msg))
}
fn do_request(
ext_ctx: &ExternalContext,
metrics: &mut MetricsUpdate,
auth: &mut Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let Some(Value::String(cmd)) = request.get(CMD_KEY) else {
return debug("request is missing cmd");
};
match cmd.as_str() {
"device/register" => do_device_register(ext_ctx, metrics, auth, state, request),
"device/add_uv_key" => do_device_add_uv_key(metrics, auth, state, request),
"device/forget" => do_device_forget(metrics, auth, state, request),
"debug/success" => do_debug_success(metrics),
"debug/dump" => do_debug_dump(ext_ctx, metrics, state, request),
"keys/genpair" => do_keys_genpair(metrics, auth, state, request),
"keys/wrap" => do_keys_wrap(metrics, auth, state, request),
"passkeys/assert" => passkeys::do_assert(metrics, auth, state, request),
"passkeys/create" => passkeys::do_create(metrics, auth, state, request),
"passkeys/wrap_pin" => passkeys::do_wrap_pin(metrics, auth, state, request),
"recovery_key_store/wrap" => {
recovery_key_store::do_wrap(ext_ctx.current_time_epoch_millis, metrics, request)
}
"recovery_key_store/wrap_as_member" => recovery_key_store::do_wrap_as_member(
metrics,
auth,
state,
ext_ctx.current_time_epoch_millis,
request,
),
"recovery_key_store/wrap_pin_and_secret" => recovery_key_store::do_wrap_pin_and_secret(
metrics,
auth,
state,
ext_ctx.current_time_epoch_millis,
request,
),
"recovery_key_store/rewrap" => recovery_key_store::do_rewrap(
metrics,
auth,
state,
ext_ctx.current_time_epoch_millis,
request,
),
"device_auth_keys/wrap" => do_wrap_device_auth_keys(
metrics,
auth,
state,
ext_ctx.device_authorization_keys.as_slice(),
),
_ => debug("unknown command"),
}
}
fn do_device_register(
ext_ctx: &ExternalContext,
metrics: &mut MetricsUpdate,
auth: &mut Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let Some(Value::Bytestring(device_id)) = request.get(DEVICE_ID_KEY) else {
return debug("missing device_id");
};
if device_id.len() > 128 {
return debug("device_id too long");
}
let device_id = device_id.clone();
let mut device: BTreeMap<MapKey, Value> = BTreeMap::new();
device.insert(
MapKey::String(String::from(REGISTER_TIME)),
Value::Int(ext_ctx.current_time_epoch_millis),
);
device.insert(
MapKey::String(String::from(EXTERNAL_DEVICE_IDENTIFIER)),
Value::Bytestring(Bytes::from(ext_ctx.client_device_identifier.clone())),
);
let mut has_uv_key = false;
let mut has_uv_key_pending = false;
for (key, value) in request {
let MapKey::String(key) = key else {
continue;
};
match key.as_str() {
PUB_KEYS => {
let Value::Map(pub_keys) = value else {
return debug("pub_keys must be a map");
};
if pub_keys.is_empty() {
return debug("pub_keys cannot be empty");
}
for (k, v) in &pub_keys {
let MapKey::String(k) = k else {
return debug("pub_keys contains non-string key");
};
let Value::Bytestring(spki) = v else {
return debug("pub_keys contains non-bytestring value");
};
if spki::parse(spki).is_none() {
return debug("cannot parse SPKI from pub_key entry");
};
if k == AuthLevel::UserVerification.as_str()
|| k == AuthLevel::SoftwareUserVerification.as_str()
{
if has_uv_key {
return debug("can't register both uv and swuv key");
}
has_uv_key = true;
}
}
device.insert(MapKey::String(key), Value::Map(pub_keys));
}
UV_KEY_PENDING => {
device.insert(
MapKey::String(String::from(UV_KEY_PENDING)),
Value::Boolean(true),
);
has_uv_key_pending = true;
}
_ => continue,
}
}
if !device.contains_key(PUB_KEYS_KEY) {
return debug("missing pub_keys");
}
if has_uv_key && has_uv_key_pending {
return debug("can't defer UV key creation when also setting one");
}
fn entry_matches(existing: &Value, new: &BTreeMap<MapKey, Value>) -> bool {
let Value::Map(existing) = existing else {
return false;
};
let Some(Value::Map(existing_pub_keys)) = existing.get(PUB_KEYS_KEY) else {
return false;
};
let Some(Value::Map(new_pub_keys)) = new.get(PUB_KEYS_KEY) else {
return false;
};
let Value::Boolean(existing_uv_key_pending) = existing
.get(UV_KEY_PENDING_KEY)
.unwrap_or(&Value::Boolean(false))
else {
return false;
};
let Value::Boolean(new_uv_key_pending) = new
.get(UV_KEY_PENDING_KEY)
.unwrap_or(&Value::Boolean(false))
else {
return false;
};
existing_pub_keys == new_pub_keys && existing_uv_key_pending == new_uv_key_pending
}
let did_insert = match state.get_mut().get_device_entry(device_id.to_vec())? {
btree_map::Entry::Vacant(entry) => {
entry.insert(Value::Map(device));
true
}
btree_map::Entry::Occupied(entry) => {
if !entry_matches(entry.get(), &device) {
return Err(RequestError::Duplicate);
}
false
}
};
if did_insert {
let Some(Value::Map(wrapping_keys)) =
state.get_mut().confidential.get_mut(WRAPPING_KEYS_KEY)
else {
return debug("malformed confidential data");
};
let mut random_key = [0u8; 32];
crypto::rand_bytes(&mut random_key);
wrapping_keys.insert(
MapKey::Bytestring(device_id.to_vec()),
random_key.to_vec().into(),
);
}
if let Authentication::None = *auth {
*auth = Authentication::NewlyRegistered(device_id.to_vec());
}
metrics.device_register += 1;
Ok(Value::Boolean(true))
}
fn do_device_add_uv_key(
metrics: &mut MetricsUpdate,
auth: &mut Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let (device_id, auth_level, reauth) = match auth {
Authentication::Device(device_id, auth_level, _, reauth) => (device_id, auth_level, reauth),
_ => {
return debug("device identity required");
}
};
let Some(Value::Bytestring(spki)) = request.get(PUB_KEY_KEY) else {
return debug("need pub_key");
};
if spki::parse(spki).is_none() {
return debug("invalid SPKI");
}
let Some(device) = state.get_device(device_id) else {
return debug("no device record");
};
let Some(Value::Map(pub_keys)) = device.get(PUB_KEYS_KEY) else {
return debug("device missing pub_keys");
};
let swuv = MapKey::String(String::from(AuthLevel::SoftwareUserVerification.as_str()));
if pub_keys.contains_key(&swuv) {
return debug("software UV key already registered");
}
let uv = MapKey::String(String::from(AuthLevel::UserVerification.as_str()));
match pub_keys.get(&uv) {
Some(Value::Bytestring(existing_uv_key)) => {
if existing_uv_key == spki {
metrics.device_add_uv_key += 1;
return Ok(Value::Boolean(true));
} else {
return debug("different UV key already registered");
}
}
Some(_) => {
return debug("UV key is wrong type");
}
None => (),
}
match device.get(UV_KEY_PENDING_KEY) {
Some(Value::Boolean(uv_key_pending)) if *uv_key_pending => (),
_ => return debug("uv_key_pending is missing"),
}
let Some(device) = state.get_mut().get_mut_device(device_id) else {
return debug("no device record");
};
device.remove(UV_KEY_PENDING_KEY);
let Some(Value::Map(pub_keys)) = device.get_mut(PUB_KEYS_KEY) else {
return debug("internal error");
};
pub_keys.insert(uv, Value::Bytestring(spki.clone()));
*auth = Authentication::Device(device_id.clone(), *auth_level, OneTimeUV::Consumed, *reauth);
metrics.device_add_uv_key += 1;
Ok(Value::Boolean(true))
}
fn do_device_forget(
metrics: &mut MetricsUpdate,
_auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let Some(Value::Bytestring(device_id)) = request.get(DEVICE_ID_KEY) else {
return debug("missing device_id");
};
let btree_map::Entry::Occupied(entry) = state.get_mut().get_device_entry(device_id.to_vec())?
else {
return Ok(Value::Boolean(false));
};
entry.remove_entry();
metrics.device_forget += 1;
Ok(Value::Boolean(true))
}
fn do_wrap_device_auth_keys(
metrics: &mut MetricsUpdate,
auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
device_authorization_keys: &[DeviceAuthorizationKey],
) -> Result<cbor::Value, RequestError> {
let device_id: &Vec<u8> = match auth {
Authentication::Device(device_id, _, _, _) => device_id,
Authentication::NewlyRegistered(device_id) => device_id,
Authentication::None => {
return debug("device identity required");
}
};
let mut wrapped_device_auth_keys = Vec::with_capacity(device_authorization_keys.len());
for key in device_authorization_keys.iter() {
let purpose = format!(
"{}{}",
KEY_PURPOSE_DEVICE_AUTHORIZATION_KEY_PREFIX, key.version
);
let wrapped_key = state.wrap(device_id, key.key.as_slice(), purpose.as_str())?;
wrapped_device_auth_keys.push(cbor!([(key.version), wrapped_key]));
}
metrics.device_auth_keys_wrap += 1;
Ok(cbor!({
DEVICE_AUTH_KEYS: wrapped_device_auth_keys
}))
}
fn do_keys_genpair(
metrics: &mut MetricsUpdate,
auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let device_id: &Vec<u8> = match auth {
Authentication::Device(device_id, _, _, _) => device_id,
Authentication::NewlyRegistered(device_id) => device_id,
Authentication::None => {
return debug("device identity required");
}
};
let Some(Value::String(purpose)) = request.get(PURPOSE_KEY) else {
return debug("purpose required");
};
let key = crypto::P256Scalar::generate();
metrics.keys_genpair += 1;
Ok(cbor!({
PUB_KEY: (key.compute_public_key().to_vec()),
PRIV_KEY: (state.wrap(device_id, &key.bytes(), purpose)?),
}))
}
fn do_keys_wrap(
metrics: &mut MetricsUpdate,
auth: &Authentication,
state: &mut DirtyFlag<ParsedState>,
request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
let device_id: &Vec<u8> = match auth {
Authentication::Device(device_id, _, _, _) => device_id,
Authentication::NewlyRegistered(device_id) => device_id,
Authentication::None => {
return debug("device identity required");
}
};
let Some(Value::Bytestring(key)) = request.get(KEY_KEY) else {
return debug("key required");
};
let Some(Value::String(purpose)) = request.get(PURPOSE_KEY) else {
return debug("purpose required");
};
metrics.keys_wrap += 1;
Ok(state.wrap(device_id, key, purpose)?.into())
}
fn do_debug_success(metrics: &mut MetricsUpdate) -> Result<cbor::Value, RequestError> {
metrics.debug_success += 1;
Ok(Value::Boolean(true))
}
fn do_debug_dump(
ext_ctx: &ExternalContext,
metrics: &mut MetricsUpdate,
state: &mut DirtyFlag<ParsedState>,
_request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
metrics.debug_dump += 1;
Ok(cbor!({
"transparent": (Value::Map(state.transparent.clone())),
"current_time": (ext_ctx.current_time_epoch_millis),
"reauth": (ext_ctx.is_reauthenticated),
}))
}
#[cfg(test)]
mod tests {
extern crate bytes;
extern crate hex;
extern crate std;
use crate::pin::VaultCohortDetails;
use super::*;
use alloc::boxed::Box;
use alloc::{format, vec};
use cbor::cbor;
use crypto::EcdsaKeyPair;
use passkeys::{
CLAIMED_PIN, CLIENT_DATA_JSON, CLIENT_DATA_JSON_HASH, COSE_ALGORITHM, PIN_CLAIM_KEY,
PIN_HASH, PROTOBUF, PUB_KEY_CRED_PARAMS, RP_ID, WEBAUTHN_REQUEST,
};
use prost::Message;
use recovery_key_store::{CERT_XML, SIG_XML};
const ERR_KEY: &dyn MapLookupKey = &MapKeyRef::Str(ERR) as &dyn MapLookupKey;
pub const SAMPLE_SECURITY_DOMAIN_SECRET : &[u8] = b"\xc4\xdf\xa4\xed\xfc\xf9\x7c\xc0\x3a\xb1\xcb\x3c\x03\x02\x9b\x5a\x05\xec\x88\x48\x54\x42\xf1\x20\xb4\x75\x01\xde\x61\xf1\x39\x5d";
pub const WEBAUTHN_SECRETS_ENCRYPTION_KEY : &[u8] = b"\x55\x9d\xec\xf5\xc3\x42\xbd\xd1\x74\xd3\x3a\x9f\x8f\x8a\x4a\xe0\xf6\x60\x3b\xf8\xe2\xda\x2c\x59\x58\x90\xae\xd9\x3b\xcf\xa8\x18";
pub const PROTOBUF_BYTES : &[u8] = b"\x0a\x10\x78\x0e\x1d\x97\x71\xc7\xc4\x21\x1a\xdf\xf5\x6f\x88\xe8\xf8\x0b\x12\x10\x2e\x32\x3a\x5b\x2a\x6b\xb8\x8f\x8b\x86\x98\x01\xc8\xfd\x55\xff\x1a\x0b\x77\x65\x62\x61\x75\x74\x68\x6e\x2e\x69\x6f\x22\x0f\x52\x57\x35\x6a\x62\x47\x46\x32\x5a\x56\x52\x6c\x63\x33\x51\x30\x9e\x90\xde\xc9\xa5\x31\x3a\x0b\x45\x6e\x63\x6c\x61\x76\x65\x54\x65\x73\x74\x42\x0b\x45\x6e\x63\x6c\x61\x76\x65\x54\x65\x73\x74\x4a\xa6\x01\x7a\x8c\xb5\xf4\x9b\x0a\xeb\xc3\xd7\x7f\xbf\xe5\x25\xcf\x81\x5f\x7e\x2a\xd2\x6b\xe4\xfb\xd7\x71\x14\x2a\x7f\xc7\xe4\xad\xb1\xa2\x9b\xe9\x7a\xac\x56\x9f\x21\xe3\xc3\xa6\x91\x5a\x0a\xd1\x41\x59\xff\xb7\xad\x5a\x3a\x20\x3d\x35\xac\x5c\x8d\xc8\xfe\x2c\x59\x69\x23\x3f\xda\x6c\x3b\xc9\x30\x45\x8b\xc2\x87\x64\x33\xb0\x87\x6d\x55\x48\x96\x36\x39\x03\xc2\x18\x43\xa0\xde\x9c\x47\x37\x58\xb9\x1e\x29\xdf\x14\xcd\x3b\xb8\x19\x02\x7e\xc6\x44\x57\xf0\xce\x1b\x77\xa3\xb5\x63\x08\x81\x1a\x1b\x28\x98\xc3\x6c\xc0\x8e\xd6\x45\xe0\x5d\x14\x98\x3d\x1f\xe6\xba\x9f\xe1\xe5\xe9\x09\xbd\xbf\x85\xe9\xef\xe0\x5c\x9a\xea\x62\xfa\xa5\xe3\xfc\x05\x42\x62\xa7\xeb\x26\xb4\x77\xe0\xe0\x39\x58\x00";
pub const PROTOBUF2_BYTES : &[u8] = b"\x0a\x10\x1d\x3e\xb1\xeb\xd4\x37\x0c\xc1\xfe\xaa\xdc\x49\x7b\x5c\x24\xa1\x12\x10\x8f\xb8\xa3\x31\xd7\xdf\x84\x47\xdb\x3a\x64\x49\xc9\x70\x3f\xfa\x1a\x0b\x77\x65\x62\x61\x75\x74\x68\x6e\x2e\x69\x6f\x22\x10\x52\x57\x35\x6a\x62\x47\x46\x32\x5a\x56\x52\x6c\x63\x33\x51\x79\x30\xe4\xe1\xc2\x82\xa6\x31\x3a\x0c\x45\x6e\x63\x6c\x61\x76\x65\x54\x65\x73\x74\x32\x42\x0c\x45\x6e\x63\x6c\x61\x76\x65\x54\x65\x73\x74\x32\x58\x00\x62\xcb\x01\x3f\x25\xa1\x79\x8b\xc5\x55\x01\x15\xc8\xe5\xb4\xf4\x00\xc6\x03\x70\xc1\x61\xaf\x4a\x02\xeb\xa6\xea\x9b\xd4\x2c\x88\x7b\x80\x59\xfd\xf5\xe9\xef\xf6\xa2\x8a\xbb\xa1\xe8\x44\x91\x8e\x83\x05\x28\x5c\x98\x9a\xd9\xa5\x9a\x99\x74\x05\x47\x67\xc3\x65\xff\xcf\x98\x2f\xfd\xcb\xd4\x6c\x1a\xeb\x8d\xcf\xee\x24\x42\x5b\x14\xfe\x77\x4a\x2d\x4e\x6c\x87\x56\xdb\xf3\x36\x42\x12\xb7\x49\x11\xee\xb6\x97\xa3\x78\xca\xbf\x75\xeb\xe8\x6f\xf5\xa0\xf3\x04\x48\xf5\x99\x44\x4b\x1c\x80\x08\x6a\x37\xe4\x8e\xf9\xbb\xa7\xd2\xa1\xc8\xa1\x89\xf0\x60\x6d\x69\xf8\x3f\x03\x53\x3f\xbd\x9b\x8c\xfd\x82\xf7\x13\xc0\xd3\xae\xf5\x73\x3c\x31\xad\x95\xb4\x4b\xc3\x94\xbc\xd6\x0b\x84\x9b\xe2\x0f\xed\x8f\x25\x1a\x9b\xda\xad\xff\x2f\xe2\xd0\x07\xfc\x6e\xb0\x2a\x78\x0d\xd6\xf5\x83\x42\x66\x10\x4b\xc7\x51\xd5\x01\xb5\x54\xf5\x4a\xcd\x5e\x8c\xdd\xa3";
pub const RSA_PKCS8 : &[u8] = b"\x30\x82\x04\xbd\x02\x01\x00\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00\x04\x82\x04\xa7\x30\x82\x04\xa3\x02\x01\x00\x02\x82\x01\x01\x00\xaa\x67\xa4\x73\xd7\xa3\xf1\x2e\xb5\x54\x03\xc7\x4f\x69\x02\x7e\x64\x74\x3b\x7d\xd2\xe5\xc6\x07\x94\xb3\x38\xf4\xc3\xb6\x2b\xe1\x27\xe0\x95\x90\xdd\x5e\x00\xb9\x64\x5a\x35\xa1\x03\x5b\xf3\x3f\x13\xfe\x74\xb6\x2b\x73\xe9\x0f\xd9\x32\xc6\xf6\x83\x5e\xe4\xbb\xd3\x2a\x77\xb3\xb5\x91\xd5\xa7\x69\x6b\x81\x55\xd8\x13\xb7\x48\xf6\xa6\xa7\x5d\x7c\xcf\x03\x50\x5d\xd6\xc3\x05\xed\x55\x69\xe7\x1c\x59\xef\x2a\x87\xbc\x1a\xfe\x30\xc4\xe8\x29\x54\x13\x61\xdd\x3a\x9d\x1e\x20\xf5\x03\x00\x53\xb1\x98\x05\x88\xc9\xba\xe8\x41\x09\x32\x91\x57\x42\xa9\xf7\x93\xb6\xfb\x16\x0e\x6b\x05\x49\xc4\x19\xe9\x2a\x5b\x37\x19\x0a\xd4\x2c\x1b\x84\x77\x46\x6e\xd8\xbe\x32\x32\xc2\x44\x3a\xaf\xc1\xf5\xf0\xdc\x56\x75\x24\xd6\xe0\xc4\x1c\xae\x63\xe5\xca\x97\x9f\x73\x8c\x70\xf7\xe4\x8f\xf8\x42\xd8\x0c\x14\xa6\xde\x25\xa7\xb8\xd1\xb9\x8b\xd0\x92\x4b\xff\x6e\xee\xe3\x88\x77\xe0\xc4\xe1\xc7\x4a\x2a\x75\x70\xde\x9a\xda\xf3\x27\x2c\x42\xaf\x9c\x00\x4a\x4f\x01\x1d\xa8\x9e\xfc\x86\x05\xbb\x51\x65\x29\x64\x8f\xb1\x5e\x66\xfb\xbc\xdc\x33\x21\x82\x76\x8c\xc3\x02\x03\x01\x00\x01\x02\x82\x01\x00\x44\xe3\x4d\x4a\x3f\x7c\xd9\x3d\xa6\xb4\x66\x2a\xa6\xe1\xae\xce\x65\xd1\xcf\x53\x18\x75\x27\x4f\x5d\x3f\xee\xe0\x94\x56\x0a\xfb\x24\xe1\xd7\xd5\x0e\x88\xb8\x06\x3a\x99\x75\x60\xb8\x38\xed\xe7\x2c\x30\x0c\x02\xb1\x22\x54\xaf\xc1\x80\x93\x8a\x88\xa5\x4e\x16\xd8\x51\x2c\xbf\x0b\xc1\xfe\xfb\x84\xd4\x9f\x1e\x93\x11\xb5\x60\xdb\xc5\x97\x97\x65\xa3\x52\x95\xa4\xb9\xf3\x71\x6b\xf6\xc1\xaf\x5a\x78\xc9\x05\x0a\x86\x72\xeb\x1b\xd0\x1e\x82\xc6\xa8\x67\x41\xc6\x36\x4a\x3d\xcc\x8f\x00\x0c\xd5\x98\xbd\x74\x05\x09\x78\x66\x59\x65\xdf\x37\xf6\x6f\x8b\xb6\xa9\x33\x0c\xd1\xa7\x47\xe8\x57\x4d\x8f\xb8\xd5\x33\xd3\xda\xad\xd9\xab\x3c\xfd\xb7\xec\xfa\x6a\x97\x06\xdd\xb5\x6a\x19\xb5\x5d\x82\xe4\x5d\x0e\xe3\x60\x83\x6f\x72\xe3\x8a\x59\x9f\x5e\x79\xed\x45\x15\x87\xc1\x9a\xa6\x14\xac\x33\x77\xe6\x67\xb2\x2b\xdc\x27\xb3\xa0\x64\xc7\xfc\x08\x30\xff\x0f\x02\x6f\xf1\x54\x6a\x18\xe1\x52\x47\x0a\x4b\x2d\xa7\x94\x79\xa2\xa5\xf4\x30\x14\x08\xf3\xf1\x4a\x02\x64\x69\xdc\x87\x54\x7b\x89\x01\xe1\x77\xa8\x74\x94\xaa\xd5\xc5\x11\x89\x2d\xe6\x3a\xd1\x02\x81\x81\x00\xd5\x7a\x7e\x60\x62\x9a\x39\xcd\x70\xc5\x5f\xd2\x34\x69\x53\xc5\xdc\xc4\x8f\x0e\xea\xd6\xd9\xfa\xe6\x8c\x37\x5f\x7a\xa7\xab\x0a\x98\xa0\x09\x3f\xfe\x7c\xef\x01\x9c\x5d\xc3\x9d\x58\xca\xfa\xb3\xcd\x01\x80\xe3\xd9\xb3\x89\x13\x86\xb7\xbe\x5d\x20\x06\x77\x84\xa1\x60\x0d\x17\x77\xc4\x04\xca\x3a\x5f\x23\x80\x65\x15\x01\x93\xcd\x8a\xd8\x3a\xc7\xa9\xdb\x41\x33\xb1\x49\xb1\xa9\x61\x93\x6e\x08\x0a\x18\xfc\xa7\xd1\xcc\xcc\x88\x35\x23\x5f\x4c\x22\x12\xa4\x52\x80\x53\x57\xfb\x4b\x7d\x65\x23\x1e\xfc\xf5\x13\x0e\x4e\x05\x02\x81\x81\x00\xcc\x58\xc6\xa1\xb6\x75\x90\x60\xb6\x3d\x89\xd1\xbb\x1b\x47\x4d\x33\xc7\x9c\x3c\x6c\xf2\x4b\xbb\x9a\xb2\x1e\x5f\xf7\x6d\x41\x60\xf3\xa2\x2c\xfb\xe3\x77\x4c\x52\xe2\xab\xad\xcf\x09\xdf\x94\x0c\x58\xb0\xcc\x3b\x39\x2f\x71\x61\x2c\x0e\x8e\x6e\xc6\x45\xdd\x78\x2b\xfe\x94\x19\x31\x26\x69\x12\x43\x52\xdb\xcb\x60\x73\x24\x7c\xec\x94\xf3\x13\xc5\x91\x4e\xbb\xec\x3b\x04\x31\xe9\x0a\x81\x1f\xe6\xd4\x3e\x84\xd4\x50\xc6\xbf\xd2\x62\xe5\xd7\x8a\x4f\x18\xca\xc7\xd1\xe0\x99\x9c\xf2\xeb\x23\xd3\x09\xff\x3f\xc8\xfc\x22\x27\x02\x81\x81\x00\x84\x7b\xe0\xb2\x30\x7f\x46\x20\x19\x3c\x64\x9b\x2f\xab\xae\x31\xbd\x30\xbf\x17\xa2\xe6\x73\xa1\x22\x33\x22\xaa\x3e\x94\x8f\xb1\xa3\xc6\xad\xf6\xe9\x18\xdf\xbb\x40\x2f\x70\x96\xd5\xe4\x22\x72\x33\x68\x1b\x75\x4c\x45\xff\x6b\xfe\xcf\x49\x74\xc1\xcb\x41\xa1\x2e\x05\x4e\x1a\xa2\x59\x24\x1f\xdc\xd9\xee\x4e\x60\x6d\x08\xed\x91\x41\xf9\xaf\x80\xfa\x08\xf8\x0d\xfc\x98\x9f\x89\x5e\xe5\x00\x04\x3d\x40\x04\x8c\xa1\xc7\x57\xa7\xb0\x52\xa3\x71\xbc\x33\x95\x87\x1d\xdc\x9b\x5d\x79\x1b\xf9\x08\x32\xd3\x09\xc5\x29\xbb\x81\x02\x81\x80\x2f\xe6\x37\x59\x3c\xad\xbe\x14\x0d\x63\xcb\x64\x70\x19\x6a\xd3\x3b\xe9\xf4\x43\x6d\xbe\x35\xe6\x59\xd2\x9a\xb0\x20\x0d\x6a\x1f\xd1\xbc\x18\x13\x4b\x34\x71\x9d\x94\x28\x6d\xeb\x74\x03\x06\x6f\x06\x73\x1a\xcc\x5f\x11\x31\xe0\x77\x35\x4a\x49\xc9\x0c\x23\x67\xc1\xd8\x40\xda\xce\xdc\x94\x10\x85\xdb\x6c\x4d\xf5\xe3\xc7\x8f\xc8\xdc\xf9\x45\x8f\x30\x0a\x66\x9e\x6f\x0f\x02\xab\xff\x9c\x58\xe0\x00\xac\x4e\xf2\x7d\xa4\xb8\xde\x15\xf4\x8e\x5b\x8b\x42\xe2\x75\x88\x4a\xbf\x77\x3c\xb1\xc5\x89\xf8\x73\xee\x7d\xac\x2c\x4d\x02\x81\x80\x44\x70\x7e\x1d\x0f\x2a\xce\x43\xf5\x0c\x09\x8a\xb7\x81\x4a\x40\xf1\xf3\x09\xa7\x72\xdc\x0a\x7e\x8b\x39\x11\x24\x00\x49\x00\x0e\xab\x74\xf4\xf0\xef\x5e\x1f\xac\x4b\x89\x30\xe8\x95\x45\xcd\x5b\x6a\xa6\x73\xe8\x33\x1e\xb4\x5a\x4c\xe9\x96\xf3\x36\xd9\xe8\xd5\x33\xe4\x8c\x89\xd2\xcb\x0a\xa1\x43\x13\xe5\x67\xe7\x8a\x23\x5d\xd9\xf4\xd7\xff\xce\x4f\x4b\x81\x48\xcd\x54\x9d\xf9\x21\x5d\x5a\x36\x6b\x25\xbb\x9f\xe0\x44\x8c\x1a\x5c\x67\x17\x80\x59\x20\xc4\xf6\x55\x70\xee\x7f\x66\x75\x6d\x20\x2a\xb0\xc3\xd4\xce\xe5\x1a";
pub const RSA_SPKI : &[u8] = b"\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x00\x30\x82\x01\x0a\x02\x82\x01\x01\x00\xaa\x67\xa4\x73\xd7\xa3\xf1\x2e\xb5\x54\x03\xc7\x4f\x69\x02\x7e\x64\x74\x3b\x7d\xd2\xe5\xc6\x07\x94\xb3\x38\xf4\xc3\xb6\x2b\xe1\x27\xe0\x95\x90\xdd\x5e\x00\xb9\x64\x5a\x35\xa1\x03\x5b\xf3\x3f\x13\xfe\x74\xb6\x2b\x73\xe9\x0f\xd9\x32\xc6\xf6\x83\x5e\xe4\xbb\xd3\x2a\x77\xb3\xb5\x91\xd5\xa7\x69\x6b\x81\x55\xd8\x13\xb7\x48\xf6\xa6\xa7\x5d\x7c\xcf\x03\x50\x5d\xd6\xc3\x05\xed\x55\x69\xe7\x1c\x59\xef\x2a\x87\xbc\x1a\xfe\x30\xc4\xe8\x29\x54\x13\x61\xdd\x3a\x9d\x1e\x20\xf5\x03\x00\x53\xb1\x98\x05\x88\xc9\xba\xe8\x41\x09\x32\x91\x57\x42\xa9\xf7\x93\xb6\xfb\x16\x0e\x6b\x05\x49\xc4\x19\xe9\x2a\x5b\x37\x19\x0a\xd4\x2c\x1b\x84\x77\x46\x6e\xd8\xbe\x32\x32\xc2\x44\x3a\xaf\xc1\xf5\xf0\xdc\x56\x75\x24\xd6\xe0\xc4\x1c\xae\x63\xe5\xca\x97\x9f\x73\x8c\x70\xf7\xe4\x8f\xf8\x42\xd8\x0c\x14\xa6\xde\x25\xa7\xb8\xd1\xb9\x8b\xd0\x92\x4b\xff\x6e\xee\xe3\x88\x77\xe0\xc4\xe1\xc7\x4a\x2a\x75\x70\xde\x9a\xda\xf3\x27\x2c\x42\xaf\x9c\x00\x4a\x4f\x01\x1d\xa8\x9e\xfc\x86\x05\xbb\x51\x65\x29\x64\x8f\xb1\x5e\x66\xfb\xbc\xdc\x33\x21\x82\x76\x8c\xc3\x02\x03\x01\x00\x01";
pub const TIMESTAMP: i64 = recovery_key_store::SAMPLE_VALIDATION_EPOCH_MILLIS;
pub const EXTERNAL_CONTEXT: ExternalContext = ExternalContext {
current_time_epoch_millis: TIMESTAMP,
client_device_identifier: Vec::new(),
is_reauthenticated: false,
device_authorization_keys: Vec::new(),
};
pub const TEST_PIN_HASH: [u8; 32] = [1u8; 32];
pub const TEST_CLAIM_KEY: [u8; 32] = [2u8; 32];
pub const TEST_COUNTER_ID: [u8; recovery_key_store::COUNTER_ID_LEN] =
[3u8; recovery_key_store::COUNTER_ID_LEN];
pub const TEST_VAULT_HANDLE_WITHOUT_TYPE: [u8; recovery_key_store::VAULT_HANDLE_LEN - 1] =
[4u8; recovery_key_store::VAULT_HANDLE_LEN - 1];
pub const TEST_CERT_XML_SERIAL_NUMBER: i64 = 10016;
pub const TEST_COHORT_PUBLIC_KEY: [u8; 32] = [4u8; 32];
fn bytes(b: Vec<u8>) -> Value {
Value::Bytestring(Bytes::from(b))
}
fn x962_to_spki(x962: &[u8]) -> Vec<u8> {
const PREFIX : &[u8] = b"\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42\x00";
[PREFIX, x962].concat()
}
lazy_static! {
static ref TEST_DEVICE_ID: Vec<u8> = hex::decode("01020304").unwrap();
static ref TEST_DEVICE_ID2: Vec<u8> = hex::decode("01020305").unwrap();
static ref TEST_HANDSHAKE_HASH: [u8; 32] = [42u8; 32];
static ref KEYPAIR: EcdsaKeyPair = {
let pkcs8_bytes = EcdsaKeyPair::generate_pkcs8();
EcdsaKeyPair::from_pkcs8(pkcs8_bytes.as_ref()).unwrap()
};
static ref SPKI: Vec<u8> = x962_to_spki(KEYPAIR.public_key().as_ref());
static ref REGISTERED_STATE: ClientState = {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"hw": (SPKI.as_slice())},
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, StateUpdate::Major(state)) = process_client_msg(
ClientState::Initial,
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap() else {
panic!("");
};
assert_eq!(output, cbor!([{"ok": true}]));
ClientState::Explicit(state)
};
static ref REGISTERED_STATE_WRAPPED_SECRET: Vec<u8> = {
let msg = sign_request(cbor!({
CMD: "keys/wrap",
KEY: SAMPLE_SECURITY_DOMAIN_SECRET,
PURPOSE: KEY_PURPOSE_SECURITY_DOMAIN_SECRET,
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
let Value::Bytestring(wrapped) = ok_value(&output).unwrap() else {
panic!("unexpected result")
};
wrapped.to_vec()
};
static ref REGISTERED_STATE_UV_PENDING: ClientState = {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"hw": (SPKI.as_slice())},
UV_KEY_PENDING: true,
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, StateUpdate::Major(state)) = process_client_msg(
ClientState::Initial,
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap() else {
panic!("");
};
assert_eq!(output, cbor!([{"ok": true}]));
ClientState::Explicit(state)
};
static ref REGISTERED_STATE_NO_KEYS: ClientState = {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"dummyentry": (SPKI.as_slice())},
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, StateUpdate::Major(state)) = process_client_msg(
ClientState::Initial,
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap() else {
panic!("");
};
assert_eq!(output, cbor!([{"ok": true}]));
ClientState::Explicit(state)
};
static ref ENTITY_PROTOBUF_BYTES: Vec<u8> = {
let msg = sign_request(cbor!({
CMD: "passkeys/create",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
WEBAUTHN_REQUEST: {
PUB_KEY_CRED_PARAMS: [{
COSE_ALGORITHM: (-7),
}],
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let Value::Map(result) = ok_value(&output).unwrap() else {
panic!("wrong type: {:?}", output)
};
let Some(Value::Bytestring(encrypted)) = result.get(passkeys::ENCRYPTED_KEY) else {
panic!("missing encrypted data: {:?}", result)
};
let Some(Value::Bytestring(_)) = result.get(PUB_KEY_KEY) else {
panic!("missing public key: {:?}", result)
};
chromesync::pb::WebauthnCredentialSpecifics {
sync_id: None,
credential_id: Some(vec![4, 3, 2, 1]),
rp_id: None,
user_id: Some(vec![1, 2, 3, 4]),
newly_shadowed_credential_ids: vec![],
creation_time: None,
user_name: None,
user_display_name: None,
third_party_payments_support: None,
last_used_time_windows_epoch_micros: None,
key_version: Some(1),
encrypted_data: Some(
chromesync::pb::webauthn_credential_specifics::EncryptedData::Encrypted(
encrypted.clone(),
),
),
}
.encode_to_vec()
};
static ref RSA_REGISTERED_STATE: ClientState = {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"hw": RSA_SPKI},
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, StateUpdate::Major(state)) = process_client_msg(
ClientState::Initial,
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap() else {
panic!("");
};
assert_eq!(output, cbor!([{"ok": true}]));
ClientState::Explicit(state)
};
static ref RSA_KEYPAIR: crypto::RsaKeyPair =
crypto::RsaKeyPair::from_pkcs8(RSA_PKCS8).unwrap();
static ref DEVICE_AUTHORIZATION_KEYS_UNWRAPPED: Vec<DeviceAuthorizationKey> = vec![
DeviceAuthorizationKey {
version: 1,
key: vec![0u8; 32]
},
DeviceAuthorizationKey {
version: 2,
key: vec![32u8; 32]
},
DeviceAuthorizationKey {
version: 3,
key: vec![255u8; 32]
}
];
}
fn unauthenticated_request(cmd: BTreeMap<MapKey, Value>) -> Vec<u8> {
let encoded_requests = cbor!([(Value::Map(cmd))]).to_bytes();
cbor!({ENCODED_REQUESTS: encoded_requests}).to_bytes()
}
fn sign_authenticated_request<F>(
cmd: BTreeMap<MapKey, Value>,
auth_level: &str,
sign: F,
) -> Vec<u8>
where
F: FnOnce(&[u8]) -> Vec<u8>,
{
let encoded_requests = cbor!([(Value::Map(cmd))]).to_bytes();
let encoded_requests_hash = crypto::sha256(&encoded_requests);
let signed_message = vec![
TEST_HANDSHAKE_HASH.as_slice(),
encoded_requests_hash.as_ref(),
]
.concat();
cbor!({
DEVICE_ID: (TEST_DEVICE_ID.clone()),
AUTH_LEVEL: auth_level,
SIG: (sign(&signed_message).as_slice()),
ENCODED_REQUESTS: encoded_requests,
})
.to_bytes()
}
fn authenticated_request(cmd: BTreeMap<MapKey, Value>) -> Vec<u8> {
sign_authenticated_request(cmd, "hw", |to_be_signed| {
KEYPAIR.sign(to_be_signed).unwrap().as_ref().to_vec()
})
}
fn sign_request(request: Value) -> Vec<u8> {
let Value::Map(map) = request else {
panic!("requests must be maps");
};
authenticated_request(map)
}
fn get_device_entry(state: ClientState) -> BTreeMap<MapKey, Value> {
let ClientState::Explicit(state) = state else {
panic!("");
};
let Ok(Value::Map(mut transparent)) = cbor::parse(state.transparent) else {
panic!("");
};
let Some(Value::Map(devices)) = transparent.get_mut(DEVICES_KEY) else {
panic!("");
};
let Some(Value::Map(device)) = devices.remove(&MapKey::Bytestring(TEST_DEVICE_ID.clone()))
else {
panic!("");
};
device
}
#[test]
fn test_registration_timestamp() {
let device = get_device_entry(REGISTERED_STATE.clone());
let Some(Value::Int(timestamp)) = device.get(REGISTER_TIME_KEY) else {
panic!("");
};
assert_eq!(*timestamp, TIMESTAMP);
if let Some(Value::Int(_timestamp)) = device.get(LAST_USED_KEY) {
panic!("last_used should not be set");
}
}
#[test]
fn test_registration() {
let msg = sign_request(cbor!({CMD: "debug/success"}));
let device_id = vec![1, 2, 3];
let mut metrics = MetricsUpdate::default();
let (output, state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
client_device_identifier: device_id.clone(),
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(is_ok(&output), "{:?}", output);
assert_eq!(
metrics,
MetricsUpdate {
debug_success: 1,
..MetricsUpdate::default()
}
);
let StateUpdate::Minor(new_state) = state else {
panic!("update from debug request was not minor");
};
let device = get_device_entry(ClientState::Explicit(new_state));
let Some(Value::Int(timestamp)) = device.get(LAST_USED_KEY) else {
panic!("");
};
assert_eq!(*timestamp, TIMESTAMP);
let Some(Value::Bytestring(client_device_identifier)) =
device.get(EXTERNAL_DEVICE_IDENTIFIER_KEY)
else {
panic!("");
};
assert_eq!(*client_device_identifier, device_id)
}
#[test]
fn test_rsa_registration() {
let Value::Map(cmd) = cbor!({CMD: "debug/success"}) else {
panic!("!");
};
let msg = sign_authenticated_request(cmd, "hw", |to_be_signed| {
RSA_KEYPAIR.sign(to_be_signed).unwrap().as_ref().to_vec()
});
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
RSA_REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(is_ok(&output), "{:?}", output);
assert_eq!(
metrics,
MetricsUpdate {
debug_success: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_device_register_twice_matching_keys() {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"hw": (SPKI.as_slice())},
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert_eq!(output, cbor!([{"ok": true}]));
assert_eq!(
metrics,
MetricsUpdate {
device_register: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_device_register_twice_mismatching_keys() {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"nothw": (SPKI.as_slice())},
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert_eq!(output, cbor!([{"err": 2}]));
assert_eq!(
metrics,
MetricsUpdate {
error_result: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_device_register_uv_and_uv_pending() {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"uv": (SPKI.as_slice())},
UV_KEY_PENDING: true,
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
ClientState::Initial,
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(!is_ok(&output));
assert_eq!(
metrics,
MetricsUpdate {
error_result: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_device_register_uv_and_swuv() {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"uv": (SPKI.as_slice()), "swuv": (SPKI.as_slice())},
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
ClientState::Initial,
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(!is_ok(&output));
assert_eq!(
metrics,
MetricsUpdate {
error_result: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_device_register_software_uv_and_uv_pending() {
let encoded_register = cbor!([{
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"swuv": (SPKI.as_slice())},
UV_KEY_PENDING: true,
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
ClientState::Initial,
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(!is_ok(&output));
assert_eq!(
metrics,
MetricsUpdate {
error_result: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_device_add_uv_key_without_uv_pending() {
let encoded_register = cbor!([{
CMD: "device/add_uv_key",
PUB_KEY: (SPKI.as_slice()),
}])
.to_bytes();
let msg = cbor!({ENCODED_REQUESTS: encoded_register}).to_bytes();
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(!is_ok(&output));
assert_eq!(
metrics,
MetricsUpdate {
error_result: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_uv_key_missing_metric() {
let Value::Map(cmd) = cbor!({CMD: "debug/success"}) else {
panic!("!");
};
let msg = sign_authenticated_request(cmd, "uv", |to_be_signed| {
KEYPAIR.sign(to_be_signed).unwrap().as_ref().to_vec()
});
let mut metrics = MetricsUpdate::default();
let Err(_) = process_client_msg(
REGISTERED_STATE_UV_PENDING.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
) else {
panic!("should have failed");
};
assert_eq!(
metrics,
MetricsUpdate {
missing_uv_key: 1,
missing_uv_key_with_deferred_bit: 1,
missing_uv_key_with_hw_key_present: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_uv_key_missing_with_no_deferred_uv_and_no_hw_key_metrics() {
let Value::Map(cmd) = cbor!({CMD: "debug/success"}) else {
panic!("!");
};
let msg = sign_authenticated_request(cmd, "uv", |to_be_signed| {
KEYPAIR.sign(to_be_signed).unwrap().as_ref().to_vec()
});
let mut metrics = MetricsUpdate::default();
let Err(_) = process_client_msg(
REGISTERED_STATE_NO_KEYS.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
) else {
panic!("should have failed");
};
assert_eq!(
metrics,
MetricsUpdate {
missing_uv_key: 1,
missing_uv_key_without_deferred_bit: 1,
missing_uv_and_hw_key: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_subsequent_uv() {
let msg = sign_request(cbor!({
CMD: "device/add_uv_key",
PUB_KEY: (SPKI.as_slice()),
}));
let mut metrics = MetricsUpdate::default();
let (output, StateUpdate::Major(state)) = process_client_msg(
REGISTERED_STATE_UV_PENDING.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap() else {
panic!("")
};
assert!(is_ok(&output));
assert_eq!(
metrics,
MetricsUpdate {
device_add_uv_key: 1,
..MetricsUpdate::default()
}
);
let msg = sign_request(cbor!({
CMD: "device/add_uv_key",
PUB_KEY: (SPKI.as_slice()),
}));
let mut metrics = MetricsUpdate::default();
let (output, _update) = process_client_msg(
ClientState::Explicit(state.clone()),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(is_ok(&output));
assert_eq!(
metrics,
MetricsUpdate {
device_add_uv_key: 1,
..MetricsUpdate::default()
}
);
let msg = sign_request(cbor!({
CMD: "device/add_uv_key",
PUB_KEY: RSA_SPKI,
}));
let mut metrics = MetricsUpdate::default();
let (output, _update) = process_client_msg(
ClientState::Explicit(state.clone()),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(!is_ok(&output));
assert_eq!(
metrics,
MetricsUpdate {
error_result: 1,
..MetricsUpdate::default()
}
);
let Value::Map(cmd) = cbor!({CMD: "debug/success"}) else {
panic!("!");
};
let msg = sign_authenticated_request(cmd, "uv", |to_be_signed| {
KEYPAIR.sign(to_be_signed).unwrap().as_ref().to_vec()
});
let mut metrics = MetricsUpdate::default();
let (output, _update) = process_client_msg(
ClientState::Explicit(state),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(is_ok(&output), "{:?}", output);
assert_eq!(
metrics,
MetricsUpdate {
debug_success: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_device_forget() {
let msg = sign_request(cbor!({
CMD: "device/forget",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg,
)
.unwrap();
assert!(is_ok(&output), "{:?}", output);
assert_eq!(
metrics,
MetricsUpdate {
device_forget: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_keys_genpair() {
let msg = sign_request(cbor!({
CMD: "keys/genpair",
PURPOSE: "not yet used",
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let Value::Map(response) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
assert!(matches!(
response.get(PUB_KEY_KEY),
Some(Value::Bytestring(_))
));
assert!(matches!(
response.get(PRIV_KEY_KEY),
Some(Value::Bytestring(_))
));
assert_eq!(
metrics,
MetricsUpdate {
keys_genpair: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_claim_sw_key_for_hw_key() {
let mut metrics = MetricsUpdate::default();
let Value::Map(msg) = cbor!({
CMD: "debug/success"
}) else {
panic!("Not a CBOR map");
};
let request = sign_authenticated_request(msg, "sw", |to_be_signed| {
KEYPAIR.sign(to_be_signed).unwrap().as_ref().to_vec()
});
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
request.clone(),
)
.unwrap();
assert!(is_ok(&output));
}
#[test]
fn test_passkeys_assert() {
let client_data_json =
r#"{"type": "webauthn.get", challenge: "1234", "origin": "example.com"}"#;
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: (PROTOBUF_BYTES.to_vec()),
CLIENT_DATA_JSON: client_data_json,
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let Some(Value::Map(result)) = ok_value(&output) else {
panic!("{:?}", output);
};
let Some(Value::Map(response)) =
result.get(&MapKeyRef::Str("response") as &dyn MapLookupKey)
else {
panic!("{:?}", result);
};
let Some(Value::String(response_client_data_json)) =
response.get(&MapKeyRef::Str("clientDataJSON") as &dyn MapLookupKey)
else {
panic!("{:?}", response);
};
assert_eq!(response_client_data_json, client_data_json);
assert_eq!(
metrics,
MetricsUpdate {
passkeys_assert: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_passkeys_assert_with_hash() {
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: (PROTOBUF_BYTES.to_vec()),
CLIENT_DATA_JSON_HASH: (&[1u8; 32]),
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let Some(Value::Map(result)) = ok_value(&output) else {
panic!("{:?}", output);
};
let Some(Value::Map(response)) =
result.get(&MapKeyRef::Str("response") as &dyn MapLookupKey)
else {
panic!("{:?}", result);
};
assert!(response
.get(&MapKeyRef::Str("clientDataJSON") as &dyn MapLookupKey)
.is_none());
assert_eq!(
metrics,
MetricsUpdate {
passkeys_assert: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_passkeys_assert_missing_client_data_json() {
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: (PROTOBUF_BYTES.to_vec()),
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
assert!(!is_ok(&output));
let error = single_error_string(&output).unwrap();
assert!(
error.contains("either clientDataJson or clientDataJsonHash are required"),
"{:?}",
output
);
}
#[test]
fn test_passkeys_assert_client_data_json_wrong_hash_length() {
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: (PROTOBUF_BYTES.to_vec()),
CLIENT_DATA_JSON_HASH: (&[1u8; 33]),
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
assert!(!is_ok(&output));
let error = single_error_string(&output).unwrap();
assert!(
error.contains("clientDataJsonHash does not match expected length"),
"{:?}",
output
);
}
#[test]
fn test_passkeys_assert_client_data_json_not_a_string() {
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: (PROTOBUF_BYTES.to_vec()),
CLIENT_DATA_JSON: (&[1u8; 32]),
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
assert!(!is_ok(&output));
let error = single_error_string(&output).unwrap();
assert!(
error.contains("clientDataJson is not a string"),
"{:?}",
output
);
}
#[test]
fn test_passkeys_assert_client_data_json_hash_not_a_bytestring() {
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: (PROTOBUF_BYTES.to_vec()),
CLIENT_DATA_JSON_HASH: "not a bytestring",
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
assert!(!is_ok(&output));
let error = single_error_string(&output).unwrap();
assert!(
error.contains("clientDataJsonHash is not a bytestring"),
"{:?}",
output
);
}
#[test]
fn test_passkeys_create() {
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: (ENTITY_PROTOBUF_BYTES.clone()),
CLIENT_DATA_JSON: r#"{"type": "webauthn.get", challenge: "1234", "origin": "example.com"}"#,
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
assert!(is_ok(&output), "{:?}", output);
assert_eq!(
metrics,
MetricsUpdate {
passkeys_assert: 1,
..MetricsUpdate::default()
}
);
}
#[test]
fn test_both_wrapped_and_unwrapped() {
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
SECRET: SAMPLE_SECURITY_DOMAIN_SECRET,
PROTOBUF: (ENTITY_PROTOBUF_BYTES.clone()),
CLIENT_DATA_JSON: r#"{"type": "webauthn.get", challenge: "1234", "origin": "example.com"}"#,
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
assert!(!is_ok(&output));
let error = single_error_string(&output).unwrap();
assert!(error.contains("both wrapped and unwrapped"), "{:?}", output);
assert_eq!(
metrics,
MetricsUpdate {
error_result: 1,
..MetricsUpdate::default()
}
);
}
fn seal_aes_256_gcm(key: &[u8; 32], plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
let mut plaintext = plaintext.to_vec();
let mut nonce = [0u8; 12];
crypto::rand_bytes(&mut nonce);
crypto::aes_256_gcm_seal_in_place(&key, &nonce, aad, &mut plaintext);
[nonce.as_slice(), &plaintext].concat()
}
fn attempt_pin(
state: ClientState,
wrapped_pin_data: &[u8],
pin_claim: &[u8],
) -> (Option<cbor::Value>, PINState, ClientState) {
let msg = sign_request(cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: (ENTITY_PROTOBUF_BYTES.clone()),
WRAPPED_PIN_DATA: wrapped_pin_data,
CLAIMED_PIN: pin_claim,
CLIENT_DATA_JSON: r#"{"type": "webauthn.get", challenge: "1234", "origin": "example.com"}"#,
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
}));
let mut metrics = MetricsUpdate::default();
let (output, state_update) = process_client_msg(
state.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let state_data = match state_update {
StateUpdate::Minor(state_data) => state_data,
StateUpdate::Major(state_data) => state_data,
StateUpdate::None => match state {
ClientState::Explicit(state_data) => state_data,
ClientState::Initial => panic!(""),
},
};
let parsed_state = ClientState::Explicit(state_data.clone()).parse().unwrap();
(
single_response(&output)
.unwrap()
.get(&MapKeyRef::Str("err") as &dyn MapLookupKey)
.cloned(),
parsed_state.get_pin_state(&TEST_DEVICE_ID).unwrap(),
ClientState::Explicit(state_data),
)
}
#[test]
fn test_use_pin() {
let pin_data = pin::Data {
pin_hash: TEST_PIN_HASH.clone(),
claim_key: TEST_CLAIM_KEY.clone(),
counter_id: TEST_COUNTER_ID.clone(),
vault_handle_without_type: TEST_VAULT_HANDLE_WITHOUT_TYPE.clone(),
vault_cohort_details: None,
};
let wrapped_pin_data = pin_data.encrypt(SAMPLE_SECURITY_DOMAIN_SECRET);
let pin_claim = seal_aes_256_gcm(
&pin_data.claim_key,
&pin_data.pin_hash,
passkeys::PIN_CLAIM_AAD,
);
let (error, pin_state, state) =
attempt_pin(REGISTERED_STATE.clone(), &wrapped_pin_data, &pin_claim);
assert!(error.is_none());
assert_eq!(pin_state.attempts, 0);
let (error, pin_state, state) = attempt_pin(state, &wrapped_pin_data, &pin_claim);
assert!(error.is_none());
assert_eq!(pin_state.attempts, 0);
let wrong_pin_hash = [20u8; 32];
let wrong_pin_claim = seal_aes_256_gcm(
&pin_data.claim_key,
&wrong_pin_hash,
passkeys::PIN_CLAIM_AAD,
);
let (error, pin_state, state) = attempt_pin(state, &wrapped_pin_data, &wrong_pin_claim);
assert_eq!(error, Some(Value::Int(3)));
assert_eq!(pin_state.attempts, 1);
let (error, pin_state, state) = attempt_pin(state, &wrapped_pin_data, &pin_claim);
assert!(error.is_none());
assert_eq!(pin_state.attempts, 0);
let (_error, _pin_state, state) = attempt_pin(state, &wrapped_pin_data, &wrong_pin_claim);
let (_error, _pin_state, state) = attempt_pin(state, &wrapped_pin_data, &wrong_pin_claim);
let (_error, _pin_state, state) = attempt_pin(state, &wrapped_pin_data, &wrong_pin_claim);
let (_error, _pin_state, state) = attempt_pin(state, &wrapped_pin_data, &wrong_pin_claim);
let (error, pin_state, state) = attempt_pin(state, &wrapped_pin_data, &wrong_pin_claim);
assert_eq!(error, Some(Value::Int(3)));
assert_eq!(pin_state.attempts, 5);
let (error, pin_state, state) = attempt_pin(state, &wrapped_pin_data, &wrong_pin_claim);
assert_eq!(error, Some(Value::Int(4)));
assert_eq!(pin_state.attempts, 5);
let (error, pin_state, _state) = attempt_pin(state, &wrapped_pin_data, &pin_claim);
assert_eq!(error, Some(Value::Int(4)));
assert_eq!(pin_state.attempts, 5);
}
#[test]
fn test_wrap_pin() {
let msg = sign_request(cbor!({
CMD: "passkeys/wrap_pin",
PIN_HASH: (&TEST_PIN_HASH),
PIN_CLAIM_KEY: (&TEST_CLAIM_KEY),
COUNTER_ID: (&TEST_COUNTER_ID),
VAULT_HANDLE_WITHOUT_TYPE: (&TEST_VAULT_HANDLE_WITHOUT_TYPE),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
}));
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: true,
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let Value::Bytestring(wrapped_pin_data) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
let pin_claim = seal_aes_256_gcm(&TEST_CLAIM_KEY, &TEST_PIN_HASH, passkeys::PIN_CLAIM_AAD);
let (error, pin_state, _) =
attempt_pin(REGISTERED_STATE.clone(), &wrapped_pin_data, &pin_claim);
assert!(error.is_none());
assert_eq!(pin_state.attempts, 0);
}
#[test]
fn test_wrap_pin_with_cohort_details() {
let msg = sign_request(cbor!({
CMD: "passkeys/wrap_pin",
PIN_HASH: (&TEST_PIN_HASH),
PIN_CLAIM_KEY: (&TEST_CLAIM_KEY),
COUNTER_ID: (&TEST_COUNTER_ID),
VAULT_HANDLE_WITHOUT_TYPE: (&TEST_VAULT_HANDLE_WITHOUT_TYPE),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
CERT_XML_SERIAL_NUMBER: (TEST_CERT_XML_SERIAL_NUMBER),
COHORT_PUBLIC_KEY: (&TEST_COHORT_PUBLIC_KEY),
}));
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: true,
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let Value::Bytestring(wrapped_pin_data) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
let pin_claim = seal_aes_256_gcm(&TEST_CLAIM_KEY, &TEST_PIN_HASH, passkeys::PIN_CLAIM_AAD);
let (error, pin_state, _) =
attempt_pin(REGISTERED_STATE.clone(), &wrapped_pin_data, &pin_claim);
assert!(error.is_none());
assert_eq!(pin_state.attempts, 0);
}
#[test]
fn test_wrap_pin_with_cohort_details_error_handling() {
let mut metrics = MetricsUpdate::default();
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: true,
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "passkeys/wrap_pin",
PIN_HASH: (&TEST_PIN_HASH),
PIN_CLAIM_KEY: (&TEST_CLAIM_KEY),
COUNTER_ID: (&TEST_COUNTER_ID),
VAULT_HANDLE_WITHOUT_TYPE: (&TEST_VAULT_HANDLE_WITHOUT_TYPE),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
COHORT_PUBLIC_KEY: (&TEST_COHORT_PUBLIC_KEY),
})),
)
.unwrap();
assert_eq!(output, cbor!([{"err": "cert xml serial number required"}]));
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: true,
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "passkeys/wrap_pin",
PIN_HASH: (&TEST_PIN_HASH),
PIN_CLAIM_KEY: (&TEST_CLAIM_KEY),
COUNTER_ID: (&TEST_COUNTER_ID),
VAULT_HANDLE_WITHOUT_TYPE: (&TEST_VAULT_HANDLE_WITHOUT_TYPE),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
CERT_XML_SERIAL_NUMBER: (TEST_CERT_XML_SERIAL_NUMBER),
})),
)
.unwrap();
assert_eq!(output, cbor!([{"err": "cohort public key required"}]));
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: true,
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "passkeys/wrap_pin",
PIN_HASH: (&TEST_PIN_HASH),
PIN_CLAIM_KEY: (&TEST_CLAIM_KEY),
COUNTER_ID: (&TEST_COUNTER_ID),
VAULT_HANDLE_WITHOUT_TYPE: (&TEST_VAULT_HANDLE_WITHOUT_TYPE),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
CERT_XML_SERIAL_NUMBER: "not a number",
COHORT_PUBLIC_KEY: (&TEST_COHORT_PUBLIC_KEY),
})),
)
.unwrap();
assert_eq!(
output,
cbor!([{"err": "cert xml serial number has wrong format"}])
);
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: true,
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "passkeys/wrap_pin",
PIN_HASH: (&TEST_PIN_HASH),
PIN_CLAIM_KEY: (&TEST_CLAIM_KEY),
COUNTER_ID: (&TEST_COUNTER_ID),
VAULT_HANDLE_WITHOUT_TYPE: (&TEST_VAULT_HANDLE_WITHOUT_TYPE),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
CERT_XML_SERIAL_NUMBER: TEST_CERT_XML_SERIAL_NUMBER,
COHORT_PUBLIC_KEY: "not a bytestring",
})),
)
.unwrap();
assert_eq!(
output,
cbor!([{"err": "cohort public key has wrong format"}])
);
}
#[test]
fn test_wrap_device_auth_keys() {
let msg: Vec<u8> = sign_request(cbor!({
CMD: "device_auth_keys/wrap",
}));
let mut metrics = MetricsUpdate::default();
let (output, state_update) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: true,
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let Value::Map(response) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
let Some(Value::Array(x)) = response.get(DEVICE_AUTH_KEYS_KEY) else {
panic!("{:?}", response);
};
assert_eq!(x.len(), 0);
let StateUpdate::Minor(_) = state_update else {
panic!("{:?}", state_update);
};
let (output, state_update) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: false,
device_authorization_keys: DEVICE_AUTHORIZATION_KEYS_UNWRAPPED.to_vec(),
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
msg.clone(),
)
.unwrap();
let Value::Map(response) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
let Some(Value::Array(ref keys)) = response.get(DEVICE_AUTH_KEYS_KEY) else {
panic!("missing device auth keys: {:?}", response);
};
assert_eq!(keys.len(), DEVICE_AUTHORIZATION_KEYS_UNWRAPPED.len());
let ClientState::Explicit(state_data) = REGISTERED_STATE.clone() else {
panic!("");
};
let parsed_state: ParsedState = ClientState::Explicit(state_data.clone()).parse().unwrap();
for (i, unwrapped) in DEVICE_AUTHORIZATION_KEYS_UNWRAPPED.iter().enumerate() {
let Some(Value::Array(ref version_and_key)) = keys.get(i) else {
panic!("invalid key format: {:?}", keys);
};
let [Value::Int(version), Value::Bytestring(ref key)] = version_and_key.to_vec()[..]
else {
panic!("invalid key format: {:?}", version_and_key);
};
assert_eq!(version, unwrapped.version);
let aad = format!("{}{}", "device authorization key v_", version);
let unwrapped_key = parsed_state
.unwrap(&TEST_DEVICE_ID.clone(), key, aad.as_str())
.unwrap();
assert_eq!(unwrapped_key, unwrapped.key);
}
let StateUpdate::Minor(_) = state_update else {
panic!("{:?}", state_update);
};
}
fn is_single_error_response(value: &Value) -> bool {
let Value::Array(array) = value else {
return false;
};
matches!(&array[..], [Value::Map(map)] if map.contains_key(ERR_KEY))
}
fn single_response(value: &Value) -> Option<&BTreeMap<cbor::MapKey, cbor::Value>> {
let Value::Array(array) = value else {
return None;
};
let [first] = &array[..] else {
return None;
};
let Value::Map(map) = first else {
return None;
};
Some(map)
}
fn ok_value(value: &Value) -> Option<&cbor::Value> {
single_response(value)?.get(&MapKeyRef::Str("ok") as &dyn MapLookupKey)
}
fn is_ok(value: &Value) -> bool {
ok_value(value).is_some()
}
fn single_error_string(value: &Value) -> Option<&str> {
let error = single_response(value)?.get(&MapKeyRef::Str("err") as &dyn MapLookupKey)?;
let Value::String(error) = error else {
return None;
};
Some(error)
}
struct MutatedMap {
map: BTreeMap<MapKey, Value>,
should_fail: bool,
debug: String,
}
#[derive(Default)]
struct MutationConfig {
is_optional: bool,
invalid_values: Option<Vec<Value>>,
subconfig: Option<Box<BTreeMap<String, MutationConfig>>>,
}
fn mutate_map(
map: &BTreeMap<MapKey, Value>,
configs: &BTreeMap<String, MutationConfig>,
) -> Vec<MutatedMap> {
let default_config: MutationConfig = Default::default();
let mut ret: Vec<MutatedMap> = Vec::new();
for key in map.keys() {
let MapKey::String(key_str) = key else {
panic!("only string-keyed maps expected");
};
let config = configs.get(key_str).unwrap_or(&default_config);
let mut mutated = map.clone();
mutated.remove(key);
ret.push(MutatedMap {
map: mutated.clone(),
should_fail: !config.is_optional,
debug: format!("removed {key_str}"),
});
let mut mutated = map.clone();
let Some(value) = mutated.remove(key as &dyn MapLookupKey) else {
panic!("impossible");
};
mutated.insert(
key.clone(),
match value {
Value::String(_) => Value::Boolean(true),
Value::Bytestring(_) => Value::Boolean(true),
Value::Array(_) => Value::Boolean(true),
Value::Map(_) => Value::Boolean(true),
Value::Int(_) => Value::Boolean(true),
Value::Boolean(_) => Value::Int(42),
},
);
ret.push(MutatedMap {
map: mutated,
should_fail: !config.is_optional,
debug: format!("mutated {key_str}"),
});
if let Some(invalid_values) = &config.invalid_values {
for value in invalid_values {
let mut mutated = map.clone();
mutated.insert(key.clone(), value.clone());
ret.push(MutatedMap {
map: mutated,
should_fail: true,
debug: format!("invalid for {key_str}"),
});
}
}
if let Some(subconfig) = &config.subconfig {
let mut mutated = map.clone();
let Some(Value::Map(map)) = mutated.remove(key) else {
panic!("subconfig provided for non-map {key_str}");
};
for mutation in mutate_map(&map, subconfig) {
mutated.insert(key.clone(), Value::Map(mutation.map));
ret.push(MutatedMap {
map: mutated.clone(),
should_fail: mutation.should_fail,
debug: format!("mutating {key_str}: {}", mutation.debug),
});
}
}
}
ret
}
struct MutatedRequest {
request: Vec<u8>,
should_fail: bool,
debug: String,
}
enum RequestAuthentication {
Required,
Never,
}
fn mutate_request(
request: &BTreeMap<MapKey, Value>,
authentication: RequestAuthentication,
configs: &BTreeMap<String, MutationConfig>,
) -> Vec<MutatedRequest> {
let serialize = if matches!(authentication, RequestAuthentication::Never) {
unauthenticated_request
} else {
authenticated_request
};
let mut ret: Vec<MutatedRequest> = Vec::new();
ret.push(MutatedRequest {
request: serialize(request.clone()),
should_fail: false,
debug: String::from("unmodified"),
});
if !matches!(authentication, RequestAuthentication::Never) {
ret.push(MutatedRequest {
request: unauthenticated_request(request.clone()),
should_fail: true,
debug: String::from("unauthenticated"),
});
}
ret.extend(
mutate_map(request, configs)
.into_iter()
.map(|mutated_map| MutatedRequest {
request: serialize(mutated_map.map),
should_fail: mutated_map.should_fail,
debug: mutated_map.debug,
}),
);
ret
}
fn test_invalid_requests(
request: &Value,
initial_state: ClientState,
authentication: RequestAuthentication,
configs: &BTreeMap<String, MutationConfig>,
) {
let Value::Map(request) = request else {
panic!("requests must be maps");
};
for mutated_request in mutate_request(request, authentication, configs) {
let mut metrics = MetricsUpdate::default();
let (output, _state) = process_client_msg(
initial_state.clone(),
&mut metrics,
ExternalContext {
is_reauthenticated: true,
..EXTERNAL_CONTEXT.clone()
},
TEST_HANDSHAKE_HASH.as_slice(),
mutated_request.request,
)
.unwrap();
if mutated_request.should_fail {
assert!(
is_single_error_response(&output),
"{}: {:?}",
mutated_request.debug,
output
);
} else {
assert!(is_ok(&output), "{}: {:?}", mutated_request.debug, output);
}
}
}
#[test]
fn test_invalid_device_register() {
let request = cbor!({
CMD: "device/register",
DEVICE_ID: (TEST_DEVICE_ID.clone()),
PUB_KEYS: {"hw": (SPKI.as_slice())},
});
let configs = BTreeMap::from([
(
String::from(DEVICE_ID),
MutationConfig {
invalid_values: Some(vec![bytes((0..=255).collect())]),
..Default::default()
},
),
(
String::from(PUB_KEYS),
MutationConfig {
subconfig: Some(Box::new(Default::default())),
..Default::default()
},
),
]);
test_invalid_requests(
&request,
ClientState::Initial,
RequestAuthentication::Never,
&configs,
);
}
#[test]
fn test_invalid_device_add_uv_key() {
let request = cbor!({
CMD: "device/add_uv_key",
PUB_KEY: (SPKI.as_slice()),
});
let configs = BTreeMap::from([]);
test_invalid_requests(
&request,
REGISTERED_STATE_UV_PENDING.clone(),
RequestAuthentication::Required,
&configs,
);
}
#[test]
fn test_invalid_passkeys_assert() {
let request = cbor!({
CMD: "passkeys/assert",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
PROTOBUF: PROTOBUF_BYTES,
CLIENT_DATA_JSON: r#"{"type": "webauthn.get", challenge: "1234", "origin": "example.com"}"#,
WEBAUTHN_REQUEST: {
RP_ID: "example.com",
},
});
let configs = BTreeMap::from([
(
String::from(passkeys::PROTOBUF),
MutationConfig {
invalid_values: Some(vec![bytes((0..128).collect())]),
..Default::default()
},
),
(
String::from(passkeys::WEBAUTHN_REQUEST),
MutationConfig {
subconfig: Some(Box::new(BTreeMap::new())),
..Default::default()
},
),
]);
test_invalid_requests(
&request,
REGISTERED_STATE.clone(),
RequestAuthentication::Required,
&configs,
);
}
#[test]
fn test_invalid_passkeys_create() {
let request = cbor!({
CMD: "passkeys/create",
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
WEBAUTHN_REQUEST: {
PUB_KEY_CRED_PARAMS: [{
COSE_ALGORITHM: (-7),
}],
},
});
let configs = BTreeMap::from([(
String::from(passkeys::COSE_ALGORITHM),
MutationConfig {
invalid_values: Some(vec![Value::Array(vec![Value::Int(-1)])]),
..Default::default()
},
)]);
test_invalid_requests(
&request,
REGISTERED_STATE.clone(),
RequestAuthentication::Required,
&configs,
);
}
#[test]
fn test_invalid_passkeys_wrap_pin() {
let pin_hash = [1u8; 32];
let claim_key = [2u8; 32];
let counter_id = [3u8; recovery_key_store::COUNTER_ID_LEN];
let vault_handle_without_type = [4u8; recovery_key_store::VAULT_HANDLE_LEN - 1];
let request = cbor!({
CMD: "passkeys/wrap_pin",
PIN_HASH: (&pin_hash),
PIN_CLAIM_KEY: (&claim_key),
COUNTER_ID: (&counter_id),
VAULT_HANDLE_WITHOUT_TYPE: (&vault_handle_without_type),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
});
let configs = BTreeMap::from([]);
test_invalid_requests(
&request,
REGISTERED_STATE.clone(),
RequestAuthentication::Required,
&configs,
);
}
#[test]
fn test_invalid_recovery_key_store_wrap() {
let pin_hash = [1u8; 32];
let request = cbor!({
CMD: "recovery_key_store/wrap",
PIN_HASH: (&pin_hash),
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
});
let configs = BTreeMap::from([]);
test_invalid_requests(
&request,
REGISTERED_STATE.clone(),
RequestAuthentication::Never,
&configs,
);
}
#[test]
fn test_invalid_recovery_key_store_wrap_as_member() {
let pin_hash = [1u8; 32];
let request = cbor!({
CMD: "recovery_key_store/wrap_as_member",
PIN_HASH: (&pin_hash),
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
COUNTER_ID: (&[3u8; recovery_key_store::COUNTER_ID_LEN]),
VAULT_HANDLE_WITHOUT_TYPE: (&[4u8; recovery_key_store::VAULT_HANDLE_LEN - 1]),
});
let configs = BTreeMap::from([]);
test_invalid_requests(
&request,
REGISTERED_STATE.clone(),
RequestAuthentication::Required,
&configs,
);
}
#[test]
fn test_wrap_pin_and_secret_resets_pin_counter() {
let pin_data = pin::Data {
pin_hash: TEST_PIN_HASH.clone(),
claim_key: TEST_CLAIM_KEY.clone(),
counter_id: TEST_COUNTER_ID.clone(),
vault_handle_without_type: TEST_VAULT_HANDLE_WITHOUT_TYPE.clone(),
vault_cohort_details: None,
};
let wrapped_pin_data = pin_data.encrypt(SAMPLE_SECURITY_DOMAIN_SECRET);
let state = REGISTERED_STATE.clone();
let wrong_pin_hash = [20u8; 32];
let wrong_pin_claim = seal_aes_256_gcm(
&pin_data.claim_key,
&wrong_pin_hash,
passkeys::PIN_CLAIM_AAD,
);
let (error, pin_state, state) = attempt_pin(state, &wrapped_pin_data, &wrong_pin_claim);
assert_eq!(error, Some(Value::Int(3)));
assert_eq!(pin_state.attempts, 1);
let mut metrics = MetricsUpdate::default();
let pin_hash = [1u8; 32];
let pin_claim_key = [2u8; 32];
let request = cbor!({
CMD: "recovery_key_store/wrap_pin_and_secret",
PIN_HASH: (&pin_hash),
PIN_CLAIM_KEY: (&pin_claim_key),
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
});
let mut context = EXTERNAL_CONTEXT.clone();
context.is_reauthenticated = true;
let (output, state_update) = process_client_msg(
state,
&mut metrics,
context,
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(request),
)
.unwrap();
ok_value(&output).unwrap();
let state_data = match state_update {
StateUpdate::Major(state_data) => state_data,
_ => panic!("Expected major state change"),
};
let state = ClientState::Explicit(state_data).parse().unwrap();
assert_eq!(state.get_pin_state(&TEST_DEVICE_ID).unwrap().attempts, 0);
}
#[test]
fn test_wrap_pin_and_secret_parameters_match() {
let mut metrics = MetricsUpdate::default();
let pin_hash = [1u8; 32];
let pin_claim_key = [2u8; 32];
let request = cbor!({
CMD: "recovery_key_store/wrap_pin_and_secret",
PIN_HASH: (&pin_hash),
PIN_CLAIM_KEY: (&pin_claim_key),
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
});
let mut context = EXTERNAL_CONTEXT.clone();
context.is_reauthenticated = true;
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
context,
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(request),
)
.unwrap();
let Value::Map(result) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
let Value::Bytestring(wrapped_pin) = result
.get(&MapKeyRef::Str("wrapped_pin") as &dyn MapLookupKey)
.unwrap()
else {
panic!("{:?}", result);
};
let pin_data = pin::Data::from_wrapped(wrapped_pin, SAMPLE_SECURITY_DOMAIN_SECRET).unwrap();
let Value::Map(vault_params) = result
.get(&MapKeyRef::Str("wrapped") as &dyn MapLookupKey)
.unwrap()
else {
panic!("{:?}", result);
};
let Value::Bytestring(vault_counter_id) = vault_params
.get(&MapKeyRef::Str("counter_id") as &dyn MapLookupKey)
.unwrap()
else {
panic!("Could not find vault counter ID");
};
let Value::Bytestring(vault_handle) = vault_params
.get(&MapKeyRef::Str("vault_handle") as &dyn MapLookupKey)
.unwrap()
else {
panic!("Could not find vault handle");
};
let Value::Int(cert_xml_serial) = vault_params
.get(&MapKeyRef::Str("serial") as &dyn MapLookupKey)
.unwrap()
else {
panic!("Could not find serial number");
};
let Value::Bytestring(cohort_public_key) = vault_params
.get(&MapKeyRef::Str("cohort_public_key") as &dyn MapLookupKey)
.unwrap()
else {
panic!("Could not find cohort public key");
};
assert_eq!(pin_data.pin_hash, pin_hash);
assert_eq!(pin_data.claim_key, pin_claim_key);
assert_eq!(pin_data.counter_id.to_vec(), vault_counter_id.to_vec());
assert_eq!(
pin_data.vault_handle_without_type.to_vec(),
vault_handle[1..].to_vec()
);
assert_eq!(
pin_data
.vault_cohort_details
.as_ref()
.unwrap()
.cert_xml_serial_number,
*cert_xml_serial
);
assert_eq!(
pin_data
.vault_cohort_details
.unwrap()
.cohort_public_key
.to_vec(),
cohort_public_key.to_vec()
);
}
#[test]
fn test_invalid_recovery_key_store_rewrap() {
let pin_data = pin::Data {
pin_hash: [1u8; 32],
claim_key: [2u8; 32],
counter_id: [3u8; recovery_key_store::COUNTER_ID_LEN],
vault_handle_without_type: [4u8; recovery_key_store::VAULT_HANDLE_LEN - 1],
vault_cohort_details: None,
};
let wrapped_pin_data = pin_data.encrypt(SAMPLE_SECURITY_DOMAIN_SECRET);
let request = cbor!({
CMD: "recovery_key_store/rewrap",
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
WRAPPED_PIN_DATA: wrapped_pin_data,
});
let configs = BTreeMap::from([]);
test_invalid_requests(
&request,
REGISTERED_STATE.clone(),
RequestAuthentication::Required,
&configs,
);
}
#[test]
fn test_rewrap_updates_wrapped_pin_with_cohort_details() {
let mut metrics = MetricsUpdate::default();
let pin_data = pin::Data {
pin_hash: [1u8; 32],
claim_key: [2u8; 32],
counter_id: [3u8; recovery_key_store::COUNTER_ID_LEN],
vault_handle_without_type: [4u8; recovery_key_store::VAULT_HANDLE_LEN - 1],
vault_cohort_details: None,
};
let wrapped_pin_data = pin_data.encrypt(SAMPLE_SECURITY_DOMAIN_SECRET);
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "recovery_key_store/rewrap",
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
WRAPPED_PIN_DATA: wrapped_pin_data,
})),
)
.unwrap();
let Value::Map(result) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
let Value::Bytestring(wrapped_pin) = result
.get(&MapKeyRef::Str("wrapped_pin") as &dyn MapLookupKey)
.unwrap()
else {
panic!("{:?}", result);
};
let result_pin_data =
pin::Data::from_wrapped(wrapped_pin, SAMPLE_SECURITY_DOMAIN_SECRET).unwrap();
assert_eq!(result_pin_data.pin_hash, pin_data.pin_hash);
assert_eq!(result_pin_data.claim_key, pin_data.claim_key);
assert_eq!(result_pin_data.counter_id, pin_data.counter_id);
assert_eq!(
result_pin_data.vault_handle_without_type,
pin_data.vault_handle_without_type
);
let vault_cohort_details = result_pin_data.vault_cohort_details.unwrap();
assert_eq!(
vault_cohort_details.cert_xml_serial_number,
TEST_CERT_XML_SERIAL_NUMBER
);
assert!(!vault_cohort_details.cohort_public_key.is_empty());
}
#[test]
fn test_rewrap_new_vault_mode_handles_no_vault_cohort_details() {
let mut metrics = MetricsUpdate::default();
let pin_data = pin::Data {
pin_hash: [1u8; 32],
claim_key: [2u8; 32],
counter_id: [3u8; recovery_key_store::COUNTER_ID_LEN],
vault_handle_without_type: [4u8; recovery_key_store::VAULT_HANDLE_LEN - 1],
vault_cohort_details: None,
};
let wrapped_pin_data = pin_data.encrypt(SAMPLE_SECURITY_DOMAIN_SECRET);
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "recovery_key_store/rewrap",
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
CREATE_NEW_VAULT: (true),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
WRAPPED_PIN_DATA: wrapped_pin_data,
})),
)
.unwrap();
let Value::Map(result) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
let Value::Bytestring(wrapped_pin) = result
.get(&MapKeyRef::Str("wrapped_pin") as &dyn MapLookupKey)
.unwrap()
else {
panic!("{:?}", result);
};
let result_pin_data =
pin::Data::from_wrapped(wrapped_pin, SAMPLE_SECURITY_DOMAIN_SECRET).unwrap();
assert_eq!(result_pin_data.pin_hash, pin_data.pin_hash);
assert_eq!(result_pin_data.claim_key, pin_data.claim_key);
assert_eq!(result_pin_data.counter_id, pin_data.counter_id);
assert_eq!(
result_pin_data.vault_handle_without_type,
pin_data.vault_handle_without_type
);
let vault_cohort_details = result_pin_data.vault_cohort_details.unwrap();
assert_eq!(
vault_cohort_details.cert_xml_serial_number,
TEST_CERT_XML_SERIAL_NUMBER
);
assert!(!vault_cohort_details.cohort_public_key.is_empty());
}
#[test]
fn test_rewrap_new_vault_mode_downgrade_cert_xml() {
let mut metrics = MetricsUpdate::default();
let pin_data = pin::Data {
pin_hash: [1u8; 32],
claim_key: [2u8; 32],
counter_id: [3u8; recovery_key_store::COUNTER_ID_LEN],
vault_handle_without_type: [4u8; recovery_key_store::VAULT_HANDLE_LEN - 1],
vault_cohort_details: Some(VaultCohortDetails {
cert_xml_serial_number: TEST_CERT_XML_SERIAL_NUMBER + 1,
cohort_public_key: vec![1, 2, 3, 4],
}),
};
let wrapped_pin_data = pin_data.encrypt(SAMPLE_SECURITY_DOMAIN_SECRET);
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "recovery_key_store/rewrap",
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
CREATE_NEW_VAULT: (true),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
WRAPPED_PIN_DATA: wrapped_pin_data,
})),
)
.unwrap();
assert_eq!(
output,
cbor!([{"err": (RequestError::RecoveryKeyStoreDowngrade.to_cbor())}])
);
}
#[test]
fn test_rewrap_new_vault_mode_cohort_not_yet_deprecated() {
let mut metrics = MetricsUpdate::default();
let pin_data = pin::Data {
pin_hash: [1u8; 32],
claim_key: [2u8; 32],
counter_id: [3u8; recovery_key_store::COUNTER_ID_LEN],
vault_handle_without_type: [4u8; recovery_key_store::VAULT_HANDLE_LEN - 1],
vault_cohort_details: Some(VaultCohortDetails {
cert_xml_serial_number: TEST_CERT_XML_SERIAL_NUMBER,
cohort_public_key: recovery_key_store::SAMPLE_ENDPOINT_PUBLIC_KEY.to_vec(),
}),
};
let wrapped_pin_data = pin_data.encrypt(SAMPLE_SECURITY_DOMAIN_SECRET);
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "recovery_key_store/rewrap",
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
CREATE_NEW_VAULT: (true),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
WRAPPED_PIN_DATA: wrapped_pin_data,
})),
)
.unwrap();
assert_eq!(
output,
cbor!([{"err": (RequestError::CohortNotYetDeprecated.to_cbor())}])
);
}
#[test]
fn test_rewrap_new_vault_mode_cohort_deprecated() {
let mut metrics = MetricsUpdate::default();
let pin_data = pin::Data {
pin_hash: [1u8; 32],
claim_key: [2u8; 32],
counter_id: [3u8; recovery_key_store::COUNTER_ID_LEN],
vault_handle_without_type: [4u8; recovery_key_store::VAULT_HANDLE_LEN - 1],
vault_cohort_details: Some(VaultCohortDetails {
cert_xml_serial_number: TEST_CERT_XML_SERIAL_NUMBER - 1,
cohort_public_key: b"Deprecated".to_vec(),
}),
};
let wrapped_pin_data = pin_data.encrypt(SAMPLE_SECURITY_DOMAIN_SECRET);
let (output, _) = process_client_msg(
REGISTERED_STATE.clone(),
&mut metrics,
EXTERNAL_CONTEXT.clone(),
TEST_HANDSHAKE_HASH.as_slice(),
sign_request(cbor!({
CMD: "recovery_key_store/rewrap",
CERT_XML: (recovery_key_store::SAMPLE_CERTS_XML),
CREATE_NEW_VAULT: (true),
SIG_XML: (recovery_key_store::SAMPLE_SIG_XML),
WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.clone()),
WRAPPED_PIN_DATA: wrapped_pin_data,
})),
)
.unwrap();
let Value::Map(result) = ok_value(&output).unwrap() else {
panic!("{:?}", output);
};
let Value::Bytestring(wrapped_pin) = result
.get(&MapKeyRef::Str("wrapped_pin") as &dyn MapLookupKey)
.unwrap()
else {
panic!("{:?}", result);
};
let result_pin_data =
pin::Data::from_wrapped(wrapped_pin, SAMPLE_SECURITY_DOMAIN_SECRET).unwrap();
assert_eq!(result_pin_data.pin_hash, pin_data.pin_hash);
assert_eq!(result_pin_data.claim_key, pin_data.claim_key);
assert_eq!(result_pin_data.counter_id, pin_data.counter_id);
let mut expected_vault_handle = pin_data.vault_handle_without_type;
*expected_vault_handle.last_mut().unwrap() += 1;
assert_eq!(
result_pin_data.vault_handle_without_type,
expected_vault_handle
);
let vault_cohort_details = result_pin_data.vault_cohort_details.unwrap();
assert_eq!(
vault_cohort_details.cert_xml_serial_number,
TEST_CERT_XML_SERIAL_NUMBER
);
assert!(!vault_cohort_details.cohort_public_key.is_empty());
let Value::Map(vault_params) = result
.get(&MapKeyRef::Str("wrapped") as &dyn MapLookupKey)
.unwrap()
else {
panic!("{:?}", result);
};
let Value::Bytestring(vault_counter_id) = vault_params
.get(&MapKeyRef::Str("counter_id") as &dyn MapLookupKey)
.unwrap()
else {
panic!("Could not find vault counter ID");
};
let Value::Bytestring(vault_handle) = vault_params
.get(&MapKeyRef::Str("vault_handle") as &dyn MapLookupKey)
.unwrap()
else {
panic!("Could not find vault handle");
};
let Value::Int(cert_xml_serial) = vault_params
.get(&MapKeyRef::Str("serial") as &dyn MapLookupKey)
.unwrap()
else {
panic!("Could not find serial number");
};
let Value::Bytestring(cohort_public_key) = vault_params
.get(&MapKeyRef::Str("cohort_public_key") as &dyn MapLookupKey)
.unwrap()
else {
panic!("Could not find cohort public key");
};
assert_eq!(vault_counter_id.to_vec(), pin_data.counter_id.to_vec());
assert_eq!(vault_handle[1..].to_vec(), expected_vault_handle);
assert_eq!(*cert_xml_serial, TEST_CERT_XML_SERIAL_NUMBER);
assert_eq!(
vault_cohort_details.cohort_public_key.to_vec(),
cohort_public_key.to_vec()
);
}
#[test]
fn test_invalid_wrap_device_auth_keys() {
let request = cbor!({
CMD: "device_auth_keys/wrap",
});
let configs = BTreeMap::from([]);
test_invalid_requests(
&request,
REGISTERED_STATE.clone(),
RequestAuthentication::Required,
&configs,
);
}
}