Files

1278 lines
36 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}
}