chromium/third_party/cloud_authenticator/processor/src/lib.rs

// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! `processor` updates the account's state in response to a request.
//!
//! The account's state is passed as a pair of bytestrings (in [`StateData`]).
//! The `transparent` data must be at least integrity protected. The
//! `confidential` data must have confidentiality and integrity.
//!
//! The caller must ensure that the account's state is updated atomically. So
//! if the account state was changed by something else while this code was also
//! processing it then the output of this code must be discarded. This code
//! doesn't have side effects so it's reasonable to try apply the commands again
//! to resolve this.
//!
//! The client's request is an array of one of more commands. Commands are
//! applied in sequence until either all have been applied, or else a command
//! fails. The output of a request is an array of results, one from each
//! applied command plus, optionally, one error result from the failed command.
//!
//! Partially successful requests are valid and the account's state should still
//! be updated.
//!
//! Not all commands must be authenticated. It's assumed that some level of
//! authentication (i.e. access to the account) has already been confirmed.

#![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;

// When building for fuzzing, these functions are re-exported so the fuzzer can
// call them.
#[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 bytes::Bytes;
use cbor::{cbor, MapKey, MapKeyRef, MapLookupKey, Value};

/// Holds the account state to which the commands are applied.
///
/// If an account has never been used before, its state is [`Initial`].
/// Otherwise there is state stored for the account.
#[derive(Clone)]
pub enum ClientState {
    Initial,
    Explicit(StateData),
}

/// A `StateUpdate` is produced by successfully processing a client message.
pub enum StateUpdate {
    /// This update contains important changes. The response should not be sent
    /// to the client until it has been accepted by the storage system.
    Major(StateData),
    /// This update contains minor changes. The response can be sent to the
    /// client immediately and it's ok if this update is lost.
    Minor(StateData),
    /// There is no change to the state.
    None,
}

/// Holds stored account state.
///
/// The `transparent` data must be at least integrity protected. The
/// `confidential` data must have confidentiality and integrity. These
/// properties must be provided within the enclave, but outside of this module.
#[derive(Clone)]
pub struct StateData {
    pub transparent: Vec<u8>,
    pub confidential: Vec<u8>,
}

/// Represents fatal processing errors.
///
/// Individual commands within an request can fail without being fatal of the
/// whole request. But some errors doom the whole request and are represented
/// here.
#[derive(Debug)]
pub enum Error {
    // These errors indicate an internal error with the enclave system because
    // account state should never be corrupt.
    TransparentDataCBORError(cbor::Error),
    ConfidentialDataCBORError(cbor::Error),

    // This error is worth distinguishing to the client because it indicates
    // that it is not recognised and thus no authenticated request will ever
    // be accepted from it.
    UnknownClient,

    // A large number of errors are not distinguished and are just strings.
    // The only exception is an error while parsing the client's request, since
    // we can include the detail of the CBOR parse error, which may be useful
    // for debugging.
    Str(&'static str),
    CBORError(cbor::Error),
}

/// ExternalContext contains context about a client request that comes from
/// server-side components outside of this enclave.
#[derive(Clone)]
pub struct ExternalContext {
    /// The current time, in milliseconds since the UNIX epoch.
    pub current_time_epoch_millis: i64,
    /// An opaque identifier for the device that the client's request came from.
    /// This will be recorded in the enclave's "transparent" state for this
    /// device.
    pub client_device_identifier: Vec<u8>,
    /// A signal that this client performed reauthentication very recently. This
    /// can authorize some actions.
    pub is_reauthenticated: bool,
}

// These constants are map keys used within the CBOR. For each map key constant
// there is also a `*_KEY` constant that can be used to lookup that key in a
// `BTreeMap<MapKey, Value>`. (Looking up enum keys in a map without allocating
// is a little awkward in Rust.)

const OK: &str = "ok";
const ERR: &str = "err";

// The "purpose" value of a security domain secret. Used when the client
// presents a wrapped secret that will be used as such.
pub(crate) const KEY_PURPOSE_SECURITY_DOMAIN_SECRET: &str = "security domain secret";

map_keys! {
    AUTH_LEVEL, AUTH_LEVEL_KEY = "auth_level",
    CMD, CMD_KEY = "cmd",
    COUNTER_ID, COUNTER_ID_KEY = "counter_id",
    DEVICE_ID, DEVICE_ID_KEY = "device_id",
    DEVICES, DEVICES_KEY = "devices",
    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",
}

// Since AES-GCM can only handle 2^32 encryptions per key, the per-registration
// keys use a two-step construction where the nonce is a pair of 96-bit values.
// The first of the pair is used with HKDF to derive an AES-GCM key, and the
// second of the pair is the standard AES-GCM nonce.

const LARGE_NONCE_LEN: usize = 24;
const AES256_KEY_LEN: usize = 32;
const GCM_OVERHEAD: usize = 16;

/// Return an AES-256-GCM key for encrypting account data, plus the GCM
/// nonce to use.
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];
    // unwrap: only fails if output is too long, but output here is only 32 bytes.
    crypto::hkdf_sha256(wrapping_key, key_nonce, b"derive wrapping key", &mut gcm_key).unwrap();
    // unwrap: `gcm_nonce` is the correct length, as checked above.
    (gcm_key, gcm_nonce.try_into().unwrap())
}

