TEMP: in-progress
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,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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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…
Reference in New Issue