world_id_primitives/
credential.rs

1use ark_babyjubjub::EdwardsAffine;
2use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
3use ruint::aliases::U256;
4use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::{FieldElement, PrimitiveError};
7
8/// Version of the `Credential` object
9#[derive(Default, Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
10#[repr(u8)]
11pub enum CredentialVersion {
12    /// Version 1 of the `Credential`. In addition to the specific attributes,
13    /// - Hashing function: `Poseidon2`
14    /// - Signature scheme: `EdDSA` on `BabyJubJub` Curve
15    /// - Curve (Base) Field (`Fq`): `BabyJubJub` Curve Field (also the BN254 Scalar Field)
16    /// - Scalar Field (`Fr`): `BabyJubJub` Scalar Field
17    #[default]
18    V1 = 1,
19}
20
21/// Base representation of a `Credential` in the World ID Protocol.
22///
23/// A credential is generally a verifiable digital statement about a subject. It is
24/// the canonical object: everything a verifier needs for proofs and authorization.
25///
26/// In the case of World ID these statements are about humans, with the most common
27/// credentials being Orb verification or document verification.
28///
29/// Design Principles:
30/// - A credential clearly separates:
31///    - **Assertion** (the claim being made)
32///    - **Issuer** (who attests to it / vouches for it)
33///    - **Subject** (who it is about)
34///    - **Presenter binding** (who can present it)
35/// - Credentials are **usable across authenticators** without leaking correlate-able identifiers to RPs.
36/// - Revocation, expiry, and re-issuance are **first-class lifecycle properties**.
37/// - Flexibility: credentials may take different formats but share **common metadata** (validity, issuer, trust, type).
38///
39/// All credentials have an issuer and schema, identified with the `issuer_schema_id` field. This identifier
40/// is registered in the `CredentialSchemaIssuerRegistry` contract. It represents a particular schema issued by
41/// a particular issuer. Some schemas are intended to be global (e.g. representing an ICAO-compliant passport) and
42/// some issuer-specific. Schemas should be registered in the `CredentialSchemaIssuerRegistry` contract and should be
43/// publicly accessible.
44///
45/// We want to encourage schemas to be widely distributed and adopted. If everyone uses the same passport schema,
46/// for example, the Protocol will have better interoperability across passport credential issuers, reducing the
47/// burden on holders (to make sense of which passport they have), and similarly, RPs.
48#[derive(Debug, Serialize, Deserialize, Clone)]
49pub struct Credential {
50    /// Version representation of this structure
51    pub version: CredentialVersion,
52    /// Unique credential type id that is used to lookup of verifying information
53    pub issuer_schema_id: u64,
54    /// The subject (World ID) to which the credential is issued.
55    ///
56    /// This ID comes from the `AccountRegistry` and it's the `leaf_index` of the World ID on the Merkle tree.
57    pub sub: u64,
58    /// Timestamp of **first issuance** of this credential (unix seconds), i.e. this represents when the holder
59    /// first obtained the credential. Even if the credential has been issued multiple times (e.g. because of a renewal),
60    /// this timestamp should stay constant.
61    pub genesis_issued_at: u64,
62    /// Expiration timestamp (unix seconds)
63    pub expires_at: u64,
64    /// These are concrete statements that the issuer attests about the receiver.
65    /// Could be just commitments to data (e.g. passport image) or
66    /// the value directly (e.g. date of birth)
67    pub claims: Vec<FieldElement>,
68    /// If needed, can be used as commitment to the underlying data.
69    /// This can be useful to tie multiple proofs about the same data together.
70    pub associated_data_hash: FieldElement,
71    /// The signature of the credential (signed by the issuer's key)
72    #[serde(serialize_with = "serialize_signature")]
73    #[serde(deserialize_with = "deserialize_signature")]
74    #[serde(default)]
75    pub signature: Option<EdDSASignature>,
76    /// The issuer's public key of the credential.
77    pub issuer: EdDSAPublicKey,
78}
79
80impl Credential {
81    /// The maximum number of claims that can be included in a credential.
82    pub const MAX_CLAIMS: usize = 16;
83
84    /// Initializes a new credential.
85    ///
86    /// Note default fields occupy a sentinel value of `BaseField::zero()`
87    #[must_use]
88    pub fn new() -> Self {
89        Self {
90            version: CredentialVersion::V1,
91            issuer_schema_id: 0,
92            sub: 0,
93            genesis_issued_at: 0,
94            expires_at: 0,
95            claims: vec![FieldElement::ZERO; Self::MAX_CLAIMS],
96            associated_data_hash: FieldElement::ZERO,
97            signature: None,
98            issuer: EdDSAPublicKey {
99                pk: EdwardsAffine::default(),
100            },
101        }
102    }
103
104    /// Set the `version` of the credential.
105    #[must_use]
106    pub const fn version(mut self, version: CredentialVersion) -> Self {
107        self.version = version;
108        self
109    }
110
111    /// Set the `issuerSchemaId` of the credential.
112    #[must_use]
113    pub const fn issuer_schema_id(mut self, issuer_schema_id: u64) -> Self {
114        self.issuer_schema_id = issuer_schema_id;
115        self
116    }
117
118    /// Set the `sub` of the credential.
119    #[must_use]
120    pub const fn sub(mut self, sub: u64) -> Self {
121        self.sub = sub;
122        self
123    }
124
125    /// Set the genesis issued at of the credential.
126    #[must_use]
127    pub const fn genesis_issued_at(mut self, genesis_issued_at: u64) -> Self {
128        self.genesis_issued_at = genesis_issued_at;
129        self
130    }
131
132    /// Set the expires at of the credential.
133    #[must_use]
134    pub const fn expires_at(mut self, expires_at: u64) -> Self {
135        self.expires_at = expires_at;
136        self
137    }
138
139    /// Set a claim for the credential at an index.
140    ///
141    /// # Errors
142    /// Will error if the index is out of bounds.
143    pub fn claim(mut self, index: usize, claim: U256) -> Result<Self, PrimitiveError> {
144        if index >= self.claims.len() {
145            return Err(PrimitiveError::OutOfBounds);
146        }
147        self.claims[index] = claim.try_into().map_err(|_| PrimitiveError::NotInField)?;
148        Ok(self)
149    }
150
151    /// Set the associated data hash of the credential.
152    ///
153    /// # Errors
154    /// Will error if the provided hash cannot be lowered into the field.
155    pub fn associated_data_hash(
156        mut self,
157        associated_data_hash: U256,
158    ) -> Result<Self, PrimitiveError> {
159        self.associated_data_hash = associated_data_hash
160            .try_into()
161            .map_err(|_| PrimitiveError::NotInField)?;
162        Ok(self)
163    }
164
165    /// Get the credential domain separator for the given version.
166    #[must_use]
167    pub fn get_cred_ds(&self) -> FieldElement {
168        match self.version {
169            CredentialVersion::V1 => {
170                FieldElement::from_be_bytes_mod_order(b"POSEIDON2+EDDSA-BJJ+DLBE-v1")
171            }
172        }
173    }
174}
175
176impl Default for Credential {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182/// Serializes the signature as compressed bytes (encoding r and s concatenated)
183/// where `r` is compressed to a single coordinate. Result is hex-encoded.
184#[allow(clippy::ref_option)]
185fn serialize_signature<S>(
186    signature: &Option<EdDSASignature>,
187    serializer: S,
188) -> Result<S::Ok, S::Error>
189where
190    S: Serializer,
191{
192    let Some(signature) = signature else {
193        return serializer.serialize_none();
194    };
195    let sig = signature
196        .to_compressed_bytes()
197        .map_err(serde::ser::Error::custom)?;
198    serializer.serialize_str(&hex::encode(sig))
199}
200
201fn deserialize_signature<'de, D>(deserializer: D) -> Result<Option<EdDSASignature>, D::Error>
202where
203    D: Deserializer<'de>,
204{
205    let maybe_str = Option::<String>::deserialize(deserializer)?;
206
207    let Some(s) = maybe_str else { return Ok(None) };
208
209    let bytes = hex::decode(s).map_err(de::Error::custom)?;
210    if bytes.len() != 64 {
211        return Err(de::Error::custom("Invalid signature. Expected 64 bytes."));
212    }
213    let mut arr = [0u8; 64];
214    arr.copy_from_slice(&bytes);
215    let signature = EdDSASignature::from_compressed_bytes(arr).map_err(de::Error::custom)?;
216    Ok(Some(signature))
217}