Skip to main content

defuse_webauthn/
lib.rs

1#[cfg(feature = "ed25519")]
2pub mod ed25519;
3#[cfg(feature = "mock")]
4pub mod mock;
5#[cfg(feature = "p256")]
6pub mod p256;
7
8use core::marker::PhantomData;
9
10use defuse_crypto::Curve;
11use defuse_digest::{Digest, sha2::Sha256};
12use serde::{Deserialize, Serialize};
13use serde_with::{
14    base64::{Base64, UrlSafe},
15    formats::Unpadded,
16    serde_as,
17};
18
19/// [Webauthn](https://w3c.github.io/webauthn/) signing standard generic over
20/// underlying [`Algorithm`]
21pub struct Webauthn<A: Algorithm, UV: UserVerification> {
22    _algorithm: PhantomData<A>,
23    _user_verification: PhantomData<UV>,
24}
25
26impl<A, UV> Webauthn<A, UV>
27where
28    A: Algorithm,
29    UV: UserVerification,
30{
31    /// Check that given assertion corresponds to `msg` and verify the
32    /// signature over it for given public key.
33    ///
34    /// See <https://w3c.github.io/webauthn/#sctn-verifying-assertion>.
35    ///
36    /// Credits to:
37    /// * [ERC-4337 Smart Wallet](https://github.com/passkeys-4337/smart-wallet/blob/f3aa9fd44646fde0316fc810e21cc553a9ed73e0/contracts/src/WebAuthn.sol#L75-L172)
38    /// * [CAP-0051](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md)
39    pub fn verify(
40        public_key: &<A::Curve as Curve>::PublicKey,
41        challenge: impl AsRef<[u8]>,
42        assertion: &WebauthnAssertion,
43        signature: &<A::Curve as Curve>::Signature,
44    ) -> bool {
45        if !Self::check(challenge, assertion) {
46            return false;
47        }
48
49        // 21. Using credentialRecord.publicKey, verify that sig is a valid
50        // signature over the binary concatenation of authData and hash.
51        A::verify(public_key, assertion.effective_msg(), signature)
52    }
53
54    /// Check the assertion and whether is corresponds to given `challenge`.
55    fn check(challenge: impl AsRef<[u8]>, p: &WebauthnAssertion) -> bool {
56        // check authData flags before `clientDataJSON` to save gas
57        if p.authenticator_data.len() < 37 || !Self::check_flags(p.authenticator_data[32]) {
58            return false;
59        }
60
61        // 10. Verify that the value of C.type is the string webauthn.get.
62        let Ok(c) = serde_json::from_str::<CollectedClientData>(&p.client_data_json) else {
63            return false;
64        };
65        if c.typ != ClientDataType::Get {
66            return false;
67        }
68
69        // 11. Verify that the value of C.challenge equals the base64url
70        // encoding of pkOptions.challenge
71        if c.challenge != challenge.as_ref() {
72            return false;
73        }
74
75        true
76    }
77
78    #[allow(clippy::identity_op)]
79    const AUTH_DATA_FLAGS_UP: u8 = 1 << 0;
80    const AUTH_DATA_FLAGS_UV: u8 = 1 << 2;
81    const AUTH_DATA_FLAGS_BE: u8 = 1 << 3;
82    const AUTH_DATA_FLAGS_BS: u8 = 1 << 4;
83
84    /// Check flags in authData.
85    ///
86    /// See <https://w3c.github.io/webauthn/#sctn-verifying-assertion>.
87    const fn check_flags(flags: u8) -> bool {
88        // 16. Verify that the UP bit of the flags in authData is set.
89        if flags & Self::AUTH_DATA_FLAGS_UP != Self::AUTH_DATA_FLAGS_UP {
90            return false;
91        }
92
93        // 17. If user verification was determined to be required, verify that
94        // the UV bit of the flags in authData is set. Otherwise, ignore the
95        // value of the UV flag.
96        if UV::REQUIRED && (flags & Self::AUTH_DATA_FLAGS_UV != Self::AUTH_DATA_FLAGS_UV) {
97            return false;
98        }
99
100        // 18. If the BE bit of the flags in authData is not set, verify that
101        // the BS bit is not set.
102        if (flags & Self::AUTH_DATA_FLAGS_BE != Self::AUTH_DATA_FLAGS_BE)
103            && (flags & Self::AUTH_DATA_FLAGS_BS == Self::AUTH_DATA_FLAGS_BS)
104        {
105            return false;
106        }
107
108        true
109    }
110}
111
112/// Actual payload signed according to [`Webauthn`] standard
113#[serde_as]
114#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))]
115#[cfg_attr(feature = "schemars-v0_8", derive(::schemars::JsonSchema))]
116#[cfg_attr(
117    feature = "borsh",
118    derive(::borsh::BorshSerialize, ::borsh::BorshDeserialize),
119    cfg_attr(feature = "borsh-schema", derive(::borsh::BorshSchema))
120)]
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct WebauthnAssertion {
123    #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
124    #[serde(alias = "authenticatorData")]
125    /// Base64Url-encoded [authenticatorData](https://w3c.github.io/webauthn/#authenticator-data)
126    pub authenticator_data: Vec<u8>,
127
128    /// Serialized [clientDataJSON](https://w3c.github.io/webauthn/#dom-authenticatorresponse-clientdatajson)
129    #[serde(alias = "clientDataJSON")]
130    pub client_data_json: String,
131}
132
133impl WebauthnAssertion {
134    /// Returns effective signable message to be verified by the signature
135    /// algorithm.
136    fn effective_msg(&self) -> Vec<u8> {
137        // 20. Let hash be the result of computing a hash over the cData using
138        // SHA-256
139        let hash = Sha256::digest(self.client_data_json.as_bytes());
140
141        // 21. Using credentialRecord.publicKey, verify that sig is a valid
142        // signature over the binary concatenation of authData and hash.
143        [self.authenticator_data.as_slice(), hash.as_ref()].concat()
144    }
145}
146
147/// [`CollectedClientData`](https://w3c.github.io/webauthn/#dictdef-collectedclientdata)
148#[serde_as]
149#[cfg_attr(feature = "schemars-v0_8", derive(::schemars::JsonSchema))]
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct CollectedClientData {
153    #[serde(rename = "type")]
154    pub typ: ClientDataType,
155
156    #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
157    pub challenge: Vec<u8>,
158
159    pub origin: String,
160}
161
162/// [Type](https://w3c.github.io/webauthn/#dom-collectedclientdata-type) of
163/// [`CollectedClientData`]
164#[cfg_attr(feature = "schemars-v0_8", derive(::schemars::JsonSchema))]
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166pub enum ClientDataType {
167    /// Serializes to the string `"webauthn.create"`
168    #[serde(rename = "webauthn.create")]
169    Create,
170
171    /// Serializes to the string `"webauthn.get"`
172    #[serde(rename = "webauthn.get")]
173    Get,
174}
175
176/// [User verification](https://w3c.github.io/webauthn/#user-verification) mode.
177///
178/// # Compatibility
179///
180/// `UV` (User Verified) flag is only set by FIDO2-capable devices with
181/// PIN / biometric setup.
182///
183/// FIDO U2F (CTAP 1) authenticators (such as old Ledger and Yubikey
184/// devices) only set `UP` (User Present) flag and doesn't support `UV`
185/// (User Verified).
186pub trait UserVerification {
187    const REQUIRED: bool;
188}
189
190/// Ignore [user verification](https://w3c.github.io/webauthn/#user-verification)
191pub struct IgnoreUserVerification;
192impl UserVerification for IgnoreUserVerification {
193    const REQUIRED: bool = false;
194}
195
196/// Require [user verification](https://w3c.github.io/webauthn/#user-verification)
197pub struct RequireUserVerification;
198impl UserVerification for RequireUserVerification {
199    const REQUIRED: bool = true;
200}
201
202/// Signature algorithm.
203///
204/// See <https://www.iana.org/assignments/cose/cose.xhtml#algorithms>
205pub trait Algorithm {
206    type Curve: Curve;
207
208    /// Optionally, prehash the message (or perform any other manipulations)
209    /// before passing it to [`Self::Curve::verify()`](Curve::verify) in the
210    /// last signature verification step:
211    ///
212    /// > 21. Using credentialRecord.publicKey, verify that sig is a valid
213    /// > signature over the binary concatenation of authData and hash.
214    fn maybe_prehash(msg: impl AsRef<[u8]>) -> impl AsRef<[u8]>;
215
216    /// Last algorithm-specific signature verification step:
217    ///
218    /// > 21. Using credentialRecord.publicKey, verify that sig is a valid
219    /// > signature over the binary concatenation of authData and hash.
220    #[inline]
221    fn verify(
222        public_key: &<Self::Curve as Curve>::PublicKey,
223        msg: impl AsRef<[u8]>,
224        signature: &<Self::Curve as Curve>::Signature,
225    ) -> bool {
226        // optionally prehash the message (or perform any other manipulations)
227        let msg = Self::maybe_prehash(msg);
228
229        // verify using `Self::Curve`
230        Self::Curve::verify(public_key, msg.as_ref(), signature)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use hex_literal::hex;
237    use rstest::rstest;
238
239    use super::*;
240
241    #[rstest]
242    #[case(
243        WebauthnAssertion {
244            authenticator_data: hex!("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000").to_vec(),
245            client_data_json: r#"{"type":"webauthn.get","challenge":"BvJpGRQxNyM3oMYGoVgi40m9DV7DF3BPl77xpO1vXh0","origin":"http://localhost:5173","crossOrigin":false}"#.to_string(),
246        },
247        hex!("49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d000000007f105e868e4e52b17998984478130627a6e301002d9928c48c1a95b0b0e2e0da"),
248    )]
249    fn effective_msg(#[case] assertion: WebauthnAssertion, #[case] msg: impl AsRef<[u8]>) {
250        assert_eq!(assertion.effective_msg(), msg.as_ref());
251    }
252}