diff --git a/Cargo.toml b/Cargo.toml index 849f505f..ca0112ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,8 @@ toml = "0.8" u256-literal = "1" url = "2" wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4.42" +wasm-webauthn = { git = "https://github.com/broody/wasm-webauthn" } webauthn-rs-proto = "0.4" account-sdk = { path = "crates/account_sdk" } tokio = { version = "1", features = ["macros", "time"] } diff --git a/crates/account_sdk/Cargo.toml b/crates/account_sdk/Cargo.toml index f1f2dcf7..618f9afe 100644 --- a/crates/account_sdk/Cargo.toml +++ b/crates/account_sdk/Cargo.toml @@ -28,13 +28,14 @@ thiserror.workspace = true toml.workspace = true u256-literal.workspace = true url.workspace = true +wasm-bindgen-futures.workspace = true wasm-bindgen.workspace = true -webauthn-rs-proto.workspace = true +wasm-webauthn.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-test = "0.3.34" js-sys = "0.3.69" web-sys = "0.3.69" -wasm-bindgen-futures = "0.4.42" diff --git a/crates/account_sdk/src/lib.rs b/crates/account_sdk/src/lib.rs index f6c4514e..d1bbf841 100644 --- a/crates/account_sdk/src/lib.rs +++ b/crates/account_sdk/src/lib.rs @@ -6,12 +6,6 @@ pub mod abigen; pub mod felt_ser; pub mod session_token; +#[cfg(not(target_arch = "wasm32"))] #[cfg(test)] pub mod tests; - -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -extern "C" { - pub fn alert(s: &str); -} diff --git a/crates/account_sdk/src/tests/webauthn/mod.rs b/crates/account_sdk/src/tests/webauthn/mod.rs index caab51fd..6300fe72 100644 --- a/crates/account_sdk/src/tests/webauthn/mod.rs +++ b/crates/account_sdk/src/tests/webauthn/mod.rs @@ -10,7 +10,9 @@ use crate::abigen::account::WebauthnPubKey; use crate::abigen::account::WebauthnSignature; use crate::{ tests::runners::katana_runner::KatanaRunner, - webauthn_signer::{cairo_args::VerifyWebauthnSignerArgs, P256r1Signer}, + webauthn_signer::{ + cairo_args::VerifyWebauthnSignerArgs, signers::p256r1::P256r1Signer, signers::Signer, + }, }; #[tokio::test] @@ -63,7 +65,11 @@ async fn test_verify_webauthn_explicit() { let challenge = felt!("0x0169af1f6f99d35e0b80e0140235ec4a2041048868071a8654576223934726f5"); let challenge_bytes = challenge.to_bytes_be().to_vec(); - let response = data.signer.sign(&challenge_bytes); + let response = data + .signer + .sign(&challenge_bytes) + .await + .expect("signer error"); let args = VerifyWebauthnSignerArgs::from_response(origin, challenge_bytes, response.clone()); @@ -110,3 +116,11 @@ async fn test_verify_webauthn_execute() { .await; result.unwrap(); } + +#[tokio::test] +async fn test_signer() { + let rp_id = "https://localhost:8080".to_string(); + let signer = P256r1Signer::random(rp_id); + let calldata = signer.sign("842903840923".as_bytes()).await.unwrap(); + dbg!(&calldata); +} diff --git a/crates/account_sdk/src/tests/webauthn/utils.rs b/crates/account_sdk/src/tests/webauthn/utils.rs index de6a2180..510df1a8 100644 --- a/crates/account_sdk/src/tests/webauthn/utils.rs +++ b/crates/account_sdk/src/tests/webauthn/utils.rs @@ -18,7 +18,7 @@ use crate::{ }; use crate::{ deploy_contract::single_owner_account, tests::runners::TestnetRunner, - webauthn_signer::P256r1Signer, + webauthn_signer::signers::p256r1::P256r1Signer, }; use super::super::deployment_test::{declare, deploy}; @@ -112,7 +112,7 @@ where } pub async fn webauthn_executor( &self, - ) -> CartridgeAccount>> { + ) -> CartridgeAccount, P256r1Signer>> { CartridgeAccount::new( self.address, WebauthnAccount::new( diff --git a/crates/account_sdk/src/webauthn_signer/account.rs b/crates/account_sdk/src/webauthn_signer/account.rs index 87ea32ab..9ad7762f 100644 --- a/crates/account_sdk/src/webauthn_signer/account.rs +++ b/crates/account_sdk/src/webauthn_signer/account.rs @@ -19,32 +19,28 @@ use std::sync::Arc; use crate::felt_ser::to_felts; -use super::{cairo_args::VerifyWebauthnSignerArgs, P256r1Signer}; +use super::{cairo_args::VerifyWebauthnSignerArgs, signers::device::DeviceError}; use crate::abigen::account::WebauthnSignature; +use crate::webauthn_signer::signers::Signer; -pub struct WebauthnAccount

