Compare commits

..

5 Commits
main ... dev

628
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -5,4 +5,4 @@ members = [
]
[patch.crates-io]
serde = { git = "https://github.com/AndrewScull/serde", branch = "intnames" }
serde = { path = "../serde/serde" }

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

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

@ -8,15 +8,10 @@ edition = "2021"
[dependencies]
fido-common = { path = "../fido-common" }
bounded-integer = { version = "0.5.3", features = ["types", "std"] }
serde = { version = "=1.0.136", features = ["derive"], optional = true }
coset = "0.3.3"
serde = { version = "1.0", features = ["derive"], optional = true }
typed-builder = { version = "0.14.0", default-features = false }
# Version <= to support older serde
serde_with = { version = "<=2.2.0", optional = true }
cosey = "0.3.0"
flagset = { version = "0.4.3", default-features = false, features = ["serde"] }
[dev-dependencies]
hex = "0.4.3"
serde_with = { path = "../../../serde_with/serde_with", optional = true }
[features]
serde = ["dep:serde", "dep:serde_with", "fido-common/serde", "bounded-integer/serde1"]

@ -0,0 +1,260 @@
#![feature(let_chains)]
extern crate ctap2_proto;
use std::collections::BTreeMap;
use ctap2_proto::authenticator::Transport;
// Recommended
use ctap2_proto::prelude::*;
use ctap2_proto::{
authenticator,
prelude::{
client_pin::AuthProtocolVersion,
device::{Aaguid, Version},
},
};
use fido_common::credential::public_key::{RelyingPartyEntity, UserEntity};
use fido_common::{
attestation,
credential::{self, public_key::Parameters},
registry::algorithms::Signature,
};
const AAGUID: Aaguid = Aaguid::from(*b"\xed\xeeZow\xdc\xf5\xadZ\xe3\xd7\xb8\xf5\xf6\xf7\xd7");
const VERSION: usize = 1;
const SUPPORTED_AUTH_PROTOCOL_VERSIONS: [AuthProtocolVersion; 1] = [AuthProtocolVersion::Two];
const SUPPORTED_PUBLIC_KEY_ALGORITHMS: [Parameters; 1] = [Parameters {
credential_type: credential::Type::PublicKey,
algorithm: Signature::Ed25519EddsaSha512Raw,
}];
struct VirtualAuthenticator<R, W>
where
R: std::io::Read + Send,
W: std::io::Write + Send,
{
pin: Option<Vec<u8>>,
input: R,
output: W,
}
enum Presence {
Present,
NotPresent,
}
impl<R, W> VirtualAuthenticator<R, W>
where
R: std::io::Read + Send,
W: std::io::Write + Send,
{
fn verify_presence(reader: &mut R, writer: &mut W) -> Result<Presence, std::io::Error> {
write!(writer, "Allow this operation? [Y/n]:")?;
let mut input = <[u8; 1]>::default();
reader.read_exact(&mut input)?;
writeln!(writer)?;
if input[0] == b'Y' {
Ok(Presence::Present)
} else {
Ok(Presence::NotPresent)
}
}
}
impl<R, W> Ctap2_2Authenticator for VirtualAuthenticator<R, W>
where
R: std::io::Read + Send,
W: std::io::Write + Send,
{
fn make_credential(&mut self, request: make::Request) -> Result<make::Response, make::Error> {
// If authenticator supports either pinUvAuthToken or clientPin features and the
// platform sends a zero length pinUvAuthParam:
if let Some(options) = self.get_info().options &&
(options.contains_key(&device::OptionId::PinUvAuthToken) || options.contains_key(&device::OptionId::ClientPin)) && request.pin_uv_auth_param.len() == 0
{
// Request evidence of user interaction in an authenticator-specific way (e.g., flash
// the LED light).
// If the user declines permission, or the operation times out, then end the operation
// by returning CTAP2_ERR_OPERATION_DENIED.
let Ok(Presence::Present) = Self::verify_presence(&mut self.input, &mut self.output) else {
return Err(make::Error::OperationDenied);
};
// If evidence of user interaction is provided in this step then return either
// CTAP2_ERR_PIN_NOT_SET if PIN is not set or CTAP2_ERR_PIN_INVALID if PIN has been
// set.
if self.pin.is_some() {
return Err(make::Error::PinInvalid);
} else {
return Err(make::Error::PinNotSet);
}
}
// If the pinUvAuthParam parameter is present:
debug_assert!(request.pin_uv_auth_param.len() > 0);
// If the pinUvAuthProtocol parameters value is not supported, return
// CTAP1_ERR_INVALID_PARAMETER error.
// If the pinUvAuthProtocol parameter is absent, return
// CTAP2_ERR_MISSING_PARAMETER error.
let Some(auth_protocol_version) = request.pin_uv_auth_protocol_version else { return Err(make::Error::MissingParameter) };
if !SUPPORTED_AUTH_PROTOCOL_VERSIONS.contains(&auth_protocol_version) {
return Err(make::Error::InvalidParameter);
}
// Filter out any invalid or unsupported params, then take the first valid one.
// TODO: The following NOTE from the specs refers to the following loop, however
// this loop only iterates over the algorithms until a match is found.
//
// The only risk I can imagine is enumerating the supported algorithms of the
// authenticator, which doesn't seem that important to protect against.
//
// NOTE: This loop chooses the first occurrence of an algorithm identifier
// supported by this authenticator but always iterates over every
// element of pubKeyCredParams to validate them.
// Validate pubKeyCredParams with the following steps:
// 1. For each element of pubKeyCredParams:
let Some(public_key_algorithm) = request.public_key_credential_params.into_iter().filter(|params| {
// TODO: Ensure that invalid params are unrepresentable
// If the element is missing required members, including members that are mandatory
// only for the specific type, then return an error, for example
// CTAP2_ERR_INVALID_CBOR.
// TODO: What would it mean for a credential to have the wrong type?
// If the values of any known members have the wrong type then return an error, for
// example CTAP2_ERR_CBOR_UNEXPECTED_TYPE.
// If the element specifies an algorithm that is supported by the authenticator, and no
// algorithm has yet been chosen by this loop, then let the algorithm specified by the
// current element be the chosen algorithm.
return SUPPORTED_PUBLIC_KEY_ALGORITHMS.contains(params);
}).next() else {
// If the loop completes and no algorithm was chosen then return
// CTAP2_ERR_UNSUPPORTED_ALGORITHM.
return Err(make::Error::UnsupportedAlgorithm);
};
// No attestation
let format = attestation::FormatIdentifier::None;
// No enterprise attestation
let enterprise_attestation = None;
// If the "uv" option is absent, let the "uv" option be treated as being present
// with the value false. (This is the default)
let mut user_verification = false;
let mut discoverable_credential = false;
let mut user_presence = true;
let authenticator_data = authenticator::Data {
relying_party_id_hash: todo!(),
user_is_present: todo!(),
user_is_verified: todo!(),
signature_counter: todo!(),
attested_credential_data: todo!(),
extensions: None,
};
if let Some(req_options) = request.options {
// If the pinUvAuthParam is present, let the "uv" option be treated as being
// present with the value false.
if let Some(true) = req_options.get(&make::OptionKey::UserVerification) {}
}
Ok(make::Response {
format,
authenticator_data,
enterprise_attestation,
large_blob_key: None,
unsigned_extension_outputs: None,
})
}
fn get_assertion(request: get::Request) -> Result<get::Response, get::Error> {
todo!()
}
fn get_info(&self) -> device::Info {
device::Info {
versions: [Version::Fido2_1].into_iter().collect(),
extensions: None,
aaguid: AAGUID,
options: None,
max_message_size: None,
pin_uv_auth_protocols: Some(SUPPORTED_AUTH_PROTOCOL_VERSIONS.into()),
max_credential_count_in_list: None,
max_credential_id_length: None,
transports: Some([Transport::Internal].into_iter().collect()),
algorithms: Some(SUPPORTED_PUBLIC_KEY_ALGORITHMS.into()),
max_serialized_large_blob_array_size: None,
force_pin_change: Some(self.pin.is_none()),
min_pin_length: None,
firmware_version: Some(VERSION),
max_cred_blob_length: None,
max_rpids_for_set_min_pin_length: None,
preferred_platform_uv_attempts: None,
uv_modality: None,
certifications: None,
remaining_discoverable_credentials: None,
vendor_prototype_config_commands: None,
}
}
fn client_pin(request: client_pin::Request) -> Result<client_pin::Response, client_pin::Error> {
todo!()
}
fn reset() -> Result<(), reset::Error> {
todo!()
}
fn selection() -> Result<(), selection::Error> {
todo!()
}
}
fn main() {
let pin = None;
let authenticator = VirtualAuthenticator {
pin,
input: std::io::stdin(),
output: std::io::stdout(),
};
let client_data_hash = [0; 32];
let relying_party = RelyingPartyEntity {
id: "example.com".into(),
name: Some("Example Inc.".into()),
};
let user_entity = UserEntity {
id: [1u8; 64].into(),
name: Some("user@example.com".into()),
display_name: Some("Example User".into()),
};
// Get authenticator info
let info = authenticator.get_info();
// Make a new discoverable credential
let options = BTreeMap::from([
(make::OptionKey::UserVerification, true),
(make::OptionKey::Discoverable, true),
(make::OptionKey::UserPresence, false),
]);
let request = make::Request {
client_data_hash: &client_data_hash,
relying_party: &relying_party,
user: &user_entity,
public_key_credential_params: &SUPPORTED_PUBLIC_KEY_ALGORITHMS,
exclude_list: None,
extensions: None,
options: Some(&options),
pin_uv_auth_param: todo!(),
pin_uv_auth_protocol_version: todo!(),
enterprise_attestation: None,
};
authenticator.make_credential(request);
}

