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
19pub 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 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 A::verify(public_key, assertion.effective_msg(), signature)
52 }
53
54 fn check(challenge: impl AsRef<[u8]>, p: &WebauthnAssertion) -> bool {
56 if p.authenticator_data.len() < 37 || !Self::check_flags(p.authenticator_data[32]) {
58 return false;
59 }
60
61 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 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 const fn check_flags(flags: u8) -> bool {
88 if flags & Self::AUTH_DATA_FLAGS_UP != Self::AUTH_DATA_FLAGS_UP {
90 return false;
91 }
92
93 if UV::REQUIRED && (flags & Self::AUTH_DATA_FLAGS_UV != Self::AUTH_DATA_FLAGS_UV) {
97 return false;
98 }
99
100 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#[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 pub authenticator_data: Vec<u8>,
127
128 #[serde(alias = "clientDataJSON")]
130 pub client_data_json: String,
131}
132
133impl WebauthnAssertion {
134 fn effective_msg(&self) -> Vec<u8> {
137 let hash = Sha256::digest(self.client_data_json.as_bytes());
140
141 [self.authenticator_data.as_slice(), hash.as_ref()].concat()
144 }
145}
146
147#[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#[cfg_attr(feature = "schemars-v0_8", derive(::schemars::JsonSchema))]
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166pub enum ClientDataType {
167 #[serde(rename = "webauthn.create")]
169 Create,
170
171 #[serde(rename = "webauthn.get")]
173 Get,
174}
175
176pub trait UserVerification {
187 const REQUIRED: bool;
188}
189
190pub struct IgnoreUserVerification;
192impl UserVerification for IgnoreUserVerification {
193 const REQUIRED: bool = false;
194}
195
196pub struct RequireUserVerification;
198impl UserVerification for RequireUserVerification {
199 const REQUIRED: bool = true;
200}
201
202pub trait Algorithm {
206 type Curve: Curve;
207
208 fn maybe_prehash(msg: impl AsRef<[u8]>) -> impl AsRef<[u8]>;
215
216 #[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 let msg = Self::maybe_prehash(msg);
228
229 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}