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 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
57/// Extract a cryptographic secret key from a [`CoseKey`].
58// possible candidate for a `passkey-crypto` crate?
59pub 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
90/// Convert a Cose Key to a X.509 SubjectPublicKeyInfo formatted byte array.
91///
92/// This should be used by the client when creating the [Easy Credential Data Accessors][ez]
93///
94/// [ez]: https://w3c.github.io/webauthn/#sctn-public-key-easy
95pub 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
148/// A COSE key pair, containing both the public and private keys.
149pub struct CoseKeyPair {
150    /// The public key.
151    pub public: CoseKey,
152    /// The private key.
153    pub private: CoseKey,
154}
155
156impl CoseKeyPair {
157    /// Create a new COSE key pair from a secret key and algorithm.
158    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        // SAFETY: These unwraps are safe because the public_key above is not compressed (false
163        // parameter) therefore x and y are guarateed to contain values.
164        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;