diff --git a/crates/ctap2-platform/Cargo.toml b/crates/ctap2-platform/Cargo.toml new file mode 100644 index 0000000..aabac88 --- /dev/null +++ b/crates/ctap2-platform/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ctap2-platform" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aes = { version = "0.8.2", default-features = false, features = ["zeroize"] } +cbc = { version = "0.1.2", default-features = false, features = ["zeroize"] } +cosey = "0.3.0" +ctap2-proto = { path = "../ctap2-proto" } +hmac = { version = "0.12.1", default-features = false } +p256 = { version = "0.13.2", default-features = false, features = ["ecdh", "sha2", "sha256", "pkcs8"] } +rand = { version = "0.8.5", features = ["getrandom"], default-features = false } +sha2 = { version = "0.10.6", default-features = false } diff --git a/crates/ctap2-platform/src/lib.rs b/crates/ctap2-platform/src/lib.rs new file mode 100644 index 0000000..464ff24 --- /dev/null +++ b/crates/ctap2-platform/src/lib.rs @@ -0,0 +1,144 @@ +#![feature(split_array)] +use aes::{ + cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}, + Block, +}; +use ctap2_proto::{ + authenticator::client_pin::auth_protocol, + prelude::{ + client_pin::{ + self, + auth_protocol::{platform, BLOCK_SIZE}, + }, + Sha256Hash, + }, +}; +use hmac::{Hmac, Mac}; +use p256::{ecdh::EphemeralSecret, EncodedPoint, PublicKey}; +use rand::rngs::OsRng; +use sha2::{Digest, Sha256}; + +const IV: [u8; BLOCK_SIZE] = [0; BLOCK_SIZE]; + +pub struct Session { + platform_key_agreement_key: cosey::PublicKey, + shared_secret: Sha256Hash, +} + +impl Session { + fn ecdh( + private_key_agreement_key: EphemeralSecret, + peer_cose_key: cosey::PublicKey, + ) -> Result<[u8; 32], client_pin::Error> { + // 1. Parse peerCoseKey as specified for getPublicKey, below, and produce a + // P-256 point, Y. If unsuccessful, or if the resulting point is not on + // the curve, return error. + let cosey::PublicKey::EcdhEsHkdf256Key(peer_public_key) = peer_cose_key else { + return Err(client_pin::Error::InvalidParameter); + }; + + // Magical SEC1 incantation + let mut encoded = 0x04u8.to_be_bytes().to_vec(); + encoded.extend_from_slice(&peer_public_key.x); + encoded.extend_from_slice(&peer_public_key.y); + + let peer_public_key = PublicKey::from_sec1_bytes(&encoded).unwrap(); + + // 2. Calculate xY, the shared point. (I.e. the scalar-multiplication of the + // peer’s point, Y, with the local private key agreement key.) + // 3. Let Z be the 32-byte, big-endian encoding of the x-coordinate of the + // shared point. + let z = private_key_agreement_key + .diffie_hellman(&peer_public_key) + .raw_secret_bytes() + .as_slice() + .to_owned(); + + // 4. Return kdf(Z). + Ok(Self::kdf(z.as_slice())) + } + + /// Return SHA-256(Z) + fn kdf(z: &[u8]) -> Sha256Hash { + Sha256::digest(z).into() + } +} + +impl platform::Session<{ auth_protocol::Version::One }> for Session { + type Error = client_pin::Error; + + fn initialize(peer_cose_key: cosey::PublicKey) -> Result { + let platform_key_agreement_key = EphemeralSecret::random(&mut OsRng); + let public_key = platform_key_agreement_key.public_key(); + + let public_key = EncodedPoint::from(&public_key); + + let public_key = cosey::P256PublicKey { + x: cosey::Bytes::from_slice(public_key.x().unwrap().as_slice()).unwrap(), + y: cosey::Bytes::from_slice(public_key.y().unwrap().as_slice()).unwrap(), + }; + + let shared_secret = Self::ecdh(platform_key_agreement_key, peer_cose_key)?; + Ok(Self { + platform_key_agreement_key: public_key.into(), + shared_secret, + }) + } + + fn platform_key_agreement_key(&self) -> &cosey::PublicKey { + &self.platform_key_agreement_key + } + + fn encrypt( + &self, + plaintext: &[[u8; BLOCK_SIZE]; N], + ) -> Result<[[u8; BLOCK_SIZE]; N], Self::Error> { + // Return the AES-256-CBC encryption of demPlaintext using an all-zero + // IV. (No padding is performed as the size of demPlaintext is required + // to be a multiple of the AES block length.) + + let mut ciphertext = plaintext.map(Block::from); + + cbc::Encryptor::::new(&self.shared_secret.into(), &IV.into()) + .encrypt_blocks_mut(&mut ciphertext); + + let ciphertext = ciphertext.map(|block| block.into()); + + Ok(ciphertext) + } + + fn decrypt(&self, ciphertext: &[[u8; 16]; N]) -> [[u8; 16]; N] { + // If the size of demCiphertext is not a multiple of the AES block length, + // return error. Otherwise return the AES-256-CBC decryption of demCiphertext + // using an all-zero IV. + + let mut plaintext = ciphertext.map(Block::from); + + cbc::Decryptor::::new(&self.shared_secret.into(), &IV.into()) + .decrypt_blocks_mut(&mut plaintext); + + let plaintext = plaintext.map(|block| block.into()); + + plaintext + } + + fn authenticate(&self, message: &[u8]) -> Result<[u8; 16], Self::Error> { + // Return the first 16 bytes of the result of computing HMAC-SHA-256 with the + // given key and message. + + let mut mac = Hmac::::new_from_slice(&self.shared_secret) + .expect("HMAC can take key of any size"); + + mac.update(message); + + let result = mac + .finalize() + .into_bytes() + .as_slice() + .split_array_ref() + .0 + .to_owned(); + + Ok(result) + } +} diff --git a/crates/ctap2-proto/src/authenticator/credential/management.rs b/crates/ctap2-proto/src/authenticator/credential/management.rs index cfdb4f2..2ab57ea 100644 --- a/crates/ctap2-proto/src/authenticator/credential/management.rs +++ b/crates/ctap2-proto/src/authenticator/credential/management.rs @@ -1,52 +1,79 @@ use crate::{authenticator::client_pin, extensions::cred_protect, Sha256Hash}; +use client_pin::PinUvAuthParam; use fido_common::credential::public_key; +<<<<<<< Updated upstream pub type PinUvAuthParam = [u8; 16]; #[derive(Clone, Copy)] +======= +use std::{borrow::Cow, fmt::Display}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "serde")] +mod raw; + +#[cfg(feature = "serde")] +use raw::{RawRequest, RawResponse}; + +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(try_from = "RawRequest<'a>", into = "RawRequest<'a>") +)] +>>>>>>> Stashed changes pub enum Request<'a> { GetCredentialsMetadata { /// > PIN/UV protocol version chosen by the platform. pin_uv_auth_protocol: client_pin::AuthProtocolVersion, /// > First 16 bytes of HMAC-SHA-256 of contents using `pinUvAuthToken`. - pin_uv_auth_param: &'a PinUvAuthParam, + pin_uv_auth_param: PinUvAuthParam, }, EnumerateRPsBegin { /// > PIN/UV protocol version chosen by the platform. pin_uv_auth_protocol: client_pin::AuthProtocolVersion, /// > First 16 bytes of HMAC-SHA-256 of contents using `pinUvAuthToken`. - pin_uv_auth_param: &'a PinUvAuthParam, + pin_uv_auth_param: PinUvAuthParam, }, EnumerateRPsGetNextRP, EnumerateCredentialsBegin { /// The ID of the relying party to enumerate credentials for. - relying_party_id_hash: &'a Sha256Hash, + relying_party_id_hash: Sha256Hash, /// > PIN/UV protocol version chosen by the platform. pin_uv_auth_protocol: client_pin::AuthProtocolVersion, /// > First 16 bytes of HMAC-SHA-256 of contents using `pinUvAuthToken`. - pin_uv_auth_param: &'a PinUvAuthParam, + pin_uv_auth_param: PinUvAuthParam, }, EnumerateCredentialsGetNextCredential, DeleteCredential { /// The ID of the credential to delete. - credential_id: &'a public_key::Descriptor, + credential_id: Cow<'a, public_key::Descriptor>, /// > PIN/UV protocol version chosen by the platform. pin_uv_auth_protocol: client_pin::AuthProtocolVersion, /// > First 16 bytes of HMAC-SHA-256 of contents using `pinUvAuthToken`. - pin_uv_auth_param: &'a PinUvAuthParam, + pin_uv_auth_param: PinUvAuthParam, }, UpdateUserInformation { /// The ID of the credential to update. - credential_id: &'a public_key::Descriptor, + credential_id: Cow<'a, public_key::Descriptor>, /// The updated user information. - user: &'a public_key::UserEntity, + user: Cow<'a, public_key::UserEntity>, /// > PIN/UV protocol version chosen by the platform. pin_uv_auth_protocol: client_pin::AuthProtocolVersion, /// > First 16 bytes of HMAC-SHA-256 of contents using `pinUvAuthToken`. - pin_uv_auth_param: &'a PinUvAuthParam, + pin_uv_auth_param: PinUvAuthParam, }, } +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(into = "RawResponse", try_from = "RawResponse") +)] pub enum Response { GetCredentialsMetadata { /// > Number of existing discoverable credentials present on the @@ -77,6 +104,7 @@ pub enum Response { UpdateUserInformation, } +#[derive(Debug, Clone)] pub struct RelyingParty { /// The description of the relying party. pub relying_party: public_key::RelyingPartyEntity, @@ -84,6 +112,7 @@ pub struct RelyingParty { pub relying_party_id_hash: Sha256Hash, } +#[derive(Debug, Clone)] pub struct Credential { /// The description of the user account associated with the credential. pub user: public_key::UserEntity, @@ -98,6 +127,7 @@ pub struct Credential { pub large_blob_key: Vec, } +#[derive(Debug, Clone)] pub enum Error { PinUvAuthTokenRequired, MissingParameter, diff --git a/crates/ctap2-proto/src/authenticator/credential/management/raw.rs b/crates/ctap2-proto/src/authenticator/credential/management/raw.rs new file mode 100644 index 0000000..bbe969c --- /dev/null +++ b/crates/ctap2-proto/src/authenticator/credential/management/raw.rs @@ -0,0 +1,414 @@ +use crate::{prelude::client_pin::{PinUvAuthParam, self}, extensions::cred_protect}; + +use super::{Error, Request, Response, RelyingParty, Credential}; +use fido_common::{credential::public_key, Sha256Hash}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, Bytes}; +use std::borrow::Cow; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(into = "u8", try_from = "u8")] +pub(super) enum RawSubcommand { + GetCredsMetadata = 0x01, + EnumerateRpsBegin = 0x02, + EnumerateRpsGetNextRp = 0x03, + EnumerateCredentialsBegin = 0x04, + EnumerateCredentialsGetNextCredential = 0x05, + DeleteCredential = 0x06, + UpdateUserInformation = 0x07, +} + +impl From for u8 { + fn from(val: RawSubcommand) -> Self { + val as u8 + } +} + +impl TryFrom for RawSubcommand { + type Error = Error; + + fn try_from(value: u8) -> Result { + Ok(match value { + 0x01 => RawSubcommand::GetCredsMetadata, + 0x02 => RawSubcommand::EnumerateRpsBegin, + 0x03 => RawSubcommand::EnumerateRpsGetNextRp, + 0x04 => RawSubcommand::EnumerateCredentialsBegin, + 0x05 => RawSubcommand::EnumerateCredentialsGetNextCredential, + 0x06 => RawSubcommand::DeleteCredential, + 0x07 => RawSubcommand::UpdateUserInformation, + _ => return Err(Error::InvalidParameter), + }) + } +} + +#[serde_as] +#[derive(Clone, Serialize, Deserialize)] +pub(super) struct RawSubcommandParams<'a> { + #[serde(rename = 0x01)] + pub rp_id_hash: Option, + #[serde(rename = 0x02)] + pub credential_id: Option>, + #[serde(rename = 0x03)] + pub user: Option>, +} + +#[serde_as] +#[derive(Clone, Serialize, Deserialize)] +pub(super) struct RawRequest<'a> { + #[serde(rename = 0x01)] + pub subcommand: RawSubcommand, + #[serde(rename = 0x02, skip_serializing_if = "Option::is_none")] + pub subcommand_params: Option>, + #[serde(rename = 0x03)] + pub pin_uv_auth_protocol: client_pin::auth_protocol::Version, + #[serde_as(as = "Bytes")] + #[serde(rename = 0x04)] + pub pin_uv_auth_param: PinUvAuthParam, +} + +impl<'a> From> for RawRequest<'a> { + fn from(value: Request<'a>) -> Self { + match value { + Request::GetCredentialsMetadata { + pin_uv_auth_protocol, + pin_uv_auth_param, + } => RawRequest { + subcommand: RawSubcommand::GetCredsMetadata, + subcommand_params: None, + pin_uv_auth_protocol, + pin_uv_auth_param, + }, + Request::EnumerateRPsBegin { + pin_uv_auth_protocol, + pin_uv_auth_param, + } => RawRequest { + subcommand: RawSubcommand::EnumerateRpsBegin, + subcommand_params: None, + pin_uv_auth_protocol, + pin_uv_auth_param, + }, + Request::EnumerateRPsGetNextRP => todo!(), + Request::EnumerateCredentialsBegin { + relying_party_id_hash, + pin_uv_auth_protocol, + pin_uv_auth_param, + } => RawRequest { + subcommand: RawSubcommand::EnumerateCredentialsBegin, + subcommand_params: Some(RawSubcommandParams { + rp_id_hash: Some(relying_party_id_hash), + credential_id: None, + user: None, + }), + pin_uv_auth_protocol, + pin_uv_auth_param, + }, + Request::EnumerateCredentialsGetNextCredential => todo!(), + Request::DeleteCredential { + credential_id, + pin_uv_auth_protocol, + pin_uv_auth_param, + } => RawRequest { + subcommand: RawSubcommand::DeleteCredential, + subcommand_params: Some(RawSubcommandParams { + rp_id_hash: None, + credential_id: Some(credential_id), + user: None, + }), + pin_uv_auth_protocol, + pin_uv_auth_param, + }, + Request::UpdateUserInformation { + credential_id, + user, + pin_uv_auth_protocol, + pin_uv_auth_param, + } => RawRequest { + subcommand: RawSubcommand::UpdateUserInformation, + subcommand_params: Some(RawSubcommandParams { + rp_id_hash: None, + credential_id: Some(credential_id), + user: Some(user), + }), + pin_uv_auth_protocol, + pin_uv_auth_param, + }, + } + } +} + +impl<'a> TryFrom> for Request<'a> { + type Error = Error; + + fn try_from(value: RawRequest<'a>) -> Result { + Ok(match value.subcommand { + RawSubcommand::GetCredsMetadata => Request::GetCredentialsMetadata { + pin_uv_auth_protocol: value.pin_uv_auth_protocol, + pin_uv_auth_param: value.pin_uv_auth_param, + }, + RawSubcommand::EnumerateRpsBegin => Request::EnumerateRPsBegin { + pin_uv_auth_protocol: value.pin_uv_auth_protocol, + pin_uv_auth_param: value.pin_uv_auth_param, + }, + RawSubcommand::EnumerateRpsGetNextRp => Request::EnumerateRPsGetNextRP, + RawSubcommand::EnumerateCredentialsBegin => Request::EnumerateCredentialsBegin { + relying_party_id_hash: value + .subcommand_params + .ok_or(Error::MissingParameter)? + .rp_id_hash + .ok_or(Error::MissingParameter)?, + pin_uv_auth_protocol: value.pin_uv_auth_protocol, + pin_uv_auth_param: value.pin_uv_auth_param, + }, + RawSubcommand::EnumerateCredentialsGetNextCredential => { + Request::EnumerateCredentialsGetNextCredential + } + RawSubcommand::DeleteCredential => Request::DeleteCredential { + credential_id: value + .subcommand_params + .ok_or(Error::MissingParameter)? + .credential_id + .ok_or(Error::MissingParameter)?, + pin_uv_auth_protocol: value.pin_uv_auth_protocol, + pin_uv_auth_param: value.pin_uv_auth_param, + }, + RawSubcommand::UpdateUserInformation => { + let subcommand_params = value.subcommand_params.ok_or(Error::MissingParameter)?; + Request::UpdateUserInformation { + credential_id: subcommand_params + .credential_id + .ok_or(Error::MissingParameter)?, + user: subcommand_params.user.ok_or(Error::MissingParameter)?, + pin_uv_auth_protocol: value.pin_uv_auth_protocol, + pin_uv_auth_param: value.pin_uv_auth_param, + } + } + }) + } +} + +#[derive(Serialize, Deserialize)] +pub(super) struct RawResponse { + #[serde(rename = 0x01)] + pub existing_resident_credentials_count: Option, + #[serde(rename = 0x02)] + pub max_possible_remaining_resident_credentials_count: Option, + #[serde(rename = 0x03)] + pub rp: Option, + #[serde(rename = 0x04)] + pub rp_id_hash: Option, + #[serde(rename = 0x05)] + pub total_rps: Option, + #[serde(rename = 0x06)] + pub user: Option, + #[serde(rename = 0x07)] + pub credential_id: Option, + #[serde(rename = 0x08)] + pub public_key: Option>, // TODO: COSE_Key type + #[serde(rename = 0x09)] + pub total_credentials: Option, + #[serde(rename = 0x0A)] + pub cred_protect: Option, + #[serde(rename = 0x0B)] + pub large_blob_key: Option>, + #[serde(rename = 0x0C)] + pub third_party_payment: Option, +} + +impl From for RawResponse { + fn from(value: Response) -> Self { + match value { + Response::GetCredentialsMetadata { + existing_resident_credentials_count, + max_possible_remaining_resident_credentials_count, + } => RawResponse { + existing_resident_credentials_count: Some(existing_resident_credentials_count), + max_possible_remaining_resident_credentials_count: Some( + max_possible_remaining_resident_credentials_count, + ), + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + third_party_payment: None, + }, + Response::EnumerateRPsBegin { + relying_party, + total_relying_parties, + } => RawResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: Some(relying_party.relying_party), + rp_id_hash: Some(relying_party.relying_party_id_hash), + total_rps: Some(total_relying_parties), + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + third_party_payment: None, + }, + Response::EnumerateRPsGetNextRP { relying_party } => RawResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: Some(relying_party.relying_party), + rp_id_hash: Some(relying_party.relying_party_id_hash), + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + third_party_payment: None, + }, + Response::EnumerateCredentialsBegin { + credential, + total_credentials, + } => RawResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: Some(credential.user), + credential_id: Some(credential.credential_id), + public_key: None, + total_credentials: Some(total_credentials), + cred_protect: None, + large_blob_key: None, + third_party_payment: None, + }, + Response::EnumerateCredentialsGetNextCredential { credential } => RawResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: Some(credential.user), + credential_id: Some(credential.credential_id), + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + third_party_payment: None, + }, + Response::DeleteCredential => RawResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + third_party_payment: None, + }, + Response::UpdateUserInformation => RawResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + third_party_payment: None, + }, + } + } +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: RawResponse) -> Result { + // These values are manually checked becaues I guess the client is expected to + // hold the state of which type of request was made and subsequently + // which type of response it is expecting, and thus the response does + // not have a subcommand field. + // + // This should be compared against other implementations to see if there is a + // better way to handle this. + match value { + RawResponse { + existing_resident_credentials_count: Some(existing), + max_possible_remaining_resident_credentials_count: Some(max), + .. + } => Ok(Self::GetCredentialsMetadata { + existing_resident_credentials_count: existing, + max_possible_remaining_resident_credentials_count: max, + }), + RawResponse { + rp: Some(rp), + rp_id_hash: Some(rp_id_hash), + total_rps: Some(total_rps), + .. + } => Ok(Self::EnumerateRPsBegin { + relying_party: RelyingParty { + relying_party: rp, + relying_party_id_hash: rp_id_hash, + }, + total_relying_parties: total_rps, + }), + RawResponse { + rp: Some(rp), + rp_id_hash: Some(rp_id_hash), + .. + } => Ok(Self::EnumerateRPsGetNextRP { + relying_party: RelyingParty { + relying_party: rp, + relying_party_id_hash: rp_id_hash, + }, + }), + RawResponse { + user: Some(user), + credential_id: Some(credential_id), + public_key: Some(public_key), + total_credentials: Some(total_credentials), + cred_protect: Some(cred_protect), + large_blob_key: Some(large_blob_key), + .. + } => Ok(Self::EnumerateCredentialsBegin { + credential: Credential { + user, + credential_id, + public_key, + credential_protection_policy: cred_protect, + large_blob_key, + }, + total_credentials, + }), + RawResponse { + user: Some(user), + public_key: Some(public_key), + credential_id: Some(credential_id), + cred_protect: Some(cred_protect), + large_blob_key: Some(large_blob_key), + .. + } => Ok(Self::EnumerateCredentialsGetNextCredential { + credential: Credential { + user, + credential_id, + public_key, + credential_protection_policy: cred_protect, + large_blob_key, + }, + }), + // Looks like we have to have the context of what the request was to + // know the response type when no fields are set... + _ => todo!(), + } + } +} diff --git a/crates/fido-common/src/lib.rs b/crates/fido-common/src/lib.rs index 9f605e7..eebe9bd 100644 --- a/crates/fido-common/src/lib.rs +++ b/crates/fido-common/src/lib.rs @@ -7,3 +7,5 @@ pub mod extensions; pub mod registry; pub type Sha256Hash = [u8; 32]; + + diff --git a/crates/hid-ctap2-proto/Cargo.toml b/crates/hid-ctap2-proto/Cargo.toml index 4147b31..42cc8ae 100644 --- a/crates/hid-ctap2-proto/Cargo.toml +++ b/crates/hid-ctap2-proto/Cargo.toml @@ -11,8 +11,19 @@ ciborium-io = "0.2.1" ctap2-proto = { path = "../ctap2-proto", features = ["serde"] } hidapi = { version = "2.3.2" } ctaphid = { version = "0.3.1", default_features = false } +<<<<<<< Updated upstream serde = "1.0.163" +======= +serde = "=1.0.136" +ctap2-platform = { path = "../ctap2-platform" } +sha2 = "0.10.6" +hmac = { version = "0.12.1", default-features = false } +>>>>>>> Stashed changes [dev-dependencies] rand = "0.8.5" env_logger = "0.10.0" +<<<<<<< Updated upstream +======= +hex = "0.4.3" +>>>>>>> Stashed changes diff --git a/crates/hid-ctap2-proto/src/hid.rs b/crates/hid-ctap2-proto/src/hid.rs index c988033..0f7f02d 100644 --- a/crates/hid-ctap2-proto/src/hid.rs +++ b/crates/hid-ctap2-proto/src/hid.rs @@ -78,7 +78,19 @@ mod device { } impl HidAuthenticator { +<<<<<<< Updated upstream fn send_raw(&self, command: Command, bytes: &[u8]) -> Result, ctaphid::error::Error> { +======= + fn send_ctap1_raw(&self, bytes: &[u8]) -> Result, ctaphid::error::Error> { + self.0.ctap1(bytes) + } + + fn send_ctap2_raw( + &self, + command: Command, + bytes: &[u8], + ) -> Result, ctaphid::error::Error> { +>>>>>>> Stashed changes self.0.ctap2(command as u8, bytes) } @@ -95,6 +107,7 @@ impl HidAuthenticator { } })?; +<<<<<<< Updated upstream let response = self.send_raw(command, &data)?; match ciborium::de::from_reader(response.as_slice()) { @@ -105,5 +118,23 @@ impl HidAuthenticator { todo!() } } +======= + let response = self.send_ctap2_raw(command, &data)?; + + ciborium::de::from_reader(response.as_slice()) + .map_err(|e| match e { + ciborium::de::Error::Io(io_err) => match io_err.kind() { + std::io::ErrorKind::UnexpectedEof => ctaphid::types::ParseError::NotEnoughData, + _ => todo!(), + }, + ciborium::de::Error::Syntax(_) => todo!(), + ciborium::de::Error::Semantic(pos, err) => { + panic!("semantic error at {pos:#?}: {err:#?}") + } + ciborium::de::Error::RecursionLimitExceeded => todo!(), + }) + .map_err(ctaphid::error::ResponseError::PacketParsingFailed) + .map_err(ctaphid::error::Error::ResponseError) +>>>>>>> Stashed changes } } diff --git a/crates/hid-ctap2-proto/src/lib.rs b/crates/hid-ctap2-proto/src/lib.rs index f0f717f..6986ba9 100644 --- a/crates/hid-ctap2-proto/src/lib.rs +++ b/crates/hid-ctap2-proto/src/lib.rs @@ -5,6 +5,12 @@ use hid::HidAuthenticator; pub mod hid; +<<<<<<< Updated upstream +======= +#[cfg(test)] +mod tests; + +>>>>>>> Stashed changes impl Ctap2_2Authenticator for HidAuthenticator { fn make_credential(&mut self, request: make::Request) -> Result { Ok(self @@ -23,6 +29,7 @@ impl Ctap2_2Authenticator for HidAuthenticator { } fn client_pin( +<<<<<<< Updated upstream _request: client_pin::Request, ) -> Result { todo!() @@ -150,3 +157,65 @@ mod tests { println!("response: {response:#?}"); } } +======= + &mut self, + request: client_pin::Request, + ) -> Result { + let response = self.send(Command::AuthenticatorClientPin, request.clone()); + match &request { + client_pin::Request::SetPin { .. } | client_pin::Request::ChangePin { .. } => { + // Workaround for ctaphid not supporting empty response types + let Err(ctaphid::error::Error::ResponseError( + ctaphid::error::ResponseError::PacketParsingFailed( + ctaphid::types::ParseError::NotEnoughData, + ), + )) = response else { + return Ok(response.unwrap()); + }; + Ok(client_pin::Response::SetPin) + } + _ => Ok(response.unwrap()), + } + } + + fn reset(&mut self) -> Result<(), reset::Error> { + use ctaphid::{ + error::{Error, ResponseError}, + types::ParseError, + }; + + let response = self.send::<_, ()>(Command::AuthenticatorReset, &[0u8; 0]); + + // Workaround for ctaphid not supporting empty response types + let Err(Error::ResponseError(ResponseError::PacketParsingFailed(ParseError::NotEnoughData))) = response else { + return Ok(response.unwrap()); + }; + + Ok(()) + } + + fn selection(&mut self) -> Result<(), ctap2_proto::authenticator::selection::Error> { + todo!() + } + + fn bio_enrollment( + &mut self, + request: bio_enrollment::Request, + ) -> Result { + todo!() + } + + fn credential_management( + &mut self, + request: management::Request, + ) -> Result { + Ok(self + .send(Command::PrototypeAuthenticatorCredentialManagement, request) + .unwrap()) + } + + fn authenticator_config(&mut self, request: config::Request) -> Result<(), config::Error> { + todo!() + } +} +>>>>>>> Stashed changes diff --git a/crates/hid-ctap2-proto/src/tests.rs b/crates/hid-ctap2-proto/src/tests.rs new file mode 100644 index 0000000..864bc77 --- /dev/null +++ b/crates/hid-ctap2-proto/src/tests.rs @@ -0,0 +1,152 @@ +extern crate hidapi; + +use crate::hid::HidAuthenticator; +use ctap2_proto::prelude::{ + client_pin::auth_protocol::{self, platform::Session}, + credential::public_key, + *, +}; +use rand::{distributions, Rng}; +use std::sync::{LazyLock, Mutex}; + +mod test_client_pin; +mod test_credential_management; + +static AUTHENTICATOR: LazyLock>> = + LazyLock::new(|| Mutex::new(get_authenticator())); + +fn init() { + let _ = env_logger::builder().is_test(true).try_init(); +} + +fn get_authenticator() -> Option { + init(); // done here to ensure init is only run once + + let hidapi = hidapi::HidApi::new().unwrap(); + for info in hidapi.device_list() { + let Ok(device) = info.open_device(&hidapi) else { + continue; + }; + let Ok(authenticator) = device.try_into() else { continue }; + return Some(authenticator); + } + + None +} + +fn get_session(authenticator: &mut HidAuthenticator) -> ctap2_platform::Session { + let info = authenticator.get_info(); + assert!(info + .pin_uv_auth_protocols + .unwrap() + .contains(&auth_protocol::Version::One)); + + let version = auth_protocol::Version::One; + + // Getting Shared Secret K + let req = client_pin::Request::GetKeyAgreement { version }; + println!("request: {req:#?}"); + let res = authenticator.client_pin(req).unwrap(); + println!("response: {res:#?}"); + + let client_pin::Response::GetKeyAgreement { key_agreement } = res else { + panic!("unexpected response"); + }; + let session = ctap2_platform::Session::initialize(key_agreement).unwrap(); + + return session; +} + +#[test] +fn test_get_info() { + let guard = AUTHENTICATOR.lock().unwrap(); + let authenticator = guard.as_ref().unwrap(); + + let info = authenticator.get_info(); + println!("deserialized: {:#?}", info); +} + +#[test] +fn test_reset() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let guarded_authenticator = guard.as_mut().unwrap(); + + println!("Remove and re-insert your YubiKey to perform the reset..."); + + // Wait for the authenticator to be removed + while let Some(_) = get_authenticator() { + std::thread::sleep(std::time::Duration::from_millis(500)); + } + + // Wait for the authenticator to be re-inserted + let authenticator = loop { + if let Some(authenticator) = get_authenticator() { + break authenticator; + } + std::thread::sleep(std::time::Duration::from_millis(500)); + }; + + *guarded_authenticator = authenticator; + + // Reset the authenticator + guarded_authenticator.reset().unwrap(); +} + +#[test] +fn test_make_credential() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let authenticator = guard.as_mut().unwrap(); + + let info = authenticator.get_info(); + + let client_data_hash: Vec = rand::thread_rng() + .sample_iter(&distributions::Standard) + .take(32) + .collect(); + + let user_id = rand::thread_rng() + .sample_iter(&distributions::Standard) + .take(32) + .collect(); + + let rp = public_key::RelyingPartyEntity { + id: "com.example".to_string(), + name: Some("Example Inc.".into()), + }; + + let user = public_key::UserEntity { + id: user_id, + name: Some("example_user".to_string()), + display_name: Some("Example User".to_string()), + }; + + let pub_key_params: Vec<_> = info.algorithms.unwrap().into_iter().collect(); + + let options = [(make::OptionKey::Discoverable, true)].into(); + + let req = make::Request::builder() + .client_data_hash(&client_data_hash.split_array_ref::<32>().0) + .relying_party(&rp) + .user(&user) + .public_key_credential_params(&pub_key_params) + .options(&options) + .build(); + + println!("request: {req:#?}"); + let response = authenticator.make_credential(req); + println!("response: {response:#?}"); + + let req = get::Request { + relying_party_id: &rp.id, + client_data_hash: client_data_hash.as_slice().try_into().unwrap(), + allow_list: None, + extensions: None, + options: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol_version: None, + }; + + println!("request: {req:#?}"); + let response = authenticator.get_assertion(req); + println!("response: {response:#?}"); +} diff --git a/crates/hid-ctap2-proto/src/tests/test_client_pin.rs b/crates/hid-ctap2-proto/src/tests/test_client_pin.rs new file mode 100644 index 0000000..48efba3 --- /dev/null +++ b/crates/hid-ctap2-proto/src/tests/test_client_pin.rs @@ -0,0 +1,247 @@ +use super::get_session; +use crate::tests::AUTHENTICATOR; +use ctap2_proto::{ + prelude::{ + client_pin::{ + auth_protocol::{self, platform::Session}, + Permission, Request, Response, + }, + device, + }, + Ctap2_2Authenticator, +}; +use sha2::Digest; + +#[test] +fn test_get_pin_retries() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let authenticator = guard.as_mut().unwrap(); + + let req = Request::GetPinRetries; + println!("request: {req:#?}"); + let res = authenticator.client_pin(req).unwrap(); + println!("response: {res:#?}"); + + let Response::GetPinRetries { + pin_retries, + power_cycle_state, + } = res + else { + panic!("unexpected response"); + }; + + println!( + "pin_retries: {:#?}, power_cycle_state: {:#?}", + pin_retries, power_cycle_state + ); +} + +#[test] +fn test_get_key_agreement() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let mut authenticator = guard.as_mut().unwrap(); + + get_session(&mut authenticator); +} + +#[test] +fn test_set_new_pin() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let mut authenticator = guard.as_mut().unwrap(); + + let version = auth_protocol::Version::One; + let session = get_session(&mut authenticator); + + let pin = "12345678"; + let mut padded_pin = [0u8; 64]; + pin.as_bytes().iter().enumerate().for_each(|(i, b)| { + padded_pin[i] = *b; + }); + let padded_pin: [[u8; 16]; 4] = unsafe { std::mem::transmute(padded_pin) }; + let new_pin_encrypted = session.encrypt(&padded_pin).unwrap(); + let new_pin_encrypted = new_pin_encrypted.into_iter().flatten().collect::>(); + + println!("new_pin_encrypted: {:#?}", new_pin_encrypted); + + // Set New Pin + let req = Request::SetPin { + key_agreement: session.platform_key_agreement_key().clone(), + new_pin_encrypted: new_pin_encrypted.split_array_ref::<64>().0.to_owned(), + pin_uv_auth_param: session.authenticate(&new_pin_encrypted).unwrap(), + version, + }; + println!("request: {req:#?}"); + let res = authenticator.client_pin(req).unwrap(); + println!("response: {res:#?}"); +} + +#[test] +fn test_change_pin() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let mut authenticator = guard.as_mut().unwrap(); + + let version = auth_protocol::Version::One; + let session = get_session(&mut authenticator); + + let old_pin = "12345678"; + let new_pin = "87654321"; + + let mut padded_new_pin = [0u8; 64]; + new_pin.as_bytes().iter().enumerate().for_each(|(i, b)| { + padded_new_pin[i] = *b; + }); + let padded_new_pin: [[u8; 16]; 4] = unsafe { std::mem::transmute(padded_new_pin) }; + let new_pin_encrypted = session.encrypt(&padded_new_pin).unwrap(); + let new_pin_encrypted = new_pin_encrypted.into_iter().flatten().collect::>(); + + println!("new_pin_encrypted: {:#?}", new_pin_encrypted); + + // pinHashEnc: The result of calling encrypt(shared secret, + // LEFT(SHA-256(curPin), 16)). + let pin_hash_encrypted = session + .encrypt(&[*sha2::Sha256::digest(old_pin.as_bytes()) + .split_array_ref::<16>() + .0]) + .unwrap()[0]; + + // newPinEnc: the result of calling encrypt(shared secret, paddedPin) + let new_pin_encrypted = session.encrypt(&padded_new_pin).unwrap(); + let new_pin_encrypted = unsafe { std::mem::transmute::<_, [u8; 64]>(new_pin_encrypted) }; + + // pinUvAuthParam: the result of calling authenticate(shared secret, newPinEnc + // || pinHashEnc). + let pin_uv_auth_param = session + .authenticate( + [new_pin_encrypted.as_slice(), pin_hash_encrypted.as_slice()] + .concat() + .as_slice(), + ) + .unwrap(); + + // Change Pin + let req = Request::ChangePin { + version, + pin_hash_encrypted, + new_pin_encrypted, + pin_uv_auth_param, + key_agreement: session.platform_key_agreement_key().clone(), + }; + println!("request: {req:#?}"); + let res = authenticator.client_pin(req).unwrap(); + println!("response: {res:#?}"); +} + +#[test] +fn test_get_pin_token() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let mut authenticator = guard.as_mut().unwrap(); + + let version = auth_protocol::Version::One; + let session = get_session(&mut authenticator); + + let pin = "87654321"; + + let mut padded_pin = [0u8; 64]; + pin.as_bytes().iter().enumerate().for_each(|(i, b)| { + padded_pin[i] = *b; + }); + let padded_pin: [[u8; 16]; 4] = unsafe { std::mem::transmute(padded_pin) }; + let new_pin_encrypted = session.encrypt(&padded_pin).unwrap(); + let new_pin_encrypted = new_pin_encrypted.into_iter().flatten().collect::>(); + + println!("new_pin_encrypted: {:#?}", new_pin_encrypted); + + // pinHashEnc: The result of calling encrypt(shared secret, + // LEFT(SHA-256(curPin), 16)). + let pin_hash_encrypted = session + .encrypt(&[*sha2::Sha256::digest(pin.as_bytes()) + .split_array_ref::<16>() + .0]) + .unwrap()[0]; + + // Get Pin Token + let req = Request::GetPinToken { + key_agreement: session.platform_key_agreement_key().clone(), + version, + pin_hash_encrypted, + }; + println!("request: {req:#?}"); + let res = authenticator.client_pin(req).unwrap(); + println!("response: {res:#?}"); +} + +#[test] +fn test_get_pin_uv_auth_token_using_uv_with_permissions() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let authenticator = guard.as_mut().unwrap(); + + let info = authenticator.get_info(); + + if let Some(options) = info.options { + if !options.contains_key(&device::OptionId::UserVerification) { + panic!("UserVerification not supported"); + } + } + + todo!() +} + +#[test] +fn test_get_uv_retries() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let authenticator = guard.as_mut().unwrap(); + + let options = authenticator.get_info().options.unwrap(); + if !options.contains_key(&device::OptionId::UserVerification) { + panic!("UserVerification not supported"); + } + + // Get UV Retries + let req = Request::GetUvRetries; + println!("request: {req:#?}"); + let res = authenticator.client_pin(req).unwrap(); + println!("response: {res:#?}"); +} + +#[test] +fn test_get_pin_uv_auth_token_using_pin_with_permissions() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let mut authenticator = guard.as_mut().unwrap(); + + let info = authenticator.get_info(); + if let Some(options) = info.options { + if !options + .get(&device::OptionId::UserVerification) + .unwrap_or(&false) + || !options.get(&device::OptionId::ClientPin).unwrap_or(&false) + { + panic!("UserVerification or ClientPin not supported"); + } + } + + let pin = "87654321"; + let pin_hash_encrypted = sha2::Sha256::digest(pin.as_bytes()) + .split_array_ref::<16>() + .0 + .to_owned(); + + let version = auth_protocol::Version::One; + let session = get_session(&mut authenticator); + + let req = Request::GetPinUvAuthTokenUsingPinWithPermissions { + version, + key_agreement: session.platform_key_agreement_key().clone(), + pin_hash_encrypted, + permissions: &[ + Permission::MakeCredential, + Permission::GetAssertion, + Permission::CredentialManagement, + ] + .into_iter() + .collect(), + relying_party_id: Some("example.com".into()), + }; + println!("request: {req:#?}"); + let res = authenticator.client_pin(req).unwrap(); + println!("response: {res:#?}"); +} diff --git a/crates/hid-ctap2-proto/src/tests/test_credential_management.rs b/crates/hid-ctap2-proto/src/tests/test_credential_management.rs new file mode 100644 index 0000000..46eb294 --- /dev/null +++ b/crates/hid-ctap2-proto/src/tests/test_credential_management.rs @@ -0,0 +1,67 @@ +use ctap2_proto::{ + prelude::{ + client_pin::{self, auth_protocol::platform::Session, Permission, PinUvAuthToken}, + credential, management, + }, + Ctap2_2Authenticator, +}; +use hmac::{Hmac, Mac}; +use sha2::Digest; + +use super::{get_session, AUTHENTICATOR}; + +#[test] +fn test_get_credentials_metadata() { + let mut guard = AUTHENTICATOR.lock().unwrap(); + let mut authenticator = guard.as_mut().unwrap(); + + let session = get_session(&mut authenticator); + + let pin = "87654321"; + + // pinHashEnc: The result of calling encrypt(shared secret, + // LEFT(SHA-256(curPin), 16)). + let pin_hash_encrypted = session + .encrypt(&[*sha2::Sha256::digest(pin.as_bytes()) + .split_array_ref::<16>() + .0]) + .unwrap()[0]; + + let req = client_pin::Request::GetPinToken { + version: client_pin::auth_protocol::Version::One, + key_agreement: session.platform_key_agreement_key().clone(), + pin_hash_encrypted, + }; + + let res = authenticator.client_pin(req).unwrap(); + + let client_pin::Response::GetPinToken { pin_uv_auth_token } = res else { + panic!("expected GetPinToken response"); + }; + + let PinUvAuthToken::Long(pin_uv_auth_token) = pin_uv_auth_token else { + panic!("Expected long pinuvauthtoken"); + }; + + // Decrypt pin_uv_auth_token + let encrypted: &[_; 2] = unsafe { std::mem::transmute(&pin_uv_auth_token) }; + let pin_uv_auth_token: [u8; 32] = unsafe { std::mem::transmute(session.decrypt(encrypted)) }; + + // pinAuth (0x04): LEFT(HMAC-SHA-256(pinToken, getCredsMetadata (0x01)), 16). + let mut mac = Hmac::::new_from_slice(pin_uv_auth_token.as_ref()).unwrap(); + + mac.update(&[0x01]); + + let pin_uv_auth_param: [u8; 16] = mac + .finalize() + .into_bytes() + .split_array_ref::<16>() + .0 + .to_owned(); + + let req = management::Request::GetCredentialsMetadata { + pin_uv_auth_protocol: client_pin::auth_protocol::Version::One, + pin_uv_auth_param, + }; + let res = authenticator.credential_management(req).unwrap(); +}