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}