saa_auth/passkey/
passkey.rs1use saa_schema::saa_type;
2use saa_common::{AuthError, Binary, CredentialId, String, Verifiable, ensure};
3
4#[cfg(any(feature = "wasm", feature = "native"))]
6use {
7 saa_common::hashes::sha256,
8 sha2::{Digest, Sha256}
9};
10
11
12
13
14#[saa_type]
15pub struct PasskeyCredential {
16 pub id : String,
18 pub signature : Binary,
20 pub authenticator_data : Binary,
22 pub client_data : ClientData,
24 pub user_handle : Option<String>,
26 pub pubkey : Option<Binary>,
29}
30
31
32
33
34
35#[saa_type]
36pub struct PasskeyInfo {
37 pub authenticator_data: Binary,
39 pub origin: String,
41 pub pubkey: Binary,
43 #[cfg_attr(feature = "wasm", serde(rename = "crossOrigin"))]
45 pub cross_origin: bool,
46 pub user_handle: Option<String>,
48}
49
50
51
52
53
54#[cfg(feature = "wasm")]
55#[saa_type(no_deny)]
56#[non_exhaustive]
57pub struct ClientData {
58 #[serde(rename = "type")]
59 pub ty: String,
60 pub challenge: String,
61 pub origin: String,
62 #[serde(rename = "crossOrigin")]
63 pub cross_origin: bool,
64 #[serde(flatten, skip_serializing_if = "Option::is_none")]
65 pub other_keys : Option<ClientDataOtherKeys>,
66}
67
68
69#[cfg(not(feature = "wasm"))]
70#[saa_type(no_deny)]
71#[non_exhaustive]
72pub struct ClientData {
73 pub ty: String,
74 pub challenge: String,
75 pub origin: String,
76 pub cross_origin: bool,
77 pub other_keys: Option<ClientDataOtherKeys>,
78}
79
80
81
82#[saa_type]
83pub struct PasskeyPayload {
84 pub other_keys : Option<ClientDataOtherKeys>,
86 pub origin: Option<String>
88}
89
90
91
92
93#[saa_type(no_deny)]
94#[non_exhaustive]
95pub struct ClientDataOtherKeys {
96 pub other_keys_can_be_added_here : Option<String>,
97}
98
99
100impl ClientData {
101 pub fn new(
102 ty: impl ToString,
103 challenge: impl ToString,
104 origin: impl ToString,
105 cross_origin: bool,
106 other_keys: Option<ClientDataOtherKeys>
107 ) -> Self {
108 Self {
109 ty: ty.to_string(),
110 challenge: challenge.to_string(),
111 origin: origin.to_string(),
112 cross_origin,
113 other_keys,
114 }
115 }
116}
117
118
119impl ClientDataOtherKeys {
120 pub fn new(
121 other_keys_can_be_added_here: Option<String>
122 ) -> Self {
123 Self {
124 other_keys_can_be_added_here
125 }
126 }
127}
128
129
130
131
132impl PasskeyCredential {
133
134 pub fn base64_message_bytes(&self) -> Result<Vec<u8>, AuthError> {
135 let base64_str = super::utils::url_to_base64(&self.client_data.challenge);
136 let binary = Binary::from_base64(&base64_str)
137 .map_err(|_| AuthError::PasskeyChallenge)?;
138 Ok(binary.to_vec())
139 }
140
141 #[cfg(any(feature = "wasm", feature = "native"))]
142 fn message_digest(&self) -> Result<Vec<u8>, AuthError> {
143 let client_data_hash = sha256(saa_common::to_json_binary(&self.client_data)?.as_slice());
144 let mut hasher = Sha256::new();
145 hasher.update(&self.authenticator_data);
146 hasher.update(&client_data_hash);
147 let hash = hasher.finalize();
148 Ok(hash.to_vec())
149 }
150}
151
152impl Verifiable for PasskeyCredential {
153
154 fn id(&self) -> CredentialId {
155 self.id.clone()
156 }
157
158 fn validate(&self) -> Result<(), AuthError> {
159 ensure!(self.authenticator_data.len() >= 37, AuthError::generic("Invalid authenticator data"));
160 ensure!(self.signature.len() > 0, AuthError::generic("Empty signature"));
161 ensure!(self.client_data.challenge.len() > 0, AuthError::generic("Empty challenge"));
162 ensure!(self.client_data.ty == "webauthn.get", AuthError::generic("Invalid client data type"));
163 ensure!(self.pubkey.is_some(), AuthError::generic("Missing public key"));
164 self.base64_message_bytes()?;
165 Ok(())
166 }
167
168 #[cfg(feature = "native")]
169 fn verify(&self) -> Result<(), AuthError> {
170 let res = saa_common::crypto::secp256r1_verify(
171 &self.message_digest()?,
172 &self.signature,
173 self.pubkey.as_ref().unwrap()
174 )?;
175 ensure!(res, AuthError::generic("Passkey Signature verification failed"));
176 Ok(())
177 }
178
179
180 #[cfg(feature = "wasm")]
181 fn verify_cosmwasm(
182 &self,
183 #[allow(unused_variables)]
184 api : &dyn saa_common::wasm::Api
185 ) -> Result<(), AuthError> {
186
187 #[cfg(feature = "cosmwasm")]
188 let res = api.secp256r1_verify(
189 &self.message_digest()?,
190 &self.signature,
191 &self.pubkey.as_ref().unwrap()
192 )?;
193
194 #[cfg(not(feature = "cosmwasm"))]
195 let res = saa_curves::secp256r1::implementation::secp256r1_verify(
196 &self.message_digest()?,
197 &self.signature,
198 &self.pubkey.as_ref().unwrap()
199 )?;
200 ensure!(res, AuthError::Signature("Passkey Signature verification failed".to_string()));
201 Ok(())
202 }
203
204}
205