world_id_primitives/
credential.rs

1use ark_babyjubjub::EdwardsAffine;
2use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
3use poseidon2::{Poseidon2, POSEIDON2_BN254_T3_PARAMS};
4use rand::Rng;
5use ruint::aliases::U256;
6use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::{sponge::hash_bytes_to_field_element, FieldElement, PrimitiveError};
9
10/// Domain separation tag to avoid collisions with other Poseidon2 usages.
11const ASSOCIATED_DATA_HASH_DS_TAG: &[u8] = b"ASSOCIATED_DATA_HASH_V1";
12const CLAIMS_HASH_DS_TAG: &[u8] = b"CLAIMS_HASH_V1";
13
14/// Version of the `Credential` object
15#[derive(Default, Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
16#[repr(u8)]
17pub enum CredentialVersion {
18    /// Version 1 of the `Credential`. In addition to the specific attributes,
19    /// - Hashing function: `Poseidon2`
20    /// - Signature scheme: `EdDSA` on `BabyJubJub` Curve
21    /// - Curve (Base) Field (`Fq`): `BabyJubJub` Curve Field (also the BN254 Scalar Field)
22    /// - Scalar Field (`Fr`): `BabyJubJub` Scalar Field
23    #[default]
24    V1 = 1,
25}
26
27/// Base representation of a `Credential` in the World ID Protocol.
28///
29/// A credential is generally a verifiable digital statement about a subject. It is
30/// the canonical object: everything a verifier needs for proofs and authorization.
31///
32/// In the case of World ID these statements are about humans, with the most common
33/// credentials being Orb verification or document verification.
34///
35/// # Associated Data
36///
37/// Credentials have a pre-defined strict structure, which is determined by their version. Extending this,
38/// issuers may opt to include additional arbitrary data with the Credential. This data is called
39/// **Associated Data**.
40/// - Associated data is stored by Authenticators with the Credential.
41/// - Including associated data is a decision by the issuer. Its structure and content is solely
42///   determined by the issuer and the data will not be exposed to RPs or others.
43/// - An example of associated data use is supporting data to re-issue a credential (e.g. a sign up number).
44/// - Associated data is never exposed to RPs or others. It only lives in the Authenticator.
45/// - Associated data is authenticated in the Credential through the `associated_data_hash` field. The issuer
46///   can determine how this data is hashed. However providing the raw data to `associated_data` can ensure a
47///   consistent hashing into the field.
48/// ```text
49/// +------------------------------+
50/// |          Credential          |
51/// |                              |
52/// |  - associated_data_hash <----+
53/// |  - signature                 |
54/// +------------------------------+
55///           ^
56///           |
57///     Hash(associated_data)
58///           |
59/// Associated Data
60/// +------------------------------+
61/// | Optional arbitrary data      |
62/// +------------------------------+
63/// ```
64///
65/// # Design Principles:
66/// - A credential clearly separates:
67///    - **Assertion** (the claim being made)
68///    - **Issuer** (who attests to it / vouches for it)
69///    - **Subject** (who it is about)
70///    - **Presenter binding** (who can present it)
71/// - Credentials are **usable across authenticators** without leaking correlate-able identifiers to RPs.
72/// - Revocation, expiry, and re-issuance are **first-class lifecycle properties**.
73/// - Flexibility: credentials may take different formats but share **common metadata** (validity, issuer, trust, type).
74///
75/// All credentials have an issuer and schema, identified with the `issuer_schema_id` field. This identifier
76/// is registered in the `CredentialSchemaIssuerRegistry` contract. It represents a particular schema issued by
77/// a particular issuer. Some schemas are intended to be global (e.g. representing an ICAO-compliant passport) and
78/// some issuer-specific. Schemas should be registered in the `CredentialSchemaIssuerRegistry` contract and should be
79/// publicly accessible.
80///
81/// We want to encourage schemas to be widely distributed and adopted. If everyone uses the same passport schema,
82/// for example, the Protocol will have better interoperability across passport credential issuers, reducing the
83/// burden on holders (to make sense of which passport they have), and similarly, RPs.
84#[derive(Debug, Serialize, Deserialize, Clone)]
85pub struct Credential {
86    /// A reference identifier for the credential. This can be used by issuers
87    /// to manage credential lifecycle.
88    ///
89    /// - This ID is never exposed or used outside of issuer scope. It is never part of proofs
90    ///   or exposed to RPs.
91    /// - Generally, it is recommended to maintain the default of a random identifier.
92    ///
93    /// # Example Uses
94    /// - Track issued credentials to later support revocation after refreshing.
95    pub id: u64,
96    /// The version of the Credential determines its structure.
97    pub version: CredentialVersion,
98    /// Unique issuer schema id represents the unique combination of the credential's
99    /// schema and the issuer.
100    ///
101    /// The `issuer_schema_id` is registered in the `CredentialSchemaIssuerRegistry`. With this
102    /// identifier, the RPs lookup the authorized keys that can sign the credential.
103    pub issuer_schema_id: u64,
104    /// The blinded subject (World ID) for which the credential is issued.
105    ///
106    /// The underlying identifier comes from the `WorldIDRegistry` and is
107    /// the `leaf_index` of the World ID on the Merkle tree. However, this is blinded
108    /// for each `issuer_schema_id` with a blinding factor to prevent correlation of credentials
109    /// by malicious issuers.
110    pub sub: FieldElement,
111    /// Timestamp of **first issuance** of this credential (unix seconds), i.e. this represents when the holder
112    /// first obtained the credential. Even if the credential has been issued multiple times (e.g. because of a renewal),
113    /// this timestamp should stay constant.
114    ///
115    /// This timestamp can be queried (only as a minimum value) by RPs.
116    pub genesis_issued_at: u64,
117    /// Expiration timestamp (unix seconds)
118    pub expires_at: u64,
119    /// **For Future Use**. Concrete statements that the issuer attests about the receiver.
120    ///
121    /// They can be just commitments to data (e.g. passport image) or
122    /// the value directly (e.g. date of birth).
123    ///
124    /// Currently these statements are not in use in the Proofs yet.
125    pub claims: Vec<FieldElement>,
126    /// The commitment to the associated data issued with the Credential.
127    ///
128    /// By default this uses the internal `hash_bytes_to_field_element` function,
129    /// but each issuer may determine their own hashing algorithm.
130    ///
131    /// This hash is generally only used by the issuer.
132    pub associated_data_hash: FieldElement,
133    /// The signature of the credential (signed by the issuer's key)
134    #[serde(serialize_with = "serialize_signature")]
135    #[serde(deserialize_with = "deserialize_signature")]
136    #[serde(default)]
137    pub signature: Option<EdDSASignature>,
138    /// The public component of the issuer's key which signed the Credential.
139    pub issuer: EdDSAPublicKey,
140}
141
142impl Credential {
143    /// The maximum number of claims that can be included in a credential.
144    pub const MAX_CLAIMS: usize = 16;
145
146    /// Initializes a new credential.
147    ///
148    /// Note default fields occupy a sentinel value of `BaseField::zero()`
149    #[must_use]
150    pub fn new() -> Self {
151        let mut rng = rand::thread_rng();
152        Self {
153            id: rng.gen(),
154            version: CredentialVersion::V1,
155            issuer_schema_id: 0,
156            sub: FieldElement::ZERO,
157            genesis_issued_at: 0,
158            expires_at: 0,
159            claims: vec![FieldElement::ZERO; Self::MAX_CLAIMS],
160            associated_data_hash: FieldElement::ZERO,
161            signature: None,
162            issuer: EdDSAPublicKey {
163                pk: EdwardsAffine::default(),
164            },
165        }
166    }
167
168    /// Set the `id` of the credential.
169    #[must_use]
170    pub const fn id(mut self, id: u64) -> Self {
171        self.id = id;
172        self
173    }
174
175    /// Set the `version` of the credential.
176    #[must_use]
177    pub const fn version(mut self, version: CredentialVersion) -> Self {
178        self.version = version;
179        self
180    }
181
182    /// Set the `issuerSchemaId` of the credential.
183    #[must_use]
184    pub const fn issuer_schema_id(mut self, issuer_schema_id: u64) -> Self {
185        self.issuer_schema_id = issuer_schema_id;
186        self
187    }
188
189    /// Set the `sub` for the credential computed from `leaf_index` and a `blinding_factor`.
190    #[must_use]
191    pub fn sub(mut self, leaf_index: u64, blinding_factor: FieldElement) -> Self {
192        let hasher = Poseidon2::new(&POSEIDON2_BN254_T3_PARAMS);
193        let mut input = [*self.get_sub_ds(), leaf_index.into(), *blinding_factor];
194        hasher.permutation_in_place(&mut input);
195        self.sub = input[1].into();
196        self
197    }
198
199    /// Set the genesis issued at of the credential.
200    #[must_use]
201    pub const fn genesis_issued_at(mut self, genesis_issued_at: u64) -> Self {
202        self.genesis_issued_at = genesis_issued_at;
203        self
204    }
205
206    /// Set the expires at of the credential.
207    #[must_use]
208    pub const fn expires_at(mut self, expires_at: u64) -> Self {
209        self.expires_at = expires_at;
210        self
211    }
212
213    /// Set a claim hash for the credential at an index.
214    ///
215    /// # Errors
216    /// Will error if the index is out of bounds.
217    pub fn claim_hash(mut self, index: usize, claim: U256) -> Result<Self, PrimitiveError> {
218        if index >= self.claims.len() {
219            return Err(PrimitiveError::OutOfBounds);
220        }
221        self.claims[index] = claim.try_into().map_err(|_| PrimitiveError::NotInField)?;
222        Ok(self)
223    }
224
225    /// Set the claim hash at specific index by hashing arbitrary bytes using Poseidon2.
226    ///
227    /// This method accepts arbitrary bytes, converts them to field elements,
228    /// applies a Poseidon2 hash, and stores the result as claim at the provided index.
229    ///
230    /// # Arguments
231    /// * `claim` - Arbitrary bytes to hash (any length).
232    ///
233    /// # Errors
234    /// Will error if the data is empty and if the index is out of bounds.
235    pub fn claim(mut self, index: usize, claim: &[u8]) -> Result<Self, PrimitiveError> {
236        if index >= self.claims.len() {
237            return Err(PrimitiveError::OutOfBounds);
238        }
239        self.claims[index] = hash_bytes_to_field_element(CLAIMS_HASH_DS_TAG, claim)?;
240        Ok(self)
241    }
242    /// Set the associated data hash of the credential from a pre-computed hash.
243    ///
244    /// # Errors
245    /// Will error if the provided hash cannot be lowered into the field.
246    pub fn associated_data_hash(
247        mut self,
248        associated_data_hash: U256,
249    ) -> Result<Self, PrimitiveError> {
250        self.associated_data_hash = associated_data_hash
251            .try_into()
252            .map_err(|_| PrimitiveError::NotInField)?;
253        Ok(self)
254    }
255
256    /// Set the associated data hash by hashing arbitrary bytes using Poseidon2.
257    ///
258    /// This method accepts arbitrary bytes, converts them to field elements,
259    /// applies a Poseidon2 hash, and stores the result as the associated data hash.
260    ///
261    /// # Arguments
262    /// * `data` - Arbitrary bytes to hash (any length).
263    ///
264    /// # Errors
265    /// Will error if the data is empty.
266    pub fn associated_data(mut self, data: &[u8]) -> Result<Self, PrimitiveError> {
267        self.associated_data_hash = hash_bytes_to_field_element(ASSOCIATED_DATA_HASH_DS_TAG, data)?;
268        Ok(self)
269    }
270
271    /// Get the credential domain separator for the given version.
272    #[must_use]
273    pub fn get_cred_ds(&self) -> FieldElement {
274        match self.version {
275            CredentialVersion::V1 => FieldElement::from_be_bytes_mod_order(b"POSEIDON2+EDDSA-BJJ"),
276        }
277    }
278
279    /// Get the sub domain separator for the given version.
280    #[must_use]
281    pub fn get_sub_ds(&self) -> FieldElement {
282        match self.version {
283            CredentialVersion::V1 => FieldElement::from_be_bytes_mod_order(b"H_CS(id, r)"),
284        }
285    }
286}
287
288impl Default for Credential {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294/// Serializes the signature as compressed bytes (encoding r and s concatenated)
295/// where `r` is compressed to a single coordinate. Result is hex-encoded.
296#[allow(clippy::ref_option)]
297fn serialize_signature<S>(
298    signature: &Option<EdDSASignature>,
299    serializer: S,
300) -> Result<S::Ok, S::Error>
301where
302    S: Serializer,
303{
304    let Some(signature) = signature else {
305        return serializer.serialize_none();
306    };
307    let sig = signature
308        .to_compressed_bytes()
309        .map_err(serde::ser::Error::custom)?;
310    serializer.serialize_str(&hex::encode(sig))
311}
312
313fn deserialize_signature<'de, D>(deserializer: D) -> Result<Option<EdDSASignature>, D::Error>
314where
315    D: Deserializer<'de>,
316{
317    let maybe_str = Option::<String>::deserialize(deserializer)?;
318
319    let Some(s) = maybe_str else { return Ok(None) };
320
321    let bytes = hex::decode(s).map_err(de::Error::custom)?;
322    if bytes.len() != 64 {
323        return Err(de::Error::custom("Invalid signature. Expected 64 bytes."));
324    }
325    let mut arr = [0u8; 64];
326    arr.copy_from_slice(&bytes);
327    let signature = EdDSASignature::from_compressed_bytes(arr).map_err(de::Error::custom)?;
328    Ok(Some(signature))
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_associated_data_matches_direct_hash() {
337        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
338
339        // Using the associated_data method
340        let credential = Credential::new().associated_data(&data).unwrap();
341
342        // Using the hash function directly
343        let direct_hash = hash_bytes_to_field_element(ASSOCIATED_DATA_HASH_DS_TAG, &data).unwrap();
344
345        // Both should produce the same hash
346        assert_eq!(credential.associated_data_hash, direct_hash);
347    }
348
349    #[test]
350    fn test_associated_data_method() {
351        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
352
353        let credential = Credential::new().associated_data(&data).unwrap();
354
355        // Should have a non-zero associated data hash
356        assert_ne!(credential.associated_data_hash, FieldElement::ZERO);
357    }
358
359    #[test]
360    fn test_claim_matches_direct_hash() {
361        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
362
363        // Using the claim method
364        let credential = Credential::new().claim(0, &data).unwrap();
365
366        // Using the hash function directly
367        let direct_hash = hash_bytes_to_field_element(CLAIMS_HASH_DS_TAG, &data).unwrap();
368
369        // Both should produce the same hash
370        assert_eq!(credential.claims[0], direct_hash);
371    }
372
373    #[test]
374    fn test_claim_method() {
375        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
376
377        let credential = Credential::new().claim(1, &data).unwrap();
378
379        // Should have a non-zero claim hash
380        assert_ne!(credential.claims[1], FieldElement::ZERO);
381    }
382}