diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index c91c3f3..0000000 --- a/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[net] -git-fetch-with-cli = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9f10b55 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + check-fuzz: + name: Check fuzz targets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + - name: Check fuzz targets + run: | + cargo check --manifest-path fuzz/Cargo.toml + + build: + runs-on: ubuntu-latest + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu + - thumbv7em-none-eabi + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Rust toolchain + run: | + rustup show + rustup target add ${{ matrix.target }} + + - name: Install build dependencies + run: > + sudo apt-get update -y -qq && + sudo apt-get install -y -qq llvm libc6-dev-i386 libclang-dev + + - uses: fiam/arm-none-eabi-gcc@v1 + with: + release: "9-2020-q2" + + - name: Build + run: cargo build --verbose --target ${{ matrix.target }} + + - name: Check all targets without default features + run: cargo check --all-targets --no-default-features + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check all targets with default features + run: cargo check --all-targets + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check all features and targets + run: cargo check --all-features --all-targets + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Run tests + run: cargo test --verbose --features dispatch + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check formatting + run: cargo fmt -- --check + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check clippy lints + run: cargo clippy --all-features --all-targets -- --deny warnings + if: matrix.target == 'x86_64-unknown-linux-gnu' + + - name: Check documentation + run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps + if: matrix.target == 'x86_64-unknown-linux-gnu' diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a0a32c..46011cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,25 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Set the `makeCredUvNotRqd` CTAP option to `true` to indicate that we support makeCredential operations without user verification ([#26][]) -- Ignore public key credential parameters with an unknown type, as required by +- Ignore public key credential paramters with an unknown type, as required by the Webauthn spec ([#28][]) - Reject `rk` option in getAssertion ([#31][]) - Ignore user data with empty ID in getAssertion ([#32][]) - Allow three instead of two PIN retries per boot ([#35][]) - Add log messages for requests, responses and errors +- Add config option for setting a maximum number of resident credentials. +- Reduce ID length for new credentials ([#37][]) +- Update apdu-dispatch and reject calls to `select` ([#40][]) +- Implement the `largeBlobKey` extension and the `largeBlobs` command ([#38][]) +- Fix error type for third invalid PIN entry ([#60][]) +- Fix error type for cancelled user presence ([#61][]) +- PIN protocol changes: + - Extract PIN protocol implementation into separate module ([#62][]) + - Implement PIN protocol 2 ([#63][]) + - Implement PIN token permissions ([#63][]) +- Implement UpdateUserInformation subcommand for CredentialManagement +- Support CTAP 2.1 +- Serialize PIN hash with `serde-bytes` ([#52][]) +- Reduce the space taken by credential serialization ([#59][]) +- Update dependencies: + - Replace `trussed` dependency with `trussed-core` + - Replace `ctaphid-dispatch` dependeny with `ctaphid-app` +- Remove the per-relying party directory to save space ([#55][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 [#31]: https://github.com/solokeys/fido-authenticator/issues/31 [#32]: https://github.com/solokeys/fido-authenticator/issues/32 [#35]: https://github.com/solokeys/fido-authenticator/issues/35 +[#37]: https://github.com/solokeys/fido-authenticator/issues/37 +[#40]: https://github.com/nitrokey/fido-authenticator/pull/40 +[#38]: https://github.com/Nitrokey/fido-authenticator/issues/38 +[#60]: https://github.com/Nitrokey/fido-authenticator/pull/60 +[#61]: https://github.com/Nitrokey/fido-authenticator/pull/61 +[#62]: https://github.com/Nitrokey/fido-authenticator/pull/62 +[#63]: https://github.com/Nitrokey/fido-authenticator/pull/63 +[#52]: https://github.com/Nitrokey/fido-authenticator/issues/52 +[#59]: https://github.com/Nitrokey/fido-authenticator/issues/59 +[#55]: https://github.com/Nitrokey/fido-authenticator/issues/55 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp - Add config option to skip UP when device was just booted, as insertion is a kind of UP check @robin-nitrokey -## [Unreleased] +## [0.1.0] - 2022-03-17 - use 2021 edition - use @szszszsz's credential ID shortening diff --git a/Cargo.toml b/Cargo.toml index a9124f0..6cc2bda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,39 +8,80 @@ repository = "https://github.com/solokeys/fido-authenticator" documentation = "https://docs.rs/fido-authenticator" description = "FIDO authenticator Trussed app" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -ctap-types = "0.1.0" +cbor-smol = { version = "0.5" } +ctap-types = { version = "0.4", features = ["get-info-full", "large-blobs", "third-party-payment"] } +cosey = "0.3" delog = "0.1.0" heapless = "0.7" -interchange = "0.2.0" -littlefs2 = "0.3.1" +heapless-bytes = "0.3" +littlefs2-core = "0.1" serde = { version = "1.0", default-features = false } -serde_cbor = { version = "0.11.0", default-features = false } +serde_bytes = { version = "0.11.14", default-features = false } serde-indexed = "0.1.0" -trussed = "0.1" +sha2 = { version = "0.10", default-features = false } +trussed-core = { version = "0.1.0", features = ["aes256-cbc", "certificate-client", "chacha8-poly1305", "crypto-client", "ed255", "filesystem-client", "hmac-sha256", "management-client", "p256", "sha256", "ui-client"] } +trussed-fs-info = "0.2.0" +trussed-hkdf = { version = "0.3.0" } +trussed-chunked = { version = "0.2.0", optional = true } -apdu-dispatch = { version = "0.1", optional = true } -ctaphid-dispatch = { version = "0.1", optional = true } -iso7816 = { version = "0.1", optional = true } +apdu-app = { version = "0.1", optional = true } +ctaphid-app = { version = "0.1.0-rc.1", optional = true } +iso7816 = { version = "0.1.2", optional = true } [features] -default = [] dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"] +apdu-dispatch = ["dep:apdu-app"] +ctaphid-dispatch = ["dep:ctaphid-app"] disable-reset-time-window = [] -enable-fido-pre = [] + +# enables support for a large-blob array longer than 1024 bytes +chunked = ["trussed-chunked"] log-all = [] log-none = [] +log-trace = [] log-info = [] log-debug = [] log-warn = [] log-error = [] [dev-dependencies] -# quickcheck = "1" +admin-app = { version = "0.1.0", features = ["migration-tests"] } +aes = "0.8.4" +cbc = { version = "0.1.2", features = ["alloc"] } +ciborium = { version = "0.2.2" } +ciborium-io = "0.2.2" +cipher = "0.4.4" +ctaphid = { version = "0.3.1", default-features = false } +ctaphid-dispatch = "0.3" +delog = { version = "0.1.6", features = ["std-log"] } +env_logger = "0.11.0" +hex-literal = "0.4.1" +hmac = "0.12.1" +interchange = "0.3.0" +itertools = "0.14.0" +littlefs2 = "0.6.0" +log = "0.4.21" +p256 = { version = "0.13.2", features = ["ecdh"] } rand = "0.8.4" +rand_chacha = "0.3" +sha2 = "0.10" +serde_test = "1.0.176" +trussed = { version = "0.1", features = ["virt"] } +trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } +trussed-usbip = { version = "0.0.1", default-features = false, features = ["ctaphid"] } +usbd-ctaphid = "0.3.0" +x509-parser = "0.16.0" [package.metadata.docs.rs] features = ["dispatch"] + +[patch.crates-io] +admin-app = { git = "https://github.com/Nitrokey/admin-app.git", tag = "v0.1.0-nitrokey.20" } +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "024e0eca5fb7dbd2457831f7c7bffe4341e08775" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "7922d67e9637a87e5625aaff9e5111f0d4ec0346" } +trussed-usbip = { git = "https://github.com/trussed-dev/pc-usbip-runner.git", rev = "504674453c9573a30aa2f155101df49eb2af1ba7" } + +[profile.test] +opt-level = 2 diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..9b9d04b --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "fido-authenticator-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +ctap-types = { version = "0.4", features = ["arbitrary"] } +libfuzzer-sys = "0.4" +trussed = { version = "0.1", features = ["clients-1", "certificate-client", "crypto-client", "filesystem-client", "management-client", "aes256-cbc", "ed255", "p256", "sha256"] } +trussed-staging = { version = "0.3.0", features = ["chunked", "hkdf", "virt", "fs-info"] } + +[dependencies.fido-authenticator] +path = ".." + +[[bin]] +name = "ctap" +path = "fuzz_targets/ctap.rs" +test = false +doc = false +bench = false + +[patch.crates-io] +trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "6bba8fde36d05c0227769eb63345744e87d84b2b" } +trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "1e1ca03a3a62ea9b802f4070ea4bce002eeb4bec" } diff --git a/fuzz/fuzz_targets/ctap.rs b/fuzz/fuzz_targets/ctap.rs new file mode 100644 index 0000000..01c4aad --- /dev/null +++ b/fuzz/fuzz_targets/ctap.rs @@ -0,0 +1,34 @@ +#![no_main] + +use ctap_types::{authenticator::Request, ctap1::Authenticator as _, ctap2::Authenticator as _}; +use fido_authenticator::{Authenticator, Config, Conforming}; +use trussed_staging::virt; + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|requests: Vec>| { + virt::with_ram_client("fido", |client| { + let mut authenticator = Authenticator::new( + client, + Conforming {}, + Config { + max_msg_size: 0, + skip_up_timeout: None, + max_resident_credential_count: None, + large_blobs: None, + nfc_transport: false, + }, + ); + + for request in requests { + match request { + Request::Ctap1(request) => { + authenticator.call_ctap1(&request).ok(); + } + Request::Ctap2(request) => { + authenticator.call_ctap2(&request).ok(); + } + } + } + }); +}); diff --git a/src/constants.rs b/src/constants.rs index 43e88da..687fc1e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,9 +1,11 @@ //! Constants. -use trussed::types::{CertId, KeyId}; +use trussed_core::types::{CertId, KeyId}; pub const FIDO2_UP_TIMEOUT: u32 = 30_000; pub const U2F_UP_TIMEOUT: u32 = 250; pub const ATTESTATION_CERT_ID: CertId = CertId::from_special(0); pub const ATTESTATION_KEY_ID: KeyId = KeyId::from_special(0); + +pub const MAX_RESIDENT_CREDENTIALS_GUESSTIMATE: u32 = 100; diff --git a/src/credential.rs b/src/credential.rs index dac78c6..7aabed7 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -2,13 +2,23 @@ use core::cmp::Ordering; -use trussed::{client, syscall, try_syscall, types::KeyId}; +use serde::Serialize; +use serde_bytes::ByteArray; +use trussed_core::{ + mechanisms::{Chacha8Poly1305, Sha256}, + syscall, try_syscall, + types::{EncryptedData, KeyId}, + CryptoClient, FilesystemClient, +}; pub(crate) use ctap_types::{ // authenticator::{ctap1, ctap2, Error, Request, Response}, ctap2::credential_management::CredentialProtectionPolicy, sizes::*, - webauthn::PublicKeyCredentialDescriptor, + webauthn::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialDescriptorRef, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + }, Bytes, String, }; @@ -29,37 +39,49 @@ pub enum CtapVersion { #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct CredentialId(pub Bytes); -// TODO: how to determine necessary size? -// pub type SerializedCredential = Bytes<512>; -// pub type SerializedCredential = Bytes<256>; -pub(crate) type SerializedCredential = trussed::types::Message; - -#[derive(Clone, Debug)] -struct EncryptedSerializedCredential(pub trussed::api::reply::Encrypt); - -impl TryFrom for CredentialId { - type Error = Error; - - fn try_from(esc: EncryptedSerializedCredential) -> Result { - Ok(CredentialId( - trussed::cbor_serialize_bytes(&esc.0).map_err(|_| Error::Other)?, - )) +impl CredentialId { + fn new( + trussed: &mut T, + credential: &C, + key_encryption_key: KeyId, + rp_id_hash: &[u8; 32], + nonce: &[u8; 12], + ) -> Result { + let mut serialized_credential = SerializedCredential::new(); + cbor_smol::cbor_serialize_to(credential, &mut serialized_credential) + .map_err(|_| Error::Other)?; + let message = &serialized_credential; + // info!("serialized cred = {:?}", message).ok(); + let associated_data = &rp_id_hash[..]; + let encrypted_serialized_credential = syscall!(trussed.encrypt_chacha8poly1305( + key_encryption_key, + message, + associated_data, + Some(nonce) + )); + let mut credential_id = Bytes::new(); + cbor_smol::cbor_serialize_to( + &EncryptedData::from(encrypted_serialized_credential), + &mut credential_id, + ) + .map_err(|_| Error::RequestTooLarge)?; + Ok(Self(credential_id)) } } -impl TryFrom for EncryptedSerializedCredential { - // tag = 16B - // nonce = 12B - type Error = Error; +struct CredentialIdRef<'a>(&'a [u8]); - fn try_from(cid: CredentialId) -> Result { - let encrypted_serialized_credential = EncryptedSerializedCredential( - ctap_types::serde::cbor_deserialize(&cid.0).map_err(|_| Error::InvalidCredential)?, - ); - Ok(encrypted_serialized_credential) +impl CredentialIdRef<'_> { + fn deserialize(&self) -> Result { + cbor_smol::cbor_deserialize(self.0).map_err(|_| Error::InvalidCredential) } } +// TODO: how to determine necessary size? +// pub type SerializedCredential = Bytes<512>; +// pub type SerializedCredential = Bytes<256>; +pub(crate) type SerializedCredential = trussed_core::types::Message; + /// Credential keys can either be "discoverable" or not. /// /// The FIDO Alliance likes to refer to "resident keys" as "(client-side) discoverable public key @@ -71,15 +93,395 @@ pub enum Key { WrappedKey(Bytes<128>), } -/// The main content of a `Credential`. +/// A credential that is managed by the authenticator. +/// +/// The authenticator uses two credential representations: +/// - [`FullCredential`][] contains all data available for a credential and is used for resident +/// credentials that are stored on the filesystem. Older versions of this app used this +/// reprensentation for non-resident credentials too. +/// - [`StrippedCredential`][] contains the minimal data required for non-resident credentials. As +/// the data for these credentials is encoded in the credential ID, we try to keep it as small as +/// possible. +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +pub enum Credential { + Full(FullCredential), + Stripped(StrippedCredential), +} + +impl Credential { + pub fn try_from( + authnr: &mut Authenticator, + rp_id_hash: &[u8; 32], + descriptor: &PublicKeyCredentialDescriptorRef, + ) -> Result { + Self::try_from_bytes(authnr, rp_id_hash, descriptor.id) + } + + pub fn try_from_bytes< + UP: UserPresence, + T: CryptoClient + Chacha8Poly1305 + FilesystemClient, + >( + authnr: &mut Authenticator, + rp_id_hash: &[u8; 32], + id: &[u8], + ) -> Result { + let encrypted_serialized = CredentialIdRef(id).deserialize()?; + + let kek = authnr + .state + .persistent + .key_encryption_key(&mut authnr.trussed)?; + + let serialized = try_syscall!(authnr.trussed.decrypt_chacha8poly1305( + kek, + &encrypted_serialized.ciphertext, + &rp_id_hash[..], + &encrypted_serialized.nonce, + &encrypted_serialized.tag, + )) + .map_err(|_| Error::InvalidCredential)? + .plaintext + .ok_or(Error::InvalidCredential)?; + + // In older versions of this app, we serialized the full credential. Now we only serialize + // the stripped credential. For compatibility, we have to try both. + FullCredential::deserialize(&serialized) + .map(Self::Full) + .or_else(|_| StrippedCredential::deserialize(&serialized).map(Self::Stripped)) + .map_err(|_| Error::InvalidCredential) + } + + pub fn id( + &self, + trussed: &mut T, + key_encryption_key: KeyId, + rp_id_hash: &[u8; 32], + ) -> Result { + match self { + Self::Full(credential) => credential.id(trussed, key_encryption_key, Some(rp_id_hash)), + Self::Stripped(credential) => CredentialId::new( + trussed, + credential, + key_encryption_key, + rp_id_hash, + &credential.nonce, + ), + } + } + + pub fn algorithm(&self) -> i32 { + match self { + Self::Full(credential) => credential.algorithm, + Self::Stripped(credential) => credential.algorithm, + } + } + + pub fn cred_protect(&self) -> Option { + match self { + Self::Full(credential) => credential.cred_protect, + Self::Stripped(credential) => credential.cred_protect, + } + } + + pub fn key(&self) -> &Key { + match self { + Self::Full(credential) => &credential.key, + Self::Stripped(credential) => &credential.key, + } + } + + pub fn third_party_payment(&self) -> Option { + match self { + Self::Full(credential) => credential.data.third_party_payment, + Self::Stripped(credential) => credential.third_party_payment, + } + } +} + +fn deserialize_bytes( + s: &[u8], +) -> core::result::Result, E> { + Bytes::from_slice(s).map_err(|_| E::invalid_length(s.len(), &"a fixed-size sequence of bytes")) +} + +fn deserialize_str( + s: &str, +) -> core::result::Result, E> { + Ok(s.into()) +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SerializationFormat { + Short, + Long, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Rp { + format: SerializationFormat, + inner: PublicKeyCredentialRpEntity, +} + +impl Rp { + fn new(inner: PublicKeyCredentialRpEntity) -> Self { + Self { + format: SerializationFormat::Short, + inner, + } + } + + fn raw(&self) -> RawRp<'_> { + let mut raw = RawRp::default(); + match self.format { + SerializationFormat::Short => { + raw.i = Some(&self.inner.id); + raw.n = self.inner.name.as_deref(); + } + SerializationFormat::Long => { + raw.id = Some(&self.inner.id); + raw.name = self.inner.name.as_deref(); + } + } + raw + } + + pub fn id(&self) -> &str { + &self.inner.id + } +} + +impl AsRef for Rp { + fn as_ref(&self) -> &PublicKeyCredentialRpEntity { + &self.inner + } +} + +impl AsMut for Rp { + fn as_mut(&mut self) -> &mut PublicKeyCredentialRpEntity { + &mut self.inner + } +} + +impl<'de> serde::Deserialize<'de> for Rp { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + let r = RawRp::deserialize(deserializer)?; + + if r.i.is_some() && r.id.is_some() { + return Err(D::Error::duplicate_field("i")); + } + + let (format, id, name) = if let Some(i) = r.i { + if r.name.is_some() { + return Err(D::Error::unknown_field("name", &["i", "n"])); + } + (SerializationFormat::Short, i, r.n) + } else if let Some(id) = r.id { + if r.n.is_some() { + return Err(D::Error::unknown_field("n", &["id", "name"])); + } + (SerializationFormat::Long, id, r.name) + } else { + return Err(D::Error::missing_field("i")); + }; + + let inner = PublicKeyCredentialRpEntity { + id: deserialize_str(id)?, + name: name.map(deserialize_str).transpose()?, + icon: None, + }; + Ok(Self { format, inner }) + } +} + +impl serde::Serialize for Rp { + fn serialize( + &self, + serializer: S, + ) -> core::result::Result { + self.raw().serialize(serializer) + } +} + +impl From for PublicKeyCredentialRpEntity { + fn from(rp: Rp) -> PublicKeyCredentialRpEntity { + rp.inner + } +} + +#[derive(Default, serde::Deserialize, serde::Serialize)] +struct RawRp<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + i: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + n: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option<&'a str>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct User { + format: SerializationFormat, + inner: PublicKeyCredentialUserEntity, +} + +impl User { + fn new(inner: PublicKeyCredentialUserEntity) -> Self { + Self { + format: SerializationFormat::Short, + inner, + } + } + + fn raw(&self) -> RawUser<'_> { + let mut raw = RawUser::default(); + match self.format { + SerializationFormat::Short => { + raw.i = Some(self.inner.id.as_slice().into()); + raw.ii = self.inner.icon.as_deref(); + raw.n = self.inner.name.as_deref(); + raw.d = self.inner.display_name.as_deref(); + } + SerializationFormat::Long => { + raw.id = Some(self.inner.id.as_slice().into()); + raw.icon = self.inner.icon.as_deref(); + raw.name = self.inner.name.as_deref(); + raw.display_name = self.inner.display_name.as_deref(); + } + } + raw + } + + pub fn id(&self) -> &Bytes<64> { + &self.inner.id + } +} + +impl AsRef for User { + fn as_ref(&self) -> &PublicKeyCredentialUserEntity { + &self.inner + } +} + +impl AsMut for User { + fn as_mut(&mut self) -> &mut PublicKeyCredentialUserEntity { + &mut self.inner + } +} + +impl<'de> serde::Deserialize<'de> for User { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + let u = RawUser::deserialize(deserializer)?; + + if u.i.is_some() && u.id.is_some() { + return Err(D::Error::duplicate_field("i")); + } + + let (format, id, icon, name, display_name) = if let Some(i) = u.i { + // short format + let fields = &["i", "I", "n", "d"]; + if u.icon.is_some() { + return Err(D::Error::unknown_field("icon", fields)); + } + if u.name.is_some() { + return Err(D::Error::unknown_field("name", fields)); + } + if u.display_name.is_some() { + return Err(D::Error::unknown_field("display_name", fields)); + } + + (SerializationFormat::Short, i, u.ii, u.n, u.d) + } else if let Some(id) = u.id { + // long format + let fields = &["id", "icon", "name", "display_name"]; + if u.ii.is_some() { + return Err(D::Error::unknown_field("ii", fields)); + } + if u.n.is_some() { + return Err(D::Error::unknown_field("n", fields)); + } + if u.d.is_some() { + return Err(D::Error::unknown_field("d", fields)); + } + + ( + SerializationFormat::Long, + id, + u.icon, + u.name, + u.display_name, + ) + } else { + // ID is missing + return Err(D::Error::missing_field("i")); + }; + + let inner = PublicKeyCredentialUserEntity { + id: deserialize_bytes(id)?, + icon: icon.map(deserialize_str).transpose()?, + name: name.map(deserialize_str).transpose()?, + display_name: display_name.map(deserialize_str).transpose()?, + }; + Ok(Self { format, inner }) + } +} + +impl serde::Serialize for User { + fn serialize( + &self, + serializer: S, + ) -> core::result::Result { + self.raw().serialize(serializer) + } +} + +impl From for PublicKeyCredentialUserEntity { + fn from(user: User) -> PublicKeyCredentialUserEntity { + user.inner + } +} + +#[derive(Default, serde::Deserialize, serde::Serialize)] +struct RawUser<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + i: Option<&'a serde_bytes::Bytes>, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option<&'a serde_bytes::Bytes>, + #[serde(skip_serializing_if = "Option::is_none", rename = "I")] + ii: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + icon: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + n: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + d: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none", rename = "displayName")] + display_name: Option<&'a str>, +} + +/// The main content of a `FullCredential`. #[derive( Clone, Debug, PartialEq, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed, )] pub struct CredentialData { // id, name, url - pub rp: ctap_types::webauthn::PublicKeyCredentialRpEntity, + pub rp: Rp, // id, icon, name, display_name - pub user: ctap_types::webauthn::PublicKeyCredentialUserEntity, + pub user: User, // can be just a counter, need to be able to determine "latest" pub creation_time: u32, @@ -101,16 +503,30 @@ pub struct CredentialData { pub cred_protect: Option, // TODO: add `sig_counter: Option`, // and grant RKs a per-credential sig-counter. + + // In older app versions, we serialized the full credential to determine the credential ID. In + // newer app versions, we strip unnecessary fields to generate a shorter credential ID. To + // make sure that the credential ID does not change for an existing credential, this field is + // used as a marker for new credentials. + #[serde(skip_serializing_if = "Option::is_none")] + use_short_id: Option, + + // extensions (cont. -- we can only append new options due to index-based deserialization) + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } // TODO: figure out sizes // We may or may not follow https://github.com/satoshilabs/slips/blob/master/slip-0022.md /// The core structure this authenticator creates and uses. #[derive(Clone, Debug, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)] -pub struct Credential { +pub struct FullCredential { ctap: CtapVersion, pub data: CredentialData, - nonce: Bytes<12>, + nonce: ByteArray<12>, } // Alas... it would be more symmetrical to have Credential { meta, data }, @@ -121,7 +537,7 @@ pub struct Credential { // nonce: Bytes<12>, // } -impl core::ops::Deref for Credential { +impl core::ops::Deref for FullCredential { type Target = CredentialData; fn deref(&self) -> &Self::Target { @@ -132,34 +548,34 @@ impl core::ops::Deref for Credential { /// Compare credentials based on key + timestamp. /// /// Likely comparison based on timestamp would be good enough? -impl PartialEq for Credential { +impl PartialEq for FullCredential { fn eq(&self, other: &Self) -> bool { (self.creation_time == other.creation_time) && (self.key == other.key) } } -impl PartialEq<&Credential> for Credential { +impl PartialEq<&FullCredential> for FullCredential { fn eq(&self, other: &&Self) -> bool { self == *other } } -impl Eq for Credential {} +impl Eq for FullCredential {} -impl Ord for Credential { +impl Ord for FullCredential { fn cmp(&self, other: &Self) -> Ordering { self.data.creation_time.cmp(&other.data.creation_time) } } /// Order by timestamp of creation. -impl PartialOrd for Credential { +impl PartialOrd for FullCredential { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialOrd<&Credential> for Credential { +impl PartialOrd<&FullCredential> for FullCredential { fn partial_cmp(&self, other: &&Self) -> Option { Some(self.cmp(*other)) } @@ -181,7 +597,7 @@ impl From for PublicKeyCredentialDescriptor { } } -impl Credential { +impl FullCredential { #[allow(clippy::too_many_arguments)] pub fn new( ctap: CtapVersion, @@ -193,12 +609,14 @@ impl Credential { timestamp: u32, hmac_secret: Option, cred_protect: Option, + large_blob_key: Option>, + third_party_payment: Option, nonce: [u8; 12], ) -> Self { info!("credential for algorithm {}", algorithm); let data = CredentialData { - rp: rp.clone(), - user: user.clone(), + rp: Rp::new(rp.clone()), + user: User::new(user.clone()), creation_time: timestamp, use_counter: true, @@ -207,12 +625,16 @@ impl Credential { hmac_secret, cred_protect, + large_blob_key, + third_party_payment, + + use_short_id: Some(true), }; - Credential { + FullCredential { ctap, data, - nonce: Bytes::from_slice(&nonce).unwrap(), + nonce: ByteArray::new(nonce), } } @@ -229,42 +651,43 @@ impl Credential { // the ID will stay below 255 bytes. // // Existing keyhandles can still be decoded - pub fn id( + pub fn id( &self, trussed: &mut T, key_encryption_key: KeyId, - rp_id_hash: Option<&Bytes<32>>, + rp_id_hash: Option<&[u8; 32]>, ) -> Result { - let serialized_credential = self.strip().serialize()?; - let message = &serialized_credential; - // info!("serialized cred = {:?}", message).ok(); - - let rp_id_hash: Bytes<32> = if let Some(hash) = rp_id_hash { - hash.clone() + let rp_id_hash: [u8; 32] = if let Some(hash) = rp_id_hash { + *hash } else { - syscall!(trussed.hash_sha256(self.rp.id.as_ref())) + syscall!(trussed.hash_sha256(self.rp.id().as_ref())) .hash - .to_bytes() + .as_slice() + .try_into() .map_err(|_| Error::Other)? }; - - let associated_data = &rp_id_hash[..]; - let nonce: [u8; 12] = self.nonce.as_slice().try_into().unwrap(); - let encrypted_serialized_credential = EncryptedSerializedCredential(syscall!(trussed - .encrypt_chacha8poly1305(key_encryption_key, message, associated_data, Some(&nonce)))); - let credential_id: CredentialId = encrypted_serialized_credential - .try_into() - .map_err(|_| Error::RequestTooLarge)?; - - Ok(credential_id) + if self.use_short_id.unwrap_or_default() { + StrippedCredential::from(self).id(trussed, key_encryption_key, &rp_id_hash) + } else { + let stripped_credential = self.strip(); + CredentialId::new( + trussed, + &stripped_credential, + key_encryption_key, + &rp_id_hash, + &self.nonce, + ) + } } pub fn serialize(&self) -> Result { - trussed::cbor_serialize_bytes(self).map_err(|_| Error::Other) + let mut serialized_credential = SerializedCredential::new(); + cbor_smol::cbor_serialize_to(self, &mut serialized_credential).map_err(|_| Error::Other)?; + Ok(serialized_credential) } pub fn deserialize(bytes: &SerializedCredential) -> Result { - match ctap_types::serde::cbor_deserialize(bytes) { + match cbor_smol::cbor_deserialize(bytes) { Ok(s) => Ok(s), Err(_) => { info_now!("could not deserialize {:?}", bytes); @@ -273,88 +696,144 @@ impl Credential { } } - pub fn try_from( - authnr: &mut Authenticator, - rp_id_hash: &Bytes<32>, - descriptor: &PublicKeyCredentialDescriptor, - ) -> Result { - Self::try_from_bytes(authnr, rp_id_hash, &descriptor.id) - } - - pub fn try_from_bytes( - authnr: &mut Authenticator, - rp_id_hash: &Bytes<32>, - id: &[u8], - ) -> Result { - let mut cred: Bytes = Bytes::new(); - cred.extend_from_slice(id) - .map_err(|_| Error::InvalidCredential)?; - - let encrypted_serialized = EncryptedSerializedCredential::try_from(CredentialId(cred))?; - - let kek = authnr - .state - .persistent - .key_encryption_key(&mut authnr.trussed)?; - - let serialized = try_syscall!(authnr.trussed.decrypt_chacha8poly1305( - // TODO: use RpId as associated data here? - kek, - &encrypted_serialized.0.ciphertext, - &rp_id_hash[..], - &encrypted_serialized.0.nonce, - &encrypted_serialized.0.tag, - )) - .map_err(|_| Error::InvalidCredential)? - .plaintext - .ok_or(Error::InvalidCredential)?; - - let credential = - Credential::deserialize(&serialized).map_err(|_| Error::InvalidCredential)?; - - Ok(credential) - } - - // Remove inessential metadata from credential. - // - // Called by the `id` method, see its documentation. - pub fn strip(&self) -> Self { + // This method is only kept for compatibility. To strip new credentials, use + // `StrippedCredential`. + #[must_use] + fn strip(&self) -> Self { info_now!(":: stripping ID"); let mut stripped = self.clone(); - let data = &mut stripped.data; + let rp = stripped.data.rp.as_mut(); + rp.name = None; + let user = stripped.data.user.as_mut(); + user.icon = None; + user.name = None; + user.display_name = None; + stripped + } +} - data.rp.name = None; - data.rp.url = None; +/// A reduced version of `FullCredential` that is used for non-resident credentials. +/// +/// As the credential data is encodeded in the credential ID, we only want to include necessary +/// data to keep the credential ID as short as possible. +#[derive(Clone, Debug, serde_indexed::DeserializeIndexed, serde_indexed::SerializeIndexed)] +pub struct StrippedCredential { + pub ctap: CtapVersion, + pub creation_time: u32, + pub use_counter: bool, + pub algorithm: i32, + pub key: Key, + pub nonce: ByteArray<12>, + // extensions + #[serde(skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, + // TODO: HACK -- remove + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob_key: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, +} - data.user.icon = None; - data.user.name = None; - data.user.display_name = None; +impl StrippedCredential { + fn deserialize(bytes: &SerializedCredential) -> Result { + match cbor_smol::cbor_deserialize(bytes) { + Ok(s) => Ok(s), + Err(_) => { + info_now!("could not deserialize {:?}", bytes); + Err(Error::Other) + } + } + } - // data.hmac_secret = None; - // data.cred_protect = None; + pub fn id( + &self, + trussed: &mut T, + key_encryption_key: KeyId, + rp_id_hash: &[u8; 32], + ) -> Result { + CredentialId::new(trussed, self, key_encryption_key, rp_id_hash, &self.nonce) + } +} - stripped +impl From<&FullCredential> for StrippedCredential { + fn from(credential: &FullCredential) -> Self { + Self { + ctap: credential.ctap, + creation_time: credential.data.creation_time, + use_counter: credential.data.use_counter, + algorithm: credential.data.algorithm, + key: credential.data.key.clone(), + nonce: credential.nonce, + hmac_secret: credential.data.hmac_secret, + cred_protect: credential.data.cred_protect, + large_blob_key: credential.data.large_blob_key, + third_party_payment: credential.data.third_party_payment, + } } } #[cfg(test)] mod test { use super::*; + use hex_literal::hex; + use littlefs2_core::path; + use rand::SeedableRng as _; + use rand_chacha::ChaCha8Rng; + use serde_test::{assert_de_tokens, assert_tokens, Token}; + use trussed::{ + client::{Chacha8Poly1305, Sha256}, + key::{Kind, Secrecy}, + store::keystore::{ClientKeystore, Keystore as _}, + types::Location, + virt::{self, StoreConfig}, + Platform as _, + }; fn credential_data() -> CredentialData { - use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; - CredentialData { - rp: PublicKeyCredentialRpEntity { + rp: Rp::new(PublicKeyCredentialRpEntity { id: String::from("John Doe"), name: None, - url: None, - }, - user: PublicKeyCredentialUserEntity { + icon: None, + }), + user: User::new(PublicKeyCredentialUserEntity { id: Bytes::from_slice(&[1, 2, 3]).unwrap(), icon: None, name: None, display_name: None, + }), + creation_time: 123, + use_counter: false, + algorithm: -7, + key: Key::WrappedKey(Bytes::from_slice(&[1, 2, 3]).unwrap()), + hmac_secret: Some(false), + cred_protect: None, + use_short_id: Some(true), + large_blob_key: Some(ByteArray::new([0xff; 32])), + third_party_payment: Some(true), + } + } + + fn old_credential_data() -> CredentialData { + CredentialData { + rp: Rp { + format: SerializationFormat::Long, + inner: PublicKeyCredentialRpEntity { + id: String::from("John Doe"), + name: None, + icon: None, + }, + }, + user: User { + format: SerializationFormat::Long, + inner: PublicKeyCredentialUserEntity { + id: Bytes::from_slice(&[1, 2, 3]).unwrap(), + icon: None, + name: None, + display_name: None, + }, }, creation_time: 123, use_counter: false, @@ -362,9 +841,19 @@ mod test { key: Key::WrappedKey(Bytes::from_slice(&[1, 2, 3]).unwrap()), hmac_secret: Some(false), cred_protect: None, + use_short_id: None, + large_blob_key: None, + third_party_payment: None, } } + fn random_byte_array() -> ByteArray { + use rand::{rngs::OsRng, RngCore}; + let mut bytes = [0; N]; + OsRng.fill_bytes(&mut bytes); + ByteArray::new(bytes) + } + fn random_bytes() -> Bytes { use rand::{ distributions::{Distribution, Uniform}, @@ -421,26 +910,27 @@ mod test { } fn random_credential_data() -> CredentialData { - use ctap_types::webauthn::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}; - CredentialData { - rp: PublicKeyCredentialRpEntity { + rp: Rp::new(PublicKeyCredentialRpEntity { id: random_string(), name: maybe_random_string(), - url: maybe_random_string(), - }, - user: PublicKeyCredentialUserEntity { + icon: None, + }), + user: User::new(PublicKeyCredentialUserEntity { id: random_bytes(), //Bytes::from_slice(&[1,2,3]).unwrap(), icon: maybe_random_string(), name: maybe_random_string(), display_name: maybe_random_string(), - }, + }), creation_time: 123, use_counter: false, algorithm: -7, key: Key::WrappedKey(random_bytes()), hmac_secret: Some(false), cred_protect: None, + use_short_id: Some(true), + large_blob_key: Some(random_byte_array()), + third_party_payment: Some(false), } } @@ -461,6 +951,391 @@ mod test { assert_eq!(credential_data, deserialized); } + #[test] + fn old_credential_id() { + // generated with v0.1.1-nitrokey.4 (NK3 firmware version v1.4.0) + const OLD_ID: &[u8] = &hex!("A300583A71AEF80C4DA56033D66EB3266E9ACB8D84923D13F89BCBCE9FF30D8CD77ED968A436CA3D39C49999EC0F69A289CB2A65A08ABF251DEB21BB4B56014C00000000000000000000000002504DF499ABDAE80F5615C870985B74A799"); + const SERIALIZED_DATA: &[u8] = &hex!( + "A700A1626964684A6F686E20446F6501A16269644301020302187B03F404260582014301020306F4" + ); + const SERIALIZED_CREDENTIAL: &[u8] = &hex!("A3000201A700A1626964684A6F686E20446F6501A16269644301020302187B03F404260582014301020306F4024C000000000000000000000000"); + + virt::with_platform(StoreConfig::ram(), |mut platform| { + let kek = [0; 44]; + let client_id = path!("fido"); + let kek = { + let rng = ChaCha8Rng::from_rng(platform.rng()).unwrap(); + let mut keystore = ClientKeystore::new(client_id.into(), rng, platform.store()); + keystore + .store_key( + Location::Internal, + Secrecy::Secret, + Kind::Symmetric32Nonce(12), + &kek, + ) + .unwrap() + }; + platform.run_client(client_id.as_str(), |mut client| { + let data = old_credential_data(); + let rp_id_hash = syscall!(client.hash_sha256(data.rp.id().as_ref())).hash; + let encrypted_serialized = CredentialIdRef(OLD_ID).deserialize().unwrap(); + let serialized = syscall!(client.decrypt_chacha8poly1305( + kek, + &encrypted_serialized.ciphertext, + &rp_id_hash, + &encrypted_serialized.nonce, + &encrypted_serialized.tag, + )) + .plaintext + .unwrap(); + + let full = FullCredential::deserialize(&serialized).unwrap(); + assert_eq!( + full, + FullCredential { + ctap: CtapVersion::Fido21Pre, + data, + nonce: [0; 12].into(), + } + ); + + let stripped_credential = full.strip(); + + let serialized_data: Bytes<1024> = + trussed::cbor_serialize_bytes(&stripped_credential.data).unwrap(); + assert_eq!( + delog::hexstr!(&serialized_data).to_string(), + delog::hexstr!(SERIALIZED_DATA).to_string() + ); + + let serialized_credential: Bytes<1024> = + trussed::cbor_serialize_bytes(&stripped_credential).unwrap(); + assert_eq!( + delog::hexstr!(&serialized_credential).to_string(), + delog::hexstr!(SERIALIZED_CREDENTIAL).to_string() + ); + + let credential = Credential::Full(full); + let id = credential + .id(&mut client, kek, rp_id_hash.as_ref().try_into().unwrap()) + .unwrap() + .0; + assert_eq!( + delog::hexstr!(&id).to_string(), + delog::hexstr!(OLD_ID).to_string() + ); + }); + }); + } + + #[test] + fn credential_ids() { + trussed::virt::with_client(StoreConfig::ram(), "fido", |mut client| { + let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; + let nonce = ByteArray::new([0; 12]); + let data = credential_data(); + let mut full_credential = FullCredential { + ctap: CtapVersion::Fido21Pre, + data, + nonce, + }; + let rp_id_hash = syscall!(client.hash_sha256(full_credential.rp.id().as_ref())) + .hash + .as_slice() + .try_into() + .unwrap(); + + // Case 1: credential with use_short_id = Some(true) uses new (short) format + full_credential.data.use_short_id = Some(true); + let stripped_credential = StrippedCredential::from(&full_credential); + let full_id = full_credential + .id(&mut client, kek, Some(&rp_id_hash)) + .unwrap(); + let short_id = stripped_credential + .id(&mut client, kek, &rp_id_hash) + .unwrap(); + assert_eq!(full_id.0, short_id.0); + + // Case 2: credential with use_short_id = None uses old (long) format + full_credential.data.use_short_id = None; + let stripped_credential = full_credential.strip(); + let full_id = full_credential + .id(&mut client, kek, Some(&rp_id_hash)) + .unwrap(); + let long_id = CredentialId::new( + &mut client, + &stripped_credential, + kek, + &rp_id_hash, + &full_credential.nonce, + ) + .unwrap(); + assert_eq!(full_id.0, long_id.0); + + assert!(short_id.0.len() < long_id.0.len()); + }); + } + + #[test] + fn max_credential_id() { + let rp_id: String<256> = core::iter::repeat_n('?', 256).collect(); + let key = Bytes::from_slice(&[u8::MAX; 128]).unwrap(); + let credential = StrippedCredential { + ctap: CtapVersion::Fido21Pre, + creation_time: u32::MAX, + use_counter: true, + algorithm: i32::MAX, + key: Key::WrappedKey(key), + nonce: ByteArray::new([u8::MAX; 12]), + hmac_secret: Some(true), + cred_protect: Some(CredentialProtectionPolicy::Required), + large_blob_key: Some(ByteArray::new([0xff; 32])), + third_party_payment: Some(true), + }; + trussed::virt::with_client(StoreConfig::ram(), "fido", |mut client| { + let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; + let rp_id_hash = syscall!(client.hash_sha256(rp_id.as_ref())) + .hash + .as_slice() + .try_into() + .unwrap(); + let id = credential.id(&mut client, kek, &rp_id_hash).unwrap(); + assert_eq!(id.0.len(), 241); + }); + } + + fn test_serde(item: &T, name: &'static str, fields: &[(&'static str, Token)]) + where + for<'a> T: core::fmt::Debug + PartialEq + serde::Deserialize<'a> + serde::Serialize, + { + let len = fields.len(); + + let mut struct_tokens = vec![Token::Struct { name, len }]; + let mut map_tokens = vec![Token::Map { len: Some(len) }]; + for (key, value) in fields { + struct_tokens.push(Token::Str(key)); + struct_tokens.push(Token::Some); + struct_tokens.push(*value); + + map_tokens.push(Token::Str(key)); + map_tokens.push(Token::Some); + map_tokens.push(*value); + } + struct_tokens.push(Token::StructEnd); + map_tokens.push(Token::MapEnd); + + assert_tokens(item, &struct_tokens); + assert_de_tokens(item, &map_tokens); + } + + struct RpValues { + id: &'static str, + name: Option<&'static str>, + } + + impl RpValues { + fn test(&self) { + for format in [SerializationFormat::Short, SerializationFormat::Long] { + self.test_format(format); + } + } + + fn test_format(&self, format: SerializationFormat) { + let (id_field, name_field) = match format { + SerializationFormat::Short => ("i", "n"), + SerializationFormat::Long => ("id", "name"), + }; + let rp = Rp { + format, + inner: self.inner(), + }; + + let mut fields = vec![(id_field, Token::BorrowedStr(self.id))]; + if let Some(name) = self.name { + fields.push((name_field, Token::BorrowedStr(name))); + } + + test_serde(&rp, "RawRp", &fields); + } + + fn inner(&self) -> PublicKeyCredentialRpEntity { + PublicKeyCredentialRpEntity { + id: self.id.into(), + name: self.name.map(From::from), + icon: None, + } + } + } + + #[test] + fn serde_rp_name_none() { + RpValues { + id: "Testing rp id", + name: None, + } + .test() + } + + #[test] + fn serde_rp_name_some() { + RpValues { + id: "Testing rp id", + name: Some("Testing rp name"), + } + .test() + } + + struct UserValues { + id: &'static [u8], + icon: Option<&'static str>, + name: Option<&'static str>, + display_name: Option<&'static str>, + } + + impl UserValues { + fn test(&self) { + for format in [SerializationFormat::Short, SerializationFormat::Long] { + self.test_format(format); + } + } + + fn test_format(&self, format: SerializationFormat) { + let (id_field, icon_field, name_field, display_name_field) = match format { + SerializationFormat::Short => ("i", "I", "n", "d"), + SerializationFormat::Long => ("id", "icon", "name", "displayName"), + }; + let user = User { + format, + inner: self.inner(), + }; + + let mut fields = vec![(id_field, Token::BorrowedBytes(self.id))]; + if let Some(icon) = self.icon { + fields.push((icon_field, Token::BorrowedStr(icon))); + } + if let Some(name) = self.name { + fields.push((name_field, Token::BorrowedStr(name))); + } + if let Some(display_name) = self.display_name { + fields.push((display_name_field, Token::BorrowedStr(display_name))); + } + + test_serde(&user, "RawUser", &fields); + } + + fn inner(&self) -> PublicKeyCredentialUserEntity { + PublicKeyCredentialUserEntity { + id: Bytes::from_slice(self.id).unwrap(), + icon: self.icon.map(From::from), + name: self.name.map(From::from), + display_name: self.display_name.map(From::from), + } + } + } + + #[test] + fn serde_user_full() { + UserValues { + id: b"Testing user id", + icon: Some("Testing user icon"), + name: Some("Testing user name"), + display_name: Some("Testing user display_name"), + } + .test(); + } + + #[test] + fn serde_user_display_name() { + UserValues { + id: b"Testing user id", + icon: None, + name: None, + display_name: Some("Testing user display_name"), + } + .test(); + } + + #[test] + fn serde_user_icon_display_name() { + UserValues { + id: b"Testing user id", + icon: Some("Testing user icon"), + name: None, + display_name: Some("Testing user display_name"), + } + .test(); + } + + #[test] + fn serde_user_icon() { + UserValues { + id: b"Testing user id", + icon: Some("Testing user icon"), + name: None, + display_name: None, + } + .test(); + } + + #[test] + fn serde_user_empty() { + UserValues { + id: b"Testing user id", + icon: None, + name: None, + display_name: None, + } + .test(); + } + + // Test credentials that were serialized before the migration to shorter field names for serialization + #[test] + fn legacy_full_credential() { + use hex_literal::hex; + let data = hex!( + " + a3000201a700a16269646b776562617574686e2e696f01a2626964476447 + 567a644445646e616d65657465737431020003f504260582005037635754 + c9882b21565a9f8a47b0ece408f5024cf62ca01ed181a3d03d561fc7 + " + ); + + let credential = FullCredential::deserialize(&Bytes::from_slice(&data).unwrap()).unwrap(); + assert!(matches!(credential.ctap, CtapVersion::Fido21Pre)); + assert_eq!(credential.nonce, &hex!("F62CA01ED181A3D03D561FC7")); + assert_eq!( + credential.data, + CredentialData { + rp: Rp { + format: SerializationFormat::Long, + inner: PublicKeyCredentialRpEntity { + id: "webauthn.io".into(), + name: None, + icon: None, + }, + }, + user: User { + format: SerializationFormat::Long, + inner: PublicKeyCredentialUserEntity { + id: Bytes::from_slice(&hex!("6447567A644445")).unwrap(), + icon: None, + name: Some("test1".into()), + display_name: None, + }, + }, + creation_time: 0, + use_counter: true, + algorithm: -7, + key: Key::ResidentKey(KeyId::from_value(0x37635754C9882B21565A9F8A47B0ECE4)), + hmac_secret: None, + cred_protect: None, + use_short_id: Some(true), + large_blob_key: None, + third_party_payment: None, + }, + ); + } + // use quickcheck::TestResult; // quickcheck::quickcheck! { // fn prop( diff --git a/src/ctap1.rs b/src/ctap1.rs index e519b82..b472a4e 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -4,15 +4,16 @@ use ctap_types::{ ctap1::{authenticate, register, Authenticator, ControlByte, Error, Result}, heapless_bytes::Bytes, }; +use serde_bytes::ByteArray; -use trussed::{ +use trussed_core::{ syscall, types::{KeySerialization, Location, Mechanism, SignatureSerialization}, }; use crate::{ constants, - credential::{self, Credential, Key}, + credential::{self, Credential, Key, StrippedCredential}, SigningAlgorithm, TrussedRequirements, UserPresence, }; @@ -50,8 +51,8 @@ impl Authenticator for crate::Authenti .serialize_p256_key(public_key, KeySerialization::EcdhEsHkdf256)) .serialized_key; syscall!(self.trussed.delete(public_key)); - let cose_key: ctap_types::cose::EcdhEsHkdf256PublicKey = - trussed::cbor_deserialize(&serialized_cose_public_key).unwrap(); + let cose_key: cosey::EcdhEsHkdf256PublicKey = + cbor_smol::cbor_deserialize(&serialized_cose_public_key).unwrap(); let wrapping_key = self .state @@ -63,7 +64,7 @@ impl Authenticator for crate::Authenti let wrapped_key = syscall!(self .trussed - .wrap_key_chacha8poly1305(wrapping_key, private_key, &[])) + .wrap_key_chacha8poly1305(wrapping_key, private_key, &[], None)) .wrapped_key; // debug!("wrapped_key = {:?}", &wrapped_key); @@ -74,45 +75,24 @@ impl Authenticator for crate::Authenti .to_bytes() .map_err(|_| Error::UnspecifiedCheckingError)?, ); - let nonce = syscall!(self.trussed.random_bytes(12)) - .bytes - .as_slice() - .try_into() - .unwrap(); - - let mut rp_id = heapless::String::new(); - - // We do not know the rpId string in U2F. Just using placeholder. - // TODO: Is this true? - // - rp_id.push_str("u2f").ok(); - let rp = ctap_types::webauthn::PublicKeyCredentialRpEntity { - id: rp_id, - name: None, - url: None, - }; - - let user = ctap_types::webauthn::PublicKeyCredentialUserEntity { - id: Bytes::from_slice(&[0u8; 8]).unwrap(), - icon: None, - name: None, - display_name: None, - }; + let nonce = ByteArray::new(self.nonce()); - let credential = Credential::new( - credential::CtapVersion::U2fV2, - &rp, - &user, - SigningAlgorithm::P256 as i32, - key, - self.state + let credential = StrippedCredential { + ctap: credential::CtapVersion::U2fV2, + creation_time: self + .state .persistent .timestamp(&mut self.trussed) .map_err(|_| Error::NotEnoughMemory)?, - None, - None, + use_counter: true, + algorithm: SigningAlgorithm::P256 as i32, + key, nonce, - ); + hmac_secret: None, + cred_protect: None, + large_blob_key: None, + third_party_payment: None, + }; // info!("made credential {:?}", &credential); @@ -123,14 +103,14 @@ impl Authenticator for crate::Authenti .key_encryption_key(&mut self.trussed) .map_err(|_| Error::NotEnoughMemory)?; let credential_id = credential - .id(&mut self.trussed, kek, Some(®.app_id)) + .id(&mut self.trussed, kek, reg.app_id) .map_err(|_| Error::NotEnoughMemory)?; let mut commitment = Commitment::new(); commitment.push(0).unwrap(); // reserve byte - commitment.extend_from_slice(®.app_id).unwrap(); - commitment.extend_from_slice(®.challenge).unwrap(); + commitment.extend_from_slice(reg.app_id).unwrap(); + commitment.extend_from_slice(reg.challenge).unwrap(); commitment.extend_from_slice(&credential_id.0).unwrap(); @@ -165,14 +145,14 @@ impl Authenticator for crate::Authenti Ok(register::Response::new( 0x05, &cose_key, - &credential_id.0, + credential_id.0, signature, - &cert, + cert, )) } fn authenticate(&mut self, auth: &authenticate::Request) -> Result { - let cred = Credential::try_from_bytes(self, &auth.app_id, &auth.key_handle); + let cred = Credential::try_from_bytes(self, auth.app_id, auth.key_handle); let user_presence_byte = match auth.control_byte { ControlByte::CheckOnly => { @@ -198,7 +178,7 @@ impl Authenticator for crate::Authenti let cred = cred.map_err(|_| Error::IncorrectDataParameter)?; - let key = match &cred.key { + let key = match cred.key() { Key::WrappedKey(bytes) => { let wrapping_key = self .state @@ -226,7 +206,7 @@ impl Authenticator for crate::Authenti _ => return Err(Error::IncorrectDataParameter), }; - if cred.algorithm != -7 { + if cred.algorithm() != -7 { info!("Unexpected mechanism for u2f"); return Err(Error::IncorrectDataParameter); } @@ -239,12 +219,12 @@ impl Authenticator for crate::Authenti let mut commitment = Commitment::new(); - commitment.extend_from_slice(&auth.app_id).unwrap(); + commitment.extend_from_slice(auth.app_id).unwrap(); commitment.push(user_presence_byte).unwrap(); commitment .extend_from_slice(&sig_count.to_be_bytes()) .unwrap(); - commitment.extend_from_slice(&auth.challenge).unwrap(); + commitment.extend_from_slice(auth.challenge).unwrap(); let signature = syscall!(self.trussed.sign( Mechanism::P256, diff --git a/src/ctap2.rs b/src/ctap2.rs index 79389ff..d30a443 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,120 +1,111 @@ //! The `ctap_types::ctap2::Authenticator` implementation. +use credential_management::CredentialManagement; use ctap_types::{ - ctap2::{self, Authenticator, VendorOperation}, + ctap2::{ + self, client_pin::Permissions, AttestationFormatsPreference, AttestationStatement, + AttestationStatementFormat, Authenticator, NoneAttestationStatement, + PackedAttestationStatement, VendorOperation, + }, heapless::{String, Vec}, heapless_bytes::Bytes, - sizes, Error, + sizes, + webauthn::PublicKeyCredentialUserEntity, + ByteArray, Error, }; +use littlefs2_core::{path, Path, PathBuf}; +use sha2::{Digest as _, Sha256}; -use littlefs2::path::Path; - -use trussed::{ +use trussed_core::{ syscall, try_syscall, - types::{ - KeyId, KeySerialization, Location, Mechanism, MediumData, Message, PathBuf, - SignatureSerialization, - }, + types::{KeyId, Location, Mechanism, MediumData, Message, StorageAttributes}, }; use crate::{ - constants, - credential::{ - self, - Credential, - // CredentialList, - Key, - }, - format_hex, - state::{ - self, - // // (2022-02-27): 9288 bytes - // MinCredentialHeap, - }, - Result, SigningAlgorithm, TrussedRequirements, UserPresence, + constants::{self, MAX_RESIDENT_CREDENTIALS_GUESSTIMATE}, + credential::{self, Credential, FullCredential, Key, StrippedCredential}, + format_hex, state, Result, SigningAlgorithm, TrussedRequirements, UserPresence, }; #[allow(unused_imports)] use crate::msp; pub mod credential_management; -// pub mod pin; +pub mod large_blobs; +pub mod pin; + +use pin::{PinProtocol, PinProtocolVersion, RpScope, SharedSecret}; + +pub const RK_DIR: &Path = path!("rk"); /// Implement `ctap2::Authenticator` for our Authenticator. impl Authenticator for crate::Authenticator { #[inline(never)] fn get_info(&mut self) -> ctap2::get_info::Response { + use ctap2::get_info::{Extension, Transport, Version}; + debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); - use core::str::FromStr; - let mut versions = Vec::, 4>::new(); - versions.push(String::from_str("U2F_V2").unwrap()).unwrap(); - versions - .push(String::from_str("FIDO_2_0").unwrap()) - .unwrap(); - // #[cfg(feature = "enable-fido-pre")] - versions - .push(String::from_str("FIDO_2_1_PRE").unwrap()) - .unwrap(); + let mut versions = Vec::new(); + versions.push(Version::U2fV2).unwrap(); + versions.push(Version::Fido2_0).unwrap(); + versions.push(Version::Fido2_1).unwrap(); - let mut extensions = Vec::, 4>::new(); - // extensions.push(String::from_str("credProtect").unwrap()).unwrap(); - extensions - .push(String::from_str("credProtect").unwrap()) - .unwrap(); - extensions - .push(String::from_str("hmac-secret").unwrap()) - .unwrap(); + let mut extensions = Vec::new(); + extensions.push(Extension::CredProtect).unwrap(); + extensions.push(Extension::HmacSecret).unwrap(); + if self.config.supports_large_blobs() { + extensions.push(Extension::LargeBlobKey).unwrap(); + } + extensions.push(Extension::ThirdPartyPayment).unwrap(); - let mut pin_protocols = Vec::::new(); - pin_protocols.push(1).unwrap(); - - let options = ctap2::get_info::CtapOptions { - ep: None, - rk: true, - up: true, - uv: None, - plat: Some(false), - cred_mgmt: Some(true), - client_pin: match self.state.persistent.pin_is_set() { - true => Some(true), - false => Some(false), - }, - credential_mgmt_preview: Some(true), - make_cred_uv_not_rqd: Some(true), - ..Default::default() + let mut pin_protocols = Vec::new(); + for pin_protocol in self.pin_protocols() { + pin_protocols.push(u8::from(*pin_protocol)).unwrap(); + } + + let mut options = ctap2::get_info::CtapOptions::default(); + options.rk = true; + options.up = true; + options.plat = Some(false); + options.cred_mgmt = Some(true); + options.client_pin = match self.state.persistent.pin_is_set() { + true => Some(true), + false => Some(false), }; - // options.rk = true; - // options.up = true; - // options.uv = None; // "uv" here refers to "in itself", e.g. biometric - // options.plat = Some(false); - // options.cred_mgmt = Some(true); - // options.credential_mgmt_preview = Some(true); - // // options.client_pin = None; // not capable of PIN - // options.client_pin = match self.state.persistent.pin_is_set() { - // true => Some(true), - // false => Some(false), - // }; + options.large_blobs = Some(self.config.supports_large_blobs()); + options.pin_uv_auth_token = Some(true); + options.make_cred_uv_not_rqd = Some(true); let mut transports = Vec::new(); - transports.push(String::from("nfc")).unwrap(); - transports.push(String::from("usb")).unwrap(); + if self.config.nfc_transport { + transports.push(Transport::Nfc).unwrap(); + } + transports.push(Transport::Usb).unwrap(); + + let mut attestation_formats = Vec::new(); + attestation_formats + .push(AttestationStatementFormat::Packed) + .unwrap(); + attestation_formats + .push(AttestationStatementFormat::None) + .unwrap(); let (_, aaguid) = self.state.identity.attestation(&mut self.trussed); - ctap2::get_info::Response { - versions, - extensions: Some(extensions), - aaguid: Bytes::from_slice(&aaguid).unwrap(), - options: Some(options), - transports: Some(transports), - // 1200 - max_msg_size: Some(self.config.max_msg_size), - pin_protocols: Some(pin_protocols), - max_creds_in_list: Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST), - max_cred_id_length: Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH), - ..ctap2::get_info::Response::default() - } + let mut response = ctap2::get_info::Response::default(); + response.versions = versions; + response.extensions = Some(extensions); + response.aaguid = Bytes::from_slice(&aaguid).unwrap(); + response.options = Some(options); + response.transports = Some(transports); + // 1200 + response.max_msg_size = Some(self.config.max_msg_size); + response.pin_protocols = Some(pin_protocols); + response.max_creds_in_list = Some(ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST); + response.max_cred_id_length = Some(ctap_types::sizes::MAX_CREDENTIAL_ID_LENGTH); + response.attestation_formats = Some(attestation_formats); + response } #[inline(never)] @@ -148,7 +139,7 @@ impl Authenticator for crate::Authenti // 7. reset timer // 8. increment credential counter (not applicable) - self.assert_with_credential(None, credential) + self.assert_with_credential(None, Credential::Full(credential)) } #[inline(never)] @@ -165,11 +156,16 @@ impl Authenticator for crate::Authenti return Err(Error::InvalidOption); } } + if parameters.enterprise_attestation.is_some() { + return Err(Error::InvalidParameter); + } let uv_performed = self.pin_prechecks( ¶meters.options, - ¶meters.pin_auth, - ¶meters.pin_protocol, + parameters.pin_auth.map(AsRef::as_ref), + parameters.pin_protocol, parameters.client_data_hash.as_ref(), + Permissions::MAKE_CREDENTIAL, + ¶meters.rp.id, )?; // 5. "persist credProtect value for this credential" @@ -183,7 +179,7 @@ impl Authenticator for crate::Authenti if let Ok(excluded_cred) = result { use credential::CredentialProtectionPolicy; // If UV is not performed, than CredProtectRequired credentials should not be visibile. - if !(excluded_cred.cred_protect == Some(CredentialProtectionPolicy::Required)) + if !(excluded_cred.cred_protect() == Some(CredentialProtectionPolicy::Required)) || uv_performed { info_now!("Excluded!"); @@ -198,12 +194,7 @@ impl Authenticator for crate::Authenti // 7. check pubKeyCredParams algorithm is valid + supported COSE identifier let mut algorithm: Option = None; - for param in parameters.pub_key_cred_params.iter() { - // Ignore unknown key types - if param.key_type != "public-key" { - continue; - } - + for param in parameters.pub_key_cred_params.0.iter() { match param.alg { -7 => { if algorithm.is_none() { @@ -213,7 +204,6 @@ impl Authenticator for crate::Authenti -8 => { algorithm = Some(SigningAlgorithm::Ed25519); } - // -9 => { algorithm = Some(SigningAlgorithm::Totp); } _ => {} } } @@ -241,6 +231,8 @@ impl Authenticator for crate::Authenti let mut hmac_secret_requested = None; // let mut cred_protect_requested = CredentialProtectionPolicy::Optional; let mut cred_protect_requested = None; + let mut large_blob_key_requested = false; + let mut third_party_payment_requested = false; if let Some(extensions) = ¶meters.extensions { hmac_secret_requested = extensions.hmac_secret; @@ -248,6 +240,23 @@ impl Authenticator for crate::Authenti cred_protect_requested = Some(credential::CredentialProtectionPolicy::try_from(*policy)?); } + + if self.config.supports_large_blobs() { + if let Some(large_blob_key) = extensions.large_blob_key { + if large_blob_key { + if !rk_requested { + // the largeBlobKey extension is only available for resident keys + return Err(Error::InvalidOption); + } + large_blob_key_requested = true; + } else { + // large_blob_key must be Some(true) or omitted, Some(false) is invalid + return Err(Error::InvalidOption); + } + } + } + + third_party_payment_requested = extensions.third_party_payment.unwrap_or_default(); } // debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); @@ -261,55 +270,8 @@ impl Authenticator for crate::Authenti true => Location::Internal, false => Location::Volatile, }; - - let private_key: KeyId; - let public_key: KeyId; - let cose_public_key; - match algorithm { - SigningAlgorithm::P256 => { - private_key = syscall!(self.trussed.generate_p256_private_key(location)).key; - public_key = syscall!(self - .trussed - .derive_p256_public_key(private_key, Location::Volatile)) - .key; - cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, - public_key, - KeySerialization::Cose - )) - .serialized_key; - let _success = syscall!(self.trussed.delete(public_key)).success; - info_now!("deleted public P256 key: {}", _success); - } - SigningAlgorithm::Ed25519 => { - private_key = syscall!(self.trussed.generate_ed255_private_key(location)).key; - public_key = syscall!(self - .trussed - .derive_ed255_public_key(private_key, Location::Volatile)) - .key; - cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::Ed255, - public_key, - KeySerialization::Cose - )) - .serialized_key; - let _success = syscall!(self.trussed.delete(public_key)).success; - info_now!("deleted public Ed25519 key: {}", _success); - } // SigningAlgorithm::Totp => { - // if parameters.client_data_hash.len() != 32 { - // return Err(Error::InvalidParameter); - // } - // // b'TOTP---W\x0e\xf1\xe0\xd7\x83\xfe\t\xd1\xc1U\xbf\x08T_\x07v\xb2\xc6--TOTP' - // let totp_secret: [u8; 20] = parameters.client_data_hash[6..26].try_into().unwrap(); - // private_key = syscall!(self.trussed.unsafe_inject_shared_key( - // &totp_secret, Location::Internal)).key; - // // info_now!("totes injected"); - // let fake_cose_pk = ctap_types::cose::TotpPublicKey {}; - // let fake_serialized_cose_pk = trussed::cbor_serialize_bytes(&fake_cose_pk) - // .map_err(|_| Error::NotAllowed)?; - // cose_public_key = fake_serialized_cose_pk; // Bytes::from_slice(&[0u8; 20]).unwrap(); - // } - } + let private_key = algorithm.generate_private_key(&mut self.trussed, location); + let cose_public_key = algorithm.derive_public_key(&mut self.trussed, private_key); // 12. if `rk` is set, store or overwrite key pair, if full error KeyStoreFull @@ -319,11 +281,13 @@ impl Authenticator for crate::Authenti false => { // WrappedKey version let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; - let wrapped_key = - syscall!(self - .trussed - .wrap_key_chacha8poly1305(wrapping_key, private_key, &[])) - .wrapped_key; + let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305( + wrapping_key, + private_key, + &[], + None + )) + .wrapped_key; // 32B key, 12B nonce, 16B tag + some info on algorithm (P256/Ed25519) // Turns out it's size 92 (enum serialization not optimized yet...) @@ -334,11 +298,7 @@ impl Authenticator for crate::Authenti }; // injecting this is a bit mehhh.. - let nonce = syscall!(self.trussed.random_bytes(12)) - .bytes - .as_slice() - .try_into() - .unwrap(); + let nonce = self.nonce(); info_now!("nonce = {:?}", &nonce); // 12.b generate credential ID { = AEAD(Serialize(Credential)) } @@ -350,7 +310,14 @@ impl Authenticator for crate::Authenti // store it. // TODO: overwrite, error handling with KeyStoreFull - let credential = Credential::new( + let large_blob_key = if large_blob_key_requested { + let key = syscall!(self.trussed.random_bytes(32)).bytes; + Some(ByteArray::new(key.as_slice().try_into().unwrap())) + } else { + None + }; + + let credential = FullCredential::new( credential::CtapVersion::Fido21Pre, ¶meters.rp, ¶meters.user, @@ -359,31 +326,47 @@ impl Authenticator for crate::Authenti self.state.persistent.timestamp(&mut self.trussed)?, hmac_secret_requested, cred_protect_requested, + large_blob_key, + third_party_payment_requested.then_some(true), nonce, ); // note that this does the "stripping" of OptionalUI etc. - let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; + let credential_id = + StrippedCredential::from(&credential).id(&mut self.trussed, kek, &rp_id_hash)?; if rk_requested { // serialization with all metadata let serialized_credential = credential.serialize()?; // first delete any other RK cred with same RP + UserId if there is one. - self.delete_resident_key_by_user_id(&rp_id_hash, &credential.user.id) + self.delete_resident_key_by_user_id(&rp_id_hash, credential.user.id()) .ok(); - // then store key, making it resident - let credential_id_hash = self.hash(credential_id.0.as_ref()); - try_syscall!(self.trussed.write_file( - Location::Internal, - rk_path(&rp_id_hash, &credential_id_hash), - serialized_credential, - // user attribute for later easy lookup - // Some(rp_id_hash.clone()), - None, - )) - .map_err(|_| Error::KeyStoreFull)?; + let mut key_store_full = self.can_fit(serialized_credential.len()) == Some(false) + || CredentialManagement::new(self).count_credentials()? + >= self + .config + .max_resident_credential_count + .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE); + + if !key_store_full { + // then store key, making it resident + let credential_id_hash = self.hash(credential_id.0.as_ref()); + let result = try_syscall!(self.trussed.write_file( + Location::Internal, + rk_path(&rp_id_hash, &credential_id_hash), + serialized_credential, + // user attribute for later easy lookup + // Some(rp_id_hash.clone()), + None, + )); + key_store_full = result.is_err(); + } + + if key_store_full { + return Err(Error::KeyStoreFull); + } } // 13. generate and return attestation statement using clientDataHash @@ -395,7 +378,7 @@ impl Authenticator for crate::Authenti let (attestation_maybe, aaguid) = self.state.identity.attestation(&mut self.trussed); let authenticator_data = ctap2::make_credential::AuthenticatorData { - rp_id_hash: rp_id_hash.to_bytes().map_err(|_| Error::Other)?, + rp_id_hash: &rp_id_hash, flags: { let mut flags = Flags::USER_PRESENCE; @@ -416,9 +399,9 @@ impl Authenticator for crate::Authenti attested_credential_data: { // debug_now!("acd in, cid len {}, pk len {}", credential_id.0.len(), cose_public_key.len()); let attested_credential_data = ctap2::make_credential::AttestedCredentialData { - aaguid: Bytes::from_slice(&aaguid).unwrap(), - credential_id: credential_id.0.to_bytes().unwrap(), - credential_public_key: cose_public_key.to_bytes().unwrap(), + aaguid: &aaguid, + credential_id: &credential_id.0, + credential_public_key: &cose_public_key, }; // debug_now!("cose PK = {:?}", &attested_credential_data.credential_public_key); Some(attested_credential_data) @@ -426,10 +409,10 @@ impl Authenticator for crate::Authenti extensions: { if hmac_secret_requested.is_some() || cred_protect_requested.is_some() { - Some(ctap2::make_credential::Extensions { - cred_protect: parameters.extensions.as_ref().unwrap().cred_protect, - hmac_secret: parameters.extensions.as_ref().unwrap().hmac_secret, - }) + let mut extensions = ctap2::make_credential::Extensions::default(); + extensions.cred_protect = parameters.extensions.as_ref().unwrap().cred_protect; + extensions.hmac_secret = parameters.extensions.as_ref().unwrap().hmac_secret; + Some(extensions) } else { None } @@ -437,107 +420,66 @@ impl Authenticator for crate::Authenti }; // debug_now!("authData = {:?}", &authenticator_data); - let serialized_auth_data = authenticator_data.serialize(); - - // 13.b The Signature + let serialized_auth_data = authenticator_data.serialize()?; - // can we write Sum somehow? - // debug_now!("seeking commitment, {} + {}", serialized_auth_data.len(), parameters.client_data_hash.len()); - let mut commitment = Bytes::<1024>::new(); - commitment - .extend_from_slice(&serialized_auth_data) - .map_err(|_| Error::Other)?; - // debug_now!("serialized_auth_data ={:?}", &serialized_auth_data); - commitment - .extend_from_slice(¶meters.client_data_hash) - .map_err(|_| Error::Other)?; - // debug_now!("client_data_hash = {:?}", ¶meters.client_data_hash); - // debug_now!("commitment = {:?}", &commitment); - - // NB: the other/normal one is called "basic" or "batch" attestation, - // because it attests the authenticator is part of a batch: the model - // specified by AAGUID. - // "self signed" is also called "surrogate basic". - // - // we should also directly support "none" format, it's a bit weird - // how browsers firefox this - - let (signature, attestation_algorithm) = { - if attestation_maybe.is_none() { - match algorithm { - SigningAlgorithm::Ed25519 => { - let signature = - syscall!(self.trussed.sign_ed255(private_key, &commitment)).signature; - (signature.to_bytes().map_err(|_| Error::Other)?, -8) - } - - SigningAlgorithm::P256 => { - // DO NOT prehash here, `trussed` does that - let der_signature = syscall!(self.trussed.sign_p256( - private_key, - &commitment, - SignatureSerialization::Asn1Der - )) - .signature; - (der_signature.to_bytes().map_err(|_| Error::Other)?, -7) - } // SigningAlgorithm::Totp => { - // // maybe we can fake it here too, but seems kinda weird - // // return Err(Error::UnsupportedAlgorithm); - // // micro-ecc is borked. let's self-sign anyway - // let hash = syscall!(self.trussed.hash_sha256(&commitment.as_ref())).hash; - // let tmp_key = syscall!(self.trussed - // .generate_p256_private_key(Location::Volatile)) - // .key; - - // let signature = syscall!(self.trussed.sign_p256( - // tmp_key, - // &hash, - // SignatureSerialization::Asn1Der, - // )).signature; - // (signature.to_bytes().map_err(|_| Error::Other)?, -7) - // } + // select attestation format or use packed attestation as default + let att_stmt_fmt = parameters + .attestation_formats_preference + .as_ref() + .map(SupportedAttestationFormat::select) + .unwrap_or(Some(SupportedAttestationFormat::Packed)); + let att_stmt = if let Some(format) = att_stmt_fmt { + match format { + SupportedAttestationFormat::None => { + Some(AttestationStatement::None(NoneAttestationStatement {})) + } + SupportedAttestationFormat::Packed => { + let mut commitment = Bytes::<1024>::new(); + commitment + .extend_from_slice(&serialized_auth_data) + .map_err(|_| Error::Other)?; + commitment + .extend_from_slice(parameters.client_data_hash) + .map_err(|_| Error::Other)?; + + let (attestation_key, attestation_algorithm) = attestation_maybe + .as_ref() + .map(|attestation| (attestation.0, SigningAlgorithm::P256)) + .unwrap_or((private_key, algorithm)); + let signature = + attestation_algorithm.sign(&mut self.trussed, attestation_key, &commitment); + let packed = PackedAttestationStatement { + alg: attestation_algorithm.into(), + sig: signature.to_bytes().map_err(|_| Error::Other)?, + x5c: attestation_maybe.as_ref().map(|attestation| { + // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements + let cert = attestation.1.clone(); + let mut x5c = Vec::new(); + x5c.push(cert).ok(); + x5c + }), + }; + Some(AttestationStatement::Packed(packed)) } - } else { - let signature = syscall!(self.trussed.sign_p256( - attestation_maybe.as_ref().unwrap().0, - &commitment, - SignatureSerialization::Asn1Der, - )) - .signature; - (signature.to_bytes().map_err(|_| Error::Other)?, -7) } + } else { + None }; - // debug_now!("SIG = {:?}", &signature); if !rk_requested { let _success = syscall!(self.trussed.delete(private_key)).success; info_now!("deleted private credential key: {}", _success); } - let packed_attn_stmt = ctap2::make_credential::PackedAttestationStatement { - alg: attestation_algorithm, - sig: signature, - x5c: match attestation_maybe.is_some() { - false => None, - true => { - // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements - let cert = attestation_maybe.as_ref().unwrap().1.clone(); - let mut x5c = Vec::new(); - x5c.push(cert).ok(); - Some(x5c) - } - }, - }; - - let fmt = String::<32>::from("packed"); - let att_stmt = ctap2::make_credential::AttestationStatement::Packed(packed_attn_stmt); - - let attestation_object = ctap2::make_credential::Response { - fmt, + let mut attestation_object = ctap2::make_credential::ResponseBuilder { + fmt: att_stmt_fmt + .map(From::from) + .unwrap_or(AttestationStatementFormat::None), auth_data: serialized_auth_data, - att_stmt, - }; - + } + .build(); + attestation_object.att_stmt = att_stmt; + attestation_object.large_blob_key = large_blob_key; Ok(attestation_object) } @@ -560,7 +502,10 @@ impl Authenticator for crate::Authenti syscall!(self.trussed.delete_all(Location::Internal)); syscall!(self .trussed - .remove_dir_all(Location::Internal, PathBuf::from("rk"),)); + .remove_dir_all(Location::Internal, RK_DIR.into())); + + // Delete large-blob array + large_blobs::reset(&mut self.trussed); // b. delete persistent state self.state.persistent.reset(&mut self.trussed)?; @@ -579,51 +524,30 @@ impl Authenticator for crate::Authenti #[inline(never)] fn client_pin( &mut self, - parameters: &ctap2::client_pin::Request, + parameters: &ctap2::client_pin::Request<'_>, ) -> Result { use ctap2::client_pin::PinV1Subcommand as Subcommand; debug_now!("CTAP2.PIN..."); // info_now!("{:?}", parameters); - // TODO: Handle pin protocol V2 - if parameters.pin_protocol != 1 { - return Err(Error::InvalidParameter); - } + let pin_protocol = parameters + .pin_protocol + .ok_or(Error::MissingParameter) + .and_then(|pin_protocol| self.parse_pin_protocol(pin_protocol)); + let mut response = ctap2::client_pin::Response::default(); - Ok(match parameters.sub_command { + match parameters.sub_command { Subcommand::GetRetries => { debug_now!("CTAP2.Pin.GetRetries"); - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: Some(self.state.persistent.retries()), - } + response.retries = Some(self.state.persistent.retries()); } Subcommand::GetKeyAgreement => { debug_now!("CTAP2.Pin.GetKeyAgreement"); - let private_key = self.state.runtime.key_agreement_key(&mut self.trussed); - let public_key = syscall!(self - .trussed - .derive_p256_public_key(private_key, Location::Volatile)) - .key; - let serialized_cose_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, - public_key, - KeySerialization::EcdhEsHkdf256 - )) - .serialized_key; - let cose_key = trussed::cbor_deserialize(&serialized_cose_key).unwrap(); - - syscall!(self.trussed.delete(public_key)); - - ctap2::client_pin::Response { - key_agreement: cose_key, - pin_token: None, - retries: None, - } + let pin_protocol = pin_protocol?; + response.key_agreement = Some(self.pin_protocol(pin_protocol).key_agreement_key()); } Subcommand::SetPin => { @@ -647,6 +571,7 @@ impl Authenticator for crate::Authenti return Err(Error::MissingParameter); } }; + let pin_protocol = pin_protocol?; // 2. is pin already set if self.state.persistent.pin_is_set() { @@ -654,34 +579,26 @@ impl Authenticator for crate::Authenti } // 3. generate shared secret - let shared_secret = self - .state - .runtime - .generate_shared_secret(&mut self.trussed, platform_kek)?; + let mut pin_protocol = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol.shared_secret(platform_kek)?; // TODO: there are moar early returns!! // - implement Drop? // - do garbage collection outside of this? // 4. verify pinAuth - self.verify_pin_auth(shared_secret, new_pin_enc, pin_auth)?; + pin_protocol.verify_pin_auth(&shared_secret, new_pin_enc, pin_auth)?; // 5. decrypt and verify new PIN - let new_pin = self.decrypt_pin_check_length(shared_secret, new_pin_enc)?; + let new_pin = self.decrypt_pin_check_length(&shared_secret, new_pin_enc)?; - syscall!(self.trussed.delete(shared_secret)); + shared_secret.delete(&mut self.trussed); // 6. store LEFT(SHA-256(newPin), 16), set retries to 8 self.hash_store_pin(&new_pin)?; self.state .reset_retries(&mut self.trussed) .map_err(|_| Error::Other)?; - - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: None, - } } Subcommand::ChangePin => { @@ -712,15 +629,14 @@ impl Authenticator for crate::Authenti return Err(Error::MissingParameter); } }; + let pin_protocol = pin_protocol?; // 2. fail if no retries left self.state.pin_blocked()?; // 3. generate shared secret - let shared_secret = self - .state - .runtime - .generate_shared_secret(&mut self.trussed, platform_kek)?; + let mut pin_protocol_impl = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol_impl.shared_secret(platform_kek)?; // 4. verify pinAuth let mut data = MediumData::new(); @@ -728,111 +644,202 @@ impl Authenticator for crate::Authenti .map_err(|_| Error::InvalidParameter)?; data.extend_from_slice(pin_hash_enc) .map_err(|_| Error::InvalidParameter)?; - self.verify_pin_auth(shared_secret, &data, pin_auth)?; + pin_protocol_impl.verify_pin_auth(&shared_secret, &data, pin_auth)?; // 5. decrement retries self.state.decrement_retries(&mut self.trussed)?; // 6. decrypt pinHashEnc, compare with stored - self.decrypt_pin_hash_and_maybe_escalate(shared_secret, pin_hash_enc)?; + self.decrypt_pin_hash_and_maybe_escalate( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; // 7. reset retries self.state.reset_retries(&mut self.trussed)?; // 8. decrypt and verify new PIN - let new_pin = self.decrypt_pin_check_length(shared_secret, new_pin_enc)?; + let new_pin = self.decrypt_pin_check_length(&shared_secret, new_pin_enc)?; - syscall!(self.trussed.delete(shared_secret)); + shared_secret.delete(&mut self.trussed); // 9. store hashed PIN self.hash_store_pin(&new_pin)?; - ctap2::client_pin::Response { - key_agreement: None, - pin_token: None, - retries: None, - } + self.pin_protocol(pin_protocol).reset_pin_tokens(); } + // § 6.5.5.7.1 No 4 Subcommand::GetPinToken => { debug_now!("CTAP2.Pin.GetPinToken"); - // 1. check mandatory parameters - let platform_kek = match parameters.key_agreement.as_ref() { - Some(key) => key, - None => { - return Err(Error::MissingParameter); - } - }; - let pin_hash_enc = match parameters.pin_hash_enc.as_ref() { - Some(hash) => hash, - None => { - return Err(Error::MissingParameter); - } - }; + // 1. Check mandatory parameters + let key_agreement = parameters + .key_agreement + .as_ref() + .ok_or(Error::MissingParameter)?; + let pin_hash_enc = parameters + .pin_hash_enc + .as_ref() + .ok_or(Error::MissingParameter)?; - // 2. fail if no retries left + // 2. Check PIN protocol + let pin_protocol = pin_protocol?; + + // 3. + 4. Check invalid parameters + if parameters.permissions.is_some() || parameters.rp_id.is_some() { + return Err(Error::InvalidParameter); + } + + // 5. Check PIN retries self.state.pin_blocked()?; - // 3. generate shared secret + // 6. Obtain shared secret let shared_secret = self - .state - .runtime - .generate_shared_secret(&mut self.trussed, platform_kek)?; + .pin_protocol(pin_protocol) + .shared_secret(key_agreement)?; - // 4. decrement retires + // 7. Request user consent using display -- skipped + + // 8. Decrement PIN retries self.state.decrement_retries(&mut self.trussed)?; - // 5. decrypt and verify pinHashEnc - self.decrypt_pin_hash_and_maybe_escalate(shared_secret, pin_hash_enc)?; + // 9. Check PIN + self.decrypt_pin_hash_and_maybe_escalate( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; - // 6. reset retries + // 10. Reset PIN retries self.state.reset_retries(&mut self.trussed)?; - // 7. return encrypted pinToken - let pin_token = self.state.runtime.pin_token(&mut self.trussed); - debug_now!("wrapping pin token"); - // info_now!("exists? {}", syscall!(self.trussed.exists(shared_secret)).exists); - let pin_token_enc = - syscall!(self.trussed.wrap_key_aes256cbc(shared_secret, pin_token)).wrapped_key; + // 11. Check forcePINChange -- skipped + + // 12. Reset all PIN tokens + // 13. Call beginUsingPinUvAuthToken + let mut pin_protocol = self.pin_protocol(pin_protocol); + let mut pin_token = pin_protocol.reset_and_begin_using_pin_token(false); + + // 14. Assign the default permissions + let mut permissions = Permissions::empty(); + permissions.insert(Permissions::MAKE_CREDENTIAL); + permissions.insert(Permissions::GET_ASSERTION); + pin_token.restrict(permissions, None); - syscall!(self.trussed.delete(shared_secret)); + // 15. Return PIN token + response.pin_token = Some(pin_token.encrypt(&shared_secret)?); - // ble... - if pin_token_enc.len() != 16 { - return Err(Error::Other); + shared_secret.delete(&mut self.trussed); + } + + // § 6.5.5.7.2 No 4 + Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { + debug_now!("CTAP2.Pin.GetPinUvAuthTokenUsingPinWithPermissions"); + + // 1. Check mandatory parameters + let key_agreement = parameters + .key_agreement + .as_ref() + .ok_or(Error::MissingParameter)?; + let pin_hash_enc = parameters + .pin_hash_enc + .as_ref() + .ok_or(Error::MissingParameter)?; + let permissions = parameters.permissions.ok_or(Error::MissingParameter)?; + + // 2. Check PIN protocol + let pin_protocol = pin_protocol?; + + // 3. Check that permissions are not empty + let permissions = Permissions::from_bits_truncate(permissions); + if permissions.is_empty() { + return Err(Error::InvalidParameter); } - let pin_token_enc_32 = Bytes::from_slice(&pin_token_enc).unwrap(); - ctap2::client_pin::Response { - key_agreement: None, - pin_token: Some(pin_token_enc_32), - retries: None, + // 4. Check that all requested permissions are supported + let mut unauthorized_permissions = Permissions::empty(); + unauthorized_permissions.insert(Permissions::BIO_ENROLLMENT); + if !self.config.supports_large_blobs() { + unauthorized_permissions.insert(Permissions::LARGE_BLOB_WRITE); + } + unauthorized_permissions.insert(Permissions::AUTHENTICATOR_CONFIGURATION); + if permissions.intersects(unauthorized_permissions) { + return Err(Error::UnauthorizedPermission); } + + // 5. Check PIN retries + self.state.pin_blocked()?; + + // 6. Obtain shared secret + let shared_secret = self + .pin_protocol(pin_protocol) + .shared_secret(key_agreement)?; + + // 7. Request user consent using display -- skipped + + // 8. Decrement PIN retries + self.state.decrement_retries(&mut self.trussed)?; + + // 9. Check PIN + self.decrypt_pin_hash_and_maybe_escalate( + pin_protocol, + &shared_secret, + pin_hash_enc, + )?; + + // 10. Reset PIN retries + self.state.reset_retries(&mut self.trussed)?; + + // 11. Check forcePINChange -- skipped + + // 12. Reset all PIN tokens + // 13. Call beginUsingPinUvAuthToken + let mut pin_protocol = self.pin_protocol(pin_protocol); + let mut pin_token = pin_protocol.reset_and_begin_using_pin_token(false); + + // 14. Assign the requested permissions + // 15. Assign the requested RP id + let rp_id = parameters + .rp_id + .map(TryInto::try_into) + .transpose() + .map_err(|_| Error::InvalidParameter)?; + pin_token.restrict(permissions, rp_id); + + // 16. Return PIN token + response.pin_token = Some(pin_token.encrypt(&shared_secret)?); + + shared_secret.delete(&mut self.trussed); } - Subcommand::GetPinUvAuthTokenUsingUvWithPermissions - | Subcommand::GetUVRetries - | Subcommand::GetPinUvAuthTokenUsingPinWithPermissions => { + Subcommand::GetPinUvAuthTokenUsingUvWithPermissions | Subcommand::GetUVRetries => { // todo!("not implemented yet") return Err(Error::InvalidParameter); } - }) + + _ => { + return Err(Error::InvalidParameter); + } + } + + Ok(response) } #[inline(never)] fn credential_management( &mut self, - parameters: &ctap2::credential_management::Request, + parameters: &ctap2::credential_management::Request<'_>, ) -> Result { use credential_management as cm; use ctap2::credential_management::Subcommand; - // TODO: I see "failed pinauth" output, but then still continuation... - self.verify_pin_auth_using_token(parameters)?; + self.verify_credential_management_pin_auth(parameters)?; let mut cred_mgmt = cm::CredentialManagement::new(self); let sub_parameters = ¶meters.sub_command_params; + // TODO: use custom enum of known commands match parameters.sub_command { // 0x1 Subcommand::GetCredsMetadata => cred_mgmt.get_creds_metadata(), @@ -869,6 +876,23 @@ impl Authenticator for crate::Authenti .ok_or(Error::MissingParameter)?, ) } + + // 0x7 + Subcommand::UpdateUserInformation => { + let sub_parameters = sub_parameters.as_ref().ok_or(Error::MissingParameter)?; + let credential_id = sub_parameters + .credential_id + .as_ref() + .ok_or(Error::MissingParameter)?; + let user = sub_parameters + .user + .as_ref() + .ok_or(Error::MissingParameter)?; + + cred_mgmt.update_user_information(credential_id, user) + } + + _ => Err(Error::InvalidParameter), } } @@ -895,9 +919,11 @@ impl Authenticator for crate::Authenti // 1-4. let uv_performed = match self.pin_prechecks( ¶meters.options, - ¶meters.pin_auth, - ¶meters.pin_protocol, + parameters.pin_auth.map(AsRef::as_ref), + parameters.pin_protocol, parameters.client_data_hash.as_ref(), + Permissions::GET_ASSERTION, + parameters.rp_id, ) { Ok(b) => b, Err(Error::PinRequired) => { @@ -913,7 +939,7 @@ impl Authenticator for crate::Authenti // If no allowList is passed, credential is None and the retrieved credentials // are stored in state.runtime.credential_heap let (credential, num_credentials) = self - .prepare_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed) + .prepare_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed)? .ok_or(Error::NoCredentials)?; info_now!("found {:?} applicable credentials", num_credentials); @@ -932,8 +958,8 @@ impl Authenticator for crate::Authenti } // UP occurs by default, but option could specify not to. - let do_up = if parameters.options.is_some() { - parameters.options.as_ref().unwrap().up.unwrap_or(true) + let do_up = if let Some(options) = parameters.options.as_ref() { + options.up.unwrap_or(true) } else { true }; @@ -960,13 +986,14 @@ impl Authenticator for crate::Authenti }, client_data_hash: { let mut buf = [0u8; 32]; - buf.copy_from_slice(¶meters.client_data_hash); + buf.copy_from_slice(parameters.client_data_hash); buf }, uv_performed, up_performed, multiple_credentials, extensions: parameters.extensions.clone(), + attestation_formats_preference: parameters.attestation_formats_preference.clone(), }); let num_credentials = match num_credentials { @@ -976,10 +1003,52 @@ impl Authenticator for crate::Authenti self.assert_with_credential(num_credentials, credential) } + + #[inline(never)] + fn large_blobs( + &mut self, + request: &ctap2::large_blobs::Request, + ) -> Result { + let Some(config) = self.config.large_blobs else { + return Err(Error::InvalidCommand); + }; + + // 1. offset is validated by serde + + // 2.-3. Exactly one of get or set must be present + match (request.get, request.set) { + (None, None) | (Some(_), Some(_)) => Err(Error::InvalidParameter), + // 4. Implement get subcommand + (Some(get), None) => self.large_blobs_get(request, config, get), + // 5. Implement set subcommand + (None, Some(set)) => self.large_blobs_set(request, config, set), + } + } } // impl Authenticator for crate::Authenticator impl crate::Authenticator { + fn parse_pin_protocol(&self, version: impl TryInto) -> Result { + if let Ok(version) = version.try_into() { + for pin_protocol in self.pin_protocols() { + if u8::from(*pin_protocol) == version { + return Ok(*pin_protocol); + } + } + } + Err(Error::InvalidParameter) + } + + // This is the single source of truth for the supported PIN protocols. + fn pin_protocols(&self) -> &'static [PinProtocolVersion] { + &[PinProtocolVersion::V2, PinProtocolVersion::V1] + } + + fn pin_protocol(&mut self, pin_protocol: PinProtocolVersion) -> PinProtocol<'_, T> { + let state = self.state.runtime.pin_protocol(&mut self.trussed); + PinProtocol::new(&mut self.trussed, state, pin_protocol) + } + #[inline(never)] fn check_credential_applicable( &mut self, @@ -987,14 +1056,14 @@ impl crate::Authenticator { allowlist_passed: bool, uv_performed: bool, ) -> bool { - if !self.check_key_exists(credential.algorithm, &credential.key) { + if !self.check_key_exists(credential.algorithm(), credential.key()) { return false; } if !{ use credential::CredentialProtectionPolicy as Policy; - debug_now!("CredentialProtectionPolicy {:?}", &credential.cred_protect); - match credential.cred_protect { + debug_now!("CredentialProtectionPolicy {:?}", credential.cred_protect()); + match credential.cred_protect() { None | Some(Policy::Optional) => true, Some(Policy::OptionalWithCredentialIdList) => allowlist_passed || uv_performed, Some(Policy::Required) => uv_performed, @@ -1008,10 +1077,10 @@ impl crate::Authenticator { #[inline(never)] fn prepare_credentials( &mut self, - rp_id_hash: &Bytes<32>, + rp_id_hash: &[u8; 32], allow_list: &Option, uv_performed: bool, - ) -> Option<(Credential, u32)> { + ) -> Result> { debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); self.state.runtime.clear_credential_cache(); @@ -1045,57 +1114,86 @@ impl crate::Authenticator { continue; } - return Some((credential, 1)); + return Ok(Some((credential, 1))); } // we don't recognize any credentials in the allowlist - return None; + return Ok(None); } } // we are only dealing with discoverable credentials. debug_now!("Allowlist not passed, fetching RKs"); + self.prepare_cache(rp_id_hash, uv_performed)?; - let mut maybe_path = - syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_rk_dir(rp_id_hash), None,)) - .entry - .map(|entry| PathBuf::try_from(entry.path()).unwrap()); + let num_credentials = self.state.runtime.remaining_credentials(); + let credential = self.state.runtime.pop_credential(&mut self.trussed); + Ok(credential.map(|credential| (Credential::Full(credential), num_credentials))) + } + /// Populate the cache with the RP credentials. + #[inline(never)] + fn prepare_cache(&mut self, rp_id_hash: &[u8; 32], uv_performed: bool) -> Result<()> { use crate::state::CachedCredential; use core::str::FromStr; - while let Some(path) = maybe_path { - let credential_data = - syscall!(self.trussed.read_file(Location::Internal, path.clone(),)).data; + let file_name_prefix = rp_file_name_prefix(rp_id_hash); + let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(file_name_prefix.clone()) + )) + .entry; + + while let Some(entry) = maybe_entry.take() { + if !entry + .file_name() + .as_ref() + .starts_with(file_name_prefix.as_ref()) + { + // We got past all credentials for the relevant RP + break; + } + + if entry.file_name() == &*file_name_prefix { + debug_assert!(entry.metadata().is_dir()); + error!("Migration missing"); + return Err(Error::Other); + } + + let credential_data = syscall!(self + .trussed + .read_file(Location::Internal, entry.path().into(),)) + .data; - let credential = Credential::deserialize(&credential_data).ok()?; + let credential = FullCredential::deserialize(&credential_data).map_err(|_err| { + error!("Failed to deserialize credential: {_err:?}"); + Error::Other + })?; + let timestamp = credential.creation_time; + let credential = Credential::Full(credential); if self.check_credential_applicable(&credential, false, uv_performed) { self.state.runtime.push_credential(CachedCredential { - timestamp: credential.creation_time, - path: String::from_str(path.as_str_ref_with_trailing_nul()).ok()?, + timestamp, + path: String::from_str(entry.path().as_str_ref_with_trailing_nul()) + .map_err(|_| Error::Other)?, }); } - maybe_path = syscall!(self.trussed.read_dir_next()) - .entry - .map(|entry| PathBuf::try_from(entry.path()).unwrap()); + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; } - - let num_credentials = self.state.runtime.remaining_credentials(); - let credential = self.state.runtime.pop_credential(&mut self.trussed); - credential.map(|credential| (credential, num_credentials)) + Ok(()) } fn decrypt_pin_hash_and_maybe_escalate( &mut self, - shared_secret: KeyId, - pin_hash_enc: &Bytes<64>, + pin_protocol: PinProtocolVersion, + shared_secret: &SharedSecret, + pin_hash_enc: &[u8], ) -> Result<()> { - let pin_hash = syscall!(self.trussed.decrypt_aes256cbc(shared_secret, pin_hash_enc)) - .plaintext + let pin_hash = shared_secret + .decrypt(&mut self.trussed, pin_hash_enc) .ok_or(Error::Other)?; let stored_pin_hash = match self.state.persistent.pin_hash() { @@ -1107,15 +1205,8 @@ impl crate::Authenticator { if pin_hash != stored_pin_hash { // I) generate new KEK - self.state - .runtime - .rotate_key_agreement_key(&mut self.trussed); - if self.state.persistent.retries() == 0 { - return Err(Error::PinBlocked); - } - if self.state.persistent.pin_blocked() { - return Err(Error::PinAuthBlocked); - } + self.pin_protocol(pin_protocol).regenerate(); + self.state.pin_blocked()?; return Err(Error::PinInvalid); } @@ -1135,7 +1226,7 @@ impl crate::Authenticator { fn decrypt_pin_check_length( &mut self, - shared_secret: KeyId, + shared_secret: &SharedSecret, pin_enc: &[u8], ) -> Result { // pin is expected to be filled with null bytes to length at least 64 @@ -1144,8 +1235,8 @@ impl crate::Authenticator { return Err(Error::PinPolicyViolation); } - let mut pin = syscall!(self.trussed.decrypt_aes256cbc(shared_secret, pin_enc)) - .plaintext + let mut pin = shared_secret + .decrypt(&mut self.trussed, pin_enc) .ok_or(Error::Other)?; // // temp @@ -1163,61 +1254,43 @@ impl crate::Authenticator { Ok(pin) } - // fn verify_pin(&mut self, pin_auth: &Bytes<16>, client_data_hash: &Bytes<32>) -> bool { - fn verify_pin(&mut self, pin_auth: &[u8; 16], data: &[u8]) -> Result<()> { - let key = self.state.runtime.pin_token(&mut self.trussed); - let tag = syscall!(self.trussed.sign_hmacsha256(key, data)).signature; - if pin_auth == &tag[..16] { - Ok(()) - } else { - Err(Error::PinAuthInvalid) - } - } - - fn verify_pin_auth( - &mut self, - shared_secret: KeyId, - data: &[u8], - pin_auth: &Bytes<16>, - ) -> Result<()> { - let expected_pin_auth = - syscall!(self.trussed.sign_hmacsha256(shared_secret, data)).signature; - - if expected_pin_auth[..16] == pin_auth[..] { - Ok(()) - } else { - Err(Error::PinAuthInvalid) - } - } - - // fn verify_pin_auth_using_token(&mut self, data: &[u8], pin_auth: &Bytes<16>) - fn verify_pin_auth_using_token( + fn verify_credential_management_pin_auth( &mut self, parameters: &ctap2::credential_management::Request, ) -> Result<()> { - // info_now!("CM params: {:?}", parameters); use ctap2::credential_management::Subcommand; + let rp_scope = match parameters.sub_command { + Subcommand::EnumerateCredentialsBegin => { + let rp_id_hash = parameters + .sub_command_params + .as_ref() + .and_then(|subparams| subparams.rp_id_hash) + .ok_or(Error::MissingParameter)?; + RpScope::RpIdHash(rp_id_hash) + } + Subcommand::DeleteCredential | Subcommand::UpdateUserInformation => { + // TODO: determine RP ID from credential ID + RpScope::All + } + _ => RpScope::All, + }; match parameters.sub_command { - // are we Haskell yet lol - sub_command @ Subcommand::GetCredsMetadata - | sub_command @ Subcommand::EnumerateRpsBegin - | sub_command @ Subcommand::EnumerateCredentialsBegin - | sub_command @ Subcommand::DeleteCredential => { + Subcommand::GetCredsMetadata + | Subcommand::EnumerateRpsBegin + | Subcommand::EnumerateCredentialsBegin + | Subcommand::DeleteCredential + | Subcommand::UpdateUserInformation => { // check pinProtocol - let pin_protocol = parameters - // .sub_command_params.as_ref().ok_or(Error::MissingParameter)? - .pin_protocol - .ok_or(Error::MissingParameter)?; - if pin_protocol != 1 { - return Err(Error::InvalidParameter); - } + let pin_protocol = parameters.pin_protocol.ok_or(Error::MissingParameter)?; + let pin_protocol = self.parse_pin_protocol(pin_protocol)?; // check pinAuth - let pin_token = self.state.runtime.pin_token(&mut self.trussed); let mut data: Bytes<{ sizes::MAX_CREDENTIAL_ID_LENGTH_PLUS_256 }> = - Bytes::from_slice(&[sub_command as u8]).unwrap(); - let len = 1 + match sub_command { - Subcommand::EnumerateCredentialsBegin | Subcommand::DeleteCredential => { + Bytes::from_slice(&[parameters.sub_command as u8]).unwrap(); + let len = 1 + match parameters.sub_command { + Subcommand::EnumerateCredentialsBegin + | Subcommand::DeleteCredential + | Subcommand::UpdateUserInformation => { data.resize_to_capacity(); // ble, need to reserialize ctap_types::serde::cbor_serialize( @@ -1233,17 +1306,16 @@ impl crate::Authenticator { _ => 0, }; - // info_now!("input to hmacsha256: {:?}", &data[..len]); - let expected_pin_auth = - syscall!(self.trussed.sign_hmacsha256(pin_token, &data[..len],)).signature; - let pin_auth = parameters .pin_auth .as_ref() .ok_or(Error::MissingParameter)?; - if expected_pin_auth[..16] == pin_auth[..] { + let mut pin_protocol = self.pin_protocol(pin_protocol); + if let Ok(pin_token) = pin_protocol.verify_pin_token(&data[..len], pin_auth) { info_now!("passed pinauth"); + pin_token.require_permissions(Permissions::CREDENTIAL_MANAGEMENT)?; + pin_token.require_valid_for_rp(rp_scope)?; Ok(()) } else { info_now!("failed pinauth!"); @@ -1263,6 +1335,8 @@ impl crate::Authenticator { // of already checked CredMgmt subcommands Subcommand::EnumerateRpsGetNextRp | Subcommand::EnumerateCredentialsGetNextCredential => Ok(()), + + _ => Err(Error::InvalidParameter), } } @@ -1270,17 +1344,19 @@ impl crate::Authenticator { fn pin_prechecks( &mut self, options: &Option, - pin_auth: &Option, - pin_protocol: &Option, + pin_auth: Option<&[u8]>, + pin_protocol: Option, data: &[u8], + permissions: Permissions, + rp_id: &str, ) -> Result { // 1. pinAuth zero length -> wait for user touch, then // return PinNotSet if not set, PinInvalid if set // // the idea is for multi-authnr scenario where platform // wants to enforce PIN and needs to figure out which authnrs support PIN - if let Some(pin_auth) = pin_auth.as_ref() { - if pin_auth.len() == 0 { + if let Some(pin_auth) = pin_auth { + if pin_auth.is_empty() { self.up .user_present(&mut self.trussed, constants::FIDO2_UP_TIMEOUT)?; if !self.state.persistent.pin_is_set() { @@ -1292,12 +1368,13 @@ impl crate::Authenticator { } // 2. check PIN protocol is 1 if pinAuth was sent - if let Some(ref _pin_auth) = pin_auth { - if let Some(1) = pin_protocol { - } else { - return Err(Error::PinAuthInvalid); - } - } + let pin_protocol = if pin_auth.is_some() { + let pin_protocol = pin_protocol.ok_or(Error::MissingParameter)?; + let pin_protocol = self.parse_pin_protocol(pin_protocol)?; + Some(pin_protocol) + } else { + None + }; // 3. if no PIN is set (we have no other form of UV), // and platform sent `uv` or `pinAuth`, return InvalidOption @@ -1313,26 +1390,25 @@ impl crate::Authenticator { } // 4. If authenticator is protected by som form of user verification, do it - // - // TODO: Should we should fail if `uv` is passed? - // Current thinking: no + + // Reject uv = true as we do not support built-in user verification + if pin_auth.is_none() && options.as_ref().and_then(|options| options.uv) == Some(true) { + return Err(Error::InvalidOption); + } + if self.state.persistent.pin_is_set() { // let mut uv_performed = false; - if let Some(ref pin_auth) = pin_auth { - if pin_auth.len() != 16 { - return Err(Error::InvalidParameter); - } + if let Some(pin_auth) = pin_auth { // seems a bit redundant to check here in light of 2. // I guess the CTAP spec writers aren't implementers :D - if let Some(1) = pin_protocol { + if let Some(pin_protocol) = pin_protocol { // 5. if pinAuth is present and pinProtocol = 1, verify // success --> set uv = 1 // error --> PinAuthInvalid - self.verify_pin( - // unwrap panic ruled out above - pin_auth.as_slice().try_into().unwrap(), - data, - )?; + let mut pin_protocol = self.pin_protocol(pin_protocol); + let pin_token = pin_protocol.verify_pin_token(data, pin_auth)?; + pin_token.require_permissions(permissions)?; + pin_token.require_valid_for_rp(RpScope::RpId(rp_id))?; return Ok(true); } else { @@ -1340,8 +1416,8 @@ impl crate::Authenticator { return Err(Error::PinAuthInvalid); } } else { - // 6. pinAuth not present + clientPin set --> error PinRequired - if self.state.persistent.pin_is_set() { + // 6. pinAuth not present + clientPin set + rk = true --> error PinRequired + if options.as_ref().and_then(|options| options.rk) == Some(true) { return Err(Error::PinRequired); } } @@ -1358,16 +1434,9 @@ impl crate::Authenticator { Key::WrappedKey(_) => true, Key::ResidentKey(key) => { debug_now!("checking if ResidentKey {:?} exists", key); - match alg { - -7 => syscall!(self.trussed.exists(Mechanism::P256, *key)).exists, - -8 => syscall!(self.trussed.exists(Mechanism::Ed255, *key)).exists, - // -9 => { - // let exists = syscall!(self.trussed.exists(Mechanism::Totp, key)).exists; - // info_now!("found it"); - // exists - // } - _ => false, - } + SigningAlgorithm::try_from(alg) + .map(|alg| syscall!(self.trussed.exists(alg.mechanism(), *key)).exists) + .unwrap_or_default() } } } @@ -1377,14 +1446,20 @@ impl crate::Authenticator { &mut self, get_assertion_state: &state::ActiveGetAssertionData, extensions: &ctap2::get_assertion::ExtensionsInput, - _credential: &Credential, + credential: &Credential, credential_key: KeyId, ) -> Result> { + let mut output = ctap2::get_assertion::ExtensionsOutput::default(); + if let Some(hmac_secret) = &extensions.hmac_secret { - if let Some(pin_protocol) = hmac_secret.pin_protocol { - if pin_protocol != 1 { - return Err(Error::InvalidParameter); - } + let pin_protocol = hmac_secret + .pin_protocol + .map(|i| self.parse_pin_protocol(i)) + .transpose()? + .unwrap_or(PinProtocolVersion::V1); + + if !get_assertion_state.up_performed { + return Err(Error::UnsupportedOption); } // We derive credRandom as an hmac of the existing private key. @@ -1394,35 +1469,29 @@ impl crate::Authenticator { Mechanism::HmacSha256, credential_key, Some(Bytes::from_slice(&[get_assertion_state.uv_performed as u8]).unwrap()), - trussed::types::StorageAttributes::new().set_persistence(Location::Volatile) + StorageAttributes::new().set_persistence(Location::Volatile) )) .key; // Verify the auth tag, which uses the same process as the pinAuth - let kek = self - .state - .runtime - .generate_shared_secret(&mut self.trussed, &hmac_secret.key_agreement)?; - self.verify_pin_auth(kek, &hmac_secret.salt_enc, &hmac_secret.salt_auth) - .map_err(|_| Error::ExtensionFirst)?; - - if hmac_secret.salt_enc.len() != 32 && hmac_secret.salt_enc.len() != 64 { + let mut pin_protocol = self.pin_protocol(pin_protocol); + let shared_secret = pin_protocol.shared_secret(&hmac_secret.key_agreement)?; + pin_protocol.verify_pin_auth( + &shared_secret, + &hmac_secret.salt_enc, + &hmac_secret.salt_auth, + )?; + + // decrypt input salt_enc to get salt1 or (salt1 || salt2) + let salts = shared_secret + .decrypt(&mut self.trussed, &hmac_secret.salt_enc) + .ok_or(Error::InvalidOption)?; + + if salts.len() != 32 && salts.len() != 64 { debug_now!("invalid hmac-secret length"); return Err(Error::InvalidLength); } - // decrypt input salt_enc to get salt1 or (salt1 || salt2) - let salts = syscall!(self.trussed.decrypt( - Mechanism::Aes256Cbc, - kek, - &hmac_secret.salt_enc, - b"", - b"", - b"" - )) - .plaintext - .ok_or(Error::InvalidOption)?; - let mut salt_output: Bytes<64> = Bytes::new(); // output1 = hmac_sha256(credRandom, salt1) @@ -1442,18 +1511,18 @@ impl crate::Authenticator { syscall!(self.trussed.delete(cred_random)); // output_enc = aes256-cbc(sharedSecret, IV=0, output1 || output2) - let output_enc = - syscall!(self - .trussed - .encrypt(Mechanism::Aes256Cbc, kek, &salt_output, b"", None)) - .ciphertext; - - Ok(Some(ctap2::get_assertion::ExtensionsOutput { - hmac_secret: Some(Bytes::from_slice(&output_enc).unwrap()), - })) - } else { - Ok(None) + let output_enc = shared_secret.encrypt(&mut self.trussed, &salt_output); + + shared_secret.delete(&mut self.trussed); + + output.hmac_secret = Some(Bytes::from_slice(&output_enc).unwrap()); } + + if extensions.third_party_payment.unwrap_or_default() { + output.third_party_payment = Some(credential.third_party_payment().unwrap_or_default()); + } + + Ok(output.is_set().then_some(output)) } #[inline(never)] @@ -1463,9 +1532,9 @@ impl crate::Authenticator { credential: Credential, ) -> Result { let data = self.state.runtime.active_get_assertion.clone().unwrap(); - let rp_id_hash = Bytes::from_slice(&data.rp_id_hash).unwrap(); + let rp_id_hash = &data.rp_id_hash; - let (key, is_rk) = match credential.key.clone() { + let (key, is_rk) = match credential.key().clone() { Key::ResidentKey(key) => (key, true), Key::WrappedKey(bytes) => { let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)?; @@ -1489,7 +1558,15 @@ impl crate::Authenticator { }; // 8. process any extensions present + let mut large_blob_key_requested = false; let extensions_output = if let Some(extensions) = &data.extensions { + if self.config.supports_large_blobs() { + if extensions.large_blob_key == Some(false) { + // large_blob_key must be Some(true) or omitted + return Err(Error::InvalidOption); + } + large_blob_key_requested = extensions.large_blob_key == Some(true); + } self.process_assertion_extensions(&data, extensions, &credential, key)? } else { None @@ -1502,7 +1579,7 @@ impl crate::Authenticator { .state .persistent .key_encryption_key(&mut self.trussed)?; - let credential_id = credential.id(&mut self.trussed, kek, Some(&rp_id_hash))?; + let credential_id = credential.id(&mut self.trussed, kek, rp_id_hash)?; use ctap2::AuthenticatorDataFlags as Flags; @@ -1512,7 +1589,7 @@ impl crate::Authenticator { rp_id_hash, flags: { - let mut flags = Flags::EMPTY; + let mut flags = Flags::empty(); if data.up_performed { flags |= Flags::USER_PRESENCE; } @@ -1530,7 +1607,7 @@ impl crate::Authenticator { extensions: extensions_output, }; - let serialized_auth_data = authenticator_data.serialize(); + let serialized_auth_data = authenticator_data.serialize()?; let mut commitment = Bytes::<1024>::new(); commitment @@ -1540,47 +1617,96 @@ impl crate::Authenticator { .extend_from_slice(&data.client_data_hash) .map_err(|_| Error::Other)?; - let (mechanism, serialization) = match credential.algorithm { - -7 => (Mechanism::P256, SignatureSerialization::Asn1Der), - -8 => (Mechanism::Ed255, SignatureSerialization::Raw), - // -9 => (Mechanism::Totp, SignatureSerialization::Raw), - _ => { - return Err(Error::Other); + let signing_algorithm = + SigningAlgorithm::try_from(credential.algorithm()).map_err(|_| Error::Other)?; + let signature = signing_algorithm + .sign(&mut self.trussed, key, &commitment) + .to_bytes() + .unwrap(); + + // select preferred format or skip attestation statement + let att_stmt_fmt = data + .attestation_formats_preference + .as_ref() + .and_then(SupportedAttestationFormat::select); + let att_stmt = if let Some(format) = att_stmt_fmt { + match format { + SupportedAttestationFormat::None => { + Some(AttestationStatement::None(NoneAttestationStatement {})) + } + SupportedAttestationFormat::Packed => { + let (attestation_maybe, _) = self.state.identity.attestation(&mut self.trussed); + let (signature, attestation_algorithm) = { + if let Some(attestation) = attestation_maybe.as_ref() { + let signing_algorithm = SigningAlgorithm::P256; + let signature = signing_algorithm.sign( + &mut self.trussed, + attestation.0, + &commitment, + ); + ( + signature.to_bytes().map_err(|_| Error::Other)?, + signing_algorithm.into(), + ) + } else { + (signature.clone(), credential.algorithm()) + } + }; + let packed = PackedAttestationStatement { + alg: attestation_algorithm, + sig: signature, + x5c: attestation_maybe.as_ref().map(|attestation| { + // See: https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements + let cert = attestation.1.clone(); + let mut x5c = Vec::new(); + x5c.push(cert).ok(); + x5c + }), + }; + Some(AttestationStatement::Packed(packed)) + } } + } else { + None }; - debug_now!("signing with {:?}, {:?}", &mechanism, &serialization); - let signature = syscall!(self - .trussed - .sign(mechanism, key, &commitment, serialization)) - .signature - .to_bytes() - .unwrap(); - if !is_rk { syscall!(self.trussed.delete(key)); } - let mut response = ctap2::get_assertion::Response { - credential: Some(credential_id.into()), - auth_data: Bytes::from_slice(&serialized_auth_data).map_err(|_| Error::Other)?, + let mut response = ctap2::get_assertion::ResponseBuilder { + credential: credential_id.into(), + auth_data: serialized_auth_data, signature, - user: None, - number_of_credentials: num_credentials, - }; + } + .build(); + response.number_of_credentials = num_credentials; + response.att_stmt = att_stmt; // User with empty IDs are ignored for compatibility - if is_rk && !credential.user.id.is_empty() { - let mut user = credential.user.clone(); - // User identifiable information (name, DisplayName, icon) MUST not - // be returned if user verification is not done by the authenticator. - // For single account per RP case, authenticator returns "id" field. - if !data.uv_performed || !data.multiple_credentials { - user.icon = None; - user.name = None; - user.display_name = None; + if is_rk { + if let Credential::Full(credential) = &credential { + if !credential.user.id().is_empty() { + let mut user: PublicKeyCredentialUserEntity = credential.user.clone().into(); + // User identifiable information (name, DisplayName, icon) MUST not + // be returned if user verification is not done by the authenticator. + // For single account per RP case, authenticator returns "id" field. + if !data.uv_performed || !data.multiple_credentials { + user.icon = None; + user.name = None; + user.display_name = None; + } + response.user = Some(user); + } + } + + if large_blob_key_requested { + debug!("Sending largeBlobKey in getAssertion"); + response.large_blob_key = match credential { + Credential::Stripped(stripped) => stripped.large_blob_key, + Credential::Full(full) => full.data.large_blob_key, + }; } - response.user = Some(user); } Ok(response) @@ -1589,32 +1715,44 @@ impl crate::Authenticator { #[inline(never)] fn delete_resident_key_by_user_id( &mut self, - rp_id_hash: &Bytes<32>, + rp_id_hash: &[u8; 32], user_id: &Bytes<64>, ) -> Result<()> { // Prepare to iterate over all credentials associated to RP. - let rp_path = rp_rk_dir(rp_id_hash); - let mut entry = syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_path, None,)) + let file_name_prefix = rp_file_name_prefix(rp_id_hash); + let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(file_name_prefix.clone()) + )) .entry; - loop { + while let Some(entry) = maybe_entry.take() { + if !entry + .file_name() + .as_ref() + .starts_with(file_name_prefix.as_ref()) + { + // We got past all credentials for the relevant RP + break; + } + + if entry.file_name() == &*file_name_prefix { + debug_assert!(entry.metadata().is_dir()); + error!("Migration missing"); + return Err(Error::Other); + } + info_now!("this may be an RK: {:?}", &entry); - let rk_path = match entry { - // no more RKs left - // break breaks inner loop here - None => break, - Some(entry) => PathBuf::from(entry.path()), - }; + let rk_path = PathBuf::from(entry.path()); info_now!("checking RK {:?} for userId ", &rk_path); let credential_data = syscall!(self.trussed.read_file(Location::Internal, rk_path.clone(),)).data; - let credential_maybe = Credential::deserialize(&credential_data); + let credential_maybe = FullCredential::deserialize(&credential_data); if let Ok(old_credential) = credential_maybe { - if old_credential.user.id == user_id { + if old_credential.user.id() == user_id { match old_credential.key { credential::Key::ResidentKey(key) => { info_now!(":: deleting resident key"); @@ -1634,7 +1772,7 @@ impl crate::Authenticator { } // prepare for next loop iteration - entry = syscall!(self.trussed.read_dir_next()).entry; + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; } Ok(()) @@ -1647,7 +1785,7 @@ impl crate::Authenticator { .trussed .read_file(Location::Internal, PathBuf::from(rk_path),)) .data; - let credential_maybe = Credential::deserialize(&credential_data); + let credential_maybe = FullCredential::deserialize(&credential_data); // info_now!("deleting credential {:?}", &credential); if let Ok(credential) = credential_maybe { @@ -1671,25 +1809,277 @@ impl crate::Authenticator { Ok(()) } + + fn large_blobs_get( + &mut self, + request: &ctap2::large_blobs::Request, + config: large_blobs::Config, + length: u32, + ) -> Result { + debug!( + "large_blobs_get: length = {length}, offset = {}", + request.offset + ); + // 1.-2. Validate parameters + if request.length.is_some() + || request.pin_uv_auth_param.is_some() + || request.pin_uv_auth_protocol.is_some() + { + error!("length/pin set"); + return Err(Error::InvalidParameter); + } + // 3. Validate length + let Ok(length) = usize::try_from(length) else { + return Err(Error::InvalidLength); + }; + if length > self.config.max_msg_size.saturating_sub(64) { + return Err(Error::InvalidLength); + } + // 4. Validate offset + let Ok(offset) = usize::try_from(request.offset) else { + error!("offset too large"); + return Err(Error::InvalidParameter); + }; + let stored_length = large_blobs::size(&mut self.trussed, config.location)?; + if offset > stored_length { + error!("offset: {offset}, stored_length: {stored_length}"); + return Err(Error::InvalidParameter); + }; + // 5. Return requested data + info!("Reading large-blob array from offset {offset}"); + let data = large_blobs::read_chunk(&mut self.trussed, config.location, offset, length)?; + let mut response = ctap2::large_blobs::Response::default(); + response.config = Some(data); + Ok(response) + } + + fn large_blobs_set( + &mut self, + request: &ctap2::large_blobs::Request, + config: large_blobs::Config, + data: &[u8], + ) -> Result { + debug!( + "large_blobs_set: |data| = {}, offset = {}, length = {:?}", + data.len(), + request.offset, + request.length + ); + // 1. Validate data + if data.len() > self.config.max_msg_size.saturating_sub(64) { + return Err(Error::InvalidLength); + } + if request.offset == 0 { + // 2. Calculate expected length and offset + // 2.1. Require length + let Some(length) = request.length else { + return Err(Error::InvalidParameter); + }; + // 2.2. Check that length is not too big + let Ok(length) = usize::try_from(length) else { + return Err(Error::LargeBlobStorageFull); + }; + if length > config.max_size() { + return Err(Error::LargeBlobStorageFull); + } + // 2.3. Check that length is not too small + if length < large_blobs::MIN_SIZE { + return Err(Error::InvalidParameter); + } + // 2.4-5. Set expected length and offset + self.state.runtime.large_blobs.expected_length = length; + self.state.runtime.large_blobs.expected_next_offset = 0; + } else { + // 3. Validate parameters + if request.length.is_some() { + return Err(Error::InvalidParameter); + } + } + + // 4. Validate offset + let Ok(offset) = usize::try_from(request.offset) else { + return Err(Error::InvalidSeq); + }; + if offset != self.state.runtime.large_blobs.expected_next_offset { + return Err(Error::InvalidSeq); + } + + // 5. Perform uv + // TODO: support alwaysUv + if self.state.persistent.pin_is_set() { + let Some(pin_uv_auth_param) = request.pin_uv_auth_param else { + return Err(Error::PinRequired); + }; + let Some(pin_uv_auth_protocol) = request.pin_uv_auth_protocol else { + return Err(Error::PinRequired); + }; + if pin_uv_auth_protocol != 1 { + return Err(Error::PinAuthInvalid); + } + let pin_protocol = self.parse_pin_protocol(pin_uv_auth_protocol)?; + // TODO: check pinUvAuthToken + let pin_auth: [u8; 16] = pin_uv_auth_param + .as_ref() + .try_into() + .map_err(|_| Error::PinAuthInvalid)?; + + let mut auth_data: Bytes<70> = Bytes::new(); + // 32x 0xff + auth_data.resize(32, 0xff).unwrap(); + // h'0c00' + auth_data.push(0x0c).unwrap(); + auth_data.push(0x00).unwrap(); + // uint32LittleEndian(offset) + auth_data + .extend_from_slice(&request.offset.to_le_bytes()) + .unwrap(); + // SHA-256(data) + auth_data.extend_from_slice(&Sha256::digest(data)).unwrap(); + + let mut pin_protocol = self.pin_protocol(pin_protocol); + let pin_token = pin_protocol.verify_pin_token(&pin_auth, &auth_data)?; + pin_token.require_permissions(Permissions::LARGE_BLOB_WRITE)?; + } + + // 6. Validate data length + if offset + data.len() > self.state.runtime.large_blobs.expected_length { + return Err(Error::InvalidParameter); + } + + // 7.-11. Write the buffer + info!("Writing large-blob array to offset {offset}"); + large_blobs::write_chunk( + &mut self.trussed, + &mut self.state.runtime.large_blobs, + config.location, + data, + )?; + + Ok(ctap2::large_blobs::Response::default()) + } } -fn rp_rk_dir(rp_id_hash: &Bytes<32>) -> PathBuf { - // uses only first 8 bytes of hash, which should be "good enough" - let mut hex = [b'0'; 16]; - format_hex(&rp_id_hash[..8], &mut hex); +#[derive(Clone, Copy, Debug)] +enum SupportedAttestationFormat { + None, + Packed, +} - let mut dir = PathBuf::from(b"rk"); - dir.push(&PathBuf::from(&hex)); +impl SupportedAttestationFormat { + fn select(preference: &AttestationFormatsPreference) -> Option { + if preference.known_formats() == [AttestationStatementFormat::None] + && !preference.includes_unknown_formats() + { + // platform requested only None --> omit attestation statement + return None; + } + // use first known and supported format, or default to packed format + let format = preference + .known_formats() + .iter() + .copied() + .flat_map(Self::try_from) + .next() + .unwrap_or(Self::Packed); + Some(format) + } +} - dir +impl From for AttestationStatementFormat { + fn from(format: SupportedAttestationFormat) -> Self { + match format { + SupportedAttestationFormat::None => Self::None, + SupportedAttestationFormat::Packed => Self::Packed, + } + } } -fn rk_path(rp_id_hash: &Bytes<32>, credential_id_hash: &Bytes<32>) -> PathBuf { - let mut path = rp_rk_dir(rp_id_hash); +impl TryFrom for SupportedAttestationFormat { + type Error = Error; - let mut hex = [0u8; 16]; - format_hex(&credential_id_hash[..8], &mut hex); - path.push(&PathBuf::from(&hex)); + fn try_from(format: AttestationStatementFormat) -> core::result::Result { + match format { + AttestationStatementFormat::None => Ok(Self::None), + AttestationStatementFormat::Packed => Ok(Self::Packed), + _ => Err(Error::Other), + } + } +} +// The new path scheme for disvoerable credentials (= resident keys) is: +// rk/. +// The hashes are truncated to the first eight bytes and formatted as hex strings. +// We use the following terms for the components: +// rk_path: rk/. +// rp_file_name_prefix: + +fn rp_file_name_prefix(rp_id_hash: &[u8; 32]) -> PathBuf { + let mut hex = [b'0'; 16]; + super::format_hex(&rp_id_hash[..8], &mut hex); + PathBuf::try_from(&hex).unwrap() +} + +fn rk_path(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32]) -> PathBuf { + // 16 bytes per hash + dot + trailing zero = 34 + let mut buf = [0; 34]; + buf[16] = b'.'; + format_hex(&rp_id_hash[..8], &mut buf[..16]); + format_hex(&credential_id_hash[..8], &mut buf[17..33]); + + let mut path = PathBuf::from(RK_DIR); + path.push(Path::from_bytes_with_nul(&buf).unwrap()); path } + +#[cfg(test)] +mod tests { + use super::{rk_path, rp_file_name_prefix}; + + const TEST_HASH: &[u8; 32] = &[ + 134, 54, 157, 96, 10, 28, 233, 79, 219, 59, 195, 125, 165, 251, 120, 14, 49, 152, 212, 191, + 114, 137, 180, 207, 255, 177, 187, 106, 173, 1, 203, 171, + ]; + const TEST_HASH_HEX: &str = "86369d600a1ce94f"; + + #[test] + fn test_rp_file_name_prefix() { + assert_eq!(rp_file_name_prefix(&[0; 32]).as_str(), "0000000000000000"); + assert_eq!(rp_file_name_prefix(TEST_HASH).as_str(), TEST_HASH_HEX); + } + + #[test] + fn test_rk_path() { + fn test(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32], expected: &str) { + println!("rp_id_hash: {rp_id_hash:?}"); + println!("credential_id_hash: {credential_id_hash:?}"); + let actual = rk_path(rp_id_hash, credential_id_hash); + assert_eq!(actual.as_str(), expected); + } + + let input_zero = &[0; 32]; + let output_zero = "0000000000000000"; + let input_nonzero = TEST_HASH; + let output_nonzero = TEST_HASH_HEX; + + test( + input_zero, + input_zero, + &format!("rk/{output_zero}.{output_zero}"), + ); + test( + input_zero, + input_nonzero, + &format!("rk/{output_zero}.{output_nonzero}"), + ); + test( + input_nonzero, + input_zero, + &format!("rk/{output_nonzero}.{output_zero}"), + ); + test( + input_nonzero, + input_nonzero, + &format!("rk/{output_nonzero}.{output_nonzero}"), + ); + } +} diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index d8bc26f..1218766 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -1,24 +1,24 @@ //! TODO: T -use core::convert::TryFrom; +use core::{cmp, convert::TryFrom, num::NonZeroU32}; -use trussed::{ - syscall, +use littlefs2_core::{Path, PathBuf}; +use trussed_core::{ + syscall, try_syscall, types::{DirEntry, Location}, }; +use cosey::PublicKey; use ctap_types::{ - cose::PublicKey, ctap2::credential_management::{CredentialProtectionPolicy, Response}, - heapless_bytes::Bytes, - webauthn::PublicKeyCredentialDescriptor, - Error, + webauthn::{PublicKeyCredentialDescriptorRef, PublicKeyCredentialUserEntity}, + ByteArray, Error, }; -use littlefs2::path::{Path, PathBuf}; - +use super::RK_DIR; use crate::{ - credential::Credential, + constants::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE, + credential::FullCredential, state::{CredentialManagementEnumerateCredentials, CredentialManagementEnumerateRps}, Authenticator, Result, TrussedRequirements, UserPresence, }; @@ -58,6 +58,18 @@ where } } +/// Get the hex hashed ID of the RP from the filename of a RP directory OR a "new" RK path +fn get_rp_id_hex(entry: &DirEntry) -> &str { + get_rp_id_hex_from_file_name(entry.file_name().as_str()) +} + +fn get_rp_id_hex_from_file_name(file_name: &str) -> &str { + file_name + .split('.') + .next() + .expect("Split always returns at least one empty string") +} + impl CredentialManagement<'_, UP, T> where UP: UserPresence, @@ -67,130 +79,110 @@ where info!("get metadata"); let mut response: Response = Default::default(); - let guesstimate = self.state.persistent.max_resident_credentials_guesstimate(); - response.existing_resident_credentials_count = Some(0); - response.max_possible_remaining_residential_credentials_count = Some(guesstimate); + let credential_count = self.count_credentials()?; + // We have a fixed limit determined by the configuration and an estimated limit determined + // by the available space on the filesystem. The effective limit is the lower of the two. + let max_remaining = self + .config + .max_resident_credential_count + .unwrap_or(MAX_RESIDENT_CREDENTIALS_GUESSTIMATE) + .saturating_sub(credential_count); + let estimate_remaining = self.estimate_remaining().unwrap_or(u32::MAX); - let dir = PathBuf::from(b"rk"); - let maybe_first_rp = - syscall!(self - .trussed - .read_dir_first(Location::Internal, dir.clone(), None)) - .entry; + response.existing_resident_credentials_count = Some(credential_count); + response.max_possible_remaining_residential_credentials_count = + Some(cmp::min(max_remaining, estimate_remaining)); - let first_rp = match maybe_first_rp { - None => return Ok(response), - Some(rp) => rp, - }; + Ok(response) + } - let (mut num_rks, _) = self.count_rp_rks(PathBuf::from(first_rp.path()))?; - let mut last_rp = PathBuf::from(first_rp.file_name()); + pub fn count_credentials(&mut self) -> Result { + let dir = PathBuf::from(RK_DIR); + let mut num_rks = 0; - loop { + let mut maybe_next = syscall!(self .trussed - .read_dir_first(Location::Internal, dir.clone(), Some(last_rp),)) - .entry - .unwrap(); - let maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; - - match maybe_next_rp { - None => { - response.existing_resident_credentials_count = Some(num_rks); - response.max_possible_remaining_residential_credentials_count = - Some(if num_rks >= guesstimate { - 0 - } else { - guesstimate - num_rks - }); - return Ok(response); - } - Some(rp) => { - last_rp = PathBuf::from(rp.file_name()); - info!("counting.."); - let (this_rp_rk_count, _) = self.count_rp_rks(PathBuf::from(rp.path()))?; - info!("{:?}", this_rp_rk_count); - num_rks += this_rp_rk_count; - } + .read_dir_first(Location::Internal, dir.clone(), None)) + .entry; + + while let Some(rp) = maybe_next { + if rp.metadata().is_dir() { + error!("Migration not complete"); + return Err(Error::Other); } + + num_rks += 1; + maybe_next = syscall!(self.trussed.read_dir_next()).entry; } + + Ok(num_rks) } pub fn first_relying_party(&mut self) -> Result { info!("first rp"); - // rp (0x03): PublicKeyCredentialRpEntity - // rpIDHash (0x04) : RP ID SHA-256 hash. - // totalRPs (0x05) : Total number of RPs present on the authenticator. - - let mut response: Response = Default::default(); - - let dir = PathBuf::from(b"rk"); + let mut response = Response::default(); + let dir = PathBuf::from(RK_DIR); let maybe_first_rp = - syscall!(self.trussed.read_dir_first(Location::Internal, dir, None)).entry; - - response.total_rps = Some(match maybe_first_rp { - None => 0, - _ => { - let mut num_rps = 1; - loop { - let maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; - match maybe_next_rp { - None => break, - _ => num_rps += 1, - } - } - num_rps - } - }); - - if let Some(rp) = maybe_first_rp { - // load credential and extract rp and rpIdHash - let maybe_first_credential = syscall!(self.trussed.read_dir_first( - Location::Internal, - PathBuf::from(rp.path()), - None - )) + syscall!(self + .trussed + .read_dir_first(Location::Internal, dir.clone(), None)) .entry; - match maybe_first_credential { - None => panic!("chaos! disorder!"), - Some(rk_entry) => { - let serialized = syscall!(self - .trussed - .read_file(Location::Internal, rk_entry.path().into(),)) - .data; + let Some(first_rp) = maybe_first_rp else { + response.total_rps = Some(0); + return Ok(response); + }; - let credential = Credential::deserialize(&serialized) - // this may be a confusing error message - .map_err(|_| Error::InvalidCredential)?; + // The first one counts + let mut total_rps = 1; - let rp = credential.data.rp; + if first_rp.metadata().is_dir() { + warn!("Migration did not finish"); + return Err(Error::Other); + } - response.rp_id_hash = Some(self.hash(rp.id.as_ref())); - response.rp = Some(rp); - } + let first_credential_data = syscall!(self + .trussed + .read_file(Location::Internal, first_rp.path().into())) + .data; + + let credential = FullCredential::deserialize(&first_credential_data)?; + let rp_id_hash: [u8; 32] = syscall!(self.trussed.hash_sha256(credential.rp.id().as_ref())) + .hash + .as_slice() + .try_into() + .map_err(|_| Error::Other)?; + + let mut current_rp = first_rp; + + let mut current_id_hex = get_rp_id_hex(¤t_rp); + + while let Some(entry) = syscall!(self.trussed.read_dir_next()).entry { + let id_hex = get_rp_id_hex(&entry); + if id_hex != current_id_hex { + total_rps += 1; + current_rp = entry; + current_id_hex = get_rp_id_hex(¤t_rp) } + } - // cache state for next call - if let Some(total_rps) = response.total_rps { - if total_rps > 1 { - let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); - self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { - remaining: total_rps - 1, - rp_id_hash, - }); - } - } + if let Some(remaining) = NonZeroU32::new(total_rps - 1) { + self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { + remaining, + rp_id_hash, + }); } + response.total_rps = Some(total_rps); + response.rp_id_hash = Some(ByteArray::new(rp_id_hash)); + response.rp = Some(credential.data.rp.into()); Ok(response) } pub fn next_relying_party(&mut self) -> Result { - info!("next rp"); - let CredentialManagementEnumerateRps { remaining, rp_id_hash: last_rp_id_hash, @@ -201,114 +193,123 @@ where .clone() .ok_or(Error::NotAllowed)?; - let dir = PathBuf::from(b"rk"); + let filename = super::rp_file_name_prefix(&last_rp_id_hash); - let mut hex = [b'0'; 16]; - super::format_hex(&last_rp_id_hash[..8], &mut hex); - let filename = PathBuf::from(&hex); + let dir = PathBuf::from(RK_DIR); - let mut maybe_next_rp = - syscall!(self - .trussed - .read_dir_first(Location::Internal, dir, Some(filename),)) - .entry; + let maybe_next_rp = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + dir, + Some(filename.clone()) + )) + .entry; + + let mut response = Response::default(); - // Advance to the next - if maybe_next_rp.is_some() { - maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; - } else { + let Some(current_rp) = maybe_next_rp else { return Err(Error::NotAllowed); - } + }; - let mut response: Response = Default::default(); + let current_id_hex = get_rp_id_hex(¤t_rp); - if let Some(rp) = maybe_next_rp { - // load credential and extract rp and rpIdHash - let maybe_first_credential = syscall!(self.trussed.read_dir_first( - Location::Internal, - PathBuf::from(rp.path()), - None - )) - .entry; + debug_assert!(current_rp + .file_name() + .as_str() + .starts_with(filename.as_str())); - match maybe_first_credential { - None => panic!("chaos! disorder!"), - Some(rk_entry) => { - let serialized = syscall!(self - .trussed - .read_file(Location::Internal, rk_entry.path().into(),)) - .data; - - let credential = Credential::deserialize(&serialized) - // this may be a confusing error message - .map_err(|_| Error::InvalidCredential)?; - - let rp = credential.data.rp; - - response.rp_id_hash = Some(self.hash(rp.id.as_ref())); - response.rp = Some(rp); - - // cache state for next call - if remaining > 1 { - let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); - self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { - remaining: remaining - 1, - rp_id_hash, - }); - } else { - self.state.runtime.cached_rp = None; - } - } + while let Some(entry) = syscall!(self.trussed.read_dir_next()).entry { + let id_hex = get_rp_id_hex(&entry); + if id_hex == current_id_hex { + continue; } - } else { - self.state.runtime.cached_rp = None; - } - Ok(response) - } + if entry.metadata().is_dir() { + warn!("While iterating: migration is not finished"); + return Err(Error::Other); + } - fn count_rp_rks(&mut self, rp_dir: PathBuf) -> Result<(u32, DirEntry)> { - let maybe_first_rk = - syscall!(self + let data = syscall!(self .trussed - .read_dir_first(Location::Internal, rp_dir, None)) - .entry; - - let first_rk = maybe_first_rk.ok_or(Error::NoCredentials)?; + .read_file(Location::Internal, entry.path().into())) + .data; + + let credential = FullCredential::deserialize(&data)?; + let rp_id_hash: [u8; 32] = + syscall!(self.trussed.hash_sha256(credential.rp.id().as_ref())) + .hash + .as_slice() + .try_into() + .map_err(|_| Error::Other)?; + response.rp_id_hash = Some(ByteArray::new(rp_id_hash)); + response.rp = Some(credential.data.rp.into()); + + if let Some(new_remaining) = NonZeroU32::new(remaining.get() - 1) { + self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { + remaining: new_remaining, + rp_id_hash, + }); + } - // count the rest of them - let mut num_rks = 1; - while syscall!(self.trussed.read_dir_next()).entry.is_some() { - num_rks += 1; + return Ok(response); } - Ok((num_rks, first_rk)) + + Err(Error::NotAllowed) } - pub fn first_credential(&mut self, rp_id_hash: &Bytes<32>) -> Result { + pub fn first_credential(&mut self, rp_id_hash: &[u8; 32]) -> Result { info!("first credential"); self.state.runtime.cached_rk = None; - let mut hex = [b'0'; 16]; - super::format_hex(&rp_id_hash[..8], &mut hex); + let rp_dir_start = super::rp_file_name_prefix(rp_id_hash); + + let mut num_rks = 0; + + let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(rp_dir_start.clone()) + )) + .entry; + + let mut first_rk = None; - let rp_dir = PathBuf::from(b"rk").join(&PathBuf::from(&hex)); - let (num_rks, first_rk) = self.count_rp_rks(rp_dir)?; + while let Some(entry) = maybe_entry { + if !entry + .file_name() + .as_str() + .starts_with(rp_dir_start.as_str()) + { + // We got past all credentials for the relevant RP + break; + } + + if entry.file_name() == &*rp_dir_start { + // This is the case where we + debug_assert!(entry.metadata().is_dir()); + error!("Migration did not run"); + return Err(Error::Other); + } + + first_rk = first_rk.or(Some(entry)); + num_rks += 1; + + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; + } + + let first_rk = first_rk.ok_or(Error::NoCredentials)?; // extract data required into response let mut response = self.extract_response_from_credential_file(first_rk.path())?; response.total_credentials = Some(num_rks); // cache state for next call - if let Some(num_rks) = response.total_credentials { - if num_rks > 1 { - // let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); - self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { - remaining: num_rks - 1, - rp_dir: first_rk.path().parent().unwrap(), - prev_filename: PathBuf::from(first_rk.file_name()), - }); - } + if num_rks > 1 { + // let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); + self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { + remaining: num_rks - 1, + prev_filename: first_rk.file_name().into(), + }); } Ok(response) @@ -317,60 +318,52 @@ where pub fn next_credential(&mut self) -> Result { info!("next credential"); - let CredentialManagementEnumerateCredentials { - remaining, - rp_dir, - prev_filename, - } = self + let cache = self .state .runtime .cached_rk - .clone() + .take() .ok_or(Error::NotAllowed)?; - // let (remaining, rp_dir, prev_filename) = match self.state.runtime.cached_rk { - // Some(CredentialManagementEnumerateCredentials( - // x, ref y, ref z)) - // => (x, y.clone(), z.clone()), - // _ => return Err(Error::NotAllowed), - // }; - - self.state.runtime.cached_rk = None; - // let mut hex = [b'0'; 16]; - // super::format_hex(&rp_id_hash[..8], &mut hex); - // let rp_dir = PathBuf::from(b"rk").join(&PathBuf::from(&hex)); + let CredentialManagementEnumerateCredentials { + remaining, + prev_filename, + } = cache; + + let rp_id_hex = get_rp_id_hex_from_file_name(prev_filename.as_str()); + syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(prev_filename.clone()), + )) + .entry; + + // The previous entry was already read. Skip to the next + let Some(entry) = syscall!(self.trussed.read_dir_next()).entry else { + return Err(Error::NoCredentials); + }; - let mut maybe_next_rk = - syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_dir, Some(prev_filename))) - .entry; + if get_rp_id_hex(&entry) != rp_id_hex { + // We reached the end of the credentials for the rp + return Err(Error::NoCredentials); + } - // Advance to the next - if maybe_next_rk.is_some() { - maybe_next_rk = syscall!(self.trussed.read_dir_next()).entry; - } else { - return Err(Error::NotAllowed); + if entry.metadata().is_dir() { + warn!("Migration did not finish"); + return Err(Error::Other); } - match maybe_next_rk { - Some(rk) => { - // extract data required into response - let response = self.extract_response_from_credential_file(rk.path())?; - - // cache state for next call - if remaining > 1 { - self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { - remaining: remaining - 1, - rp_dir: rk.path().parent().unwrap(), - prev_filename: PathBuf::from(rk.file_name()), - }); - } - - Ok(response) - } - None => Err(Error::NoCredentials), + let response = self.extract_response_from_credential_file(entry.path())?; + + // cache state for next call + if remaining > 1 { + self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { + remaining: remaining - 1, + prev_filename: entry.file_name().into(), + }); } + + Ok(response) } fn extract_response_from_credential_file(&mut self, rk_path: &Path) -> Result { @@ -382,7 +375,7 @@ where let serialized = syscall!(self.trussed.read_file(Location::Internal, rk_path.into(),)).data; - let credential = Credential::deserialize(&serialized) + let credential = FullCredential::deserialize(&serialized) // this may be a confusing error message .map_err(|_| Error::InvalidCredential)?; @@ -404,102 +397,111 @@ where }; use crate::SigningAlgorithm; - use trussed::types::{KeySerialization, Mechanism}; let algorithm = SigningAlgorithm::try_from(credential.algorithm)?; + let cose_public_key = algorithm.derive_public_key(&mut self.trussed, private_key); let cose_public_key = match algorithm { SigningAlgorithm::P256 => { - let public_key = syscall!(self - .trussed - .derive_p256_public_key(private_key, Location::Volatile)) - .key; - let cose_public_key = syscall!(self.trussed.serialize_key( - Mechanism::P256, - public_key, - // KeySerialization::EcdhEsHkdf256 - KeySerialization::Cose, - )) - .serialized_key; - syscall!(self.trussed.delete(public_key)); PublicKey::P256Key(ctap_types::serde::cbor_deserialize(&cose_public_key).unwrap()) } - SigningAlgorithm::Ed25519 => { - let public_key = syscall!(self - .trussed - .derive_ed255_public_key(private_key, Location::Volatile)) - .key; - let cose_public_key = syscall!(self - .trussed - .serialize_ed255_key(public_key, KeySerialization::Cose)) - .serialized_key; - syscall!(self.trussed.delete(public_key)); - PublicKey::Ed25519Key( - ctap_types::serde::cbor_deserialize(&cose_public_key).unwrap(), - ) - } // SigningAlgorithm::Totp => { - // PublicKey::TotpKey(Default::default()) - // } + SigningAlgorithm::Ed25519 => PublicKey::Ed25519Key( + ctap_types::serde::cbor_deserialize(&cose_public_key).unwrap(), + ), }; let cred_protect = match credential.cred_protect { Some(x) => Some(x), None => Some(CredentialProtectionPolicy::Optional), }; - let response = Response { - user: Some(credential.data.user), - credential_id: Some(credential_id.into()), - public_key: Some(cose_public_key), - cred_protect, - ..Default::default() - }; - + let mut response = Response::default(); + response.user = Some(credential.data.user.into()); + response.credential_id = Some(credential_id.into()); + response.public_key = Some(cose_public_key); + response.cred_protect = cred_protect; + response.large_blob_key = credential.data.large_blob_key; + response.third_party_payment = + Some(credential.data.third_party_payment.unwrap_or_default()); Ok(response) } + fn find_credential( + &mut self, + credential: &PublicKeyCredentialDescriptorRef<'_>, + ) -> Option { + let credential_id_hash = self.hash(credential.id); + let mut hex = [b'0'; 16]; + let hex_str = super::format_hex(&credential_id_hash[..8], &mut hex); + let dir = PathBuf::from(RK_DIR); + + let mut maybe_entry = + try_syscall!(self.trussed.read_dir_first(Location::Internal, dir, None)) + .ok()? + .entry; + while let Some(entry) = maybe_entry { + if entry.file_name().as_str().ends_with(&hex_str) { + return Some(entry.path().into()); + } + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; + } + None + } + pub fn delete_credential( &mut self, - credential_descriptor: &PublicKeyCredentialDescriptor, + credential_descriptor: &PublicKeyCredentialDescriptorRef<'_>, ) -> Result { info!("delete credential"); - let credential_id_hash = self.hash(&credential_descriptor.id[..]); - let mut hex = [b'0'; 16]; - super::format_hex(&credential_id_hash[..8], &mut hex); - let dir = PathBuf::from(b"rk"); - let filename = PathBuf::from(&hex); - - let rk_path = syscall!(self - .trussed - .locate_file(Location::Internal, Some(dir), filename,)) - .path - .ok_or(Error::InvalidCredential)?; + let rk_path = self + .find_credential(credential_descriptor) + .ok_or(Error::InvalidCredential)?; // DELETE self.delete_resident_key_by_path(&rk_path)?; - // get rid of directory if it's now empty - let rp_path = rk_path - .parent() - // by construction, RK has a parent, its RP - .unwrap(); - - let maybe_first_remaining_rk = - syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_path.clone(), None,)) - .entry; - - if maybe_first_remaining_rk.is_none() { - info!("deleting parent {:?} as this was its last RK", &rp_path); - syscall!(self.trussed.remove_dir(Location::Internal, rp_path,)); - } else { - info!( - "not deleting deleting parent {:?} as there is {:?}", - &rp_path, - &maybe_first_remaining_rk.unwrap().path(), - ); - } // just return OK let response = Default::default(); Ok(response) } + + pub fn update_user_information( + &mut self, + credential_descriptor: &PublicKeyCredentialDescriptorRef<'_>, + user: &PublicKeyCredentialUserEntity, + ) -> Result { + info!("update user information"); + + // locate and parse existing credential + let rk_path = self + .find_credential(credential_descriptor) + .ok_or(Error::NoCredentials)?; + let serialized = syscall!(self.trussed.read_file(Location::Internal, rk_path.clone())).data; + let mut credential = + FullCredential::deserialize(&serialized).map_err(|_| Error::InvalidCredential)?; + + // TODO: check remaining space, return KeyStoreFull + + // the updated user ID must match the stored user ID + if credential.user.id() != &user.id { + error!("updated user ID does not match original user ID"); + return Err(Error::InvalidParameter); + } + + // update user name and display name unless the values are not set or empty + let credential_user = credential.data.user.as_mut(); + credential_user.name = user.name.as_ref().filter(|s| !s.is_empty()).cloned(); + credential_user.display_name = user + .display_name + .as_ref() + .filter(|s| !s.is_empty()) + .cloned(); + + // write updated credential + let serialized = credential.serialize()?; + try_syscall!(self + .trussed + .write_file(Location::Internal, rk_path, serialized, None)) + .map_err(|_| Error::KeyStoreFull)?; + + Ok(Default::default()) + } } diff --git a/src/ctap2/large_blobs.rs b/src/ctap2/large_blobs.rs new file mode 100644 index 0000000..2df5f73 --- /dev/null +++ b/src/ctap2/large_blobs.rs @@ -0,0 +1,406 @@ +use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error}; +use littlefs2_core::{path, Path, PathBuf}; +use trussed_core::{ + config::MAX_MESSAGE_LENGTH, + try_syscall, + types::{Bytes, Location, Message}, + FilesystemClient, +}; + +#[cfg(feature = "chunked")] +use trussed_chunked::ChunkedClient; +#[cfg(not(feature = "chunked"))] +use trussed_core::{mechanisms::Sha256, syscall, types::Mechanism, CryptoClient}; + +use crate::{Result, TrussedRequirements}; + +const HASH_SIZE: usize = 16; +pub const MIN_SIZE: usize = HASH_SIZE + 1; +// empty CBOR array (0x80) + hash +const EMPTY_ARRAY: &[u8; MIN_SIZE] = &[ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, 0x6d, + 0x3c, +]; +const FILENAME: &Path = path!("large-blob-array"); +const FILENAME_TMP: &Path = path!(".large-blob-array"); + +pub type Chunk = Bytes; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Config { + /// The location for storing the large-blob array. + pub location: Location, + /// The maximum size for the large-blob array including metadata. + /// + /// This value must be at least 1024 according to the CTAP2.1 spec. Without the chunking + /// extension, it cannot be larger than 1024 because the large-blob array must fit into a + /// Trussed message. Therefore, this setting is only available if the chunked feature is + /// enabled. + #[cfg(feature = "chunked")] + pub max_size: usize, +} + +impl Config { + pub fn max_size(&self) -> usize { + #[cfg(feature = "chunked")] + { + self.max_size + } + + #[cfg(not(feature = "chunked"))] + { + MAX_MESSAGE_LENGTH + } + } +} + +pub fn size(client: &mut C, location: Location) -> Result { + Ok( + try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME))) + .map_err(|_| Error::Other)? + .metadata + .map(|metadata| metadata.len()) + .unwrap_or_default() + // If the data is shorter than MIN_SIZE, it is missing or corrupted and we fall back to + // an empty array which has exactly MIN_SIZE + .max(MIN_SIZE), + ) +} + +pub fn read_chunk( + client: &mut C, + location: Location, + offset: usize, + length: usize, +) -> Result { + SelectedStorage::read(client, location, offset, length) +} + +pub fn write_chunk( + client: &mut C, + state: &mut State, + location: Location, + data: &[u8], +) -> Result<()> { + write_impl::<_, SelectedStorage>(client, state, location, data) +} + +pub fn reset(client: &mut C) { + for location in [Location::Internal, Location::External, Location::Volatile] { + try_syscall!(client.remove_file(location, PathBuf::from(FILENAME))).ok(); + } + try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok(); +} + +fn write_impl>( + client: &mut C, + state: &mut State, + location: Location, + data: &[u8], +) -> Result<()> { + // sanity checks + if state.expected_next_offset + data.len() > state.expected_length { + return Err(Error::InvalidParameter); + } + + let mut writer = S::start_write( + client, + location, + state.expected_next_offset, + state.expected_length, + )?; + state.expected_next_offset = writer.extend_buffer(client, data)?; + if state.expected_next_offset == state.expected_length { + if writer.validate_checksum(client)? { + writer.commit(client) + } else { + writer.abort(client)?; + Err(Error::IntegrityFailure) + } + } else { + Ok(()) + } +} + +#[derive(Clone, Debug, Default)] +pub struct State { + pub expected_length: usize, + pub expected_next_offset: usize, +} + +trait Storage: Sized { + fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result; + + fn start_write( + client: &mut C, + location: Location, + offset: usize, + expected_length: usize, + ) -> Result; + + fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result; + + fn validate_checksum(&mut self, client: &mut C) -> Result; + + fn commit(&mut self, client: &mut C) -> Result<()>; + + fn abort(&mut self, client: &mut C) -> Result<()> { + let _ = client; + Ok(()) + } +} + +#[cfg(not(feature = "chunked"))] +type SelectedStorage = SimpleStorage; +#[cfg(feature = "chunked")] +type SelectedStorage = ChunkedStorage; + +// Basic implementation using a file in the volatile storage as a buffer based on the core Trussed +// API. Maximum size for the entire large blob array: 1024 bytes. +#[cfg(not(feature = "chunked"))] +struct SimpleStorage { + location: Location, + buffer: Message, +} + +#[cfg(not(feature = "chunked"))] +impl Storage for SimpleStorage { + fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result { + let result = try_syscall!(client.read_file(location, PathBuf::from(FILENAME))); + let data = if let Ok(reply) = &result { + reply.data.as_slice() + } else { + EMPTY_ARRAY.as_slice() + }; + let Some(max_length) = data.len().checked_sub(offset) else { + return Err(Error::InvalidParameter); + }; + let length = length.min(max_length); + let mut buffer = Chunk::new(); + buffer.extend_from_slice(&data[offset..][..length]).unwrap(); + Ok(buffer) + } + + fn start_write( + client: &mut C, + location: Location, + offset: usize, + expected_length: usize, + ) -> Result { + let buffer = if offset == 0 { + Message::new() + } else { + try_syscall!(client.read_file(Location::Volatile, PathBuf::from(FILENAME_TMP))) + .map_err(|_| Error::Other)? + .data + }; + + // sanity checks + if expected_length > buffer.capacity() { + return Err(Error::InvalidLength); + } + if buffer.len() != offset { + return Err(Error::Other); + } + + Ok(Self { buffer, location }) + } + + fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result { + self.buffer + .extend_from_slice(data) + .map_err(|_| Error::InvalidParameter)?; + try_syscall!(client.write_file( + Location::Volatile, + PathBuf::from(FILENAME_TMP), + self.buffer.clone(), + None + )) + .map_err(|_| Error::Other)?; + Ok(self.buffer.len()) + } + + fn validate_checksum(&mut self, client: &mut C) -> Result { + let Some(n) = self.buffer.len().checked_sub(HASH_SIZE) else { + return Ok(false); + }; + let mut message = Message::new(); + message.extend_from_slice(&self.buffer[..n]).unwrap(); + let checksum = syscall!(client.hash(Mechanism::Sha256, message)).hash; + Ok(checksum[..HASH_SIZE] == self.buffer[n..]) + } + + fn commit(&mut self, client: &mut C) -> Result<()> { + try_syscall!(client.write_file( + self.location, + PathBuf::from(FILENAME), + self.buffer.clone(), + None + )) + .map_err(|_| Error::Other)?; + try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok(); + Ok(()) + } +} + +#[cfg(feature = "chunked")] +struct ChunkedStorage { + location: Location, + expected_length: usize, + create_file: bool, +} + +#[cfg(feature = "chunked")] +impl Storage for ChunkedStorage { + fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result { + debug!("ChunkedStorage::read: offset = {offset}, length = {length}"); + let mut chunk = Chunk::new(); + let file_size = try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME))) + .map_err(|_| Error::Other)? + .metadata + .map(|metadata| metadata.len()) + .unwrap_or_default(); + if file_size < MIN_SIZE { + // The stored file is missing or too short, so we fall back to an empty array. + trace!("Sending empty array instead of missing or corrupted file"); + let start = offset.min(MIN_SIZE); + let end = (offset + length).min(MIN_SIZE); + chunk.extend_from_slice(&EMPTY_ARRAY[start..end]).unwrap(); + return Ok(chunk); + } + + while offset + chunk.len() < offset + length { + let n = MAX_MESSAGE_LENGTH.min(length - chunk.len()); + let reply = try_syscall!(client.partial_read_file( + location, + PathBuf::from(FILENAME), + offset + chunk.len(), + n + )) + .map_err(|_| Error::Other)?; + chunk + .extend_from_slice(&reply.data) + .map_err(|_| Error::Other)?; + if offset + chunk.len() >= reply.file_length { + break; + } + } + + trace!("Read chunk with {} bytes", chunk.len()); + Ok(chunk) + } + + fn start_write( + _client: &mut C, + location: Location, + offset: usize, + expected_length: usize, + ) -> Result { + debug!( + "ChunkedStorage::start_write: offset = {offset}, expected_length = {expected_length}" + ); + let create_file = offset == 0; + Ok(ChunkedStorage { + location, + create_file, + expected_length, + }) + } + + fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result { + debug!("ChunkedStorage::extend_buffer: |data| = {}", data.len()); + let mut n = 0; + for chunk in data.chunks(trussed_core::config::MAX_MESSAGE_LENGTH) { + trace!("Writing {} bytes", chunk.len()); + let path = PathBuf::from(FILENAME_TMP); + let mut message = Message::new(); + message.extend_from_slice(chunk).unwrap(); + if self.create_file { + try_syscall!(client.write_file(self.location, path, message, None)).map_err( + |_err| { + error!("failed to write initial chunk: {_err:?}"); + Error::Other + }, + )?; + self.create_file = false; + n = data.len(); + } else { + n = try_syscall!(client.append_file(self.location, path, message)) + .map(|reply| reply.file_length) + .map_err(|_err| { + error!("failed to append chunk: {_err:?}"); + Error::Other + })?; + } + } + Ok(n) + } + + fn validate_checksum(&mut self, client: &mut C) -> Result { + use sha2::{digest::Digest as _, Sha256}; + + debug!("ChunkedStorage::validate_checksum"); + + let mut digest = Sha256::new(); + let mut received_hash: Bytes = Bytes::new(); + let mut bytes_read = 0; + + let (mut chunk, mut len) = + try_syscall!(client.start_chunked_read(self.location, PathBuf::from(FILENAME_TMP))) + .map(|reply| (reply.data, reply.len)) + .map_err(|_err| { + error!("Failed to read file: {:?}", _err); + Error::Other + })?; + loop { + trace!("read chunk: {}", chunk.len()); + + let remaining_data = self + .expected_length + .saturating_sub(bytes_read) + .saturating_sub(HASH_SIZE); + let data_end = remaining_data.min(chunk.len()); + digest.update(&chunk[..data_end]); + if received_hash + .extend_from_slice(&chunk[data_end..chunk.len()]) + .is_err() + { + return Ok(false); + } + + bytes_read += chunk.len(); + if bytes_read >= len { + break; + } + + (chunk, len) = try_syscall!(client.read_file_chunk()) + .map(|reply| (reply.data, reply.len)) + .map_err(|_err| { + error!("Failed to read chunk: {:?}", _err); + Error::Other + })?; + } + + let actual_hash = digest.finalize(); + Ok(bytes_read == self.expected_length + && received_hash.as_slice() == &actual_hash[..HASH_SIZE]) + } + + fn commit(&mut self, client: &mut C) -> Result<()> { + debug!("ChunkedStorage::commit"); + try_syscall!(client.rename( + self.location, + PathBuf::from(FILENAME_TMP), + PathBuf::from(FILENAME) + )) + .map_err(|_| Error::Other)?; + Ok(()) + } + + fn abort(&mut self, client: &mut C) -> Result<()> { + debug!("ChunkedStorage::abort"); + try_syscall!(client.remove_file(self.location, PathBuf::from(FILENAME_TMP))) + .map_err(|_| Error::Other)?; + Ok(()) + } +} diff --git a/src/ctap2/pin.rs b/src/ctap2/pin.rs index e0ec212..a06f223 100644 --- a/src/ctap2/pin.rs +++ b/src/ctap2/pin.rs @@ -1,8 +1,498 @@ -// TODO: extract this, like credential_management.rs +use cosey::EcdhEsHkdf256PublicKey; +use ctap_types::{ctap2::client_pin::Permissions, Error, Result}; +use heapless::String; +use trussed_core::{ + mechanisms::{HmacSha256, P256}, + syscall, try_syscall, + types::{ + Bytes, KeyId, KeySerialization, Location, Mechanism, Message, ShortData, StorageAttributes, + }, + CryptoClient, +}; +use trussed_hkdf::{HkdfClient, KeyOrData, OkmId}; -pub(crate) struct ClientPin<'a, UP, T> -where UP: UserPresence, -{ - authnr: &'a mut Authenticator, +// PIN protocol 1 supports 16 or 32 bytes, PIN protocol 2 requires 32 bytes. +const PIN_TOKEN_LENGTH: usize = 32; + +#[derive(Clone, Copy, Debug)] +pub enum PinProtocolVersion { + V1, + V2, +} + +impl From for u8 { + fn from(version: PinProtocolVersion) -> Self { + match version { + PinProtocolVersion::V1 => 1, + PinProtocolVersion::V2 => 2, + } + } +} + +#[derive(Debug)] +pub enum RpScope<'a> { + All, + RpId(&'a str), + RpIdHash(&'a [u8; 32]), +} + +#[derive(Debug)] +pub struct PinToken { + key_id: KeyId, + state: PinTokenState, +} + +impl PinToken { + fn generate(trussed: &mut T) -> PinToken { + let key_id = + syscall!(trussed.generate_secret_key(PIN_TOKEN_LENGTH, Location::Volatile)).key; + Self::new(key_id) + } + + fn new(key_id: KeyId) -> Self { + Self { + key_id, + state: Default::default(), + } + } + + fn delete(self, trussed: &mut T) { + syscall!(trussed.delete(self.key_id)); + } + + pub fn require_permissions(&self, permissions: Permissions) -> Result<()> { + if self.state.permissions.contains(permissions) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } + + fn is_valid_for_rp(&self, scope: RpScope<'_>) -> bool { + if let Some(rp) = self.state.rp.as_ref() { + // if an RP id is set, the token is only valid for that scope + match scope { + RpScope::All => false, + RpScope::RpId(rp_id) => rp.id == rp_id, + RpScope::RpIdHash(hash) => &rp.hash == hash, + } + } else { + // if no RP ID is set, the token is valid for all scopes + true + } + } + + pub fn require_valid_for_rp(&self, scope: RpScope<'_>) -> Result<()> { + if self.is_valid_for_rp(scope) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } +} + +#[derive(Debug)] +pub struct PinTokenMut<'a, T: CryptoClient> { + pin_token: &'a mut PinToken, + trussed: &'a mut T, +} + +impl PinTokenMut<'_, T> { + pub fn restrict(&mut self, permissions: Permissions, rp_id: Option>) { + self.pin_token.state.permissions = permissions; + self.pin_token.state.rp = rp_id.map(|id| Rp::new(self.trussed, id)); + } + + // in spec: encrypt(..., pinUvAuthToken) + pub fn encrypt(&mut self, shared_secret: &SharedSecret) -> Result> { + let token = shared_secret.wrap(self.trussed, self.pin_token.key_id); + Bytes::from_slice(&token).map_err(|_| Error::Other) + } } +#[derive(Debug)] +struct Rp { + id: String<256>, + hash: [u8; 32], +} + +impl Rp { + fn new(trussed: &mut T, id: String<256>) -> Self { + let hash = + syscall!(trussed.hash(Mechanism::Sha256, Message::from_slice(id.as_ref()).unwrap())) + .hash + .as_slice() + .try_into() + .unwrap(); + Self { id, hash } + } +} + +#[derive(Debug, Default)] +struct PinTokenState { + permissions: Permissions, + rp: Option, + is_user_present: bool, + is_user_verified: bool, + is_in_use: bool, +} + +#[derive(Debug)] +pub struct PinProtocolState { + key_agreement_key: KeyId, + // only used to delete the old shared secret from VFS when generating a new one. ideally, the + // SharedSecret struct would clean up after itself. + shared_secret: Option, + + // for protocol version 1 + pin_token_v1: Option, + // for protocol version 2 + pin_token_v2: Option, +} + +impl PinProtocolState { + // in spec: initialize(...) + pub fn new(trussed: &mut T) -> Self { + Self { + key_agreement_key: generate_key_agreement_key(trussed), + shared_secret: None, + pin_token_v1: None, + pin_token_v2: None, + } + } + + pub fn reset(self, trussed: &mut T) { + if let Some(token) = self.pin_token_v1 { + token.delete(trussed); + } + if let Some(token) = self.pin_token_v2 { + token.delete(trussed); + } + syscall!(trussed.delete(self.key_agreement_key)); + if let Some(shared_secret) = self.shared_secret { + shared_secret.delete(trussed); + } + } +} + +#[derive(Debug)] +pub struct PinProtocol<'a, T> { + trussed: &'a mut T, + state: &'a mut PinProtocolState, + version: PinProtocolVersion, +} + +impl<'a, T: CryptoClient + HkdfClient + HmacSha256 + P256> PinProtocol<'a, T> { + pub fn new( + trussed: &'a mut T, + state: &'a mut PinProtocolState, + version: PinProtocolVersion, + ) -> Self { + Self { + trussed, + state, + version, + } + } + + fn pin_token(&self) -> Option<&PinToken> { + match self.version { + PinProtocolVersion::V1 => self.state.pin_token_v1.as_ref(), + PinProtocolVersion::V2 => self.state.pin_token_v2.as_ref(), + } + } + + pub fn regenerate(&mut self) { + syscall!(self.trussed.delete(self.state.key_agreement_key)); + if let Some(shared_secret) = self.state.shared_secret.take() { + shared_secret.delete(self.trussed); + } + self.state.key_agreement_key = generate_key_agreement_key(self.trussed); + } + + // in spec: resetPinUvAuthToken() + pub fn reset_pin_tokens(&mut self) { + if let Some(token) = self.state.pin_token_v1.take() { + token.delete(self.trussed); + } + if let Some(token) = self.state.pin_token_v2.take() { + token.delete(self.trussed); + } + } + + // in spec: beginUsingPinUvAuthToken(userIsPresent) + fn begin_using_pin_token(&mut self, is_user_present: bool) -> PinTokenMut<'_, T> { + // we assume that the previous PIN token has already been reset so we don’t need to delete it + let mut pin_token = PinToken::generate(self.trussed); + pin_token.state.is_user_present = is_user_present; + pin_token.state.is_user_verified = true; + // TODO: set initial usage time limit + // TODO: start and observe usage timer + pin_token.state.is_in_use = true; + let pin_token_field = match self.version { + PinProtocolVersion::V1 => &mut self.state.pin_token_v1, + PinProtocolVersion::V2 => &mut self.state.pin_token_v2, + }; + let pin_token = pin_token_field.insert(pin_token); + PinTokenMut { + pin_token, + trussed: self.trussed, + } + } + + pub fn reset_and_begin_using_pin_token(&mut self, is_user_present: bool) -> PinTokenMut<'_, T> { + self.reset_pin_tokens(); + self.begin_using_pin_token(is_user_present) + } + + // in spec: getPublicKey + #[must_use] + pub fn key_agreement_key(&mut self) -> EcdhEsHkdf256PublicKey { + let public_key = syscall!(self + .trussed + .derive_p256_public_key(self.state.key_agreement_key, Location::Volatile)) + .key; + let serialized_cose_key = syscall!(self.trussed.serialize_key( + Mechanism::P256, + public_key, + KeySerialization::EcdhEsHkdf256 + )) + .serialized_key; + let cose_key = cbor_smol::cbor_deserialize(&serialized_cose_key).unwrap(); + syscall!(self.trussed.delete(public_key)); + cose_key + } + + #[must_use] + fn verify(&mut self, key: KeyId, data: &[u8], signature: &[u8]) -> bool { + let actual_signature = syscall!(self.trussed.sign_hmacsha256(key, data)).signature; + let expected_signature = match self.version { + PinProtocolVersion::V1 => &actual_signature[..16], + PinProtocolVersion::V2 => &actual_signature, + }; + expected_signature == signature + } + + // in spec: verify(pinUvAuthToken, ...) + pub fn verify_pin_token(&mut self, data: &[u8], signature: &[u8]) -> Result<&PinToken> { + let pin_token = self.pin_token().ok_or(Error::PinAuthInvalid)?; + if pin_token.state.is_in_use && self.verify(pin_token.key_id, data, signature) { + // We previously checked that `pin_token()` is not None in the first line of this + // function so this cannot panic, but we cannot return the `pin_token` variable here + // because of the `verify` call after it. + Ok(self.pin_token().unwrap()) + } else { + Err(Error::PinAuthInvalid) + } + } + + // in spec: verify(shared secret, ...) + pub fn verify_pin_auth( + &mut self, + shared_secret: &SharedSecret, + data: &[u8], + pin_auth: &[u8], + ) -> Result<()> { + let key_id = shared_secret.hmac_key_id(); + if self.verify(key_id, data, pin_auth) { + Ok(()) + } else { + Err(Error::PinAuthInvalid) + } + } + + // in spec: decapsulate(...) = ecdh(...) + // The returned key ID is valid until the next call of shared_secret or regenerate. The caller + // has to delete the key from the VFS after end of use. Ideally, this should be enforced by + // the compiler, for example by using a callback. + pub fn shared_secret(&mut self, peer_key: &EcdhEsHkdf256PublicKey) -> Result { + self.shared_secret_impl(peer_key) + .ok_or(Error::InvalidParameter) + } + + fn shared_secret_impl(&mut self, peer_key: &EcdhEsHkdf256PublicKey) -> Option { + let mut serialized_peer_key = Message::new(); + cbor_smol::cbor_serialize_to(peer_key, &mut serialized_peer_key).ok()?; + let peer_key = try_syscall!(self.trussed.deserialize_p256_key( + &serialized_peer_key, + KeySerialization::EcdhEsHkdf256, + StorageAttributes::new().set_persistence(Location::Volatile) + )) + .ok()? + .key; + + let result = try_syscall!(self.trussed.agree( + Mechanism::P256, + self.state.key_agreement_key, + peer_key, + StorageAttributes::new().set_persistence(Location::Volatile), + )); + syscall!(self.trussed.delete(peer_key)); + let pre_shared_secret = result.ok()?.shared_secret; + + if let Some(shared_secret) = self.state.shared_secret.take() { + shared_secret.delete(self.trussed); + } + + let shared_secret = self.kdf(pre_shared_secret); + syscall!(self.trussed.delete(pre_shared_secret)); + + let shared_secret = shared_secret?; + self.state.shared_secret = Some(shared_secret.clone()); + Some(shared_secret) + } + + fn kdf(&mut self, input: KeyId) -> Option { + match self.version { + PinProtocolVersion::V1 => self.kdf_v1(input), + PinProtocolVersion::V2 => self.kdf_v2(input), + } + } + + // PIN protocol 1: derive a single key using SHA-256 + fn kdf_v1(&mut self, input: KeyId) -> Option { + let key_id = syscall!(self.trussed.derive_key( + Mechanism::Sha256, + input, + None, + StorageAttributes::new().set_persistence(Location::Volatile) + )) + .key; + Some(SharedSecret::V1 { key_id }) + } + + // PIN protocol 2: derive two keys using HKDF-SHA-256 + // In the spec, the keys are concatenated and the relevant part is selected during the key + // operations. For simplicity, we store two separate keys instead. + fn kdf_v2(&mut self, input: KeyId) -> Option { + fn hkdf(trussed: &mut T, okm: OkmId, info: &[u8]) -> Option { + let info = Message::from_slice(info).ok()?; + try_syscall!(trussed.hkdf_expand(okm, info, 32, Location::Volatile)) + .ok() + .map(|reply| reply.key) + } + + // salt: 0x00 * 32 => None + let okm = try_syscall!(self.trussed.hkdf_extract( + KeyOrData::Key(input), + None, + Location::Volatile + )) + .ok()? + .okm; + let hmac_key_id = hkdf(self.trussed, okm, b"CTAP2 HMAC key"); + let aes_key_id = hkdf(self.trussed, okm, b"CTAP2 AES key"); + + syscall!(self.trussed.delete(okm.0)); + + Some(SharedSecret::V2 { + hmac_key_id: hmac_key_id?, + aes_key_id: aes_key_id?, + }) + } +} + +#[derive(Clone, Debug)] +pub enum SharedSecret { + V1 { + key_id: KeyId, + }, + V2 { + hmac_key_id: KeyId, + aes_key_id: KeyId, + }, +} + +impl SharedSecret { + fn aes_key_id(&self) -> KeyId { + match self { + Self::V1 { key_id } => *key_id, + Self::V2 { aes_key_id, .. } => *aes_key_id, + } + } + + fn hmac_key_id(&self) -> KeyId { + match self { + Self::V1 { key_id } => *key_id, + Self::V2 { hmac_key_id, .. } => *hmac_key_id, + } + } + + fn generate_iv(&self, trussed: &mut T) -> ShortData { + match self { + Self::V1 { .. } => ShortData::from_slice(&[0; 16]).unwrap(), + Self::V2 { .. } => syscall!(trussed.random_bytes(16)) + .bytes + .try_convert_into() + .unwrap(), + } + } + + #[must_use] + pub fn encrypt(&self, trussed: &mut T, data: &[u8]) -> Bytes<1024> { + let key_id = self.aes_key_id(); + let iv = self.generate_iv(trussed); + let mut ciphertext = + syscall!(trussed.encrypt(Mechanism::Aes256Cbc, key_id, data, &[], Some(iv.clone()))) + .ciphertext; + if matches!(self, Self::V2 { .. }) { + ciphertext.insert_slice_at(&iv, 0).unwrap(); + } + ciphertext + } + + #[must_use] + fn wrap(&self, trussed: &mut T, key: KeyId) -> Bytes<1024> { + let wrapping_key = self.aes_key_id(); + let iv = self.generate_iv(trussed); + let mut wrapped_key = syscall!(trussed.wrap_key( + Mechanism::Aes256Cbc, + wrapping_key, + key, + &[], + Some(iv.clone()) + )) + .wrapped_key; + if matches!(self, Self::V2 { .. }) { + wrapped_key.insert_slice_at(&iv, 0).unwrap(); + } + wrapped_key + } + + #[must_use] + pub fn decrypt(&self, trussed: &mut T, data: &[u8]) -> Option> { + let key_id = self.aes_key_id(); + let (iv, data) = match self { + Self::V1 { .. } => (Default::default(), data), + Self::V2 { .. } => { + if data.len() < 16 { + return None; + } + data.split_at(16) + } + }; + try_syscall!(trussed.decrypt(Mechanism::Aes256Cbc, key_id, data, b"", iv, b"")) + .ok() + .and_then(|response| response.plaintext) + } + + pub fn delete(self, trussed: &mut T) { + match self { + Self::V1 { key_id } => { + syscall!(trussed.delete(key_id)); + } + Self::V2 { + hmac_key_id, + aes_key_id, + } => { + for key_id in [hmac_key_id, aes_key_id] { + syscall!(trussed.delete(key_id)); + } + } + } + } +} + +fn generate_key_agreement_key(trussed: &mut T) -> KeyId { + syscall!(trussed.generate_p256_private_key(Location::Volatile)).key +} diff --git a/src/dispatch.rs b/src/dispatch.rs index b0d8183..50e4c96 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -8,7 +8,7 @@ use crate::msp; use crate::{Authenticator, TrussedRequirements, UserPresence}; use ctap_types::{ctap1, ctap2}; -use iso7816::Status; +use iso7816::{command::CommandView, Data, Status}; impl iso7816::App for Authenticator where @@ -21,10 +21,10 @@ where #[inline(never)] /// Deserialize U2F, call authenticator, serialize response *Result*. -fn handle_ctap1_from_hid( +fn handle_ctap1_from_hid( authenticator: &mut Authenticator, data: &[u8], - response: &mut apdu_dispatch::response::Data, + response: &mut Data, ) where T: TrussedRequirements, UP: UserPresence, @@ -34,16 +34,18 @@ fn handle_ctap1_from_hid( msp() - 0x2000_0000 ); { - let command = apdu_dispatch::Command::try_from(data); - if let Err(_status) = command { - let code: [u8; 2] = (Status::IncorrectDataParameter).into(); - debug!("CTAP1 parse error: {:?} ({})", status, hex_str!(&code)); - response.extend_from_slice(&code).ok(); - return; - } + let command = match CommandView::try_from(data) { + Ok(command) => command, + Err(_status) => { + let code: [u8; 2] = (Status::IncorrectDataParameter).into(); + debug!("CTAP1 parse error: {:?} ({})", _status, hex_str!(&code)); + response.extend_from_slice(&code).ok(); + return; + } + }; // debug!("1A SP: {:X}", msp()); - match try_handle_ctap1(authenticator, &command.unwrap(), response) { + match try_handle_ctap1(authenticator, command, response) { Ok(()) => { debug!("U2F response {} bytes", response.len()); // Need to add x9000 success code (normally the apdu-dispatch does this, but @@ -63,10 +65,10 @@ fn handle_ctap1_from_hid( #[inline(never)] /// Deserialize CBOR, call authenticator, serialize response *Result*. -fn handle_ctap2( +fn handle_ctap2( authenticator: &mut Authenticator, data: &[u8], - response: &mut apdu_dispatch::response::Data, + response: &mut Data, ) where T: TrussedRequirements, UP: UserPresence, @@ -87,10 +89,10 @@ fn handle_ctap2( } #[inline(never)] -fn try_handle_ctap1( +fn try_handle_ctap1( authenticator: &mut Authenticator, - command: &apdu_dispatch::Command, - response: &mut apdu_dispatch::response::Data, + command: CommandView<'_>, + response: &mut Data, ) -> Result<(), Status> where T: TrussedRequirements, @@ -122,10 +124,10 @@ where } #[inline(never)] -fn try_handle_ctap2( +fn try_handle_ctap2( authenticator: &mut Authenticator, data: &[u8], - response: &mut apdu_dispatch::response::Data, + response: &mut Data, ) -> Result<(), u8> where T: TrussedRequirements, @@ -175,10 +177,9 @@ where // Goal of these nested scopes is to keep stack small. let ctap_request = ctap2::Request::deserialize(data) - .map(|request| { - info!("Received CTAP2 request {:?}", request_operation(&request)); - trace!("CTAP2 request: {:?}", request); - request + .inspect(|_request| { + info!("Received CTAP2 request {:?}", request_operation(_request)); + trace!("CTAP2 request: {:?}", _request); }) .map_err(|error| { error!("Failed to deserialize CTAP2 request: {:?}", error); @@ -189,10 +190,9 @@ where use ctap2::Authenticator; authenticator .call_ctap2(&ctap_request) - .map(|response| { - info!("Sending CTAP2 response {:?}", response_operation(&response)); - trace!("CTAP2 response: {:?}", response); - response + .inspect(|_response| { + info!("Sending CTAP2 response {:?}", response_operation(_response)); + trace!("CTAP2 response: {:?}", _response); }) .map_err(|error| { info!("CTAP2 error: {:?}", error); @@ -201,17 +201,20 @@ where } #[allow(unused)] -fn request_operation(request: &ctap2::Request) -> ctap2::Operation { +fn request_operation(request: &ctap2::Request) -> Option { + // TODO: move into ctap-types match request { - ctap2::Request::MakeCredential(_) => ctap2::Operation::MakeCredential, - ctap2::Request::GetAssertion(_) => ctap2::Operation::GetAssertion, - ctap2::Request::GetNextAssertion => ctap2::Operation::GetNextAssertion, - ctap2::Request::GetInfo => ctap2::Operation::GetInfo, - ctap2::Request::ClientPin(_) => ctap2::Operation::ClientPin, - ctap2::Request::Reset => ctap2::Operation::Reset, - ctap2::Request::CredentialManagement(_) => ctap2::Operation::CredentialManagement, - ctap2::Request::Selection => ctap2::Operation::Selection, - ctap2::Request::Vendor(operation) => ctap2::Operation::Vendor(*operation), + ctap2::Request::MakeCredential(_) => Some(ctap2::Operation::MakeCredential), + ctap2::Request::GetAssertion(_) => Some(ctap2::Operation::GetAssertion), + ctap2::Request::GetNextAssertion => Some(ctap2::Operation::GetNextAssertion), + ctap2::Request::GetInfo => Some(ctap2::Operation::GetInfo), + ctap2::Request::ClientPin(_) => Some(ctap2::Operation::ClientPin), + ctap2::Request::Reset => Some(ctap2::Operation::Reset), + ctap2::Request::CredentialManagement(_) => Some(ctap2::Operation::CredentialManagement), + ctap2::Request::Selection => Some(ctap2::Operation::Selection), + ctap2::Request::LargeBlobs(_) => Some(ctap2::Operation::LargeBlobs), + ctap2::Request::Vendor(operation) => Some(ctap2::Operation::Vendor(*operation)), + _ => None, } } @@ -226,6 +229,8 @@ fn response_operation(request: &ctap2::Response) -> Option { ctap2::Response::Reset => Some(ctap2::Operation::Reset), ctap2::Response::CredentialManagement(_) => Some(ctap2::Operation::CredentialManagement), ctap2::Response::Selection => Some(ctap2::Operation::Selection), + ctap2::Response::LargeBlobs(_) => Some(ctap2::Operation::LargeBlobs), ctap2::Response::Vendor => None, + _ => None, } } diff --git a/src/dispatch/apdu.rs b/src/dispatch/apdu.rs index cbc2ed4..f572766 100644 --- a/src/dispatch/apdu.rs +++ b/src/dispatch/apdu.rs @@ -1,7 +1,6 @@ -use apdu_dispatch::{app as apdu, response::Data, Command}; +use apdu_app::Interface; use ctap_types::{serde::error::Error as SerdeError, Error}; -use ctaphid_dispatch::app as ctaphid; -use iso7816::Status; +use iso7816::{command::CommandView, Data, Status}; use crate::{Authenticator, TrussedRequirements, UserPresence}; @@ -22,13 +21,23 @@ impl From for Error { } } -impl apdu::App<{ apdu_dispatch::command::SIZE }, { apdu_dispatch::response::SIZE }> - for Authenticator +impl apdu_app::App for Authenticator where UP: UserPresence, T: TrussedRequirements, { - fn select(&mut self, _: &Command, reply: &mut Data) -> apdu::Result { + fn select( + &mut self, + interface: Interface, + _: CommandView<'_>, + reply: &mut Data, + ) -> apdu_app::Result { + // FIDO-over-CCID does not seem to officially be a thing; we don't support it. + // If we would, need to review the following cases catering to semi-documented U2F legacy. + if interface != Interface::Contactless { + return Err(Status::ConditionsOfUseNotSatisfied); + } + reply.extend_from_slice(b"U2F_V2").unwrap(); Ok(()) } @@ -37,13 +46,13 @@ where fn call( &mut self, - interface: apdu::Interface, - apdu: &Command, - response: &mut Data, - ) -> apdu::Result { + interface: Interface, + apdu: CommandView<'_>, + response: &mut Data, + ) -> apdu_app::Result { // FIDO-over-CCID does not seem to officially be a thing; we don't support it. // If we would, need to review the following cases catering to semi-documented U2F legacy. - if interface != apdu::Interface::Contactless { + if interface != Interface::Contactless { return Err(Status::ConditionsOfUseNotSatisfied); } @@ -59,14 +68,16 @@ where match instruction { // U2F instruction codes // NB(nickray): I don't think 0x00 is a valid case. - 0x00 | 0x01 | 0x02 => super::try_handle_ctap1(self, apdu, response)?, //self.call_authenticator_u2f(apdu, response), + 0x00..=0x02 => super::try_handle_ctap1(self, apdu, response)?, //self.call_authenticator_u2f(apdu, response), _ => { - match ctaphid::Command::try_from(instruction) { + match ctaphid_app::Command::try_from(instruction) { // 0x10 - Ok(ctaphid::Command::Cbor) => super::handle_ctap2(self, apdu.data(), response), - Ok(ctaphid::Command::Msg) => super::try_handle_ctap1(self, apdu, response)?, - Ok(ctaphid::Command::Deselect) => self.deselect(), + Ok(ctaphid_app::Command::Cbor) => { + super::handle_ctap2(self, apdu.data(), response) + } + Ok(ctaphid_app::Command::Msg) => super::try_handle_ctap1(self, apdu, response)?, + Ok(ctaphid_app::Command::Deselect) => apdu_app::App::::deselect(self), _ => { info!("Unsupported ins for fido app {:02x}", instruction); return Err(iso7816::Status::InstructionNotSupportedOrInvalid); diff --git a/src/dispatch/ctaphid.rs b/src/dispatch/ctaphid.rs index 60f0c8e..34e9cc0 100644 --- a/src/dispatch/ctaphid.rs +++ b/src/dispatch/ctaphid.rs @@ -1,25 +1,27 @@ -use ctaphid_dispatch::app as ctaphid; +use ctaphid_app::{App, Command, Error}; +use heapless_bytes::Bytes; +use trussed_core::InterruptFlag; #[allow(unused_imports)] use crate::msp; use crate::{Authenticator, TrussedRequirements, UserPresence}; -impl ctaphid::App for Authenticator +impl App<'static, N> for Authenticator where UP: UserPresence, T: TrussedRequirements, { - fn commands(&self) -> &'static [ctaphid::Command] { - &[ctaphid::Command::Cbor, ctaphid::Command::Msg] + fn commands(&self) -> &'static [Command] { + &[Command::Cbor, Command::Msg] } #[inline(never)] fn call( &mut self, - command: ctaphid::Command, - request: &ctaphid::Message, - response: &mut ctaphid::Message, - ) -> ctaphid::AppResult { + command: Command, + request: &[u8], + response: &mut Bytes, + ) -> Result<(), Error> { debug_now!( "ctaphid-dispatch: remaining stack: {} bytes", msp() - 0x2000_0000 @@ -27,18 +29,22 @@ where if request.is_empty() { debug_now!("invalid request length in ctaphid.call"); - return Err(ctaphid::Error::InvalidLength); + return Err(Error::InvalidLength); } // info_now!("request: "); // blocking::dump_hex(request, request.len()); match command { - ctaphid::Command::Cbor => super::handle_ctap2(self, request, response), - ctaphid::Command::Msg => super::handle_ctap1_from_hid(self, request, response), + Command::Cbor => super::handle_ctap2(self, request, response), + Command::Msg => super::handle_ctap1_from_hid(self, request, response), _ => { debug_now!("ctaphid trying to dispatch {:?}", command); } }; Ok(()) } + + fn interrupt(&self) -> Option<&'static InterruptFlag> { + self.trussed.interrupt() + } } diff --git a/src/lib.rs b/src/lib.rs index e1f776b..2aba994 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,9 @@ //! //! With feature `dispatch` activated, it also implements the `App` traits //! of [`apdu_dispatch`] and [`ctaphid_dispatch`]. +//! +//! [`apdu_dispatch`]: https://docs.rs/apdu-dispatch +//! [`ctaphid_dispatch`]: https://docs.rs/ctaphid-dispatch #![cfg_attr(not(test), no_std)] // #![warn(missing_docs)] @@ -15,11 +18,20 @@ extern crate delog; generate_macros!(); -use core::time::Duration; +pub use state::migrate; -use trussed::{client, syscall, types::Message, Client as TrussedClient}; +use core::time::Duration; -use ctap_types::heapless_bytes::Bytes; +use trussed_core::{ + mechanisms, syscall, + types::{ + KeyId, KeySerialization, Location, Mechanism, SerializedKey, Signature, + SignatureSerialization, StorageAttributes, + }, + CertificateClient, CryptoClient, FilesystemClient, ManagementClient, UiClient, +}; +use trussed_fs_info::{FsInfoClient, FsInfoReply}; +use trussed_hkdf::HkdfClient; /// Re-export of `ctap-types` authenticator errors. pub use ctap_types::Error; @@ -34,6 +46,8 @@ pub mod constants; pub mod credential; pub mod state; +pub use ctap2::large_blobs::Config as LargeBlobsConfig; + /// Results with our [`Error`]. pub type Result = core::result::Result; @@ -43,28 +57,56 @@ pub type Result = core::result::Result; /// - Ed25519 and P-256 are the core signature algorithms. /// - AES-256, SHA-256 and its HMAC are used within the CTAP protocols. /// - ChaCha8Poly1305 is our AEAD of choice, used e.g. for the key handles. +/// - Some Trussed extensions might be required depending on the activated features, see +/// [`ExtensionRequirements`][]. pub trait TrussedRequirements: - client::Client - + client::P256 - + client::Chacha8Poly1305 - + client::Aes256Cbc - + client::Sha256 - + client::HmacSha256 - + client::Ed255 // + client::Totp + CertificateClient + + CryptoClient + + FilesystemClient + + ManagementClient + + UiClient + + mechanisms::P256 + + mechanisms::Chacha8Poly1305 + + mechanisms::Aes256Cbc + + mechanisms::Sha256 + + mechanisms::HmacSha256 + + mechanisms::Ed255 + + FsInfoClient + + HkdfClient + + ExtensionRequirements { } impl TrussedRequirements for T where - T: client::Client - + client::P256 - + client::Chacha8Poly1305 - + client::Aes256Cbc - + client::Sha256 - + client::HmacSha256 - + client::Ed255 // + client::Totp + T: CertificateClient + + CryptoClient + + FilesystemClient + + ManagementClient + + UiClient + + mechanisms::P256 + + mechanisms::Chacha8Poly1305 + + mechanisms::Aes256Cbc + + mechanisms::Sha256 + + mechanisms::HmacSha256 + + mechanisms::Ed255 + + FsInfoClient + + HkdfClient + + ExtensionRequirements { } +#[cfg(not(feature = "chunked"))] +pub trait ExtensionRequirements {} + +#[cfg(not(feature = "chunked"))] +impl ExtensionRequirements for T {} + +#[cfg(feature = "chunked")] +pub trait ExtensionRequirements: trussed_chunked::ChunkedClient {} + +#[cfg(feature = "chunked")] +impl ExtensionRequirements for T where T: trussed_chunked::ChunkedClient {} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] /// Externally defined configuration. pub struct Config { @@ -76,6 +118,20 @@ pub struct Config { /// If set, the first Get Assertion or Authenticate request within the specified time after /// boot is accepted without additional user presence verification. pub skip_up_timeout: Option, + /// The maximum number of resident credentials. + pub max_resident_credential_count: Option, + /// Configuration for the largeBlobKey extension and the largeBlobs command. + /// + /// If this is `None`, the extension and the command are disabled. + pub large_blobs: Option, + /// Whether the authenticator supports the NFC transport. + pub nfc_transport: bool, +} + +impl Config { + pub fn supports_large_blobs(&self) -> bool { + self.large_blobs.is_some() + } } // impl Default for Config { @@ -111,13 +167,16 @@ where } // EWW.. this is a bit unsafe isn't it -fn format_hex(data: &[u8], mut buffer: &mut [u8]) { +fn format_hex<'a>(data: &[u8], buffer: &'a mut [u8]) -> &'a str { const HEX_CHARS: &[u8] = b"0123456789abcdef"; - for byte in data.iter() { - buffer[0] = HEX_CHARS[(byte >> 4) as usize]; - buffer[1] = HEX_CHARS[(byte & 0xf) as usize]; - buffer = &mut buffer[2..]; + assert!(data.len() * 2 >= buffer.len()); + for (idx, byte) in data.iter().enumerate() { + buffer[idx * 2] = HEX_CHARS[(byte >> 4) as usize]; + buffer[idx * 2 + 1] = HEX_CHARS[(byte & 0xf) as usize]; } + + // SAFETY: we just added only ascii chars to buffer from 0 to data.len() - 1 + unsafe { core::str::from_utf8_unchecked(&buffer[0..data.len() * 2]) } } // NB: to actually use this, replace the constant implementation with the inline assembly. @@ -147,17 +206,79 @@ pub enum SigningAlgorithm { Ed25519 = -8, /// The NIST P-256 signature algorithm. P256 = -7, - // #[doc(hidden)] - // Totp = -9, } -impl core::convert::TryFrom for SigningAlgorithm { +impl SigningAlgorithm { + pub fn mechanism(&self) -> Mechanism { + match self { + Self::Ed25519 => Mechanism::Ed255, + Self::P256 => Mechanism::P256, + } + } + + pub fn signature_serialization(&self) -> SignatureSerialization { + match self { + Self::Ed25519 => SignatureSerialization::Raw, + Self::P256 => SignatureSerialization::Asn1Der, + } + } + + pub fn generate_private_key( + &self, + trussed: &mut C, + location: Location, + ) -> KeyId { + syscall!(trussed.generate_key( + self.mechanism(), + StorageAttributes::new().set_persistence(location) + )) + .key + } + + pub fn derive_public_key( + &self, + trussed: &mut C, + private_key: KeyId, + ) -> SerializedKey { + let mechanism = self.mechanism(); + let public_key = syscall!(trussed.derive_key( + mechanism, + private_key, + None, + StorageAttributes::new().set_persistence(Location::Volatile) + )) + .key; + let cose_public_key = + syscall!(trussed.serialize_key(mechanism, public_key, KeySerialization::Cose)) + .serialized_key; + if !syscall!(trussed.delete(public_key)).success { + error!("failed to delete credential public key"); + } + cose_public_key + } + + pub fn sign(&self, trussed: &mut C, key: KeyId, data: &[u8]) -> Signature { + syscall!(trussed.sign(self.mechanism(), key, data, self.signature_serialization())) + .signature + } +} + +impl From for i32 { + fn from(alg: SigningAlgorithm) -> Self { + match alg { + SigningAlgorithm::P256 => -7, + SigningAlgorithm::Ed25519 => -8, + } + } +} + +impl TryFrom for SigningAlgorithm { type Error = Error; + fn try_from(alg: i32) -> Result { Ok(match alg { -7 => SigningAlgorithm::P256, -8 => SigningAlgorithm::Ed25519, - // -9 => SigningAlgorithm::Totp, _ => return Err(Error::UnsupportedAlgorithm), }) } @@ -165,7 +286,7 @@ impl core::convert::TryFrom for SigningAlgorithm { /// Method to check for user presence. pub trait UserPresence: Copy { - fn user_present( + fn user_present( self, trussed: &mut T, timeout_milliseconds: u32, @@ -181,7 +302,7 @@ pub type SilentAuthenticator = Silent; pub struct Silent {} impl UserPresence for Silent { - fn user_present(self, _: &mut T, _: u32) -> Result<()> { + fn user_present(self, _: &mut T, _: u32) -> Result<()> { Ok(()) } } @@ -195,26 +316,20 @@ pub type NonSilentAuthenticator = Conforming; pub struct Conforming {} impl UserPresence for Conforming { - fn user_present( + fn user_present( self, trussed: &mut T, timeout_milliseconds: u32, ) -> Result<()> { let result = syscall!(trussed.confirm_user_present(timeout_milliseconds)).result; result.map_err(|err| match err { - trussed::types::consent::Error::TimedOut => Error::UserActionTimeout, - // trussed::types::consent::Error::TimedOut => Error::KeepaliveCancel, + trussed_core::types::consent::Error::TimedOut => Error::UserActionTimeout, + trussed_core::types::consent::Error::Interrupted => Error::KeepaliveCancel, _ => Error::OperationDenied, }) } } -fn cbor_serialize_message( - object: &T, -) -> core::result::Result { - trussed::cbor_serialize_bytes(object) -} - impl Authenticator where UP: UserPresence, @@ -230,9 +345,49 @@ where } } - fn hash(&mut self, data: &[u8]) -> Bytes<32> { + fn estimate_remaining_inner(info: &FsInfoReply) -> Option { + let block_size = info.block_info.as_ref()?.size; + // 1 block for the directory, 1 for the private key, 400 bytes for a reasonnable key and metadata + let size_taken = 2 * block_size + 400; + // Remove 5 block kept as buffer + Some((info.available_space.saturating_sub(5 * block_size) / size_taken) as u32) + } + + fn estimate_remaining(&mut self) -> Option { + let info = syscall!(self.trussed.fs_info(Location::Internal)); + debug!("Got filesystem info: {info:?}"); + Self::estimate_remaining_inner(&info) + } + + fn can_fit_inner(info: &FsInfoReply, size: usize) -> Option { + let block_size = info.block_info.as_ref()?.size; + // 1 block for the rp directory, 5 block of margin, 50 bytes for a reasonnable metadata + let size_taken = 6 * block_size + size + 50; + Some(size_taken < info.available_space) + } + + /// Can a credential of size `size` be stored with safe margins + /// + /// This assumes that the key has already been generated and is stored. + fn can_fit(&mut self, size: usize) -> Option { + debug!("Can fit for {size} bytes"); + let info = syscall!(self.trussed.fs_info(Location::Internal)); + debug!("Got filesystem info: {info:?}"); + debug!( + "Available storage: {:?}", + Self::estimate_remaining_inner(&info) + ); + Self::can_fit_inner(&info, size) + } + + fn hash(&mut self, data: &[u8]) -> [u8; 32] { let hash = syscall!(self.trussed.hash_sha256(data)).hash; - hash.to_bytes().expect("hash should fit") + hash.as_slice().try_into().expect("hash should fit") + } + + fn nonce(&mut self) -> [u8; 12] { + let bytes = syscall!(self.trussed.random_bytes(12)).bytes; + bytes.as_slice().try_into().expect("hash should fit") } fn skip_up_check(&mut self) -> bool { @@ -250,4 +405,14 @@ where } #[cfg(test)] -mod test {} +mod test { + use super::*; + + #[test] + fn hex() { + let data = [0x01, 0x02, 0xB1, 0xA1]; + let buffer = &mut [0; 8]; + assert_eq!(format_hex(&data, buffer), "0102b1a1"); + assert_eq!(buffer, b"0102b1a1"); + } +} diff --git a/src/state.rs b/src/state.rs index b54d1eb..9e85855 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,26 +2,34 @@ //! //! Needs cleanup. +pub mod migrate; + +use core::num::NonZeroU32; + use ctap_types::{ - cose::EcdhEsHkdf256PublicKey as CoseEcdhEsHkdf256PublicKey, + ctap2::AttestationFormatsPreference, // 2022-02-27: 10 credentials sizes::MAX_CREDENTIAL_COUNT_IN_LIST, // U8 currently - Bytes, Error, String, }; -use trussed::{ - client, syscall, try_syscall, - types::{self, KeyId, Location, Mechanism}, - Client as TrussedClient, +use littlefs2_core::{path, Path}; +use trussed_core::{ + mechanisms::{Chacha8Poly1305, P256}, + syscall, try_syscall, + types::{KeyId, Location, Mechanism, Message, PathBuf}, + CertificateClient, CryptoClient, FilesystemClient, }; use heapless::binary_heap::{BinaryHeap, Max}; -use littlefs2::path::PathBuf; -use crate::{cbor_serialize_message, credential::Credential, Result}; +use crate::{ + credential::FullCredential, + ctap2::{self, pin::PinProtocolState}, + Result, +}; -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct CachedCredential { pub timestamp: u32, // PathBuf has length 255 + 1, we only need 36 + 1 @@ -41,7 +49,7 @@ impl Ord for CachedCredential { } } -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default)] pub struct CredentialCacheGeneric(BinaryHeap); impl CredentialCacheGeneric { pub fn push(&mut self, item: CachedCredential) { @@ -71,7 +79,7 @@ impl CredentialCacheGeneric { pub type CredentialCache = CredentialCacheGeneric; -#[derive(Clone, Debug, /*uDebug, Eq, PartialEq,*/ serde::Deserialize, serde::Serialize)] +#[derive(Debug)] pub struct State { /// Batch device identity (aaguid, certificate, key). pub identity: Identity, @@ -101,13 +109,13 @@ impl State { } } - pub fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { + pub fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { self.persistent.decrement_retries(trussed)?; self.runtime.decrement_retries(); Ok(()) } - pub fn reset_retries(&mut self, trussed: &mut T) -> Result<()> { + pub fn reset_retries(&mut self, trussed: &mut T) -> Result<()> { self.persistent.reset_retries(trussed)?; self.runtime.reset_retries(); Ok(()) @@ -126,7 +134,7 @@ impl State { } /// Batch device identity (aaguid, certificate, key). -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Identity { // can this be [u8; 16] or need Bytes for serialization? // aaguid: Option>, @@ -134,7 +142,7 @@ pub struct Identity { } pub type Aaguid = [u8; 16]; -pub type Certificate = trussed::types::Message; +pub type Certificate = trussed_core::types::Message; impl Identity { // Attempt to yank out the aaguid of a certificate. @@ -168,7 +176,7 @@ impl Identity { } /// Lookup batch key and certificate, together with AAUGID. - pub fn attestation( + pub fn attestation( &mut self, trussed: &mut T, ) -> (Option<(KeyId, Certificate)>, Aaguid) { @@ -194,22 +202,19 @@ impl Identity { } } -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct CredentialManagementEnumerateRps { - pub remaining: u32, - pub rp_id_hash: Bytes<32>, + pub remaining: NonZeroU32, + pub rp_id_hash: [u8; 32], } -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct CredentialManagementEnumerateCredentials { pub remaining: u32, - pub rp_dir: PathBuf, pub prev_filename: PathBuf, } -#[derive( - Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize, -)] +#[derive(Clone, Debug, Default)] pub struct ActiveGetAssertionData { pub rp_id_hash: [u8; 32], pub client_data_hash: [u8; 32], @@ -217,24 +222,22 @@ pub struct ActiveGetAssertionData { pub up_performed: bool, pub multiple_credentials: bool, pub extensions: Option, + pub attestation_formats_preference: Option, } -#[derive( - Clone, Debug, /*uDebug,*/ Default, /*PartialEq,*/ serde::Deserialize, serde::Serialize, -)] +#[derive(Debug, Default)] pub struct RuntimeState { - key_agreement_key: Option, - pin_token: Option, - // TODO: why is this field not used? - shared_secret: Option, + pin_protocol: Option, consecutive_pin_mismatches: u8, // both of these are a cache for previous Get{Next,}Assertion call cached_credentials: CredentialCache, pub active_get_assertion: Option, - channel: Option, pub cached_rp: Option, pub cached_rk: Option, + + // largeBlob command + pub large_blobs: ctap2::large_blobs::State, } // TODO: Plan towards future extensibility @@ -251,7 +254,7 @@ pub struct RuntimeState { // Currently, this causes the entire authnr to reset state. Maybe it should even reformat disk // // - An alternative would be `heapless::Map`, but I'd prefer something more typed. -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Default)] pub struct PersistentState { #[serde(skip)] // TODO: there has to be a better way than.. this @@ -263,6 +266,7 @@ pub struct PersistentState { key_encryption_key: Option, key_wrapping_key: Option, consecutive_pin_mismatches: u8, + #[serde(with = "serde_bytes")] pin_hash: Option<[u8; 16]>, // Ideally, we'd dogfood a "Monotonic Counter" from trussed. // TODO: Add per-key counters for resident keys. @@ -272,14 +276,9 @@ pub struct PersistentState { impl PersistentState { const RESET_RETRIES: u8 = 8; - const FILENAME: &'static [u8] = b"persistent-state.cbor"; - const MAX_RESIDENT_CREDENTIALS_GUESSTIMATE: u32 = 100; + const FILENAME: &'static Path = path!("persistent-state.cbor"); - pub fn max_resident_credentials_guesstimate(&self) -> u32 { - Self::MAX_RESIDENT_CREDENTIALS_GUESSTIMATE - } - - pub fn load(trussed: &mut T) -> Result { + pub fn load(trussed: &mut T) -> Result { // TODO: add "exists_file" method instead? let result = try_syscall!(trussed.read_file(Location::Internal, PathBuf::from(Self::FILENAME),)) @@ -292,19 +291,20 @@ impl PersistentState { let data = result.unwrap().data; - let result = trussed::cbor_deserialize(&data); - - if result.is_err() { - info!("err deser'ing: {:?}", result.err().unwrap()); + let state: Self = cbor_smol::cbor_deserialize(&data).map_err(|_err| { + info!("err deser'ing: {_err:?}",); info!("{}", hex_str!(&data)); - return Err(Error::Other); - } + Error::Other + })?; + + debug!("Loaded state: {state:#?}"); - result.map_err(|_| Error::Other) + Ok(state) } - pub fn save(&self, trussed: &mut T) -> Result<()> { - let data = crate::cbor_serialize_message(self).unwrap(); + pub fn save(&self, trussed: &mut T) -> Result<()> { + let mut data = Message::new(); + cbor_smol::cbor_serialize_to(self, &mut data).unwrap(); syscall!(trussed.write_file( Location::Internal, @@ -315,7 +315,7 @@ impl PersistentState { Ok(()) } - pub fn reset(&mut self, trussed: &mut T) -> Result<()> { + pub fn reset(&mut self, trussed: &mut T) -> Result<()> { if let Some(key) = self.key_encryption_key { syscall!(trussed.delete(key)); } @@ -330,10 +330,7 @@ impl PersistentState { self.save(trussed) } - pub fn load_if_not_initialised( - &mut self, - trussed: &mut T, - ) { + pub fn load_if_not_initialised(&mut self, trussed: &mut T) { if !self.initialised { match Self::load(trussed) { Ok(previous_self) => { @@ -348,14 +345,14 @@ impl PersistentState { } } - pub fn timestamp(&mut self, trussed: &mut T) -> Result { + pub fn timestamp(&mut self, trussed: &mut T) -> Result { let now = self.timestamp; self.timestamp += 1; self.save(trussed)?; Ok(now) } - pub fn key_encryption_key( + pub fn key_encryption_key( &mut self, trussed: &mut T, ) -> Result { @@ -365,7 +362,7 @@ impl PersistentState { } } - pub fn rotate_key_encryption_key( + pub fn rotate_key_encryption_key( &mut self, trussed: &mut T, ) -> Result { @@ -378,7 +375,7 @@ impl PersistentState { Ok(key) } - pub fn key_wrapping_key( + pub fn key_wrapping_key( &mut self, trussed: &mut T, ) -> Result { @@ -388,7 +385,7 @@ impl PersistentState { } } - pub fn rotate_key_wrapping_key( + pub fn rotate_key_wrapping_key( &mut self, trussed: &mut T, ) -> Result { @@ -407,14 +404,14 @@ impl PersistentState { } pub fn retries(&self) -> u8 { - Self::RESET_RETRIES - self.consecutive_pin_mismatches + Self::RESET_RETRIES.saturating_sub(self.consecutive_pin_mismatches) } pub fn pin_blocked(&self) -> bool { self.consecutive_pin_mismatches >= Self::RESET_RETRIES } - fn reset_retries(&mut self, trussed: &mut T) -> Result<()> { + fn reset_retries(&mut self, trussed: &mut T) -> Result<()> { if self.consecutive_pin_mismatches > 0 { self.consecutive_pin_mismatches = 0; self.save(trussed)?; @@ -422,7 +419,7 @@ impl PersistentState { Ok(()) } - fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { + fn decrement_retries(&mut self, trussed: &mut T) -> Result<()> { // error to call before initialization if self.consecutive_pin_mismatches < Self::RESET_RETRIES { self.consecutive_pin_mismatches += 1; @@ -438,7 +435,7 @@ impl PersistentState { self.pin_hash } - pub fn set_pin_hash( + pub fn set_pin_hash( &mut self, trussed: &mut T, pin_hash: [u8; 16], @@ -482,118 +479,60 @@ impl RuntimeState { self.cached_credentials.push(credential); } - pub fn pop_credential( + pub fn pop_credential( &mut self, trussed: &mut T, - ) -> Option { + ) -> Option { let cached_credential = self.cached_credentials.pop()?; let credential_data = syscall!(trussed.read_file( Location::Internal, - PathBuf::from(cached_credential.path.as_str()), + PathBuf::try_from(cached_credential.path.as_str()).unwrap(), )) .data; - Credential::deserialize(&credential_data).ok() + FullCredential::deserialize(&credential_data).ok() } pub fn remaining_credentials(&self) -> u32 { self.cached_credentials.len() as _ } - pub fn key_agreement_key(&mut self, trussed: &mut T) -> KeyId { - match self.key_agreement_key { - Some(key) => key, - None => self.rotate_key_agreement_key(trussed), - } - } - - pub fn rotate_key_agreement_key(&mut self, trussed: &mut T) -> KeyId { - // TODO: need to rotate pin token? - if let Some(key) = self.key_agreement_key { - syscall!(trussed.delete(key)); - } - if let Some(previous_shared_secret) = self.shared_secret { - syscall!(trussed.delete(previous_shared_secret)); - } - - let key = syscall!(trussed.generate_p256_private_key(Location::Volatile)).key; - self.key_agreement_key = Some(key); - self.shared_secret = None; - key + pub fn pin_protocol(&mut self, trussed: &mut T) -> &mut PinProtocolState { + self.pin_protocol + .get_or_insert_with(|| PinProtocolState::new(trussed)) } - pub fn pin_token(&mut self, trussed: &mut impl client::HmacSha256) -> KeyId { - match self.pin_token { - Some(token) => token, - None => self.rotate_pin_token(trussed), - } - } - - pub fn rotate_pin_token(&mut self, trussed: &mut T) -> KeyId { - // TODO: need to rotate key agreement key? - if let Some(token) = self.pin_token { - syscall!(trussed.delete(token)); - } - let token = syscall!(trussed.generate_secret_key(16, Location::Volatile)).key; - self.pin_token = Some(token); - token - } - - pub fn reset( - &mut self, - trussed: &mut T, - ) { + pub fn reset(&mut self, trussed: &mut T) { // Could use `free_credential_heap`, but since we're deleting everything here, this is quicker. syscall!(trussed.delete_all(Location::Volatile)); self.clear_credential_cache(); self.active_get_assertion = None; - self.rotate_pin_token(trussed); - self.rotate_key_agreement_key(trussed); - } - - pub fn generate_shared_secret( - &mut self, - trussed: &mut T, - platform_key_agreement_key: &CoseEcdhEsHkdf256PublicKey, - ) -> Result { - let private_key = self.key_agreement_key(trussed); - - let serialized_pkak = cbor_serialize_message(platform_key_agreement_key) - .map_err(|_| Error::InvalidParameter)?; - let platform_kak = try_syscall!(trussed.deserialize_p256_key( - &serialized_pkak, - types::KeySerialization::EcdhEsHkdf256, - types::StorageAttributes::new().set_persistence(types::Location::Volatile) - )) - .map_err(|_| Error::InvalidParameter)? - .key; - - let pre_shared_secret = syscall!(trussed.agree( - types::Mechanism::P256, - private_key, - platform_kak, - types::StorageAttributes::new().set_persistence(types::Location::Volatile), - )) - .shared_secret; - syscall!(trussed.delete(platform_kak)); - - if let Some(previous_shared_secret) = self.shared_secret { - syscall!(trussed.delete(previous_shared_secret)); + if let Some(pin_protocol) = self.pin_protocol.take() { + pin_protocol.reset(trussed); } + // to speed up future operations, we already generate the key agreement key + self.pin_protocol = Some(PinProtocolState::new(trussed)); + } +} - let shared_secret = syscall!(trussed.derive_key( - types::Mechanism::Sha256, - pre_shared_secret, - None, - types::StorageAttributes::new().set_persistence(types::Location::Volatile) +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn deser() { + let _state: PersistentState = trussed::cbor_deserialize(&hex!( + " + a5726b65795f656e6372797074696f6e5f6b657950b19a5a2845e5ec71e3 + 2a1b890892376c706b65795f7772617070696e675f6b6579f6781a636f6e + 73656375746976655f70696e5f6d69736d617463686573006870696e5f68 + 6173689018ef1879187c1881181818f0182d18fb186418960718dd185d18 + 3f188c18766974696d657374616d7009 + " )) - .key; - self.shared_secret = Some(shared_secret); - - syscall!(trussed.delete(pre_shared_secret)); - - Ok(shared_secret) + .unwrap(); } } diff --git a/src/state/migrate.rs b/src/state/migrate.rs new file mode 100644 index 0000000..d33d0c9 --- /dev/null +++ b/src/state/migrate.rs @@ -0,0 +1,267 @@ +use littlefs2_core::{path, DirEntry, DynFilesystem, Error, Path, PathBuf}; + +fn ignore_does_not_exists(error: Error) -> Result<(), Error> { + if matches!(error, Error::NO_SUCH_ENTRY) { + return Ok(()); + } + Err(error) +} + +/// Migration function, to be used with trussed-staging's `migrate` management syscall +/// +/// `base_path` must be the base of the file directory of the fido app (often `/fido/dat`) +pub fn migrate_no_rp_dir(fs: &dyn DynFilesystem, base_path: &Path) -> Result<(), Error> { + let rk_dir = base_path.join(path!("rk")); + + fs.read_dir_and_then(&rk_dir, &mut |dir| migrate_rk_dir(fs, &rk_dir, dir)) + .or_else(ignore_does_not_exists) +} + +fn migrate_rk_dir( + fs: &dyn DynFilesystem, + rk_dir: &Path, + dir: &mut dyn Iterator>, +) -> Result<(), Error> { + for rp in dir.skip(2) { + let rp = rp?; + if rp.metadata().is_file() { + continue; + } + + migrate_rp_dir(fs, rk_dir, rp.path())?; + } + Ok(()) +} + +fn migrate_rp_dir(fs: &dyn DynFilesystem, rk_dir: &Path, rp_path: &Path) -> Result<(), Error> { + let rp_id_hex = rp_path.file_name().unwrap().as_str(); + debug_assert_eq!(rp_id_hex.len(), 16); + + fs.read_dir_and_then(rp_path, &mut |rp_dir| { + for file in rp_dir.skip(2) { + let file = file?; + let cred_id_hex = file.file_name().as_str(); + let mut buf = [0; 33]; + buf[0..16].copy_from_slice(rp_id_hex.as_bytes()); + buf[16] = b'.'; + buf[17..].copy_from_slice(cred_id_hex.as_bytes()); + fs.rename( + file.path(), + &rk_dir.join(&PathBuf::try_from(buf.as_slice()).unwrap()), + )?; + } + Ok(()) + })?; + + fs.remove_dir(rp_path)?; + + Ok(()) +} + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod tests { + use admin_app::migrations::test_utils::{test_migration_one, FsValues}; + + use super::*; + + const FIDO_DAT_DIR_BEFORE: FsValues = FsValues::Dir(&[ + (path!("persistent-state.cbor"), FsValues::File(137)), + ( + path!("rk"), + FsValues::Dir(&[( + path!("74a6ea9213c99c2f"), + FsValues::Dir(&[ + (path!("038dfc6165b78be9"), FsValues::File(128)), + (path!("1ecbbfbed8992287"), FsValues::File(122)), + (path!("7c24db95312eac56"), FsValues::File(122)), + (path!("978cba44dfe39871"), FsValues::File(155)), + (path!("ac889a0433749726"), FsValues::File(138)), + ]), + )]), + ), + ]); + + const FIDO_DAT_DIR_AFTER: FsValues = FsValues::Dir(&[ + (path!("persistent-state.cbor"), FsValues::File(137)), + ( + path!("rk"), + FsValues::Dir(&[ + ( + path!("74a6ea9213c99c2f.038dfc6165b78be9"), + FsValues::File(128), + ), + ( + path!("74a6ea9213c99c2f.1ecbbfbed8992287"), + FsValues::File(122), + ), + ( + path!("74a6ea9213c99c2f.7c24db95312eac56"), + FsValues::File(122), + ), + ( + path!("74a6ea9213c99c2f.978cba44dfe39871"), + FsValues::File(155), + ), + ( + path!("74a6ea9213c99c2f.ac889a0433749726"), + FsValues::File(138), + ), + ]), + ), + ]); + + const FIDO_SEC_DIR: FsValues = FsValues::Dir(&[ + ( + path!("069386c3c735689061ac51b8bca9f160"), + FsValues::File(48), + ), + ( + path!("233d86bfc2f196ff7c108cf23a282bd5"), + FsValues::File(36), + ), + ( + path!("2bdef14a0e18d28191162f8c1599d598"), + FsValues::File(36), + ), + ( + path!("3efe6394c20aa8128e27b376e226a58b"), + FsValues::File(36), + ), + ( + path!("4711aa79b4834ef8e551f80e523ba8d2"), + FsValues::File(36), + ), + ( + path!("b43bf8b7897087b7195b8ac53dcb5f11"), + FsValues::File(36), + ), + ]); + + #[test] + fn migration_no_auth() { + const TEST_VALUES_BEFORE: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_BEFORE), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + + const TEST_VALUES_AFTER: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_AFTER), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + + test_migration_one(&TEST_VALUES_BEFORE, &TEST_VALUES_AFTER, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } + + #[test] + fn migration_auth() { + const AUTH_SECRETS_DIR: (&Path, FsValues) = ( + path!("secrets"), + FsValues::Dir(&[( + path!("backend-auth"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[ + (path!("application_salt"), FsValues::File(16)), + (path!("pin.00"), FsValues::File(118)), + ]), + )]), + )]), + ); + + const BACKEND_DIR: (&Path, FsValues) = ( + path!("backend-auth"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("salt"), FsValues::File(16))]), + )]), + ); + + const TRUSSED_DIR: (&Path, FsValues) = ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ); + + const TEST_BEFORE: FsValues = FsValues::Dir(&[ + BACKEND_DIR, + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_BEFORE), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + AUTH_SECRETS_DIR, + TRUSSED_DIR, + ]); + + const TEST_AFTER: FsValues = FsValues::Dir(&[ + BACKEND_DIR, + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_AFTER), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + AUTH_SECRETS_DIR, + TRUSSED_DIR, + ]); + + test_migration_one(&TEST_BEFORE, &TEST_AFTER, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } + + #[test] + fn migration_empty() { + const TEST_VALUES: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FsValues::Dir(&[])), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + test_migration_one(&TEST_VALUES, &TEST_VALUES, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } +} diff --git a/tests/authenticator/mod.rs b/tests/authenticator/mod.rs new file mode 100644 index 0000000..331b07f --- /dev/null +++ b/tests/authenticator/mod.rs @@ -0,0 +1,234 @@ +use sha2::{Digest as _, Sha256}; + +use super::{ + virt::{Ctap2, Ctap2Error}, + webauthn::{ + AttStmtFormat, ClientPin, CredentialData, CredentialManagement, CredentialManagementParams, + KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, + PubKeyCredParam, PublicKey, Rp, SharedSecret, User, + }, +}; + +pub struct Authenticator<'a, P: PinState> { + ctap2: Ctap2<'a>, + key_agreement_key: KeyAgreementKey, + shared_secret: Option, + pin: P, +} + +impl<'a> Authenticator<'a, NoPin> { + pub fn new(ctap2: Ctap2<'a>) -> Self { + Self { + ctap2, + key_agreement_key: KeyAgreementKey::generate(), + shared_secret: None, + pin: NoPin, + } + } + + pub fn set_pin(mut self, pin: &[u8]) -> Authenticator<'a, Pin> { + let shared_secret = self.shared_secret(); + let mut padded_pin = [0; 64]; + padded_pin[..pin.len()].copy_from_slice(pin); + let pin_enc = shared_secret.encrypt(&padded_pin); + let pin_auth = shared_secret.authenticate(&pin_enc); + let mut request = ClientPin::new(2, 3); + request.key_agreement = Some(self.key_agreement_key.public_key()); + request.new_pin_enc = Some(pin_enc); + request.pin_auth = Some(pin_auth); + self.ctap2.exec(request).unwrap(); + Authenticator { + ctap2: self.ctap2, + key_agreement_key: self.key_agreement_key, + shared_secret: self.shared_secret, + pin: Pin(pin.into()), + } + } +} + +impl Authenticator<'_, P> { + fn shared_secret(&mut self) -> &SharedSecret { + self.shared_secret.get_or_insert_with(|| { + let reply = self.ctap2.exec(ClientPin::new(2, 2)).unwrap(); + let authenticator_key_agreement: PublicKey = reply.key_agreement.unwrap().into(); + self.key_agreement_key + .shared_secret(&authenticator_key_agreement) + }) + } +} + +impl Authenticator<'_, Pin> { + fn get_pin_token(&mut self, permissions: u8, rp_id: Option) -> PinToken { + let mut hasher = Sha256::new(); + hasher.update(&self.pin.0); + let pin_hash = hasher.finalize(); + let pin_hash_enc = self.shared_secret().encrypt(&pin_hash[..16]); + let mut request = ClientPin::new(2, 9); + request.key_agreement = Some(self.key_agreement_key.public_key()); + request.pin_hash_enc = Some(pin_hash_enc); + request.permissions = Some(permissions); + request.rp_id = rp_id; + let reply = self.ctap2.exec(request).unwrap(); + let encrypted_pin_token = reply.pin_token.as_ref().unwrap().as_bytes().unwrap(); + self.shared_secret().decrypt_pin_token(encrypted_pin_token) + } + + pub fn make_credential(&mut self, rp: Rp, user: User) -> Result { + let pin_token = self.get_pin_token(0x01, None); + // TODO: client data + let client_data_hash = b""; + let pin_auth = pin_token.authenticate(client_data_hash); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + request.options = Some(MakeCredentialOptions::default().rk(true)); + request.pin_auth = Some(pin_auth); + request.pin_protocol = Some(2); + let reply = self.ctap2.exec(request)?; + assert_eq!( + reply.auth_data.flags & 0b1, + 0b1, + "up flag not set in auth_data: 0b{:b}", + reply.auth_data.flags + ); + assert_eq!( + reply.auth_data.flags & 0b100, + 0b100, + "uv flag not set in auth_data: 0b{:b}", + reply.auth_data.flags + ); + let format = AttStmtFormat::Packed; + assert_eq!(reply.fmt, format.as_str()); + reply.att_stmt.unwrap().validate(format, &reply.auth_data); + Ok(reply.auth_data.credential.unwrap()) + } + + fn credential_management(&mut self, subcommand: u8) -> CredentialManagement { + let pin_token = self.get_pin_token(0x04, None); + let pin_auth = pin_token.authenticate(&[subcommand]); + CredentialManagement { + subcommand, + subcommand_params: None, + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + } + } + + pub fn credentials_metadata(&mut self) -> CredentialsMetadata { + let request = self.credential_management(0x01); + let reply = self.ctap2.exec(request).unwrap(); + CredentialsMetadata { + existing: reply.existing_resident_credentials_count.unwrap(), + remaining: reply + .max_possible_remaining_resident_credentials_count + .unwrap(), + } + } + + pub fn list_rps(&mut self) -> Vec { + let request = self.credential_management(0x02); + let reply = self.ctap2.exec(request).unwrap(); + // TODO: check RP ID hash + let total_rps = reply.total_rps.unwrap(); + let mut rps = Vec::with_capacity(total_rps); + rps.push(reply.rp.unwrap().into()); + + for _ in 1..total_rps { + let request = CredentialManagement::new(0x03); + let reply = self.ctap2.exec(request).unwrap(); + // TODO: check RP ID hash + rps.push(reply.rp.unwrap().into()); + } + + rps + } + + pub fn list_credentials(&mut self, rp_id: &str) -> Vec<(User, PubKeyCredDescriptor)> { + let rp_id_hash = rp_id_hash(rp_id); + let pin_token = self.get_pin_token(0x04, Some(rp_id.to_owned())); + let params = CredentialManagementParams { + rp_id_hash: Some(rp_id_hash.to_vec()), + ..Default::default() + }; + let mut pin_auth_param = vec![0x04]; + pin_auth_param.extend_from_slice(¶ms.serialized()); + let pin_auth = pin_token.authenticate(&pin_auth_param); + let request = CredentialManagement { + subcommand: 0x04, + subcommand_params: Some(params), + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + }; + let reply = self.ctap2.exec(request).unwrap(); + // TODO: check other fields + let total_credentials = reply.total_credentials.unwrap(); + let mut credentials = Vec::with_capacity(total_credentials); + credentials.push((reply.user.unwrap().into(), reply.credential_id.unwrap())); + + for _ in 1..total_credentials { + let request = CredentialManagement::new(0x05); + let reply = self.ctap2.exec(request).unwrap(); + // TODO: check other fields + credentials.push((reply.user.unwrap().into(), reply.credential_id.unwrap())); + } + + credentials + } + + pub fn delete_credential(&mut self, id: &[u8]) { + let pin_token = self.get_pin_token(0x04, None); + let params = CredentialManagementParams { + credential_id: Some(PubKeyCredDescriptor::new("public-key", id)), + ..Default::default() + }; + let mut pin_auth_param = vec![0x06]; + pin_auth_param.extend_from_slice(¶ms.serialized()); + let pin_auth = pin_token.authenticate(&pin_auth_param); + let request = CredentialManagement { + subcommand: 0x06, + subcommand_params: Some(params), + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + }; + self.ctap2.exec(request).unwrap(); + } + + pub fn update_user(&mut self, id: &[u8], user: User) -> Result<(), Ctap2Error> { + let pin_token = self.get_pin_token(0x04, None); + let params = CredentialManagementParams { + credential_id: Some(PubKeyCredDescriptor::new("public-key", id)), + user: Some(user), + ..Default::default() + }; + let mut pin_auth_param = vec![0x07]; + pin_auth_param.extend_from_slice(¶ms.serialized()); + let pin_auth = pin_token.authenticate(&pin_auth_param); + let request = CredentialManagement { + subcommand: 0x07, + subcommand_params: Some(params), + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + }; + self.ctap2.exec(request).map(|_| ()) + } +} + +pub struct CredentialsMetadata { + pub existing: usize, + pub remaining: usize, +} + +pub trait PinState {} + +pub struct NoPin; + +impl PinState for NoPin {} + +pub struct Pin(Vec); + +impl PinState for Pin {} + +fn rp_id_hash(rp_id: &str) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(rp_id); + hasher.finalize().into() +} diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..9e340ca --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,1142 @@ +#![cfg(feature = "dispatch")] + +pub mod fs; +pub mod virt; +pub mod webauthn; + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Debug, +}; + +use ciborium::Value; +use hex_literal::hex; +use itertools::iproduct; +use rand::RngCore as _; + +use fs::list_fs; +use virt::{Ctap2, Ctap2Error, Options}; +use webauthn::{ + exhaustive_struct, AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, + Exhaustive, GetAssertion, GetAssertionExtensionsInput, GetAssertionOptions, GetInfo, + GetNextAssertion, HmacSecretInput, KeyAgreementKey, MakeCredential, + MakeCredentialExtensionsInput, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, + PubKeyCredParam, PublicKey, Rp, SharedSecret, User, +}; + +trait Test: Debug { + fn test(&self); + + fn run(&self) { + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{self:#?}"); + println!(); + + self.test(); + } + + fn run_all() + where + Self: Exhaustive, + { + for test in Self::iter_exhaustive() { + test.run(); + } + } +} + +#[test] +fn test_ping() { + virt::run_ctaphid(|device| { + device.ping(&[0xf1, 0xd0]).unwrap(); + }); +} + +#[test] +fn test_get_info() { + let options = Options { + inspect_ifs: Some(Box::new(|ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.assert_empty(); + })), + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { + let reply = device.exec(GetInfo).unwrap(); + assert!(reply.versions.contains(&"FIDO_2_0".to_owned())); + assert!(reply.versions.contains(&"FIDO_2_1".to_owned())); + assert_eq!( + reply.aaguid.as_bytes().unwrap(), + &hex!("8BC5496807B14D5FB249607F5D527DA2") + ); + assert_eq!(reply.pin_protocols, Some(vec![2, 1])); + assert_eq!( + reply.attestation_formats, + Some(vec!["packed".to_owned(), "none".to_owned()]) + ); + }); +} + +fn get_shared_secret(device: &Ctap2, platform_key_agreement: &KeyAgreementKey) -> SharedSecret { + let reply = device.exec(ClientPin::new(2, 2)).unwrap(); + let authenticator_key_agreement: PublicKey = reply.key_agreement.unwrap().into(); + platform_key_agreement.shared_secret(&authenticator_key_agreement) +} + +fn set_pin( + device: &Ctap2, + key_agreement_key: &KeyAgreementKey, + shared_secret: &SharedSecret, + pin: &[u8], +) -> Result<(), Ctap2Error> { + let mut padded_pin = [0; 64]; + padded_pin[..pin.len()].copy_from_slice(pin); + let pin_enc = shared_secret.encrypt(&padded_pin); + let pin_auth = shared_secret.authenticate(&pin_enc); + let mut request = ClientPin::new(2, 3); + request.key_agreement = Some(key_agreement_key.public_key()); + request.new_pin_enc = Some(pin_enc); + request.pin_auth = Some(pin_auth); + device.exec(request).map(|_| ()) +} + +#[test] +fn test_set_pin() { + let key_agreement_key = KeyAgreementKey::generate(); + let options = Options { + inspect_ifs: Some(Box::new(|ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + files.assert_empty(); + })), + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { + let reply = device.exec(GetInfo).unwrap(); + let options = reply.options.unwrap(); + assert_eq!(options.get("clientPin"), Some(&Value::from(false))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, b"123456").unwrap(); + + let reply = device.exec(GetInfo).unwrap(); + let options = reply.options.unwrap(); + assert_eq!(options.get("clientPin"), Some(&Value::from(true))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = set_pin(&device, &key_agreement_key, &shared_secret, b"123456"); + // TODO: review error code + assert_eq!(result, Err(Ctap2Error(0x30))); + + let reply = device.exec(GetInfo).unwrap(); + let options = reply.options.unwrap(); + assert_eq!(options.get("clientPin"), Some(&Value::from(true))); + }) +} + +fn get_pin_hash_enc(shared_secret: &SharedSecret, pin: &[u8]) -> Vec { + use sha2::{Digest as _, Sha256}; + + let mut hasher = Sha256::new(); + hasher.update(pin); + let pin_hash = hasher.finalize(); + shared_secret.encrypt(&pin_hash[..16]) +} + +fn change_pin( + device: &Ctap2, + key_agreement_key: &KeyAgreementKey, + shared_secret: &SharedSecret, + old_pin: &[u8], + new_pin: &[u8], +) -> Result<(), Ctap2Error> { + let old_pin_hash_enc = get_pin_hash_enc(shared_secret, old_pin); + let mut padded_new_pin = [0; 64]; + padded_new_pin[..new_pin.len()].copy_from_slice(new_pin); + let new_pin_enc = shared_secret.encrypt(&padded_new_pin); + let mut pin_auth_data = Vec::new(); + pin_auth_data.extend_from_slice(&new_pin_enc); + pin_auth_data.extend_from_slice(&old_pin_hash_enc); + let pin_auth = shared_secret.authenticate(&pin_auth_data); + let mut request = ClientPin::new(2, 4); + request.key_agreement = Some(key_agreement_key.public_key()); + request.pin_hash_enc = Some(old_pin_hash_enc); + request.new_pin_enc = Some(new_pin_enc); + request.pin_auth = Some(pin_auth); + device.exec(request).map(|_| ()) +} + +#[test] +fn test_change_pin() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin1 = b"123456"; + let pin2 = b"654321"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = change_pin(&device, &key_agreement_key, &shared_secret, pin1, pin2); + // TODO: review error code + assert_eq!(result, Err(Ctap2Error(0x35))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin1).unwrap(); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + change_pin(&device, &key_agreement_key, &shared_secret, pin1, pin2).unwrap(); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = change_pin(&device, &key_agreement_key, &shared_secret, pin1, pin2); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin1, + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin2, + 0x01, + None, + ) + .unwrap(); + }) +} + +fn get_pin_token( + device: &Ctap2, + key_agreement_key: &KeyAgreementKey, + shared_secret: &SharedSecret, + pin: &[u8], + permissions: u8, + rp_id: Option, +) -> Result { + let pin_hash_enc = get_pin_hash_enc(shared_secret, pin); + let mut request = ClientPin::new(2, 9); + request.key_agreement = Some(key_agreement_key.public_key()); + request.pin_hash_enc = Some(pin_hash_enc); + request.permissions = Some(permissions); + request.rp_id = rp_id; + let reply = device.exec(request)?; + let encrypted_pin_token = reply.pin_token.as_ref().unwrap().as_bytes().unwrap(); + Ok(shared_secret.decrypt_pin_token(encrypted_pin_token)) +} + +fn get_pin_retries(device: &Ctap2) -> u8 { + let reply = device.exec(ClientPin::new(2, 1)).unwrap(); + reply.pin_retries.unwrap() +} + +#[test] +fn test_get_pin_retries() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + assert_eq!(get_pin_retries(&device), 8); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + assert_eq!(get_pin_retries(&device), 8); + + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + assert_eq!(get_pin_retries(&device), 8); + + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + assert_eq!(get_pin_retries(&device), 7); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + assert_eq!(get_pin_retries(&device), 6); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x34))); + assert_eq!(get_pin_retries(&device), 5); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x34))); + assert_eq!(get_pin_retries(&device), 5); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + assert_eq!(result, Err(Ctap2Error(0x34))); + assert_eq!(get_pin_retries(&device), 5); + }) +} + +#[test] +fn test_get_pin_retries_reset() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + assert_eq!(get_pin_retries(&device), 7); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + assert_eq!(get_pin_retries(&device), 8); + }) +} + +#[test] +fn test_get_pin_token() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + }) +} + +#[test] +fn test_get_pin_token_invalid_pin() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + }) +} + +#[test] +fn test_get_pin_token_invalid_shared_secret() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + // presenting an invalid PIN resets the shared secret so even the correct PIN is not accepted + let result = get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + assert_eq!(result, Err(Ctap2Error(0x31))); + + // requesting a new shared secret fixes the authentication + let shared_secret = get_shared_secret(&device, &key_agreement_key); + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None).unwrap(); + }) +} + +// TODO: simulate reboot and test that PIN_AUTH_BLOCKED is reset +// TODO: simulate reboot and test PIN_BLOCKED + +#[test] +fn test_get_pin_token_pin_auth_blocked() { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x31))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + assert_eq!(result, Err(Ctap2Error(0x34))); + + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None); + assert_eq!(result, Err(Ctap2Error(0x34))); + }) +} + +#[test] +fn test_get_pin_token_no_pin() { + let key_agreement_key = KeyAgreementKey::generate(); + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + let result = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + b"654321", + 0x01, + None, + ); + // TODO: review if this is the correct error code + assert_eq!(result, Err(Ctap2Error(0x35))); + }) +} + +#[derive(Clone, Copy, Debug)] +enum RequestPinToken { + InvalidPermissions, + InvalidRpId, + NoRpId, + ValidRpId, +} + +impl RequestPinToken { + fn permissions(&self, valid: u8, invalid: u8) -> u8 { + if matches!(self, Self::InvalidPermissions) { + invalid + } else { + valid + } + } + + fn rp_id(&self, valid: &str, invalid: &str) -> Option { + match self { + Self::InvalidPermissions => None, + Self::InvalidRpId => Some(invalid.to_owned()), + Self::NoRpId => None, + Self::ValidRpId => Some(valid.to_owned()), + } + } +} + +impl Exhaustive for RequestPinToken { + fn iter_exhaustive() -> impl Iterator + Clone { + [ + Self::InvalidPermissions, + Self::InvalidRpId, + Self::NoRpId, + Self::ValidRpId, + ] + .into_iter() + } +} + +#[derive(Clone, Copy, Debug)] +enum AttestationFormatsPreference { + Empty, + None, + Packed, + NonePacked, + PackedNone, + OtherNonePacked, + MultiOtherNonePacked, +} + +impl AttestationFormatsPreference { + fn format(&self) -> Option { + match self { + Self::Empty | Self::Packed | Self::PackedNone => Some(AttStmtFormat::Packed), + Self::NonePacked | Self::OtherNonePacked | Self::MultiOtherNonePacked => { + Some(AttStmtFormat::None) + } + Self::None => None, + } + } +} + +impl From for Vec<&'static str> { + fn from(preference: AttestationFormatsPreference) -> Self { + let mut vec = Vec::new(); + match preference { + AttestationFormatsPreference::Empty => {} + AttestationFormatsPreference::None => { + vec.push("none"); + } + AttestationFormatsPreference::Packed => { + vec.push("packed"); + } + AttestationFormatsPreference::NonePacked => { + vec.push("none"); + vec.push("packed"); + } + AttestationFormatsPreference::PackedNone => { + vec.push("packed"); + vec.push("none"); + } + AttestationFormatsPreference::OtherNonePacked => { + vec.push("tpm"); + vec.push("none"); + vec.push("packed"); + } + AttestationFormatsPreference::MultiOtherNonePacked => { + vec.resize(100, "tpm"); + vec.push("none"); + vec.push("packed"); + } + } + vec + } +} + +impl Exhaustive for AttestationFormatsPreference { + fn iter_exhaustive() -> impl Iterator + Clone { + [ + Self::Empty, + Self::None, + Self::Packed, + Self::NonePacked, + Self::PackedNone, + Self::OtherNonePacked, + Self::MultiOtherNonePacked, + ] + .into_iter() + } +} + +#[derive(Clone, Copy, Debug)] +enum PinAuth { + NoPin, + PinNoToken, + PinToken(RequestPinToken), +} + +impl Exhaustive for PinAuth { + fn iter_exhaustive() -> impl Iterator + Clone { + [Self::NoPin, Self::PinNoToken] + .into_iter() + .chain(RequestPinToken::iter_exhaustive().map(Self::PinToken)) + } +} + +#[derive(Clone, Debug)] +struct TestMakeCredential { + pin_auth: PinAuth, + options: Option, + valid_pub_key_alg: bool, + attestation_formats_preference: Option, + hmac_secret: bool, +} + +impl TestMakeCredential { + fn expected_error(&self) -> Option { + if let Some(options) = self.options { + // TODO: this is the current implementation, but the spec allows Some(true) + if options.up.is_some() { + return Some(0x2c); + } + if !matches!(self.pin_auth, PinAuth::PinToken(_)) && options.uv == Some(true) { + return Some(0x2c); + } + if matches!(self.pin_auth, PinAuth::PinNoToken) && options.rk == Some(true) { + return Some(0x36); + } + } + if let PinAuth::PinToken( + RequestPinToken::InvalidPermissions | RequestPinToken::InvalidRpId, + ) = &self.pin_auth + { + return Some(0x33); + } + if !self.valid_pub_key_alg { + return Some(0x26); + } + None + } +} + +impl Test for TestMakeCredential { + fn test(&self) { + let pin = b"123456"; + let rp_id = "example.com"; + let invalid_rp_id = "test.com"; + // TODO: client data + let client_data_hash = b""; + + let is_rk = self + .options + .and_then(|options| options.rk) + .unwrap_or_default(); + let is_successful = self.expected_error().is_none(); + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.try_remove_state(); + let n = files.try_remove_keys(); + assert!(n <= 2, "n: {n}, files: {files:?}"); + if is_rk && is_successful { + assert_eq!(files.try_remove_rks(), 1, "{files:?}"); + } + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let mut pin_auth = None; + match &self.pin_auth { + PinAuth::NoPin => {} + PinAuth::PinNoToken => { + let key_agreement_key = KeyAgreementKey::generate(); + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + } + PinAuth::PinToken(pin_token) => { + let key_agreement_key = KeyAgreementKey::generate(); + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + let pin_token = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin, + pin_token.permissions(0x01, 0x04), + pin_token.rp_id(rp_id, invalid_rp_id), + ) + .unwrap(); + pin_auth = Some(pin_token.authenticate(client_data_hash)); + } + } + + let rp = Rp::new(rp_id); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_alg = if self.valid_pub_key_alg { -7 } else { -11 }; + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", pub_key_alg)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + request.options = self.options; + if let Some(pin_auth) = pin_auth { + request.pin_auth = Some(pin_auth); + request.pin_protocol = Some(2); + } + request.attestation_formats_preference = + self.attestation_formats_preference.map(From::from); + // TODO: test other extensions and permutations + if self.hmac_secret { + request.extensions = Some(MakeCredentialExtensionsInput { + hmac_secret: Some(true), + ..Default::default() + }); + } + + let result = device.exec(request); + if let Some(error) = self.expected_error() { + assert_eq!(result, Err(Ctap2Error(error))); + } else { + let reply = result.unwrap(); + assert!(reply.auth_data.credential.is_some()); + assert!(reply.auth_data.up_flag()); + // TODO: review conditions + assert_eq!( + reply.auth_data.uv_flag(), + self.options.and_then(|options| options.uv).unwrap_or(false) + || matches!(self.pin_auth, PinAuth::PinToken(_)) + ); + assert!(reply.auth_data.at_flag()); + assert_eq!(reply.auth_data.ed_flag(), self.hmac_secret); + let format = self + .attestation_formats_preference + .unwrap_or(AttestationFormatsPreference::Packed) + .format(); + if let Some(format) = format { + assert_eq!(reply.fmt, format.as_str()); + reply.att_stmt.unwrap().validate(format, &reply.auth_data); + } else { + assert_eq!(reply.fmt, AttStmtFormat::None.as_str()); + assert!(reply.att_stmt.is_none()); + } + if self.hmac_secret { + let extensions = reply.auth_data.extensions.unwrap(); + assert_eq!(extensions.get("hmac-secret"), Some(&Value::from(true))); + } else { + assert_eq!(reply.auth_data.extensions, None); + } + } + }); + } +} + +impl Exhaustive for TestMakeCredential { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + pin_auth: PinAuth, + options: Option, + valid_pub_key_alg: bool, + attestation_formats_preference: Option, + hmac_secret: bool, + } + } +} + +#[test] +fn test_make_credential() { + TestMakeCredential::run_all(); +} + +#[derive(Clone, Debug)] +struct TestGetAssertion { + rk: bool, + allow_list: bool, + options: Option, + mc_extensions: Option, + ga_hmac_secret: bool, + ga_third_party_payment: Option, +} + +impl TestGetAssertion { + fn expected_error(&self) -> Option { + if let Some(options) = self.options { + if options.uv == Some(true) { + return Some(0x2c); + } + } + if !self.rk && !self.allow_list { + return Some(0x2e); + } + if let Some(options) = self.options { + if options.up == Some(false) && self.ga_hmac_secret { + return Some(0x2b); + } + } + None + } +} + +impl Test for TestGetAssertion { + fn test(&self) { + let rp_id = "example.com"; + // TODO: client data + let client_data_hash = &[0; 32]; + + // TODO: test with PIN + virt::run_ctap2(|device| { + let key_agreement_key = KeyAgreementKey::generate(); + let shared_secret = get_shared_secret(&device, &key_agreement_key); + + let rp = Rp::new(rp_id); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + if self.rk { + request.options = Some(MakeCredentialOptions::default().rk(true)); + } + request.extensions = self.mc_extensions; + let response = device.exec(request).unwrap(); + let credential = response.auth_data.credential.unwrap(); + + let mut request = GetAssertion::new(rp_id, client_data_hash); + // TODO: test more cases: + // - multiple credentials in allow list + // - invalid allow list + if self.allow_list { + request.allow_list = Some(vec![PubKeyCredDescriptor::new( + "public-key", + credential.id.clone(), + )]); + } + if self.ga_hmac_secret || self.ga_third_party_payment.is_some() { + let mut extensions = GetAssertionExtensionsInput { + third_party_payment: self.ga_third_party_payment, + ..Default::default() + }; + if self.ga_hmac_secret { + // TODO: We always set the last byte to 0xff to work around the zero padding + // currently used by trussed. + let mut salt = [0xff; 32]; + rand::thread_rng().fill_bytes(&mut salt[..31]); + let salt_enc = shared_secret.encrypt(&salt); + let salt_auth = shared_secret.authenticate(&salt_enc); + extensions.hmac_secret = Some(HmacSecretInput { + key_agreement: key_agreement_key.public_key(), + salt_enc, + salt_auth, + pin_protocol: Some(2), + }); + } + request.extensions = Some(extensions); + } + request.options = self.options; + let result = device.exec(request); + if let Some(error) = self.expected_error() { + assert_eq!(result, Err(Ctap2Error(error))); + return; + } + let has_extensions = + self.ga_hmac_secret || self.ga_third_party_payment.unwrap_or_default(); + let response = result.unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.credential.id, credential.id); + assert_eq!(response.auth_data.credential, None); + assert_eq!( + response.auth_data.up_flag(), + self.options.and_then(|options| options.up).unwrap_or(true) + ); + assert!(!response.auth_data.uv_flag()); + assert!(!response.auth_data.at_flag()); + assert_eq!(response.auth_data.ed_flag(), has_extensions,); + assert_eq!(response.number_of_credentials, None); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + if has_extensions { + let extensions = response.auth_data.extensions.unwrap(); + + if self.ga_hmac_secret { + let hmac_secret = extensions.get("hmac-secret").unwrap().as_bytes().unwrap(); + let output = shared_secret.decrypt(hmac_secret); + assert_eq!(output.len(), 32); + } + + if self.ga_third_party_payment.unwrap_or_default() { + let expected = self + .mc_extensions + .and_then(|e| e.third_party_payment) + .unwrap_or_default(); + assert_eq!( + extensions.get("thirdPartyPayment"), + Some(&Value::from(expected)) + ); + } + } else { + assert!(response.auth_data.extensions.is_none()); + } + }); + } +} + +impl Exhaustive for TestGetAssertion { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + rk: bool, + allow_list: bool, + options: Option, + mc_extensions: Option, + ga_hmac_secret: bool, + ga_third_party_payment: Option, + } + } +} + +#[test] +fn test_get_assertion() { + TestGetAssertion::run_all(); +} + +fn run_test_get_next_assertion(device: &Ctap2) { + let rp_id = "example.com"; + // TODO: client data + let client_data_hash = &[0; 32]; + + let rp = Rp::new(rp_id); + let users = vec![User::new(b"id1"), User::new(b"id2"), User::new(b"id3")]; + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + // TODO: test non-discoverable credentials and with allow list + let mut credentials: Vec<_> = users + .into_iter() + .map(|user| { + let mut request = MakeCredential::new( + client_data_hash, + rp.clone(), + user.clone(), + pub_key_cred_params.clone(), + ); + request.options = Some(MakeCredentialOptions::default().rk(true)); + let response = device.exec(request).unwrap(); + response.auth_data.credential.unwrap() + }) + .collect(); + + let credential_ids: BTreeSet<_> = credentials + .iter() + .map(|credential| &credential.id) + .collect(); + assert_eq!(credential_ids.len(), credentials.len()); + + let request = GetAssertion::new(rp_id, client_data_hash); + let response = device.exec(request).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.auth_data.credential, None); + assert_eq!(response.number_of_credentials, Some(credentials.len())); + let i = credentials + .iter() + .position(|credential| credential.id == response.credential.id) + .unwrap(); + let credential = credentials.remove(i); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + assert!(response.auth_data.extensions.is_none()); + + let response = device.exec(GetNextAssertion).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.auth_data.credential, None); + // TODO: fix number_of_credentials + // assert_eq!(response.number_of_credentials, Some(credentials.len())); + assert_eq!(response.number_of_credentials, None); + let i = credentials + .iter() + .position(|credential| credential.id == response.credential.id) + .unwrap(); + let credential = credentials.remove(i); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + assert!(response.auth_data.extensions.is_none()); + + let response = device.exec(GetNextAssertion).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.auth_data.credential, None); + assert_eq!(response.number_of_credentials, None); + let i = credentials + .iter() + .position(|credential| credential.id == response.credential.id) + .unwrap(); + let credential = credentials.remove(i); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + assert!(response.auth_data.extensions.is_none()); + + assert_eq!(credentials, Vec::new()); + + let error = device.exec(GetNextAssertion).unwrap_err(); + assert_eq!(error, Ctap2Error(0x30)); +} + +#[test] +fn test_get_next_assertion() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 4); + assert_eq!(files.try_remove_rks(), 3); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + run_test_get_next_assertion(&device); + }); +} + +#[test] +fn test_get_next_assertion_multi_rp() { + let client_data_hash = b""; + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 10); + assert_eq!(files.try_remove_rks(), 9); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + for rp in ["test.com", "something.dev", "else.foobar"] { + for user in [b"john.doe", b"jane.doe"] { + let mut request = MakeCredential::new( + client_data_hash, + Rp::new(rp), + User::new(user), + pub_key_cred_params.clone(), + ); + request.options = Some(MakeCredentialOptions::default().rk(true)); + device.exec(request).unwrap(); + } + } + run_test_get_next_assertion(&device); + }); +} + +#[derive(Clone, Debug)] +struct TestListCredentials { + pin_token_rp_id: bool, + third_party_payment: Option, +} + +impl Test for TestListCredentials { + fn test(&self) { + let key_agreement_key = KeyAgreementKey::generate(); + let pin = b"123456"; + let rp_id = "example.com"; + let user_id = b"id123"; + virt::run_ctap2(|device| { + let shared_secret = get_shared_secret(&device, &key_agreement_key); + set_pin(&device, &key_agreement_key, &shared_secret, pin).unwrap(); + + let pin_token = + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x01, None) + .unwrap(); + // TODO: client data + let client_data_hash = b""; + let pin_auth = pin_token.authenticate(client_data_hash); + + let rp = Rp::new(rp_id); + let user = User::new(user_id).name("john.doe").display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + request.options = Some(MakeCredentialOptions::default().rk(true)); + request.pin_auth = Some(pin_auth); + request.pin_protocol = Some(2); + if let Some(third_party_payment) = self.third_party_payment { + request.extensions = Some(MakeCredentialExtensionsInput { + third_party_payment: Some(third_party_payment), + ..Default::default() + }); + } + let reply = device.exec(request).unwrap(); + assert_eq!( + reply.auth_data.flags & 0b1, + 0b1, + "up flag not set in auth_data: 0b{:b}", + reply.auth_data.flags + ); + assert_eq!( + reply.auth_data.flags & 0b100, + 0b100, + "uv flag not set in auth_data: 0b{:b}", + reply.auth_data.flags + ); + + let pin_token = + get_pin_token(&device, &key_agreement_key, &shared_secret, pin, 0x04, None) + .unwrap(); + let pin_auth = pin_token.authenticate(&[0x02]); + let request = CredentialManagement { + subcommand: 0x02, + subcommand_params: None, + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + }; + let reply = device.exec(request).unwrap(); + let rp: BTreeMap = reply.rp.unwrap().deserialized().unwrap(); + // TODO: check rp ID hash + assert!(reply.rp_id_hash.is_some()); + assert_eq!(reply.total_rps, Some(1)); + assert_eq!(rp.get("id").unwrap(), &Value::from(rp_id)); + + let pin_token_rp_id = self.pin_token_rp_id.then(|| rp_id.to_owned()); + let pin_token = get_pin_token( + &device, + &key_agreement_key, + &shared_secret, + pin, + 0x04, + pin_token_rp_id, + ) + .unwrap(); + let params = CredentialManagementParams { + rp_id_hash: Some(reply.rp_id_hash.unwrap().as_bytes().unwrap().to_owned()), + ..Default::default() + }; + let mut pin_auth_param = vec![0x04]; + pin_auth_param.extend_from_slice(¶ms.serialized()); + let pin_auth = pin_token.authenticate(&pin_auth_param); + let request = CredentialManagement { + subcommand: 0x04, + subcommand_params: Some(params), + pin_protocol: Some(2), + pin_auth: Some(pin_auth), + }; + let reply = device.exec(request).unwrap(); + let user: BTreeMap = reply.user.unwrap().deserialized().unwrap(); + assert_eq!(reply.total_credentials, Some(1)); + assert_eq!(user.get("id").unwrap(), &Value::from(user_id.as_slice())); + assert_eq!( + reply.third_party_payment, + Some(self.third_party_payment.unwrap_or_default()) + ); + }); + } +} + +impl Exhaustive for TestListCredentials { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + pin_token_rp_id: bool, + third_party_payment: Option, + } + } +} + +#[test] +fn test_list_credentials() { + TestListCredentials::run_all(); +} diff --git a/tests/cred_mgmt.rs b/tests/cred_mgmt.rs new file mode 100644 index 0000000..4f1d683 --- /dev/null +++ b/tests/cred_mgmt.rs @@ -0,0 +1,461 @@ +#![cfg(feature = "dispatch")] + +pub mod authenticator; +pub mod fs; +pub mod virt; +pub mod webauthn; + +use std::collections::BTreeSet; + +use littlefs2::path::PathBuf; + +use authenticator::{Authenticator, Pin}; +use fs::list_fs; +use virt::{Ctap2Error, Options}; +use webauthn::{CredentialData, PubKeyCredDescriptor, Rp, User}; + +struct CredMgmt<'a> { + authenticator: Authenticator<'a, Pin>, + credentials: Vec<(Rp, User, CredentialData)>, +} + +impl<'a> CredMgmt<'a> { + fn new(authenticator: Authenticator<'a, Pin>) -> Self { + Self { + authenticator, + credentials: Default::default(), + } + } + + fn make_credential(&mut self, rp: Rp, user: User) -> Result { + self.authenticator + .make_credential(rp.clone(), user.clone()) + .inspect(|credential_data| { + self.credentials + .retain(|(old_rp, old_user, _)| old_rp != &rp || old_user != &user); + self.credentials.push((rp, user, credential_data.clone())); + }) + } + + fn delete_credential(&mut self, id: Vec) -> Result<(), Ctap2Error> { + self.authenticator.delete_credential(&id); + self.credentials.retain(|(_, _, data)| data.id != id); + Ok(()) + } + + fn delete_credential_at(&mut self, i: usize) -> Result<(), Ctap2Error> { + assert!(i < self.credentials.len()); + let id = self.credentials[i].2.id.clone(); + self.delete_credential(id) + } + + fn update_user(&mut self, id: Vec, user: User) -> Result<(), Ctap2Error> { + self.authenticator.update_user(&id, user.clone())?; + self.credentials + .iter_mut() + .filter(|(_, _, data)| data.id == id) + .for_each(|cred| cred.1 = user.clone()); + Ok(()) + } + + fn update_user_at(&mut self, i: usize, user: User) -> Result<(), Ctap2Error> { + assert!(i < self.credentials.len()); + let id = self.credentials[i].2.id.clone(); + self.update_user(id, user) + } + + fn list(&mut self) { + let expected_rp_ids = self.rp_ids(); + let actual_rps = self.authenticator.list_rps(); + let actual_rp_ids: BTreeSet<_> = actual_rps.iter().map(|rp| rp.id.clone()).collect(); + assert_eq!(expected_rp_ids, actual_rp_ids); + // TODO: check other RP fields than ID + + for rp_id in expected_rp_ids { + assert!(actual_rps.iter().any(|rp| rp.id == rp_id)); + let expected_credentials = self.credentials(&rp_id); + let actual_credentials = self.authenticator.list_credentials(&rp_id); + let actual_credentials: BTreeSet<_> = actual_credentials.into_iter().collect(); + assert_eq!(expected_credentials, actual_credentials); + } + } + + fn rp_ids(&self) -> BTreeSet { + self.credentials + .iter() + .map(|(rp, _, _)| rp.id.clone()) + .collect() + } + + fn credentials(&self, rp_id: &str) -> BTreeSet<(User, PubKeyCredDescriptor)> { + self.credentials + .iter() + .filter(|(rp, _, _)| rp.id == rp_id) + .map(|(_, user, data)| { + ( + user.clone(), + PubKeyCredDescriptor::new("public-key", data.id.clone()), + ) + }) + .collect() + } +} + +fn generate_rp(i: usize) -> Rp { + // TODO: set other fields than id + let rp_id = format!("rp{i}"); + Rp::new(rp_id) +} + +fn generate_user(i: u8) -> User { + // TODO: set other fields than id + let mut user = Vec::from(b"john.doe"); + user.push(i); + User::new(user) +} + +#[test] +fn test_list_credentials() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for i in 0..10 { + let rp = generate_rp(i); + let user = generate_user(0); + cred_mgmt.make_credential(rp, user).unwrap(); + } + + cred_mgmt.list(); + }) +} + +#[test] +fn test_list_credentials_multi() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } + } + + cred_mgmt.list(); + }) +} + +#[test] +fn test_list_credentials_overwrite() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 2); + assert_eq!(files.try_remove_rks(), 1); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + let rp = generate_rp(0); + let user = generate_user(0); + cred_mgmt.make_credential(rp.clone(), user.clone()).unwrap(); + cred_mgmt.make_credential(rp, user).unwrap(); + + cred_mgmt.list(); + }) +} + +#[test] +fn test_list_credentials_delete() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 9); + assert_eq!(files.try_remove_rks(), 8); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } + } + + // deletes the only credential for rp2 + cred_mgmt.delete_credential_at(4).unwrap(); + // deletes one of three credentials for rp1 + cred_mgmt.delete_credential_at(2).unwrap(); + + cred_mgmt.list(); + }) +} + +#[test] +fn test_list_credentials_delete_all() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 1); + files.remove_empty_dir("fido/dat/rk"); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } + } + + for _ in 0..10 { + cred_mgmt.delete_credential_at(0).unwrap(); + } + }) +} + +#[test] +fn test_list_credentials_update_user() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } + } + + // case 1: updates the only credential for rp2 + + // changing the user ID fails + let user = generate_user(98); + assert_eq!(cred_mgmt.update_user_at(4, user), Err(Ctap2Error(0x02))); + + cred_mgmt.list(); + + // setting the display name works + let mut user = generate_user(0); + user.display_name = Some("John Doe".into()); + cred_mgmt.update_user_at(4, user).unwrap(); + + cred_mgmt.list(); + + // case 2: updates one of three credentials for rp1 + + // changing the user ID fails + let user = generate_user(99); + assert_eq!(cred_mgmt.update_user_at(2, user), Err(Ctap2Error(0x02))); + + cred_mgmt.list(); + + // setting the display name works + let mut user = generate_user(1); + user.display_name = Some("John Doe".into()); + cred_mgmt.update_user_at(2, user).unwrap(); + + cred_mgmt.list(); + }) +} + +#[test] +fn test_max_credential_count() { + let options = Options { + max_resident_credential_count: Some(10), + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { + let mut authenticator = Authenticator::new(device).set_pin(b"123456"); + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, 0); + assert_eq!(metadata.remaining, 10); + + for i in 0..10 { + let rp = Rp::new(format!("rp{i}")); + let user = User::new(b"john.doe"); + authenticator.make_credential(rp, user).unwrap(); + + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, i + 1); + assert_eq!(metadata.remaining, 9 - i); + } + + let rps = authenticator.list_rps(); + assert_eq!(rps.len(), 10); + for rp in &rps { + let credentials = authenticator.list_credentials(&rp.id); + assert_eq!(credentials.len(), 1); + } + + let rp = Rp::new("rp11"); + let user = User::new(b"john.doe"); + let result = authenticator.make_credential(rp, user); + assert_eq!(result, Err(Ctap2Error(0x28))); + + let rps = authenticator.list_rps(); + assert_eq!(rps.len(), 10); + for rp in &rps { + let credentials = authenticator.list_credentials(&rp.id); + assert_eq!(credentials.len(), 1); + } + }) +} + +#[test] +fn test_filesystem_full() { + let mut options = Options { + max_resident_credential_count: Some(10), + inspect_ifs: Some(Box::new(|ifs| { + let blocks = ifs.available_blocks().unwrap(); + assert!(blocks < 10, "{blocks}"); + assert!(blocks > 1, "{blocks}"); + })), + ..Default::default() + }; + for i in 0..80 { + let path = PathBuf::try_from(format!("/test/{i}").as_str()).unwrap(); + options.files.push((path, vec![0; 512])); + } + virt::run_ctap2_with_options(options, |device| { + let mut authenticator = Authenticator::new(device).set_pin(b"123456"); + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, 0); + // This number depends on filesystem layout details and may change if the filesystem + // layout or implementation are changed. + assert_eq!(metadata.remaining, 5); + let n = metadata.remaining; + + let mut i = 0; + loop { + let rp = Rp::new(format!("rp{i}")); + let user = User::new(b"john.doe"); + let result = authenticator.make_credential(rp, user); + + if result == Err(Ctap2Error(0x28)) { + break; + } + result.unwrap(); + + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, i + 1); + + i += 1; + } + + // We should be able to create at least one credential. + assert!(i > 0, "i = {i}"); + // Our estimate should not be too low. + assert!(i >= n, "i = {i}, n = {n}"); + // Our estime should not be more than five credentials too high. + assert!(i <= n + 5, "i = {i}, n = {n}"); + + let metadata = authenticator.credentials_metadata(); + assert_eq!(metadata.existing, i); + assert_eq!(metadata.remaining, 0); + }) +} + +#[test] +fn test_filesystem_full_update_user() { + let mut options = Options { + max_resident_credential_count: Some(10), + inspect_ifs: Some(Box::new(|ifs| { + let blocks = ifs.available_blocks().unwrap(); + assert!(blocks < 10, "{blocks}"); + assert!(blocks > 1, "{blocks}"); + })), + ..Default::default() + }; + for i in 0..80 { + let path = PathBuf::try_from(format!("/test/{i}").as_str()).unwrap(); + options.files.push((path, vec![0; 512])); + } + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + + let mut i = 0; + loop { + let rp = generate_rp(i); + let user = generate_user(0); + let result = cred_mgmt.make_credential(rp, user); + + if result == Err(Ctap2Error(0x28)) { + break; + } + result.unwrap(); + + i += 1; + } + + cred_mgmt.list(); + + // filesystem is now full, we cannot create new credentials + // but: we still want to be able to update existing credentials + let mut user = generate_user(0); + user.display_name = Some("John Doe".into()); + cred_mgmt.update_user_at(1, user).unwrap(); + + cred_mgmt.list(); + }) +} diff --git a/tests/ctap1.rs b/tests/ctap1.rs new file mode 100644 index 0000000..e90efba --- /dev/null +++ b/tests/ctap1.rs @@ -0,0 +1,253 @@ +#![cfg(feature = "dispatch")] + +pub mod virt; +pub mod webauthn; + +use ctaphid::{Device, HidDevice}; +use hex_literal::hex; +use iso7816::{ + command::{class::Class, instruction::Instruction, CommandBuilder, ExpectedLen}, + response::Status, +}; +use littlefs2_core::path; +use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; +use x509_parser::public_key::PublicKey; + +use virt::Options; + +#[test] +fn test_version() { + virt::run_ctaphid(|device| { + let response = version(&device); + assert_eq!(response, b"U2F_V2".as_slice()); + }) +} + +#[test] +fn test_authenticate() { + virt::run_ctaphid(|device| { + let challenge = &hex!("4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"); + let application = &hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); + let register = register(&device, challenge, application); + + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + + // check-only + let err = authenticate(&device, 7, challenge, application, ®ister) + .err() + .unwrap(); + assert_eq!(err, Status::ConditionsOfUseNotSatisfied); + + // enforce user presence + let response = authenticate(&device, 3, challenge, application, ®ister).unwrap(); + assert_eq!(response.user_presence & 1, 1); + assert_eq!(response.counter, 1); + + // don’t enforce user presence + let response = authenticate(&device, 8, challenge, application, ®ister).unwrap(); + assert_eq!(response.user_presence & 1, 0); + assert_eq!(response.counter, 2); + }); +} + +#[test] +fn test_authenticate_wrong_application() { + virt::run_ctaphid(|device| { + let challenge = &hex!("4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"); + let mut application = + hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); + let register = register(&device, challenge, &application); + application.reverse(); + + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + for mode in [3, 7, 8] { + let err = authenticate(&device, mode, challenge, &application, ®ister) + .err() + .unwrap(); + assert_eq!(err, Status::IncorrectDataParameter); + } + }); +} + +#[test] +fn test_authenticate_wrong_keyhandle() { + virt::run_ctaphid(|device| { + let challenge = &hex!("4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"); + let application = &hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); + let mut register = register(&device, challenge, application); + register.keyhandle.reverse(); + + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + for mode in [3, 7, 8] { + let err = authenticate(&device, mode, challenge, application, ®ister) + .err() + .unwrap(); + assert_eq!(err, Status::IncorrectDataParameter); + } + }); +} + +#[test] +fn test_authenticate_upgrade() { + // manually extracted after running register on commit 79b05b576863236fe54750b18e862ce0801f2040 + let state: &[u8] = &hex!("A5726B65795F656E6372797074696F6E5F6B65795010926EC1ACF475A6ED273BD951BC4A6E706B65795F7772617070696E675F6B65795061743976EDACA263BD30DE70ADFBAF0B781A636F6E73656375746976655F70696E5F6D69736D617463686573006870696E5F68617368F66974696D657374616D7001"); + let key_encryption_key: &[u8] = &hex!("00020003A0BAA6066B22616147F242DEC9C4B450F6189A10EE036C36E697E647B2C1D3E1000000000000000000000000"); + let key_wrapping_key: &[u8] = &hex!("000200037719CE721FB206F9788BB7E550777C03795ECFE0B211AB7D50C5C2CE21B43E8E010000000000000000000000"); + let options = Options { + files: vec![ + (path!("fido/dat/persistent-state.cbor").into(), state.into()), + ( + path!("fido/sec/10926ec1acf475a6ed273bd951bc4a6e").into(), + key_encryption_key.into(), + ), + ( + path!("fido/sec/61743976edaca263bd30de70adfbaf0b").into(), + key_wrapping_key.into(), + ), + ], + ..Default::default() + }; + + virt::run_ctaphid_with_options(options, |device| { + let application = &hex!("f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"); + let keyhandle = hex!("A3005878B3F2499ACECB2C08F437DEF0F41929BD4DCFBCA7D43E893B18799BA61F6D84A36EAFCB87D9E833AEA1FE68BABD27A4B89C83C32EC25B092D915D9EA207ECA4BDE5A06E3CDCCFE0E93600AC28A6A8A61E4A1C6881C67E252F00425672427CFC59463B097364F45FD050F8E6BE1C6CD45C1F7D9B5732E334A8014C533D8BF37EEF0D8D7D16B6DF025055B1A6492F5607139EF420D47051A5F3"); + let user_key = &hex!("04AE6B38AE33494A3A58A9FED8A1C5DA2683F510A69B9DE4D8849648485ECDCC21918E6124F6E0B71E7B3C5D92F08EC38D3161E236FF72743923141E97089AA2C4"); + let register = Register { + user_key: VerifyingKey::from_sec1_bytes(user_key).unwrap(), + keyhandle: keyhandle.into(), + }; + + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + + // check-only + let err = authenticate(&device, 7, challenge, application, ®ister) + .err() + .unwrap(); + assert_eq!(err, Status::ConditionsOfUseNotSatisfied); + + // enforce user presence + let response = authenticate(&device, 3, challenge, application, ®ister).unwrap(); + assert_eq!(response.user_presence & 1, 1); + assert_eq!(response.counter, 1); + + // don’t enforce user presence + let response = authenticate(&device, 8, challenge, application, ®ister).unwrap(); + assert_eq!(response.user_presence & 1, 0); + assert_eq!(response.counter, 2); + }); +} + +fn version(device: &Device) -> Vec { + let command = build_command(3, 0, &[]); + exec(device, &command).unwrap() +} + +struct Register { + user_key: VerifyingKey, + keyhandle: Vec, +} + +fn register( + device: &Device, + challenge: &[u8; 32], + application: &[u8; 32], +) -> Register { + let mut request = [0; 64]; + request[..32].copy_from_slice(challenge); + request[32..].copy_from_slice(application); + let command = build_command(1, 0, &request); + let response = exec(device, &command).unwrap(); + + let (first, response) = response.split_first().unwrap(); + let (user_key, response) = response.split_at(65); + let (keyhandle_len, response) = response.split_first().unwrap(); + let (keyhandle, response) = response.split_at(usize::from(*keyhandle_len)); + let (signature, cert) = x509_parser::parse_x509_certificate(response).unwrap(); + + assert_eq!(*first, 0x05); + + let mut message = Vec::new(); + message.push(0x00); + message.extend_from_slice(application); + message.extend_from_slice(challenge); + message.extend_from_slice(keyhandle); + message.extend_from_slice(user_key); + + let signature = DerSignature::from_bytes(signature).unwrap(); + let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap(); + let PublicKey::EC(ec_point) = public_key else { + panic!("unexpected public key in attestation certificate"); + }; + let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap(); + public_key.verify(&message, &signature).unwrap(); + + Register { + user_key: VerifyingKey::from_sec1_bytes(user_key).unwrap(), + keyhandle: keyhandle.into(), + } +} + +struct Authenticate { + user_presence: u8, + counter: u32, +} + +fn authenticate( + device: &Device, + mode: u8, + challenge: &[u8; 32], + application: &[u8; 32], + register: &Register, +) -> Result { + let mut request = Vec::new(); + request.extend_from_slice(challenge); + request.extend_from_slice(application); + request.push(register.keyhandle.len().try_into().unwrap()); + request.extend_from_slice(®ister.keyhandle); + let command = build_command(2, mode, &request); + let response = exec(device, &command)?; + + let (user_presence, response) = response.split_first().unwrap(); + let (counter, signature) = response.split_at(4); + + let mut message = Vec::new(); + message.extend_from_slice(application); + message.push(*user_presence); + message.extend_from_slice(counter); + message.extend_from_slice(challenge); + + let signature = DerSignature::from_bytes(signature).unwrap(); + register.user_key.verify(&message, &signature).unwrap(); + + Ok(Authenticate { + user_presence: *user_presence, + counter: u32::from_be_bytes(counter.try_into().unwrap()), + }) +} + +fn exec(device: &Device, command: &[u8]) -> Result, Status> { + let mut response = device.ctap1(command).unwrap(); + let low = response.pop().unwrap(); + let high = response.pop().unwrap(); + let status = u16::from_be_bytes([high, low]); + let status = Status::from_u16(status); + if status == Status::Success { + Ok(response) + } else { + Err(status) + } +} + +fn build_command(ins: u8, p1: u8, data: &[u8]) -> heapless::Vec { + let builder = CommandBuilder::new( + Class::from_byte(0).unwrap(), + Instruction::from(ins), + p1, + 0, + data, + ExpectedLen::Max, + ); + let mut buffer = heapless::Vec::new(); + builder.serialize_into(&mut buffer).unwrap(); + buffer +} diff --git a/tests/data/fido-cert.der b/tests/data/fido-cert.der new file mode 100644 index 0000000..7dabc45 Binary files /dev/null and b/tests/data/fido-cert.der differ diff --git a/tests/data/fido-key.trussed b/tests/data/fido-key.trussed new file mode 100644 index 0000000..4f2c64c Binary files /dev/null and b/tests/data/fido-key.trussed differ diff --git a/tests/fs/mod.rs b/tests/fs/mod.rs new file mode 100644 index 0000000..9ef52e7 --- /dev/null +++ b/tests/fs/mod.rs @@ -0,0 +1,91 @@ +use std::collections::BTreeMap; + +use littlefs2_core::{path, DynFilesystem, FileType, Path}; + +#[derive(Debug, PartialEq)] +pub enum Entry { + File, + EmptyDir, +} + +#[derive(Debug, Default, PartialEq)] +pub struct Entries(pub BTreeMap); + +impl Entries { + pub fn remove_standard(&mut self) { + self.remove_file("fido/sec/00"); + self.remove_file("fido/x5c/00"); + self.remove_file("trussed/dat/rng-state.bin"); + } + + pub fn remove_state(&mut self) { + self.remove_file("fido/dat/persistent-state.cbor"); + } + + pub fn try_remove_state(&mut self) { + self.0.remove("fido/dat/persistent-state.cbor"); + } + + pub fn try_remove_keys(&mut self) -> usize { + self.try_remove_dir("fido/sec") + } + + pub fn try_remove_rks(&mut self) -> usize { + self.try_remove_dir("fido/dat/rk") + } + + pub fn try_remove_dir(&mut self, dir: &str) -> usize { + let n = self.0.len(); + self.0.retain(|path, _| { + let (start, _) = path.rsplit_once('/').unwrap(); + start != dir + }); + n - self.0.len() + } + + pub fn remove_file(&mut self, path: &str) { + let entry = self.0.remove(path); + assert_eq!(entry, Some(Entry::File), "{path}"); + } + + pub fn remove_empty_dir(&mut self, path: &str) { + let entry = self.0.remove(path); + assert_eq!(entry, Some(Entry::EmptyDir), "{path}"); + } + + pub fn assert_empty(&self) { + assert_eq!(self.0, Default::default()); + } +} + +pub fn list_fs(fs: &dyn DynFilesystem) -> Entries { + fn list_dir(fs: &dyn DynFilesystem, dir: &Path, files: &mut BTreeMap) -> usize { + fs.read_dir_and_then(dir, &mut |iter| { + let mut child_count = 0; + for entry in iter { + let entry = entry.unwrap(); + if entry.file_name().as_str() == "." || entry.file_name().as_str() == ".." { + continue; + } + child_count += 1; + match entry.file_type() { + FileType::File => { + files.insert(entry.path().as_str().to_owned(), Entry::File); + } + FileType::Dir => { + let n = list_dir(fs, entry.path(), files); + if n == 0 { + files.insert(entry.path().as_str().to_owned(), Entry::EmptyDir); + } + } + } + } + Ok(child_count) + }) + .unwrap() + } + + let mut entries = BTreeMap::new(); + list_dir(fs, path!(""), &mut entries); + Entries(entries) +} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs new file mode 100644 index 0000000..fa070b0 --- /dev/null +++ b/tests/virt/mod.rs @@ -0,0 +1,287 @@ +mod pipe; + +use std::{ + borrow::Cow, + cell::RefCell, + fmt::{self, Debug, Formatter}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Once, + }, + thread, + time::{Duration, SystemTime}, +}; + +use ciborium::Value; +use ctap_types::ctap2::Operation; +use ctaphid::{ + error::{RequestError, ResponseError}, + HidDevice, HidDeviceInfo, +}; +use ctaphid_dispatch::{Channel, Dispatch, Requester, DEFAULT_MESSAGE_SIZE}; +use fido_authenticator::{Authenticator, Config, Conforming}; +use littlefs2::{object_safe::DynFilesystem, path, path::PathBuf}; +use rand::{ + distributions::{Distribution, Uniform}, + RngCore as _, +}; +use trussed::{ + backend::BackendId, + platform::Platform as _, + store::Store as _, + virt::{self, StoreConfig}, +}; +use trussed_staging::virt::{BackendIds, Client, Dispatcher}; + +use crate::webauthn::Request; + +use pipe::Pipe; + +// see: https://github.com/Nitrokey/nitrokey-3-firmware/tree/main/utils/test-certificates/fido +const ATTESTATION_CERT: &[u8] = include_bytes!("../data/fido-cert.der"); +const ATTESTATION_KEY: &[u8] = include_bytes!("../data/fido-key.trussed"); + +static INIT_LOGGER: Once = Once::new(); + +pub fn run_ctaphid(f: F) -> T +where + F: FnOnce(ctaphid::Device) -> T + Send, + T: Send, +{ + run_ctaphid_with_options(Default::default(), f) +} + +pub fn run_ctaphid_with_options(options: Options, f: F) -> T +where + F: FnOnce(ctaphid::Device) -> T + Send, + T: Send, +{ + INIT_LOGGER.call_once(|| { + env_logger::init(); + }); + let mut files = options.files; + files.push((path!("fido/x5c/00").into(), ATTESTATION_CERT.into())); + files.push((path!("fido/sec/00").into(), ATTESTATION_KEY.into())); + with_client( + &files, + |client| { + let mut authenticator = Authenticator::new( + client, + Conforming {}, + Config { + max_msg_size: 0, + skip_up_timeout: None, + max_resident_credential_count: options.max_resident_credential_count, + large_blobs: None, + nfc_transport: false, + }, + ); + + let channel = Channel::new(); + let (rq, rp) = channel.split().unwrap(); + + thread::scope(|s| { + let stop = Arc::new(AtomicBool::new(false)); + let poller_stop = stop.clone(); + let poller = s.spawn(move || { + let mut dispatch = Dispatch::new(rp); + while !poller_stop.load(Ordering::Relaxed) { + dispatch.poll(&mut [&mut authenticator]); + thread::sleep(Duration::from_millis(1)); + } + }); + + let runner = s.spawn(move || { + let device = Device::new(rq); + let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); + f(device) + }); + + let result = runner.join(); + stop.store(true, Ordering::Relaxed); + poller.join().unwrap(); + result.unwrap() + }) + }, + |ifs| { + if let Some(inspect_ifs) = options.inspect_ifs { + inspect_ifs(ifs); + } + }, + ) +} + +pub fn run_ctap2(f: F) -> T +where + F: FnOnce(Ctap2) -> T + Send, + T: Send, +{ + run_ctaphid(|device| f(Ctap2(device))) +} + +pub fn run_ctap2_with_options(options: Options, f: F) -> T +where + F: FnOnce(Ctap2) -> T + Send, + T: Send, +{ + run_ctaphid_with_options(options, |device| f(Ctap2(device))) +} + +pub type InspectFsFn = Box; + +#[derive(Default)] +pub struct Options { + pub files: Vec<(PathBuf, Vec)>, + pub max_resident_credential_count: Option, + pub inspect_ifs: Option, +} + +pub struct Ctap2<'a>(ctaphid::Device>); + +impl Ctap2<'_> { + pub fn exec(&self, request: R) -> Result { + let operation = Operation::try_from(R::COMMAND) + .map(|op| format!("{op:?}")) + .unwrap_or_else(|_| "?".to_owned()); + log::info!("Executing command {:#x} ({})", R::COMMAND, operation); + let request = request.into(); + log::debug!("Sending request {request:?}"); + let mut serialized = Vec::new(); + ciborium::into_writer(&request, &mut serialized).unwrap(); + let reply = self + .0 + .ctap2(R::COMMAND, &serialized) + .map_err(|err| match err { + ctaphid::error::Error::CommandError(ctaphid::error::CommandError::CborError( + value, + )) => { + log::warn!("Received CTAP2 error {value:#x}"); + Ctap2Error(value) + } + err => panic!("failed to execute CTAP2 command: {err:?}"), + })?; + let value: Value = if reply.is_empty() { + Value::Map(Vec::new()) + } else { + ciborium::from_reader(reply.as_slice()).unwrap() + }; + log::debug!("Received reply {value:?}"); + Ok(value.into()) + } +} + +#[derive(PartialEq)] +pub struct Ctap2Error(pub u8); + +impl Debug for Ctap2Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("Ctap2Error") + .field(&format_args!("{:#x}", self.0)) + .finish() + } +} + +#[derive(Debug)] +pub struct DeviceInfo; + +impl HidDeviceInfo for DeviceInfo { + fn vendor_id(&self) -> u16 { + 0x20a0 + } + + fn product_id(&self) -> u16 { + 0x42b2 + } + + fn path(&self) -> Cow<'_, str> { + "test".into() + } +} + +pub struct Device<'a>(RefCell>); + +impl<'a> Device<'a> { + fn new(requester: Requester<'a, DEFAULT_MESSAGE_SIZE>) -> Self { + Self(RefCell::new(Pipe::new(requester))) + } +} + +impl HidDevice for Device<'_> { + type Info = DeviceInfo; + + fn send(&self, data: &[u8]) -> Result<(), RequestError> { + self.0.borrow_mut().push(data); + Ok(()) + } + + fn receive<'a>( + &self, + buffer: &'a mut [u8], + timeout: Option, + ) -> Result<&'a [u8], ResponseError> { + let start = SystemTime::now(); + + loop { + if let Some(timeout) = timeout { + let elapsed = start.elapsed().unwrap(); + if elapsed >= timeout { + return Err(ResponseError::Timeout); + } + } + + if let Some(response) = self.0.borrow_mut().pop() { + return if buffer.len() >= response.len() { + log::info!("received response: {} bytes", response.len()); + buffer[..response.len()].copy_from_slice(&response); + Ok(&buffer[..response.len()]) + } else { + Err(ResponseError::PacketReceivingFailed( + "invalid buffer size".into(), + )) + }; + } + + thread::sleep(Duration::from_millis(1)); + } + } +} + +fn with_client(files: &[(PathBuf, Vec)], f: F, inspect_ifs: F2) -> T +where + F: FnOnce(Client) -> T, + F2: FnOnce(&dyn DynFilesystem), +{ + virt::with_platform(StoreConfig::ram(), |mut platform| { + // virt always uses the same seed -- request some random bytes to reach a somewhat random + // state + let uniform = Uniform::from(0..64); + let n = uniform.sample(&mut rand::thread_rng()); + for _ in 0..n { + platform.rng().next_u32(); + } + + let store = platform.store(); + let ifs = store.ifs(); + + for (path, content) in files { + if let Some(dir) = path.parent() { + ifs.create_dir_all(&dir).unwrap(); + } + ifs.write(path, content).unwrap(); + } + + let result = platform.run_client_with_backends( + "fido", + Dispatcher::default(), + &[ + BackendId::Custom(BackendIds::StagingBackend), + BackendId::Core, + ], + f, + ); + + inspect_ifs(ifs); + + result + }) +} diff --git a/tests/virt/pipe.rs b/tests/virt/pipe.rs new file mode 100644 index 0000000..5d1b3ae --- /dev/null +++ b/tests/virt/pipe.rs @@ -0,0 +1,422 @@ +// Extracted from the usbd-ctaphid crate: +// https://github.com/trussed-dev/usbd-ctaphid/blob/1db2e014f28669bc484c81ab0406c54b16bba33c/src/pipe.rs +// +// License: Apache-2.0 or MIT +// +// Authors: +// - Conor Patrick +// - Nicolas Stalder +// - Robin Krahl +// - Sosthène Guédon + +use std::collections::VecDeque; + +use ctap_types::Error; +use ctaphid_dispatch::{app::Command, Requester}; +use heapless_bytes::Bytes; + +const MESSAGE_SIZE: usize = 3072; +const PACKET_SIZE: usize = 64; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +struct Version { + major: u8, + minor: u8, + build: u8, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct Request { + channel: u32, + command: Command, + length: u16, + timestamp: u32, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct Response { + channel: u32, + command: Command, + length: u16, +} + +impl Response { + fn from_request_and_size(request: Request, size: usize) -> Self { + Self { + channel: request.channel, + command: request.command, + length: size as u16, + } + } + + fn error_from_request(request: Request) -> Self { + Self::error_on_channel(request.channel) + } + + fn error_on_channel(channel: u32) -> Self { + Self { + channel, + command: Command::Error, + length: 1, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct MessageState { + next_sequence: u8, + transmitted: usize, +} + +impl Default for MessageState { + fn default() -> Self { + Self { + next_sequence: 0, + transmitted: PACKET_SIZE - 7, + } + } +} + +impl MessageState { + fn absorb_packet(&mut self) { + self.next_sequence += 1; + self.transmitted += PACKET_SIZE - 5; + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum State { + Idle, + Receiving((Request, MessageState)), + WaitingOnAuthenticator(Request), + WaitingToSend(Response), + Sending((Response, MessageState)), +} + +pub struct Pipe<'a, const N: usize> { + queue: VecDeque<[u8; PACKET_SIZE]>, + state: State, + interchange: Requester<'a, N>, + buffer: [u8; MESSAGE_SIZE], + last_channel: u32, + implements: u8, + last_milliseconds: u32, + started_processing: bool, + needs_keepalive: bool, + version: Version, +} + +impl<'a, const N: usize> Pipe<'a, N> { + pub fn new(interchange: Requester<'a, N>) -> Self { + Self { + queue: Default::default(), + state: State::Idle, + interchange, + buffer: [0; MESSAGE_SIZE], + last_channel: Default::default(), + implements: 0x84, + last_milliseconds: Default::default(), + started_processing: Default::default(), + needs_keepalive: Default::default(), + version: Default::default(), + } + } + + pub fn push(&mut self, packet: &[u8]) { + let (_, packet) = packet.split_first().unwrap(); + self.read_and_handle_packet(packet); + } + + pub fn pop(&mut self) -> Option<[u8; PACKET_SIZE]> { + self.handle_response(); + self.maybe_write_packet(); + self.queue.pop_front() + } + + fn read_and_handle_packet(&mut self, packet: &[u8]) { + if packet.len() != PACKET_SIZE { + panic!("unexpected packet size"); + } + let channel = u32::from_be_bytes(packet[..4].try_into().unwrap()); + let is_initialization = (packet[4] >> 7) != 0; + if is_initialization { + let command_number = packet[4] & !0x80; + let Ok(command) = Command::try_from(command_number) else { + self.start_sending_error_on_channel(channel, Error::InvalidCommand); + return; + }; + let length = u16::from_be_bytes(packet[5..][..2].try_into().unwrap()); + let timestamp = self.last_milliseconds; + let current_request = Request { + channel, + command, + length, + timestamp, + }; + + if !(self.state == State::Idle) { + let request = match self.state { + State::WaitingOnAuthenticator(request) => request, + State::Receiving((request, _message_state)) => request, + _ => { + return; + } + }; + if packet[4] == 0x86 { + // self.cancel_ongoing_activity(); + } else { + if channel == request.channel { + if command == Command::Cancel { + // self.cancel_ongoing_activity(); + } else { + self.start_sending_error(request, Error::InvalidSeq); + } + } else { + self.send_error_now(current_request, Error::ChannelBusy); + } + + return; + } + } + + if length > MESSAGE_SIZE as u16 { + self.send_error_now(current_request, Error::InvalidLength); + return; + } + + if length > PACKET_SIZE as u16 - 7 { + self.buffer[..PACKET_SIZE - 7].copy_from_slice(&packet[7..]); + self.state = State::Receiving((current_request, { MessageState::default() })); + } else { + self.buffer[..length as usize].copy_from_slice(&packet[7..][..length as usize]); + self.dispatch_request(current_request); + } + } else { + match self.state { + State::Receiving((request, mut message_state)) => { + let sequence = packet[4]; + if sequence != message_state.next_sequence { + self.start_sending_error(request, Error::InvalidSeq); + return; + } + if channel != request.channel { + return; + } + + let payload_length = request.length as usize; + if message_state.transmitted + (PACKET_SIZE - 5) < payload_length { + self.buffer[message_state.transmitted..][..PACKET_SIZE - 5] + .copy_from_slice(&packet[5..]); + message_state.absorb_packet(); + self.state = State::Receiving((request, message_state)); + } else { + let missing = request.length as usize - message_state.transmitted; + self.buffer[message_state.transmitted..payload_length] + .copy_from_slice(&packet[5..][..missing]); + self.dispatch_request(request); + } + } + _ => { + panic!("unexpected continuation packet"); + } + } + } + } + + fn start_sending(&mut self, response: Response) { + self.state = State::WaitingToSend(response); + self.maybe_write_packet(); + } + + fn start_sending_error(&mut self, request: Request, error: Error) { + self.start_sending_error_on_channel(request.channel, error); + } + + fn start_sending_error_on_channel(&mut self, channel: u32, error: Error) { + self.buffer[0] = error as u8; + let response = Response::error_on_channel(channel); + self.start_sending(response); + } + + fn send_error_now(&mut self, request: Request, error: Error) { + let last_state = core::mem::replace(&mut self.state, State::Idle); + let last_first_byte = self.buffer[0]; + + self.buffer[0] = error as u8; + let response = Response::error_from_request(request); + self.start_sending(response); + self.maybe_write_packet(); + + self.state = last_state; + self.buffer[0] = last_first_byte; + } + + fn maybe_write_packet(&mut self) { + match self.state { + State::WaitingToSend(response) => { + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&response.channel.to_be_bytes()); + packet[4] = response.command.into_u8() | 0x80; + packet[5..7].copy_from_slice(&response.length.to_be_bytes()); + + let fits_in_one_packet = 7 + response.length as usize <= PACKET_SIZE; + if fits_in_one_packet { + packet[7..][..response.length as usize] + .copy_from_slice(&self.buffer[..response.length as usize]); + self.state = State::Idle; + } else { + packet[7..].copy_from_slice(&self.buffer[..PACKET_SIZE - 7]); + } + + self.queue.push_back(packet); + + if fits_in_one_packet { + self.state = State::Idle; + } else { + self.state = State::Sending((response, MessageState::default())); + } + } + State::Sending((response, mut message_state)) => { + let mut packet = [0u8; PACKET_SIZE]; + packet[..4].copy_from_slice(&response.channel.to_be_bytes()); + packet[4] = message_state.next_sequence; + + let sent = message_state.transmitted; + let remaining = response.length as usize - sent; + let last_packet = 5 + remaining <= PACKET_SIZE; + if last_packet { + packet[5..][..remaining] + .copy_from_slice(&self.buffer[message_state.transmitted..][..remaining]); + } else { + packet[5..].copy_from_slice( + &self.buffer[message_state.transmitted..][..PACKET_SIZE - 5], + ); + } + + self.queue.push_back(packet); + + if last_packet { + self.state = State::Idle; + } else { + message_state.absorb_packet(); + self.state = State::Sending((response, message_state)); + } + } + _ => {} + } + } + + fn dispatch_request(&mut self, request: Request) { + match request.command { + Command::Init => {} + _ => { + if request.channel == 0xffffffff { + self.start_sending_error(request, Error::InvalidChannel); + return; + } + } + } + match request.command { + Command::Init => { + match request.channel { + 0 => { + self.start_sending_error(request, Error::InvalidChannel); + } + cid => { + if request.length == 8 { + self.last_channel += 1; + let _nonce = &self.buffer[..8]; + let response = Response { + channel: cid, + command: request.command, + length: 17, + }; + + self.buffer[8..12].copy_from_slice(&self.last_channel.to_be_bytes()); + // CTAPHID protocol version + self.buffer[12] = 2; + // major device version number + self.buffer[13] = self.version.major; + // minor device version number + self.buffer[14] = self.version.minor; + // build device version number + self.buffer[15] = self.version.build; + // capabilities flags + // 0x1: implements WINK + // 0x4: implements CBOR + // 0x8: does not implement MSG + // self.buffer[16] = 0x01 | 0x08; + self.buffer[16] = self.implements; + self.start_sending(response); + } + } + } + } + + Command::Ping => { + let response = Response::from_request_and_size(request, request.length as usize); + self.start_sending(response); + } + + Command::Cancel => { + // self.cancel_ongoing_activity(); + } + + _ => { + self.needs_keepalive = request.command == Command::Cbor; + if self.interchange.state() == interchange::State::Responded { + self.interchange.take_response(); + } + match self.interchange.request(( + request.command, + Bytes::from_slice(&self.buffer[..request.length as usize]).unwrap(), + )) { + Ok(_) => { + self.state = State::WaitingOnAuthenticator(request); + self.started_processing = true; + } + Err(_) => { + self.send_error_now(request, Error::ChannelBusy); + } + } + } + } + } + + fn handle_response(&mut self) { + if let State::WaitingOnAuthenticator(request) = self.state { + if let Ok(response) = self.interchange.response() { + match &response.0 { + Err(ctaphid_dispatch::app::Error::InvalidCommand) => { + self.start_sending_error(request, Error::InvalidCommand); + } + Err(ctaphid_dispatch::app::Error::InvalidLength) => { + self.start_sending_error(request, Error::InvalidLength); + } + Err(ctaphid_dispatch::app::Error::NoResponse) => { + log::info!("Got waiting noresponse from authenticator??"); + } + + Ok(message) => { + if message.len() > self.buffer.len() { + log::error!( + "Message is longer than buffer ({} > {})", + message.len(), + self.buffer.len(), + ); + self.start_sending_error(request, Error::InvalidLength); + } else { + log::info!( + "Got {} bytes response from authenticator, starting send", + message.len() + ); + let response = Response::from_request_and_size(request, message.len()); + self.buffer[..message.len()].copy_from_slice(message); + self.start_sending(response); + } + } + } + } + } + } +} diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs new file mode 100644 index 0000000..9c87184 --- /dev/null +++ b/tests/webauthn/mod.rs @@ -0,0 +1,1029 @@ +use std::{collections::BTreeMap, iter}; + +use ciborium::Value; +use cipher::{BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit}; +use hmac::Mac; +use itertools::iproduct; +use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey}; +use rand::RngCore as _; +use serde::Deserialize; + +pub trait Exhaustive: Sized { + fn iter_exhaustive() -> impl Iterator + Clone; +} + +impl Exhaustive for bool { + fn iter_exhaustive() -> impl Iterator + Clone { + [false, true].into_iter() + } +} + +impl Exhaustive for Option { + fn iter_exhaustive() -> impl Iterator + Clone { + iter::once(None).chain(T::iter_exhaustive().map(Some)) + } +} + +macro_rules! exhaustive_struct { + ($($field:ident: $type:ty,)*) => { + iproduct!( + $(<$type as Exhaustive>::iter_exhaustive(),)* + ) + .map(|($($field,)*)| Self { $($field,)* }) + } +} + +pub(crate) use exhaustive_struct; + +pub struct KeyAgreementKey(p256::ecdh::EphemeralSecret); + +impl KeyAgreementKey { + pub fn generate() -> Self { + Self(p256::ecdh::EphemeralSecret::random(&mut rand::thread_rng())) + } + + pub fn public_key(&self) -> PublicKey { + PublicKey(self.0.public_key()) + } + + pub fn shared_secret(&self, peer: &PublicKey) -> SharedSecret { + let shared_point = self.0.diffie_hellman(&peer.0); + let hkdf = shared_point.extract::(Some(&[0; 32])); + let mut hmac_key = [0; 32]; + let mut aes_key = [0; 32]; + hkdf.expand(b"CTAP2 HMAC key", &mut hmac_key).unwrap(); + hkdf.expand(b"CTAP2 AES key", &mut aes_key).unwrap(); + SharedSecret { hmac_key, aes_key } + } +} + +#[derive(Copy, Clone, Debug)] +pub struct PublicKey(p256::PublicKey); + +impl From for Value { + fn from(public_key: PublicKey) -> Value { + let encoded = p256::EncodedPoint::from(&public_key.0); + let mut map = Map::default(); + map.push(1, 2); + map.push(3, -25); + map.push(-1, 1); + map.push(-2, encoded.x().unwrap().as_slice()); + map.push(-3, encoded.y().unwrap().as_slice()); + map.into() + } +} + +impl From for PublicKey { + fn from(value: Value) -> Self { + let map: BTreeMap = value.deserialized().unwrap(); + let kty = map.get(&1).unwrap(); + let alg = map.get(&3).unwrap(); + let crv = map.get(&-1).unwrap(); + let x = map.get(&-2).unwrap().as_bytes().unwrap().as_slice(); + let y = map.get(&-3).unwrap().as_bytes().unwrap().as_slice(); + + assert_eq!(kty, &Value::from(2)); + assert_eq!(alg, &Value::from(-25)); + assert_eq!(crv, &Value::from(1)); + let encoded = p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false); + Self(encoded.try_into().unwrap()) + } +} + +pub struct SharedSecret { + hmac_key: [u8; 32], + aes_key: [u8; 32], +} + +impl SharedSecret { + pub fn encrypt(&self, data: &[u8]) -> Vec { + let mut iv = [0; 16]; + rand::thread_rng().fill_bytes(&mut iv); + + let cipher: cbc::Encryptor = + KeyIvInit::new(self.aes_key.as_ref().into(), iv.as_ref().into()); + let encrypted = cipher.encrypt_padded_vec_mut::(data); + + let mut result = Vec::new(); + result.extend_from_slice(&iv); + result.extend_from_slice(&encrypted); + result + } + + pub fn decrypt(&self, data: &[u8]) -> Vec { + let (iv, data) = data.split_first_chunk::<16>().unwrap(); + let cipher: cbc::Decryptor = + KeyIvInit::new(self.aes_key.as_ref().into(), iv.into()); + cipher + .decrypt_padded_vec_mut::(data) + .unwrap() + } + + pub fn decrypt_pin_token(&self, data: &[u8]) -> PinToken { + PinToken(self.decrypt(data).try_into().unwrap()) + } + + pub fn authenticate(&self, data: &[u8]) -> [u8; 32] { + let mut mac: hmac::Hmac = Mac::new_from_slice(&self.hmac_key).unwrap(); + mac.update(data); + mac.finalize().into_bytes().into() + } +} + +#[derive(Debug, PartialEq)] +pub struct PinToken([u8; 32]); + +impl PinToken { + pub fn authenticate(&self, data: &[u8]) -> [u8; 32] { + let mut mac: hmac::Hmac = Mac::new_from_slice(&self.0).unwrap(); + mac.update(data); + mac.finalize().into_bytes().into() + } +} + +#[derive(Default)] +pub struct Map(Vec<(Value, Value)>); + +impl Map { + pub fn push(&mut self, key: impl Into, value: impl Into) { + self.0.push((key.into(), value.into())); + } +} + +impl From for Value { + fn from(map: Map) -> Value { + Value::from(map.0) + } +} + +pub trait Request: Into { + const COMMAND: u8; + + type Reply: From; +} + +pub struct ClientPin { + protocol: u8, + subcommand: u8, + pub key_agreement: Option, + pub pin_auth: Option<[u8; 32]>, + pub new_pin_enc: Option>, + pub pin_hash_enc: Option>, + pub permissions: Option, + pub rp_id: Option, +} + +impl ClientPin { + pub fn new(protocol: u8, subcommand: u8) -> Self { + Self { + protocol, + subcommand, + key_agreement: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + pin_auth: None, + rp_id: None, + } + } +} + +impl From for Value { + fn from(request: ClientPin) -> Self { + let mut map = Map::default(); + map.push(1, request.protocol); + map.push(2, request.subcommand); + if let Some(key_agreement) = request.key_agreement { + map.push(3, key_agreement); + } + if let Some(pin_auth) = request.pin_auth { + map.push(4, pin_auth.as_slice()); + } + if let Some(new_pin_enc) = request.new_pin_enc { + map.push(5, new_pin_enc); + } + if let Some(pin_hash_enc) = request.pin_hash_enc { + map.push(6, pin_hash_enc); + } + if let Some(permissions) = request.permissions { + map.push(9, permissions); + } + if let Some(rp_id) = request.rp_id { + map.push(0x0a, rp_id); + } + map.into() + } +} + +impl Request for ClientPin { + const COMMAND: u8 = 0x06; + + type Reply = ClientPinReply; +} + +pub struct ClientPinReply { + pub key_agreement: Option, + pub pin_token: Option, + pub pin_retries: Option, +} + +impl From for ClientPinReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + key_agreement: map.remove(&1), + pin_token: map.remove(&2), + pin_retries: map.remove(&3).map(|value| value.deserialized().unwrap()), + } + } +} + +#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] +pub struct Rp { + pub id: String, + pub name: Option, +} + +impl Rp { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + name: None, + } + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } +} + +impl From for Value { + fn from(rp: Rp) -> Value { + let mut map = Map::default(); + map.push("id", rp.id); + if let Some(name) = rp.name { + map.push("name", name); + } + map.into() + } +} + +impl From for Rp { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + id: map.remove("id").unwrap().deserialized().unwrap(), + name: map + .remove("name") + .map(|value| value.deserialized().unwrap()), + } + } +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct User { + pub id: Vec, + pub name: Option, + pub display_name: Option, +} + +impl User { + pub fn new(id: impl Into>) -> Self { + Self { + id: id.into(), + name: None, + display_name: None, + } + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn display_name(mut self, display_name: impl Into) -> Self { + self.display_name = Some(display_name.into()); + self + } +} + +impl From for Value { + fn from(user: User) -> Value { + let mut map = Map::default(); + map.push("id", user.id); + if let Some(name) = user.name { + map.push("name", name); + } + if let Some(display_name) = user.display_name { + map.push("displayName", display_name); + } + map.into() + } +} + +impl From for User { + fn from(value: Value) -> User { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + id: map.remove("id").unwrap().into_bytes().unwrap(), + name: map + .remove("name") + .map(|value| value.deserialized().unwrap()), + display_name: map + .remove("displayName") + .map(|value| value.deserialized().unwrap()), + } + } +} + +#[derive(Clone)] +pub struct PubKeyCredParam { + ty: String, + alg: i32, +} + +impl PubKeyCredParam { + pub fn new(ty: impl Into, alg: impl Into) -> Self { + Self { + ty: ty.into(), + alg: alg.into(), + } + } +} + +impl From for Value { + fn from(param: PubKeyCredParam) -> Value { + let mut map = Map::default(); + map.push("type", param.ty); + map.push("alg", param.alg); + map.into() + } +} + +pub struct MakeCredential { + client_data_hash: Vec, + rp: Rp, + user: User, + pub_key_cred_params: Vec, + pub extensions: Option, + pub options: Option, + pub pin_auth: Option<[u8; 32]>, + pub pin_protocol: Option, + pub attestation_formats_preference: Option>, +} + +impl MakeCredential { + pub fn new( + client_data_hash: impl Into>, + rp: Rp, + user: User, + pub_key_cred_params: impl Into>, + ) -> Self { + Self { + client_data_hash: client_data_hash.into(), + rp, + user, + pub_key_cred_params: pub_key_cred_params.into(), + extensions: None, + options: None, + pin_auth: None, + pin_protocol: None, + attestation_formats_preference: None, + } + } +} + +impl From for Value { + fn from(request: MakeCredential) -> Value { + let mut map = Map::default(); + map.push(1, request.client_data_hash); + map.push(2, request.rp); + map.push(3, request.user); + map.push( + 4, + request + .pub_key_cred_params + .into_iter() + .map(Value::from) + .collect::>(), + ); + if let Some(extensions) = request.extensions { + map.push(6, extensions); + } + if let Some(options) = request.options { + map.push(7, options); + } + if let Some(pin_auth) = request.pin_auth { + map.push(8, pin_auth.as_slice()); + } + if let Some(pin_protocol) = request.pin_protocol { + map.push(9, pin_protocol); + } + if let Some(attestation_formats_preference) = request.attestation_formats_preference { + let preference: Vec<_> = attestation_formats_preference + .into_iter() + .map(Value::from) + .collect(); + map.push(0x0b, preference); + } + map.into() + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct MakeCredentialExtensionsInput { + pub hmac_secret: Option, + pub third_party_payment: Option, +} + +impl Exhaustive for MakeCredentialExtensionsInput { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + hmac_secret: Option, + third_party_payment: Option, + } + } +} + +impl From for Value { + fn from(extensions: MakeCredentialExtensionsInput) -> Value { + let mut map = Map::default(); + if let Some(hmac_secret) = extensions.hmac_secret { + map.push("hmac-secret", hmac_secret); + } + if let Some(third_party_payment) = extensions.third_party_payment { + map.push("thirdPartyPayment", third_party_payment); + } + map.into() + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct MakeCredentialOptions { + pub rk: Option, + pub up: Option, + pub uv: Option, +} + +impl MakeCredentialOptions { + pub fn rk(mut self, rk: bool) -> Self { + self.rk = Some(rk); + self + } + + pub fn up(mut self, up: bool) -> Self { + self.up = Some(up); + self + } + + pub fn uv(mut self, uv: bool) -> Self { + self.uv = Some(uv); + self + } +} + +impl From for Value { + fn from(options: MakeCredentialOptions) -> Value { + let mut map = Map::default(); + if let Some(rk) = options.rk { + map.push("rk", rk); + } + if let Some(up) = options.up { + map.push("up", up); + } + if let Some(uv) = options.uv { + map.push("uv", uv); + } + map.into() + } +} + +impl Exhaustive for MakeCredentialOptions { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + rk: Option, + up: Option, + uv: Option, + } + } +} + +impl Request for MakeCredential { + const COMMAND: u8 = 0x01; + + type Reply = MakeCredentialReply; +} + +pub enum AttStmtFormat { + None, + Packed, +} + +impl AttStmtFormat { + pub fn as_str(&self) -> &'static str { + match self { + Self::None => "none", + Self::Packed => "packed", + } + } +} + +#[derive(Debug, PartialEq, Deserialize)] +pub struct AttStmt(BTreeMap); + +impl AttStmt { + pub fn validate(&self, format: AttStmtFormat, auth_data: &AuthData) { + match format { + AttStmtFormat::Packed => { + let alg = self.0.get("alg").unwrap(); + let x5c = self.0.get("x5c").unwrap().as_array().unwrap(); + let cert = x5c.first().unwrap().as_bytes().unwrap(); + let sig = self.0.get("sig").unwrap().as_bytes().unwrap(); + assert_eq!(alg, &Value::from(-7)); + + let (rest, cert) = x509_parser::parse_x509_certificate(cert).unwrap(); + assert!(rest.is_empty()); + let signature = DerSignature::from_bytes(sig).unwrap(); + let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap(); + let x509_parser::public_key::PublicKey::EC(ec_point) = public_key else { + panic!("unexpected public key in attestation certificate"); + }; + let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap(); + public_key.verify(&auth_data.bytes, &signature).unwrap(); + } + AttStmtFormat::None => { + assert!(self.0.is_empty()); + } + } + } +} + +#[derive(Debug, PartialEq)] +pub struct MakeCredentialReply { + pub fmt: String, + pub auth_data: AuthData, + pub att_stmt: Option, +} + +impl From for MakeCredentialReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + fmt: map.remove(&1).unwrap().deserialized().unwrap(), + auth_data: map.remove(&2).unwrap().into(), + att_stmt: map.remove(&3).map(|value| value.deserialized().unwrap()), + } + } +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct PubKeyCredDescriptor { + pub ty: String, + pub id: Vec, +} + +impl PubKeyCredDescriptor { + pub fn new(ty: impl Into, id: impl Into>) -> Self { + Self { + ty: ty.into(), + id: id.into(), + } + } +} + +impl From for Value { + fn from(descriptor: PubKeyCredDescriptor) -> Value { + let mut map = Map::default(); + map.push("id", descriptor.id); + map.push("type", descriptor.ty); + map.into() + } +} + +impl From for PubKeyCredDescriptor { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + ty: map.remove("type").unwrap().into_text().unwrap(), + id: map.remove("id").unwrap().into_bytes().unwrap(), + } + } +} + +pub struct GetAssertion { + rp_id: String, + client_data_hash: Vec, + pub allow_list: Option>, + pub extensions: Option, + pub options: Option, +} + +impl GetAssertion { + pub fn new(rp_id: impl Into, client_data_hash: impl Into>) -> Self { + Self { + rp_id: rp_id.into(), + client_data_hash: client_data_hash.into(), + allow_list: None, + extensions: None, + options: None, + } + } +} + +impl From for Value { + fn from(request: GetAssertion) -> Value { + let mut map = Map::default(); + map.push(0x01, request.rp_id); + map.push(0x02, request.client_data_hash); + if let Some(allow_list) = request.allow_list { + let values: Vec<_> = allow_list.into_iter().map(Value::from).collect(); + map.push(0x03, values); + } + if let Some(extensions) = request.extensions { + map.push(0x04, extensions); + } + if let Some(options) = request.options { + map.push(0x05, options); + } + map.into() + } +} + +impl Request for GetAssertion { + const COMMAND: u8 = 0x02; + + type Reply = GetAssertionReply; +} + +#[derive(Clone, Debug, Default)] +pub struct GetAssertionExtensionsInput { + pub third_party_payment: Option, + pub hmac_secret: Option, +} + +impl From for Value { + fn from(extensions: GetAssertionExtensionsInput) -> Value { + let mut map = Map::default(); + if let Some(hmac_secret) = extensions.hmac_secret { + map.push("hmac-secret", hmac_secret); + } + if let Some(third_party_payment) = extensions.third_party_payment { + map.push("thirdPartyPayment", third_party_payment); + } + map.into() + } +} + +#[derive(Clone, Debug)] +pub struct HmacSecretInput { + pub key_agreement: PublicKey, + pub salt_enc: Vec, + pub salt_auth: [u8; 32], + pub pin_protocol: Option, +} + +impl From for Value { + fn from(input: HmacSecretInput) -> Value { + let mut map = Map::default(); + map.push(0x01, input.key_agreement); + map.push(0x02, input.salt_enc); + map.push(0x03, input.salt_auth.as_slice()); + if let Some(pin_protocol) = input.pin_protocol { + map.push(0x04, pin_protocol); + } + map.into() + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct GetAssertionOptions { + pub up: Option, + pub uv: Option, +} + +impl GetAssertionOptions { + pub fn up(mut self, up: bool) -> Self { + self.up = Some(up); + self + } + + pub fn uv(mut self, uv: bool) -> Self { + self.uv = Some(uv); + self + } +} + +impl From for Value { + fn from(options: GetAssertionOptions) -> Value { + let mut map = Map::default(); + if let Some(up) = options.up { + map.push("up", up); + } + if let Some(uv) = options.uv { + map.push("uv", uv); + } + map.into() + } +} + +impl Exhaustive for GetAssertionOptions { + fn iter_exhaustive() -> impl Iterator + Clone { + exhaustive_struct! { + up: Option, + uv: Option, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct GetAssertionReply { + pub credential: PubKeyCredDescriptor, + pub auth_data: AuthData, + pub signature: Vec, + pub number_of_credentials: Option, +} + +impl From for GetAssertionReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + credential: map.remove(&0x01).unwrap().into(), + auth_data: map.remove(&0x02).unwrap().into(), + signature: map.remove(&0x03).unwrap().into_bytes().unwrap(), + number_of_credentials: map.remove(&0x05).map(|value| value.deserialized().unwrap()), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct AuthData { + pub bytes: Vec, + pub flags: u8, + pub credential: Option, + pub extensions: Option>, +} + +impl AuthData { + pub fn up_flag(&self) -> bool { + self.flags & 0b1 != 0 + } + + pub fn uv_flag(&self) -> bool { + self.flags & 0b100 != 0 + } + + pub fn at_flag(&self) -> bool { + self.flags & 0b1000000 != 0 + } + + pub fn ed_flag(&self) -> bool { + self.flags & 0b10000000 != 0 + } +} + +impl From> for AuthData { + fn from(vec: Vec) -> Self { + let (_rp_id_hash, bytes) = vec.split_at(32); + let (&flags, bytes) = bytes.split_first().unwrap(); + let (_sign_count, bytes) = bytes.split_at(4); + let (credential, bytes) = if flags & 0b0100_0000 == 0b0100_0000 { + let (credential, bytes) = CredentialData::parse(bytes); + (Some(credential), bytes) + } else { + (None, bytes) + }; + let extensions = if flags & 0b1000_0000 == 0b1000_0000 { + Some(ciborium::from_reader(bytes).unwrap()) + } else { + None + }; + Self { + bytes: vec, + flags, + credential, + extensions, + } + } +} + +impl From for AuthData { + fn from(value: Value) -> Self { + value.into_bytes().unwrap().into() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct CredentialData { + pub id: Vec, + pub public_key: BTreeMap, +} + +impl CredentialData { + fn parse(bytes: &[u8]) -> (Self, &[u8]) { + let (_aaguid, bytes) = bytes.split_at(16); + let (id_length, bytes) = bytes.split_at(2); + let id_length = u16::from_be_bytes([id_length[0], id_length[1]]); + let (id, bytes) = bytes.split_at(id_length.into()); + let mut cursor = std::io::Cursor::new(bytes); + let public_key = ciborium::from_reader(&mut cursor).unwrap(); + let bytes = &bytes[cursor.position().try_into().unwrap()..]; + ( + Self { + id: id.into(), + public_key, + }, + bytes, + ) + } + + pub fn verify_assertion( + &self, + auth_data: &AuthData, + client_data_hash: &[u8], + signature: &[u8], + ) { + let kty = self.public_key.get(&1).unwrap(); + let alg = self.public_key.get(&3).unwrap(); + let crv = self.public_key.get(&-1).unwrap(); + let x = self + .public_key + .get(&-2) + .unwrap() + .as_bytes() + .unwrap() + .as_slice(); + let y = self + .public_key + .get(&-3) + .unwrap() + .as_bytes() + .unwrap() + .as_slice(); + assert_eq!(kty, &Value::from(2)); + assert_eq!(alg, &Value::from(-7)); + assert_eq!(crv, &Value::from(1)); + + let encoded = p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false); + let public_key = VerifyingKey::from_encoded_point(&encoded).unwrap(); + let signature = DerSignature::from_bytes(signature).unwrap(); + let mut message = Vec::new(); + message.extend_from_slice(&auth_data.bytes); + message.extend_from_slice(client_data_hash); + public_key.verify(&message, &signature).unwrap(); + } +} + +pub struct GetNextAssertion; + +impl From for Value { + fn from(_: GetNextAssertion) -> Self { + Self::Null + } +} + +impl Request for GetNextAssertion { + const COMMAND: u8 = 0x08; + + type Reply = GetAssertionReply; +} + +pub struct GetInfo; + +impl From for Value { + fn from(_: GetInfo) -> Self { + Self::Null + } +} + +impl Request for GetInfo { + const COMMAND: u8 = 0x04; + + type Reply = GetInfoReply; +} + +pub struct GetInfoReply { + pub versions: Vec, + pub aaguid: Value, + pub options: Option>, + pub pin_protocols: Option>, + pub attestation_formats: Option>, +} + +impl From for GetInfoReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + versions: map.remove(&1).unwrap().deserialized().unwrap(), + aaguid: map.remove(&3).unwrap().deserialized().unwrap(), + options: map.remove(&4).map(|value| value.deserialized().unwrap()), + pin_protocols: map.remove(&6).map(|value| value.deserialized().unwrap()), + attestation_formats: map.remove(&0x16).map(|value| value.deserialized().unwrap()), + } + } +} + +pub struct CredentialManagement { + pub subcommand: u8, + pub subcommand_params: Option, + pub pin_protocol: Option, + pub pin_auth: Option<[u8; 32]>, +} + +impl CredentialManagement { + pub fn new(subcommand: u8) -> Self { + Self { + subcommand, + subcommand_params: None, + pin_protocol: None, + pin_auth: None, + } + } +} + +impl From for Value { + fn from(request: CredentialManagement) -> Value { + let mut map = Map::default(); + map.push(1, request.subcommand); + if let Some(subcommand_params) = request.subcommand_params { + map.push(2, subcommand_params); + } + if let Some(pin_protocol) = request.pin_protocol { + map.push(3, pin_protocol); + } + if let Some(pin_auth) = request.pin_auth { + map.push(4, pin_auth.as_slice()); + } + map.into() + } +} + +impl Request for CredentialManagement { + const COMMAND: u8 = 0x0A; + + type Reply = CredentialManagementReply; +} + +#[derive(Clone, Default)] +pub struct CredentialManagementParams { + pub rp_id_hash: Option>, + pub credential_id: Option, + pub user: Option, +} + +impl CredentialManagementParams { + pub fn serialized(&self) -> Vec { + let mut serialized = Vec::new(); + ciborium::into_writer(&Value::from(self.clone()), &mut serialized).unwrap(); + serialized + } +} + +impl From for Value { + fn from(params: CredentialManagementParams) -> Value { + let mut map = Map::default(); + if let Some(rp_id_hash) = params.rp_id_hash { + map.push(1, rp_id_hash); + } + if let Some(credential_id) = params.credential_id { + map.push(2, credential_id); + } + if let Some(user) = params.user { + map.push(3, user); + } + map.into() + } +} + +pub struct CredentialManagementReply { + pub existing_resident_credentials_count: Option, + pub max_possible_remaining_resident_credentials_count: Option, + pub rp: Option, + pub rp_id_hash: Option, + pub total_rps: Option, + pub user: Option, + pub credential_id: Option, + pub total_credentials: Option, + pub third_party_payment: Option, +} + +impl From for CredentialManagementReply { + fn from(value: Value) -> Self { + let mut map: BTreeMap = value.deserialized().unwrap(); + Self { + existing_resident_credentials_count: map + .remove(&1) + .map(|value| value.deserialized().unwrap()), + max_possible_remaining_resident_credentials_count: map + .remove(&2) + .map(|value| value.deserialized().unwrap()), + rp: map.remove(&3), + rp_id_hash: map.remove(&4), + total_rps: map.remove(&5).map(|value| value.deserialized().unwrap()), + user: map.remove(&6), + credential_id: map.remove(&7).map(|value| value.into()), + total_credentials: map.remove(&9).map(|value| value.deserialized().unwrap()), + third_party_payment: map.remove(&0x0c).map(|value| value.deserialized().unwrap()), + } + } +}