// Encrypt `data` with `wrapping_key`. The same `purpose` value must be
// presented to `unwrap` for `unwrap` to be successful.
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
}

// Decrypt `data` that was encrypted by calling `wrap` with the same
// `wrapping_key` and `purpose`.
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);
    // unwrap: we know that the length is correct because it came from `split_at`.
    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);
    // unwrap: the length is correct because it just came from `split_at`.
    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,
}

/// Get the security domain secret for a client's request, either because it's
/// wrapped, or because the client provided it directly.
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 {
    /// The number of incorrect attempts made.
    attempts: i64,
}

// The parsed form of the account's state.
//
// This code only does a shallow parse of the account's state and manipulates
// the CBOR structures directly. This results in a few cases of "impossible"
// errors, where the account state is invalid, but saves having another
// representation and its accompanying parse and serialise logic. This might
// be worth revisiting in the future if we later feel the tradeoff wasn't
// worthwhile.
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(),
        }
    }

    /// Gets a trusted device, by id.
    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)
    }

    /// Gets a [`btree_map::Entry`] for a trusted device, which can be used
    /// to insert or delete it.
    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)))
    }

    /// Get a per-device wrapping key.
    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)
    }

    /// Encrypt `data` for a given device to store. See the top-level `wrap`
    /// function.
    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))
    }

    /// Decrypt data previously encrypted with `wrap`. See the top-level
    /// `unwrap` function.
    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,
        }
    }
}

/// Represents the type of public key that authenticates a request.
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
enum AuthLevel {
    /// The key is kept in software.
    Software,
    /// The key is hardware bound to the device.
    Hardware,
    /// The key is bound to the device and requires user verification before
    /// it can be used for signing.
    UserVerification,
    // The key is kept in software, but user verification is performed before it is used for
    // signing.
    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(()),
        }
    }
}

/// Represents whether a client has very recently reauthenticated. This is a
/// feature of Google Accounts and so the host simply tells this enclave whether
/// it's true or not for a given client.
#[derive(Copy, Clone)]
enum Reauth {
    None,
    Done,
}

/// Represents whether the client is using its declared option to register a UV
/// key after registration. In this case, it is able to make UV assertions if
/// the assertion command is in the same batch.
#[derive(Copy, Clone)]
enum OneTimeUV {
    Consumed,
    None,
}

/// Represents which device a request is coming from.
enum Authentication {
    None,
    // Contains the device ID, authentication level, whether the client is using
    // a one-time UV assertion, and whether the client reauthenticated very
    // recently.
    Device(Vec<u8>, AuthLevel, OneTimeUV, Reauth),
    // Requests processed after a registration will observe this special
    // authentication level. Duplicate registrations are silently accepted so
    // one must be very careful with this authentication level since it can be
    // asserted by anyone with knowledge of the (semi-public) device ID and
    // public keys of an existing device.
    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
    }

    /// Declare that a mutation is minor and thus shouldn't set the dirty flag.
    fn get_mut_for_minor_change(&mut self) -> &mut T where {
        self.minor_change = true;
        self._contents
    }
}

