diff --git a/Cargo.lock b/Cargo.lock index c0dc52fc8..163f2b692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9709,8 +9709,9 @@ dependencies = [ name = "world-id-primitives" version = "0.8.2" dependencies = [ - "alloy", "alloy-primitives", + "alloy-signer", + "alloy-signer-local", "ark-bn254 0.5.0", "ark-ff 0.5.0", "ark-groth16 0.5.0", @@ -9718,7 +9719,6 @@ dependencies = [ "arrayvec", "ciborium", "embed-doc-image", - "eyre", "getrandom 0.2.17", "hex", "k256", diff --git a/Cargo.toml b/Cargo.toml index 87bea0dc2..f6e87b2d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ rust-version = "1.87" # alloy alloy = { version = "1.7.3", default-features = false } alloy-primitives = { version = "1.5.6", default-features = false } +alloy-signer = { version = "1.7.3", default-features = false } +alloy-signer-local = { version = "1.7.3", default-features = false } alloy-node-bindings = "1.0.42" anyhow = "1" diff --git a/crates/authenticator/Cargo.toml b/crates/authenticator/Cargo.toml index deb2ea665..9efcaea9e 100644 --- a/crates/authenticator/Cargo.toml +++ b/crates/authenticator/Cargo.toml @@ -30,6 +30,7 @@ alloy = { workspace = true, features = [ "providers", "contract", "reqwest", + "signer-local" ] } anyhow = { workspace = true } ark-babyjubjub = { workspace = true } @@ -39,7 +40,7 @@ reqwest = { workspace = true, features = ["json"] } poseidon2 = { workspace = true } eddsa-babyjubjub = { workspace = true } eyre = { workspace = true } -world-id-primitives = { workspace = true } +world-id-primitives = { workspace = true, features = ["proofs"] } secrecy = { workspace = true } taceo-oprf = { workspace = true, features = ["client"] } thiserror = { workspace = true } diff --git a/crates/authenticator/src/authenticator.rs b/crates/authenticator/src/authenticator.rs index c471f1248..ca476b70e 100644 --- a/crates/authenticator/src/authenticator.rs +++ b/crates/authenticator/src/authenticator.rs @@ -36,9 +36,6 @@ use world_id_primitives::{ }, }; -#[expect(unused_imports, reason = "used for docs")] -use world_id_primitives::{Nullifier, SessionId}; - /// Shared helper that polls `GET {gateway_url}/status/{request_id}` and /// returns the current [`GatewayRequestState`]. pub(crate) async fn fetch_gateway_status( @@ -91,7 +88,7 @@ pub struct CredentialInput { pub struct ProofResult { /// The session_id_r_seed (`r`), if a session proof was generated. /// - /// The SDK should cache this keyed by [`SessionId::oprf_seed`]. + /// The SDK should cache this keyed by [`SessionId::oprf_seed`](world_id_primitives::SessionId::oprf_seed). pub session_id_r_seed: Option, /// The response to deliver to an RP. diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 8d2daf6be..e193df39c 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -11,26 +11,31 @@ rust-version.workspace = true repository.workspace = true [features] +default = [] # Enable OpenAPI schema derives for API types. openapi = ["dep:utoipa"] +# Enable proof generation types (circuit inputs, Groth16 proof construction). +proofs = ["dep:ark-groth16", "dep:groth16-material", "dep:taceo-groth16-sol"] + + [dependencies] -alloy = { workspace = true, features = ["sol-types", "signer-local"] } alloy-primitives = { workspace = true, default-features = false, features = [ "serde", "std", ] } +alloy-signer = { workspace = true } +alloy-signer-local = { workspace = true } ark-babyjubjub = { workspace = true } ark-bn254 = { workspace = true } ark-ff = { workspace = true } -ark-groth16 = { workspace = true } +ark-groth16 = { workspace = true, optional = true } ark-serde-compat = { workspace = true } arrayvec = { workspace = true, features = ["serde"] } circom-types = { workspace = true, features = ["groth16"] } -groth16-material = { workspace = true, features = ["circom"] } +groth16-material = { workspace = true, features = ["circom"], optional = true } eddsa-babyjubjub = { workspace = true } -eyre = { workspace = true } hex = { workspace = true } -taceo-groth16-sol = { workspace = true } +taceo-groth16-sol = { workspace = true, optional = true } taceo-oprf = { workspace = true, features = ["types"] } ruint = { workspace = true, features = ["ark-ff-05"] } serde_json = { workspace = true } @@ -54,3 +59,6 @@ getrandom = { workspace = true, features = ["js"] } [dev-dependencies] ark-serialize = { workspace = true } ciborium = { workspace = true } + +[metadata.docs.rs] +features = ["proofs"] diff --git a/crates/primitives/src/credential.rs b/crates/primitives/src/credential.rs index 88e0221cf..8cf826b3d 100644 --- a/crates/primitives/src/credential.rs +++ b/crates/primitives/src/credential.rs @@ -307,9 +307,16 @@ impl Credential { /// # Errors /// Will error if there are more claims than the maximum allowed. /// Will error if the claims cannot be lowered into the field. Should not occur in practice. - pub fn claims_hash(&self) -> Result { + pub fn claims_hash(&self) -> Result { if self.claims.len() > Self::MAX_CLAIMS { - eyre::bail!("There can be at most {} claims", Self::MAX_CLAIMS); + return Err(PrimitiveError::InvalidInput { + attribute: "claims".to_string(), + reason: format!( + "there can be at most {} claims, got {}", + Self::MAX_CLAIMS, + self.claims.len() + ), + }); } let mut input = [*FieldElement::ZERO; Self::MAX_CLAIMS]; for (i, claim) in self.claims.iter().enumerate() { @@ -326,7 +333,7 @@ impl Credential { /// # Errors /// - Will error if there are more claims than the maximum allowed. /// - Will error if the claims cannot be lowered into the field. Should not occur in practice. - pub fn hash(&self) -> Result { + pub fn hash(&self) -> Result { match self.version { CredentialVersion::V1 => { let mut input = [ @@ -349,7 +356,7 @@ impl Credential { /// /// # Errors /// Will error if the credential cannot be hashed. - pub fn sign(self, signer: &EdDSAPrivateKey) -> Result { + pub fn sign(self, signer: &EdDSAPrivateKey) -> Result { let mut credential = self; credential.signature = Some(signer.sign(*credential.hash()?)); credential.issuer = signer.public(); @@ -364,16 +371,20 @@ impl Credential { pub fn verify_signature( &self, expected_issuer_pubkey: &EdDSAPublicKey, - ) -> Result { + ) -> Result { if &self.issuer != expected_issuer_pubkey { - return Err(eyre::eyre!( - "Issuer public key does not match expected public key" - )); + return Err(PrimitiveError::InvalidInput { + attribute: "issuer".to_string(), + reason: "issuer public key does not match expected public key".to_string(), + }); } if let Some(signature) = &self.signature { return Ok(self.issuer.verify(*self.hash()?, signature)); } - Err(eyre::eyre!("Credential not signed")) + Err(PrimitiveError::InvalidInput { + attribute: "signature".to_string(), + reason: "credential not signed".to_string(), + }) } /// Compute the `sub` for a credential computed from `leaf_index` and a `blinding_factor`. diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 15c4b63d6..34cba6ed6 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -26,6 +26,7 @@ pub use config::Config; /// Contains the raw circuit input types for the World ID Protocol. /// /// These types are used to prepare the inputs for the Groth16 circuits. +#[cfg(feature = "proofs")] pub mod circuit_inputs; /// SAFE-style sponge utilities and helpers. diff --git a/crates/primitives/src/proof.rs b/crates/primitives/src/proof.rs index 1fbd01b7b..09d3c7234 100644 --- a/crates/primitives/src/proof.rs +++ b/crates/primitives/src/proof.rs @@ -1,8 +1,7 @@ -use ark_bn254::Bn254; -use ark_groth16::Proof; use ruint::aliases::U256; use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _}; +#[cfg(feature = "proofs")] use crate::FieldElement; /// Encoded World ID Proof. @@ -19,10 +18,14 @@ pub struct ZeroKnowledgeProof { inner: [U256; 5], } +#[cfg(feature = "proofs")] impl ZeroKnowledgeProof { /// Initialize a new proof from a Groth16 proof and Merkle root. #[must_use] - pub fn from_groth16_proof(groth16_proof: &Proof, merkle_root: FieldElement) -> Self { + pub fn from_groth16_proof( + groth16_proof: &ark_groth16::Proof, + merkle_root: FieldElement, + ) -> Self { let compressed_proof = taceo_groth16_sol::prepare_compressed_proof(groth16_proof); Self { inner: [ @@ -34,7 +37,9 @@ impl ZeroKnowledgeProof { ], } } +} +impl ZeroKnowledgeProof { /// Outputs the proof as a Solidity-friendly representation. #[must_use] pub const fn as_ethereum_representation(&self) -> [U256; 5] { diff --git a/crates/primitives/src/request/mod.rs b/crates/primitives/src/request/mod.rs index f43b1ef8a..538f33afa 100644 --- a/crates/primitives/src/request/mod.rs +++ b/crates/primitives/src/request/mod.rs @@ -75,7 +75,7 @@ pub struct ProofRequest { pub action: Option, /// The RP's ECDSA signature over the request. #[serde(with = "crate::serde_utils::hex_signature")] - pub signature: alloy::signers::Signature, + pub signature: alloy_signer::Signature, /// Unique nonce for this request provided by the RP. pub nonce: FieldElement, /// Specific credential requests. This defines which credentials to ask for. @@ -681,14 +681,13 @@ where mod tests { use super::*; use crate::SessionNullifier; - use alloy::{ - signers::{SignerSync, local::PrivateKeySigner}, - uint, - }; + use alloy_signer::SignerSync; + use alloy_signer_local::PrivateKeySigner; use k256::ecdsa::SigningKey; + use ruint::uint; // Test helpers - fn test_signature() -> alloy::signers::Signature { + fn test_signature() -> alloy_signer::Signature { let signer = PrivateKeySigner::from_signing_key(SigningKey::from_bytes(&[1u8; 32].into()).unwrap()); signer.sign_message_sync(b"test").expect("can sign") diff --git a/crates/primitives/src/signer.rs b/crates/primitives/src/signer.rs index ade1f3a2f..58a58aa2e 100644 --- a/crates/primitives/src/signer.rs +++ b/crates/primitives/src/signer.rs @@ -1,5 +1,6 @@ use crate::PrimitiveError; -use alloy::{primitives::Address, signers::local::PrivateKeySigner}; +use alloy_primitives::Address; +use alloy_signer_local::PrivateKeySigner; use eddsa_babyjubjub::{EdDSAPrivateKey, EdDSAPublicKey}; use secrecy::{ExposeSecret, SecretBox}; diff --git a/crates/proof/Cargo.toml b/crates/proof/Cargo.toml index 298ddd368..4e66f6f7a 100644 --- a/crates/proof/Cargo.toml +++ b/crates/proof/Cargo.toml @@ -48,7 +48,7 @@ eyre = { workspace = true } zeroize = { workspace = true } # Internal -world-id-primitives = { workspace = true } +world-id-primitives = { workspace = true, features = ["proofs"] } [build-dependencies] eyre = { workspace = true } diff --git a/crates/proof/src/proof.rs b/crates/proof/src/proof.rs index e588e0093..eabd28ed1 100644 --- a/crates/proof/src/proof.rs +++ b/crates/proof/src/proof.rs @@ -358,7 +358,7 @@ pub fn generate_nullifier_proof( issuer_schema_id: credential.issuer_schema_id.into(), cred_pk: credential.issuer.pk, cred_hashes: [ - *credential.claims_hash()?, + *credential.claims_hash().map_err(eyre::Report::from)?, *credential.associated_data_commitment, ], cred_genesis_issued_at: credential.genesis_issued_at.into(), diff --git a/services/oprf-dev-client/Cargo.toml b/services/oprf-dev-client/Cargo.toml index dd0453880..c8925601c 100644 --- a/services/oprf-dev-client/Cargo.toml +++ b/services/oprf-dev-client/Cargo.toml @@ -36,6 +36,6 @@ tracing = { workspace = true } uuid = { workspace = true, features = ["serde", "v4"] } webpki-roots = { workspace = true } world-id-core = { workspace = true, features = ["authenticator", "embed-zkeys"] } -world-id-primitives = { workspace = true } +world-id-primitives = { workspace = true, features = ["proofs"] } world-id-proof = { workspace = true } world-id-test-utils = { workspace = true } diff --git a/services/oprf-node/Cargo.toml b/services/oprf-node/Cargo.toml index e7f8f3d8a..d7ba78a8d 100644 --- a/services/oprf-node/Cargo.toml +++ b/services/oprf-node/Cargo.toml @@ -44,7 +44,7 @@ tokio-util = { workspace = true } tracing = { workspace = true } uuid = { workspace = true, features = ["serde", "v4"] } world-id-core = { workspace = true, features = ["authenticator", "embed-zkeys"] } -world-id-primitives = { workspace = true } +world-id-primitives = { workspace = true, features = ["proofs"] } [dev-dependencies] ark-serialize.workspace = true