@ -1,5 +1,5 @@
use crate::Sha256Hash;
use crate::{authenticator::client_pin::auth_protocol, extensions};
use crate::{authenticator::client_pin::AuthProtocolVersion, extensions};
use fido_common::credential::public_key;
use std::{collections::BTreeMap, usize};
@ -31,12 +31,12 @@ pub enum Error {
pub enum OptionKey {
/// > user presence: Instructs the authenticator to require user consent
/// > to complete the operation.
#[cfg_attr(feature = "serde", serde(rename = "up"))]
#[serde(rename = "up")]
UserPresence,
/// > user verification: If true, instructs the authenticator to require
/// > a user-verifying gesture in order to complete the request.
/// > Examples of such gestures are fingerprint scan or a PIN.
#[cfg_attr(feature = "serde", serde(rename = "uv"))]
#[serde(rename = "uv")]
UserVerification,
}
@ -46,7 +46,7 @@ pub enum OptionKey {
#[cfg_attr(feature = "serde", serde_as, skip_serializing_none, derive(Serialize))]
pub struct Request<'a> {
/// > relying party identifier
#[cfg_attr(feature = "serde", serde(rename = 0x01))]
#[serde(rename = 0x01)]
pub relying_party_id: &'a str,
/// > Hash of the serialized client data collected by the host.
#[cfg_attr(feature = "serde", serde_as(as = "Bytes"), serde(rename = 0x02))]
@ -56,20 +56,20 @@ pub struct Request<'a> {
/// > the authenticator MUST only generate a assertion using one of the
/// > denoted credentials.
// Cannot be empty if present
#[cfg_attr(feature = "serde", serde(rename = 0x03))]
#[serde(rename = 0x03)]
pub allow_list: Option<&'a Vec<&'a public_key::Descriptor>>,
/// > Parameters to influence authenticator operation. These parameters
/// > might be authenticator specific.
#[cfg_attr(feature = "serde", serde(rename = 0x04))]
#[serde(rename = 0x04)]
pub extensions: Option<&'a BTreeMap<extensions::Identifier, &'a [u8]>>,
/// > Parameters to influence authenticator operation.
#[cfg_attr(feature = "serde", serde(rename = 0x05))]
#[serde(rename = 0x05)]
pub options: Option<&'a BTreeMap<OptionKey, bool>>,
#[cfg_attr(feature = "serde", serde(rename = 0x06))]
#[serde(rename = 0x06)]
pub pin_uv_auth_param: Option<&'a [u8]>,
/// > PIN/UV protocol version selected by platform.
#[cfg_attr(feature = "serde", serde(rename = 0x07))]
pub pin_uv_auth_protocol_version: Option<auth_protocol::Version>,
#[serde(rename = 0x07)]
pub pin_uv_auth_protocol_version: Option<AuthProtocolVersion>,
}
/// Response structure for [`Ctap2Device::get_assertion`] operation.
@ -84,7 +84,7 @@ pub struct Request<'a> {
pub struct Response {
/// > PublicKeyCredentialDescriptor structure containing the credential
/// > identifier whose private key was used to generate the assertion.
#[cfg_attr(feature = "serde", serde(rename = 0x01))]
#[serde(rename = 0x01)]
pub credential: public_key::Descriptor,
/// > The signed-over contextual bindings made by the authenticator, as
/// > specified in [WebAuthn].
@ -96,26 +96,22 @@ pub struct Response {
pub signature: Vec<u8>,
/// > [`public_key::UserEntity`] structure containing the user account
/// > information
#[cfg_attr(feature = "serde", serde(rename = 0x04))]
#[serde(rename = 0x04)]
pub user: Option<public_key::UserEntity>,
/// > Total number of account credentials for the RP. Optional; defaults
/// > to one. This member is required when more than one credential is
/// > found for an RP, and the authenticator does not have a display or
/// > the UV & UP flags are false.
#[cfg_attr(feature = "serde", serde(rename = 0x05))]
#[serde(rename = 0x05)]
pub number_of_credentials: Option<usize>,
/// > Indicates that a credential was selected by the user via
/// > interaction directly with the authenticator, and thus the platform
/// > does not need to confirm the credential.
#[cfg_attr(feature = "serde", serde(rename = 0x06))]
#[serde(rename = 0x06)]
pub user_selected: Option<bool>,
/// > The contents of the associated `largeBlobKey` if present for the
/// > asserted credential, and if `largeBlobKey` was true in the
/// > extensions input.
#[cfg_attr(
feature = "serde",
serde_as(as = "Option<Bytes>"),
serde(rename = 0x07)
)]
#[cfg_attr(feature = "serde", serde_as(as = "Option<Bytes>"), serde(rename = 0x07))]
pub large_blob_key: Option<Vec<u8>>,
}