pub fn process_client_msg(
    state: ClientState,
    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 {
        return Err(Error::Str("request structure was not a map"));
    };
    let Some(Value::Bytestring(encoded_requests)) = client_msg.get(ENCODED_REQUESTS_KEY) else {
        return Err(Error::Str("encoded_requests must be given"));
    };
    let Value::Array(requests) =
        cbor::parse_bytes(encoded_requests.clone()).map_err(Error::CBORError)?
    else {
        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 {
            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 {
            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 {
            return Err(Error::Str("signature must be given"));
        };
        let Some(client) = state.get_device(&device_id) else {
            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 Some(Value::Bytestring(pub_key)) =
            pub_keys.get(&MapKeyRef::Str(auth_level.as_str()) as &dyn MapLookupKey)
        else {
            return Err(Error::Str("no such public key at that auth level"));
        };
        let Some((pub_key_type, pub_key)) = spki::parse(pub_key) else {
            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),
        } {
            return Err(Error::Str("signature validation failed"));
        }
        auth = Authentication::Device(
            device_id,
            auth_level,
            OneTimeUV::None,
            if ext_ctx.is_reauthenticated { Reauth::Done } else { Reauth::None },
        );
    }

    // The state is passed to `do_request` wrapped in a `DirtyFlag`, which tracks
    // whether any mutable references to the state were requested.
    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 {
            return Err(Error::Str("each request must be a map"));
        };
        match do_request(&ext_ctx, &mut auth, &mut state_with_dirty_flag, request) {
            Ok(result) => results
                .push(Value::Map(BTreeMap::from([(MapKey::String(String::from(OK)), result)]))),
            Err(error) => {
                results.push(Value::Map(BTreeMap::from([(
                    MapKey::String(String::from(ERR)),
                    error.to_cbor(),
                )])));
                break;
            }
        }
    }
    // If any mutable references to the state were requested then the state change
    // is "major" and must be saved to the datastore in order for the request to be
    // successful.
    let has_major_update = state_with_dirty_flag.changed;

    // If a device was recognised, the `last_used` value for it will be updated.
    // This is a "minor" state update and may be discarded.
    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))
}

/// Enumerates the possible errors from a single request.
///
/// A message from a client contains an array of requests which are performed
/// until one fails. Thus a message can partially succeed and so errors from
/// processing requests are separate from the top-level errors described by
/// `Error`.
#[derive(Debug, PartialEq)]
enum RequestError {
    /// A passkey creation request could not be satisfied because the
    /// enclave doesn't support any of the requested algorithms.
    NoSupportedAlgorithm,

    /// A resource with the same identifier already exists.
    Duplicate,

    /// The claimed PIN was incorrect.
    IncorrectPIN,

    /// The device has made too many incorrect PIN attempts and cannot make
    /// any more.
    PINLocked,

    /// Client provided recovery key store keys that had a lower version than
    /// those previously used.
    RecoveryKeyStoreDowngrade,

    /// An error that should never happen and thus is only reported for
    /// debugging purposes. Clients are not expected to handle these errors
    /// other than to log them.
    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::Debug(s) => Value::String(String::from(*s)),
        }
    }
}

/// A trivial function to return a `Debug` error.
fn debug<T>(msg: &'static str) -> Result<T, RequestError> {
    Err(RequestError::Debug(msg))
}

fn do_request(
    ext_ctx: &ExternalContext,
    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, auth, state, request),
        "device/add_uv_key" => do_device_add_uv_key(auth, state, request),
        "device/forget" => do_device_forget(auth, state, request),
        "debug/success" => Ok(Value::Boolean(true)),
        "debug/dump" => do_debug_dump(ext_ctx, state, request),
        "keys/genpair" => do_keys_genpair(auth, state, request),
        "keys/wrap" => do_keys_wrap(auth, state, request),
        "passkeys/assert" => passkeys::do_assert(auth, state, request),
        "passkeys/create" => passkeys::do_create(auth, state, request),
        "passkeys/wrap_pin" => passkeys::do_wrap_pin(auth, state, request),
        "recovery_key_store/wrap" => {
            recovery_key_store::do_wrap(ext_ctx.current_time_epoch_millis, request)
        }
        "recovery_key_store/wrap_as_member" => recovery_key_store::do_wrap_as_member(
            auth,
            state,
            ext_ctx.current_time_epoch_millis,
            request,
        ),
        "recovery_key_store/rewrap" => {
            recovery_key_store::do_rewrap(auth, state, ext_ctx.current_time_epoch_millis, request)
        }
        _ => debug("unknown command"),
    }
}

