Skip to content

Commit

Permalink
multi-identity & idComm generation (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
pdtfh authored Nov 28, 2024
1 parent d08ea58 commit 03583cd
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 35 deletions.
23 changes: 12 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"WalletKit",
"Worldcoin",
"xcframework",
"Zeroize"
"Zeroize",
"Zeroizes"
],
"enabledLanguageIds": [
"jsonc",
Expand Down
3 changes: 2 additions & 1 deletion walletkit-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
31 changes: 31 additions & 0 deletions walletkit-core/src/credential_type.rs
Original file line number Diff line number Diff line change
@@ -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",
}
}
}
131 changes: 119 additions & 12 deletions walletkit-core/src/identity.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
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<Identity> 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]
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.
Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions walletkit-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![deny(clippy::all, clippy::pedantic, clippy::nursery)]

pub mod credential_type;
pub mod error;
pub mod identity;
pub mod proof;
Expand Down
Loading

0 comments on commit 03583cd

Please sign in to comment.