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}