fn do_device_register(
    ext_ctx: &ExternalContext,
    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");
    }

    /// Check if an existing device (given as a `Value`) matches the proposed
    /// new device record.
    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) => {
            // Entry already exists. The registration will be a no-op success if
            // the device matches, otherwise a failure.
            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());
    }
    Ok(Value::Boolean(true))
}

fn do_device_add_uv_key(
    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");
    }

    // Check whether the device already has a UV key.
    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 {
                return Ok(Value::Boolean(true));
            } else {
                return debug("different UV key already registered");
            }
        }
        Some(_) => {
            return debug("UV key is wrong type");
        }
        None => (),
    }
    // Check that  `uv_key_pending` is set.
    match device.get(UV_KEY_PENDING_KEY) {
        Some(Value::Boolean(uv_key_pending)) if *uv_key_pending => (),
        _ => return debug("uv_key_pending is missing"),
    }

    // Requirements have been checked. Now get a mutable reference and update
    // the device record.
    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 {
        // Impossible since the structure of "pub_keys" was checked above.
        return debug("internal error");
    };
    pub_keys.insert(uv, Value::Bytestring(spki.clone()));

    // Allow some subsequent commands in this request to act as if UV was asserted.
    *auth = Authentication::Device(device_id.clone(), *auth_level, OneTimeUV::Consumed, *reauth);

    Ok(Value::Boolean(true))
}

fn do_device_forget(
    _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();
    Ok(Value::Boolean(true))
}

fn do_keys_genpair(
    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();

    Ok(cbor!({
        PUB_KEY: (key.compute_public_key().to_vec()),
        PRIV_KEY: (state.wrap(device_id, &key.bytes(), purpose)?),
    }))
}

fn do_keys_wrap(
    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");
    };
    Ok(state.wrap(device_id, key, purpose)?.into())
}

