Skip to main content

world_id_primitives/
credential.rs

1use ark_babyjubjub::EdwardsAffine;
2use ark_ff::BigInt;
3use eddsa_babyjubjub::{EdDSAPrivateKey, EdDSAPublicKey, EdDSASignature};
4use rand::Rng;
5use ruint::aliases::U256;
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7
8use crate::{FieldElement, PrimitiveError, sponge::hash_bytes_to_field_element};
9
10/// Domain separation tag to avoid collisions with other Poseidon2 usages.
11const ASSOCIATED_DATA_COMMITMENT_DS_TAG: &[u8] = b"ASSOCIATED_DATA_HASH_V1";
12const CLAIMS_HASH_DS_TAG: &[u8] = b"CLAIMS_HASH_V1";
13const SUB_DS_TAG: &[u8] = b"H_CS(id, r)";
14
15/// Version of the `Credential` object
16#[derive(Default, Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
17#[repr(u8)]
18pub enum CredentialVersion {
19    /// Version 1 of the `Credential`. In addition to the specific attributes,
20    /// - Hashing function: `Poseidon2`
21    /// - Signature scheme: `EdDSA` on `BabyJubJub` Curve
22    /// - Curve (Base) Field (`Fq`): `BabyJubJub` Curve Field (also the BN254 Scalar Field)
23    /// - Scalar Field (`Fr`): `BabyJubJub` Scalar Field
24    #[default]
25    V1 = 1,
26}
27
28/// Base representation of a `Credential` in the World ID Protocol.
29///
30/// A credential is generally a verifiable digital statement about a subject. It is
31/// the canonical object: everything a verifier needs for proofs and authorization.
32///
33/// In the case of World ID these statements are about humans, with the most common
34/// credentials being Orb verification or document verification.
35///
36/// # Credential Lifecycle
37///
38/// The following official terminology is defined for the lifecycle of a Credential.
39/// - **Issuance** (can also be called **Enrollment**): Process by which a credential is initially issued to a user.
40/// - **Renewal**: Process by which a user requests a new Credential from a previously existing active or
41///   expired Credential. This usually happens close to Credential expiration. _It is analogous to
42///   when you request a renewal of your passport, you get a new passport with a new expiration date._
43/// - **Re-Issuance**: Process by which a user obtains a copy of their existing Credential. The copy does not
44///   need to be exact, but the original expiration date MUST be preserved. This usually occurs when a user
45///   accidentally lost their Credential (e.g. disk failure, authenticator loss) and needs to recover for an existing period.
46///
47/// # Associated Data
48///
49/// Credentials have a pre-defined strict structure, which is determined by their version. Issuers
50/// may opt to include additional arbitrary data with the Credential (**Associated Data**). This arbitrary data
51/// can be used to support the issuer in the operation of their Credential (for example it may contain an identifier
52/// to allow credential refresh).
53///
54/// - Associated data is stored by Authenticators with the Credential.
55/// - Introducing associated data is a decision by the issuer. Its structure and content is solely
56///   determined by the issuer and the data will not be exposed to RPs or others.
57/// - An example of associated data use is supporting data to re-issue a credential (e.g. a sign up number).
58/// - Associated data is never exposed to RPs or others. It only lives in the Authenticator and may be provided
59///   to issuers.
60/// - Associated data is authenticated in the Credential through the `associated_data_commitment` field. The issuer
61///   MUST determine how this commitment is computed. Issuers may opt to use the [`Credential::associated_data_commitment_from_raw_bytes`]
62///   helper to ensure their raw data is committed, but other commitment mechanisms may make sense depending on the
63///   structure of the associated data.
64///
65/// ```text
66/// +------------------------------------+
67/// |          Credential                |
68/// |                                    |
69/// |  - associated_data_commitment <----+
70/// |  - signature                       |
71/// +------------------------------------+
72///               ^
73///               |
74///     Commitment(associated_data)
75///               |
76/// Associated Data
77/// +------------------------------------+
78/// | Optional arbitrary data            |
79/// +------------------------------------+
80/// ```
81///
82/// # Design Principles:
83/// - A credential clearly separates:
84///    - **Assertion** (the claim being made)
85///    - **Issuer** (who attests to it / vouches for it)
86///    - **Subject** (who it is about)
87///    - **Presenter binding** (who can present it)
88/// - Credentials are **usable across authenticators** without leaking correlate-able identifiers to RPs.
89/// - Revocation, expiry, and re-issuance are **first-class lifecycle properties**.
90/// - Flexibility: credentials may take different formats but share **common metadata** (validity, issuer, trust, type).
91///
92/// All credentials have an issuer and schema, identified with the `issuer_schema_id` field. This identifier
93/// is registered in the `CredentialSchemaIssuerRegistry` contract. It represents a particular schema issued by
94/// a particular issuer. Some schemas are intended to be global (e.g. representing an ICAO-compliant passport) and
95/// some issuer-specific. Schemas should be registered in the `CredentialSchemaIssuerRegistry` contract and should be
96/// publicly accessible.
97///
98/// We want to encourage schemas to be widely distributed and adopted. If everyone uses the same passport schema,
99/// for example, the Protocol will have better interoperability across passport credential issuers, reducing the
100/// burden on holders (to make sense of which passport they have), and similarly, RPs.
101#[derive(Debug, Serialize, Deserialize, Clone)]
102pub struct Credential {
103    /// A reference identifier for the credential. This can be used by issuers
104    /// to manage credential lifecycle.
105    ///
106    /// - This ID is never exposed or used outside of issuer scope. It is never part of proofs
107    ///   or exposed to RPs.
108    /// - Generally, it is recommended to maintain the default of a random identifier.
109    ///
110    /// # Example Uses
111    /// - Track issued credentials to later support revocation after refreshing.
112    pub id: u64,
113    /// The version of the Credential determines its structure.
114    pub version: CredentialVersion,
115    /// A reference version field for issuer use. Different versions can determine how
116    /// the issuer-defined fields are constructed for a Credential.
117    ///
118    /// # Usage Examples
119    /// - Different claims values and their meaning could be determined from
120    ///   distinct issuer versions.
121    /// - Different construction of associated data or its commitment can be
122    ///   determined from distinct issuer versions.
123    #[serde(default)]
124    pub issuer_version: u8,
125    /// Unique issuer schema id represents the unique combination of the credential's
126    /// schema and the issuer.
127    ///
128    /// The `issuer_schema_id` is registered in the `CredentialSchemaIssuerRegistry`. With this
129    /// identifier, the RPs lookup the authorized keys that can sign the credential.
130    pub issuer_schema_id: u64,
131    /// The blinded subject (World ID) for which the credential is issued.
132    ///
133    /// The underlying identifier comes from the `WorldIDRegistry` and is
134    /// the `leaf_index` of the World ID on the Merkle tree. However, this is blinded
135    /// for each `issuer_schema_id` with a blinding factor to prevent correlation of credentials
136    /// by malicious issuers. See [`Self::compute_sub`] for details on how the credential blinding factor
137    /// is computed.
138    pub sub: FieldElement,
139    /// Timestamp of **first issuance** of this credential (unix seconds), i.e. this represents when the holder
140    /// first obtained the credential. Even if the credential has been issued multiple times (e.g. because of a renewal),
141    /// this timestamp should stay constant.
142    ///
143    /// This timestamp can be queried (only as a minimum value) by RPs.
144    pub genesis_issued_at: u64,
145    /// Expiration timestamp (unix seconds)
146    pub expires_at: u64,
147    /// **For Future Use**. Concrete statements that the issuer attests about the receiver.
148    ///
149    /// They can be just commitments to data (e.g. passport image) or
150    /// the value directly (e.g. date of birth).
151    ///
152    /// Currently these statements are not in use in the Proofs yet.
153    pub claims: Vec<FieldElement>,
154    /// The commitment to the Associated Data issued with the Credential.
155    ///
156    /// This may use a common hashing algorithm from the raw bytes of the
157    /// asscociated data and one function is exposed for this convenience,
158    /// [`hash_bytes_to_field_element`]. Each issuer however determines how
159    /// best to construct this value to establish the integrity of their Associated Data.
160    ///
161    /// This commitment is only for issuer use.
162    #[serde(alias = "associated_data_hash")]
163    // this was previously named `associated_data_hash`; fallback will be removed in the next version
164    pub associated_data_commitment: FieldElement,
165    /// The signature of the credential (signed by the issuer's key)
166    #[serde(serialize_with = "serialize_signature")]
167    #[serde(deserialize_with = "deserialize_signature")]
168    #[serde(default)]
169    pub signature: Option<EdDSASignature>,
170    /// The public component of the issuer's key which signed the Credential.
171    #[serde(serialize_with = "serialize_public_key")]
172    #[serde(deserialize_with = "deserialize_public_key")]
173    pub issuer: EdDSAPublicKey,
174}
175
176impl Credential {
177    /// The maximum number of claims that can be included in a credential.
178    pub const MAX_CLAIMS: usize = 16;
179
180    /// Initializes a new credential.
181    ///
182    /// Note default fields occupy a sentinel value of `BaseField::zero()`
183    #[must_use]
184    pub fn new() -> Self {
185        let mut rng = rand::thread_rng();
186        Self {
187            id: rng.r#gen(),
188            version: CredentialVersion::V1,
189            issuer_version: 0,
190            issuer_schema_id: 0,
191            sub: FieldElement::ZERO,
192            genesis_issued_at: 0,
193            expires_at: 0,
194            claims: vec![FieldElement::ZERO; Self::MAX_CLAIMS],
195            associated_data_commitment: FieldElement::ZERO,
196            signature: None,
197            issuer: EdDSAPublicKey {
198                pk: EdwardsAffine::default(),
199            },
200        }
201    }
202
203    /// Set the `id` of the credential.
204    #[must_use]
205    pub const fn id(mut self, id: u64) -> Self {
206        self.id = id;
207        self
208    }
209
210    /// Set the `version` of the credential.
211    #[must_use]
212    pub const fn version(mut self, version: CredentialVersion) -> Self {
213        self.version = version;
214        self
215    }
216
217    /// Set the `issuer_version` of the credential.
218    #[must_use]
219    pub const fn issuer_version(mut self, issuer_version: u8) -> Self {
220        self.issuer_version = issuer_version;
221        self
222    }
223
224    /// Set the `issuerSchemaId` of the credential.
225    #[must_use]
226    pub const fn issuer_schema_id(mut self, issuer_schema_id: u64) -> Self {
227        self.issuer_schema_id = issuer_schema_id;
228        self
229    }
230
231    /// Set the `sub` for the credential.
232    #[must_use]
233    pub const fn subject(mut self, sub: FieldElement) -> Self {
234        self.sub = sub;
235        self
236    }
237
238    /// Set the genesis issued at of the credential.
239    #[must_use]
240    pub const fn genesis_issued_at(mut self, genesis_issued_at: u64) -> Self {
241        self.genesis_issued_at = genesis_issued_at;
242        self
243    }
244
245    /// Set the expires at of the credential.
246    #[must_use]
247    pub const fn expires_at(mut self, expires_at: u64) -> Self {
248        self.expires_at = expires_at;
249        self
250    }
251
252    /// Set a claim hash for the credential at an index.
253    ///
254    /// # Errors
255    /// Will error if the index is out of bounds.
256    pub fn claim_hash(mut self, index: usize, claim: U256) -> Result<Self, PrimitiveError> {
257        if index >= self.claims.len() {
258            return Err(PrimitiveError::OutOfBounds);
259        }
260        self.claims[index] = claim.try_into().map_err(|_| PrimitiveError::NotInField)?;
261        Ok(self)
262    }
263
264    /// Set the claim hash at specific index by hashing arbitrary bytes using Poseidon2.
265    ///
266    /// This method accepts arbitrary bytes, converts them to field elements,
267    /// applies a Poseidon2 hash, and stores the result as claim at the provided index.
268    ///
269    /// # Arguments
270    /// * `claim` - Arbitrary bytes to hash (any length).
271    ///
272    /// # Errors
273    /// Will error if the data is empty and if the index is out of bounds.
274    pub fn claim(mut self, index: usize, claim: &[u8]) -> Result<Self, PrimitiveError> {
275        if index >= self.claims.len() {
276            return Err(PrimitiveError::OutOfBounds);
277        }
278        self.claims[index] = hash_bytes_to_field_element(CLAIMS_HASH_DS_TAG, claim)?;
279        Ok(self)
280    }
281
282    /// Set the associated data commitment of the credential.
283    ///
284    /// # Errors
285    /// Will error if the provided hash cannot be lowered into the field.
286    pub fn associated_data_commitment(
287        mut self,
288        associated_data_commitment: U256,
289    ) -> Result<Self, PrimitiveError> {
290        self.associated_data_commitment = associated_data_commitment
291            .try_into()
292            .map_err(|_| PrimitiveError::NotInField)?;
293        Ok(self)
294    }
295
296    /// Set the associated data commitment from arbitrary bytes. This can be
297    /// used to construct the associated data commitment in a canonical way.
298    ///
299    /// This method takes arbitrary bytes, converts them to field elements,
300    /// applies a Poseidon2 hash, and stores the result as the associated data commitment.
301    ///
302    /// # Arguments
303    /// * `data` - Arbitrary bytes to be committed (any length).
304    ///
305    /// # Errors
306    /// Will error if the data is empty.
307    pub fn associated_data_commitment_from_raw_bytes(
308        mut self,
309        data: &[u8],
310    ) -> Result<Self, PrimitiveError> {
311        self.associated_data_commitment =
312            hash_bytes_to_field_element(ASSOCIATED_DATA_COMMITMENT_DS_TAG, data)?;
313        Ok(self)
314    }
315
316    /// Get the credential domain separator for the given version.
317    #[must_use]
318    pub fn get_cred_ds(&self) -> FieldElement {
319        match self.version {
320            CredentialVersion::V1 => FieldElement::from_be_bytes_mod_order(b"POSEIDON2+EDDSA-BJJ"),
321        }
322    }
323
324    /// Get the claims hash of the credential.
325    ///
326    /// # Errors
327    /// Will error if there are more claims than the maximum allowed.
328    /// Will error if the claims cannot be lowered into the field. Should not occur in practice.
329    pub fn claims_hash(&self) -> Result<FieldElement, eyre::Error> {
330        if self.claims.len() > Self::MAX_CLAIMS {
331            eyre::bail!("There can be at most {} claims", Self::MAX_CLAIMS);
332        }
333        let mut input = [*FieldElement::ZERO; Self::MAX_CLAIMS];
334        for (i, claim) in self.claims.iter().enumerate() {
335            input[i] = **claim;
336        }
337        poseidon2::bn254::t16::permutation_in_place(&mut input);
338        Ok(input[1].into())
339    }
340
341    /// Computes the canonical hash of the Credential.
342    ///
343    /// The hash is signed by the issuer to provide authenticity for the credential.
344    ///
345    /// # Errors
346    /// - Will error if there are more claims than the maximum allowed.
347    /// - Will error if the claims cannot be lowered into the field. Should not occur in practice.
348    pub fn hash(&self) -> Result<FieldElement, eyre::Error> {
349        match self.version {
350            CredentialVersion::V1 => {
351                let id_issuer_version = BigInt([self.id, self.issuer_version as u64, 0, 0]);
352
353                let mut input = [
354                    *self.get_cred_ds(),
355                    self.issuer_schema_id.into(),
356                    *self.sub,
357                    self.genesis_issued_at.into(),
358                    self.expires_at.into(),
359                    *self.claims_hash()?,
360                    *self.associated_data_commitment,
361                    id_issuer_version.into(),
362                ];
363                poseidon2::bn254::t8::permutation_in_place(&mut input);
364                Ok(input[1].into())
365            }
366        }
367    }
368
369    /// Sign the credential.
370    ///
371    /// # Errors
372    /// Will error if the credential cannot be hashed.
373    pub fn sign(self, signer: &EdDSAPrivateKey) -> Result<Self, eyre::Error> {
374        let mut credential = self;
375        credential.signature = Some(signer.sign(*credential.hash()?));
376        credential.issuer = signer.public();
377        Ok(credential)
378    }
379
380    /// Verify the signature of the credential against the issuer public key and expected hash.
381    ///
382    /// # Errors
383    /// Will error if the credential is not signed.
384    /// Will error if the credential cannot be hashed.
385    pub fn verify_signature(
386        &self,
387        expected_issuer_pubkey: &EdDSAPublicKey,
388    ) -> Result<bool, eyre::Error> {
389        if &self.issuer != expected_issuer_pubkey {
390            return Err(eyre::eyre!(
391                "Issuer public key does not match expected public key"
392            ));
393        }
394        if let Some(signature) = &self.signature {
395            return Ok(self.issuer.verify(*self.hash()?, signature));
396        }
397        Err(eyre::eyre!("Credential not signed"))
398    }
399
400    /// Compute the `sub` for a credential computed from `leaf_index` and a `blinding_factor`.
401    #[must_use]
402    pub fn compute_sub(leaf_index: u64, blinding_factor: FieldElement) -> FieldElement {
403        let mut input = [
404            *FieldElement::from_be_bytes_mod_order(SUB_DS_TAG),
405            leaf_index.into(),
406            *blinding_factor,
407        ];
408        poseidon2::bn254::t3::permutation_in_place(&mut input);
409        input[1].into()
410    }
411}
412
413impl Default for Credential {
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419/// Serializes the signature as compressed bytes (encoding r and s concatenated)
420/// where `r` is compressed to a single coordinate. Result is hex-encoded.
421#[expect(clippy::ref_option)]
422fn serialize_signature<S>(
423    signature: &Option<EdDSASignature>,
424    serializer: S,
425) -> Result<S::Ok, S::Error>
426where
427    S: Serializer,
428{
429    let Some(signature) = signature else {
430        return serializer.serialize_none();
431    };
432    let sig = signature
433        .to_compressed_bytes()
434        .map_err(serde::ser::Error::custom)?;
435    if serializer.is_human_readable() {
436        serializer.serialize_str(&hex::encode(sig))
437    } else {
438        serializer.serialize_bytes(&sig)
439    }
440}
441
442fn deserialize_signature<'de, D>(deserializer: D) -> Result<Option<EdDSASignature>, D::Error>
443where
444    D: Deserializer<'de>,
445{
446    let bytes: Option<Vec<u8>> = if deserializer.is_human_readable() {
447        Option::<String>::deserialize(deserializer)?
448            .map(|s| hex::decode(s).map_err(de::Error::custom))
449            .transpose()?
450    } else {
451        Option::<Vec<u8>>::deserialize(deserializer)?
452    };
453
454    let Some(bytes) = bytes else {
455        return Ok(None);
456    };
457
458    if bytes.len() != 64 {
459        return Err(de::Error::custom("Invalid signature. Expected 64 bytes."));
460    }
461
462    let mut arr = [0u8; 64];
463    arr.copy_from_slice(&bytes);
464    EdDSASignature::from_compressed_bytes(arr)
465        .map(Some)
466        .map_err(de::Error::custom)
467}
468
469fn serialize_public_key<S>(public_key: &EdDSAPublicKey, serializer: S) -> Result<S::Ok, S::Error>
470where
471    S: Serializer,
472{
473    let pk = public_key
474        .to_compressed_bytes()
475        .map_err(serde::ser::Error::custom)?;
476    if serializer.is_human_readable() {
477        serializer.serialize_str(&hex::encode(pk))
478    } else {
479        serializer.serialize_bytes(&pk)
480    }
481}
482
483fn deserialize_public_key<'de, D>(deserializer: D) -> Result<EdDSAPublicKey, D::Error>
484where
485    D: Deserializer<'de>,
486{
487    let bytes: Vec<u8> = if deserializer.is_human_readable() {
488        hex::decode(String::deserialize(deserializer)?).map_err(de::Error::custom)?
489    } else {
490        Vec::<u8>::deserialize(deserializer)?
491    };
492
493    if bytes.len() != 32 {
494        return Err(de::Error::custom("Invalid public key. Expected 32 bytes."));
495    }
496
497    let mut arr = [0u8; 32];
498    arr.copy_from_slice(&bytes);
499    EdDSAPublicKey::from_compressed_bytes(arr).map_err(de::Error::custom)
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    /// Tests the hash is deterministically computed for a default credential, this
507    /// helps detect issues where hashing inadvertently changed.
508    ///
509    /// Particularly relevant to ensure the introduction of other attributes into the last
510    /// item of the permutation does not bring in breaking changes.
511    #[test]
512    fn test_deterministic_credential_hash() {
513        let mut credential = Credential::new();
514        credential.id = 1;
515        assert_eq!(
516            hex::encode(credential.hash().unwrap().to_be_bytes()),
517            "2bc705762cbe8f31e0c3045ca347109ba3630b4b7ea955dc71515f182a079ae9"
518        );
519    }
520
521    #[test]
522    fn test_associated_data_matches_direct_hash() {
523        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
524
525        // Using the associated_data_commitment_from_raw_bytes method
526        let credential = Credential::new()
527            .associated_data_commitment_from_raw_bytes(&data)
528            .unwrap();
529
530        // Using the hash function directly
531        let direct_hash =
532            hash_bytes_to_field_element(ASSOCIATED_DATA_COMMITMENT_DS_TAG, &data).unwrap();
533
534        // Both should produce the same hash
535        assert_eq!(credential.associated_data_commitment, direct_hash);
536    }
537
538    #[test]
539    fn test_associated_data_method() {
540        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
541
542        let credential = Credential::new()
543            .associated_data_commitment_from_raw_bytes(&data)
544            .unwrap();
545
546        // Should have a non-zero associated data commitment
547        assert_ne!(credential.associated_data_commitment, FieldElement::ZERO);
548    }
549
550    #[test]
551    fn test_claim_matches_direct_hash() {
552        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
553
554        // Using the claim method
555        let credential = Credential::new().claim(0, &data).unwrap();
556
557        // Using the hash function directly
558        let direct_hash = hash_bytes_to_field_element(CLAIMS_HASH_DS_TAG, &data).unwrap();
559
560        // Both should produce the same hash
561        assert_eq!(credential.claims[0], direct_hash);
562    }
563
564    #[test]
565    fn test_claim_method() {
566        let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
567
568        let credential = Credential::new().claim(1, &data).unwrap();
569
570        // Should have a non-zero claim hash
571        assert_ne!(credential.claims[1], FieldElement::ZERO);
572    }
573
574    /// Tests that `issuer_version` is bound into the credential hash so that it
575    /// cannot be tampered with after issuance without invalidating the signature.
576    #[test]
577    fn test_issuer_version_is_bound_to_credential_hash() {
578        let mut credential = Credential::new();
579        credential.id = 1;
580        credential.issuer_version = 1;
581
582        let mut tampered = credential.clone();
583        tampered.issuer_version = 2;
584
585        let original_hash = credential.hash().unwrap();
586        let tampered_hash = tampered.hash().unwrap();
587        assert_ne!(original_hash, tampered_hash);
588
589        let signer = EdDSAPrivateKey::random(&mut rand::thread_rng());
590        let signed = credential.sign(&signer).unwrap();
591        let issuer_pubkey = signer.public();
592
593        assert!(signed.verify_signature(&issuer_pubkey).unwrap());
594
595        let mut tampered_signed = signed.clone();
596        tampered_signed.issuer_version = signed.issuer_version.wrapping_add(1);
597        assert!(
598            !tampered_signed.verify_signature(&issuer_pubkey).unwrap(),
599            "tampering with issuer_version must invalidate the signature"
600        );
601    }
602}