mirror of
https://github.com/trussed-dev/fido-authenticator.git
synced 2026-06-20 04:16:16 -07:00
1278 lines
36 KiB
Rust
1278 lines
36 KiB
Rust
use std::{collections::BTreeMap, fmt::Debug, iter};
|
||
|
||
use ciborium::Value;
|
||
use cipher::{BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit};
|
||
use hmac::Mac;
|
||
use p256::ecdsa::{signature::Verifier as _, DerSignature, VerifyingKey};
|
||
use rand::RngCore as _;
|
||
use serde::Deserialize;
|
||
|
||
pub trait Exhaustive: Sized {
|
||
fn iter_exhaustive() -> impl Iterator<Item = Self> + Clone;
|
||
}
|
||
|
||
impl Exhaustive for bool {
|
||
fn iter_exhaustive() -> impl Iterator<Item = Self> + Clone {
|
||
[false, true].into_iter()
|
||
}
|
||
}
|
||
|
||
impl<T: Exhaustive + Clone> Exhaustive for Option<T> {
|
||
fn iter_exhaustive() -> impl Iterator<Item = Self> + Clone {
|
||
iter::once(None).chain(T::iter_exhaustive().map(Some))
|
||
}
|
||
}
|
||
|
||
macro_rules! iter_map {
|
||
([ $($name:ident: $iter:expr,)* ] => $map:expr) => {
|
||
::itertools::iproduct!(
|
||
$($iter,)*
|
||
).map(|($($name,)*)| { $map })
|
||
}
|
||
}
|
||
|
||
#[allow(unused_imports)]
|
||
pub(crate) use iter_map;
|
||
|
||
macro_rules! exhaustive_struct {
|
||
($($field:ident: $type:ty,)*) => {
|
||
$crate::webauthn::iter_map! {
|
||
[
|
||
$($field: <$type as Exhaustive>::iter_exhaustive(),)*
|
||
] => Self { $($field,)* }
|
||
}
|
||
}
|
||
}
|
||
|
||
#[allow(unused_imports)]
|
||
pub(crate) use exhaustive_struct;
|
||
|
||
pub trait Test: Debug {
|
||
fn test(&self);
|
||
|
||
fn run(&self) {
|
||
println!("{}", "=".repeat(80));
|
||
println!("Running test:");
|
||
println!("{self:#?}");
|
||
println!();
|
||
|
||
self.test();
|
||
}
|
||
|
||
fn run_all()
|
||
where
|
||
Self: Exhaustive,
|
||
{
|
||
for test in Self::iter_exhaustive() {
|
||
test.run();
|
||
}
|
||
}
|
||
}
|
||
|
||
pub struct KeyAgreementKey(p256::ecdh::EphemeralSecret);
|
||
|
||
impl KeyAgreementKey {
|
||
pub fn generate() -> Self {
|
||
Self(p256::ecdh::EphemeralSecret::random(&mut rand::thread_rng()))
|
||
}
|
||
|
||
pub fn public_key(&self) -> PublicKey {
|
||
PublicKey(self.0.public_key())
|
||
}
|
||
|
||
pub fn shared_secret(&self, peer: &PublicKey) -> SharedSecret {
|
||
let shared_point = self.0.diffie_hellman(&peer.0);
|
||
let hkdf = shared_point.extract::<sha2::Sha256>(Some(&[0; 32]));
|
||
let mut hmac_key = [0; 32];
|
||
let mut aes_key = [0; 32];
|
||
hkdf.expand(b"CTAP2 HMAC key", &mut hmac_key).unwrap();
|
||
hkdf.expand(b"CTAP2 AES key", &mut aes_key).unwrap();
|
||
SharedSecret { hmac_key, aes_key }
|
||
}
|
||
}
|
||
|
||
#[derive(Copy, Clone, Debug)]
|
||
pub struct PublicKey(p256::PublicKey);
|
||
|
||
impl From<PublicKey> for Value {
|
||
fn from(public_key: PublicKey) -> Value {
|
||
let encoded = p256::EncodedPoint::from(&public_key.0);
|
||
let mut map = Map::default();
|
||
map.push(1, 2);
|
||
map.push(3, -25);
|
||
map.push(-1, 1);
|
||
map.push(-2, encoded.x().unwrap().as_slice());
|
||
map.push(-3, encoded.y().unwrap().as_slice());
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl From<Value> for PublicKey {
|
||
fn from(value: Value) -> Self {
|
||
let map: BTreeMap<i8, Value> = value.deserialized().unwrap();
|
||
let kty = map.get(&1).unwrap();
|
||
let alg = map.get(&3).unwrap();
|
||
let crv = map.get(&-1).unwrap();
|
||
let x = map.get(&-2).unwrap().as_bytes().unwrap().as_slice();
|
||
let y = map.get(&-3).unwrap().as_bytes().unwrap().as_slice();
|
||
|
||
assert_eq!(kty, &Value::from(2));
|
||
assert_eq!(alg, &Value::from(-25));
|
||
assert_eq!(crv, &Value::from(1));
|
||
let encoded = p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false);
|
||
Self(encoded.try_into().unwrap())
|
||
}
|
||
}
|
||
|
||
pub struct SharedSecret {
|
||
hmac_key: [u8; 32],
|
||
aes_key: [u8; 32],
|
||
}
|
||
|
||
impl SharedSecret {
|
||
pub fn encrypt(&self, data: &[u8]) -> Vec<u8> {
|
||
let mut iv = [0; 16];
|
||
rand::thread_rng().fill_bytes(&mut iv);
|
||
|
||
let cipher: cbc::Encryptor<aes::Aes256> =
|
||
KeyIvInit::new(self.aes_key.as_ref().into(), iv.as_ref().into());
|
||
let encrypted = cipher.encrypt_padded_vec_mut::<cipher::block_padding::NoPadding>(data);
|
||
|
||
let mut result = Vec::new();
|
||
result.extend_from_slice(&iv);
|
||
result.extend_from_slice(&encrypted);
|
||
result
|
||
}
|
||
|
||
pub fn decrypt(&self, data: &[u8]) -> Vec<u8> {
|
||
let (iv, data) = data.split_first_chunk::<16>().unwrap();
|
||
let cipher: cbc::Decryptor<aes::Aes256> =
|
||
KeyIvInit::new(self.aes_key.as_ref().into(), iv.into());
|
||
cipher
|
||
.decrypt_padded_vec_mut::<cipher::block_padding::NoPadding>(data)
|
||
.unwrap()
|
||
}
|
||
|
||
pub fn decrypt_pin_token(&self, data: &[u8]) -> PinToken {
|
||
PinToken(self.decrypt(data).try_into().unwrap())
|
||
}
|
||
|
||
pub fn authenticate(&self, data: &[u8]) -> [u8; 32] {
|
||
let mut mac: hmac::Hmac<sha2::Sha256> = Mac::new_from_slice(&self.hmac_key).unwrap();
|
||
mac.update(data);
|
||
mac.finalize().into_bytes().into()
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, PartialEq)]
|
||
pub struct PinToken([u8; 32]);
|
||
|
||
impl PinToken {
|
||
pub fn authenticate(&self, data: &[u8]) -> [u8; 32] {
|
||
let mut mac: hmac::Hmac<sha2::Sha256> = Mac::new_from_slice(&self.0).unwrap();
|
||
mac.update(data);
|
||
mac.finalize().into_bytes().into()
|
||
}
|
||
}
|
||
|
||
#[derive(Default)]
|
||
pub struct Map(Vec<(Value, Value)>);
|
||
|
||
impl Map {
|
||
pub fn push(&mut self, key: impl Into<Value>, value: impl Into<Value>) {
|
||
self.0.push((key.into(), value.into()));
|
||
}
|
||
}
|
||
|
||
impl From<Map> for Value {
|
||
fn from(map: Map) -> Value {
|
||
Value::from(map.0)
|
||
}
|
||
}
|
||
|
||
pub trait Request: Into<Value> {
|
||
const COMMAND: u8;
|
||
|
||
type Reply: From<Value>;
|
||
}
|
||
|
||
pub struct ClientPin {
|
||
protocol: u8,
|
||
subcommand: u8,
|
||
pub key_agreement: Option<PublicKey>,
|
||
pub pin_auth: Option<[u8; 32]>,
|
||
pub new_pin_enc: Option<Vec<u8>>,
|
||
pub pin_hash_enc: Option<Vec<u8>>,
|
||
pub permissions: Option<u8>,
|
||
pub rp_id: Option<String>,
|
||
}
|
||
|
||
impl ClientPin {
|
||
pub fn new(protocol: u8, subcommand: u8) -> Self {
|
||
Self {
|
||
protocol,
|
||
subcommand,
|
||
key_agreement: None,
|
||
new_pin_enc: None,
|
||
pin_hash_enc: None,
|
||
permissions: None,
|
||
pin_auth: None,
|
||
rp_id: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<ClientPin> for Value {
|
||
fn from(request: ClientPin) -> Self {
|
||
let mut map = Map::default();
|
||
map.push(1, request.protocol);
|
||
map.push(2, request.subcommand);
|
||
if let Some(key_agreement) = request.key_agreement {
|
||
map.push(3, key_agreement);
|
||
}
|
||
if let Some(pin_auth) = request.pin_auth {
|
||
map.push(4, pin_auth.as_slice());
|
||
}
|
||
if let Some(new_pin_enc) = request.new_pin_enc {
|
||
map.push(5, new_pin_enc);
|
||
}
|
||
if let Some(pin_hash_enc) = request.pin_hash_enc {
|
||
map.push(6, pin_hash_enc);
|
||
}
|
||
if let Some(permissions) = request.permissions {
|
||
map.push(9, permissions);
|
||
}
|
||
if let Some(rp_id) = request.rp_id {
|
||
map.push(0x0a, rp_id);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl Request for ClientPin {
|
||
const COMMAND: u8 = 0x06;
|
||
|
||
type Reply = ClientPinReply;
|
||
}
|
||
|
||
pub struct ClientPinReply {
|
||
pub key_agreement: Option<Value>,
|
||
pub pin_token: Option<Value>,
|
||
pub pin_retries: Option<u8>,
|
||
}
|
||
|
||
impl From<Value> for ClientPinReply {
|
||
fn from(value: Value) -> Self {
|
||
let mut map: BTreeMap<u8, Value> = value.deserialized().unwrap();
|
||
Self {
|
||
key_agreement: map.remove(&1),
|
||
pin_token: map.remove(&2),
|
||
pin_retries: map.remove(&3).map(|value| value.deserialized().unwrap()),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
|
||
pub struct Rp {
|
||
pub id: String,
|
||
pub name: Option<String>,
|
||
}
|
||
|
||
impl Rp {
|
||
pub fn new(id: impl Into<String>) -> Self {
|
||
Self {
|
||
id: id.into(),
|
||
name: None,
|
||
}
|
||
}
|
||
|
||
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||
self.name = Some(name.into());
|
||
self
|
||
}
|
||
}
|
||
|
||
impl From<Rp> for Value {
|
||
fn from(rp: Rp) -> Value {
|
||
let mut map = Map::default();
|
||
map.push("id", rp.id);
|
||
if let Some(name) = rp.name {
|
||
map.push("name", name);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl From<Value> for Rp {
|
||
fn from(value: Value) -> Self {
|
||
let mut map: BTreeMap<String, Value> = value.deserialized().unwrap();
|
||
Self {
|
||
id: map.remove("id").unwrap().deserialized().unwrap(),
|
||
name: map
|
||
.remove("name")
|
||
.map(|value| value.deserialized().unwrap()),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||
pub struct User {
|
||
pub id: Vec<u8>,
|
||
pub name: Option<String>,
|
||
pub display_name: Option<String>,
|
||
}
|
||
|
||
impl User {
|
||
pub fn new(id: impl Into<Vec<u8>>) -> Self {
|
||
Self {
|
||
id: id.into(),
|
||
name: None,
|
||
display_name: None,
|
||
}
|
||
}
|
||
|
||
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||
self.name = Some(name.into());
|
||
self
|
||
}
|
||
|
||
pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
|
||
self.display_name = Some(display_name.into());
|
||
self
|
||
}
|
||
}
|
||
|
||
impl From<User> for Value {
|
||
fn from(user: User) -> Value {
|
||
let mut map = Map::default();
|
||
map.push("id", user.id);
|
||
if let Some(name) = user.name {
|
||
map.push("name", name);
|
||
}
|
||
if let Some(display_name) = user.display_name {
|
||
map.push("displayName", display_name);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl From<Value> for User {
|
||
fn from(value: Value) -> User {
|
||
let mut map: BTreeMap<String, Value> = value.deserialized().unwrap();
|
||
Self {
|
||
id: map.remove("id").unwrap().into_bytes().unwrap(),
|
||
name: map
|
||
.remove("name")
|
||
.map(|value| value.deserialized().unwrap()),
|
||
display_name: map
|
||
.remove("displayName")
|
||
.map(|value| value.deserialized().unwrap()),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub struct PubKeyCredParam {
|
||
ty: String,
|
||
alg: i32,
|
||
}
|
||
|
||
impl PubKeyCredParam {
|
||
pub fn new(ty: impl Into<String>, alg: impl Into<i32>) -> Self {
|
||
Self {
|
||
ty: ty.into(),
|
||
alg: alg.into(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<PubKeyCredParam> for Value {
|
||
fn from(param: PubKeyCredParam) -> Value {
|
||
let mut map = Map::default();
|
||
map.push("type", param.ty);
|
||
map.push("alg", param.alg);
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
pub struct MakeCredential {
|
||
client_data_hash: Vec<u8>,
|
||
rp: Rp,
|
||
user: User,
|
||
pub_key_cred_params: Vec<PubKeyCredParam>,
|
||
pub extensions: Option<MakeCredentialExtensionsInput>,
|
||
pub options: Option<MakeCredentialOptions>,
|
||
pub pin_auth: Option<[u8; 32]>,
|
||
/// Variable-length override for `pin_auth`, used by tests that exercise
|
||
/// the zero-length `pinUvAuthParam` path (CTAP 2.1 §6.1.2 step 1 +
|
||
/// §6.5.5.7 step 2 — "CTAP 2.0 backwards-compat" — where the platform
|
||
/// sends a 0-byte param to probe whether the authenticator has a PIN).
|
||
/// When `Some(_)`, this field is serialised instead of `pin_auth`.
|
||
pub pin_auth_raw: Option<Vec<u8>>,
|
||
pub pin_protocol: Option<u8>,
|
||
pub attestation_formats_preference: Option<Vec<&'static str>>,
|
||
}
|
||
|
||
impl MakeCredential {
|
||
pub fn new(
|
||
client_data_hash: impl Into<Vec<u8>>,
|
||
rp: Rp,
|
||
user: User,
|
||
pub_key_cred_params: impl Into<Vec<PubKeyCredParam>>,
|
||
) -> Self {
|
||
Self {
|
||
client_data_hash: client_data_hash.into(),
|
||
rp,
|
||
user,
|
||
pub_key_cred_params: pub_key_cred_params.into(),
|
||
extensions: None,
|
||
options: None,
|
||
pin_auth: None,
|
||
pin_auth_raw: None,
|
||
pin_protocol: None,
|
||
attestation_formats_preference: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<MakeCredential> for Value {
|
||
fn from(request: MakeCredential) -> Value {
|
||
let mut map = Map::default();
|
||
map.push(1, request.client_data_hash);
|
||
map.push(2, request.rp);
|
||
map.push(3, request.user);
|
||
map.push(
|
||
4,
|
||
request
|
||
.pub_key_cred_params
|
||
.into_iter()
|
||
.map(Value::from)
|
||
.collect::<Vec<_>>(),
|
||
);
|
||
if let Some(extensions) = request.extensions {
|
||
map.push(6, extensions);
|
||
}
|
||
if let Some(options) = request.options {
|
||
map.push(7, options);
|
||
}
|
||
// `pin_auth_raw` takes precedence over the fixed-size `pin_auth` —
|
||
// tests that need a variable-length (e.g. zero-length) pinUvAuthParam
|
||
// use the raw field. The mutual-exclusion is a soft contract; the
|
||
// serializer just prefers raw when both are set.
|
||
if let Some(pin_auth_raw) = request.pin_auth_raw.as_ref() {
|
||
map.push(8, pin_auth_raw.as_slice());
|
||
} else if let Some(pin_auth) = request.pin_auth {
|
||
map.push(8, pin_auth.as_slice());
|
||
}
|
||
if let Some(pin_protocol) = request.pin_protocol {
|
||
map.push(9, pin_protocol);
|
||
}
|
||
if let Some(attestation_formats_preference) = request.attestation_formats_preference {
|
||
let preference: Vec<_> = attestation_formats_preference
|
||
.into_iter()
|
||
.map(Value::from)
|
||
.collect();
|
||
map.push(0x0b, preference);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default)]
|
||
pub struct MakeCredentialExtensionsInput {
|
||
pub hmac_secret: Option<bool>,
|
||
/// CTAP 2.2 §11.4.5: `hmac-secret-mc` allows the platform to evaluate
|
||
/// hmac-secret at MakeCredential time. Same payload shape as the
|
||
/// GetAssertion hmac-secret input (key_agreement + salt_enc + salt_auth
|
||
/// + pin_protocol).
|
||
pub hmac_secret_mc: Option<HmacSecretInput>,
|
||
pub third_party_payment: Option<bool>,
|
||
pub cred_blob: Option<Vec<u8>>,
|
||
pub min_pin_length: Option<bool>,
|
||
}
|
||
|
||
impl MakeCredentialExtensionsInput {
|
||
pub fn cred_blob(mut self, cred_blob: Vec<u8>) -> Self {
|
||
self.cred_blob = Some(cred_blob);
|
||
self
|
||
}
|
||
|
||
pub fn min_pin_length(mut self, min_pin_length: bool) -> Self {
|
||
self.min_pin_length = Some(min_pin_length);
|
||
self
|
||
}
|
||
}
|
||
|
||
impl From<MakeCredentialExtensionsInput> for Value {
|
||
fn from(extensions: MakeCredentialExtensionsInput) -> Value {
|
||
let mut map = Map::default();
|
||
if let Some(cred_blob) = extensions.cred_blob {
|
||
map.push("credBlob", cred_blob);
|
||
}
|
||
if let Some(hmac_secret) = extensions.hmac_secret {
|
||
map.push("hmac-secret", hmac_secret);
|
||
}
|
||
if let Some(min_pin_length) = extensions.min_pin_length {
|
||
map.push("minPinLength", min_pin_length);
|
||
}
|
||
if let Some(hmac_secret_mc) = extensions.hmac_secret_mc {
|
||
map.push("hmac-secret-mc", hmac_secret_mc);
|
||
}
|
||
if let Some(third_party_payment) = extensions.third_party_payment {
|
||
map.push("thirdPartyPayment", third_party_payment);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
pub struct MakeCredentialOptions {
|
||
pub rk: Option<bool>,
|
||
pub up: Option<bool>,
|
||
pub uv: Option<bool>,
|
||
}
|
||
|
||
impl MakeCredentialOptions {
|
||
pub fn rk(mut self, rk: bool) -> Self {
|
||
self.rk = Some(rk);
|
||
self
|
||
}
|
||
|
||
pub fn up(mut self, up: bool) -> Self {
|
||
self.up = Some(up);
|
||
self
|
||
}
|
||
|
||
pub fn uv(mut self, uv: bool) -> Self {
|
||
self.uv = Some(uv);
|
||
self
|
||
}
|
||
}
|
||
|
||
impl MakeCredentialOptions {
|
||
pub fn iter_valid() -> impl Iterator<Item = Self> + Clone {
|
||
iter_map! {
|
||
[
|
||
rk: [true, false],
|
||
uv: [true, false],
|
||
] => Self {
|
||
rk: rk.then_some(true),
|
||
up: None,
|
||
uv: uv.then_some(true),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<MakeCredentialOptions> for Value {
|
||
fn from(options: MakeCredentialOptions) -> Value {
|
||
let mut map = Map::default();
|
||
if let Some(rk) = options.rk {
|
||
map.push("rk", rk);
|
||
}
|
||
if let Some(up) = options.up {
|
||
map.push("up", up);
|
||
}
|
||
if let Some(uv) = options.uv {
|
||
map.push("uv", uv);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl Exhaustive for MakeCredentialOptions {
|
||
fn iter_exhaustive() -> impl Iterator<Item = Self> + Clone {
|
||
exhaustive_struct! {
|
||
rk: Option<bool>,
|
||
up: Option<bool>,
|
||
uv: Option<bool>,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Request for MakeCredential {
|
||
const COMMAND: u8 = 0x01;
|
||
|
||
type Reply = MakeCredentialReply;
|
||
}
|
||
|
||
pub enum AttStmtFormat {
|
||
None,
|
||
Packed,
|
||
}
|
||
|
||
impl AttStmtFormat {
|
||
pub fn as_str(&self) -> &'static str {
|
||
match self {
|
||
Self::None => "none",
|
||
Self::Packed => "packed",
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, PartialEq, Deserialize)]
|
||
pub struct AttStmt(BTreeMap<String, Value>);
|
||
|
||
impl AttStmt {
|
||
pub fn validate(&self, format: AttStmtFormat, auth_data: &AuthData) {
|
||
match format {
|
||
AttStmtFormat::Packed => {
|
||
let alg = self.0.get("alg").unwrap();
|
||
let x5c = self.0.get("x5c").unwrap().as_array().unwrap();
|
||
let cert = x5c.first().unwrap().as_bytes().unwrap();
|
||
let sig = self.0.get("sig").unwrap().as_bytes().unwrap();
|
||
assert_eq!(alg, &Value::from(-7));
|
||
|
||
let (rest, cert) = x509_parser::parse_x509_certificate(cert).unwrap();
|
||
assert!(rest.is_empty());
|
||
let signature = DerSignature::from_bytes(sig).unwrap();
|
||
let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap();
|
||
let x509_parser::public_key::PublicKey::EC(ec_point) = public_key else {
|
||
panic!("unexpected public key in attestation certificate");
|
||
};
|
||
let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap();
|
||
public_key.verify(&auth_data.bytes, &signature).unwrap();
|
||
}
|
||
AttStmtFormat::None => {
|
||
assert!(self.0.is_empty());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, PartialEq)]
|
||
pub struct MakeCredentialReply {
|
||
pub fmt: String,
|
||
pub auth_data: AuthData,
|
||
pub att_stmt: Option<AttStmt>,
|
||
}
|
||
|
||
impl From<Value> for MakeCredentialReply {
|
||
fn from(value: Value) -> Self {
|
||
let mut map: BTreeMap<u8, Value> = value.deserialized().unwrap();
|
||
Self {
|
||
fmt: map.remove(&1).unwrap().deserialized().unwrap(),
|
||
auth_data: map.remove(&2).unwrap().into(),
|
||
att_stmt: map.remove(&3).map(|value| value.deserialized().unwrap()),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||
pub struct PubKeyCredDescriptor {
|
||
pub ty: String,
|
||
pub id: Vec<u8>,
|
||
}
|
||
|
||
impl PubKeyCredDescriptor {
|
||
pub fn new(ty: impl Into<String>, id: impl Into<Vec<u8>>) -> Self {
|
||
Self {
|
||
ty: ty.into(),
|
||
id: id.into(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<PubKeyCredDescriptor> for Value {
|
||
fn from(descriptor: PubKeyCredDescriptor) -> Value {
|
||
let mut map = Map::default();
|
||
map.push("id", descriptor.id);
|
||
map.push("type", descriptor.ty);
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl From<Value> for PubKeyCredDescriptor {
|
||
fn from(value: Value) -> Self {
|
||
let mut map: BTreeMap<String, Value> = value.deserialized().unwrap();
|
||
Self {
|
||
ty: map.remove("type").unwrap().into_text().unwrap(),
|
||
id: map.remove("id").unwrap().into_bytes().unwrap(),
|
||
}
|
||
}
|
||
}
|
||
|
||
pub struct GetAssertion {
|
||
rp_id: String,
|
||
client_data_hash: Vec<u8>,
|
||
pub allow_list: Option<Vec<PubKeyCredDescriptor>>,
|
||
pub extensions: Option<GetAssertionExtensionsInput>,
|
||
pub options: Option<GetAssertionOptions>,
|
||
}
|
||
|
||
impl GetAssertion {
|
||
pub fn new(rp_id: impl Into<String>, client_data_hash: impl Into<Vec<u8>>) -> Self {
|
||
Self {
|
||
rp_id: rp_id.into(),
|
||
client_data_hash: client_data_hash.into(),
|
||
allow_list: None,
|
||
extensions: None,
|
||
options: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<GetAssertion> for Value {
|
||
fn from(request: GetAssertion) -> Value {
|
||
let mut map = Map::default();
|
||
map.push(0x01, request.rp_id);
|
||
map.push(0x02, request.client_data_hash);
|
||
if let Some(allow_list) = request.allow_list {
|
||
let values: Vec<_> = allow_list.into_iter().map(Value::from).collect();
|
||
map.push(0x03, values);
|
||
}
|
||
if let Some(extensions) = request.extensions {
|
||
map.push(0x04, extensions);
|
||
}
|
||
if let Some(options) = request.options {
|
||
map.push(0x05, options);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl Request for GetAssertion {
|
||
const COMMAND: u8 = 0x02;
|
||
|
||
type Reply = GetAssertionReply;
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default)]
|
||
pub struct GetAssertionExtensionsInput {
|
||
pub third_party_payment: Option<bool>,
|
||
pub hmac_secret: Option<HmacSecretInput>,
|
||
pub cred_blob: Option<bool>,
|
||
}
|
||
|
||
impl GetAssertionExtensionsInput {
|
||
pub fn cred_blob(mut self, cred_blob: bool) -> Self {
|
||
self.cred_blob = Some(cred_blob);
|
||
self
|
||
}
|
||
}
|
||
|
||
impl From<GetAssertionExtensionsInput> for Value {
|
||
fn from(extensions: GetAssertionExtensionsInput) -> Value {
|
||
let mut map = Map::default();
|
||
if let Some(cred_blob) = extensions.cred_blob {
|
||
map.push("credBlob", cred_blob);
|
||
}
|
||
if let Some(hmac_secret) = extensions.hmac_secret {
|
||
map.push("hmac-secret", hmac_secret);
|
||
}
|
||
if let Some(third_party_payment) = extensions.third_party_payment {
|
||
map.push("thirdPartyPayment", third_party_payment);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub struct HmacSecretInput {
|
||
pub key_agreement: PublicKey,
|
||
pub salt_enc: Vec<u8>,
|
||
pub salt_auth: [u8; 32],
|
||
pub pin_protocol: Option<u32>,
|
||
}
|
||
|
||
impl From<HmacSecretInput> for Value {
|
||
fn from(input: HmacSecretInput) -> Value {
|
||
let mut map = Map::default();
|
||
map.push(0x01, input.key_agreement);
|
||
map.push(0x02, input.salt_enc);
|
||
map.push(0x03, input.salt_auth.as_slice());
|
||
if let Some(pin_protocol) = input.pin_protocol {
|
||
map.push(0x04, pin_protocol);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
pub struct GetAssertionOptions {
|
||
pub up: Option<bool>,
|
||
pub uv: Option<bool>,
|
||
}
|
||
|
||
impl GetAssertionOptions {
|
||
pub fn up(mut self, up: bool) -> Self {
|
||
self.up = Some(up);
|
||
self
|
||
}
|
||
|
||
pub fn uv(mut self, uv: bool) -> Self {
|
||
self.uv = Some(uv);
|
||
self
|
||
}
|
||
|
||
pub fn iter_valid() -> impl Iterator<Item = Self> + Clone {
|
||
iter_map! {
|
||
[
|
||
up: [true, false],
|
||
uv: [true, false],
|
||
] => Self {
|
||
up: Some(up),
|
||
uv: uv.then_some(true),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<GetAssertionOptions> for Value {
|
||
fn from(options: GetAssertionOptions) -> Value {
|
||
let mut map = Map::default();
|
||
if let Some(up) = options.up {
|
||
map.push("up", up);
|
||
}
|
||
if let Some(uv) = options.uv {
|
||
map.push("uv", uv);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl Exhaustive for GetAssertionOptions {
|
||
fn iter_exhaustive() -> impl Iterator<Item = Self> + Clone {
|
||
exhaustive_struct! {
|
||
up: Option<bool>,
|
||
uv: Option<bool>,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, PartialEq)]
|
||
pub struct GetAssertionReply {
|
||
pub credential: PubKeyCredDescriptor,
|
||
pub auth_data: AuthData,
|
||
pub signature: Vec<u8>,
|
||
pub user: Option<Value>,
|
||
pub number_of_credentials: Option<usize>,
|
||
}
|
||
|
||
impl From<Value> for GetAssertionReply {
|
||
fn from(value: Value) -> Self {
|
||
let mut map: BTreeMap<u8, Value> = value.deserialized().unwrap();
|
||
Self {
|
||
credential: map.remove(&0x01).unwrap().into(),
|
||
auth_data: map.remove(&0x02).unwrap().into(),
|
||
signature: map.remove(&0x03).unwrap().into_bytes().unwrap(),
|
||
// 0x04: user (CTAP 2.1 §6.2.3 — included for RKs by both
|
||
// no-allowList and allowList paths)
|
||
user: map.remove(&0x04),
|
||
number_of_credentials: map.remove(&0x05).map(|value| value.deserialized().unwrap()),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, PartialEq)]
|
||
pub struct AuthData {
|
||
pub bytes: Vec<u8>,
|
||
pub flags: u8,
|
||
pub sign_count: u32,
|
||
pub credential: Option<CredentialData>,
|
||
pub extensions: Option<BTreeMap<String, Value>>,
|
||
}
|
||
|
||
impl AuthData {
|
||
pub fn up_flag(&self) -> bool {
|
||
self.flags & 0b1 != 0
|
||
}
|
||
|
||
pub fn uv_flag(&self) -> bool {
|
||
self.flags & 0b100 != 0
|
||
}
|
||
|
||
pub fn at_flag(&self) -> bool {
|
||
self.flags & 0b1000000 != 0
|
||
}
|
||
|
||
pub fn ed_flag(&self) -> bool {
|
||
self.flags & 0b10000000 != 0
|
||
}
|
||
}
|
||
|
||
impl From<Vec<u8>> for AuthData {
|
||
fn from(vec: Vec<u8>) -> Self {
|
||
let (_rp_id_hash, bytes) = vec.split_at(32);
|
||
let (&flags, bytes) = bytes.split_first().unwrap();
|
||
let (sign_count, bytes) = bytes.split_first_chunk::<4>().unwrap();
|
||
let sign_count = u32::from_be_bytes(*sign_count);
|
||
let (credential, bytes) = if flags & 0b0100_0000 == 0b0100_0000 {
|
||
let (credential, bytes) = CredentialData::parse(bytes);
|
||
(Some(credential), bytes)
|
||
} else {
|
||
(None, bytes)
|
||
};
|
||
let extensions = if flags & 0b1000_0000 == 0b1000_0000 {
|
||
Some(ciborium::from_reader(bytes).unwrap())
|
||
} else {
|
||
None
|
||
};
|
||
Self {
|
||
bytes: vec,
|
||
flags,
|
||
sign_count,
|
||
credential,
|
||
extensions,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<Value> for AuthData {
|
||
fn from(value: Value) -> Self {
|
||
value.into_bytes().unwrap().into()
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct CredentialData {
|
||
pub id: Vec<u8>,
|
||
pub public_key: BTreeMap<i32, Value>,
|
||
}
|
||
|
||
impl CredentialData {
|
||
fn parse(bytes: &[u8]) -> (Self, &[u8]) {
|
||
let (_aaguid, bytes) = bytes.split_at(16);
|
||
let (id_length, bytes) = bytes.split_at(2);
|
||
let id_length = u16::from_be_bytes([id_length[0], id_length[1]]);
|
||
let (id, bytes) = bytes.split_at(id_length.into());
|
||
let mut cursor = std::io::Cursor::new(bytes);
|
||
let public_key = ciborium::from_reader(&mut cursor).unwrap();
|
||
let bytes = &bytes[cursor.position().try_into().unwrap()..];
|
||
(
|
||
Self {
|
||
id: id.into(),
|
||
public_key,
|
||
},
|
||
bytes,
|
||
)
|
||
}
|
||
|
||
pub fn verify_assertion(
|
||
&self,
|
||
auth_data: &AuthData,
|
||
client_data_hash: &[u8],
|
||
signature: &[u8],
|
||
) {
|
||
let kty = self.public_key.get(&1).unwrap();
|
||
let alg = self.public_key.get(&3).unwrap();
|
||
let crv = self.public_key.get(&-1).unwrap();
|
||
let x = self
|
||
.public_key
|
||
.get(&-2)
|
||
.unwrap()
|
||
.as_bytes()
|
||
.unwrap()
|
||
.as_slice();
|
||
let y = self
|
||
.public_key
|
||
.get(&-3)
|
||
.unwrap()
|
||
.as_bytes()
|
||
.unwrap()
|
||
.as_slice();
|
||
assert_eq!(kty, &Value::from(2));
|
||
assert_eq!(alg, &Value::from(-7));
|
||
assert_eq!(crv, &Value::from(1));
|
||
|
||
let encoded = p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false);
|
||
let public_key = VerifyingKey::from_encoded_point(&encoded).unwrap();
|
||
let signature = DerSignature::from_bytes(signature).unwrap();
|
||
let mut message = Vec::new();
|
||
message.extend_from_slice(&auth_data.bytes);
|
||
message.extend_from_slice(client_data_hash);
|
||
public_key.verify(&message, &signature).unwrap();
|
||
}
|
||
}
|
||
|
||
pub struct GetNextAssertion;
|
||
|
||
impl From<GetNextAssertion> for Value {
|
||
fn from(_: GetNextAssertion) -> Self {
|
||
Self::Null
|
||
}
|
||
}
|
||
|
||
impl Request for GetNextAssertion {
|
||
const COMMAND: u8 = 0x08;
|
||
|
||
type Reply = GetAssertionReply;
|
||
}
|
||
|
||
pub struct GetInfo;
|
||
|
||
impl From<GetInfo> for Value {
|
||
fn from(_: GetInfo) -> Self {
|
||
Self::Null
|
||
}
|
||
}
|
||
|
||
impl Request for GetInfo {
|
||
const COMMAND: u8 = 0x04;
|
||
|
||
type Reply = GetInfoReply;
|
||
}
|
||
|
||
/// `authenticatorReset` (CTAP 2.1 §6.7), command 0x07. No body, no reply
|
||
/// body — just a status byte.
|
||
pub struct Reset;
|
||
|
||
impl From<Reset> for Value {
|
||
fn from(_: Reset) -> Self {
|
||
Self::Null
|
||
}
|
||
}
|
||
|
||
impl Request for Reset {
|
||
const COMMAND: u8 = 0x07;
|
||
|
||
type Reply = ResetReply;
|
||
}
|
||
|
||
pub struct ResetReply;
|
||
|
||
impl From<Value> for ResetReply {
|
||
fn from(_: Value) -> Self {
|
||
Self
|
||
}
|
||
}
|
||
|
||
pub struct GetInfoReply {
|
||
pub versions: Vec<String>,
|
||
pub extensions: Option<Vec<String>>,
|
||
pub aaguid: Value,
|
||
pub options: Option<BTreeMap<String, Value>>,
|
||
pub pin_protocols: Option<Vec<u8>>,
|
||
pub transports: Option<Vec<String>>,
|
||
pub force_pin_change: Option<bool>,
|
||
pub min_pin_length: Option<u32>,
|
||
pub attestation_formats: Option<Vec<String>>,
|
||
pub long_touch_for_reset: Option<bool>,
|
||
}
|
||
|
||
impl From<Value> for GetInfoReply {
|
||
fn from(value: Value) -> Self {
|
||
let mut map: BTreeMap<u8, Value> = value.deserialized().unwrap();
|
||
Self {
|
||
versions: map.remove(&1).unwrap().deserialized().unwrap(),
|
||
// 0x02: extensions (CTAP 2.0+)
|
||
extensions: map.remove(&2).map(|value| value.deserialized().unwrap()),
|
||
aaguid: map.remove(&3).unwrap().deserialized().unwrap(),
|
||
options: map.remove(&4).map(|value| value.deserialized().unwrap()),
|
||
pin_protocols: map.remove(&6).map(|value| value.deserialized().unwrap()),
|
||
// 0x09: transports (CTAP 2.1)
|
||
transports: map.remove(&9).map(|value| value.deserialized().unwrap()),
|
||
// 0x0C: forcePINChange (CTAP 2.1)
|
||
force_pin_change: map.remove(&0x0C).map(|value| value.deserialized().unwrap()),
|
||
// 0x0D: minPINLength (CTAP 2.1)
|
||
min_pin_length: map.remove(&0x0D).map(|value| value.deserialized().unwrap()),
|
||
attestation_formats: map.remove(&0x16).map(|value| value.deserialized().unwrap()),
|
||
// 0x18: longTouchForReset (CTAP 2.3)
|
||
long_touch_for_reset: map.remove(&0x18).map(|value| value.deserialized().unwrap()),
|
||
}
|
||
}
|
||
}
|
||
|
||
pub struct CredentialManagement {
|
||
pub subcommand: u8,
|
||
pub subcommand_params: Option<CredentialManagementParams>,
|
||
pub pin_protocol: Option<u8>,
|
||
pub pin_auth: Option<[u8; 32]>,
|
||
}
|
||
|
||
impl CredentialManagement {
|
||
pub fn new(subcommand: u8) -> Self {
|
||
Self {
|
||
subcommand,
|
||
subcommand_params: None,
|
||
pin_protocol: None,
|
||
pin_auth: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<CredentialManagement> for Value {
|
||
fn from(request: CredentialManagement) -> Value {
|
||
let mut map = Map::default();
|
||
map.push(1, request.subcommand);
|
||
if let Some(subcommand_params) = request.subcommand_params {
|
||
map.push(2, subcommand_params);
|
||
}
|
||
if let Some(pin_protocol) = request.pin_protocol {
|
||
map.push(3, pin_protocol);
|
||
}
|
||
if let Some(pin_auth) = request.pin_auth {
|
||
map.push(4, pin_auth.as_slice());
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl Request for CredentialManagement {
|
||
const COMMAND: u8 = 0x0A;
|
||
|
||
type Reply = CredentialManagementReply;
|
||
}
|
||
|
||
#[derive(Clone, Default)]
|
||
pub struct CredentialManagementParams {
|
||
pub rp_id_hash: Option<Vec<u8>>,
|
||
pub credential_id: Option<PubKeyCredDescriptor>,
|
||
pub user: Option<User>,
|
||
}
|
||
|
||
impl CredentialManagementParams {
|
||
pub fn serialized(&self) -> Vec<u8> {
|
||
let mut serialized = Vec::new();
|
||
ciborium::into_writer(&Value::from(self.clone()), &mut serialized).unwrap();
|
||
serialized
|
||
}
|
||
}
|
||
|
||
impl From<CredentialManagementParams> for Value {
|
||
fn from(params: CredentialManagementParams) -> Value {
|
||
let mut map = Map::default();
|
||
if let Some(rp_id_hash) = params.rp_id_hash {
|
||
map.push(1, rp_id_hash);
|
||
}
|
||
if let Some(credential_id) = params.credential_id {
|
||
map.push(2, credential_id);
|
||
}
|
||
if let Some(user) = params.user {
|
||
map.push(3, user);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
pub struct CredentialManagementReply {
|
||
pub existing_resident_credentials_count: Option<usize>,
|
||
pub max_possible_remaining_resident_credentials_count: Option<usize>,
|
||
pub rp: Option<Value>,
|
||
pub rp_id_hash: Option<Value>,
|
||
pub total_rps: Option<usize>,
|
||
pub user: Option<Value>,
|
||
pub credential_id: Option<PubKeyCredDescriptor>,
|
||
pub total_credentials: Option<usize>,
|
||
pub third_party_payment: Option<bool>,
|
||
}
|
||
|
||
impl From<Value> for CredentialManagementReply {
|
||
fn from(value: Value) -> Self {
|
||
let mut map: BTreeMap<u8, Value> = value.deserialized().unwrap();
|
||
Self {
|
||
existing_resident_credentials_count: map
|
||
.remove(&1)
|
||
.map(|value| value.deserialized().unwrap()),
|
||
max_possible_remaining_resident_credentials_count: map
|
||
.remove(&2)
|
||
.map(|value| value.deserialized().unwrap()),
|
||
rp: map.remove(&3),
|
||
rp_id_hash: map.remove(&4),
|
||
total_rps: map.remove(&5).map(|value| value.deserialized().unwrap()),
|
||
user: map.remove(&6),
|
||
credential_id: map.remove(&7).map(|value| value.into()),
|
||
total_credentials: map.remove(&9).map(|value| value.deserialized().unwrap()),
|
||
third_party_payment: map.remove(&0x0c).map(|value| value.deserialized().unwrap()),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// authenticatorConfig (CTAP 2.1 §6.11)
|
||
// ============================================================================
|
||
|
||
/// `authenticatorConfig` (CTAP 2.1 §6.11), command 0x0D.
|
||
pub struct AuthenticatorConfig {
|
||
pub subcommand: u8,
|
||
pub subcommand_params: Option<AuthenticatorConfigParams>,
|
||
pub pin_protocol: Option<u8>,
|
||
pub pin_auth: Option<[u8; 32]>,
|
||
}
|
||
|
||
impl AuthenticatorConfig {
|
||
pub fn new(subcommand: u8) -> Self {
|
||
Self {
|
||
subcommand,
|
||
subcommand_params: None,
|
||
pin_protocol: None,
|
||
pin_auth: None,
|
||
}
|
||
}
|
||
|
||
/// CTAP 2.1 §6.11.4 step 3.a: `pinUvAuthData = 32×0xff || 0x0d ||
|
||
/// uint8(subCommand) || subCommandParams (CBOR)`. The platform HMACs this
|
||
/// exact byte string with the pin-uv-auth token.
|
||
pub fn pin_uv_auth_data(&self) -> Vec<u8> {
|
||
let mut data = vec![0xff; 32];
|
||
data.push(0x0d);
|
||
data.push(self.subcommand);
|
||
if let Some(params) = &self.subcommand_params {
|
||
let mut buf = Vec::new();
|
||
ciborium::into_writer(&Value::from(params.clone()), &mut buf).unwrap();
|
||
data.extend_from_slice(&buf);
|
||
}
|
||
data
|
||
}
|
||
}
|
||
|
||
impl From<AuthenticatorConfig> for Value {
|
||
fn from(request: AuthenticatorConfig) -> Value {
|
||
let mut map = Map::default();
|
||
map.push(1, request.subcommand);
|
||
if let Some(params) = request.subcommand_params {
|
||
map.push(2, params);
|
||
}
|
||
if let Some(pin_protocol) = request.pin_protocol {
|
||
map.push(3, pin_protocol);
|
||
}
|
||
if let Some(pin_auth) = request.pin_auth {
|
||
map.push(4, pin_auth.as_slice());
|
||
}
|
||
map.into()
|
||
}
|
||
}
|
||
|
||
impl Request for AuthenticatorConfig {
|
||
const COMMAND: u8 = 0x0D;
|
||
|
||
type Reply = AuthenticatorConfigReply;
|
||
}
|
||
|
||
/// `authenticatorConfig` response — the spec defines no body, just a status
|
||
/// byte. We keep an empty marker so the `Request` trait is satisfied.
|
||
pub struct AuthenticatorConfigReply;
|
||
|
||
impl From<Value> for AuthenticatorConfigReply {
|
||
fn from(_value: Value) -> Self {
|
||
Self
|
||
}
|
||
}
|
||
|
||
/// `SubcommandParameters` for `authenticatorConfig`. Mirrors
|
||
/// `ctap_types::ctap2::authenticator_config::SubcommandParameters` but with
|
||
/// owned values for ergonomics.
|
||
#[derive(Clone, Default)]
|
||
pub struct AuthenticatorConfigParams {
|
||
pub new_min_pin_length: Option<u8>,
|
||
pub min_pin_length_rp_ids: Option<Vec<String>>,
|
||
pub force_change_pin: Option<bool>,
|
||
}
|
||
|
||
impl From<AuthenticatorConfigParams> for Value {
|
||
fn from(params: AuthenticatorConfigParams) -> Value {
|
||
let mut map = Map::default();
|
||
if let Some(v) = params.new_min_pin_length {
|
||
map.push(1, v);
|
||
}
|
||
if let Some(ids) = params.min_pin_length_rp_ids {
|
||
let values: Vec<Value> = ids.into_iter().map(Value::from).collect();
|
||
map.push(2, Value::Array(values));
|
||
}
|
||
if let Some(force) = params.force_change_pin {
|
||
map.push(3, force);
|
||
}
|
||
map.into()
|
||
}
|
||
}
|