fn do_debug_dump(
    ext_ctx: &ExternalContext,
    state: &mut DirtyFlag<ParsedState>,
    _request: BTreeMap<MapKey, Value>,
) -> Result<cbor::Value, RequestError> {
    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 super::*;
    use alloc::boxed::Box;
    use alloc::{format, vec};
    use cbor::cbor;
    use crypto::EcdsaKeyPair;
    use passkeys::{
        CLAIMED_PIN, CLIENT_DATA_JSON, COSE_ALGORITHM, PIN_CLAIM_KEY, PIN_GENERATION, 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";
    // PROTOBUF_BYTES is a serialized WebauthnCredentialSpecifics that contains an
    // encrypted private key.
    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";
    // PROTOBUF2_BYTES is a serialized WebauthnCredentialSpecifics that contains an
    // encrypted protobuf within it.
    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";
    // RSA_PKCS8 was generated with:
    //   openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt
    // rsa_keygen_pubexp:65537 | openssl pkcs8 -topk8 -nocrypt -outform der >
    // /tmp/priv
    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";
    // RSA_SPKI is the public-key from `RSA_PKCS8`. It was generated by hand with
    // der2ascii.
    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,
    };

    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 (output, StateUpdate::Major(state)) = process_client_msg(
                ClientState::Initial,
                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 (output, _state) = process_client_msg(
                REGISTERED_STATE.clone(),
                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 (output, StateUpdate::Major(state)) = process_client_msg(
                ClientState::Initial,
                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 (output, _state) = process_client_msg(
                REGISTERED_STATE.clone(),
                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 (output, StateUpdate::Major(state)) = process_client_msg(
                ClientState::Initial,
                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();
    }

    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 (output, state) = process_client_msg(
            REGISTERED_STATE.clone(),
            ExternalContext {
                client_device_identifier: device_id.clone(),
                ..EXTERNAL_CONTEXT.clone()
            },
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(is_ok(&output), "{:?}", output);

        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 (output, _state) = process_client_msg(
            RSA_REGISTERED_STATE.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(is_ok(&output), "{:?}", output);
    }

    #[test]
    fn test_device_register_twice_matching_keys() {
        // Registering the same device (defined as having the same ID and public keys)
        // is a no-op.
        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 (output, _) = process_client_msg(
            REGISTERED_STATE.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert_eq!(output, cbor!([{"ok": true}]));
    }

    #[test]
    fn test_device_register_twice_mismatching_keys() {
        // Registering different devices (defined by their public keys) with the same ID
        // is an error.
        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 (output, _) = process_client_msg(
            REGISTERED_STATE.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert_eq!(output, cbor!([{"err": 2}]));
    }

    #[test]
    fn test_device_register_uv_and_uv_pending() {
        // Can't register both a UV key and a UV-pending signal.
        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 (output, _) = process_client_msg(
            ClientState::Initial,
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(!is_ok(&output));
    }

    #[test]
    fn test_device_register_uv_and_swuv() {
        // Can't register both a UV and SWUV key.
        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 (output, _) = process_client_msg(
            ClientState::Initial,
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(!is_ok(&output));
    }

    #[test]
    fn test_device_register_software_uv_and_uv_pending() {
        // Can't register both a software UV key and a UV-pending signal.
        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 (output, _) = process_client_msg(
            ClientState::Initial,
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(!is_ok(&output));
    }

    #[test]
    fn test_device_add_uv_key_without_uv_pending() {
        // add_uv_key should fail if the device didn't opt to later add a UV
        // key at registration time.
        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 (output, _) = process_client_msg(
            REGISTERED_STATE.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();

        assert!(!is_ok(&output));
    }

    #[test]
    fn test_subsequent_uv() {
        let msg = sign_request(cbor!({
            CMD: "device/add_uv_key",
            PUB_KEY: (SPKI.as_slice()),
        }));
        let (output, StateUpdate::Major(state)) = process_client_msg(
            REGISTERED_STATE_UV_PENDING.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap() else {
            panic!("")
        };
        assert!(is_ok(&output));

        // Doing the same command a second time is fine if the public key
        // matches.
        let msg = sign_request(cbor!({
            CMD: "device/add_uv_key",
            PUB_KEY: (SPKI.as_slice()),
        }));
        let (output, _update) = process_client_msg(
            ClientState::Explicit(state.clone()),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(is_ok(&output));

        // ... but it fails if the public key is different.
        let msg = sign_request(cbor!({
            CMD: "device/add_uv_key",
            PUB_KEY: RSA_SPKI,
        }));
        let (output, _update) = process_client_msg(
            ClientState::Explicit(state.clone()),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(!is_ok(&output));

        // The UV key should work now.
        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 (output, _update) = process_client_msg(
            ClientState::Explicit(state),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(is_ok(&output), "{:?}", output);
    }

    #[test]
    fn test_device_forget() {
        let msg = sign_request(cbor!({
            CMD: "device/forget",
            DEVICE_ID: (TEST_DEVICE_ID.clone()),
        }));
        let (output, _state) = process_client_msg(
            REGISTERED_STATE.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg,
        )
        .unwrap();
        assert!(is_ok(&output), "{:?}", output);
    }

    #[test]
    fn test_keys_genpair() {
        let msg = sign_request(cbor!({
            CMD: "keys/genpair",
            PURPOSE: "not yet used",
        }));
        let (output, _state) = process_client_msg(
            REGISTERED_STATE.clone(),
            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(_))));
        // No way to use the generated key pair yet.
    }

    #[test]
    fn test_passkeys_assert() {
        let msg = sign_request(cbor!({
            CMD: "passkeys/assert",
            WRAPPED_SECRET: (REGISTERED_STATE_WRAPPED_SECRET.as_slice()),
            PROTOBUF: (PROTOBUF_BYTES.to_vec()),
            CLIENT_DATA_JSON: r#"{"type": "webauthn.get", challenge: "1234", "origin": "example.com"}"#,
            WEBAUTHN_REQUEST: {
                RP_ID: "example.com",
            },
        }));
        let (output, _state) = process_client_msg(
            REGISTERED_STATE.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg.clone(),
        )
        .unwrap();
        assert!(is_ok(&output), "{:?}", output);
    }

    #[test]
    fn test_passkeys_create() {
        // Test that we can successfully assert the credential that was
        // created with "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 (output, _state) = process_client_msg(
            REGISTERED_STATE.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg.clone(),
        )
        .unwrap();
        assert!(is_ok(&output), "{:?}", output);
    }

    #[test]
    fn test_both_wrapped_and_unwrapped() {
        let msg = sign_request(cbor!({
            CMD: "passkeys/assert",
            // Providing _both_ a wrapped and unwrapped secret should fail.
            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 (output, _state) = process_client_msg(
            REGISTERED_STATE.clone(),
            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);
    }

    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()
    }

    /// Make an assertion with the given claimed PIN. Returns any error that
    /// resulted, the resulting PIN state, and the updated account state.
    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 (output, state_update) = process_client_msg(
            state.clone(),
            EXTERNAL_CONTEXT.clone(),
            TEST_HANDSHAKE_HASH.as_slice(),
            msg.clone(),
        )
        .unwrap();

        // Get the state after processing the command. That's either a new
        // state, or the original state because no update was made.
        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: [1u8; 32],
            generation: 1,
            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],
        };
        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);

        // Using the same PIN again shouldn't change anything.
        let (error, pin_state, state) = attempt_pin(state, &wrapped_pin_data, &pin_claim);
        assert!(error.is_none());
        assert_eq!(pin_state.attempts, 0);

        // Trying the wrong PIN should fail and increment the attempts counter.
        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);

        // The correct PIN should reset it again.
        let (error, pin_state, state) = attempt_pin(state, &wrapped_pin_data, &pin_claim);
        assert!(error.is_none());
        assert_eq!(pin_state.attempts, 0);

        // The wrong PIN five times in a row should lock the device.
        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);

        // Now the wrong PIN will generate a different error and not increment the
        // counter.
        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);

        // And so will the correct PIN.
        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() {
        // Wrap a PIN and then attempt to use it.
        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 msg = sign_request(cbor!({
            CMD: "passkeys/wrap_pin",
            PIN_HASH: (&pin_hash),
            PIN_GENERATION: 1,
            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 (output, _) = process_client_msg(
            REGISTERED_STATE.clone(),
            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(&claim_key, &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);
    }

    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()
    }

    /// Return the error string from the single response in `value`.
    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)
    }

    // Automated mutation of requests:
    //
    // In order to test invalid versions of requests, the following test
    // infrastructure can mutate maps: removing entries, replacing them with
    // values of a different type, and replacing them with invalid values.
    //
    // First some utilities for mutating generic maps are provided, then those
    // are used for producing mutated requests.

    /// The result of applying a single mutation to a map.
    struct MutatedMap {
        /// The mutated map.
        map: BTreeMap<MapKey, Value>,
        /// Whether making a request with this map should result in an error.
        should_fail: bool,
        /// A string describing the mutation, for debugging.
        debug: String,
    }

    /// Instruction for mutating a specific value in a map.
    #[derive(Default)]
    struct MutationConfig {
        /// Whether the value is optional. If so, removing it shouldn't cause
        /// a request to fail.
        is_optional: bool,
        /// An optional list of invalid values that should cause failures.
        invalid_values: Option<Vec<Value>>,
        /// If the value itself is a map, instructions for recursively mutating
        /// it.
        subconfig: Option<Box<BTreeMap<String, MutationConfig>>>,
    }

    /// Applies mutations to each key of the given string-keyed map and returns
    /// a vector of mutated maps.
    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);

            // First, trying removing the value.
            let mut mutated = map.clone();
            mutated.remove(key);
            ret.push(MutatedMap {
                map: mutated.clone(),
                should_fail: !config.is_optional,
                debug: format!("removed {key_str}"),
            });

            // Next, try making it a different type.
            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 any specific, invalid values were provided, try those.
            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 a configuration was provided for mutating the value itself,
            // try all those variants.
            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
    }

    /// The result of mutating a request.
    ///
    /// This mirrors `MutatedMap`, but the values are serialized requests.
    struct MutatedRequest {
        request: Vec<u8>,
        should_fail: bool,
        debug: String,
    }

    /// An enum that describes whether a specific request need be authenticated.
    enum RequestAuthentication {
        Required,
        Never,
    }

    /// Mutate a given request and return a vector of mutated requests.
    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();

        // First, check that the unmodified request is successful.

        ret.push(MutatedRequest {
            request: serialize(request.clone()),
            should_fail: false,
            debug: String::from("unmodified"),
        });

        // Next, if the request requires authentication, check that an
        // unauthenticated request fails.

        if !matches!(authentication, RequestAuthentication::Never) {
            ret.push(MutatedRequest {
                request: unauthenticated_request(request.clone()),
                should_fail: true,
                debug: String::from("unauthenticated"),
            });
        }

        // Finally, mutate the request map itself.

        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 (output, _state) = process_client_msg(
                initial_state.clone(),
                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_GENERATION: 1,
            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_invalid_recovery_key_store_rewrap() {
        let pin_data = pin::Data {
            pin_hash: [1u8; 32],
            generation: 1,
            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],
        };
        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,
        );
    }
}