passkey_authenticator/
lib.rs1mod authenticator;
27mod credential_store;
28mod ctap2;
29mod u2f;
30mod user_validation;
31
32use coset::{
33 iana::{self, Algorithm, EnumI64},
34 CoseKey, CoseKeyBuilder,
35};
36use p256::{
37 ecdsa::SigningKey,
38 elliptic_curve::{generic_array::GenericArray, sec1::FromEncodedPoint},
39 pkcs8::EncodePublicKey,
40 EncodedPoint, PublicKey, SecretKey,
41};
42use passkey_types::{ctap2::Ctap2Error, Bytes};
43
44pub use self::{
45 authenticator::{extensions, Authenticator, CredentialIdLength},
46 credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore, StoreInfo},
47 ctap2::Ctap2Api,
48 u2f::U2fApi,
49 user_validation::{UserCheck, UserValidationMethod},
50};
51
52#[cfg(any(test, feature = "testable"))]
53pub use self::user_validation::MockUserValidationMethod;
54
55fn private_key_from_cose_key(key: &CoseKey) -> Result<SecretKey, Ctap2Error> {
58 if !matches!(
59 key.alg,
60 Some(coset::RegisteredLabelWithPrivate::Assigned(
61 Algorithm::ES256
62 ))
63 ) {
64 return Err(Ctap2Error::UnsupportedAlgorithm);
65 }
66 if !matches!(
67 key.kty,
68 coset::RegisteredLabel::Assigned(iana::KeyType::EC2)
69 ) {
70 return Err(Ctap2Error::InvalidCredential);
71 }
72
73 key.params
74 .iter()
75 .find_map(|(k, v)| {
76 if let coset::Label::Int(i) = k {
77 iana::Ec2KeyParameter::from_i64(*i)
78 .filter(|p| p == &iana::Ec2KeyParameter::D)
79 .and_then(|_| v.as_bytes())
80 .and_then(|b| SecretKey::from_slice(b).ok())
81 } else {
82 None
83 }
84 })
85 .ok_or(Ctap2Error::InvalidCredential)
86}
87
88pub fn public_key_der_from_cose_key(key: &CoseKey) -> Result<Bytes, Ctap2Error> {
94 if !matches!(
95 key.alg,
96 Some(coset::RegisteredLabelWithPrivate::Assigned(
97 Algorithm::ES256
98 ))
99 ) {
100 return Err(Ctap2Error::UnsupportedAlgorithm);
101 }
102 if !matches!(
103 key.kty,
104 coset::RegisteredLabel::Assigned(iana::KeyType::EC2)
105 ) {
106 return Err(Ctap2Error::InvalidCredential);
107 }
108
109 let (mut x, mut y) = (None, None);
110 for (key, value) in &key.params {
111 if let coset::Label::Int(i) = key {
112 let key = iana::Ec2KeyParameter::from_i64(*i).ok_or(Ctap2Error::InvalidCbor)?;
113 match key {
114 iana::Ec2KeyParameter::X => {
115 if value.as_bytes().and_then(|v| x.replace(v)).is_some() {
116 log::warn!("Cose key has multiple entries for X coordinate");
117 }
118 }
119 iana::Ec2KeyParameter::Y => {
120 if value.as_bytes().and_then(|v| y.replace(v)).is_some() {
121 log::warn!("Cose key has multiple entries for Y coordinate");
122 }
123 }
124 _ => (),
125 }
126 }
127 }
128 let (Some(x), Some(y)) = (x, y) else {
129 return Err(Ctap2Error::CborUnexpectedType);
130 };
131
132 let point = EncodedPoint::from_affine_coordinates(
133 GenericArray::from_slice(x.as_slice()),
134 GenericArray::from_slice(y.as_slice()),
135 false,
136 );
137 let Some(pub_key): Option<PublicKey> = PublicKey::from_encoded_point(&point).into() else {
138 return Err(Ctap2Error::InvalidCredential);
139 };
140 pub_key
141 .to_public_key_der()
142 .map_err(|_| Ctap2Error::InvalidCredential)
143 .map(|pk| pk.as_ref().to_vec().into())
144}
145
146pub(crate) struct CoseKeyPair {
147 public: CoseKey,
148 private: CoseKey,
149}
150
151impl CoseKeyPair {
152 fn from_secret_key(private_key: &SecretKey, algorithm: Algorithm) -> Self {
153 let public_key = SigningKey::from(private_key)
154 .verifying_key()
155 .to_encoded_point(false);
156 let x = public_key.x().unwrap().as_slice().to_vec();
159 let y = public_key.y().unwrap().as_slice().to_vec();
160 let private = CoseKeyBuilder::new_ec2_priv_key(
161 iana::EllipticCurve::P_256,
162 x.clone(),
163 y.clone(),
164 private_key.to_bytes().to_vec(),
165 )
166 .algorithm(algorithm)
167 .build();
168 let public = CoseKeyBuilder::new_ec2_pub_key(iana::EllipticCurve::P_256, x, y)
169 .algorithm(algorithm)
170 .build();
171
172 Self { public, private }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use coset::iana;
179 use p256::{
180 ecdsa::{
181 signature::{Signer, Verifier},
182 SigningKey,
183 },
184 SecretKey,
185 };
186 use passkey_types::{ctap2::AuthenticatorData, rand::random_vec};
187
188 use super::{private_key_from_cose_key, CoseKeyPair};
189
190 #[test]
191 fn private_key_cose_round_trip_sanity_check() {
192 let private_key = {
193 let mut rng = rand::thread_rng();
194 SecretKey::random(&mut rng)
195 };
196 let CoseKeyPair {
197 private: private_cose,
198 ..
199 } = CoseKeyPair::from_secret_key(&private_key, iana::Algorithm::ES256);
200 let public_signing_key = SigningKey::from(&private_key);
201 let public_key = public_signing_key.verifying_key();
202
203 let auth_data = AuthenticatorData::new("future.1password.com", None);
204 let mut signature_target = auth_data.to_vec();
205 signature_target.extend(random_vec(32));
206
207 let secret_key = private_key_from_cose_key(&private_cose).expect("to get a private key");
208
209 let private_key = SigningKey::from(secret_key);
210 let signature: p256::ecdsa::Signature = private_key.sign(&signature_target);
211
212 public_key
213 .verify(&signature_target, &signature)
214 .expect("failed to verify signature")
215 }
216}