diff --git a/Cargo.lock b/Cargo.lock index 8d5b2ef3..d83bd2da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -913,7 +913,7 @@ dependencies = [ [[package]] name = "ark-zkey" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" dependencies = [ "ark-bn254", "ark-circom", @@ -2514,7 +2514,7 @@ dependencies = [ [[package]] name = "hasher" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" dependencies = [ "bytemuck", ] @@ -3030,7 +3030,7 @@ dependencies = [ [[package]] name = "keccak" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" dependencies = [ "hasher", "tiny-keccak", @@ -3697,7 +3697,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "poseidon" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" dependencies = [ "ark-bn254", "ark-ff 0.4.2", @@ -4309,9 +4309,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.17" +version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ "once_cell", "ring", @@ -4534,7 +4534,7 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "semaphore" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" dependencies = [ "alloy-core", "ark-bn254", @@ -4577,12 +4577,12 @@ dependencies = [ [[package]] name = "semaphore-depth-config" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" [[package]] name = "semaphore-depth-macros" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" dependencies = [ "itertools 0.13.0", "proc-macro2", @@ -4875,7 +4875,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "storage" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" dependencies = [ "bytemuck", "color-eyre", @@ -5458,7 +5458,7 @@ dependencies = [ [[package]] name = "trees" version = "0.1.0" -source = "git+https://github.com/worldcoin/semaphore-rs?rev=accb14b#accb14bd6b8f3202d988182c5ef822104d262f39" +source = "git+https://github.com/worldcoin/semaphore-rs?rev=f266248#f266248b5695c3528a27f800c36d51580ee2dc7e" dependencies = [ "ark-bn254", "ark-circom", @@ -5952,6 +5952,7 @@ dependencies = [ "ruint", "semaphore", "serde_json", + "strum", "thiserror 2.0.3", "tokio", "uniffi", diff --git a/cspell.json b/cspell.json index 0d89a06c..96738081 100644 --- a/cspell.json +++ b/cspell.json @@ -12,7 +12,8 @@ "WalletKit", "Worldcoin", "xcframework", - "Zeroize" + "Zeroize", + "Zeroizes" ], "enabledLanguageIds": [ "jsonc", diff --git a/walletkit-core/Cargo.toml b/walletkit-core/Cargo.toml index 4921c609..85ddeb82 100644 --- a/walletkit-core/Cargo.toml +++ b/walletkit-core/Cargo.toml @@ -24,8 +24,9 @@ name = "walletkit_core" alloy-core = { version = "0.8.12", default-features = false, features = ["sol-types"] } hex = "0.4.3" ruint = { version = "1.12.3", default-features = false, features = ["alloc"] } -semaphore = { git = "https://github.com/worldcoin/semaphore-rs", rev = "accb14b", features = ["depth_30"] } +semaphore = { git = "https://github.com/worldcoin/semaphore-rs", rev = "f266248", features = ["depth_30"] } serde_json = "1.0.133" +strum = { version = "0.26", features = ["derive"] } thiserror = "2.0.3" uniffi = { workspace = true, features = ["build"] } diff --git a/walletkit-core/src/credential_type.rs b/walletkit-core/src/credential_type.rs new file mode 100644 index 00000000..e2afadd3 --- /dev/null +++ b/walletkit-core/src/credential_type.rs @@ -0,0 +1,31 @@ +use strum::EnumString; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Object, EnumString, Hash)] +#[strum(serialize_all = "snake_case")] +pub enum CredentialType { + Orb, + Passport, + SecurePassport, + Device, +} + +impl CredentialType { + /// Returns a predefined seed string which is used to derive the identity commitment. + /// + /// [Protocol Reference](https://docs.semaphore.pse.dev/V2/technical-reference/circuits#proof-of-membership). + /// + /// For usage reference, review [sempahore-rs](https://github.com/worldcoin/semaphore-rs/blob/main/src/identity.rs#L44). + /// + /// - For `Orb`, it's a fixed legacy default value. Changing this default would break existing verifying apps, hence its explicit specification here. + /// - `Passport` (NFC-based check on government-issued passport) + /// - `SecurePassport` (NFC-based check on government-issued passport with additional chip authentication checks) + #[must_use] + pub const fn as_identity_trapdoor(&self) -> &[u8] { + match self { + Self::Orb => b"identity_trapdoor", + Self::Device => b"phone_credential", + Self::Passport => b"passport", + Self::SecurePassport => b"secure_passport", + } + } +} diff --git a/walletkit-core/src/identity.rs b/walletkit-core/src/identity.rs index 47e84368..3bf398f0 100644 --- a/walletkit-core/src/identity.rs +++ b/walletkit-core/src/identity.rs @@ -1,14 +1,24 @@ -use semaphore::protocol::generate_nullifier_hash; +use semaphore::{identity::seed_hex, protocol::generate_nullifier_hash}; -use crate::{proof::Context, u256::U256Wrapper}; +use crate::{credential_type::CredentialType, proof::Context, u256::U256Wrapper}; +/// A base World ID identity which can be used to generate World ID Proofs for different credentials. +/// +/// Most essential primitive for World ID. +/// +/// # Security +/// TODO: Review with Security Team +/// 1. `sempahore-rs` zeroizes the bytes representing the World ID Secret and stores the trapdoor and nullifier in memory. This doesn't +/// add too much additional security versus keeping the secret in memory because for the context of Semaphore ZKPs, the nullifier and +/// trapdoor are what is actually used in the ZK circuit. +/// 2. Zeroize does not have good compatibility with `UniFFI` as `UniFFI` may make many copies of the bytes for usage in foreign code +/// ([reference](https://github.com/mozilla/uniffi-rs/issues/2080)). This needs to be further explored. #[derive(Clone, PartialEq, Eq, Debug, uniffi::Object)] -pub struct Identity(pub semaphore::identity::Identity); - -impl From for semaphore::identity::Identity { - fn from(identity: Identity) -> Self { - identity.0 - } +pub struct Identity { + /// The Semaphore-based identity specifically for the `CredentialType::Orb` + canonical_orb_semaphore_identity: semaphore::identity::Identity, + /// The hashed World ID secret, cast to 64 bytes (0-padded). Actual hashed secret is 32 bytes. + secret_hex: [u8; 64], } #[uniffi::export] @@ -16,10 +26,17 @@ impl Identity { #[must_use] #[uniffi::constructor] pub fn new(secret: &[u8]) -> Self { + let secret_hex = seed_hex(secret); + let mut secret_key = secret.to_vec(); - let identity = + + let canonical_orb_semaphore_identity = semaphore::identity::Identity::from_secret(&mut secret_key, None); - Self(identity) + + Self { + canonical_orb_semaphore_identity, + secret_hex, + } } /// Generates a nullifier hash for a particular context (i.e. app + action) and the identity. @@ -29,19 +46,109 @@ impl Identity { /// [Protocol Reference](https://docs.semaphore.pse.dev/V2/technical-reference/circuits#nullifier-hash). #[must_use] pub fn generate_nullifier_hash(&self, context: &Context) -> U256Wrapper { - generate_nullifier_hash(&self.0, *context.external_nullifier).into() + let identity = self.semaphore_identity_for_credential(&context.credential_type); + generate_nullifier_hash(&identity, *context.external_nullifier).into() + } + + /// Generates the `identity_commitment` for a specific World ID identity and for a specific credential. + /// For the same World ID, each credential will generate a different `identity_commitment` for privacy reasons. This is + /// accomplished by using a different `identity_trapdoor` internally. + /// + /// The identity commitment is the public part of a World ID. It is what gets inserted into the membership set on-chain. Identity commitments + /// are not directly used in proof verification. + #[must_use] + pub fn get_identity_commitment( + &self, + credential_type: &CredentialType, + ) -> U256Wrapper { + let identity = self.semaphore_identity_for_credential(credential_type); + identity.commitment().into() + } +} + +impl Identity { + /// Retrieves the Semaphore identity for a specific `CredentialType` from memory or by computing it on the spot. + #[must_use] + #[allow(clippy::trivially_copy_pass_by_ref)] + fn semaphore_identity_for_credential( + &self, + credential_type: &CredentialType, + ) -> semaphore::identity::Identity { + if credential_type == &CredentialType::Orb { + self.canonical_orb_semaphore_identity.clone() + } else { + // When the identity commitment for the non-canonical identity is requested, a new Semaphore identity needs to be initialized. + let mut secret_hex = self.secret_hex; + let identity = semaphore::identity::Identity::from_hashed_secret( + &mut secret_hex, + Some(credential_type.as_identity_trapdoor()), + ); + identity + } } } #[cfg(test)] mod tests { + use ruint::uint; + use std::sync::Arc; + use super::*; #[test] fn test() { let identity = Identity::new(b"not_a_real_secret"); - let context = Context::new("app_id", None); + let context = Context::new("app_id", None, Arc::new(CredentialType::Orb)); let nullifier_hash = identity.generate_nullifier_hash(&context); println!("{}", nullifier_hash.to_hex_string()); } + + #[test] + fn test_secret_hex_generation() { + let identity = Identity::new(b"not_a_real_secret"); + + // this is the expected SHA-256 of the secret (computed externally) + let expected_hash: U256Wrapper = uint!(88026203285206540949013074047154212280150971633012190779810764227609557184952_U256).into(); + + let bytes = expected_hash.to_hex_string(); + + let mut result = [0_u8; 64]; + result[..].copy_from_slice(&bytes.as_bytes()[2..]); // we slice the first 2 chars to remove the 0x + + assert_eq!(result, identity.secret_hex); + } + + #[test] + fn test_identity_commitment_generation() { + let identity = Identity::new(b"not_a_real_secret"); + let commitment = identity.get_identity_commitment(&CredentialType::Orb); + + assert_eq!( + *commitment, + uint!( + 0x000352340ece4a3509b5a053118e289300e9e9677d135ae1a625219a10923a7e_U256 + ) + ); + + let secure_passport_commitment = + identity.get_identity_commitment(&CredentialType::SecurePassport); + + assert_eq!( + *secure_passport_commitment, + uint!( + 4772776030911288417155544975787646998508849894109450205303839917538446765610_U256 + ) + ); + + let semaphore_identity = semaphore::identity::Identity::from_secret( + &mut b"not_a_real_secret".to_vec(), + Some(b"secure_passport"), + ); + assert_eq!(semaphore_identity.commitment(), *secure_passport_commitment); + + let device_commitment = + identity.get_identity_commitment(&CredentialType::Device); + + assert!(device_commitment != commitment); + } } diff --git a/walletkit-core/src/lib.rs b/walletkit-core/src/lib.rs index 80089f1f..19cd7b74 100644 --- a/walletkit-core/src/lib.rs +++ b/walletkit-core/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::all, clippy::pedantic, clippy::nursery)] +pub mod credential_type; pub mod error; pub mod identity; pub mod proof; diff --git a/walletkit-core/src/proof.rs b/walletkit-core/src/proof.rs index e2e7487f..f46c9f17 100644 --- a/walletkit-core/src/proof.rs +++ b/walletkit-core/src/proof.rs @@ -1,7 +1,9 @@ +use std::sync::Arc; + use alloy_core::sol_types::SolValue; use semaphore::hash_to_field; -use crate::u256::U256Wrapper; +use crate::{credential_type::CredentialType, u256::U256Wrapper}; /// A `Proof::Context` contains the basic information on the verifier and the specific action a user will be proving. /// @@ -9,6 +11,7 @@ use crate::u256::U256Wrapper; #[derive(Clone, PartialEq, Eq, Debug, uniffi::Object)] pub struct Context { pub external_nullifier: U256Wrapper, + pub credential_type: CredentialType, } #[uniffi::export] @@ -26,8 +29,16 @@ impl Context { /// #[must_use] #[uniffi::constructor] - pub fn new(app_id: &str, action: Option) -> Self { - Self::new_from_bytes(app_id, action.map(std::string::String::into_bytes)) + pub fn new( + app_id: &str, + action: Option, + credential_type: Arc, + ) -> Self { + Self::new_from_bytes( + app_id, + action.map(std::string::String::into_bytes), + credential_type, + ) } /// Initializes a `Proof::Context` where the `action` is provided as raw bytes. This is useful for advanced cases @@ -43,7 +54,12 @@ impl Context { /// #[must_use] #[uniffi::constructor] - pub fn new_from_bytes(app_id: &str, action: Option>) -> Self { + #[allow(clippy::needless_pass_by_value)] + pub fn new_from_bytes( + app_id: &str, + action: Option>, + credential_type: Arc, + ) -> Self { let mut pre_image = hash_to_field(app_id.as_bytes()).abi_encode_packed(); if let Some(action) = action { @@ -52,7 +68,10 @@ impl Context { let external_nullifier = hash_to_field(&pre_image).into(); - Self { external_nullifier } + Self { + external_nullifier, + credential_type: *credential_type, + } } } @@ -65,15 +84,22 @@ mod tests { #[test] fn test_context_and_external_nullifier_hash_generation() { - let context = Context::new("app_369183bd38f1641b6964ab51d7a20434", None); + let context = Context::new( + "app_369183bd38f1641b6964ab51d7a20434", + None, + Arc::new(CredentialType::Orb), + ); assert_eq!( context.external_nullifier.to_hex_string(), "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227" ); // note the same external nullifier hash is generated for an empty string action - let context = - Context::new("app_369183bd38f1641b6964ab51d7a20434", Some(String::new())); + let context = Context::new( + "app_369183bd38f1641b6964ab51d7a20434", + Some(String::new()), + Arc::new(CredentialType::Orb), + ); assert_eq!( context.external_nullifier.to_hex_string(), "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227" @@ -87,6 +113,7 @@ mod tests { let context = Context::new( "app_staging_45068dca85829d2fd90e2dd6f0bff997", Some("test-action-qli8g".to_string()), + Arc::new(CredentialType::Orb), ); assert_eq!( context.external_nullifier.to_hex_string(), @@ -99,6 +126,7 @@ mod tests { let context = Context::new( "app_10eb12bd96d8f7202892ff25f094c803", Some("test-123123".to_string()), + Arc::new(CredentialType::Orb), ); assert_eq!( context.external_nullifier.0, @@ -121,6 +149,7 @@ mod tests { let context = Context::new_from_bytes( "app_10eb12bd96d8f7202892ff25f094c803", Some(custom_action), + Arc::new(CredentialType::Orb), ); assert_eq!( context.external_nullifier.to_hex_string(), @@ -142,6 +171,7 @@ mod tests { let context = Context::new_from_bytes( "app_staging_45068dca85829d2fd90e2dd6f0bff997", Some(custom_action), + Arc::new(CredentialType::Orb), ); assert_eq!( context.external_nullifier.to_hex_string(), diff --git a/walletkit-core/tests/solidity.rs b/walletkit-core/tests/solidity.rs index 6b8125ba..107dff7f 100644 --- a/walletkit-core/tests/solidity.rs +++ b/walletkit-core/tests/solidity.rs @@ -1,9 +1,11 @@ +use std::sync::Arc; + use alloy::{ providers::{ProviderBuilder, WalletProvider}, sol, sol_types::SolValue, }; -use walletkit_core::proof::Context; +use walletkit_core::{credential_type::CredentialType, proof::Context}; sol!( #[allow(missing_docs)] @@ -31,7 +33,11 @@ async fn test_advanced_external_nullifier_generation_on_chain() { let custom_action = [addr.abi_encode_packed(), "test_text".abi_encode_packed()].concat(); - let context = Context::new_from_bytes(&app_id, Some(custom_action)); + let context = Context::new_from_bytes( + &app_id, + Some(custom_action), + Arc::new(CredentialType::Orb), + ); let nullifier = contract .generateExternalNullifier("test_text".to_string())