+pub struct WebauthnAccount where P: Provider + Send, + S: Signer + Send, { provider: P, - // Later the struct will be generic over the signer type - // and will support "external" signers - signer: P256r1Signer, + signer: S, address: FieldElement, chain_id: FieldElement, block_id: BlockId, origin: String, } -impl

WebauthnAccount

+impl WebauthnAccount where P: Provider + Send, + S: Signer + Send, { - pub fn new( - provider: P, - signer: P256r1Signer, - address: FieldElement, - chain_id: FieldElement, - ) -> Self { + pub fn new(provider: P, signer: S, address: FieldElement, chain_id: FieldElement) -> Self { Self { provider, signer, @@ -74,9 +70,10 @@ impl<'a> From<&'a Call> for SerializableCall<'a> { } } -impl

ExecutionEncoder for WebauthnAccount

+impl ExecutionEncoder for WebauthnAccount where P: Provider + Send, + S: Signer + Send, { fn encode_calls(&self, calls: &[Call]) -> Vec { to_felts(&calls.iter().map(SerializableCall::from).collect::>()) @@ -87,13 +84,16 @@ where pub enum SignError { #[error("Signer error: {0}")] Signer(EcdsaSignError), + #[error("Device error: {0}")] + Device(DeviceError), } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl

Account for WebauthnAccount

+impl Account for WebauthnAccount where P: Provider + Send + Sync, + S: Signer + Send + Sync, { type SignError = SignError; @@ -112,7 +112,7 @@ where ) -> Result, Self::SignError> { let tx_hash = execution.transaction_hash(self.chain_id, self.address, query_only, self); let challenge = tx_hash.to_bytes_be().to_vec(); - let assertion = self.signer.sign(&challenge); + let assertion = self.signer.sign(&challenge).await?; let args = VerifyWebauthnSignerArgs::from_response(self.origin.clone(), challenge, assertion); @@ -164,9 +164,10 @@ where } } -impl

ConnectedAccount for WebauthnAccount

+impl ConnectedAccount for WebauthnAccount where P: Provider + Send + Sync, + S: Signer + Send + Sync, { type Provider = P; diff --git a/crates/account_sdk/src/webauthn_signer/mod.rs b/crates/account_sdk/src/webauthn_signer/mod.rs index 6f0c2490..700c76b0 100644 --- a/crates/account_sdk/src/webauthn_signer/mod.rs +++ b/crates/account_sdk/src/webauthn_signer/mod.rs @@ -1,91 +1,12 @@ -use p256::{ - ecdsa::{signature::Signer, Signature, SigningKey, VerifyingKey}, - elliptic_curve::sec1::Coordinates, -}; -use rand_core::OsRng; use starknet::{core::types::FieldElement, macros::felt}; -use crate::webauthn_signer::credential::{AuthenticatorData, CliendData}; - -use self::credential::AuthenticatorAssertionResponse; - pub mod account; pub mod cairo_args; pub mod credential; +pub mod signers; pub type U256 = (FieldElement, FieldElement); pub type Secp256r1Point = (U256, U256); // "Webauthn v1" pub const WEBAUTHN_SIGNATURE_TYPE: FieldElement = felt!("0x576562617574686e207631"); - -#[derive(Debug, Clone)] -pub struct P256r1Signer { - pub signing_key: SigningKey, - rp_id: String, -} - -impl P256r1Signer { - pub fn random(rp_id: String) -> Self { - let signing_key = SigningKey::random(&mut OsRng); - Self::new(signing_key, rp_id) - } - pub fn new(signing_key: SigningKey, rp_id: String) -> Self { - Self { signing_key, rp_id } - } - pub fn public_key_bytes(&self) -> ([u8; 32], [u8; 32]) { - P256VerifyingKeyConverter::new(*self.signing_key.verifying_key()).to_bytes() - } - pub fn sign(&self, challenge: &[u8]) -> AuthenticatorAssertionResponse { - use sha2::{digest::Update, Digest, Sha256}; - - let authenticator_data = AuthenticatorData { - rp_id_hash: [0; 32], - flags: 0b00000101, - sign_count: 0, - }; - let client_data_json = CliendData::new(challenge, self.rp_id.clone()).to_json(); - let client_data_hash = Sha256::new().chain(client_data_json.clone()).finalize(); - - let mut to_sign = Into::>::into(authenticator_data.clone()); - to_sign.append(&mut client_data_hash.to_vec()); - let signature: Signature = self.signing_key.try_sign(&to_sign).unwrap(); - let signature = signature.to_bytes().to_vec(); - - AuthenticatorAssertionResponse { - authenticator_data, - client_data_json, - signature, - user_handle: None, - } - } -} - -pub struct P256VerifyingKeyConverter { - pub verifying_key: VerifyingKey, -} - -impl P256VerifyingKeyConverter { - pub fn new(verifying_key: VerifyingKey) -> Self { - Self { verifying_key } - } - pub fn to_bytes(&self) -> ([u8; 32], [u8; 32]) { - let encoded = &self.verifying_key.to_encoded_point(false); - let (x, y) = match encoded.coordinates() { - Coordinates::Uncompressed { x, y } => (x, y), - _ => panic!("unexpected compression"), - }; - ( - x.as_slice().try_into().unwrap(), - y.as_slice().try_into().unwrap(), - ) - } -} - -#[test] -fn test_signer() { - let rp_id = "https://localhost:8080".to_string(); - let signer = P256r1Signer::random(rp_id); - let calldata = signer.sign("842903840923".as_bytes()); - dbg!(&calldata); -} diff --git a/crates/account_sdk/src/webauthn_signer/signers/device.rs b/crates/account_sdk/src/webauthn_signer/signers/device.rs new file mode 100644 index 00000000..21cdd0db --- /dev/null +++ b/crates/account_sdk/src/webauthn_signer/signers/device.rs @@ -0,0 +1,150 @@ +use async_trait::async_trait; +use futures::channel::oneshot; +use std::result::Result; +use wasm_bindgen_futures::spawn_local; +use wasm_webauthn::*; + +use crate::webauthn_signer::{ + account::SignError, + credential::{AuthenticatorAssertionResponse, AuthenticatorData}, +}; + +use super::Signer; + +#[derive(Debug, thiserror::Error)] +pub enum DeviceError { + #[error("Create credential error: {0}")] + CreateCredential(String), + #[error("Get assertion error: {0}")] + GetAssertion(String), + #[error("Channel error: {0}")] + Channel(String), +} + +#[derive(Debug, Clone)] +pub struct DeviceSigner { + pub rp_id: String, + pub credential_id: Vec, +} + +impl DeviceSigner { + pub fn new(rp_id: String, credential_id: Vec) -> Self { + Self { + rp_id, + credential_id, + } + } + + pub async fn register( + rp_id: String, + user_name: String, + challenge: &[u8], + ) -> Result { + let MakeCredentialResponse { credential } = + Self::create_credential(rp_id.clone(), user_name, challenge).await?; + + Ok(Self { + rp_id, + credential_id: credential.id.0, + }) + } + + async fn create_credential( + rp_id: String, + user_name: String, + challenge: &[u8], + ) -> Result { + let (tx, rx) = oneshot::channel(); + let rp_id = rp_id.to_owned(); + let challenge = challenge.to_vec(); + + spawn_local(async move { + let result = MakeCredentialArgsBuilder::default() + .rp_id(Some(rp_id)) + .challenge(challenge) + .user_name(Some(user_name)) + .uv(UserVerificationRequirement::Required) + .build() + .expect("invalid args") + .make_credential() + .await; + + match result { + Ok(credential) => { + let _ = tx.send(Ok(credential)); + } + Err(e) => { + let _ = tx.send(Err(DeviceError::CreateCredential(e.to_string()))); + } + } + }); + + match rx.await { + Ok(result) => result.map_err(SignError::Device), + Err(_) => Err(SignError::Device(DeviceError::Channel( + "credential receiver dropped".to_string(), + ))), + } + } + + async fn get_assertion(&self, challenge: &[u8]) -> Result { + let (tx, rx) = oneshot::channel(); + let credential_id = self.credential_id.clone(); + let rp_id = self.rp_id.to_owned(); + let challenge = challenge.to_vec(); + + spawn_local(async move { + let credential = Credential::from(CredentialID(credential_id)); + + let result = GetAssertionArgsBuilder::default() + .rp_id(Some(rp_id)) + .credentials(Some(vec![credential])) + .challenge(challenge) + .uv(UserVerificationRequirement::Required) + .build() + .expect("invalid args") + .get_assertion() + .await; + + match result { + Ok(assertion) => { + let _ = tx.send(Ok(assertion)); + } + Err(e) => { + let _ = tx.send(Err(DeviceError::GetAssertion(e.to_string()))); + } + } + }); + + match rx.await { + Ok(result) => result.map_err(SignError::Device), + Err(_) => Err(SignError::Device(DeviceError::Channel( + "assertion receiver dropped".to_string(), + ))), + } + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl Signer for DeviceSigner { + async fn sign(&self, challenge: &[u8]) -> Result { + let GetAssertionResponse { + signature, + client_data_json, + flags, + counter, + } = self.get_assertion(challenge).await?; + + Ok(AuthenticatorAssertionResponse { + authenticator_data: AuthenticatorData { + rp_id_hash: [0; 32], + flags, + sign_count: counter, + }, + client_data_json, + signature, + user_handle: None, + }) + } +} diff --git a/crates/account_sdk/src/webauthn_signer/signers/mod.rs b/crates/account_sdk/src/webauthn_signer/signers/mod.rs new file mode 100644 index 00000000..e87d176f --- /dev/null +++ b/crates/account_sdk/src/webauthn_signer/signers/mod.rs @@ -0,0 +1,12 @@ +use super::account::SignError; +use super::credential::AuthenticatorAssertionResponse; +use async_trait::async_trait; + +pub mod device; +pub mod p256r1; + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +pub trait Signer { + async fn sign(&self, challenge: &[u8]) -> Result; +} diff --git a/crates/account_sdk/src/webauthn_signer/signers/p256r1.rs b/crates/account_sdk/src/webauthn_signer/signers/p256r1.rs new file mode 100644 index 00000000..fa9cfe17 --- /dev/null +++ b/crates/account_sdk/src/webauthn_signer/signers/p256r1.rs @@ -0,0 +1,84 @@ +use crate::webauthn_signer::{ + account::SignError, + credential::{AuthenticatorData, CliendData}, +}; +use async_trait::async_trait; +use p256::{ + ecdsa::{signature::Signer as P256Signer, Signature, SigningKey, VerifyingKey}, + elliptic_curve::sec1::Coordinates, +}; +use rand_core::OsRng; + +use crate::webauthn_signer::credential::AuthenticatorAssertionResponse; + +use super::Signer; + +#[derive(Debug, Clone)] +pub struct P256r1Signer { + pub signing_key: SigningKey, + rp_id: String, +} + +impl P256r1Signer { + pub fn new(rp_id: String, signing_key: SigningKey) -> Self { + Self { rp_id, signing_key } + } + + pub fn random(rp_id: String) -> Self { + let signing_key = SigningKey::random(&mut OsRng); + Self::new(rp_id, signing_key) + } + + pub fn public_key_bytes(&self) -> ([u8; 32], [u8; 32]) { + P256VerifyingKeyConverter::new(*self.signing_key.verifying_key()).to_bytes() + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl Signer for P256r1Signer { + async fn sign(&self, challenge: &[u8]) -> Result { + use sha2::{digest::Update, Digest, Sha256}; + + let authenticator_data = AuthenticatorData { + rp_id_hash: [0; 32], + flags: 0b00000101, + sign_count: 0, + }; + let client_data_json = CliendData::new(challenge, self.rp_id.clone()).to_json(); + let client_data_hash = Sha256::new().chain(client_data_json.clone()).finalize(); + + let mut to_sign = Into::>::into(authenticator_data.clone()); + to_sign.append(&mut client_data_hash.to_vec()); + let signature: Signature = self.signing_key.try_sign(&to_sign).unwrap(); + let signature = signature.to_bytes().to_vec(); + + Ok(AuthenticatorAssertionResponse { + authenticator_data, + client_data_json, + signature, + user_handle: None, + }) + } +} + +pub struct P256VerifyingKeyConverter { + pub verifying_key: VerifyingKey, +} + +impl P256VerifyingKeyConverter { + pub fn new(verifying_key: VerifyingKey) -> Self { + Self { verifying_key } + } + pub fn to_bytes(&self) -> ([u8; 32], [u8; 32]) { + let encoded = &self.verifying_key.to_encoded_point(false); + let (x, y) = match encoded.coordinates() { + Coordinates::Uncompressed { x, y } => (x, y), + _ => panic!("unexpected compression"), + }; + ( + x.as_slice().try_into().unwrap(), + y.as_slice().try_into().unwrap(), + ) + } +} diff --git a/crates/webauthn/tests/src/auth/verify_ecdsa.rs b/crates/webauthn/tests/src/auth/verify_ecdsa.rs index 8301ad0c..9165c3b2 100644 --- a/crates/webauthn/tests/src/auth/verify_ecdsa.rs +++ b/crates/webauthn/tests/src/auth/verify_ecdsa.rs @@ -1,4 +1,4 @@ -use account_sdk::webauthn_signer::P256VerifyingKeyConverter; +use account_sdk::webauthn_signer::signers::p256r1::P256VerifyingKeyConverter; use p256::{ ecdsa::{signature::Signer, Signature, SigningKey}, elliptic_curve::{rand_core::OsRng, SecretKey}, diff --git a/crates/webauthn/tests/src/auth/verify_signature.rs b/crates/webauthn/tests/src/auth/verify_signature.rs index 2645a81c..551d89cc 100644 --- a/crates/webauthn/tests/src/auth/verify_signature.rs +++ b/crates/webauthn/tests/src/auth/verify_signature.rs @@ -1,4 +1,4 @@ -use account_sdk::webauthn_signer::P256VerifyingKeyConverter; +use account_sdk::webauthn_signer::signers::p256r1::P256VerifyingKeyConverter; use p256::{ ecdsa::{signature::Signer, Signature, SigningKey}, elliptic_curve::rand_core::OsRng,