Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
Nick Zana | fa6a8d4c1e | 6 months ago |
Nick Zana | cf003d07e6 | 1 year ago |
Nick Zana | ce905e422d | 1 year ago |
Nick Zana | 1daffb7db7 | 1 year ago |
Nick Zana | cf792b97cb | 1 year ago |
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
#![feature(associated_const_equality, async_fn_in_trait)]
|
||||
#![allow(incomplete_features)]
|
||||
|
||||
pub mod credential;
|
||||
pub mod discovery;
|
||||
pub mod mediation;
|
||||
// #![feature(associated_const_equality, async_fn_in_trait)]
|
||||
// #![allow(clippy::missing_errors_doc, incomplete_features)]
|
||||
//
|
||||
// pub mod credential;
|
||||
// pub mod discovery;
|
||||
// pub mod mediation;
|
||||
|
@ -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 }
|
@ -1,355 +0,0 @@
|
||||
//! Used to make serialization and deseriazation of the request and response
|
||||
//! possible in CBOR format while maintaining ergonomic enum variants for public
|
||||
//! API.
|
||||
|
||||
use super::auth_protocol;
|
||||
use super::Error;
|
||||
use super::Permission;
|
||||
use super::{PinUvAuthParam, PinUvAuthToken};
|
||||
use super::{Request, Response};
|
||||
use flagset::flags;
|
||||
use flagset::FlagSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, Bytes};
|
||||
use std::borrow::Cow;
|
||||
|
||||
mod public_key;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(into = "u8")]
|
||||
pub(crate) enum RawSubcommand {
|
||||
GetPinRetries = 0x01,
|
||||
GetKeyAgreement = 0x02,
|
||||
SetPin = 0x03,
|
||||
ChangePin = 0x04,
|
||||
GetPinToken = 0x05,
|
||||
GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
|
||||
GetUvRetries = 0x07,
|
||||
GetPinUvAuthTokenUsingPinWithPermissions = 0x09,
|
||||
}
|
||||
|
||||
impl From<RawSubcommand> for u8 {
|
||||
fn from(value: RawSubcommand) -> Self {
|
||||
value as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct RawRequest<'a> {
|
||||
#[serde(rename = 0x01, skip_serializing_if = "Option::is_none")]
|
||||
pub pin_uv_auth_protocol: Option<auth_protocol::Version>,
|
||||
#[serde(rename = 0x02)]
|
||||
pub sub_command: RawSubcommand,
|
||||
#[serde(
|
||||
rename = 0x03,
|
||||
deserialize_with = "public_key::deserialize",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub key_agreement: Option<cosey::PublicKey>,
|
||||
#[serde_as(as = "Option<Bytes>")]
|
||||
#[serde(rename = 0x04, skip_serializing_if = "Option::is_none")]
|
||||
pub pin_uv_auth_param: Option<PinUvAuthParam>,
|
||||
#[serde_as(as = "Option<Bytes>")]
|
||||
#[serde(rename = 0x05, skip_serializing_if = "Option::is_none")]
|
||||
pub new_pin_enc: Option<[u8; 64]>,
|
||||
#[serde_as(as = "Option<Bytes>")]
|
||||
#[serde(rename = 0x06, skip_serializing_if = "Option::is_none")]
|
||||
pub pin_hash_enc: Option<[u8; 16]>,
|
||||
#[serde(rename = 0x09, skip_serializing_if = "Option::is_none")]
|
||||
pub permissions: Option<FlagSet<RawPermission>>, // TODO: Deserialize from bitfield
|
||||
#[serde(rename = 0x0A, skip_serializing_if = "Option::is_none")]
|
||||
pub rp_id: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> From<Request<'a>> for RawRequest<'a> {
|
||||
fn from(value: Request<'a>) -> Self {
|
||||
match value {
|
||||
Request::GetPinRetries => Self {
|
||||
pin_uv_auth_protocol: None,
|
||||
sub_command: RawSubcommand::GetPinRetries,
|
||||
key_agreement: None,
|
||||
pin_uv_auth_param: None,
|
||||
new_pin_enc: None,
|
||||
pin_hash_enc: None,
|
||||
rp_id: None,
|
||||
permissions: None,
|
||||
},
|
||||
Request::GetKeyAgreement { version } => Self {
|
||||
pin_uv_auth_protocol: Some(version),
|
||||
sub_command: RawSubcommand::GetKeyAgreement,
|
||||
key_agreement: None,
|
||||
pin_uv_auth_param: None,
|
||||
new_pin_enc: None,
|
||||
pin_hash_enc: None,
|
||||
rp_id: None,
|
||||
permissions: None,
|
||||
},
|
||||
Request::SetPin {
|
||||
key_agreement,
|
||||
new_pin_encrypted,
|
||||
pin_uv_auth_param,
|
||||
version,
|
||||
} => Self {
|
||||
pin_uv_auth_protocol: Some(version),
|
||||
sub_command: RawSubcommand::SetPin,
|
||||
key_agreement: Some(key_agreement),
|
||||
pin_uv_auth_param: Some(pin_uv_auth_param),
|
||||
new_pin_enc: Some(new_pin_encrypted.clone()),
|
||||
pin_hash_enc: None,
|
||||
rp_id: None,
|
||||
permissions: None,
|
||||
},
|
||||
Request::ChangePin {
|
||||
version,
|
||||
pin_hash_encrypted,
|
||||
new_pin_encrypted,
|
||||
pin_uv_auth_param,
|
||||
key_agreement,
|
||||
} => Self {
|
||||
pin_uv_auth_protocol: Some(version),
|
||||
sub_command: RawSubcommand::ChangePin,
|
||||
key_agreement: Some(key_agreement),
|
||||
pin_uv_auth_param: Some(pin_uv_auth_param),
|
||||
new_pin_enc: Some(new_pin_encrypted.clone()),
|
||||
pin_hash_enc: Some(pin_hash_encrypted.clone()),
|
||||
rp_id: None,
|
||||
permissions: None,
|
||||
},
|
||||
Request::GetPinToken {
|
||||
version,
|
||||
key_agreement,
|
||||
pin_hash_encrypted,
|
||||
} => Self {
|
||||
pin_uv_auth_protocol: Some(version),
|
||||
sub_command: RawSubcommand::GetPinToken,
|
||||
key_agreement: Some(key_agreement),
|
||||
pin_uv_auth_param: None,
|
||||
new_pin_enc: None,
|
||||
pin_hash_enc: Some(pin_hash_encrypted.clone()),
|
||||
rp_id: None,
|
||||
permissions: None,
|
||||
},
|
||||
Request::GetPinUvAuthTokenUsingUvWithPermissions {
|
||||
version,
|
||||
key_agreement,
|
||||
permissions,
|
||||
relying_party_id,
|
||||
} => Self {
|
||||
pin_uv_auth_protocol: Some(version),
|
||||
sub_command: RawSubcommand::GetPinUvAuthTokenUsingUvWithPermissions,
|
||||
key_agreement: Some(key_agreement),
|
||||
pin_uv_auth_param: None,
|
||||
new_pin_enc: None,
|
||||
pin_hash_enc: None,
|
||||
rp_id: relying_party_id,
|
||||
permissions: Some(permissions.iter().map(Clone::clone).collect()),
|
||||
},
|
||||
Request::GetUvRetries => Self {
|
||||
pin_uv_auth_protocol: None,
|
||||
sub_command: RawSubcommand::GetUvRetries,
|
||||
key_agreement: None,
|
||||
pin_uv_auth_param: None,
|
||||
new_pin_enc: None,
|
||||
pin_hash_enc: None,
|
||||
rp_id: None,
|
||||
permissions: None,
|
||||
},
|
||||
Request::GetPinUvAuthTokenUsingPinWithPermissions {
|
||||
version,
|
||||
key_agreement,
|
||||
pin_hash_encrypted,
|
||||
permissions,
|
||||
relying_party_id,
|
||||
} => Self {
|
||||
pin_uv_auth_protocol: Some(version),
|
||||
sub_command: RawSubcommand::GetPinUvAuthTokenUsingPinWithPermissions,
|
||||
key_agreement: Some(key_agreement),
|
||||
pin_uv_auth_param: None,
|
||||
new_pin_enc: None,
|
||||
pin_hash_enc: Some(pin_hash_encrypted),
|
||||
rp_id: relying_party_id,
|
||||
permissions: Some(permissions.iter().map(Clone::clone).collect()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<RawRequest<'a>> for Request<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: RawRequest<'a>) -> Result<Self, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct RawResponse {
|
||||
#[serde(
|
||||
rename = 0x01,
|
||||
default, // Allows for None variant to be deserialized when 0x01 is not present, required
|
||||
// because of deserialize_with
|
||||
deserialize_with = "public_key::deserialize",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
)]
|
||||
pub key_agreement: Option<cosey::PublicKey>,
|
||||
#[serde_as(as = "Option<Bytes>")]
|
||||
#[serde(rename = 0x02, skip_serializing_if = "Option::is_none")]
|
||||
pub pin_uv_auth_token: Option<PinUvAuthToken>,
|
||||
#[serde(rename = 0x03, skip_serializing_if = "Option::is_none")]
|
||||
pub pin_retries: Option<usize>,
|
||||
#[serde(rename = 0x04, skip_serializing_if = "Option::is_none")]
|
||||
pub power_cycle_state: Option<usize>,
|
||||
#[serde(rename = 0x05, skip_serializing_if = "Option::is_none")]
|
||||
pub uv_retries: Option<usize>,
|
||||
}
|
||||
|
||||
impl From<Response> for RawResponse {
|
||||
fn from(value: Response) -> Self {
|
||||
match value {
|
||||
Response::GetPinRetries {
|
||||
pin_retries,
|
||||
power_cycle_state,
|
||||
} => Self {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: None,
|
||||
pin_retries: Some(pin_retries),
|
||||
power_cycle_state,
|
||||
uv_retries: None,
|
||||
},
|
||||
Response::GetKeyAgreement { key_agreement } => Self {
|
||||
key_agreement: Some(key_agreement),
|
||||
pin_uv_auth_token: None,
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: None,
|
||||
},
|
||||
Response::SetPin => Self {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: None,
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: None,
|
||||
},
|
||||
Response::ChangePin => Self {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: None,
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: None,
|
||||
},
|
||||
Response::GetPinToken { pin_uv_auth_token } => Self {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: Some(pin_uv_auth_token),
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: None,
|
||||
},
|
||||
Response::GetPinUvAuthTokenUsingUvWithPermissions { pin_uv_auth_token } => Self {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: Some(pin_uv_auth_token),
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: None,
|
||||
},
|
||||
Response::GetUvRetries { uv_retries } => Self {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: None,
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: Some(uv_retries.get()),
|
||||
},
|
||||
Response::GetPinUvAuthTokenUsingPinWithPermissions { pin_uv_auth_token } => Self {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: Some(pin_uv_auth_token),
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawResponse> for Response {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: RawResponse) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
RawResponse {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: None,
|
||||
pin_retries: Some(pin_retries),
|
||||
power_cycle_state,
|
||||
uv_retries: None,
|
||||
} => Response::GetPinRetries {
|
||||
pin_retries,
|
||||
power_cycle_state,
|
||||
},
|
||||
RawResponse {
|
||||
key_agreement: Some(key_agreement),
|
||||
pin_uv_auth_token: None,
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: None,
|
||||
} => Response::GetKeyAgreement { key_agreement },
|
||||
RawResponse {
|
||||
key_agreement: None,
|
||||
pin_uv_auth_token: Some(pin_uv_auth_token),
|
||||
pin_retries: None,
|
||||
power_cycle_state: None,
|
||||
uv_retries: None,
|
||||
} => Response::GetPinToken { pin_uv_auth_token },
|
||||
_ => todo!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
flags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum RawPermission: u8 {
|
||||
MakeCredential = 0x01,
|
||||
GetAssertion = 0x02,
|
||||
CredentialManagement = 0x04,
|
||||
BioEnrollment = 0x08,
|
||||
LargeBlobWrite = 0x10,
|
||||
AuthenticatorConfiguration = 0x20,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Permission> for RawPermission {
|
||||
fn from(value: Permission) -> Self {
|
||||
match value {
|
||||
Permission::MakeCredential => Self::MakeCredential,
|
||||
Permission::GetAssertion => Self::GetAssertion,
|
||||
Permission::CredentialManagement => Self::CredentialManagement,
|
||||
Permission::BiometricEnrollment => Self::BioEnrollment,
|
||||
Permission::LargeBlobWrite => Self::LargeBlobWrite,
|
||||
Permission::AuthenticatorConfiguration => Self::AuthenticatorConfiguration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RawPermission> for Permission {
|
||||
fn from(value: RawPermission) -> Self {
|
||||
match value {
|
||||
RawPermission::MakeCredential => Self::MakeCredential,
|
||||
RawPermission::GetAssertion => Self::GetAssertion,
|
||||
RawPermission::CredentialManagement => Self::CredentialManagement,
|
||||
RawPermission::BioEnrollment => Self::BiometricEnrollment,
|
||||
RawPermission::LargeBlobWrite => Self::LargeBlobWrite,
|
||||
RawPermission::AuthenticatorConfiguration => Self::AuthenticatorConfiguration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Permission> for FlagSet<RawPermission> {
|
||||
fn from_iter<T: IntoIterator<Item = Permission>>(iter: T) -> Self {
|
||||
iter.into_iter()
|
||||
.map(RawPermission::from)
|
||||
.fold(None.into(), |mut set, flag| {
|
||||
set |= flag;
|
||||
set
|
||||
})
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
use cosey::{EcdhEsHkdf256PublicKey, Ed25519PublicKey, P256PublicKey};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum PublicKey {
|
||||
P256Key(P256PublicKey),
|
||||
EcdhEsHkdf256Key(EcdhEsHkdf256PublicKey),
|
||||
Ed25519Key(Ed25519PublicKey),
|
||||
}
|
||||
|
||||
impl Into<cosey::PublicKey> for PublicKey {
|
||||
fn into(self) -> cosey::PublicKey {
|
||||
match self {
|
||||
PublicKey::P256Key(key) => cosey::PublicKey::P256Key(key),
|
||||
PublicKey::EcdhEsHkdf256Key(key) => cosey::PublicKey::EcdhEsHkdf256Key(key),
|
||||
PublicKey::Ed25519Key(key) => cosey::PublicKey::Ed25519Key(key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<cosey::PublicKey>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
PublicKey::deserialize(deserializer)
|
||||
.map(Into::into)
|
||||
.map(Some)
|
||||
}
|
||||
|
@ -1,10 +1,5 @@
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum Policy {
|
||||
UserVerificationOptional = 0x01,
|
||||
UserVerificationOptionalWithCredentialIdList = 0x02,
|
||||
UserVerificationRequired = 0x03,
|
||||
UserVerificationOptional,
|
||||
UserVerificationOptionalWithCredentialIdList,
|
||||
UserVerificationRequired,
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "hid-ctap2-proto"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ciborium = "0.2.1"
|
||||
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
|
@ -0,0 +1,140 @@
|
||||
use ctap2_proto::Command;
|
||||
use device::{DeviceInfo, HidDevice};
|
||||
|
||||
pub struct HidAuthenticator<D: ctaphid::HidDevice = HidDevice>(ctaphid::Device<D>);
|
||||
|
||||
impl TryFrom<hidapi::HidDevice> for HidAuthenticator<HidDevice> {
|
||||
type Error = ctaphid::error::Error;
|
||||
|
||||
fn try_from(device: hidapi::HidDevice) -> Result<Self, Self::Error> {
|
||||
let info = device
|
||||
.get_device_info()
|
||||
.map_err(|e| ctaphid::error::RequestError::PacketSendingFailed(Box::new(e)))?;
|
||||
let device = ctaphid::Device::new(HidDevice(device), DeviceInfo(info))?;
|
||||
Ok(Self(device))
|
||||
}
|
||||
}
|
||||
|
||||
mod device {
|
||||
//! Provides wrapper types for `hidapi` device types that implement required
|
||||
//! `ctaphid` interfaces.
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub struct HidDevice(pub hidapi::HidDevice);
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceInfo(pub hidapi::DeviceInfo);
|
||||
|
||||
impl ctaphid::HidDevice for HidDevice {
|
||||
type Info = DeviceInfo;
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), ctaphid::error::RequestError> {
|
||||
self.0
|
||||
.write(data)
|
||||
.map_err(|e| ctaphid::error::RequestError::PacketSendingFailed(e.into()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive<'a>(
|
||||
&self,
|
||||
buffer: &'a mut [u8],
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> Result<&'a [u8], ctaphid::error::ResponseError> {
|
||||
let timeout_millis: i32 = match timeout {
|
||||
Some(timeout) => timeout.as_millis() as i32,
|
||||
None => -1,
|
||||
};
|
||||
let count = self
|
||||
.0
|
||||
.read_timeout(buffer, timeout_millis)
|
||||
.map_err(|e| match e {
|
||||
hidapi::HidError::HidApiError { message } => {
|
||||
ctaphid::error::ResponseError::PacketReceivingFailed(message.into())
|
||||
}
|
||||
_ => todo!(),
|
||||
})?;
|
||||
|
||||
Ok(&buffer[..count])
|
||||
}
|
||||
}
|
||||
|
||||
impl ctaphid::HidDeviceInfo for DeviceInfo {
|
||||
fn vendor_id(&self) -> u16 {
|
||||
self.0.vendor_id()
|
||||
}
|
||||
|
||||
fn product_id(&self) -> u16 {
|
||||
self.0.product_id()
|
||||
}
|
||||
|
||||
fn path(&self) -> std::borrow::Cow<'_, str> {
|
||||
let path = self
|
||||
.0
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("Device path must be valid UTF-8.");
|
||||
Cow::from(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn send<Req, Res>(&self, command: Command, req: Req) -> Result<Res, ctaphid::error::Error>
|
||||
where
|
||||
Req: serde::Serialize,
|
||||
Res: for<'de> serde::Deserialize<'de>,
|
||||
{
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
ciborium::ser::into_writer(&req, &mut data).map_err(|e| match e {
|
||||
ciborium::ser::Error::Io(_) => ctaphid::error::RequestError::IncompleteWrite,
|
||||
ciborium::ser::Error::Value(desc) => {
|
||||
ctaphid::error::RequestError::PacketSendingFailed(desc.into())
|
||||
}
|
||||
})?;
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
let response = self.send_raw(command, &data)?;
|
||||
|
||||
match ciborium::de::from_reader(response.as_slice()) {
|
||||
Ok(response) => Ok(response),
|
||||
// TODO: Improve error handling
|
||||
Err(e) => {
|
||||
println!("ERROR: {e}");
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
#![cfg_attr(test, feature(split_array, lazy_cell))]
|
||||
|
||||
use ctap2_proto::{prelude::*, Ctap2_2Authenticator};
|
||||
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
|
||||
.send(Command::AuthenticatorMakeCredential, request)
|
||||
.unwrap()) // TODO: Properly parse/convert errors
|
||||
}
|
||||
|
||||
fn get_assertion(&mut self, request: get::Request) -> Result<get::Response, get::Error> {
|
||||
Ok(self
|
||||
.send(Command::AuthenticatorGetAssertion, request)
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
fn get_info(&self) -> device::Info {
|
||||
self.send(Command::AuthenticatorGetInfo, ()).unwrap()
|
||||
}
|
||||
|
||||
fn client_pin(
|
||||
<<<<<<< Updated upstream
|
||||
_request: client_pin::Request,
|
||||
) -> Result<client_pin::Response, client_pin::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<(), reset::Error> {
|
||||
let _: () = self.send(Command::AuthenticatorReset, ()).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn selection() -> Result<(), ctap2_proto::authenticator::selection::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate hidapi;
|
||||
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use crate::hid::HidAuthenticator;
|
||||
use ctap2_proto::prelude::{credential::public_key, *};
|
||||
use rand::{distributions, Rng};
|
||||
|
||||
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> {
|
||||
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
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn reset() {
|
||||
init();
|
||||
|
||||
let mut guard = AUTHENTICATOR.lock().unwrap();
|
||||
let authenticator = guard.as_mut().unwrap();
|
||||
|
||||
authenticator.reset().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info() {
|
||||
init();
|
||||
|
||||
let guard = AUTHENTICATOR.lock().unwrap();
|
||||
let authenticator = guard.as_ref().unwrap();
|
||||
|
||||
let info = authenticator.get_info();
|
||||
println!("deserialized: {:#?}", info);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_credential() {
|
||||
init();
|
||||
|
||||
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 = info.algorithms.unwrap();
|
||||
|
||||
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.as_slice())
|
||||
.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:#?}");
|
||||
}
|
||||
}
|
||||
=======
|
||||
&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();
|
||||
}
|
@ -1,34 +1,34 @@
|
||||
#![feature(async_fn_in_trait, adt_const_params, associated_const_equality)]
|
||||
#![allow(incomplete_features)]
|
||||
|
||||
pub mod attestation;
|
||||
pub mod authenticator;
|
||||
pub mod client;
|
||||
pub mod public_key;
|
||||
pub mod token;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// > A WebAuthn Relying Party may require user verification for some of its
|
||||
/// > operations but not for others, and may use this type to express its needs.
|
||||
pub enum UserVerificationRequirement {
|
||||
/// > The Relying Party requires user verification for the operation and
|
||||
/// > will fail the overall ceremony if the response does not have the UV
|
||||
/// > flag set. The client MUST return an error if user verification cannot
|
||||
/// > be performed.
|
||||
#[cfg_attr(feature = "serde", serde(rename = "required"))]
|
||||
Required,
|
||||
/// > The Relying Party prefers user verification for the operation if
|
||||
/// > possible, but will not fail the operation if the response does not
|
||||
/// > have the UV flag set.
|
||||
#[cfg_attr(feature = "serde", serde(rename = "preferred"))]
|
||||
Preferred,
|
||||
/// > The Relying Party does not want user verification employed during the
|
||||
/// > operation (e.g., in the interest of minimizing disruption to the user
|
||||
/// > interaction flow).
|
||||
#[cfg_attr(feature = "serde", serde(rename = "discouraged"))]
|
||||
Discouraged,
|
||||
}
|
||||
// #![feature(async_fn_in_trait, adt_const_params, associated_const_equality)]
|
||||
// #![allow(incomplete_features, clippy::unused_async, clippy::doc_markdown)]
|
||||
//
|
||||
// pub mod attestation;
|
||||
// pub mod authenticator;
|
||||
// pub mod client;
|
||||
// pub mod public_key;
|
||||
// pub mod token;
|
||||
//
|
||||
// #[cfg(feature = "serde")]
|
||||
// use serde::{Deserialize, Serialize};
|
||||
//
|
||||
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
// #[derive(Debug, Clone, Copy)]
|
||||
// > A WebAuthn Relying Party may require user verification for some of its
|
||||
// > operations but not for others, and may use this type to express its needs.
|
||||
// pub enum UserVerificationRequirement {
|
||||
// > The Relying Party requires user verification for the operation and
|
||||
// > will fail the overall ceremony if the response does not have the UV
|
||||
// > flag set. The client MUST return an error if user verification cannot
|
||||
// > be performed.
|
||||
// #[cfg_attr(feature = "serde", serde(rename = "required"))]
|
||||
// Required,
|
||||
// > The Relying Party prefers user verification for the operation if
|
||||
// > possible, but will not fail the operation if the response does not
|
||||
// > have the UV flag set.
|
||||
// #[cfg_attr(feature = "serde", serde(rename = "preferred"))]
|
||||
// Preferred,
|
||||
// > The Relying Party does not want user verification employed during the
|
||||
// > operation (e.g., in the interest of minimizing disruption to the user
|
||||
// > interaction flow).
|
||||
// #[cfg_attr(feature = "serde", serde(rename = "discouraged"))]
|
||||
// Discouraged,
|
||||
// }
|
||||
|
Loading…
Reference in New Issue