Skip to main content

world_id_primitives/
authenticator.rs

1use std::{
2    ops::{Deref, DerefMut},
3    str::FromStr,
4};
5
6use ark_babyjubjub::{EdwardsAffine, Fq};
7use ark_ff::{AdditiveGroup, PrimeField as _};
8use arrayvec::ArrayVec;
9use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
10use ruint::aliases::U256;
11use serde::{Deserialize, Serialize};
12
13use crate::{FieldElement, PrimitiveError};
14
15/// The maximum number of authenticator public keys that can be registered at any time
16/// per World ID Account.
17///
18/// This constrained is introduced to maintain proof performance reasonable even
19/// in devices with limited resources.
20pub const MAX_AUTHENTICATOR_KEYS: usize = 7;
21
22/// Errors for decoding sparse authenticator pubkey slots.
23#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
24pub enum SparseAuthenticatorPubkeysError {
25    /// The input contains a non-empty slot index outside supported bounds.
26    #[error(
27        "invalid authenticator pubkey slot {slot_index}; max supported slot is {max_supported_slot}"
28    )]
29    SlotOutOfBounds {
30        /// Slot index returned by the source.
31        slot_index: usize,
32        /// Highest supported slot index.
33        max_supported_slot: usize,
34    },
35    /// A slot contained bytes that are not a valid compressed public key.
36    #[error("invalid authenticator public key at slot {slot_index}: {reason}")]
37    InvalidCompressedPubkey {
38        /// Slot index of the invalid entry.
39        slot_index: usize,
40        /// Parse error details.
41        reason: String,
42    },
43}
44
45/// Domain separator for the authenticator OPRF query digest.
46const OPRF_QUERY_DS: &[u8] = b"World ID Query";
47
48/// Computes the Poseidon2 digest for an authenticator OPRF query.
49///
50/// # Arguments
51/// * `leaf_index` - The leaf index of the authenticator in the World ID Registry.
52/// * `action` - The action field element.
53/// * `query_origin_id` - The `RpId` or `issuer_schema_id`.
54#[must_use]
55pub fn oprf_query_digest(
56    leaf_index: u64,
57    action: FieldElement,
58    query_origin_id: FieldElement,
59) -> FieldElement {
60    let input = [
61        ark_babyjubjub::Fq::from_be_bytes_mod_order(OPRF_QUERY_DS),
62        leaf_index.into(),
63        *query_origin_id,
64        *action,
65    ];
66    poseidon2::bn254::t4::permutation(&input)[1].into()
67}
68
69/// Decodes sparse authenticator pubkey slots while preserving slot positions.
70///
71/// Input may contain `None` entries for removed authenticators. Trailing empty slots are trimmed,
72/// interior holes are preserved as `None`, and used slots beyond [`MAX_AUTHENTICATOR_KEYS`]
73/// are rejected.
74///
75/// # Errors
76/// Returns [`SparseAuthenticatorPubkeysError`] if a used slot is out of bounds or any compressed
77/// key bytes are invalid.
78pub fn decode_sparse_authenticator_pubkeys(
79    pubkeys: Vec<Option<U256>>,
80) -> Result<AuthenticatorPublicKeySet, SparseAuthenticatorPubkeysError> {
81    let last_present_idx = pubkeys.iter().rposition(Option::is_some);
82    if let Some(idx) = last_present_idx
83        && idx >= MAX_AUTHENTICATOR_KEYS
84    {
85        return Err(SparseAuthenticatorPubkeysError::SlotOutOfBounds {
86            slot_index: idx,
87            max_supported_slot: MAX_AUTHENTICATOR_KEYS - 1,
88        });
89    }
90
91    let normalized_len = last_present_idx.map_or(0, |idx| idx + 1);
92    let decoded_pubkeys = pubkeys
93        .into_iter()
94        .take(normalized_len)
95        .enumerate()
96        .map(|(idx, pubkey)| match pubkey {
97            Some(pubkey) => EdDSAPublicKey::from_compressed_bytes(pubkey.to_le_bytes())
98                .map(Some)
99                .map_err(
100                    |e| SparseAuthenticatorPubkeysError::InvalidCompressedPubkey {
101                        slot_index: idx,
102                        reason: e.to_string(),
103                    },
104                ),
105            None => Ok(None),
106        })
107        .collect::<Result<Vec<_>, _>>()?;
108
109    Ok(AuthenticatorPublicKeySet(
110        decoded_pubkeys
111            .into_iter()
112            .collect::<ArrayVec<_, MAX_AUTHENTICATOR_KEYS>>(),
113    ))
114}
115
116/// A set of **off-chain** authenticator public keys for a World ID Account.
117///
118/// Each World ID Account has a number of public keys for each authorized authenticator;
119/// a commitment to the entire set of public keys is stored in the `WorldIDRegistry` contract.
120///
121/// Removed authenticator slots are represented as `None` to preserve stable slot indices.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct AuthenticatorPublicKeySet(ArrayVec<Option<EdDSAPublicKey>, MAX_AUTHENTICATOR_KEYS>);
124
125impl Default for AuthenticatorPublicKeySet {
126    fn default() -> Self {
127        Self(ArrayVec::new())
128    }
129}
130
131impl AuthenticatorPublicKeySet {
132    /// Creates a new authenticator public key set with the provided active public keys.
133    ///
134    /// The provided keys are inserted in order.
135    /// Sparse holes can be represented later via [`Self::try_clear_at_index`].
136    ///
137    /// # Errors
138    /// Returns an error if the number of public keys exceeds [`MAX_AUTHENTICATOR_KEYS`].
139    pub fn new(pubkeys: Vec<EdDSAPublicKey>) -> Result<Self, PrimitiveError> {
140        if pubkeys.len() > MAX_AUTHENTICATOR_KEYS {
141            return Err(PrimitiveError::OutOfBounds);
142        }
143
144        Ok(Self(
145            pubkeys
146                .into_iter()
147                .map(Some)
148                .collect::<ArrayVec<_, MAX_AUTHENTICATOR_KEYS>>(),
149        ))
150    }
151
152    /// Converts the set of public keys to a fixed-length array of Affine points.
153    ///
154    /// This is usually used to serialize to circuit inputs, which require defaulted Affine points
155    /// for empty (`None`) slots.
156    pub fn as_affine_array(&self) -> [EdwardsAffine; MAX_AUTHENTICATOR_KEYS] {
157        let mut array = [EdwardsAffine::default(); MAX_AUTHENTICATOR_KEYS];
158        for (i, pubkey) in self.0.iter().enumerate() {
159            array[i] = pubkey
160                .as_ref()
161                .map_or_else(EdwardsAffine::default, |pubkey| pubkey.pk);
162        }
163        array
164    }
165
166    /// Returns the number of public keys in the set.
167    #[must_use]
168    pub const fn len(&self) -> usize {
169        self.0.len()
170    }
171
172    /// Returns `true` if the set is empty.
173    #[must_use]
174    pub const fn is_empty(&self) -> bool {
175        self.0.is_empty()
176    }
177
178    /// Returns the public key at the given index.
179    ///
180    /// It will return `None` if the index is out of bounds, even if it's less than `MAX_AUTHENTICATOR_KEYS` but
181    /// the key is not initialized.
182    #[must_use]
183    pub fn get(&self, index: usize) -> Option<&EdDSAPublicKey> {
184        self.0.get(index).and_then(Option::as_ref)
185    }
186
187    /// Sets a new public key at the given index if it's within bounds of the initialized set.
188    ///
189    /// # Errors
190    /// Returns an error if the index is out of bounds.
191    pub fn try_set_at_index(
192        &mut self,
193        index: usize,
194        pubkey: EdDSAPublicKey,
195    ) -> Result<(), PrimitiveError> {
196        if index >= self.len() || index >= MAX_AUTHENTICATOR_KEYS {
197            return Err(PrimitiveError::OutOfBounds);
198        }
199        self.0[index] = Some(pubkey);
200        Ok(())
201    }
202
203    /// Clears the public key at the given index while preserving slot position.
204    ///
205    /// # Errors
206    /// Returns an error if the index is out of bounds.
207    pub fn try_clear_at_index(&mut self, index: usize) -> Result<(), PrimitiveError> {
208        if index >= self.len() || index >= MAX_AUTHENTICATOR_KEYS {
209            return Err(PrimitiveError::OutOfBounds);
210        }
211        self.0[index] = None;
212        Ok(())
213    }
214
215    /// Pushes a new public key onto the set.
216    ///
217    /// # Errors
218    /// Returns an error if the set is full.
219    pub fn try_push(&mut self, pubkey: EdDSAPublicKey) -> Result<(), PrimitiveError> {
220        self.0
221            .try_push(Some(pubkey))
222            .map_err(|_| PrimitiveError::OutOfBounds)
223    }
224
225    /// Computes the Poseidon2 leaf hash commitment for this key set as stored in the `WorldIDRegistry`.
226    ///
227    /// # Panics
228    /// Panics if the domain separator constant cannot be converted into an `Fq`.
229    #[must_use]
230    pub fn leaf_hash(&self) -> Fq {
231        let mut input = [Fq::ZERO; 16];
232
233        input[0] =
234            Fq::from_str("105702839725298824521994315").expect("domain separator fits in field");
235
236        let pk_array = self.as_affine_array();
237        for i in 0..MAX_AUTHENTICATOR_KEYS {
238            input[i * 2 + 1] = pk_array[i].x;
239            input[i * 2 + 2] = pk_array[i].y;
240        }
241
242        poseidon2::bn254::t16::permutation(&input)[1]
243    }
244}
245
246impl Deref for AuthenticatorPublicKeySet {
247    type Target = ArrayVec<Option<EdDSAPublicKey>, MAX_AUTHENTICATOR_KEYS>;
248    fn deref(&self) -> &Self::Target {
249        &self.0
250    }
251}
252
253impl DerefMut for AuthenticatorPublicKeySet {
254    fn deref_mut(&mut self) -> &mut Self::Target {
255        &mut self.0
256    }
257}
258
259/// Enables entities that sign messages within the Protocol for use with the ZK circuits.
260///
261/// This is in particular used by Authenticators to authorize requests for nullifier generation.
262pub trait ProtocolSigner {
263    /// Signs a message with the protocol signer using the `EdDSA` scheme (**off-chain** signer), for use
264    /// with the Protocol ZK circuits.
265    fn sign(&self, message: FieldElement) -> EdDSASignature
266    where
267        Self: Sized + Send + Sync;
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use crate::Signer;
274    use ark_serialize::CanonicalSerialize as _;
275
276    fn create_test_pubkey() -> EdDSAPublicKey {
277        EdDSAPublicKey {
278            pk: EdwardsAffine::default(),
279        }
280    }
281
282    fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
283        Signer::from_seed_bytes(&[seed_byte; 32])
284            .unwrap()
285            .offchain_signer_pubkey()
286    }
287
288    fn encoded_test_pubkey(seed_byte: u8) -> U256 {
289        let mut compressed = Vec::new();
290        test_pubkey(seed_byte)
291            .pk
292            .serialize_compressed(&mut compressed)
293            .unwrap();
294        U256::from_le_slice(&compressed)
295    }
296
297    #[test]
298    fn test_try_set_at_index_within_bounds() {
299        let mut key_set = AuthenticatorPublicKeySet::default();
300        let pubkey = create_test_pubkey();
301
302        // Setting at index 0 when empty should fail (index >= len)
303        let result = key_set.try_set_at_index(0, pubkey.clone());
304        assert!(result.is_err(), "Should not panic, should return error");
305
306        // Push a key first
307        key_set.try_push(pubkey.clone()).unwrap();
308
309        // Now setting at index 0 should succeed
310        key_set.try_set_at_index(0, pubkey).unwrap();
311    }
312
313    #[test]
314    fn test_try_set_at_index_at_length() {
315        let mut key_set = AuthenticatorPublicKeySet::default();
316        let pubkey = create_test_pubkey();
317
318        // Push one key
319        key_set.try_push(pubkey.clone()).unwrap();
320
321        // Try to set at index == len (should fail, not panic)
322        let result = key_set.try_set_at_index(1, pubkey);
323        assert!(result.is_err(), "Should not panic when index equals length");
324    }
325
326    #[test]
327    fn test_try_set_at_index_out_of_bounds() {
328        let mut key_set = AuthenticatorPublicKeySet::default();
329        let pubkey = create_test_pubkey();
330
331        // Try to set at index beyond MAX_AUTHENTICATOR_KEYS
332        let result = key_set.try_set_at_index(MAX_AUTHENTICATOR_KEYS, pubkey.clone());
333        assert!(
334            result.is_err(),
335            "Should not panic when index >= MAX_AUTHENTICATOR_KEYS"
336        );
337
338        let result = key_set.try_set_at_index(MAX_AUTHENTICATOR_KEYS + 1, pubkey);
339        assert!(
340            result.is_err(),
341            "Should not panic when index > MAX_AUTHENTICATOR_KEYS"
342        );
343    }
344
345    #[test]
346    fn test_try_push_within_capacity() {
347        let mut key_set = AuthenticatorPublicKeySet::default();
348        let pubkey = create_test_pubkey();
349
350        // Should be able to push up to MAX_AUTHENTICATOR_KEYS without panicking
351        for i in 0..MAX_AUTHENTICATOR_KEYS {
352            let result = key_set.try_push(pubkey.clone());
353            assert!(
354                result.is_ok(),
355                "Should not panic when pushing element {} of {}",
356                i + 1,
357                MAX_AUTHENTICATOR_KEYS
358            );
359        }
360
361        assert_eq!(key_set.len(), MAX_AUTHENTICATOR_KEYS);
362
363        let result = key_set.try_push(pubkey);
364        assert!(result.is_err()); // should return an error because the set is full, but not panic
365    }
366
367    #[test]
368    fn test_as_affine_array_empty_set() {
369        let key_set = AuthenticatorPublicKeySet::default();
370        let array = key_set.as_affine_array();
371
372        // All elements should be default
373        assert_eq!(array.len(), MAX_AUTHENTICATOR_KEYS);
374        for affine in &array {
375            assert_eq!(*affine, EdwardsAffine::default());
376        }
377    }
378
379    #[test]
380    fn test_as_affine_array_partial_set() {
381        let mut key_set = AuthenticatorPublicKeySet::default();
382        let pubkey = create_test_pubkey();
383
384        // Add 3 keys
385        for _ in 0..3 {
386            key_set.try_push(pubkey.clone()).unwrap();
387        }
388
389        let array = key_set.as_affine_array();
390
391        // Array should have correct length
392        assert_eq!(array.len(), MAX_AUTHENTICATOR_KEYS);
393
394        // First 3 should match the pubkey
395        for item in array.iter().take(3) {
396            assert_eq!(item, &pubkey.pk);
397        }
398
399        for item in array.iter().take(MAX_AUTHENTICATOR_KEYS).skip(3) {
400            assert_eq!(item, &EdwardsAffine::default());
401        }
402    }
403
404    #[test]
405    fn test_decode_sparse_pubkeys_trims_trailing_empty_slots() {
406        let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
407        encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
408
409        let key_set = decode_sparse_authenticator_pubkeys(encoded_pubkeys).unwrap();
410
411        assert_eq!(key_set.len(), 2);
412        assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
413        assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
414    }
415
416    #[test]
417    fn test_decode_sparse_pubkeys_preserves_interior_holes() {
418        let key_set = decode_sparse_authenticator_pubkeys(vec![
419            Some(encoded_test_pubkey(1)),
420            None,
421            Some(encoded_test_pubkey(2)),
422        ])
423        .unwrap();
424
425        assert_eq!(key_set.len(), 3);
426        assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
427        assert_eq!(key_set[1], None);
428        assert_eq!(key_set[2].as_ref().unwrap().pk, test_pubkey(2).pk);
429    }
430
431    #[test]
432    fn test_decode_sparse_pubkeys_rejects_used_slot_beyond_max() {
433        let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
434        encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
435
436        let error = decode_sparse_authenticator_pubkeys(encoded_pubkeys).unwrap_err();
437        assert!(matches!(
438            error,
439            SparseAuthenticatorPubkeysError::SlotOutOfBounds {
440                slot_index,
441                max_supported_slot
442            } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
443        ));
444    }
445}