saa_custom/passkey/
passkey.rs

1use saa_schema::wasm_serde;
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// Enforce serde for now until figuring how to rename fields with other serialization libraries
12#[derive(
13    Clone, Debug, PartialEq,
14    ::saa_schema::serde::Serialize,
15    ::saa_schema::serde::Deserialize
16)]
17// Manual derivation due to #[deny_unknown_fields] in the macro
18#[cfg_attr(feature = "wasm", 
19    derive(::saa_schema::schemars::JsonSchema), 
20    schemars(crate = "::saa_schema::schemars")
21)]
22#[cfg_attr(feature = "substrate", derive(
23    ::saa_schema::scale::Encode, 
24    ::saa_schema::scale::Decode
25))]
26#[cfg_attr(feature = "solana", derive(
27    ::saa_schema::borsh::BorshSerialize, 
28    ::saa_schema::borsh::BorshDeserialize
29))]
30#[cfg_attr(all(feature = "std", feature="substrate"), derive(
31    saa_schema::scale_info::TypeInfo)
32)]
33pub struct ClientData {
34    #[serde(rename = "type")]
35    pub ty: String,
36    pub challenge: Binary,
37    pub origin: String,
38    #[serde(rename = "crossOrigin")]
39    pub cross_origin: bool
40}
41
42#[cfg_attr(not(feature = "wasm"), derive(
43    ::saa_schema::serde::Serialize,
44    ::saa_schema::serde::Deserialize,
45))]
46#[wasm_serde]
47pub struct PasskeyExtension {
48    #[serde(rename = "type")]
49    pub ty: String,
50    /// Origin of the client where the passkey was created
51    pub origin: String,
52    /// Secpk256r1 Public key used for verification 
53    pub pubkey: Option<Binary>,
54    /// Optional user handle reserved for future use
55    pub user_handle: Option<String>,
56}
57
58
59#[wasm_serde]
60pub struct PasskeyPayload {
61    /// webauthn Authenticator data
62    pub authenticator_data: Binary,
63    /// Passkey client data
64    pub client_data: ClientData,
65    /// Public key is essential for verification but can be supplied on the contract side
66    pub pubkey: Option<Binary>,
67}
68
69
70
71
72#[wasm_serde]
73pub struct PasskeyCredential {
74    /// Passkey id
75    pub id                   :       String,
76    /// Secp256r1 signature
77    pub signature            :       Binary,
78    /// webauthn Authenticator data
79    pub authenticator_data   :       Binary,
80    /// Client data containg challenge, origin and type
81    pub client_data          :       ClientData,
82    /// Optional user handle reserved for future use
83    pub user_handle          :       Option<String>,
84    /// Public key is essential for verification but can be supplied on the contract side
85    /// and omitted by client
86    pub pubkey               :       Option<Binary>,
87}
88
89
90#[cfg(any(feature = "wasm", feature = "native"))]
91impl PasskeyCredential {
92    fn message_digest(&self) -> Result<Vec<u8>, AuthError> {
93        let client_data_hash = sha256(saa_common::to_json_binary(&self.client_data)?.as_slice());
94        let mut hasher = Sha256::new();
95        hasher.update(&self.authenticator_data);
96        hasher.update(&client_data_hash);
97        let hash = hasher.finalize();
98        Ok(hash.to_vec())
99    }
100}
101
102impl Verifiable for PasskeyCredential {
103
104    fn id(&self) -> CredentialId {
105        self.id.as_bytes().to_vec()
106    }
107
108    fn validate(&self) -> Result<(), AuthError> {
109        ensure!(self.authenticator_data.len() >= 37, AuthError::generic("Invalid authenticator data"));
110        ensure!(self.signature.len() > 0, AuthError::generic("Empty signature"));
111        ensure!(self.client_data.challenge.len() > 0, AuthError::generic("Empty challenge"));
112        ensure!(self.client_data.ty == "webauthn.get", AuthError::generic("Invalid client data type"));
113        ensure!(self.pubkey.is_some(), AuthError::generic("Missing public key"));
114        Ok(())
115    }
116
117    #[cfg(feature = "native")]
118    fn verify(&self) -> Result<(), AuthError> {
119        let res = saa_common::crypto::secp256r1_verify(
120            &self.message_digest()?,
121            &self.signature,
122            self.pubkey.as_ref().unwrap()
123        )?;
124        ensure!(res, AuthError::generic("Signature verification failed"));
125        Ok(())
126    }
127
128
129    #[cfg(feature = "wasm")]
130    #[allow(unused_variables)]
131    fn verify_cosmwasm(&self, 
132        api : &dyn saa_common::cosmwasm::Api) -> Result<(), AuthError> {
133        let res = saa_curves::secp256r1::implementation::secp256r1_verify(
134            &self.message_digest()?, 
135            &self.signature, 
136            &self.pubkey.as_ref().unwrap_or(&Binary::default())
137        )?;
138        ensure!(res, AuthError::Signature("Signature verification failed".to_string()));
139        Ok(())
140    }
141
142}
143