saa_auth/passkey/
passkey.rs

1use saa_schema::saa_type;
2use saa_common::{AuthError, Binary, CredentialId, String, Verifiable, ensure};
3
4// expand later after adding implementations for other platforms
5#[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    /// Passkey id
17    pub id                   :       String,
18    /// Secp256r1 signature
19    pub signature            :       Binary,
20    /// webauthn Authenticator data
21    pub authenticator_data   :       Binary,
22    /// Client data containg challenge, origin and type
23    pub client_data          :       ClientData,
24    /// Optional user handle reserved for future use
25    pub user_handle          :       Option<String>,
26    /// Public key is essential for verification but can be supplied on the backend / contract side
27    /// and omitted by client. Must be set when going through the verification process.
28    pub pubkey               :       Option<Binary>,
29}
30
31
32
33
34
35#[saa_type]
36pub struct PasskeyInfo {
37    /// webauthn Authenticator data
38    pub authenticator_data: Binary,
39    /// Origin of the client where the passkey was created
40    pub origin: String,
41    /// Secpk256r1 Public key used for verification 
42    pub pubkey: Binary,
43    // Flag to allow cross origin requests
44    #[cfg_attr(feature = "wasm", serde(rename = "crossOrigin"))]
45    pub cross_origin: bool,
46    /// Optional user handle reserved for future use
47    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    /// client data other keys
85    pub other_keys :  Option<ClientDataOtherKeys>,
86    // reserved for future use
87    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