@ -1,105 +0,0 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::marker::ConstParamTy;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, ConstParamTy)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(into = "u8", try_from = "u8")
)]
pub enum Version {
One = 1,
Two = 2,
}
impl From<Version> for u8 {
fn from(value: Version) -> Self {
value as u8
}
}
impl TryFrom<u8> for Version {
type Error = super::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Version::One),
2 => Ok(Version::Two),
_ => Err(super::Error::InvalidParameter),
}
}
}
/// The AES block size, in bytes.
pub const BLOCK_SIZE: usize = 16;
pub mod authenticator {
use super::Version;
pub trait Authenticator {
type Error; // TODO: Can the error cases be enumerated here?
const VERSION: Version;
/// This process is run by the authenticator at power-on.
fn initialize(&mut self) -> Result<(), Self::Error>;
/// Generates a fresh public key.
fn regenerate(&mut self) -> Result<(), Self::Error>;
/// Generates a fresh pinUvAuthToken.
fn reset_pin_uv_auth_token(&mut self) -> Result<(), Self::Error>;
/// Returns the authenticators public key as a COSE_Key structure.
fn get_public_key(&self) -> Result<cosey::PublicKey, Self::Error>;
/// Processes the output of encapsulate from the peer and produces a
/// shared secret, known to both platform and authenticator.
fn decapsulate(&self, peer_cose_key: cosey::PublicKey) -> Result<Vec<u8>, Self::Error>;
/// Decrypts a ciphertext, using sharedSecret as a key, and returns the
/// plaintext.
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, Self::Error>;
/// Verifies that the signature is a valid MAC for the given message. If
/// the key parameter value is the current pinUvAuthToken, it
/// also checks whether the pinUvAuthToken is in use or not.
fn verify(&self, key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Self::Error>;
}
}
pub mod platform {
use super::{Version, BLOCK_SIZE};
pub trait Session<const VERSION: Version>: Sized {
type Error; // TODO: Can the error cases be enumerated here?
/// Encompasses both the `initialize` and `encapsulate` functions in the
/// platform interface of the spec. This is done to guard against
/// private key reuse by the platform. As a consequence, a new
/// session must be initialized for each transaction with a
/// freshly generated private key.
fn initialize(peer_cose_key: cosey::PublicKey) -> Result<Self, Self::Error>;
fn platform_key_agreement_key(&self) -> &cosey::PublicKey;
/// Encrypts a plaintext to produce a ciphertext, which may be longer
/// than the plaintext. The plaintext is restricted to being a
/// multiple of the AES block size (16 bytes) in length.
fn encrypt<const N: usize>(
&self,
plaintext: &[[u8; BLOCK_SIZE]; N],
) -> Result<[[u8; BLOCK_SIZE]; N], Self::Error>;
/// Decrypts a ciphertext and returns the plaintext.
// TODO: Return a specific type instead of raw bytes?
fn decrypt<const N: usize>(
&self,
ciphertext: &[[u8; BLOCK_SIZE]; N],
) -> [[u8; BLOCK_SIZE]; N];
/// Computes a MAC of the given message.
// TODO: Return a specific type instead of raw bytes?
fn authenticate(&self, message: &[u8]) -> Result<[u8; 16], Self::Error>;
}
}

@ -1,144 +1,120 @@
use bounded_integer::BoundedUsize;
use serde_with::{Bytes, DeserializeAs, SerializeAs};
use std::{borrow::Cow, collections::BTreeSet};
use std::collections::BTreeSet;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod auth_protocol;
#[cfg(feature = "serde")]
mod raw;
#[cfg(feature = "serde")]
use raw::{RawRequest, RawResponse};
pub type PinUvAuthParam = [u8; 16];
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PinUvAuthToken {
Short([u8; 16]),
Long([u8; 32]),
}
impl AsRef<[u8]> for PinUvAuthToken {
fn as_ref(&self) -> &[u8] {
match self {
PinUvAuthToken::Short(bytes) => bytes.as_ref(),
PinUvAuthToken::Long(bytes) => bytes.as_ref(),
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum AuthProtocolVersion {
One,
Two,
}
// workaround until <https://github.com/serde-rs/serde/pull/2056> is merged
// PR: ( Integer/boolean tags for internally/adjacently tagged enums #2056 )
#[cfg(feature = "serde")]
impl TryFrom<&[u8]> for PinUvAuthToken {
type Error = Error;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
match value.len() {
16 => {
let mut short = [0; 16];
short.copy_from_slice(value);
Ok(Self::Short(short))
}
32 => {
let mut long = [0; 32];
long.copy_from_slice(value);
Ok(Self::Long(long))
}
_ => Err(Error::InvalidParameter),
}
}
}
#[cfg(feature = "serde")]
impl<'de> DeserializeAs<'de, PinUvAuthToken> for Bytes {
fn deserialize_as<D>(deserializer: D) -> Result<PinUvAuthToken, D::Error>
impl Serialize for AuthProtocolVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
D: serde::Deserializer<'de>,
S: serde::Serializer,
{
let bytes: Vec<u8> = Bytes::deserialize_as(deserializer)?;
PinUvAuthToken::try_from(bytes.as_ref()).map_err(serde::de::Error::custom)
serializer.serialize_u8(match self {
AuthProtocolVersion::One => 1,
AuthProtocolVersion::Two => 2,
})
}
}
// workaround until <https://github.com/serde-rs/serde/pull/2056> is merged
// PR: ( Integer/boolean tags for internally/adjacently tagged enums #2056 )
#[cfg(feature = "serde")]
impl SerializeAs<PinUvAuthToken> for Bytes {
fn serialize_as<S>(source: &PinUvAuthToken, serializer: S) -> Result<S::Ok, S::Error>
impl<'de> Deserialize<'de> for AuthProtocolVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
S: serde::Serializer,
D: serde::Deserializer<'de>,
{
match source {
PinUvAuthToken::Short(bytes) => Bytes::serialize_as(bytes, serializer),
PinUvAuthToken::Long(bytes) => Bytes::serialize_as(bytes, serializer),
use serde::de;
match u8::deserialize(deserializer)? {
1 => Ok(Self::One),
2 => Ok(Self::Two),
i => Err(de::Error::invalid_value(
de::Unexpected::Unsigned(i.into()),
&"1 or 2",
)),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(into = "RawRequest", try_from = "RawRequest")
)]
pub enum Subcommand {
GetPinRetries,
GetKeyAgreement,
SetPin,
ChangePin,
GetPinToken,
GetPinUvAuthTokenUsingUvWithPermissions,
GetUvRetries,
GetPinUvAuthTokenUsingPinWithPermissions,
}
#[derive(Clone, Copy)]
pub enum Request<'a> {
GetPinRetries,
GetKeyAgreement {
version: auth_protocol::Version,
version: AuthProtocolVersion,
},
SetPin {
version: auth_protocol::Version,
key_agreement: cosey::PublicKey,
new_pin_encrypted: [u8; 64],
pin_uv_auth_param: PinUvAuthParam,
key_agreement: &'a coset::CoseKey,
new_pin_encrypted: &'a [u8],
pin_uv_auth_param: &'a [u8],
},
ChangePin {
version: auth_protocol::Version,
key_agreement: cosey::PublicKey,
pin_hash_encrypted: [u8; 16],
new_pin_encrypted: [u8; 64],
pin_uv_auth_param: PinUvAuthParam,
version: AuthProtocolVersion,
pin_hash_encrypted: &'a [u8],
new_pin_encrypted: &'a [u8],
pin_uv_auth_param: &'a [u8],
},
GetPinToken {
version: auth_protocol::Version,
key_agreement: cosey::PublicKey,
pin_hash_encrypted: [u8; 16],
version: AuthProtocolVersion,
key_agreement: &'a coset::CoseKey,
pin_hash_encrypted: &'a [u8],
},
GetPinUvAuthTokenUsingUvWithPermissions {
version: auth_protocol::Version,
key_agreement: cosey::PublicKey,
version: AuthProtocolVersion,
key_agreement: &'a coset::CoseKey,
permissions: &'a BTreeSet<Permission>, // TODO: Enforce non-empty set?
relying_party_id: Option<Cow<'a, str>>,
relying_party_id: Option<usize>,
},
GetUvRetries,
GetPinUvAuthTokenUsingPinWithPermissions {
version: auth_protocol::Version,
key_agreement: cosey::PublicKey,
pin_hash_encrypted: [u8; 16],
version: AuthProtocolVersion,
key_agreement: &'a coset::CoseKey,
pin_hash_encrypted: usize,
permissions: &'a BTreeSet<Permission>, // TODO: Enforce non-empty set?
relying_party_id: Option<Cow<'a, str>>,
relying_party_id: Option<usize>,
},
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(into = "RawResponse", try_from = "RawResponse")
)]
pub enum PinUvAuthToken {
Short([u8; 16]),
Long([u8; 32]),
}
pub enum Response {
GetPinRetries {
pin_retries: usize,
power_cycle_state: Option<usize>,
},
GetKeyAgreement {
key_agreement: cosey::PublicKey,
key_agreement: coset::CoseKey,
},
SetPin,
ChangePin,
GetPinToken {
pin_uv_auth_token: PinUvAuthToken,
SetPin {
key_agreement: coset::CoseKey,
new_pin_encrypted: [u8; 64],
pin_uv_auth_param: (),
},
ChangePin,
GetPinToken,
GetPinUvAuthTokenUsingUvWithPermissions {
/// > The pinUvAuthToken, encrypted by calling encrypt with the shared
/// > secret as the key.
@ -159,7 +135,6 @@ pub enum Response {
},
}
#[derive(Debug, Clone, Copy)]
pub enum Error {
MissingParameter,
InvalidParameter,
@ -179,7 +154,7 @@ pub enum Error {
/// > When obtaining a `pinUvAuthToken`, the platform requests permissions
/// > appropriate for the operations it intends to perform. Consequently, the
/// > `pinUvAuthToken` can only be used for those operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Clone, Copy)]
pub enum Permission {
/// > This allows the `pinUvAuthToken` to be used for
/// > `authenticatorMakeCredential` operations with the provided `rpId`
@ -205,23 +180,3 @@ pub enum Permission {
/// > `authenticatorConfig` command.
AuthenticatorConfiguration,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::MissingParameter => write!(f, "Missing parameter"),
Error::InvalidParameter => write!(f, "Invalid parameter"),
Error::PinAuthInvalid => write!(f, "PIN auth invalid"),
Error::PinPolicyViolation => write!(f, "PIN policy violation"),
Error::PinBlocked => write!(f, "PIN blocked"),
Error::PinAuthBlocked => write!(f, "PIN auth blocked"),
Error::PinInvalid => write!(f, "PIN invalid"),
Error::OperationDenied => write!(f, "Operation denied"),
Error::UnauthorizedPermission => write!(f, "Unauthorized permission"),
Error::NotAllowed => write!(f, "Not allowed"),
Error::UserVerificationBlocked => write!(f, "User verification blocked"),
Error::UserActionTimeout => write!(f, "User action timeout"),
Error::UserVerificationInvalid => write!(f, "User verification invalid"),
}
}
}

@ -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,19 +1,19 @@
use std::collections::BTreeMap;
use super::client_pin::auth_protocol;
use super::client_pin::AuthProtocolVersion;
#[derive(Clone, Copy)]
pub enum Request<'a> {
/// > This `enableEnterpriseAttestation` subcommand is only implemented if
/// > the enterprise attestation feature is supported.
EnableEnterpriseAttestation {
pin_uv_auth_protocol: auth_protocol::Version,
pin_uv_auth_protocol: AuthProtocolVersion,
pin_uv_auth_param: &'a [u8], // TODO: Is using a more specific type possible?
},
/// > This `toggleAlwaysUv` subcommand is only implemented if the Always
/// > Require User Verification feature is supported.
ToggleAlwaysUserVerification {
pin_uv_auth_protocol: auth_protocol::Version,
pin_uv_auth_protocol: AuthProtocolVersion,
pin_uv_auth_param: &'a [u8], // TODO: Is using a more specific type possible?
},
/// > This `setMinPINLength` subcommand is only implemented if the
@ -22,7 +22,7 @@ pub enum Request<'a> {
/// > This command sets the minimum PIN length in Unicode code points to be
/// > enforced by the authenticator while changing/setting up a ClientPIN.
SetMinPinLength {
pin_uv_auth_protocol: auth_protocol::Version,
pin_uv_auth_protocol: AuthProtocolVersion,
pin_uv_auth_param: &'a [u8], // TODO: Is using a more specific type possible?
},
/// > This subCommand allows vendors to test authenticator configuration
@ -39,7 +39,7 @@ pub enum Request<'a> {
vendor_command_id: usize,
params: &'a BTreeMap<Vec<u8>, Vec<u8>>, /* TODO: Is the character space of keys
* restricted to UTF-8? */
pin_uv_auth_protocol: auth_protocol::Version,
pin_uv_auth_protocol: AuthProtocolVersion,
pin_uv_auth_param: &'a [u8], // TODO: Is using a more specific type possible?
},
}

@ -99,7 +99,7 @@ pub struct Request<'a> {
/// > PIN/UV protocol version selected by platform.
#[builder(default, setter(strip_option))]
#[cfg_attr(feature = "serde", serde(rename = 0x09))]
pub pin_uv_auth_protocol_version: Option<client_pin::auth_protocol::Version>,
pub pin_uv_auth_protocol_version: Option<client_pin::AuthProtocolVersion>,
/// > An authenticator supporting this enterprise attestation feature is
/// > enterprise attestation capable and signals its support via the `ep`
/// > Option ID in the `authenticatorGetInfo` command response.
@ -117,7 +117,8 @@ pub struct Request<'a> {
pub enterprise_attestation: Option<attestation::enterprise::Kind>,
}
#[cfg_attr(feature = "serde", derive(Deserialize))]
#[cfg_eval]
#[cfg_attr(feature = "serde", serde_as, derive(Deserialize))]
#[derive(Debug)]
pub struct Response {
#[cfg_attr(feature = "serde", serde(rename = 0x01))]
@ -141,3 +142,6 @@ pub struct Response {
#[cfg_attr(feature = "serde", serde(rename = 0x06))]
pub unsigned_extension_outputs: Option<BTreeMap<extensions::Identifier, Vec<u8>>>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct AttestationStatement {}

@ -1,57 +1,79 @@
use crate::{authenticator::client_pin, extensions::cred_protect, Sha256Hash};
use client_pin::PinUvAuthParam;
use fido_common::credential::public_key;
use std::fmt::Display;
<<<<<<< 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;
#[derive(Clone, Copy)]
#[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::auth_protocol::Version,
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::auth_protocol::Version,
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::auth_protocol::Version,
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::auth_protocol::Version,
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::auth_protocol::Version,
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
@ -82,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,
@ -89,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,
@ -103,6 +127,7 @@ pub struct Credential {
pub large_blob_key: Vec<u8>,
}
#[derive(Debug, Clone)]
pub enum Error {
PinUvAuthTokenRequired,
MissingParameter,
@ -111,17 +136,3 @@ pub enum Error {
NoCredentials,
KeyStoreFull,
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
Error::PinUvAuthTokenRequired => "PIN/UV auth token required",
Error::MissingParameter => "Missing parameter",
Error::InvalidParameter => "Invalid parameter",
Error::PinAuthInvalid => "PIN auth invalid",
Error::NoCredentials => "No credentials",
Error::KeyStoreFull => "Key store full",
};
write!(f, "{}", message)
}
}

@ -1,9 +1,14 @@
use super::Error;
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")]
enum RawSubcommand {
pub(super) enum RawSubcommand {
GetCredsMetadata = 0x01,
EnumerateRpsBegin = 0x02,
EnumerateRpsGetNextRp = 0x03,
@ -35,3 +40,375 @@ impl TryFrom<u8> for RawSubcommand {
})
}
}
#[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!(),
}
}
}

