passkey_authenticator/
lib.rs1mod authenticator;
27mod credential_store;
28mod ctap2;
29mod passkey;
30mod u2f;
31mod user_validation;
32
33use coset::{
34 CoseKey, CoseKeyBuilder,
35 iana::{self, Algorithm, EnumI64},
36};
37use p256::{
38 EncodedPoint, PublicKey, SecretKey,
39 ecdsa::SigningKey,
40 elliptic_curve::{generic_array::GenericArray, sec1::FromEncodedPoint},
41 pkcs8::EncodePublicKey,
42};
43use passkey_types::{Bytes, ctap2::Ctap2Error};
44
45pub use self::{
46 authenticator::{Authenticator, CredentialIdLength, extensions},
47 credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore, StoreInfo},
48 ctap2::Ctap2Api,
49 passkey::PasskeyAccessor,
50 u2f::U2fApi,
51 user_validation::{UiHint, UserCheck, UserValidationMethod},
52};
53
54#[cfg(any(test, feature = "testable"))]
55pub use self::user_validation::MockUserValidationMethod;
56
57pub fn private_key_from_cose_key(key: &CoseKey) -> Result<SecretKey, Ctap2Error> {
60 if !matches!(
61 key.alg,
62 Some(coset::RegisteredLabelWithPrivate::Assigned(
63 Algorithm::ES256
64 ))
65 ) {
66 return Err(Ctap2Error::UnsupportedAlgorithm);
67 }
68 if !matches!(
69 key.kty,
70 coset::RegisteredLabel::Assigned(iana::KeyType::EC2)
71 ) {
72 return Err(Ctap2Error::InvalidCredential);
73 }
74
75 key.params
76 .iter()
77 .find_map(|(k, v)| {
78 if let coset::Label::Int(i) = k {
79 iana::Ec2KeyParameter::from_i64(*i)
80 .filter(|p| p == &iana::Ec2KeyParameter::D)
81 .and_then(|_| v.as_bytes())
82 .and_then(|b| SecretKey::from_slice(b).ok())
83 } else {
84 None
85 }
86 })
87 .ok_or(Ctap2Error::InvalidCredential)
88}
89
90pub fn public_key_der_from_cose_key(key: &CoseKey) -> Result<Bytes, Ctap2Error> {
96 if !matches!(
97 key.alg,
98 Some(coset::RegisteredLabelWithPrivate::Assigned(
99 Algorithm::ES256
100 ))
101 ) {
102 return Err(Ctap2Error::UnsupportedAlgorithm);
103 }
104 if !matches!(
105 key.kty,
106 coset::RegisteredLabel::Assigned(iana::KeyType::EC2)
107 ) {
108 return Err(Ctap2Error::InvalidCredential);
109 }
110
111 let (mut x, mut y) = (None, None);
112 for (key, value) in &key.params {
113 if let coset::Label::Int(i) = key {
114 let key = iana::Ec2KeyParameter::from_i64(*i).ok_or(Ctap2Error::InvalidCbor)?;
115 match key {
116 iana::Ec2KeyParameter::X => {
117 if value.as_bytes().and_then(|v| x.replace(v)).is_some() {
118 log::warn!("Cose key has multiple entries for X coordinate");
119 }
120 }
121 iana::Ec2KeyParameter::Y => {
122 if value.as_bytes().and_then(|v| y.replace(v)).is_some() {
123 log::warn!("Cose key has multiple entries for Y coordinate");
124 }
125 }
126 _ => (),
127 }
128 }
129 }
130 let (Some(x), Some(y)) = (x, y) else {
131 return Err(Ctap2Error::CborUnexpectedType);
132 };
133
134 let point = EncodedPoint::from_affine_coordinates(
135 GenericArray::from_slice(x.as_slice()),
136 GenericArray::from_slice(y.as_slice()),
137 false,
138 );
139 let Some(pub_key): Option<PublicKey> = PublicKey::from_encoded_point(&point).into() else {
140 return Err(Ctap2Error::InvalidCredential);
141 };
142 pub_key
143 .to_public_key_der()
144 .map_err(|_| Ctap2Error::InvalidCredential)
145 .map(|pk| pk.as_ref().to_vec().into())
146}
147
148pub struct CoseKeyPair {
150 pub public: CoseKey,
152 pub private: CoseKey,
154}
155
156impl CoseKeyPair {
157 pub fn from_secret_key(private_key: &SecretKey, algorithm: Algorithm) -> Self {
159 let public_key = SigningKey::from(private_key)
160 .verifying_key()
161 .to_encoded_point(false);
162 let x = public_key.x().unwrap().as_slice().to_vec();
165 let y = public_key.y().unwrap().as_slice().to_vec();
166 let private = CoseKeyBuilder::new_ec2_priv_key(
167 iana::EllipticCurve::P_256,
168 x.clone(),
169 y.clone(),
170 private_key.to_bytes().to_vec(),
171 )
172 .algorithm(algorithm)
173 .build();
174 let public = CoseKeyBuilder::new_ec2_pub_key(iana::EllipticCurve::P_256, x, y)
175 .algorithm(algorithm)
176 .build();
177
178 Self { public, private }
179 }
180}
181
182#[cfg(test)]
183mod tests;