TEMP: in-progress

dev
Nick Zana 5 months ago
parent cf003d07e6
commit fa6a8d4c1e

@ -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 }

@ -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
// peers 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<Self, Self::Error> {
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<const N: usize>(
&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::<aes::Aes256>::new(&self.shared_secret.into(), &IV.into())
.encrypt_blocks_mut(&mut ciphertext);
let ciphertext = ciphertext.map(|block| block.into());
Ok(ciphertext)
}
fn decrypt<const N: usize>(&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::<aes::Aes256>::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::<Sha256>::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)
}
}

@ -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<u8>,
}
#[derive(Debug, Clone)]
pub enum Error {
PinUvAuthTokenRequired,
MissingParameter,

@ -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<RawSubcommand> for u8 {
fn from(val: RawSubcommand) -> Self {
val as u8
}
}
impl TryFrom<u8> for RawSubcommand {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
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<Sha256Hash>,
#[serde(rename = 0x02)]
pub credential_id: Option<Cow<'a, public_key::Descriptor>>,
#[serde(rename = 0x03)]
pub user: Option<Cow<'a, public_key::UserEntity>>,
}
#[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<RawSubcommandParams<'a>>,
#[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<Request<'a>> 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<RawRequest<'a>> for Request<'a> {
type Error = Error;
fn try_from(value: RawRequest<'a>) -> Result<Self, Self::Error> {
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<usize>,
#[serde(rename = 0x02)]
pub max_possible_remaining_resident_credentials_count: Option<usize>,
#[serde(rename = 0x03)]
pub rp: Option<public_key::RelyingPartyEntity>,
#[serde(rename = 0x04)]
pub rp_id_hash: Option<Sha256Hash>,
#[serde(rename = 0x05)]
pub total_rps: Option<usize>,
#[serde(rename = 0x06)]
pub user: Option<public_key::UserEntity>,
#[serde(rename = 0x07)]
pub credential_id: Option<public_key::Descriptor>,
#[serde(rename = 0x08)]
pub public_key: Option<Vec<u8>>, // TODO: COSE_Key type
#[serde(rename = 0x09)]
pub total_credentials: Option<usize>,
#[serde(rename = 0x0A)]
pub cred_protect: Option<cred_protect::Policy>,
#[serde(rename = 0x0B)]
pub large_blob_key: Option<Vec<u8>>,
#[serde(rename = 0x0C)]
pub third_party_payment: Option<bool>,
}
impl From<Response> 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<RawResponse> for Response {
type Error = Error;
fn try_from(value: RawResponse) -> Result<Self, Self::Error> {
// 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!(),
}
}
}

@ -7,3 +7,5 @@ pub mod extensions;
pub mod registry;
pub type Sha256Hash = [u8; 32];

@ -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

@ -78,7 +78,19 @@ mod device {
}
impl<D: ctaphid::HidDevice> HidAuthenticator<D> {
<<<<<<< Updated upstream
fn send_raw(&self, command: Command, bytes: &[u8]) -> Result<Vec<u8>, ctaphid::error::Error> {
=======
fn send_ctap1_raw(&self, bytes: &[u8]) -> Result<Vec<u8>, ctaphid::error::Error> {
self.0.ctap1(bytes)
}
fn send_ctap2_raw(
&self,
command: Command,
bytes: &[u8],
) -> Result<Vec<u8>, ctaphid::error::Error> {
>>>>>>> Stashed changes
self.0.ctap2(command as u8, bytes)
}
@ -95,6 +107,7 @@ impl<D: ctaphid::HidDevice> HidAuthenticator<D> {
}
})?;
<<<<<<< Updated upstream
let response = self.send_raw(command, &data)?;
match ciborium::de::from_reader(response.as_slice()) {
@ -105,5 +118,23 @@ impl<D: ctaphid::HidDevice> HidAuthenticator<D> {
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
}
}

@ -5,6 +5,12 @@ use hid::HidAuthenticator;
pub mod hid;
<<<<<<< Updated upstream
=======
#[cfg(test)]
mod tests;
>>>>>>> Stashed changes
impl<D: ctaphid::HidDevice> Ctap2_2Authenticator for HidAuthenticator<D> {
fn make_credential(&mut self, request: make::Request) -> Result<make::Response, make::Error> {
Ok(self
@ -23,6 +29,7 @@ impl<D: ctaphid::HidDevice> Ctap2_2Authenticator for HidAuthenticator<D> {
}
fn client_pin(
<<<<<<< Updated upstream
_request: client_pin::Request,
) -> Result<client_pin::Response, client_pin::Error> {
todo!()
@ -150,3 +157,65 @@ mod tests {
println!("response: {response:#?}");
}
}
=======
&mut self,
request: client_pin::Request,
) -> Result<client_pin::Response, client_pin::Error> {
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<bio_enrollment::Response, bio_enrollment::Error> {
todo!()
}
fn credential_management(
&mut self,
request: management::Request,
) -> Result<management::Response, management::Error> {
Ok(self
.send(Command::PrototypeAuthenticatorCredentialManagement, request)
.unwrap())
}
fn authenticator_config(&mut self, request: config::Request) -> Result<(), config::Error> {
todo!()
}
}
>>>>>>> Stashed changes

@ -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<Mutex<Option<HidAuthenticator>>> =
LazyLock::new(|| Mutex::new(get_authenticator()));
fn init() {
let _ = env_logger::builder().is_test(true).try_init();
}
fn get_authenticator() -> Option<HidAuthenticator> {
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<u8> = 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:#?}");
}

@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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:#?}");
}

@ -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::<sha2::Sha256>::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();
}
Loading…
Cancel
Save