From 167389b40c97876692ade7fd264519c7cb3739ed Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Sun, 5 Oct 2025 22:00:57 +0200 Subject: [PATCH 01/16] add implementation --- parachain/primitives/src/identity.rs | 6 + tee-worker/omni-executor/Cargo.lock | 8 + tee-worker/omni-executor/Cargo.toml | 1 + .../omni-executor/executor-crypto/Cargo.toml | 6 + .../omni-executor/executor-crypto/src/lib.rs | 1 + .../executor-crypto/src/passkey.rs | 396 ++++++++++++++++ .../executor-primitives/src/auth.rs | 2 +- .../omni-executor/executor-storage/.gitignore | 5 + .../omni-executor/executor-storage/src/lib.rs | 6 + .../executor-storage/src/passkey.rs | 211 +++++++++ .../executor-storage/src/passkey_challenge.rs | 439 ++++++++++++++++++ .../tests/passkey_integration.rs | 294 ++++++++++++ .../omni-executor/rpc-server/Cargo.toml | 1 + .../rpc-server/src/detailed_error.rs | 105 +++++ .../rpc-server/src/error_code.rs | 14 + .../rpc-server/src/methods/mod.rs | 2 +- .../src/methods/omni/attach_passkey.rs | 159 +++++++ .../rpc-server/src/methods/omni/common.rs | 14 + .../omni/get_hyperliquid_signature_data.rs | 173 ++++++- .../rpc-server/src/methods/omni/mod.rs | 14 +- .../src/methods/omni/remove_passkey.rs | 99 ++++ .../methods/omni/request_passkey_challenge.rs | 121 +++++ .../rpc-server/src/verify_auth.rs | 209 ++++++++- 23 files changed, 2276 insertions(+), 10 deletions(-) create mode 100644 tee-worker/omni-executor/executor-crypto/src/passkey.rs create mode 100644 tee-worker/omni-executor/executor-storage/.gitignore create mode 100644 tee-worker/omni-executor/executor-storage/src/passkey.rs create mode 100644 tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs create mode 100644 tee-worker/omni-executor/executor-storage/tests/passkey_integration.rs create mode 100644 tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs create mode 100644 tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs create mode 100644 tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs diff --git a/parachain/primitives/src/identity.rs b/parachain/primitives/src/identity.rs index 7cd00ac379..58be9db287 100644 --- a/parachain/primitives/src/identity.rs +++ b/parachain/primitives/src/identity.rs @@ -546,6 +546,8 @@ impl Identity { return Ok(Identity::Google(IdentityString::new(v[1].as_bytes().to_vec()))); } else if v[0] == "pumpx" { return Ok(Identity::Pumpx(IdentityString::new(v[1].as_bytes().to_vec()))); + } else if v[0] == "passkey" { + return Ok(Identity::Passkey(IdentityString::new(v[1].as_bytes().to_vec()))); } else { return Err("Unknown did type"); } @@ -630,6 +632,9 @@ impl Identity { Web2IdentityType::Pumpx => { Identity::Pumpx(IdentityString::new(handle.as_bytes().to_vec())) }, + Web2IdentityType::Passkey => { + Identity::Passkey(IdentityString::new(handle.as_bytes().to_vec())) + }, } } } @@ -642,6 +647,7 @@ pub enum Web2IdentityType { Email, Google, Pumpx, + Passkey, } impl From for Identity { diff --git a/tee-worker/omni-executor/Cargo.lock b/tee-worker/omni-executor/Cargo.lock index d1096d15a5..2f6a50505a 100644 --- a/tee-worker/omni-executor/Cargo.lock +++ b/tee-worker/omni-executor/Cargo.lock @@ -3529,6 +3529,7 @@ dependencies = [ "ff", "generic-array", "group", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -4191,16 +4192,22 @@ dependencies = [ name = "executor-crypto" version = "0.1.0" dependencies = [ + "base64 0.22.1", "chrono", + "ciborium", "ethers", + "hex", "jsonwebtoken 9.3.1", + "p256", "parity-scale-codec", "rand 0.8.5", "ring 0.16.20", "rsa", "secp256k1 0.29.1", "serde", + "serde_json", "serde_with", + "sha2 0.10.9", "sp-core 35.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "tracing", ] @@ -8857,6 +8864,7 @@ dependencies = [ "alloy-signer-local", "async-trait", "base58", + "base64 0.22.1", "binance-api", "chrono", "config-loader", diff --git a/tee-worker/omni-executor/Cargo.toml b/tee-worker/omni-executor/Cargo.toml index 7ee5cadc2f..97583ab4b0 100644 --- a/tee-worker/omni-executor/Cargo.toml +++ b/tee-worker/omni-executor/Cargo.toml @@ -71,6 +71,7 @@ log = "0.4.22" metrics = "0.24.1" metrics-exporter-prometheus = "0.16.2" mockall = "0.13.1" +p256 = { version = "0.13", features = ["ecdsa"] } parity-scale-codec = "3.6.12" rand = "0.8.5" regex = "1.7" diff --git a/tee-worker/omni-executor/executor-crypto/Cargo.toml b/tee-worker/omni-executor/executor-crypto/Cargo.toml index 8cdd2c4a81..b57e2c688c 100644 --- a/tee-worker/omni-executor/executor-crypto/Cargo.toml +++ b/tee-worker/omni-executor/executor-crypto/Cargo.toml @@ -5,15 +5,21 @@ authors = ['Trust Computing GmbH '] edition.workspace = true [dependencies] +base64 = { workspace = true } +ciborium = { workspace = true } ethers = { workspace = true } +hex = { workspace = true } jsonwebtoken = { workspace = true } +p256 = { workspace = true } parity-scale-codec = { workspace = true } rand = { workspace = true } ring = { workspace = true } rsa = { workspace = true, features = ["sha2"] } secp256k1 = { workspace = true, features = ["std", "global-context", "recovery", "rand-std"] } serde = { workspace = true } +serde_json = { workspace = true } serde_with = { workspace = true } +sha2 = { workspace = true } sp-core = { workspace = true } tracing = { workspace = true } diff --git a/tee-worker/omni-executor/executor-crypto/src/lib.rs b/tee-worker/omni-executor/executor-crypto/src/lib.rs index 3e02c6c2bb..3e6dc4ce30 100644 --- a/tee-worker/omni-executor/executor-crypto/src/lib.rs +++ b/tee-worker/omni-executor/executor-crypto/src/lib.rs @@ -1,5 +1,6 @@ pub mod aes256; pub mod jwt; +pub mod passkey; pub mod secp256k1; pub mod shielding_key; pub mod traits; diff --git a/tee-worker/omni-executor/executor-crypto/src/passkey.rs b/tee-worker/omni-executor/executor-crypto/src/passkey.rs new file mode 100644 index 0000000000..b502d2f73b --- /dev/null +++ b/tee-worker/omni-executor/executor-crypto/src/passkey.rs @@ -0,0 +1,396 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use base64::Engine; +use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; +use sha2::{Digest, Sha256}; +use std::fmt; + +#[derive(Debug)] +pub enum PasskeyError { + InvalidPublicKeyFormat, + InvalidSignatureFormat, + ParseError(String), + AttestationParseError(String), + ChallengeVerificationFailed, + OriginVerificationFailed, +} + +impl fmt::Display for PasskeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PasskeyError::InvalidPublicKeyFormat => write!(f, "Invalid public key format"), + PasskeyError::InvalidSignatureFormat => write!(f, "Invalid signature format"), + PasskeyError::ParseError(msg) => write!(f, "Parse error: {}", msg), + PasskeyError::AttestationParseError(msg) => { + write!(f, "Attestation parse error: {}", msg) + }, + PasskeyError::ChallengeVerificationFailed => write!(f, "Challenge verification failed"), + PasskeyError::OriginVerificationFailed => write!(f, "Origin verification failed"), + } + } +} + +impl std::error::Error for PasskeyError {} + +#[derive(Debug, Clone)] +pub struct PasskeyPublicKey { + pub verifying_key: VerifyingKey, +} + +#[derive(Debug, Clone)] +pub struct ParsedAttestationObject { + pub auth_data: Vec, + pub fmt: String, + pub att_stmt: Vec, // Simplified - could be parsed further +} + +#[derive(Debug, Clone)] +pub struct ClientData { + pub type_: String, + pub challenge: String, + pub origin: String, + pub cross_origin: Option, +} + +#[derive(Debug, Clone)] +pub struct AttestationResult { + pub credential_id: String, + pub public_key: PasskeyPublicKey, +} + +pub struct PasskeyVerifier; + +impl PasskeyVerifier { + pub fn verify_client_data_json( + client_data_json: &str, + omni_account: &[u8; 32], + expected_origin: &str, + expected_type: &str, + verify_and_consume_challenge: F, + ) -> Result<(), PasskeyError> + where + F: FnOnce(&str, &[u8; 32]) -> Result<(), PasskeyError>, + { + // Parse client data JSON + let client_data = Self::parse_client_data_json(client_data_json)?; + + // Verify origin + if client_data.origin != expected_origin { + return Err(PasskeyError::OriginVerificationFailed); + } + + // Verify type + if client_data.type_ != expected_type { + return Err(PasskeyError::AttestationParseError(format!( + "Invalid type, expected {}", + expected_type + ))); + } + + // Verify and consume challenge using the provided function + verify_and_consume_challenge(&client_data.challenge, omni_account) + .map_err(|_| PasskeyError::ChallengeVerificationFailed)?; + + Ok(()) + } + + pub fn from_sec1_bytes(sec1_bytes: &[u8]) -> Result { + let verifying_key = VerifyingKey::from_sec1_bytes(sec1_bytes) + .map_err(|_| PasskeyError::InvalidPublicKeyFormat)?; + Ok(PasskeyPublicKey { verifying_key }) + } + + pub fn verify_attestation(attestation_object: &str) -> Result { + let parsed_attestation_object = Self::parse_attestation_object(attestation_object)?; + + let (credential_id, public_key) = + Self::extract_credential_and_key_from_auth_data(&parsed_attestation_object.auth_data)?; + + Ok(AttestationResult { credential_id, public_key }) + } + + pub fn verify_rp_id_hash( + auth_data_bytes: &[u8], + expected_rp_id: &str, + ) -> Result<(), PasskeyError> { + if auth_data_bytes.len() < 32 { + return Err(PasskeyError::ParseError( + "Auth data too short to contain RP ID hash".to_string(), + )); + } + let rp_id_hash_from_auth_data = &auth_data_bytes[0..32]; + let expected_rp_id_hash = Sha256::digest(expected_rp_id.as_bytes()); + if rp_id_hash_from_auth_data != expected_rp_id_hash.as_slice() { + return Err(PasskeyError::ParseError(format!( + "RP ID hash mismatch. Expected RP ID: '{}', hash: {:02x?}, but got: {:02x?}", + expected_rp_id, + expected_rp_id_hash.as_slice(), + rp_id_hash_from_auth_data + ))); + } + + Ok(()) + } + + pub fn parse_client_data_json(client_data_json_b64: &str) -> Result { + // Decode base64 + let client_data_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(client_data_json_b64) + .map_err(|e| { + PasskeyError::AttestationParseError(format!("Base64 decode error: {}", e)) + })?; + + // Parse JSON + let client_data_json = std::str::from_utf8(&client_data_bytes) + .map_err(|e| PasskeyError::AttestationParseError(format!("UTF-8 error: {}", e)))?; + + let json_value: serde_json::Value = serde_json::from_str(client_data_json) + .map_err(|e| PasskeyError::AttestationParseError(format!("JSON parse error: {}", e)))?; + + let client_data = ClientData { + type_: json_value + .get("type") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + PasskeyError::AttestationParseError("Missing type field".to_string()) + })? + .to_string(), + challenge: json_value + .get("challenge") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + PasskeyError::AttestationParseError("Missing challenge field".to_string()) + })? + .to_string(), + origin: json_value + .get("origin") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + PasskeyError::AttestationParseError("Missing origin field".to_string()) + })? + .to_string(), + cross_origin: json_value.get("crossOrigin").and_then(|v| v.as_bool()), + }; + + Ok(client_data) + } + + fn parse_attestation_object( + attestation_object_b64: &str, + ) -> Result { + // Decode base64 + let attestation_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(attestation_object_b64) + .map_err(|e| { + PasskeyError::AttestationParseError(format!("Base64 decode error: {}", e)) + })?; + + // Parse CBOR using ciborium + // WebAuthn attestation object structure: + // { + // "fmt": text string, + // "attStmt": map, + // "authData": bytes + // } + let cbor_value: ciborium::Value = ciborium::de::from_reader(&attestation_bytes[..]) + .map_err(|e| { + PasskeyError::AttestationParseError(format!("CBOR decode error: {}", e)) + })?; + + // Extract the map from CBOR value + let map = match cbor_value { + ciborium::Value::Map(m) => m, + _ => { + return Err(PasskeyError::AttestationParseError( + "Attestation object must be a CBOR map".to_string(), + )) + }, + }; + + // Extract authData (required field) + let auth_data = Self::extract_field_from_cbor_map(&map, "authData")?; + let auth_data_bytes = match auth_data { + ciborium::Value::Bytes(bytes) => bytes.clone(), + _ => { + return Err(PasskeyError::AttestationParseError( + "authData must be bytes".to_string(), + )) + }, + }; + + // Extract fmt (format, required field) + let fmt = Self::extract_field_from_cbor_map(&map, "fmt")?; + let fmt_string = match fmt { + ciborium::Value::Text(text) => text.clone(), + _ => return Err(PasskeyError::AttestationParseError("fmt must be text".to_string())), + }; + + // Extract attStmt (attestation statement, optional but usually present) + let att_stmt = match Self::extract_field_from_cbor_map(&map, "attStmt") { + Ok(val) => { + // Serialize attStmt back to bytes for storage (if needed) + let mut att_stmt_bytes = Vec::new(); + ciborium::ser::into_writer(&val, &mut att_stmt_bytes).map_err(|e| { + PasskeyError::AttestationParseError(format!( + "Failed to serialize attStmt: {}", + e + )) + })?; + att_stmt_bytes + }, + Err(_) => vec![], // attStmt is optional for "none" format + }; + + Ok(ParsedAttestationObject { auth_data: auth_data_bytes, fmt: fmt_string, att_stmt }) + } + + fn extract_field_from_cbor_map<'a>( + map: &'a [(ciborium::Value, ciborium::Value)], + field_name: &str, + ) -> Result<&'a ciborium::Value, PasskeyError> { + for (key, value) in map { + if let ciborium::Value::Text(key_text) = key { + if key_text == field_name { + return Ok(value); + } + } + } + Err(PasskeyError::AttestationParseError(format!( + "Required field '{}' not found in attestation object", + field_name + ))) + } + + fn extract_credential_and_key_from_auth_data( + auth_data: &[u8], + ) -> Result<(String, PasskeyPublicKey), PasskeyError> { + if auth_data.len() < 55 { + return Err(PasskeyError::AttestationParseError( + "Auth data too short to contain credential data".to_string(), + )); + } + + // Check if attested credential data is present (AT flag) + let flags = auth_data[32]; + let at_flag = (flags & 0x40) != 0; + + if !at_flag { + return Err(PasskeyError::AttestationParseError( + "No attested credential data present".to_string(), + )); + } + + // Skip: RP ID hash (32) + flags (1) + counter (4) + AAGUID (16) = 53 bytes + // Then: credential ID length (2 bytes) + credential ID + public key (COSE format) + + let cred_id_len = u16::from_be_bytes([auth_data[53], auth_data[54]]) as usize; + let cred_id_start = 55; + let pub_key_start = 55 + cred_id_len; + + if auth_data.len() < pub_key_start { + return Err(PasskeyError::AttestationParseError( + "Auth data too short for credential ID".to_string(), + )); + } + + // Extract credential ID + let credential_id_bytes = &auth_data[cred_id_start..cred_id_start + cred_id_len]; + let credential_id = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(credential_id_bytes); + + // Extract and parse COSE public key (CBOR-encoded) + let cose_key_bytes = &auth_data[pub_key_start..]; + + // Parse COSE key using ciborium + let cose_key: ciborium::Value = ciborium::de::from_reader(cose_key_bytes).map_err(|e| { + PasskeyError::AttestationParseError(format!("Failed to parse COSE key: {}", e)) + })?; + + // COSE key is a CBOR map with integer keys + // For ES256 (P-256), we need: + // -1: x coordinate (bytes) + // -2: y coordinate (bytes) + // -3: curve (1 = P-256) + let cose_map = match cose_key { + ciborium::Value::Map(m) => m, + _ => { + return Err(PasskeyError::AttestationParseError( + "COSE key must be a map".to_string(), + )) + }, + }; + + let x_coord = Self::extract_cose_key_param(&cose_map, -2)?; + let y_coord = Self::extract_cose_key_param(&cose_map, -3)?; + + // Construct SEC1 uncompressed public key: 0x04 || x || y + let mut sec1_bytes = vec![0x04]; + sec1_bytes.extend_from_slice(&x_coord); + sec1_bytes.extend_from_slice(&y_coord); + + let verifying_key = VerifyingKey::from_sec1_bytes(&sec1_bytes) + .map_err(|_| PasskeyError::InvalidPublicKeyFormat)?; + + Ok((credential_id, PasskeyPublicKey { verifying_key })) + } + + fn extract_cose_key_param( + cose_map: &[(ciborium::Value, ciborium::Value)], + key: i32, + ) -> Result, PasskeyError> { + for (k, v) in cose_map { + if let ciborium::Value::Integer(int_key) = k { + if *int_key == key.into() { + if let ciborium::Value::Bytes(bytes) = v { + return Ok(bytes.clone()); + } + } + } + } + Err(PasskeyError::AttestationParseError(format!( + "COSE key parameter {} not found or invalid", + key + ))) + } + + pub fn verify_passkey_signature_only( + auth_data_hex: &str, + client_data_json: &str, + signature_hex: &str, + public_key: &PasskeyPublicKey, + ) -> Result { + // Decode signature from hex + let signature_bytes = + hex::decode(signature_hex).map_err(|_| PasskeyError::InvalidSignatureFormat)?; + // Validate signature length (64 bytes for P-256: 32 bytes r + 32 bytes s) + if signature_bytes.len() != 64 { + return Err(PasskeyError::InvalidSignatureFormat); + } + let ecdsa_signature = Signature::from_slice(&signature_bytes) + .map_err(|_| PasskeyError::InvalidSignatureFormat)?; + + let auth_data_bytes = hex::decode(auth_data_hex) + .map_err(|e| PasskeyError::ParseError(format!("Auth data decode error: {}", e)))?; + let client_data_hash = Sha256::digest(client_data_json.as_bytes()); + // Per WebAuthn spec: signature = sign(authenticatorData || SHA256(clientDataJSON)) + let mut signing_data = Vec::new(); + signing_data.extend_from_slice(&auth_data_bytes); + signing_data.extend_from_slice(&client_data_hash); + + Ok(public_key.verifying_key.verify(&signing_data, &ecdsa_signature).is_ok()) + } +} diff --git a/tee-worker/omni-executor/executor-primitives/src/auth.rs b/tee-worker/omni-executor/executor-primitives/src/auth.rs index f081cb1ff3..87a572468b 100644 --- a/tee-worker/omni-executor/executor-primitives/src/auth.rs +++ b/tee-worker/omni-executor/executor-primitives/src/auth.rs @@ -181,8 +181,8 @@ pub struct OAuth2Data { #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PasskeyData { pub user_id: String, + pub client_id: String, pub credential_id: String, - pub pubkey: String, // uncompressed, compressed, or COSE-encoded - have to figure out what authenticator sends pub signature: String, // raw 64 byte, or DER encoded - have to figure out what authenticator sends pub auth_data: String, pub client_data_json: String, diff --git a/tee-worker/omni-executor/executor-storage/.gitignore b/tee-worker/omni-executor/executor-storage/.gitignore new file mode 100644 index 0000000000..c229d0dcc1 --- /dev/null +++ b/tee-worker/omni-executor/executor-storage/.gitignore @@ -0,0 +1,5 @@ +# Test database directories created by RocksDB during testing +test_passkey_*/ +test_passkey_integration_*/ +test_passkey_challenge_storage_*/ +test_passkey_storage_*/ diff --git a/tee-worker/omni-executor/executor-storage/src/lib.rs b/tee-worker/omni-executor/executor-storage/src/lib.rs index 301edc340b..595003ff39 100644 --- a/tee-worker/omni-executor/executor-storage/src/lib.rs +++ b/tee-worker/omni-executor/executor-storage/src/lib.rs @@ -12,6 +12,12 @@ pub use heima_jwt::HeimaJwtStorage; mod intent_id; pub use intent_id::IntentIdStorage; mod asset_lock; +mod passkey; +pub use passkey::{PasskeyError, PasskeyRecord, PasskeyStorage}; +mod passkey_challenge; +pub use passkey_challenge::{ + PasskeyChallengeError, PasskeyChallengeRecord, PasskeyChallengeStorage, +}; mod pumpx_account_profile; pub use pumpx_account_profile::PumpxProfileStorage; mod wildmeta_timestamp; diff --git a/tee-worker/omni-executor/executor-storage/src/passkey.rs b/tee-worker/omni-executor/executor-storage/src/passkey.rs new file mode 100644 index 0000000000..cb1f39f768 --- /dev/null +++ b/tee-worker/omni-executor/executor-storage/src/passkey.rs @@ -0,0 +1,211 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use crate::{Storage, StorageDB}; +use executor_crypto::hashing::blake2_256; +use executor_primitives::AccountId; +use parity_scale_codec::{Decode, Encode}; +use std::sync::Arc; + +const STORAGE_NAME: &str = "passkey_storage"; + +/// Passkey data structure +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct PasskeyRecord { + pub omni_account: AccountId, + pub credential_id: String, + pub pubkey: Vec, // Store SEC1 bytes directly + pub created_at: u64, +} + +/// Errors that can occur during passkey operations +#[derive(Debug, PartialEq, Eq)] +pub enum PasskeyError { + StorageError, + DuplicatePasskey, + ValidationError, +} + +/// Simplified passkey storage using composite key of omni_account + credential_id +pub struct PasskeyStorage { + db: Arc, +} + +impl PasskeyStorage { + pub fn new(db: Arc) -> Self { + Self { db } + } + + fn generate_key(omni_account: &AccountId, credential_id: &str) -> [u8; 32] { + let mut data = Vec::new(); + data.extend_from_slice(omni_account.as_ref()); + data.extend_from_slice(credential_id.as_bytes()); + blake2_256(&data) + } + + pub fn get_passkey( + &self, + omni_account: &AccountId, + credential_id: &str, + ) -> Result, ()> { + let key = Self::generate_key(omni_account, credential_id); + self.get(&key) + } + + pub fn remove_passkey( + &self, + omni_account: &AccountId, + credential_id: &str, + ) -> Result<(), PasskeyError> { + let key = Self::generate_key(omni_account, credential_id); + self.remove(&key).map_err(|_| PasskeyError::StorageError) + } + + pub fn exists_passkey(&self, omni_account: &AccountId, credential_id: &str) -> bool { + let key = Self::generate_key(omni_account, credential_id); + self.contains_key(&key) + } + + pub fn add_passkey( + &self, + omni_account: &AccountId, + credential_id: &str, + pubkey: &[u8], // Accept SEC1 bytes directly + ) -> Result<(), PasskeyError> { + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let record = PasskeyRecord { + omni_account: omni_account.clone(), + credential_id: credential_id.to_string(), + pubkey: pubkey.to_vec(), + created_at: current_time, + }; + + let key = Self::generate_key(&record.omni_account, &record.credential_id); + if self.contains_key(&key) { + return Err(PasskeyError::DuplicatePasskey); + } + self.insert(&key, record).map_err(|_| PasskeyError::StorageError) + } +} + +impl Storage<[u8; 32], PasskeyRecord> for PasskeyStorage { + fn db(&self) -> Arc { + self.db.clone() + } + + fn name(&self) -> &'static str { + STORAGE_NAME + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + + fn create_test_storage() -> PasskeyStorage { + use std::sync::atomic::{AtomicUsize, Ordering}; + static COUNTER: AtomicUsize = AtomicUsize::new(0); + let db_path = format!("test_passkey_storage_{}", COUNTER.fetch_add(1, Ordering::SeqCst)); + if Path::new(&db_path).exists() { + fs::remove_dir_all(&db_path).unwrap(); + } + let db = Arc::new(StorageDB::open_default(&db_path).unwrap()); + PasskeyStorage::new(db) + } + + #[test] + fn test_passkey_add_and_get() { + let storage = create_test_storage(); + let omni_account = AccountId::from([1u8; 32]); + + // Add a passkey + let test_pubkey = b"test_pubkey_123"; + storage.add_passkey(&omni_account, "cred123", test_pubkey).unwrap(); + + // Test retrieval + let retrieved = storage.get_passkey(&omni_account, "cred123").unwrap().unwrap(); + assert_eq!(retrieved.omni_account, omni_account); + assert_eq!(retrieved.credential_id, "cred123"); + assert_eq!(retrieved.pubkey, test_pubkey.to_vec()); + + // Test existence check + assert!(storage.exists_passkey(&omni_account, "cred123")); + assert!(!storage.exists_passkey(&omni_account, "nonexistent")); + } + + #[test] + fn test_passkey_removal() { + let storage = create_test_storage(); + let omni_account = AccountId::from([2u8; 32]); + + // Add a passkey + let test_pubkey = b"test_pubkey_456"; + storage.add_passkey(&omni_account, "cred456", test_pubkey).unwrap(); + + // Verify it exists + assert!(storage.exists_passkey(&omni_account, "cred456")); + + // Remove it + storage.remove_passkey(&omni_account, "cred456").unwrap(); + + // Verify it's gone + assert!(!storage.exists_passkey(&omni_account, "cred456")); + assert!(storage.get_passkey(&omni_account, "cred456").unwrap().is_none()); + } + + #[test] + fn test_duplicate_detection() { + let storage = create_test_storage(); + let omni_account = AccountId::from([3u8; 32]); + + // Add first passkey + let test_pubkey1 = b"test_pubkey_789"; + storage.add_passkey(&omni_account, "cred789", test_pubkey1).unwrap(); + + // Try to add with same omni_account + credential_id + let test_pubkey2 = b"test_pubkey_789_new"; + let result = storage.add_passkey(&omni_account, "cred789", test_pubkey2); + assert_eq!(result, Err(PasskeyError::DuplicatePasskey)); + } + + #[test] + fn test_multiple_passkeys_per_omni_account() { + let storage = create_test_storage(); + let omni_account = AccountId::from([4u8; 32]); + + // Add multiple passkeys for the same omni account + let test_pubkey1 = b"test_pubkey_1"; + let test_pubkey2 = b"test_pubkey_2"; + storage.add_passkey(&omni_account, "cred1", test_pubkey1).unwrap(); + storage.add_passkey(&omni_account, "cred2", test_pubkey2).unwrap(); + + // Both should exist independently + assert!(storage.exists_passkey(&omni_account, "cred1")); + assert!(storage.exists_passkey(&omni_account, "cred2")); + + let record1 = storage.get_passkey(&omni_account, "cred1").unwrap().unwrap(); + let record2 = storage.get_passkey(&omni_account, "cred2").unwrap().unwrap(); + + assert_eq!(record1.pubkey, test_pubkey1.to_vec()); + assert_eq!(record2.pubkey, test_pubkey2.to_vec()); + } +} diff --git a/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs b/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs new file mode 100644 index 0000000000..201c217ee7 --- /dev/null +++ b/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs @@ -0,0 +1,439 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use crate::{Storage, StorageDB}; +use executor_primitives::AccountId; +use parity_scale_codec::{Decode, Encode}; +use std::sync::Arc; + +const STORAGE_NAME: &str = "passkey_challenge_storage"; + +/// Passkey challenge record with expiration +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct PasskeyChallengeRecord { + pub challenge: String, + pub omni_account: AccountId, + pub created_at: u64, + pub expires_at: u64, +} + +/// Errors that can occur during challenge operations +#[derive(Debug, PartialEq, Eq)] +pub enum PasskeyChallengeError { + StorageError, + ChallengeExpired, + ChallengeNotFound, + InvalidChallenge, +} + +/// PassKey challenge storage with expiration management +pub struct PasskeyChallengeStorage { + db: Arc, +} + +impl PasskeyChallengeStorage { + pub fn new(db: Arc) -> Self { + Self { db } + } + + pub fn store_challenge( + &self, + omni_account: &AccountId, + challenge: &str, + timeout_seconds: u64, + ) -> Result<(), PasskeyChallengeError> { + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let record = PasskeyChallengeRecord { + challenge: challenge.to_string(), + omni_account: omni_account.clone(), + created_at: current_time, + expires_at: current_time + timeout_seconds, + }; + + let challenge_key = challenge.as_bytes(); + self.insert(&challenge_key, record) + .map_err(|_| PasskeyChallengeError::StorageError) + } + + pub fn verify_and_consume_challenge( + &self, + challenge: &str, + omni_account: &AccountId, + ) -> Result<(), PasskeyChallengeError> { + let challenge_key = challenge.as_bytes(); + + let record = self + .get(&challenge_key) + .map_err(|_| PasskeyChallengeError::StorageError)? + .ok_or(PasskeyChallengeError::ChallengeNotFound)?; + + if record.omni_account != *omni_account { + return Err(PasskeyChallengeError::InvalidChallenge); + } + + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if current_time > record.expires_at { + self.remove(&challenge_key).map_err(|_| PasskeyChallengeError::StorageError)?; + return Err(PasskeyChallengeError::ChallengeExpired); + } + + self.remove(&challenge_key).map_err(|_| PasskeyChallengeError::StorageError)?; + self.cleanup_expired24h_challenges()?; + + Ok(()) + } + + /// Clean up challenges that expired more than 24 hours ago + /// + /// This method only removes challenges that expired at least 24 hours ago. + /// This grace period ensures users get accurate error messages (ChallengeExpired + /// vs ChallengeNotFound) for recently expired challenges. + /// + /// It's recommended to call this periodically (e.g., every hour) via a background job. + pub fn cleanup_expired24h_challenges(&self) -> Result { + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + // Only cleanup challenges that expired 24 hours ago or more + const CLEANUP_GRACE_PERIOD_SECONDS: u64 = 24 * 60 * 60; // 24 hours + let cleanup_threshold = current_time.saturating_sub(CLEANUP_GRACE_PERIOD_SECONDS); + + let mut cleaned_count = 0u32; + let mut expired_keys = Vec::new(); + + // Calculate the prefix for our storage namespace + // storage_key = twox_128(storage_name) + blake2_128_concat(key) + // For prefix iteration, we only need the storage name hash + use executor_crypto::hashing::twox_128; + let storage_prefix = twox_128(STORAGE_NAME.as_bytes()); + + let db = self.db(); + let iter = db.prefix_iterator(storage_prefix); + + // Iterate through all records in our namespace + for item in iter { + match item { + Ok((key, value)) => { + // Verify the key belongs to our storage (starts with our prefix) + if !key.starts_with(&storage_prefix) { + // We've moved past our namespace, stop iteration + break; + } + + // Decode the challenge record + match PasskeyChallengeRecord::decode(&mut &value[..]) { + Ok(record) => { + // Only cleanup if expired AND past the 24-hour grace period + if record.expires_at < cleanup_threshold { + // Mark for deletion (we can't delete while iterating) + expired_keys.push(key.to_vec()); + cleaned_count += 1; + } + }, + Err(e) => { + // Log decode error but continue cleanup + tracing::warn!( + "Failed to decode challenge record during cleanup: {:?}", + e + ); + // Optionally delete corrupted records + expired_keys.push(key.to_vec()); + }, + } + }, + Err(e) => { + tracing::error!("Error iterating through challenges during cleanup: {:?}", e); + return Err(PasskeyChallengeError::StorageError); + }, + } + } + + // Delete all expired challenges + for key in expired_keys { + if let Err(e) = db.delete(&key) { + tracing::error!("Failed to delete expired challenge: {:?}", e); + // Continue cleanup even if some deletions fail + } + } + + if cleaned_count > 0 { + tracing::info!("Cleaned up {} passkey challenges that expired >24h ago", cleaned_count); + } + + Ok(cleaned_count) + } + + /// Check if a challenge exists and is valid (without consuming it) + pub fn is_challenge_valid( + &self, + challenge: &str, + omni_account: &AccountId, + ) -> Result { + let challenge_key = challenge.as_bytes(); + + let record = self.get(&challenge_key).map_err(|_| PasskeyChallengeError::StorageError)?; + + match record { + Some(record) => { + // Check if challenge matches the account + if record.omni_account != *omni_account { + return Ok(false); + } + + // Check expiration + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + Ok(current_time <= record.expires_at) + }, + None => Ok(false), + } + } +} + +impl Storage<&[u8], PasskeyChallengeRecord> for PasskeyChallengeStorage { + fn db(&self) -> Arc { + self.db.clone() + } + + fn name(&self) -> &'static str { + STORAGE_NAME + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + + fn create_test_storage() -> PasskeyChallengeStorage { + use std::sync::atomic::{AtomicUsize, Ordering}; + static COUNTER: AtomicUsize = AtomicUsize::new(0); + let db_path = + format!("test_passkey_challenge_storage_{}", COUNTER.fetch_add(1, Ordering::SeqCst)); + if Path::new(&db_path).exists() { + fs::remove_dir_all(&db_path).unwrap(); + } + let db = Arc::new(StorageDB::open_default(&db_path).unwrap()); + PasskeyChallengeStorage::new(db) + } + + #[test] + fn test_store_and_verify_challenge() { + let storage = create_test_storage(); + let omni_account = AccountId::from([1u8; 32]); + let challenge = "test_challenge_123"; + + // Store challenge with 60 second timeout + storage.store_challenge(&omni_account, challenge, 60).unwrap(); + + // Verify challenge is valid + assert!(storage.is_challenge_valid(challenge, &omni_account).unwrap()); + + // Verify and consume challenge + storage.verify_and_consume_challenge(challenge, &omni_account).unwrap(); + + // Challenge should be consumed (no longer valid) + assert!(!storage.is_challenge_valid(challenge, &omni_account).unwrap()); + } + + #[test] + fn test_challenge_expiration() { + let storage = create_test_storage(); + let omni_account = AccountId::from([2u8; 32]); + let challenge = "expired_challenge"; + + // Store challenge with 0 second timeout (immediately expired) + storage.store_challenge(&omni_account, challenge, 0).unwrap(); + + // Wait at least 1 second to ensure expiration (time precision is in seconds) + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Should return ChallengeExpired (not ChallengeNotFound) because cleanup + // only removes challenges expired >24h ago + let result = storage.verify_and_consume_challenge(challenge, &omni_account); + assert_eq!(result, Err(PasskeyChallengeError::ChallengeExpired)); + } + + #[test] + fn test_invalid_account() { + let storage = create_test_storage(); + let omni_account1 = AccountId::from([3u8; 32]); + let omni_account2 = AccountId::from([4u8; 32]); + let challenge = "account_mismatch_challenge"; + + // Store challenge for account1 + storage.store_challenge(&omni_account1, challenge, 60).unwrap(); + + // Try to verify with account2 (should fail) + let result = storage.verify_and_consume_challenge(challenge, &omni_account2); + assert_eq!(result, Err(PasskeyChallengeError::InvalidChallenge)); + + // Original challenge should still exist for account1 + assert!(storage.is_challenge_valid(challenge, &omni_account1).unwrap()); + } + + #[test] + fn test_challenge_not_found() { + let storage = create_test_storage(); + let omni_account = AccountId::from([5u8; 32]); + + // Try to verify non-existent challenge + let result = storage.verify_and_consume_challenge("nonexistent", &omni_account); + assert_eq!(result, Err(PasskeyChallengeError::ChallengeNotFound)); + } + + #[test] + fn test_cleanup_no_expired_challenges() { + let storage = create_test_storage(); + let omni_account = AccountId::from([6u8; 32]); + + // Store some valid challenges (not expired) + storage.store_challenge(&omni_account, "challenge1", 60).unwrap(); + storage.store_challenge(&omni_account, "challenge2", 60).unwrap(); + storage.store_challenge(&omni_account, "challenge3", 60).unwrap(); + + // Run cleanup - should clean 0 challenges (none expired >24h) + let cleaned = storage.cleanup_expired24h_challenges().unwrap(); + assert_eq!(cleaned, 0); + + // All challenges should still be valid + assert!(storage.is_challenge_valid("challenge1", &omni_account).unwrap()); + assert!(storage.is_challenge_valid("challenge2", &omni_account).unwrap()); + assert!(storage.is_challenge_valid("challenge3", &omni_account).unwrap()); + } + + #[test] + fn test_cleanup_all_expired_challenges() { + let storage = create_test_storage(); + let omni_account = AccountId::from([7u8; 32]); + + // Store challenges with 0 second timeout (immediately expired) + storage.store_challenge(&omni_account, "expired1", 0).unwrap(); + storage.store_challenge(&omni_account, "expired2", 0).unwrap(); + storage.store_challenge(&omni_account, "expired3", 0).unwrap(); + + // Wait at least 1 second to ensure expiration + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Run cleanup - should clean 0 (challenges expired <24h ago, still in grace period) + let cleaned = storage.cleanup_expired24h_challenges().unwrap(); + assert_eq!(cleaned, 0); + + // Challenges are expired but still exist (within 24h grace period) + // They will return ChallengeExpired when verified, not ChallengeNotFound + let result1 = storage.verify_and_consume_challenge("expired1", &omni_account); + assert_eq!(result1, Err(PasskeyChallengeError::ChallengeExpired)); + } + + #[test] + fn test_cleanup_mixed_expired_and_valid() { + let storage = create_test_storage(); + let omni_account = AccountId::from([8u8; 32]); + + // Store mix of expired and valid challenges + storage.store_challenge(&omni_account, "expired1", 0).unwrap(); + storage.store_challenge(&omni_account, "valid1", 60).unwrap(); + storage.store_challenge(&omni_account, "expired2", 0).unwrap(); + storage.store_challenge(&omni_account, "valid2", 60).unwrap(); + + // Wait at least 1 second to ensure expiration + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Run cleanup - should clean 0 (expired challenges still in 24h grace period) + let cleaned = storage.cleanup_expired24h_challenges().unwrap(); + assert_eq!(cleaned, 0); + + // Valid challenges should still exist + assert!(storage.is_challenge_valid("valid1", &omni_account).unwrap()); + assert!(storage.is_challenge_valid("valid2", &omni_account).unwrap()); + + // Expired challenges still exist (in grace period) but return ChallengeExpired + let result1 = storage.verify_and_consume_challenge("expired1", &omni_account); + assert_eq!(result1, Err(PasskeyChallengeError::ChallengeExpired)); + } + + #[test] + fn test_cleanup_multiple_accounts() { + let storage = create_test_storage(); + let account1 = AccountId::from([9u8; 32]); + let account2 = AccountId::from([10u8; 32]); + + // Store expired challenges for different accounts + storage.store_challenge(&account1, "account1_expired", 0).unwrap(); + storage.store_challenge(&account2, "account2_expired", 0).unwrap(); + storage.store_challenge(&account1, "account1_valid", 60).unwrap(); + + // Wait at least 1 second to ensure expiration + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Cleanup should clean 0 (expired challenges still in 24h grace period) + let cleaned = storage.cleanup_expired24h_challenges().unwrap(); + assert_eq!(cleaned, 0); + + // Valid challenge should still exist + assert!(storage.is_challenge_valid("account1_valid", &account1).unwrap()); + + // Expired challenges return ChallengeExpired (not removed yet) + let result1 = storage.verify_and_consume_challenge("account1_expired", &account1); + assert_eq!(result1, Err(PasskeyChallengeError::ChallengeExpired)); + } + + #[test] + fn test_cleanup_idempotent() { + let storage = create_test_storage(); + let omni_account = AccountId::from([11u8; 32]); + + // Store expired challenge + storage.store_challenge(&omni_account, "expired", 0).unwrap(); + // Wait at least 1 second to ensure expiration + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Cleanup won't remove challenges expired <24h ago + let cleaned1 = storage.cleanup_expired24h_challenges().unwrap(); + assert_eq!(cleaned1, 0); + + // Second cleanup should also find nothing + let cleaned2 = storage.cleanup_expired24h_challenges().unwrap(); + assert_eq!(cleaned2, 0); + + // Idempotent - still nothing + let cleaned3 = storage.cleanup_expired24h_challenges().unwrap(); + assert_eq!(cleaned3, 0); + } + + #[test] + fn test_cleanup_empty_storage() { + let storage = create_test_storage(); + + // Cleanup on empty storage should work fine + let cleaned = storage.cleanup_expired24h_challenges().unwrap(); + assert_eq!(cleaned, 0); + } +} diff --git a/tee-worker/omni-executor/executor-storage/tests/passkey_integration.rs b/tee-worker/omni-executor/executor-storage/tests/passkey_integration.rs new file mode 100644 index 0000000000..1818c6d3d5 --- /dev/null +++ b/tee-worker/omni-executor/executor-storage/tests/passkey_integration.rs @@ -0,0 +1,294 @@ +//! Integration tests for complete passkey flows +//! +//! These tests verify the end-to-end functionality of passkey operations +//! including registration, authentication, challenge management, and cleanup. + +use executor_primitives::AccountId; +use executor_storage::{ + PasskeyChallengeError, PasskeyChallengeStorage, PasskeyError, PasskeyStorage, StorageDB, +}; +use std::sync::Arc; + +/// Helper to create a test storage database +fn create_test_storage() -> Arc { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::SystemTime; + static COUNTER: AtomicUsize = AtomicUsize::new(0); + // Use timestamp + counter for unique database paths + let timestamp = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_micros(); + let count = COUNTER.fetch_add(1, Ordering::SeqCst); + let db_path = format!("test_passkey_integration_{}_{}", timestamp, count); + Arc::new(StorageDB::open_default(&db_path).unwrap()) +} + +/// Helper to create a test omni account +fn create_test_account(id: u8) -> AccountId { + AccountId::from([id; 32]) +} + +#[test] +fn test_complete_passkey_registration_flow() { + // This test simulates the complete passkey registration workflow: + // 1. Request challenge + // 2. Store passkey + // 3. Verify passkey is stored correctly + + let storage_db = create_test_storage(); + let challenge_storage = PasskeyChallengeStorage::new(storage_db.clone()); + let passkey_storage = PasskeyStorage::new(storage_db.clone()); + + let omni_account = create_test_account(1); + + // Step 1: Generate and store challenge (simulates omni_requestPasskeyChallenge) + let challenge = "test_challenge_reg"; + let timeout = 300; // 5 minutes + challenge_storage + .store_challenge(&omni_account, challenge, timeout) + .expect("Failed to store challenge"); + + // Step 2: Store the passkey (simulates omni_attachPasskey after attestation verification) + let credential_id = "test_credential_123"; + let pubkey = b"test_pubkey_bytes_for_registration"; + + passkey_storage + .add_passkey(&omni_account, credential_id, pubkey) + .expect("Failed to add passkey"); + + // Step 3: Verify the passkey was stored correctly + let stored_passkey = passkey_storage + .get_passkey(&omni_account, credential_id) + .expect("Failed to get passkey") + .expect("Passkey not found"); + + assert_eq!(stored_passkey.credential_id, credential_id); + assert_eq!(stored_passkey.pubkey, pubkey.to_vec()); +} + +#[test] +fn test_complete_passkey_authentication_flow() { + // This test simulates the complete passkey authentication workflow: + // 1. Register a passkey + // 2. Request challenge for authentication + // 3. Verify and consume challenge + + let storage_db = create_test_storage(); + let challenge_storage = PasskeyChallengeStorage::new(storage_db.clone()); + let passkey_storage = PasskeyStorage::new(storage_db.clone()); + + let omni_account = create_test_account(2); + + // Step 1: Register a passkey + let credential_id = "test_credential_auth"; + let pubkey = b"test_pubkey_bytes_for_auth"; + + passkey_storage + .add_passkey(&omni_account, credential_id, pubkey) + .expect("Failed to add passkey"); + + // Step 2: Generate authentication challenge + let challenge = "test_challenge_auth"; + challenge_storage + .store_challenge(&omni_account, challenge, 300) + .expect("Failed to store challenge"); + + // Step 3: Verify and consume challenge (simulates authentication) + challenge_storage + .verify_and_consume_challenge(challenge, &omni_account) + .expect("Failed to verify challenge"); + + // Verify challenge was consumed (should fail on second attempt) + let result = challenge_storage.verify_and_consume_challenge(challenge, &omni_account); + assert!(matches!(result, Err(PasskeyChallengeError::ChallengeNotFound))); +} + +#[test] +fn test_passkey_challenge_expired() { + // This test verifies that expired challenges return ChallengeExpired error + + let storage_db = create_test_storage(); + let challenge_storage = PasskeyChallengeStorage::new(storage_db.clone()); + + let omni_account = create_test_account(5); + + // Create challenge with immediate expiration + challenge_storage + .store_challenge(&omni_account, "expired", 0) + .expect("Failed to store challenge"); + + // Wait for expiration + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Verify we get ChallengeExpired error (not ChallengeNotFound) + let result = challenge_storage.verify_and_consume_challenge("expired", &omni_account); + assert!(matches!(result, Err(PasskeyChallengeError::ChallengeExpired))); +} + +#[test] +fn test_multiple_passkeys_per_account() { + // This test verifies handling of multiple passkeys for the same account + + let storage_db = create_test_storage(); + let passkey_storage = PasskeyStorage::new(storage_db.clone()); + + let omni_account = create_test_account(6); + + // Register multiple passkeys (e.g., phone and laptop) + let phone_credential = "phone_credential"; + let phone_pubkey = b"phone_pubkey_bytes"; + + let laptop_credential = "laptop_credential"; + let laptop_pubkey = b"laptop_pubkey_bytes"; + + passkey_storage + .add_passkey(&omni_account, phone_credential, phone_pubkey) + .expect("Failed to add phone passkey"); + + passkey_storage + .add_passkey(&omni_account, laptop_credential, laptop_pubkey) + .expect("Failed to add laptop passkey"); + + // Verify both passkeys exist independently + let phone = passkey_storage + .get_passkey(&omni_account, phone_credential) + .expect("Failed to get phone passkey") + .expect("Phone passkey not found"); + + let laptop = passkey_storage + .get_passkey(&omni_account, laptop_credential) + .expect("Failed to get laptop passkey") + .expect("Laptop passkey not found"); + + assert_eq!(phone.pubkey, phone_pubkey.to_vec()); + assert_eq!(laptop.pubkey, laptop_pubkey.to_vec()); +} + +#[test] +fn test_passkey_removal_flow() { + // This test verifies the complete passkey removal workflow + + let storage_db = create_test_storage(); + let passkey_storage = PasskeyStorage::new(storage_db.clone()); + + let omni_account = create_test_account(7); + + // Register a passkey + let credential_id = "test_credential_remove"; + let pubkey = b"test_pubkey_remove"; + + passkey_storage + .add_passkey(&omni_account, credential_id, pubkey) + .expect("Failed to add passkey"); + + // Verify passkey exists + assert!(passkey_storage.exists_passkey(&omni_account, credential_id)); + + // Remove the passkey + passkey_storage + .remove_passkey(&omni_account, credential_id) + .expect("Failed to remove passkey"); + + // Verify passkey no longer exists + assert!(!passkey_storage.exists_passkey(&omni_account, credential_id)); + + // Verify get_passkey returns None + let result = passkey_storage + .get_passkey(&omni_account, credential_id) + .expect("Failed to query passkey"); + + assert!(result.is_none()); +} + +#[test] +fn test_challenge_account_mismatch() { + // This test verifies that challenges are properly scoped to specific accounts + + let storage_db = create_test_storage(); + let challenge_storage = PasskeyChallengeStorage::new(storage_db.clone()); + + let account1 = create_test_account(8); + let account2 = create_test_account(9); + + // Store challenge for account1 + challenge_storage + .store_challenge(&account1, "test_challenge", 300) + .expect("Failed to store challenge"); + + // Try to verify with account2 (should fail) + let result = challenge_storage.verify_and_consume_challenge("test_challenge", &account2); + + assert!(matches!(result, Err(PasskeyChallengeError::InvalidChallenge))); + + // Verify challenge still exists for account1 + challenge_storage + .verify_and_consume_challenge("test_challenge", &account1) + .expect("Challenge should still be valid for account1"); +} + +#[test] +fn test_passkey_duplicate_prevention() { + // This test verifies that duplicate passkeys are properly prevented + + let storage_db = create_test_storage(); + let passkey_storage = PasskeyStorage::new(storage_db.clone()); + + let omni_account = create_test_account(10); + + let credential_id = "test_credential_dup"; + let pubkey = b"test_pubkey_dup"; + + // Add passkey first time (should succeed) + passkey_storage + .add_passkey(&omni_account, credential_id, pubkey) + .expect("Failed to add passkey"); + + // Try to add the same passkey again (should fail) + let duplicate_result = passkey_storage.add_passkey(&omni_account, credential_id, pubkey); + + assert!(matches!(duplicate_result, Err(PasskeyError::DuplicatePasskey))); +} + +#[test] +fn test_concurrent_authentication_sessions() { + // This test verifies that multiple authentication sessions can be managed simultaneously + + let storage_db = create_test_storage(); + let challenge_storage = PasskeyChallengeStorage::new(storage_db.clone()); + let passkey_storage = PasskeyStorage::new(storage_db.clone()); + + let account1 = create_test_account(12); + let account2 = create_test_account(13); + + // Register passkeys for both accounts + passkey_storage + .add_passkey(&account1, "cred1", b"pubkey1") + .expect("Failed to add passkey for account1"); + + passkey_storage + .add_passkey(&account2, "cred2", b"pubkey2") + .expect("Failed to add passkey for account2"); + + // Create challenges for both accounts + challenge_storage + .store_challenge(&account1, "challenge1", 300) + .expect("Failed to store challenge1"); + + challenge_storage + .store_challenge(&account2, "challenge2", 300) + .expect("Failed to store challenge2"); + + // Verify challenges independently + challenge_storage + .verify_and_consume_challenge("challenge1", &account1) + .expect("Failed to verify challenge1"); + + challenge_storage + .verify_and_consume_challenge("challenge2", &account2) + .expect("Failed to verify challenge2"); + + // Verify passkeys exist + let pk1 = passkey_storage.get_passkey(&account1, "cred1").unwrap().unwrap(); + let pk2 = passkey_storage.get_passkey(&account2, "cred2").unwrap().unwrap(); + + assert_eq!(pk1.pubkey, b"pubkey1"); + assert_eq!(pk2.pubkey, b"pubkey2"); +} diff --git a/tee-worker/omni-executor/rpc-server/Cargo.toml b/tee-worker/omni-executor/rpc-server/Cargo.toml index 7d12a4bda3..c88dce669e 100644 --- a/tee-worker/omni-executor/rpc-server/Cargo.toml +++ b/tee-worker/omni-executor/rpc-server/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true alloy = { workspace = true } async-trait = { workspace = true } base58 = { workspace = true } +base64 = { workspace = true } chrono = { workspace = true } email_address = { workspace = true } ethers = { workspace = true } diff --git a/tee-worker/omni-executor/rpc-server/src/detailed_error.rs b/tee-worker/omni-executor/rpc-server/src/detailed_error.rs index 545d4825f5..3555328969 100644 --- a/tee-worker/omni-executor/rpc-server/src/detailed_error.rs +++ b/tee-worker/omni-executor/rpc-server/src/detailed_error.rs @@ -2,6 +2,11 @@ use crate::error_code::{ ACCOUNT_PARSE_ERROR_CODE, EMAIL_SERVICE_ERROR_CODE, GAS_ESTIMATION_FAILED_CODE, INVALID_ADDRESS_FORMAT_CODE, INVALID_AMOUNT_CODE, INVALID_CHAIN_ID_CODE, INVALID_HEX_FORMAT_CODE, INVALID_USER_OPERATION_CODE, INVALID_WALLET_INDEX_CODE, + PASSKEY_ALREADY_EXISTS_CODE, PASSKEY_ATTESTATION_PARSE_ERROR_CODE, + PASSKEY_CHALLENGE_EXPIRED_CODE, PASSKEY_CHALLENGE_NOT_FOUND_CODE, + PASSKEY_COUNTER_VALIDATION_FAILED_CODE, PASSKEY_INVALID_CHALLENGE_CODE, PASSKEY_NOT_FOUND_CODE, + PASSKEY_PARSE_ERROR_CODE, PASSKEY_REPLAY_ATTACK_CODE, PASSKEY_RP_ID_MISMATCH_CODE, + PASSKEY_SIGNATURE_INVALID_CODE, PASSKEY_USER_VERIFICATION_FAILED_CODE, SIGNATURE_SERVICE_UNAVAILABLE_CODE, SIGNER_SERVICE_ERROR_CODE, STORAGE_SERVICE_ERROR_CODE, UNEXPECTED_RESPONSE_TYPE_CODE, }; @@ -179,4 +184,104 @@ impl DetailedError { Self::new(SIGNATURE_SERVICE_UNAVAILABLE_CODE, "Signature service temporarily unavailable") .with_suggestion("Please try again in a few moments") } + + // Passkey/WebAuthn error factory methods + pub fn passkey_challenge_not_found() -> Self { + Self::new(PASSKEY_CHALLENGE_NOT_FOUND_CODE, "Passkey challenge not found") + .with_reason("Challenge may have expired or was never created") + .with_suggestion("Request a new challenge using omni_requestPasskeyChallenge") + } + + pub fn passkey_challenge_expired() -> Self { + Self::new(PASSKEY_CHALLENGE_EXPIRED_CODE, "Passkey challenge has expired") + .with_reason("Challenge exceeded its 5-minute validity period") + .with_suggestion("Request a new challenge using omni_requestPasskeyChallenge") + } + + pub fn passkey_invalid_challenge(reason: &str) -> Self { + Self::new(PASSKEY_INVALID_CHALLENGE_CODE, "Invalid passkey challenge") + .with_reason(reason) + .with_suggestion( + "Ensure the challenge matches the one received from omni_requestPasskeyChallenge", + ) + } + + pub fn passkey_not_found(credential_id: &str) -> Self { + Self::new(PASSKEY_NOT_FOUND_CODE, "Passkey not found") + .with_field("credential_id") + .with_received(credential_id) + .with_reason("No passkey registered for this account and credential ID") + .with_suggestion("Register a new passkey using omni_attachPasskey") + } + + pub fn passkey_signature_invalid(reason: &str) -> Self { + Self::new(PASSKEY_SIGNATURE_INVALID_CODE, "Passkey signature verification failed") + .with_reason(reason) + .with_suggestion("Ensure the passkey signature is correctly generated and matches the registered public key") + } + + pub fn passkey_parse_error(field: &str, error: &str) -> Self { + Self::new(PASSKEY_PARSE_ERROR_CODE, "Failed to parse passkey data") + .with_field(field) + .with_reason(error) + .with_suggestion("Check the passkey data format and encoding") + } + + pub fn passkey_rp_id_mismatch(expected_rp_id: &str, client_id: &str) -> Self { + Self::new(PASSKEY_RP_ID_MISMATCH_CODE, "Passkey RP ID validation failed") + .with_field("rp_id") + .with_expected(expected_rp_id) + .with_reason(format!( + "RP ID hash in authenticator data does not match expected RP ID for client '{}'", + client_id + )) + .with_suggestion("Ensure the passkey was created for the correct relying party domain") + } + + pub fn passkey_replay_attack(counter: u32) -> Self { + Self::new(PASSKEY_REPLAY_ATTACK_CODE, "Replay attack detected") + .with_field("signature_counter") + .with_received(counter.to_string()) + .with_reason( + "Signature counter is not greater than stored value - possible replay attack", + ) + .with_suggestion("This authentication request may have been captured and replayed by an attacker. Contact support if you believe this is an error.") + } + + pub fn passkey_counter_validation_failed() -> Self { + Self::new( + PASSKEY_COUNTER_VALIDATION_FAILED_CODE, + "Passkey counter validation failed", + ) + .with_reason("Authenticator may have been cloned or downgraded") + .with_suggestion("The authenticator returned a zero counter when a non-zero counter was expected. This may indicate device cloning or tampering.") + } + + pub fn passkey_user_verification_failed(flag_type: &str) -> Self { + Self::new(PASSKEY_USER_VERIFICATION_FAILED_CODE, "Passkey user verification failed") + .with_field(flag_type) + .with_reason(format!("{} flag not set in authenticator data", flag_type)) + .with_suggestion( + "Ensure user presence and verification are enabled on the authenticator", + ) + } + + pub fn passkey_attestation_parse_error(error: &str) -> Self { + Self::new( + PASSKEY_ATTESTATION_PARSE_ERROR_CODE, + "Failed to parse passkey attestation object", + ) + .with_reason(error) + .with_suggestion( + "Ensure the attestation object is properly CBOR-encoded and base64url-encoded", + ) + } + + pub fn passkey_already_exists(credential_id: &str) -> Self { + Self::new(PASSKEY_ALREADY_EXISTS_CODE, "Passkey already registered") + .with_field("credential_id") + .with_received(credential_id) + .with_reason("A passkey with this credential ID already exists for this account") + .with_suggestion("Use the existing passkey or remove it before registering a new one") + } } diff --git a/tee-worker/omni-executor/rpc-server/src/error_code.rs b/tee-worker/omni-executor/rpc-server/src/error_code.rs index d64d3d149f..72362b5584 100644 --- a/tee-worker/omni-executor/rpc-server/src/error_code.rs +++ b/tee-worker/omni-executor/rpc-server/src/error_code.rs @@ -57,6 +57,20 @@ pub const INVALID_EMAIL_FORMAT_CODE: i32 = -32107; pub const ACCOUNT_PARSE_ERROR_CODE: i32 = -32121; pub const INVALID_ACCOUNT_LENGTH_CODE: i32 = -32124; +// Passkey/WebAuthn Error Codes (-32140 to -32159) +pub const PASSKEY_CHALLENGE_NOT_FOUND_CODE: i32 = -32140; +pub const PASSKEY_CHALLENGE_EXPIRED_CODE: i32 = -32141; +pub const PASSKEY_INVALID_CHALLENGE_CODE: i32 = -32142; +pub const PASSKEY_NOT_FOUND_CODE: i32 = -32143; +pub const PASSKEY_SIGNATURE_INVALID_CODE: i32 = -32144; +pub const PASSKEY_PARSE_ERROR_CODE: i32 = -32145; +pub const PASSKEY_RP_ID_MISMATCH_CODE: i32 = -32146; +pub const PASSKEY_REPLAY_ATTACK_CODE: i32 = -32147; +pub const PASSKEY_COUNTER_VALIDATION_FAILED_CODE: i32 = -32148; +pub const PASSKEY_USER_VERIFICATION_FAILED_CODE: i32 = -32149; +pub const PASSKEY_ATTESTATION_PARSE_ERROR_CODE: i32 = -32150; +pub const PASSKEY_ALREADY_EXISTS_CODE: i32 = -32151; + // External Service Error Codes (-32160 to -32179) pub const SIGNER_SERVICE_ERROR_CODE: i32 = -32160; pub const EMAIL_SERVICE_ERROR_CODE: i32 = -32162; diff --git a/tee-worker/omni-executor/rpc-server/src/methods/mod.rs b/tee-worker/omni-executor/rpc-server/src/methods/mod.rs index 6a187642f5..dedc89a9c5 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/mod.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/mod.rs @@ -5,7 +5,7 @@ use jsonrpsee::RpcModule; mod pumpx; use pumpx::*; -mod omni; +pub mod omni; use omni::*; use parentchain_rpc_client::{SubstrateRpcClient, SubstrateRpcClientFactory}; diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs new file mode 100644 index 0000000000..7c3383d9a2 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs @@ -0,0 +1,159 @@ +use crate::{ + error_code::*, server::RpcContext, verify_auth::verify_auth, Deserialize, ErrorCode, Serialize, +}; + +use executor_core::intent_executor::IntentExecutor; +use executor_crypto::passkey::{AttestationResult, PasskeyVerifier}; +use executor_primitives::{to_omni_auth, utils::hex::ToHexPrefixed, UserAuth, UserId}; +use executor_storage::{ + PasskeyChallengeError, PasskeyChallengeStorage, PasskeyError, PasskeyStorage, +}; +use heima_primitives::Identity; +use jsonrpsee::{types::ErrorObject, RpcModule}; +use parentchain_rpc_client::{SubstrateRpcClient, SubstrateRpcClientFactory}; +use tracing::error; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct AttachPasskeyParams { + pub user_id: UserId, + pub user_auth: UserAuth, + pub client_id: String, + pub attestation_object: String, // Base64 encoded WebAuthn attestation object + pub client_data_json: String, // Base64 encoded WebAuthn client data JSON +} + +#[derive(Serialize, Clone)] +pub struct AttachPasskeyResponse { + pub success: bool, + pub message: String, +} + +pub fn register_attach_passkey< + EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, + SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, + CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, + Header: Send + Sync + 'static, + RpcClient: SubstrateRpcClient
+ Send + Sync + 'static, + RpcClientFactory: SubstrateRpcClientFactory + Send + Sync + 'static, +>( + module: &mut RpcModule< + RpcContext< + Header, + RpcClient, + RpcClientFactory, + EthereumIntentExecutor, + SolanaIntentExecutor, + CrossChainIntentExecutor, + >, + >, +) { + module + .register_async_method("omni_attachPasskey", |params, ctx, _| async move { + let params = params.parse::().map_err(|e| { + error!("Failed to parse params: {:?}", e); + ErrorCode::ParseError + })?; + + let identity = Identity::try_from(params.user_id.clone()).map_err(|_| { + error!("Invalid existing user ID format"); + ErrorCode::ParseError + })?; + + let auth = to_omni_auth(¶ms.user_auth, ¶ms.user_id, ¶ms.client_id) + .map_err(|e| { + error!("Failed to convert to OmniAuth: {:?}", e); + ErrorCode::ParseError + })?; + + verify_auth(ctx.clone(), &auth).await.map_err(|e| { + error!("Failed to verify existing user authentication: {:?}", e); + e.to_detailed_error().to_error_object() + })?; + + let omni_account = identity.to_omni_account(¶ms.client_id); + + let expected_origin = + crate::methods::omni::common::get_origin_for_client(¶ms.client_id); + + // Verify client data JSON and consume challenge + let challenge_storage = PasskeyChallengeStorage::new(ctx.storage_db.clone()); + PasskeyVerifier::verify_client_data_json( + ¶ms.client_data_json, + omni_account.as_ref(), + expected_origin, + "webauthn.create", // For passkey registration/attachment + |challenge, omni_account| { + challenge_storage + .verify_and_consume_challenge(challenge, &(*omni_account).into()) + .map_err(|e| { + match e { + PasskeyChallengeError::ChallengeNotFound => { + error!("Challenge not found"); + }, + PasskeyChallengeError::ChallengeExpired => { + error!("Challenge expired"); + }, + PasskeyChallengeError::InvalidChallenge => { + error!("Invalid challenge"); + }, + _ => { + error!("Challenge verification failed: {:?}", e); + }, + } + executor_crypto::passkey::PasskeyError::ChallengeVerificationFailed + }) + }, + ) + .map_err(|e| { + error!("Client data verification failed: {:?}", e); + match e { + executor_crypto::passkey::PasskeyError::ChallengeVerificationFailed => { + ErrorCode::ServerError(-32011) // Challenge mismatch + }, + executor_crypto::passkey::PasskeyError::OriginVerificationFailed => { + ErrorCode::ServerError(-32012) // Origin mismatch + }, + executor_crypto::passkey::PasskeyError::AttestationParseError(_) => { + ErrorCode::ServerError(-32013) // Attestation parse error + }, + _ => ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE), + } + })?; + + let AttestationResult { credential_id, public_key } = + PasskeyVerifier::verify_attestation(¶ms.attestation_object).map_err(|e| { + error!("WebAuthn attestation verification failed: {:?}", e); + match e { + executor_crypto::passkey::PasskeyError::AttestationParseError(_) => { + ErrorCode::ServerError(-32013) // Attestation parse error + }, + _ => ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE), + } + })?; + + let public_key_sec1_bytes = public_key.verifying_key.to_sec1_bytes(); + let passkey_storage = PasskeyStorage::new(ctx.storage_db.clone()); + passkey_storage + .add_passkey(&omni_account, &credential_id, &public_key_sec1_bytes) + .map_err(|e| match e { + PasskeyError::DuplicatePasskey => { + error!("Duplicate passkey (same omni_account + credential_id)"); + ErrorCode::ServerError(-32001) + }, + _ => { + error!("Failed to store passkey: {:?}", e); + ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE) + }, + })?; + + Ok::(AttachPasskeyResponse { + success: true, + message: format!( + "Passkey ({}) successfully attached to account {}", + credential_id, + omni_account.to_hex() + ), + }) + }) + .expect("Failed to register omni_attachPasskey method"); +} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs index 9d4c5f6db1..938cb7440f 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs @@ -203,3 +203,17 @@ pub fn check_auth(ext: &Extensions) -> Result { } Err(()) } + +pub fn get_rp_id_for_client(client_id: &str) -> &str { + match client_id { + "wildmeta" => "wildmeta.io", + _ => "localhost", // Development/testing + } +} + +pub fn get_origin_for_client(client_id: &str) -> &str { + match client_id { + "wildmeta" => "https://wildmeta.io", + _ => "http://localhost:3000", // Development/testing + } +} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs index 9c8ff5f896..f2d6b3fe30 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs @@ -8,8 +8,14 @@ use crate::{ }; use chrono::Utc; use executor_core::intent_executor::IntentExecutor; +use executor_crypto::passkey::{AttestationResult, PasskeyVerifier}; use executor_primitives::{ - to_omni_auth, utils::hex::hex_encode, ChainId, ClientAuth, Identity, UserAuth, UserId, + to_omni_auth, + utils::hex::{hex_encode, ToHexPrefixed}, + ChainId, ClientAuth, Identity, UserAuth, UserId, +}; +use executor_storage::{ + PasskeyChallengeError, PasskeyChallengeStorage, PasskeyError, PasskeyStorage, }; use hyperliquid_rust_sdk::{ApproveAgent, ApproveBuilderFee, Eip712, Withdraw3}; use jsonrpsee::{types::ErrorObject, RpcModule}; @@ -28,6 +34,17 @@ pub struct GetHyperliquidSignatureDataParams { pub client_auth: Option, pub action_type: HyperliquidActionType, pub chain_id: ChainId, + pub attach_passkey: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AttachPasskeyData { + /// The attestation object from the WebAuthn registration ceremony (base64url encoded) + /// This contains the credential public key, credential ID, and attestation statement + pub attestation_object: String, + /// The client data JSON from the WebAuthn registration ceremony (base64url encoded) + /// This contains the challenge, origin, and other client-side data + pub client_data_json: String, } #[derive(Debug, Deserialize, Clone)] @@ -123,6 +140,159 @@ pub fn register_get_hyperliquid_signature_data< .to_error_object()); } + if let Some(attach_passkey_data) = ¶ms.attach_passkey { + let user_auth = params.user_auth.as_ref().ok_or_else(|| { + error!("user_auth is required when attach_passkey is provided"); + DetailedError::new(MISSING_REQUIRED_FIELD_CODE, "Missing required authentication") + .with_field("user_auth") + .with_expected("User authentication data") + .with_reason("user_auth must be provided when attaching a passkey") + .with_suggestion("Provide user_auth to authenticate before attaching a passkey") + .to_error_object() + })?; + let auth = + to_omni_auth(user_auth, ¶ms.user_id, ¶ms.client_id).map_err(|e| { + error!("Failed to convert to OmniAuth: {:?}", e); + DetailedError::new(PARSE_ERROR_CODE, "Failed to convert authentication data") + .with_field("user_auth") + .with_reason(format!("OmniAuth conversion error: {:?}", e)) + .to_error_object() + })?; + + verify_auth(ctx.clone(), &auth).await.map_err(|e| { + error!("Failed to verify user authentication: {:?}", e); + DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication verification failed") + .with_field("user_auth") + .with_reason(format!("Verification error: {:?}", e)) + .with_suggestion("Please check your authentication credentials") + .to_error_object() + })?; + + let identity = Identity::try_from(params.user_id.clone()).map_err(|e| { + error!("Failed to convert user ID to identity: {}", e); + DetailedError::new(PARSE_ERROR_CODE, "Failed to parse user identity") + .with_field("user_id") + .with_reason(format!("Identity conversion error: {}", e)) + .to_error_object() + })?; + let omni_account = identity.to_omni_account(¶ms.client_id); + + // Determine expected origin based on client_id + let expected_origin = crate::methods::omni::common::get_origin_for_client(¶ms.client_id); + + // Verify client data JSON and consume challenge + let challenge_storage = PasskeyChallengeStorage::new(ctx.storage_db.clone()); + PasskeyVerifier::verify_client_data_json( + &attach_passkey_data.client_data_json, + omni_account.as_ref(), + expected_origin, + "webauthn.create", // For passkey registration/attachment + |challenge, omni_account| { + challenge_storage + .verify_and_consume_challenge(challenge, &(*omni_account).into()) + .map_err(|e| { + match e { + PasskeyChallengeError::ChallengeNotFound => { + error!("Challenge not found for passkey attachment"); + }, + PasskeyChallengeError::ChallengeExpired => { + error!("Challenge expired for passkey attachment"); + }, + PasskeyChallengeError::InvalidChallenge => { + error!("Invalid challenge for passkey attachment"); + }, + _ => { + error!("Challenge verification failed during passkey attachment: {:?}", e); + }, + } + executor_crypto::passkey::PasskeyError::ChallengeVerificationFailed + }) + }, + ) + .map_err(|e| { + error!("Client data verification failed during passkey attachment: {:?}", e); + match e { + executor_crypto::passkey::PasskeyError::ChallengeVerificationFailed => { + DetailedError::new(-32011, "Challenge verification failed") + .with_field("attach_passkey.client_data_json") + .with_reason("Challenge mismatch or not found") + .with_suggestion("Request a new challenge via omni_requestPasskeyChallenge and try again") + }, + executor_crypto::passkey::PasskeyError::OriginVerificationFailed => { + DetailedError::new(-32012, "Origin verification failed") + .with_field("attach_passkey.client_data_json") + .with_reason("Origin does not match expected value") + .with_suggestion("Ensure the WebAuthn ceremony is initiated from the correct origin") + }, + executor_crypto::passkey::PasskeyError::AttestationParseError(_) => { + DetailedError::new(-32013, "Client data parse error") + .with_field("attach_passkey.client_data_json") + .with_reason("Failed to parse client data JSON") + .with_suggestion("Ensure the client data is base64url encoded JSON") + }, + _ => DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Client data verification failed") + .with_field("attach_passkey.client_data_json") + .with_reason(format!("Verification error: {:?}", e)) + } + .to_error_object() + })?; + + // Verify attestation and extract credential_id and public_key + let AttestationResult { credential_id, public_key } = PasskeyVerifier::verify_attestation( + &attach_passkey_data.attestation_object, + ) + .map_err(|e| { + error!("WebAuthn attestation verification failed during passkey attachment: {:?}", e); + match e { + executor_crypto::passkey::PasskeyError::AttestationParseError(_) => { + DetailedError::new(-32013, "Attestation parse error") + .with_field("attach_passkey.attestation_object") + .with_reason("Failed to parse attestation object") + .with_suggestion("Ensure the attestation object is base64url encoded CBOR") + }, + _ => DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Attestation verification failed") + .with_field("attach_passkey.attestation_object") + .with_reason(format!("Verification error: {:?}", e)) + } + .to_error_object() + })?; + + + // Store public key as direct SEC1 bytes for direct usage without parsing + let public_key_sec1_bytes = public_key.verifying_key.to_sec1_bytes(); + + // Store the new passkey to the authenticated user's account + let passkey_storage = PasskeyStorage::new(ctx.storage_db.clone()); + passkey_storage + .add_passkey( + &omni_account, + &credential_id, + &public_key_sec1_bytes, + ) + .map_err(|e| { + error!("Failed to attach passkey to omni_account {}: {:?}", omni_account.to_hex(), e); + let detailed_error = match e { + PasskeyError::DuplicatePasskey => { + DetailedError::new(-32001, "Passkey already exists for this account") + .with_field("attach_passkey") + .with_reason(format!("A passkey with credential_id '{}' is already registered to this account", credential_id)) + .with_suggestion("This credential is already attached. Use a different credential or remove the existing one first with omni_removePasskey") + }, + PasskeyError::StorageError => { + DetailedError::new(INTERNAL_ERROR_CODE, "Failed to store passkey") + .with_reason("Database storage operation failed") + .with_suggestion("Please try again later or contact support if the issue persists") + }, + _ => { + DetailedError::new(INTERNAL_ERROR_CODE, "Failed to attach passkey") + .with_reason(format!("Storage error: {:?}", e)) + .with_suggestion("Please try again later") + } + }; + detailed_error.to_error_object() + })?; + } + // Unified authentication logic let main_address = if let Some(user_auth) = ¶ms.user_auth { // User authentication provided @@ -251,7 +421,6 @@ pub fn register_get_hyperliquid_signature_data< .to_error_object()); }; - // Derive omni_account for signing (works for both auth methods) let identity = Identity::try_from(params.user_id.clone()).map_err(|e| { error!("Failed to convert user ID to identity: {}", e); DetailedError::new(PARSE_ERROR_CODE, "Failed to parse user identity") diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs index 009346636e..518ac333a3 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs @@ -1,7 +1,7 @@ use crate::server::RpcContext; use jsonrpsee::RpcModule; -mod common; +pub mod common; use common::*; mod get_health; @@ -68,6 +68,15 @@ use user_login::*; mod get_hyperliquid_signature_data; use get_hyperliquid_signature_data::*; +mod attach_passkey; +use attach_passkey::*; + +mod remove_passkey; +use remove_passkey::*; + +mod request_passkey_challenge; +use request_passkey_challenge::*; + use parentchain_rpc_client::{SubstrateRpcClient, SubstrateRpcClientFactory}; #[cfg(test)] @@ -105,6 +114,9 @@ pub fn register_omni< register_get_oauth2_google_authorization_url(module); register_get_web3_sign_in_message(module); register_user_login(module); + register_request_passkey_challenge(module); + register_attach_passkey(module); + register_remove_passkey(module); register_request_jwt(module); register_export_wallet(module); diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs new file mode 100644 index 0000000000..a32ab69820 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs @@ -0,0 +1,99 @@ +use crate::{ + detailed_error::DetailedError, server::RpcContext, verify_auth::verify_auth, Deserialize, + ErrorCode, Serialize, +}; + +use executor_core::intent_executor::IntentExecutor; +use executor_primitives::{to_omni_auth, UserAuth, UserId}; +use executor_storage::{PasskeyError, PasskeyStorage}; +use heima_primitives::Identity; +use jsonrpsee::{types::ErrorObject, RpcModule}; +use parentchain_rpc_client::{SubstrateRpcClient, SubstrateRpcClientFactory}; +use tracing::error; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RemovePasskeyParams { + pub user_id: UserId, + pub user_auth: UserAuth, + pub credential_id: String, + pub client_id: String, +} + +#[derive(Serialize, Clone)] +pub struct RemovePasskeyResponse { + pub success: bool, + pub message: String, +} + +pub fn register_remove_passkey< + EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, + SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, + CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, + Header: Send + Sync + 'static, + RpcClient: SubstrateRpcClient
+ Send + Sync + 'static, + RpcClientFactory: SubstrateRpcClientFactory + Send + Sync + 'static, +>( + module: &mut RpcModule< + RpcContext< + Header, + RpcClient, + RpcClientFactory, + EthereumIntentExecutor, + SolanaIntentExecutor, + CrossChainIntentExecutor, + >, + >, +) { + module + .register_async_method("omni_removePasskey", |params, ctx, _| async move { + let params = params.parse::().map_err(|e| { + error!("Failed to parse params: {:?}", e); + ErrorCode::ParseError + })?; + + let identity = Identity::try_from(params.user_id.clone()).map_err(|_| { + error!("Invalid user ID format"); + ErrorCode::ParseError + })?; + + let auth = to_omni_auth(¶ms.user_auth, ¶ms.user_id, ¶ms.client_id) + .map_err(|e| { + error!("Failed to convert to OmniAuth: {:?}", e); + ErrorCode::ParseError + })?; + + verify_auth(ctx.clone(), &auth).await.map_err(|e| { + error!("Failed to verify user authentication: {:?}", e); + e.to_detailed_error().to_error_object() + })?; + + let omni_account = identity.to_omni_account(¶ms.client_id); + let passkey_storage = PasskeyStorage::new(ctx.storage_db.clone()); + + if !passkey_storage.exists_passkey(&omni_account, ¶ms.credential_id) { + error!("Passkey not found for this account and credential"); + return Err( + DetailedError::passkey_not_found(¶ms.credential_id).to_error_object() + ); + } + + passkey_storage + .remove_passkey(&omni_account, ¶ms.credential_id) + .map_err(|e| match e { + PasskeyError::StorageError => { + error!("Failed to remove passkey: storage error"); + DetailedError::storage_error("passkey removal").to_error_object() + }, + _ => { + error!("Failed to remove passkey: {:?}", e); + DetailedError::storage_error("passkey removal").to_error_object() + }, + })?; + + Ok::(RemovePasskeyResponse { + success: true, + message: format!("Passkey {} successfully removed", params.credential_id), + }) + }) + .expect("Failed to register omni_removePasskey method"); +} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs new file mode 100644 index 0000000000..c35af5c1d3 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs @@ -0,0 +1,121 @@ +use crate::{ + detailed_error::DetailedError, error_code::*, server::RpcContext, verify_auth::verify_auth, + Deserialize, Serialize, +}; +use executor_core::intent_executor::IntentExecutor; +use executor_primitives::{to_omni_auth, UserAuth, UserId}; +use executor_storage::PasskeyChallengeStorage; +use heima_primitives::Identity; +use jsonrpsee::{types::ErrorObject, RpcModule}; +use parentchain_rpc_client::{SubstrateRpcClient, SubstrateRpcClientFactory}; +use tracing::error; + +const CHALLENGE_TIMEOUT_SECONDS: u64 = 300; // 5 minutes + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RequestPasskeyChallengeParams { + pub user_id: UserId, + pub user_auth: UserAuth, + pub client_id: String, +} + +#[derive(Serialize, Clone)] +pub struct RequestPasskeyChallengeResponse { + pub challenge: String, // Base64 encoded 32 bytes + pub timeout: u64, // Challenge validity in seconds +} + +pub fn register_request_passkey_challenge< + EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, + SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, + CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, + Header: Send + Sync + 'static, + RpcClient: SubstrateRpcClient
+ Send + Sync + 'static, + RpcClientFactory: SubstrateRpcClientFactory + Send + Sync + 'static, +>( + module: &mut RpcModule< + RpcContext< + Header, + RpcClient, + RpcClientFactory, + EthereumIntentExecutor, + SolanaIntentExecutor, + CrossChainIntentExecutor, + >, + >, +) { + module + .register_async_method("omni_requestPasskeyChallenge", |params, ctx, _| async move { + let params = params.parse::().map_err(|e| { + error!("Failed to parse params: {:?}", e); + DetailedError::new(PARSE_ERROR_CODE, "Failed to parse request parameters") + .with_reason(format!("Invalid JSON structure: {}", e)) + .to_error_object() + })?; + + let identity = Identity::try_from(params.user_id.clone()).map_err(|_| { + error!("Invalid user ID format"); + DetailedError::new(PARSE_ERROR_CODE, "Failed to parse user identity") + .with_field("user_id") + .with_reason("Invalid user ID format") + .with_suggestion( + "Ensure user_id follows the correct format for the specified type", + ) + .to_error_object() + })?; + + let auth = to_omni_auth(¶ms.user_auth, ¶ms.user_id, ¶ms.client_id) + .map_err(|e| { + error!("Failed to convert to OmniAuth: {:?}", e); + DetailedError::new(PARSE_ERROR_CODE, "Failed to convert authentication data") + .with_field("user_auth") + .with_reason(format!("Authentication conversion failed: {}", e)) + .with_suggestion("Ensure user_auth matches the user_id type") + .to_error_object() + })?; + + verify_auth(ctx.clone(), &auth).await.map_err(|_| { + error!("Failed to verify user authentication"); + DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Authentication verification failed", + ) + .with_reason("User authentication could not be verified") + .with_suggestion("Check your authentication credentials and try again") + .to_error_object() + })?; + + // Generate a random 32-byte challenge + use rand::RngCore; + let mut challenge_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut challenge_bytes); + + // Base64 encode the challenge (URL-safe, no padding) + use base64::Engine; + let challenge = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(challenge_bytes); + + // Store challenge with expiration + let timeout = CHALLENGE_TIMEOUT_SECONDS; + let challenge_storage = PasskeyChallengeStorage::new(ctx.storage_db.clone()); + let omni_account = identity.to_omni_account(¶ms.client_id); + + challenge_storage.store_challenge(&omni_account, &challenge, timeout).map_err( + |_| { + error!("Failed to store challenge"); + DetailedError::new(INTERNAL_ERROR_CODE, "Failed to store challenge") + .with_reason("Challenge storage operation failed") + .with_suggestion( + "Please try again later or contact support if the issue persists", + ) + .to_error_object() + }, + )?; + + Ok::(RequestPasskeyChallengeResponse { + challenge, + timeout, + }) + }) + .expect("Failed to register omni_requestPasskeyChallenge method"); +} diff --git a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs index d2ea730079..8a8d2592c1 100644 --- a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs @@ -1,10 +1,13 @@ -use crate::server::RpcContext; +use crate::{detailed_error::DetailedError, server::RpcContext}; use executor_core::intent_executor::IntentExecutor; use executor_primitives::{ signature::HeimaMultiSignature, utils::hex::ToHexPrefixed, Hashable, Identity, OAuth2Data, - OAuth2Provider, OmniAuth, VerificationCode, Web2IdentityType, + OAuth2Provider, OmniAuth, PasskeyData, VerificationCode, Web2IdentityType, +}; +use executor_storage::{ + OAuth2StateVerifierStorage, PasskeyChallengeStorage, Storage, StorageDB, + VerificationCodeStorage, }; -use executor_storage::{OAuth2StateVerifierStorage, Storage, StorageDB, VerificationCodeStorage}; use heima_authentication::{ auth_token::{AuthTokenClaims, AuthTokenValidator, Error as AuthTokenError, Validation}, constants::AUTH_TOKEN_ID_TYPE, @@ -22,6 +25,7 @@ pub enum AuthenticationError { InvalidVerificationCode, OAuth2Error(String), AuthTokenError(AuthTokenError), + PasskeyError(String), } impl Display for AuthenticationError { @@ -42,6 +46,77 @@ impl Display for AuthenticationError { AuthenticationError::AuthTokenError(err) => { write!(f, "Auth token error: {:?}", err) }, + AuthenticationError::PasskeyError(msg) => { + write!(f, "Passkey error: {}", msg) + }, + } + } +} + +impl AuthenticationError { + /// Convert AuthenticationError to DetailedError with proper error codes and context + pub fn to_detailed_error(&self) -> DetailedError { + use crate::error_code::AUTH_VERIFICATION_FAILED_CODE; + match self { + AuthenticationError::PasskeyError(msg) => { + // Try to match specific passkey error types for better error messages + if msg.contains("Challenge not found") + || msg.contains("Challenge") && msg.contains("not found") + { + DetailedError::passkey_challenge_not_found() + } else if msg.contains("Challenge expired") { + DetailedError::passkey_challenge_expired() + } else if msg.contains("Invalid challenge") { + DetailedError::passkey_invalid_challenge(msg) + } else if msg.contains("Passkey not found") { + // Extract credential_id if present in message + DetailedError::passkey_not_found("") + } else if msg.contains("signature verification failed") + || msg.contains("Invalid signature") + { + DetailedError::passkey_signature_invalid(msg) + } else if msg.contains("Failed to parse") { + DetailedError::passkey_parse_error("", msg) + } else if msg.contains("RP ID") { + // Try to extract RP ID info from message + let client_id = msg + .split("client_id: '") + .nth(1) + .and_then(|s| s.split('\'').next()) + .unwrap_or(""); + let expected_rp_id = msg + .split("Expected RP ID: '") + .nth(1) + .and_then(|s| s.split('\'').next()) + .unwrap_or(""); + DetailedError::passkey_rp_id_mismatch(expected_rp_id, client_id) + } else if msg.contains("Replay attack detected") { + // Extract counter if present + let counter = msg + .split("counter (") + .nth(1) + .and_then(|s| s.split(')').next()) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + DetailedError::passkey_replay_attack(counter) + } else if msg.contains("Counter validation failed") + || msg.contains("cloned or downgraded") + { + DetailedError::passkey_counter_validation_failed() + } else if msg.contains("User presence flag") { + DetailedError::passkey_user_verification_failed("user_presence") + } else if msg.contains("User verification flag") { + DetailedError::passkey_user_verification_failed("user_verification") + } else { + // Generic passkey error + DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Passkey authentication failed", + ) + .with_reason(msg) + } + }, + _ => DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, self.to_string()), } } } @@ -83,8 +158,8 @@ pub async fn verify_auth< false, ) .map(|_| ()), - OmniAuth::Passkey(ref _passkey_data) => { - todo!() + OmniAuth::Passkey(ref passkey_data) => { + verify_passkey_authentication(ctx, passkey_data).map(|_| ()) }, } } @@ -238,6 +313,130 @@ async fn verify_google_oauth2< } } +pub fn verify_passkey_authentication< + EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, + SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, + CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, + Header: Send + Sync + 'static, + RpcClient: SubstrateRpcClient
+ Send + Sync + 'static, + RpcClientFactory: SubstrateRpcClientFactory + Send + Sync + 'static, +>( + ctx: Arc< + RpcContext< + Header, + RpcClient, + RpcClientFactory, + EthereumIntentExecutor, + SolanaIntentExecutor, + CrossChainIntentExecutor, + >, + >, + passkey_data: &PasskeyData, +) -> Result<(), AuthenticationError> { + use executor_crypto::passkey::{ClientData, PasskeyVerifier}; + use executor_storage::PasskeyStorage; + + let passkey_identity = + Identity::from_web2_account(&passkey_data.user_id, Web2IdentityType::Passkey); + let omni_account = passkey_identity.to_omni_account(&passkey_data.client_id); + let client_data: ClientData = + PasskeyVerifier::parse_client_data_json(&passkey_data.client_data_json).map_err(|e| { + AuthenticationError::PasskeyError(format!("Failed to parse client data: {}", e)) + })?; + + // Verify challenge + let challenge_storage = PasskeyChallengeStorage::new(ctx.storage_db.clone()); + challenge_storage + .verify_and_consume_challenge(&client_data.challenge, &omni_account) + .map_err(|e| { + use executor_storage::PasskeyChallengeError; + match e { + PasskeyChallengeError::ChallengeNotFound => { + AuthenticationError::PasskeyError("Challenge not found".to_string()) + }, + PasskeyChallengeError::ChallengeExpired => { + AuthenticationError::PasskeyError("Challenge expired".to_string()) + }, + PasskeyChallengeError::InvalidChallenge => { + AuthenticationError::PasskeyError("Invalid challenge".to_string()) + }, + _ => AuthenticationError::PasskeyError("Challenge verification failed".to_string()), + } + })?; + + // Look up the stored passkey record using omni_account + credential_id + let passkey_record = PasskeyStorage::new(ctx.storage_db.clone()) + .get_passkey(&omni_account, &passkey_data.credential_id) + .map_err(|_| AuthenticationError::PasskeyError("Storage error".to_string()))? + .ok_or_else(|| { + AuthenticationError::PasskeyError( + "Passkey not found for this account and credential".to_string(), + ) + })?; + let public_key = PasskeyVerifier::from_sec1_bytes(&passkey_record.pubkey).map_err(|e| { + AuthenticationError::PasskeyError(format!("Invalid stored public key: {}", e)) + })?; + + // Parse auth_data bytes for validation + let auth_data_bytes = hex::decode(&passkey_data.auth_data).map_err(|_| { + AuthenticationError::PasskeyError("Invalid auth data hex format".to_string()) + })?; + + if auth_data_bytes.len() < 37 { + return Err(AuthenticationError::PasskeyError("Auth data too short".to_string())); + } + + // CRITICAL SECURITY CHECK: Verify RP ID hash + // The first 32 bytes of auth data must be SHA-256(RP ID) to prevent phishing attacks + // This ensures the authenticator signed for the correct domain + use crate::methods::omni::common::get_rp_id_for_client; + let expected_rp_id = get_rp_id_for_client(&passkey_data.client_id); + + PasskeyVerifier::verify_rp_id_hash(&auth_data_bytes, expected_rp_id).map_err(|e| { + AuthenticationError::PasskeyError(format!( + "RP ID validation failed: {}. Expected RP ID: '{}' for client_id: '{}'", + e, expected_rp_id, passkey_data.client_id + )) + })?; + + // Check flags (UP bit must be set, UV bit for security) + let flags = auth_data_bytes[32]; + let up_flag = (flags & 0x01) != 0; + let uv_flag = (flags & 0x04) != 0; + + if !up_flag { + return Err(AuthenticationError::PasskeyError("User presence flag not set".to_string())); + } + + if !uv_flag { + return Err(AuthenticationError::PasskeyError( + "User verification flag not set".to_string(), + )); + } + + // Check for webauthn.get type + if !passkey_data.client_data_json.contains("\"type\":\"webauthn.get\"") { + return Err(AuthenticationError::PasskeyError("Invalid client data type".to_string())); + } + + // Verify the passkey signature (pure cryptographic verification) + let is_valid = PasskeyVerifier::verify_passkey_signature_only( + &passkey_data.auth_data, + &passkey_data.client_data_json, + &passkey_data.signature, + &public_key, + ) + .map_err(|e| { + AuthenticationError::PasskeyError(format!("Passkey signature verification failed: {}", e)) + })?; + + if !is_valid { + return Err(AuthenticationError::Web3InvalidSignature); + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; From ba73d43211ba8baeb8f849d6a6fd6356e1fd1709 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Mon, 6 Oct 2025 15:07:40 +0000 Subject: [PATCH 02/16] clean up space --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42906aad56..6ebe2d9930 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -739,6 +739,10 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Free up disk space + if: startsWith(runner.name, 'GitHub Actions') + uses: ./.github/actions/disk-cleanup + - name: Install dependencies run: | sudo apt-get update && \ From c4cf8a55dd3b95bf702b53d998ec95e1d751a05f Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Tue, 7 Oct 2025 13:53:22 +0200 Subject: [PATCH 03/16] client_data_json needs decode. --- .../executor-crypto/src/passkey.rs | 6 ++++- .../rpc-server/src/verify_auth.rs | 25 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tee-worker/omni-executor/executor-crypto/src/passkey.rs b/tee-worker/omni-executor/executor-crypto/src/passkey.rs index b502d2f73b..a101e36b8c 100644 --- a/tee-worker/omni-executor/executor-crypto/src/passkey.rs +++ b/tee-worker/omni-executor/executor-crypto/src/passkey.rs @@ -383,9 +383,13 @@ impl PasskeyVerifier { let ecdsa_signature = Signature::from_slice(&signature_bytes) .map_err(|_| PasskeyError::InvalidSignatureFormat)?; + let client_data_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(client_data_json) + .map_err(|e| PasskeyError::ParseError(format!("Client data decode error: {}", e)))?; + let auth_data_bytes = hex::decode(auth_data_hex) .map_err(|e| PasskeyError::ParseError(format!("Auth data decode error: {}", e)))?; - let client_data_hash = Sha256::digest(client_data_json.as_bytes()); + let client_data_hash = Sha256::digest(&client_data_bytes); // Per WebAuthn spec: signature = sign(authenticatorData || SHA256(clientDataJSON)) let mut signing_data = Vec::new(); signing_data.extend_from_slice(&auth_data_bytes); diff --git a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs index 8a8d2592c1..50a3e806c3 100644 --- a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs @@ -335,6 +335,7 @@ pub fn verify_passkey_authentication< ) -> Result<(), AuthenticationError> { use executor_crypto::passkey::{ClientData, PasskeyVerifier}; use executor_storage::PasskeyStorage; + use crate::methods::omni::common::{get_origin_for_client, get_rp_id_for_client}; let passkey_identity = Identity::from_web2_account(&passkey_data.user_id, Web2IdentityType::Passkey); @@ -344,6 +345,24 @@ pub fn verify_passkey_authentication< AuthenticationError::PasskeyError(format!("Failed to parse client data: {}", e)) })?; + let expected_origin = get_origin_for_client(&passkey_data.client_id); + if client_data.origin != expected_origin { + return Err(AuthenticationError::PasskeyError(format!( + "Client data origin mismatch: expected '{}', got '{}'", + expected_origin, + client_data.origin.as_str() + ))); + } + + const EXPECTED_PASSKEY_TYPE: &str = "webauthn.get"; + if client_data.type_ != EXPECTED_PASSKEY_TYPE { + return Err(AuthenticationError::PasskeyError(format!( + "Invalid client data type: expected '{}', got '{}'", + EXPECTED_PASSKEY_TYPE, + client_data.type_.as_str() + ))); + } + // Verify challenge let challenge_storage = PasskeyChallengeStorage::new(ctx.storage_db.clone()); challenge_storage @@ -389,7 +408,6 @@ pub fn verify_passkey_authentication< // CRITICAL SECURITY CHECK: Verify RP ID hash // The first 32 bytes of auth data must be SHA-256(RP ID) to prevent phishing attacks // This ensures the authenticator signed for the correct domain - use crate::methods::omni::common::get_rp_id_for_client; let expected_rp_id = get_rp_id_for_client(&passkey_data.client_id); PasskeyVerifier::verify_rp_id_hash(&auth_data_bytes, expected_rp_id).map_err(|e| { @@ -414,11 +432,6 @@ pub fn verify_passkey_authentication< )); } - // Check for webauthn.get type - if !passkey_data.client_data_json.contains("\"type\":\"webauthn.get\"") { - return Err(AuthenticationError::PasskeyError("Invalid client data type".to_string())); - } - // Verify the passkey signature (pure cryptographic verification) let is_valid = PasskeyVerifier::verify_passkey_signature_only( &passkey_data.auth_data, From ea082133b0cebd9e2180cb739c0d49fb3d5d8efc Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Tue, 7 Oct 2025 15:52:13 +0200 Subject: [PATCH 04/16] fmt --- tee-worker/omni-executor/rpc-server/src/verify_auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs index 50a3e806c3..843fd5b100 100644 --- a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs @@ -333,9 +333,9 @@ pub fn verify_passkey_authentication< >, passkey_data: &PasskeyData, ) -> Result<(), AuthenticationError> { + use crate::methods::omni::common::{get_origin_for_client, get_rp_id_for_client}; use executor_crypto::passkey::{ClientData, PasskeyVerifier}; use executor_storage::PasskeyStorage; - use crate::methods::omni::common::{get_origin_for_client, get_rp_id_for_client}; let passkey_identity = Identity::from_web2_account(&passkey_data.user_id, Web2IdentityType::Passkey); From 399bf68d76572858c3183cb85243374952f428fa Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Fri, 10 Oct 2025 20:48:10 +0000 Subject: [PATCH 05/16] fix errors --- .../rpc-server/src/methods/omni/attach_passkey.rs | 13 +------------ .../rpc-server/src/methods/omni/remove_passkey.rs | 13 +------------ .../src/methods/omni/request_passkey_challenge.rs | 13 +------------ .../omni-executor/rpc-server/src/verify_auth.rs | 14 +------------- 4 files changed, 4 insertions(+), 49 deletions(-) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs index 7c3383d9a2..b6c932f401 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs @@ -10,7 +10,6 @@ use executor_storage::{ }; use heima_primitives::Identity; use jsonrpsee::{types::ErrorObject, RpcModule}; -use parentchain_rpc_client::{SubstrateRpcClient, SubstrateRpcClientFactory}; use tracing::error; #[derive(Debug, Deserialize, Serialize, Clone)] @@ -32,19 +31,9 @@ pub fn register_attach_passkey< EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, - Header: Send + Sync + 'static, - RpcClient: SubstrateRpcClient
+ Send + Sync + 'static, - RpcClientFactory: SubstrateRpcClientFactory + Send + Sync + 'static, >( module: &mut RpcModule< - RpcContext< - Header, - RpcClient, - RpcClientFactory, - EthereumIntentExecutor, - SolanaIntentExecutor, - CrossChainIntentExecutor, - >, + RpcContext, >, ) { module diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs index a32ab69820..6583b3d9ea 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs @@ -8,7 +8,6 @@ use executor_primitives::{to_omni_auth, UserAuth, UserId}; use executor_storage::{PasskeyError, PasskeyStorage}; use heima_primitives::Identity; use jsonrpsee::{types::ErrorObject, RpcModule}; -use parentchain_rpc_client::{SubstrateRpcClient, SubstrateRpcClientFactory}; use tracing::error; #[derive(Debug, Deserialize, Serialize, Clone)] @@ -29,19 +28,9 @@ pub fn register_remove_passkey< EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, - Header: Send + Sync + 'static, - RpcClient: SubstrateRpcClient
+ Send + Sync + 'static, - RpcClientFactory: SubstrateRpcClientFactory + Send + Sync + 'static, >( module: &mut RpcModule< - RpcContext< - Header, - RpcClient, - RpcClientFactory, - EthereumIntentExecutor, - SolanaIntentExecutor, - CrossChainIntentExecutor, - >, + RpcContext, >, ) { module diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs index c35af5c1d3..360f1b4c9a 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs @@ -7,7 +7,6 @@ use executor_primitives::{to_omni_auth, UserAuth, UserId}; use executor_storage::PasskeyChallengeStorage; use heima_primitives::Identity; use jsonrpsee::{types::ErrorObject, RpcModule}; -use parentchain_rpc_client::{SubstrateRpcClient, SubstrateRpcClientFactory}; use tracing::error; const CHALLENGE_TIMEOUT_SECONDS: u64 = 300; // 5 minutes @@ -29,19 +28,9 @@ pub fn register_request_passkey_challenge< EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, - Header: Send + Sync + 'static, - RpcClient: SubstrateRpcClient
+ Send + Sync + 'static, - RpcClientFactory: SubstrateRpcClientFactory + Send + Sync + 'static, >( module: &mut RpcModule< - RpcContext< - Header, - RpcClient, - RpcClientFactory, - EthereumIntentExecutor, - SolanaIntentExecutor, - CrossChainIntentExecutor, - >, + RpcContext, >, ) { module diff --git a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs index aaeea5896b..19e14d7f7b 100644 --- a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs @@ -268,20 +268,8 @@ pub fn verify_passkey_authentication< EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, - Header: Send + Sync + 'static, - RpcClient: SubstrateRpcClient
+ Send + Sync + 'static, - RpcClientFactory: SubstrateRpcClientFactory + Send + Sync + 'static, >( - ctx: Arc< - RpcContext< - Header, - RpcClient, - RpcClientFactory, - EthereumIntentExecutor, - SolanaIntentExecutor, - CrossChainIntentExecutor, - >, - >, + ctx: Arc>, passkey_data: &PasskeyData, ) -> Result<(), AuthenticationError> { use crate::methods::omni::common::{get_origin_for_client, get_rp_id_for_client}; From 2b9e710457249ca56f516a871ec60be720422982 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Wed, 22 Oct 2025 09:41:24 +0200 Subject: [PATCH 06/16] fix clippy --- tee-worker/omni-executor/executor-crypto/src/passkey.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tee-worker/omni-executor/executor-crypto/src/passkey.rs b/tee-worker/omni-executor/executor-crypto/src/passkey.rs index a101e36b8c..6c711aa75f 100644 --- a/tee-worker/omni-executor/executor-crypto/src/passkey.rs +++ b/tee-worker/omni-executor/executor-crypto/src/passkey.rs @@ -134,11 +134,11 @@ impl PasskeyVerifier { } let rp_id_hash_from_auth_data = &auth_data_bytes[0..32]; let expected_rp_id_hash = Sha256::digest(expected_rp_id.as_bytes()); - if rp_id_hash_from_auth_data != expected_rp_id_hash.as_slice() { + if rp_id_hash_from_auth_data != &expected_rp_id_hash[..] { return Err(PasskeyError::ParseError(format!( "RP ID hash mismatch. Expected RP ID: '{}', hash: {:02x?}, but got: {:02x?}", expected_rp_id, - expected_rp_id_hash.as_slice(), + &expected_rp_id_hash[..], rp_id_hash_from_auth_data ))); } From 6a4fdae0fa9b50bb36db2eea3216724022dddf86 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Tue, 28 Oct 2025 12:34:16 +0100 Subject: [PATCH 07/16] fix errors --- .../rpc-server/src/methods/omni/attach_passkey.rs | 8 +++----- .../src/methods/omni/get_hyperliquid_signature_data.rs | 4 ++-- .../rpc-server/src/methods/omni/remove_passkey.rs | 4 +--- .../src/methods/omni/request_passkey_challenge.rs | 4 +--- tee-worker/omni-executor/rpc-server/src/verify_auth.rs | 5 ++--- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs index b6c932f401..8b112c187e 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs @@ -4,7 +4,7 @@ use crate::{ use executor_core::intent_executor::IntentExecutor; use executor_crypto::passkey::{AttestationResult, PasskeyVerifier}; -use executor_primitives::{to_omni_auth, utils::hex::ToHexPrefixed, UserAuth, UserId}; +use executor_primitives::{to_omni_auth, utils::hex::hex_encode, UserAuth, UserId}; use executor_storage::{ PasskeyChallengeError, PasskeyChallengeStorage, PasskeyError, PasskeyStorage, }; @@ -28,12 +28,10 @@ pub struct AttachPasskeyResponse { } pub fn register_attach_passkey< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( module: &mut RpcModule< - RpcContext, + RpcContext, >, ) { module @@ -140,7 +138,7 @@ pub fn register_attach_passkey< message: format!( "Passkey ({}) successfully attached to account {}", credential_id, - omni_account.to_hex() + hex_encode(omni_account.as_ref()) ), }) }) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs index 58709b7fe9..98247ffe2e 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs @@ -11,7 +11,7 @@ use executor_core::intent_executor::IntentExecutor; use executor_crypto::passkey::{AttestationResult, PasskeyVerifier}; use executor_primitives::{ to_omni_auth, - utils::hex::{hex_encode, ToHexPrefixed}, + utils::hex::hex_encode, ChainId, ClientAuth, Identity, UserAuth, UserId, }; use executor_storage::{ @@ -280,7 +280,7 @@ pub fn register_get_hyperliquid_signature_data< &public_key_sec1_bytes, ) .map_err(|e| { - error!("Failed to attach passkey to omni_account {}: {:?}", omni_account.to_hex(), e); + error!("Failed to attach passkey to omni_account {}: {:?}", hex_encode(omni_account.as_ref()), e); let detailed_error = match e { PasskeyError::DuplicatePasskey => { DetailedError::new(-32001, "Passkey already exists for this account") diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs index 6583b3d9ea..1e53b2961c 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs @@ -25,12 +25,10 @@ pub struct RemovePasskeyResponse { } pub fn register_remove_passkey< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( module: &mut RpcModule< - RpcContext, + RpcContext, >, ) { module diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs index 360f1b4c9a..c4aac17bcb 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs @@ -25,12 +25,10 @@ pub struct RequestPasskeyChallengeResponse { } pub fn register_request_passkey_challenge< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( module: &mut RpcModule< - RpcContext, + RpcContext, >, ) { module diff --git a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs index 1cb36857aa..fe81d64e80 100644 --- a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs @@ -5,6 +5,7 @@ use executor_primitives::{ signature::HeimaMultiSignature, utils::hex::hex_encode, Hash, Hashable, Identity, OAuth2Data, OAuth2Provider, OmniAuth, PasskeyData, VerificationCode, Web2IdentityType, }; +use executor_storage::{OAuth2StateVerifierStorage, PasskeyChallengeStorage, Storage, StorageDB, VerificationCodeStorage}; use heima_authentication::{ auth_token::{AuthTokenClaims, AuthTokenValidator, Error as AuthTokenError, Validation}, constants::AUTH_TOKEN_ID_TYPE, @@ -336,11 +337,9 @@ fn verify_id_token_claims( } pub fn verify_passkey_authentication< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - ctx: Arc>, + ctx: Arc>, passkey_data: &PasskeyData, ) -> Result<(), AuthenticationError> { use crate::methods::omni::common::{get_origin_for_client, get_rp_id_for_client}; From 17e3cdd4c757a8ea01127cc3e023e2c49bf57621 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Thu, 30 Oct 2025 07:46:39 +0100 Subject: [PATCH 08/16] fmt --- .../rpc-server/src/methods/omni/attach_passkey.rs | 8 ++------ .../src/methods/omni/get_hyperliquid_signature_data.rs | 4 +--- .../rpc-server/src/methods/omni/remove_passkey.rs | 8 ++------ .../src/methods/omni/request_passkey_challenge.rs | 4 +--- tee-worker/omni-executor/rpc-server/src/verify_auth.rs | 5 ++++- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs index 8b112c187e..570ca2f882 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs @@ -27,12 +27,8 @@ pub struct AttachPasskeyResponse { pub message: String, } -pub fn register_attach_passkey< - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_attach_passkey( + module: &mut RpcModule>, ) { module .register_async_method("omni_attachPasskey", |params, ctx, _| async move { diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs index 98247ffe2e..91f7b61140 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs @@ -10,9 +10,7 @@ use chrono::Utc; use executor_core::intent_executor::IntentExecutor; use executor_crypto::passkey::{AttestationResult, PasskeyVerifier}; use executor_primitives::{ - to_omni_auth, - utils::hex::hex_encode, - ChainId, ClientAuth, Identity, UserAuth, UserId, + to_omni_auth, utils::hex::hex_encode, ChainId, ClientAuth, Identity, UserAuth, UserId, }; use executor_storage::{ PasskeyChallengeError, PasskeyChallengeStorage, PasskeyError, PasskeyStorage, diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs index 1e53b2961c..a7ab7e97b3 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/remove_passkey.rs @@ -24,12 +24,8 @@ pub struct RemovePasskeyResponse { pub message: String, } -pub fn register_remove_passkey< - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_remove_passkey( + module: &mut RpcModule>, ) { module .register_async_method("omni_removePasskey", |params, ctx, _| async move { diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs index c4aac17bcb..7e495dacd0 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_passkey_challenge.rs @@ -27,9 +27,7 @@ pub struct RequestPasskeyChallengeResponse { pub fn register_request_passkey_challenge< CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_requestPasskeyChallenge", |params, ctx, _| async move { diff --git a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs index fe81d64e80..b7ca048572 100644 --- a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs @@ -5,7 +5,10 @@ use executor_primitives::{ signature::HeimaMultiSignature, utils::hex::hex_encode, Hash, Hashable, Identity, OAuth2Data, OAuth2Provider, OmniAuth, PasskeyData, VerificationCode, Web2IdentityType, }; -use executor_storage::{OAuth2StateVerifierStorage, PasskeyChallengeStorage, Storage, StorageDB, VerificationCodeStorage}; +use executor_storage::{ + OAuth2StateVerifierStorage, PasskeyChallengeStorage, Storage, StorageDB, + VerificationCodeStorage, +}; use heima_authentication::{ auth_token::{AuthTokenClaims, AuthTokenValidator, Error as AuthTokenError, Validation}, constants::AUTH_TOKEN_ID_TYPE, From 826ad90fce70eb59ac7a5b851aeb636848c34985 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Thu, 30 Oct 2025 12:52:31 +0100 Subject: [PATCH 09/16] move around the gitignore file --- tee-worker/omni-executor/.gitignore | 5 +++++ tee-worker/omni-executor/executor-storage/.gitignore | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 tee-worker/omni-executor/executor-storage/.gitignore diff --git a/tee-worker/omni-executor/.gitignore b/tee-worker/omni-executor/.gitignore index 092be53f11..f74f13c7c4 100644 --- a/tee-worker/omni-executor/.gitignore +++ b/tee-worker/omni-executor/.gitignore @@ -14,3 +14,8 @@ omni-executor storage_db test_get_health_storage_db test_get_shielding_key_storage_db + +executor-storage/test_passkey_*/ +executor-storage/test_passkey_integration_*/ +executor-storage/test_passkey_challenge_storage_*/ +executor-storage/test_passkey_storage_*/ diff --git a/tee-worker/omni-executor/executor-storage/.gitignore b/tee-worker/omni-executor/executor-storage/.gitignore deleted file mode 100644 index c229d0dcc1..0000000000 --- a/tee-worker/omni-executor/executor-storage/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Test database directories created by RocksDB during testing -test_passkey_*/ -test_passkey_integration_*/ -test_passkey_challenge_storage_*/ -test_passkey_storage_*/ From 7f727c74fa9dbd9d7c30777484ace02b0a36bfed Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Thu, 30 Oct 2025 13:32:23 +0100 Subject: [PATCH 10/16] refactor passkey storage key --- .../omni-executor/executor-storage/src/lib.rs | 2 +- .../executor-storage/src/passkey.rs | 146 ++++++++++++++++-- 2 files changed, 132 insertions(+), 16 deletions(-) diff --git a/tee-worker/omni-executor/executor-storage/src/lib.rs b/tee-worker/omni-executor/executor-storage/src/lib.rs index 3ed55da19c..d8e93eab3a 100644 --- a/tee-worker/omni-executor/executor-storage/src/lib.rs +++ b/tee-worker/omni-executor/executor-storage/src/lib.rs @@ -13,7 +13,7 @@ mod intent_id; pub use intent_id::IntentIdStorage; mod asset_lock; mod passkey; -pub use passkey::{PasskeyError, PasskeyRecord, PasskeyStorage}; +pub use passkey::{PasskeyError, PasskeyRecord, PasskeyStorage, PasskeyStorageKey}; mod passkey_challenge; pub use passkey_challenge::{ PasskeyChallengeError, PasskeyChallengeRecord, PasskeyChallengeStorage, diff --git a/tee-worker/omni-executor/executor-storage/src/passkey.rs b/tee-worker/omni-executor/executor-storage/src/passkey.rs index cb1f39f768..2b80f9aa5b 100644 --- a/tee-worker/omni-executor/executor-storage/src/passkey.rs +++ b/tee-worker/omni-executor/executor-storage/src/passkey.rs @@ -15,17 +15,19 @@ // along with Litentry. If not, see . use crate::{Storage, StorageDB}; -use executor_crypto::hashing::blake2_256; +use executor_crypto::hashing::{blake2_256, twox_128}; use executor_primitives::AccountId; use parity_scale_codec::{Decode, Encode}; use std::sync::Arc; const STORAGE_NAME: &str = "passkey_storage"; +/// Storage key type: (AccountId, Hash(credential_id)) +pub type PasskeyStorageKey = (AccountId, [u8; 32]); + /// Passkey data structure #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] pub struct PasskeyRecord { - pub omni_account: AccountId, pub credential_id: String, pub pubkey: Vec, // Store SEC1 bytes directly pub created_at: u64, @@ -39,7 +41,7 @@ pub enum PasskeyError { ValidationError, } -/// Simplified passkey storage using composite key of omni_account + credential_id +/// Passkey storage using composite key of (omni_account, Hash(credential_id)) pub struct PasskeyStorage { db: Arc, } @@ -49,11 +51,8 @@ impl PasskeyStorage { Self { db } } - fn generate_key(omni_account: &AccountId, credential_id: &str) -> [u8; 32] { - let mut data = Vec::new(); - data.extend_from_slice(omni_account.as_ref()); - data.extend_from_slice(credential_id.as_bytes()); - blake2_256(&data) + fn make_key(omni_account: &AccountId, credential_id: &str) -> PasskeyStorageKey { + (omni_account.clone(), blake2_256(credential_id.as_bytes())) } pub fn get_passkey( @@ -61,7 +60,7 @@ impl PasskeyStorage { omni_account: &AccountId, credential_id: &str, ) -> Result, ()> { - let key = Self::generate_key(omni_account, credential_id); + let key = Self::make_key(omni_account, credential_id); self.get(&key) } @@ -70,12 +69,12 @@ impl PasskeyStorage { omni_account: &AccountId, credential_id: &str, ) -> Result<(), PasskeyError> { - let key = Self::generate_key(omni_account, credential_id); + let key = Self::make_key(omni_account, credential_id); self.remove(&key).map_err(|_| PasskeyError::StorageError) } pub fn exists_passkey(&self, omni_account: &AccountId, credential_id: &str) -> bool { - let key = Self::generate_key(omni_account, credential_id); + let key = Self::make_key(omni_account, credential_id); self.contains_key(&key) } @@ -91,21 +90,99 @@ impl PasskeyStorage { .as_secs(); let record = PasskeyRecord { - omni_account: omni_account.clone(), credential_id: credential_id.to_string(), pubkey: pubkey.to_vec(), created_at: current_time, }; - let key = Self::generate_key(&record.omni_account, &record.credential_id); + let key = Self::make_key(omni_account, credential_id); if self.contains_key(&key) { return Err(PasskeyError::DuplicatePasskey); } self.insert(&key, record).map_err(|_| PasskeyError::StorageError) } + + /// List all passkeys for a given omni_account + /// Returns a vector of tuples (credential_id, PasskeyRecord) + pub fn list_passkeys( + &self, + omni_account: &AccountId, + ) -> Result, PasskeyError> { + let mut passkeys = Vec::new(); + + // The storage key structure is: + // storage_key = twox_128(storage_name) + blake2_128_concat(encoded_tuple_key) + // where blake2_128_concat(x) = blake2_128(x) + x + // + // For a tuple key (AccountId, [u8; 32]), the encoded form is: + // AccountId.encode() + [u8; 32].encode() + // = AccountId bytes + credential_id_hash bytes + // + // We want to match all keys for a given AccountId, so we need to construct + // a prefix that covers: + // twox_128(storage_name) + blake2_128(full_key) + AccountId.encode() + ... + // + // However, blake2_128 hashes the ENTIRE encoded key, so we can't just match + // on the AccountId portion. Instead, we iterate through ALL keys in this storage + // and filter by AccountId. + + let storage_prefix = twox_128(STORAGE_NAME.as_bytes()); + let db = self.db(); + let iter = db.prefix_iterator(&storage_prefix); + + for item in iter { + match item { + Ok((key, value)) => { + // Verify the key belongs to our storage namespace + if !key.starts_with(&storage_prefix) { + // We've moved past our namespace, stop iteration + break; + } + + // The key structure after storage_prefix is: + // blake2_128(encoded_tuple) + encoded_tuple + // where encoded_tuple = AccountId + [u8; 32] + // + // Skip the blake2_128 hash (16 bytes) to get to the encoded tuple + let encoded_tuple_start = storage_prefix.len() + 16; + if key.len() < encoded_tuple_start + 32 { + // Invalid key structure, skip + continue; + } + + // Extract the AccountId from the key (32 bytes after the hash) + let key_account_bytes = &key[encoded_tuple_start..encoded_tuple_start + 32]; + + // Compare with our target omni_account + if key_account_bytes == >::as_ref(omni_account) { + // This key belongs to our account, decode the record + match PasskeyRecord::decode(&mut &value[..]) { + Ok(record) => { + passkeys.push((record.credential_id.clone(), record)); + }, + Err(e) => { + tracing::warn!( + "Failed to decode passkey record for account {:?}: {:?}", + omni_account, + e + ); + // Continue iteration despite decode error + }, + } + } + }, + Err(e) => { + tracing::error!("Error iterating through passkeys: {:?}", e); + return Err(PasskeyError::StorageError); + }, + } + } + + Ok(passkeys) + } } -impl Storage<[u8; 32], PasskeyRecord> for PasskeyStorage { +impl Storage for PasskeyStorage { fn db(&self) -> Arc { self.db.clone() } @@ -143,7 +220,6 @@ mod tests { // Test retrieval let retrieved = storage.get_passkey(&omni_account, "cred123").unwrap().unwrap(); - assert_eq!(retrieved.omni_account, omni_account); assert_eq!(retrieved.credential_id, "cred123"); assert_eq!(retrieved.pubkey, test_pubkey.to_vec()); @@ -208,4 +284,44 @@ mod tests { assert_eq!(record1.pubkey, test_pubkey1.to_vec()); assert_eq!(record2.pubkey, test_pubkey2.to_vec()); } + + #[test] + fn test_list_passkeys() { + let storage = create_test_storage(); + let omni_account1 = AccountId::from([5u8; 32]); + let omni_account2 = AccountId::from([6u8; 32]); + + // Add multiple passkeys for account1 + let test_pubkey1 = b"test_pubkey_1"; + let test_pubkey2 = b"test_pubkey_2"; + let test_pubkey3 = b"test_pubkey_3"; + storage.add_passkey(&omni_account1, "cred1", test_pubkey1).unwrap(); + storage.add_passkey(&omni_account1, "cred2", test_pubkey2).unwrap(); + storage.add_passkey(&omni_account1, "cred3", test_pubkey3).unwrap(); + + // Add a passkey for account2 + let test_pubkey4 = b"test_pubkey_4"; + storage.add_passkey(&omni_account2, "cred4", test_pubkey4).unwrap(); + + // List passkeys for account1 + let passkeys1 = storage.list_passkeys(&omni_account1).unwrap(); + assert_eq!(passkeys1.len(), 3); + + // Verify all three passkeys are present + let cred_ids: Vec = passkeys1.iter().map(|(cid, _)| cid.clone()).collect(); + assert!(cred_ids.contains(&"cred1".to_string())); + assert!(cred_ids.contains(&"cred2".to_string())); + assert!(cred_ids.contains(&"cred3".to_string())); + + // List passkeys for account2 + let passkeys2 = storage.list_passkeys(&omni_account2).unwrap(); + assert_eq!(passkeys2.len(), 1); + assert_eq!(passkeys2[0].0, "cred4"); + assert_eq!(passkeys2[0].1.pubkey, test_pubkey4.to_vec()); + + // List passkeys for non-existent account + let omni_account3 = AccountId::from([7u8; 32]); + let passkeys3 = storage.list_passkeys(&omni_account3).unwrap(); + assert_eq!(passkeys3.len(), 0); + } } From a4b4b6b7f6ad021d05fb6d252d672e339a3e4246 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Thu, 30 Oct 2025 21:49:36 +0100 Subject: [PATCH 11/16] use challenge as storage key directly --- .../executor-storage/src/passkey_challenge.rs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs b/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs index 201c217ee7..59820091e3 100644 --- a/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs +++ b/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs @@ -24,7 +24,6 @@ const STORAGE_NAME: &str = "passkey_challenge_storage"; /// Passkey challenge record with expiration #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] pub struct PasskeyChallengeRecord { - pub challenge: String, pub omni_account: AccountId, pub created_at: u64, pub expires_at: u64, @@ -61,15 +60,12 @@ impl PasskeyChallengeStorage { .as_secs(); let record = PasskeyChallengeRecord { - challenge: challenge.to_string(), omni_account: omni_account.clone(), created_at: current_time, expires_at: current_time + timeout_seconds, }; - let challenge_key = challenge.as_bytes(); - self.insert(&challenge_key, record) - .map_err(|_| PasskeyChallengeError::StorageError) + self.insert(&challenge, record).map_err(|_| PasskeyChallengeError::StorageError) } pub fn verify_and_consume_challenge( @@ -77,10 +73,8 @@ impl PasskeyChallengeStorage { challenge: &str, omni_account: &AccountId, ) -> Result<(), PasskeyChallengeError> { - let challenge_key = challenge.as_bytes(); - let record = self - .get(&challenge_key) + .get(&challenge) .map_err(|_| PasskeyChallengeError::StorageError)? .ok_or(PasskeyChallengeError::ChallengeNotFound)?; @@ -94,11 +88,11 @@ impl PasskeyChallengeStorage { .as_secs(); if current_time > record.expires_at { - self.remove(&challenge_key).map_err(|_| PasskeyChallengeError::StorageError)?; + self.remove(&challenge).map_err(|_| PasskeyChallengeError::StorageError)?; return Err(PasskeyChallengeError::ChallengeExpired); } - self.remove(&challenge_key).map_err(|_| PasskeyChallengeError::StorageError)?; + self.remove(&challenge).map_err(|_| PasskeyChallengeError::StorageError)?; self.cleanup_expired24h_challenges()?; Ok(()) @@ -192,9 +186,7 @@ impl PasskeyChallengeStorage { challenge: &str, omni_account: &AccountId, ) -> Result { - let challenge_key = challenge.as_bytes(); - - let record = self.get(&challenge_key).map_err(|_| PasskeyChallengeError::StorageError)?; + let record = self.get(&challenge).map_err(|_| PasskeyChallengeError::StorageError)?; match record { Some(record) => { @@ -216,7 +208,7 @@ impl PasskeyChallengeStorage { } } -impl Storage<&[u8], PasskeyChallengeRecord> for PasskeyChallengeStorage { +impl Storage<&str, PasskeyChallengeRecord> for PasskeyChallengeStorage { fn db(&self) -> Arc { self.db.clone() } From 7f7e8ee8a4c862f1e3b5ab8b507f0d1e18d21b2c Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Thu, 30 Oct 2025 21:49:41 +0100 Subject: [PATCH 12/16] clippy --- tee-worker/omni-executor/executor-storage/src/passkey.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee-worker/omni-executor/executor-storage/src/passkey.rs b/tee-worker/omni-executor/executor-storage/src/passkey.rs index 2b80f9aa5b..f7e368e9d1 100644 --- a/tee-worker/omni-executor/executor-storage/src/passkey.rs +++ b/tee-worker/omni-executor/executor-storage/src/passkey.rs @@ -128,7 +128,7 @@ impl PasskeyStorage { let storage_prefix = twox_128(STORAGE_NAME.as_bytes()); let db = self.db(); - let iter = db.prefix_iterator(&storage_prefix); + let iter = db.prefix_iterator(storage_prefix); for item in iter { match item { From 90e969200f225981d809583cf2758576eccd307e Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Thu, 30 Oct 2025 22:19:03 +0100 Subject: [PATCH 13/16] spawn independent thread to clean up --- .../executor-storage/src/passkey_challenge.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs b/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs index 59820091e3..3aa37ef889 100644 --- a/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs +++ b/tee-worker/omni-executor/executor-storage/src/passkey_challenge.rs @@ -39,6 +39,7 @@ pub enum PasskeyChallengeError { } /// PassKey challenge storage with expiration management +#[derive(Clone)] pub struct PasskeyChallengeStorage { db: Arc, } @@ -93,7 +94,17 @@ impl PasskeyChallengeStorage { } self.remove(&challenge).map_err(|_| PasskeyChallengeError::StorageError)?; - self.cleanup_expired24h_challenges()?; + + let cleanup_storage = self.clone(); + + let _ = std::thread::spawn(move || { + if let Err(e) = cleanup_storage.cleanup_expired24h_challenges() { + tracing::error!( + "Failed to cleanup expired passkey challenges after verification: {:?}", + e + ); + } + }); Ok(()) } From 94e25a974775d742abe60c7524374fe866cd7b6f Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Thu, 30 Oct 2025 22:49:21 +0100 Subject: [PATCH 14/16] refactor error handling --- .../rpc-server/src/detailed_error.rs | 26 +++++-- .../rpc-server/src/error_code.rs | 2 + .../src/methods/omni/attach_passkey.rs | 78 +++++++++++++------ .../omni/get_hyperliquid_signature_data.rs | 52 ++++++------- 4 files changed, 102 insertions(+), 56 deletions(-) diff --git a/tee-worker/omni-executor/rpc-server/src/detailed_error.rs b/tee-worker/omni-executor/rpc-server/src/detailed_error.rs index 3555328969..9484ca2cb0 100644 --- a/tee-worker/omni-executor/rpc-server/src/detailed_error.rs +++ b/tee-worker/omni-executor/rpc-server/src/detailed_error.rs @@ -4,11 +4,12 @@ use crate::error_code::{ INVALID_HEX_FORMAT_CODE, INVALID_USER_OPERATION_CODE, INVALID_WALLET_INDEX_CODE, PASSKEY_ALREADY_EXISTS_CODE, PASSKEY_ATTESTATION_PARSE_ERROR_CODE, PASSKEY_CHALLENGE_EXPIRED_CODE, PASSKEY_CHALLENGE_NOT_FOUND_CODE, - PASSKEY_COUNTER_VALIDATION_FAILED_CODE, PASSKEY_INVALID_CHALLENGE_CODE, PASSKEY_NOT_FOUND_CODE, - PASSKEY_PARSE_ERROR_CODE, PASSKEY_REPLAY_ATTACK_CODE, PASSKEY_RP_ID_MISMATCH_CODE, - PASSKEY_SIGNATURE_INVALID_CODE, PASSKEY_USER_VERIFICATION_FAILED_CODE, - SIGNATURE_SERVICE_UNAVAILABLE_CODE, SIGNER_SERVICE_ERROR_CODE, STORAGE_SERVICE_ERROR_CODE, - UNEXPECTED_RESPONSE_TYPE_CODE, + PASSKEY_CLIENT_DATA_PARSE_ERROR_CODE, PASSKEY_COUNTER_VALIDATION_FAILED_CODE, + PASSKEY_INVALID_CHALLENGE_CODE, PASSKEY_NOT_FOUND_CODE, + PASSKEY_ORIGIN_VERIFICATION_FAILED_CODE, PASSKEY_PARSE_ERROR_CODE, PASSKEY_REPLAY_ATTACK_CODE, + PASSKEY_RP_ID_MISMATCH_CODE, PASSKEY_SIGNATURE_INVALID_CODE, + PASSKEY_USER_VERIFICATION_FAILED_CODE, SIGNATURE_SERVICE_UNAVAILABLE_CODE, + SIGNER_SERVICE_ERROR_CODE, STORAGE_SERVICE_ERROR_CODE, UNEXPECTED_RESPONSE_TYPE_CODE, }; use jsonrpsee::types::ErrorObject; use serde::{Deserialize, Serialize}; @@ -284,4 +285,19 @@ impl DetailedError { .with_reason("A passkey with this credential ID already exists for this account") .with_suggestion("Use the existing passkey or remove it before registering a new one") } + + pub fn passkey_origin_verification_failed(expected_origin: &str) -> Self { + Self::new(PASSKEY_ORIGIN_VERIFICATION_FAILED_CODE, "Origin verification failed") + .with_field("client_data_json") + .with_expected(format!("Origin: {}", expected_origin)) + .with_reason("Origin in client data does not match expected value") + .with_suggestion("Ensure the WebAuthn ceremony is initiated from the correct origin") + } + + pub fn passkey_client_data_parse_error(error: &str) -> Self { + Self::new(PASSKEY_CLIENT_DATA_PARSE_ERROR_CODE, "Failed to parse client data JSON") + .with_field("client_data_json") + .with_reason(error) + .with_suggestion("Ensure the client data is properly base64url-encoded JSON") + } } diff --git a/tee-worker/omni-executor/rpc-server/src/error_code.rs b/tee-worker/omni-executor/rpc-server/src/error_code.rs index 3bf4cf9548..46d72d547a 100644 --- a/tee-worker/omni-executor/rpc-server/src/error_code.rs +++ b/tee-worker/omni-executor/rpc-server/src/error_code.rs @@ -71,6 +71,8 @@ pub const PASSKEY_COUNTER_VALIDATION_FAILED_CODE: i32 = -32148; pub const PASSKEY_USER_VERIFICATION_FAILED_CODE: i32 = -32149; pub const PASSKEY_ATTESTATION_PARSE_ERROR_CODE: i32 = -32150; pub const PASSKEY_ALREADY_EXISTS_CODE: i32 = -32151; +pub const PASSKEY_ORIGIN_VERIFICATION_FAILED_CODE: i32 = -32152; +pub const PASSKEY_CLIENT_DATA_PARSE_ERROR_CODE: i32 = -32153; // External Service Error Codes (-32160 to -32179) pub const SIGNER_SERVICE_ERROR_CODE: i32 = -32160; diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs index 570ca2f882..143353f03a 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs @@ -1,5 +1,6 @@ use crate::{ - error_code::*, server::RpcContext, verify_auth::verify_auth, Deserialize, ErrorCode, Serialize, + detailed_error::DetailedError, error_code::*, server::RpcContext, verify_auth::verify_auth, + Deserialize, ErrorCode, Serialize, }; use executor_core::intent_executor::IntentExecutor; @@ -71,16 +72,19 @@ pub fn register_attach_passkey { - error!("Challenge not found"); + error!("Challenge not found for passkey attachment"); }, PasskeyChallengeError::ChallengeExpired => { - error!("Challenge expired"); + error!("Challenge expired for passkey attachment"); }, PasskeyChallengeError::InvalidChallenge => { - error!("Invalid challenge"); + error!("Invalid challenge for passkey attachment"); }, _ => { - error!("Challenge verification failed: {:?}", e); + error!( + "Challenge verification failed during passkey attachment: {:?}", + e + ); }, } executor_crypto::passkey::PasskeyError::ChallengeVerificationFailed @@ -88,45 +92,71 @@ pub fn register_attach_passkey { - ErrorCode::ServerError(-32011) // Challenge mismatch + DetailedError::passkey_invalid_challenge( + "Challenge mismatch, expired, or not found", + ) }, executor_crypto::passkey::PasskeyError::OriginVerificationFailed => { - ErrorCode::ServerError(-32012) // Origin mismatch + DetailedError::passkey_origin_verification_failed(expected_origin) }, - executor_crypto::passkey::PasskeyError::AttestationParseError(_) => { - ErrorCode::ServerError(-32013) // Attestation parse error + executor_crypto::passkey::PasskeyError::AttestationParseError(err) => { + DetailedError::passkey_client_data_parse_error(&err) }, - _ => ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE), + _ => DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Client data verification failed", + ) + .with_field("client_data_json") + .with_reason(format!("Verification error: {:?}", e)), } + .to_error_object() })?; let AttestationResult { credential_id, public_key } = PasskeyVerifier::verify_attestation(¶ms.attestation_object).map_err(|e| { - error!("WebAuthn attestation verification failed: {:?}", e); + error!( + "WebAuthn attestation verification failed during passkey attachment: {:?}", + e + ); match e { - executor_crypto::passkey::PasskeyError::AttestationParseError(_) => { - ErrorCode::ServerError(-32013) // Attestation parse error + executor_crypto::passkey::PasskeyError::AttestationParseError(err) => { + DetailedError::passkey_attestation_parse_error(&err) }, - _ => ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE), + _ => DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Attestation verification failed", + ) + .with_field("attestation_object") + .with_reason(format!("Verification error: {:?}", e)), } + .to_error_object() })?; let public_key_sec1_bytes = public_key.verifying_key.to_sec1_bytes(); let passkey_storage = PasskeyStorage::new(ctx.storage_db.clone()); passkey_storage .add_passkey(&omni_account, &credential_id, &public_key_sec1_bytes) - .map_err(|e| match e { - PasskeyError::DuplicatePasskey => { - error!("Duplicate passkey (same omni_account + credential_id)"); - ErrorCode::ServerError(-32001) - }, - _ => { - error!("Failed to store passkey: {:?}", e); - ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE) - }, + .map_err(|e| { + error!( + "Failed to attach passkey to omni_account {}: {:?}", + hex_encode(omni_account.as_ref()), + e + ); + match e { + PasskeyError::DuplicatePasskey => { + DetailedError::passkey_already_exists(&credential_id) + }, + PasskeyError::StorageError => { + DetailedError::storage_error("passkey attachment") + }, + _ => DetailedError::new(INTERNAL_ERROR_CODE, "Failed to attach passkey") + .with_reason(format!("Storage error: {:?}", e)) + .with_suggestion("Please try again later"), + } + .to_error_object() })?; Ok::(AttachPasskeyResponse { diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs index 91f7b61140..9b480f4fe5 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs @@ -221,26 +221,25 @@ pub fn register_get_hyperliquid_signature_data< error!("Client data verification failed during passkey attachment: {:?}", e); match e { executor_crypto::passkey::PasskeyError::ChallengeVerificationFailed => { - DetailedError::new(-32011, "Challenge verification failed") - .with_field("attach_passkey.client_data_json") - .with_reason("Challenge mismatch or not found") - .with_suggestion("Request a new challenge via omni_requestPasskeyChallenge and try again") + DetailedError::passkey_invalid_challenge( + "Challenge mismatch, expired, or not found", + ) + .with_field("attach_passkey.client_data_json") }, executor_crypto::passkey::PasskeyError::OriginVerificationFailed => { - DetailedError::new(-32012, "Origin verification failed") + DetailedError::passkey_origin_verification_failed(expected_origin) .with_field("attach_passkey.client_data_json") - .with_reason("Origin does not match expected value") - .with_suggestion("Ensure the WebAuthn ceremony is initiated from the correct origin") }, - executor_crypto::passkey::PasskeyError::AttestationParseError(_) => { - DetailedError::new(-32013, "Client data parse error") + executor_crypto::passkey::PasskeyError::AttestationParseError(err) => { + DetailedError::passkey_client_data_parse_error(&err) .with_field("attach_passkey.client_data_json") - .with_reason("Failed to parse client data JSON") - .with_suggestion("Ensure the client data is base64url encoded JSON") }, - _ => DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Client data verification failed") - .with_field("attach_passkey.client_data_json") - .with_reason(format!("Verification error: {:?}", e)) + _ => DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Client data verification failed", + ) + .with_field("attach_passkey.client_data_json") + .with_reason(format!("Verification error: {:?}", e)), } .to_error_object() })?; @@ -252,15 +251,16 @@ pub fn register_get_hyperliquid_signature_data< .map_err(|e| { error!("WebAuthn attestation verification failed during passkey attachment: {:?}", e); match e { - executor_crypto::passkey::PasskeyError::AttestationParseError(_) => { - DetailedError::new(-32013, "Attestation parse error") + executor_crypto::passkey::PasskeyError::AttestationParseError(err) => { + DetailedError::passkey_attestation_parse_error(&err) .with_field("attach_passkey.attestation_object") - .with_reason("Failed to parse attestation object") - .with_suggestion("Ensure the attestation object is base64url encoded CBOR") }, - _ => DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Attestation verification failed") - .with_field("attach_passkey.attestation_object") - .with_reason(format!("Verification error: {:?}", e)) + _ => DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Attestation verification failed", + ) + .with_field("attach_passkey.attestation_object") + .with_reason(format!("Verification error: {:?}", e)), } .to_error_object() })?; @@ -281,18 +281,16 @@ pub fn register_get_hyperliquid_signature_data< error!("Failed to attach passkey to omni_account {}: {:?}", hex_encode(omni_account.as_ref()), e); let detailed_error = match e { PasskeyError::DuplicatePasskey => { - DetailedError::new(-32001, "Passkey already exists for this account") + DetailedError::passkey_already_exists(&credential_id) .with_field("attach_passkey") - .with_reason(format!("A passkey with credential_id '{}' is already registered to this account", credential_id)) - .with_suggestion("This credential is already attached. Use a different credential or remove the existing one first with omni_removePasskey") }, PasskeyError::StorageError => { - DetailedError::new(INTERNAL_ERROR_CODE, "Failed to store passkey") - .with_reason("Database storage operation failed") - .with_suggestion("Please try again later or contact support if the issue persists") + DetailedError::storage_error("passkey attachment") + .with_field("attach_passkey") }, _ => { DetailedError::new(INTERNAL_ERROR_CODE, "Failed to attach passkey") + .with_field("attach_passkey") .with_reason(format!("Storage error: {:?}", e)) .with_suggestion("Please try again later") } From 895c30ce4fb74610e370aed356378db36ade089b Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Mon, 3 Nov 2025 11:31:17 +0100 Subject: [PATCH 15/16] add `omni_listPasskey` rpc call --- .../executor-storage/src/passkey.rs | 7 +- .../src/methods/omni/list_passkey.rs | 73 +++++++++++++++++++ .../rpc-server/src/methods/omni/mod.rs | 4 + 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 tee-worker/omni-executor/rpc-server/src/methods/omni/list_passkey.rs diff --git a/tee-worker/omni-executor/executor-storage/src/passkey.rs b/tee-worker/omni-executor/executor-storage/src/passkey.rs index f7e368e9d1..c262e6f4ed 100644 --- a/tee-worker/omni-executor/executor-storage/src/passkey.rs +++ b/tee-worker/omni-executor/executor-storage/src/passkey.rs @@ -103,11 +103,11 @@ impl PasskeyStorage { } /// List all passkeys for a given omni_account - /// Returns a vector of tuples (credential_id, PasskeyRecord) + /// Returns a vector of tuples (credential_id, created_at) pub fn list_passkeys( &self, omni_account: &AccountId, - ) -> Result, PasskeyError> { + ) -> Result, PasskeyError> { let mut passkeys = Vec::new(); // The storage key structure is: @@ -158,7 +158,7 @@ impl PasskeyStorage { // This key belongs to our account, decode the record match PasskeyRecord::decode(&mut &value[..]) { Ok(record) => { - passkeys.push((record.credential_id.clone(), record)); + passkeys.push((record.credential_id.clone(), record.created_at)); }, Err(e) => { tracing::warn!( @@ -317,7 +317,6 @@ mod tests { let passkeys2 = storage.list_passkeys(&omni_account2).unwrap(); assert_eq!(passkeys2.len(), 1); assert_eq!(passkeys2[0].0, "cred4"); - assert_eq!(passkeys2[0].1.pubkey, test_pubkey4.to_vec()); // List passkeys for non-existent account let omni_account3 = AccountId::from([7u8; 32]); diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/list_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/list_passkey.rs new file mode 100644 index 0000000000..068a3c3a62 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/list_passkey.rs @@ -0,0 +1,73 @@ +use crate::{ + detailed_error::DetailedError, server::RpcContext, verify_auth::verify_auth, Deserialize, + ErrorCode, Serialize, +}; + +use executor_core::intent_executor::IntentExecutor; +use executor_primitives::{to_omni_auth, UserAuth, UserId}; +use executor_storage::PasskeyStorage; +use heima_primitives::Identity; +use jsonrpsee::{types::ErrorObject, RpcModule}; +use tracing::error; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ListPasskeyParams { + pub user_id: UserId, + pub user_auth: UserAuth, + pub client_id: String, +} + +#[derive(Serialize, Clone)] +pub struct PasskeyInfo { + pub credential_id: String, + pub created_at: u64, +} + +#[derive(Serialize, Clone)] +pub struct ListPasskeyResponse { + pub passkeys: Vec, +} + +pub fn register_list_passkey( + module: &mut RpcModule>, +) { + module + .register_async_method("omni_listPasskey", |params, ctx, _| async move { + let params = params.parse::().map_err(|e| { + error!("Failed to parse params: {:?}", e); + ErrorCode::ParseError + })?; + + let identity = Identity::try_from(params.user_id.clone()).map_err(|_| { + error!("Invalid user ID format"); + ErrorCode::ParseError + })?; + + let auth = to_omni_auth(¶ms.user_auth, ¶ms.user_id, ¶ms.client_id) + .map_err(|e| { + error!("Failed to convert to OmniAuth: {:?}", e); + ErrorCode::ParseError + })?; + + verify_auth(ctx.clone(), &auth).await.map_err(|e| { + error!("Failed to verify user authentication: {:?}", e); + e.to_detailed_error().to_error_object() + })?; + + let omni_account = identity.to_omni_account(¶ms.client_id); + let passkey_storage = PasskeyStorage::new(ctx.storage_db.clone()); + + let passkeys = passkey_storage.list_passkeys(&omni_account).map_err(|e| { + error!("Failed to list passkeys: {:?}", e); + DetailedError::storage_error("passkey listing").to_error_object() + })?; + + let passkey_infos = passkeys + .into_iter() + .map(|(credential_id, created_at)| PasskeyInfo { credential_id, created_at }) + .collect(); + + Ok::(ListPasskeyResponse { passkeys: passkey_infos }) + }) + .expect("Failed to register omni_listPasskey method"); +} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs index d6c4ee7492..0e78546e66 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs @@ -77,6 +77,9 @@ use attach_passkey::*; mod remove_passkey; use remove_passkey::*; +mod list_passkey; +use list_passkey::*; + mod request_passkey_challenge; use request_passkey_challenge::*; @@ -131,6 +134,7 @@ pub fn register_omni Date: Tue, 4 Nov 2025 10:02:06 +0100 Subject: [PATCH 16/16] add debug info --- .../rpc-server/src/methods/omni/attach_passkey.rs | 4 +++- .../rpc-server/src/methods/omni/remove_passkey.rs | 4 +++- .../rpc-server/src/methods/omni/request_passkey_challenge.rs | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs index 143353f03a..028b419155 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs @@ -11,7 +11,7 @@ use executor_storage::{ }; use heima_primitives::Identity; use jsonrpsee::{types::ErrorObject, RpcModule}; -use tracing::error; +use tracing::*; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct AttachPasskeyParams { @@ -38,6 +38,8 @@ pub fn register_attach_passkey