@ -1,4 +1,4 @@
use crate::authenticator::client_pin::auth_protocol;
use crate::authenticator::client_pin::AuthProtocolVersion;
use crate::authenticator::Transport;
use crate::extensions;
use fido_common::credential::public_key;
@ -247,7 +247,7 @@ pub struct Info {
/// > authenticator preference. MUST NOT contain duplicate values...
// Cannot be empty if present
#[cfg_attr(feature = "serde", serde(rename = 0x06))]
pub pin_uv_auth_protocols: Option<Vec<auth_protocol::Version>>,
pub pin_uv_auth_protocols: Option<Vec<AuthProtocolVersion>>,
/// > Maximum number of credentials supported in credentialID list at a time
/// > by the authenticator.
#[cfg_attr(feature = "serde", serde(rename = 0x07))]

@ -1,9 +1,5 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Possible errors for the [`Ctap2Device::reset`] command.
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Error {
/// Returned if the `reset` operation is disabled for the transport used or
/// if user precense is explicitly denied.

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

@ -1,4 +1,4 @@
#![feature(cfg_eval, adt_const_params)]
#![feature(cfg_eval)]
pub mod prelude {
pub use crate::{
@ -19,10 +19,12 @@ pub mod extensions;
/// Defines the raw CTAP operations
pub trait Ctap2_2Authenticator {
#[allow(clippy::missing_errors_doc)]
/// > This method is invoked by the host to request generation of a new
/// > credential in the authenticator.
fn make_credential(&mut self, request: make::Request) -> Result<make::Response, make::Error>;
#[allow(clippy::missing_errors_doc)]
/// > This method is used by a host to request cryptographic proof of user
/// > authentication as well as user consent to a given transaction, using a
/// > previously generated credential that is bound to the authenticator and
@ -35,41 +37,42 @@ pub trait Ctap2_2Authenticator {
/// > this information to tailor their command parameters choices.
fn get_info(&self) -> device::Info;
#[allow(clippy::missing_errors_doc)]
/// > This command exists so that plaintext PINs are not sent to the
/// > authenticator. Instead, a PIN/UV auth protocol (aka
/// > `pinUvAuthProtocol`) ensures that PINs are encrypted when sent to an
/// > authenticator and are exchanged for a `pinUvAuthToken` that serves to
/// > authenticate subsequent commands.
fn client_pin(
&mut self,
request: client_pin::Request,
) -> Result<client_pin::Response, client_pin::Error>;
fn client_pin(request: client_pin::Request) -> Result<client_pin::Response, client_pin::Error>;
#[allow(clippy::missing_errors_doc)]
/// > This method is used by the client to reset an authenticator back to a
/// > factory default state.
fn reset(&mut self) -> Result<(), reset::Error>;
fn bio_enrollment(
&mut self,
request: bio_enrollment::Request,
) -> Result<bio_enrollment::Response, bio_enrollment::Error>;
// fn bio_enrollment(
// request: bio_enrollment::Request,
// ) -> Result<bio_enrollment::Response, bio_enrollment::Error>;
// #[allow(clippy::missing_errors_doc)]
// > This command is used by the platform to manage discoverable
// > credentials on the authenticator.
fn credential_management(
&mut self,
request: management::Request,
) -> Result<management::Response, management::Error>;
// fn credential_management(
// request: management::Request,
// ) -> Result<management::Response, management::Error>;
#[allow(clippy::missing_errors_doc)]
/// > This command allows the platform to let a user select a certain
/// > authenticator by asking for user presence.
fn selection(&mut self) -> Result<(), authenticator::selection::Error>;
fn selection() -> Result<(), authenticator::selection::Error>;
// fn large_blobs() -> Result<(), ()>;
// #[allow(clippy::missing_errors_doc)]
// > This command is used to configure various authenticator features
// > through the use of its subcommands.
fn authenticator_config(&mut self, request: config::Request) -> Result<(), config::Error>;
// fn authenticator_config(request: config::Request) -> Result<(),
// config::Error>;
}
#[repr(u8)]
@ -86,5 +89,5 @@ pub enum Command {
AuthenticatorLargeBlobs = 0x0C,
AuthenticatorConfig = 0x0D,
PrototypeAuthenticatorBioEnrollment = 0x40,
PrototypeAuthenticatorCredentialManagement = 0x41,
PrototypeAuthenticatorCredentialmanagement = 0x41,
}

@ -8,9 +8,8 @@ edition = "2021"
[dependencies]
ciborium = { version = "0.2.1", default-features = false, optional = true }
coset = { version = "0.3.4", default-features = false }
serde = { version = "=1.0.136", features = ["derive"], optional = true }
# Version <= to support older serde
serde_with = { version = "<=2.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_with = { path = "../../../serde_with/serde_with", optional = true }
bitflags = { version = "2.2.1", default-features = false, optional = true }
[features]

@ -77,7 +77,7 @@ pub enum FormatIdentifier {
}
#[cfg_eval]
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Debug)]
#[cfg_attr(
feature = "serde",
serde_as,
@ -109,12 +109,11 @@ pub struct CredentialData {
/// > The AAGUID of the authenticator.
pub aaguid: [u8; 16],
/// The ID of the credential.
pub id: Vec<u8>,
pub id: Vec<u8>, // TODO: Make no_std compatible with the credential id length?
/// The public key of the credential.
pub public_key: coset::CoseKey,
}
#[cfg(feature = "serde")]
impl TryFrom<&[u8]> for CredentialData {
// TODO: Custom error type?
type Error = coset::CoseError;

@ -46,7 +46,6 @@ pub struct Data {
pub extensions: Option<BTreeMap<extensions::Identifier, Vec<u8>>>,
}
#[cfg(feature = "serde")]
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
// > Flags (bit 0 is the least significant bit):
@ -72,7 +71,6 @@ bitflags! {
}
}
#[cfg(feature = "serde")]
impl DataFlags {
fn user_presence(&self) -> UserPresence {
if self.contains(DataFlags::USER_PRESENCE) {
@ -195,6 +193,5 @@ pub enum Transport {
/// > authenticators are not removable from the client device.
#[cfg_attr(feature = "serde", serde(rename = "internal"))]
Internal,
// TODO: Serialize as contents of string
Unknown(String),
Unknown(String),
}

@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
/// > enumeration are used for versioning the Authentication
/// > Assertion and attestation structures according to the type of
/// > the authenticator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum Type {

@ -34,7 +34,7 @@ pub(crate) mod algorithm {
/// > This dictionary is used to supply additional parameters when
/// > creating a new credential.
#[cfg_eval]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Parameters {
/// > This member specifies the type of credential to be
@ -50,9 +50,31 @@ pub struct Parameters {
pub algorithm: coset::iana::Algorithm,
}
// #[cfg(feature = "serde")]
// pub(crate) fn deserialize_algorithm<'de, D>(deserializer: D) ->
// Result<coset::iana::Algorithm, D::Error> where
// D: serde::Deserializer<'de>,
// {
// use serde::de;
//
// let i = i64::deserialize(deserializer)?;
// coset::iana::Algorithm::from_i64(i).ok_or(de::Error::invalid_value(
// de::Unexpected::Signed(i),
// &"an IANA-registered COSE algorithm value",
// ))
// }
//
// #[cfg(feature = "serde")]
// pub(crate) fn serialize_algorithm<S: serde::Serializer>(
// algorithm: &coset::iana::Algorithm,
// s: S,
// ) -> Result<S::Ok, S::Error> {
// s.serialize_i64(algorithm.to_i64())
// }
/// > This dictionary identifies a specific public key credential.
#[cfg_eval]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "serde",
serde_as,
@ -81,7 +103,7 @@ pub struct Descriptor {
/// > the RP.
/// Due to deprecation, the `icon` URL is omitted. See <https://github.com/w3c/webauthn/pull/1337/>.
#[cfg_eval]
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "serde",
serde_as,
@ -118,7 +140,7 @@ pub struct UserEntity {
/// > This `PublicKeyCredentialRpEntity` data structure describes a Relying
/// > Party with which the new public key credential will be associated.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct RelyingPartyEntity {
/// > A unique identifier for the Relying Party entity.

@ -21,7 +21,7 @@ use serde::{Deserialize, Serialize};
/// > Extensions that may exist in multiple versions should take care to include
/// > a version in their identifier. In effect, different versions are thus
/// > treated as different extensions, e.g., `myCompany_extension_01`
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Identifier {
#[cfg_attr(feature = "serde", serde(rename = "appid"))]

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

@ -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,11 +1,10 @@
use crate::token;
use std::marker::ConstParamTy;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(PartialEq, Eq, Clone, Copy, ConstParamTy)]
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum DataType {
#[cfg_attr(feature = "serde", serde(rename = "webauthn.create"))]
Create,

@ -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…
Cancel
Save