Skip to main content

defuse_webauthn/
lib.rs

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