saa-custom 0.13.4

Custom credentials built on top of curves and their verification logic for smart account auth
Documentation
#[cfg(feature = "cosmwasm")]
use cosmwasm_std::{Api, Env, MessageInfo};

use saa_curves::secp256r1::secp256r1_verify;
use saa_schema::wasm_serde;

use saa_common::{
    ensure, hashes::sha256, AuthError, Binary, CredentialId, CredentialInfo, CredentialName, String, Verifiable
};

use sha2::{Digest, Sha256};


#[derive(
    Clone,
    Debug,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
)]
#[cfg_attr(feature = "cosmwasm", 
    derive(::saa_schema::schemars::JsonSchema ),
    schemars(crate = "::saa_schema::schemars")
)]
#[cfg_attr(feature = "substrate", derive(
    ::saa_schema::scale::Encode, 
    ::saa_schema::scale::Decode
))]
#[cfg_attr(feature = "solana", derive(
    ::saa_schema::borsh::BorshSerialize, 
    ::saa_schema::borsh::BorshDeserialize
))]
#[cfg_attr(all(feature = "std", feature="substrate"), derive(
    saa_schema::scale_info::TypeInfo)
)]
#[allow(clippy::derive_partial_eq_without_eq)]
pub struct ClientData {
    // rename to type
    #[serde(rename = "type")]
    pub ty: String,
    pub challenge: Binary,
    pub origin: String,
    #[serde(rename = "crossOrigin")]
    pub cross_origin: bool
}



#[wasm_serde]
pub struct PasskeyExtension {
    /// webauthn Authenticator data
    pub authenticator_data: Binary,
    /// Client data containg challenge, origin and type
    pub client_data: ClientData,
    /// Optional user handle reserved for future use
    pub user_handle: Option<String>,
}


#[wasm_serde]
pub struct PasskeyCredential {
    /// Passkey id
    pub id                   :       String,
    /// Secp256r1 signature
    pub signature            :       Binary,
    /// webauthn Authenticator data
    pub authenticator_data   :       Binary,
    /// Client data containg challenge, origin and type
    pub client_data          :       ClientData,
    /// Optional user handle reserved for future use
    pub user_handle          :       Option<String>,
    /// Public key is essential for verification but can be supplied on the contract side
    /// and omitted by client
    pub pubkey               :       Option<Binary>,
}


impl Verifiable for PasskeyCredential {

    fn id(&self) -> CredentialId {
        self.id.as_bytes().to_vec()
    }

    fn message(&self) -> Binary {
        self.client_data.challenge.clone()
    }

    fn info(&self) -> CredentialInfo {
        CredentialInfo {
            name: CredentialName::Passkey,
            hrp: None,
            extension: Some(saa_common::to_json_binary(&PasskeyExtension {
                authenticator_data: self.authenticator_data.clone(),
                client_data: self.client_data.clone(),
                user_handle: self.user_handle.clone()
            }).unwrap())
        }
    }


    fn validate(&self) -> Result<(), AuthError> {
        ensure!(self.authenticator_data.len() >= 37, AuthError::generic("Invalid authenticator data"));
        ensure!(self.signature.len() > 0, AuthError::generic("Empty signature"));
        ensure!(self.client_data.challenge.len() > 0, AuthError::generic("Empty challenge"));
        ensure!(self.client_data.ty == "webauthn.get", AuthError::generic("Invalid client data type"));
        ensure!(self.pubkey.is_some(), AuthError::generic("Missing public key"));
        Ok(())
    }
    
    fn message_digest(&self) -> Result<Vec<u8>, AuthError> {
        let client_data_hash = sha256(saa_common::to_json_binary(&self.client_data)?.as_slice());
        let mut hasher = Sha256::new();
        hasher.update(&self.authenticator_data);
        hasher.update(&client_data_hash);
        let hash = hasher.finalize();
        Ok(hash.to_vec())
    }

    #[cfg(feature = "native")]
    fn verify(&self) -> Result<(), AuthError> {
        self.validate()?;
        let res = secp256r1_verify(
            &self.message_digest()?,
            &self.signature,
            self.pubkey.as_ref().unwrap()
        )?;
        ensure!(res, AuthError::generic("Signature verification failed"));
        Ok(())
    }


    #[cfg(feature = "cosmwasm")]
    fn verified_cosmwasm(&self, _: &dyn Api, _: &Env, _: &Option<MessageInfo>) -> Result<Self, AuthError> {
        self.validate()?;
        let res = secp256r1_verify(
            &self.message_digest()?,
            &self.signature,
            self.pubkey.as_ref().unwrap()
        )?;
        ensure!(res, AuthError::generic("Signature verification failed"));
        Ok(self.clone())
    }
}