passkey_authenticator/
lib.rs

1//! # Passkey Authenticator
2//!
3//! [![github]](https://github.com/1Password/passkey-rs/tree/main/passkey-authenticator)
4//! [![version]](https://crates.io/crates/passkey-authenticator)
5//! [![documentation]](https://docs.rs/passkey-authenticator/)
6//!
7//! This crate defines an [`Authenticator`] type along with a basic implementation of the [CTAP 2.0]
8//! specification. The [`Authenticator`] struct is designed in such a way that storage and user
9//! interaction are defined through traits, allowing only the parts that vary between vendors,
10//! but keeping the specification compliant implementation regardless of vendor. This is why the
11//! [`Ctap2Api`] trait is sealed, to prevent external implementations.
12//!
13//! ## Why RustCrypto?
14//!
15//! For targeting WASM, yes there are other cryptographic libraries out there that allow targeting
16//! WASM, but none of them are as easy to compile to wasm than the pure rust implementations of the
17//! [RustCrypto] libraries. Now this does come with limitations, so there are plans to provide a
18//! similar backing trait to "plug-in" the desired cryptography from a vendor. Work is ongoing for this.
19//!
20//! [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--authenticator-informational?logo=github&style=flat
21//! [version]: https://img.shields.io/crates/v/passkey-authenticator?logo=rust&style=flat
22//! [documentation]: https://img.shields.io/docsrs/passkey-authenticator/latest?logo=docs.rs&style=flat
23//! [CTAP 2.0]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html
24//! [RustCrypto]: https://github.com/RustCrypto
25
26mod 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
55/// Extract a cryptographic secret key from a [`CoseKey`].
56// possible candidate for a `passkey-crypto` crate?
57fn 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
88/// Convert a Cose Key to a X.509 SubjectPublicKeyInfo formatted byte array.
89///
90/// This should be used by the client when creating the [Easy Credential Data Accessors][ez]
91///
92/// [ez]: https://w3c.github.io/webauthn/#sctn-public-key-easy
93pub 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        // SAFETY: These unwraps are safe because the public_key above is not compressed (false
157        // parameter) therefore x and y are guarateed to contain values.
158        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}