Skip to main content

sui_crypto/zklogin/
verify.rs

1use std::collections::HashMap;
2use std::str::FromStr;
3use std::sync::LazyLock;
4use std::sync::RwLock;
5
6use crate::SignatureError;
7use ark_bn254::Fq;
8use ark_bn254::Fq2;
9use ark_bn254::Fr;
10use ark_bn254::G1Affine;
11use ark_bn254::G1Projective;
12use ark_bn254::G2Affine;
13use ark_bn254::G2Projective;
14use ark_ff::PrimeField;
15use ark_groth16::PreparedVerifyingKey;
16use ark_groth16::Proof;
17use sui_sdk_types::Bn254FieldElement;
18use sui_sdk_types::CircomG1;
19use sui_sdk_types::CircomG2;
20use sui_sdk_types::Ed25519PublicKey;
21use sui_sdk_types::Jwk;
22use sui_sdk_types::Secp256k1PublicKey;
23use sui_sdk_types::Secp256r1PublicKey;
24use sui_sdk_types::SimpleSignature;
25use sui_sdk_types::U256;
26use sui_sdk_types::ZkLoginInputs;
27use sui_sdk_types::ZkLoginProof;
28
29use super::POSEIDON;
30
31#[derive(Clone, Debug, PartialEq, Default)]
32pub struct VerifyingKey {
33    inner: PreparedVerifyingKey<ark_bn254::Bn254>,
34}
35
36const fn str_to_bn254(s: &str) -> Bn254FieldElement {
37    match Bn254FieldElement::from_str_radix_10(s) {
38        Ok(e) => e,
39        Err(_) => panic!("unable to convert bn254"),
40    }
41}
42
43const fn build_circom_g1([e0, e1, e2]: [&str; 3]) -> CircomG1 {
44    CircomG1([str_to_bn254(e0), str_to_bn254(e1), str_to_bn254(e2)])
45}
46
47const fn build_circom_g2([[e00, e01], [e10, e11], [e20, e21]]: [[&str; 2]; 3]) -> CircomG2 {
48    CircomG2([
49        [str_to_bn254(e00), str_to_bn254(e01)],
50        [str_to_bn254(e10), str_to_bn254(e11)],
51        [str_to_bn254(e20), str_to_bn254(e21)],
52    ])
53}
54
55fn circom_to_arkworks_g1(g1: &CircomG1) -> Result<G1Affine, SignatureError> {
56    let CircomG1([f0, f1, f2]) = g1;
57
58    let g1: G1Affine =
59        G1Projective::new_unchecked(bn254_to_fq(f0), bn254_to_fq(f1), bn254_to_fq(f2)).into();
60
61    if !g1.is_on_curve() || !g1.is_in_correct_subgroup_assuming_on_curve() {
62        return Err(SignatureError::from_source("invalid G1 input"));
63    }
64
65    Ok(g1)
66}
67
68fn circom_to_arkworks_g2(g2: &CircomG2) -> Result<G2Affine, SignatureError> {
69    let CircomG2([[f00, f01], [f10, f11], [f20, f21]]) = g2;
70
71    let g2: G2Affine = G2Projective::new_unchecked(
72        Fq2::new(bn254_to_fq(f00), bn254_to_fq(f01)),
73        Fq2::new(bn254_to_fq(f10), bn254_to_fq(f11)),
74        Fq2::new(bn254_to_fq(f20), bn254_to_fq(f21)),
75    )
76    .into();
77
78    if !g2.is_on_curve() || !g2.is_in_correct_subgroup_assuming_on_curve() {
79        return Err(SignatureError::from_source("invalid G2 input"));
80    }
81
82    Ok(g2)
83}
84
85fn bn254_to_fq(f: &Bn254FieldElement) -> Fq {
86    Fq::from_be_bytes_mod_order(f.padded())
87}
88
89fn bn254_to_fr(f: &Bn254FieldElement) -> Fr {
90    Fr::from_be_bytes_mod_order(f.padded())
91}
92
93fn mainnet_verifying_key() -> VerifyingKey {
94    const CIRCOM_ALPHA_G1: CircomG1 = build_circom_g1([
95        "21529901943976716921335152104180790524318946701278905588288070441048877064089",
96        "7775817982019986089115946956794180159548389285968353014325286374017358010641",
97        "1",
98    ]);
99
100    const CIRCOM_BETA_G2: CircomG2 = build_circom_g2([
101        [
102            "6600437987682835329040464538375790690815756241121776438004683031791078085074",
103            "16207344858883952201936462217289725998755030546200154201671892670464461194903",
104        ],
105        [
106            "17943105074568074607580970189766801116106680981075272363121544016828311544390",
107            "18339640667362802607939727433487930605412455701857832124655129852540230493587",
108        ],
109        ["1", "0"],
110    ]);
111
112    const CIRCOM_GAMMA_G2: CircomG2 = build_circom_g2([
113        [
114            "10857046999023057135944570762232829481370756359578518086990519993285655852781",
115            "11559732032986387107991004021392285783925812861821192530917403151452391805634",
116        ],
117        [
118            "8495653923123431417604973247489272438418190587263600148770280649306958101930",
119            "4082367875863433681332203403145435568316851327593401208105741076214120093531",
120        ],
121        ["1", "0"],
122    ]);
123
124    const CIRCOM_DELTA_G2: CircomG2 = build_circom_g2([
125        [
126            "19260309516619721648285279557078789954438346514188902804737557357941293711874",
127            "2480422554560175324649200374556411861037961022026590718777465211464278308900",
128        ],
129        [
130            "14489104692423540990601374549557603533921811847080812036788172274404299703364",
131            "12564378633583954025611992187142343628816140907276948128970903673042690269191",
132        ],
133        ["1", "0"],
134    ]);
135
136    const CIRCOM_GAMMA_ABC_G1: [CircomG1; 2] = [
137        build_circom_g1([
138            "1607694606386445293170795095076356565829000940041894770459712091642365695804",
139            "18066827569413962196795937356879694709963206118612267170825707780758040578649",
140            "1",
141        ]),
142        build_circom_g1([
143            "20653794344898475822834426774542692225449366952113790098812854265588083247207",
144            "3296759704176575765409730962060698204792513807296274014163938591826372646699",
145            "1",
146        ]),
147    ];
148
149    let vk = ark_groth16::VerifyingKey {
150        alpha_g1: circom_to_arkworks_g1(&CIRCOM_ALPHA_G1).unwrap(),
151        beta_g2: circom_to_arkworks_g2(&CIRCOM_BETA_G2).unwrap(),
152        gamma_g2: circom_to_arkworks_g2(&CIRCOM_GAMMA_G2).unwrap(),
153        delta_g2: circom_to_arkworks_g2(&CIRCOM_DELTA_G2).unwrap(),
154        gamma_abc_g1: CIRCOM_GAMMA_ABC_G1
155            .iter()
156            .map(circom_to_arkworks_g1)
157            .collect::<Result<_, _>>()
158            .unwrap(),
159    };
160
161    VerifyingKey {
162        inner: PreparedVerifyingKey::from(vk),
163    }
164}
165
166/// Load a fixed verifying key from zkLogin.vkey output. This is based on a local setup and should not use in production.
167fn dev_verifying_key() -> VerifyingKey {
168    const CIRCOM_ALPHA_G1: CircomG1 = build_circom_g1([
169        "20491192805390485299153009773594534940189261866228447918068658471970481763042",
170        "9383485363053290200918347156157836566562967994039712273449902621266178545958",
171        "1",
172    ]);
173
174    const CIRCOM_BETA_G2: CircomG2 = build_circom_g2([
175        [
176            "6375614351688725206403948262868962793625744043794305715222011528459656738731",
177            "4252822878758300859123897981450591353533073413197771768651442665752259397132",
178        ],
179        [
180            "10505242626370262277552901082094356697409835680220590971873171140371331206856",
181            "21847035105528745403288232691147584728191162732299865338377159692350059136679",
182        ],
183        ["1", "0"],
184    ]);
185
186    const CIRCOM_GAMMA_G2: CircomG2 = build_circom_g2([
187        [
188            "10857046999023057135944570762232829481370756359578518086990519993285655852781",
189            "11559732032986387107991004021392285783925812861821192530917403151452391805634",
190        ],
191        [
192            "8495653923123431417604973247489272438418190587263600148770280649306958101930",
193            "4082367875863433681332203403145435568316851327593401208105741076214120093531",
194        ],
195        ["1", "0"],
196    ]);
197
198    const CIRCOM_DELTA_G2: CircomG2 = build_circom_g2([
199        [
200            "10857046999023057135944570762232829481370756359578518086990519993285655852781",
201            "11559732032986387107991004021392285783925812861821192530917403151452391805634",
202        ],
203        [
204            "8495653923123431417604973247489272438418190587263600148770280649306958101930",
205            "4082367875863433681332203403145435568316851327593401208105741076214120093531",
206        ],
207        ["1", "0"],
208    ]);
209
210    const CIRCOM_GAMMA_ABC_G1: [CircomG1; 2] = [
211        build_circom_g1([
212            "20701306374481714853949730154526815782802808896228594855451770849676897643964",
213            "2766989084754673216772682210231588284954002353414778477810174100808747060165",
214            "1",
215        ]),
216        build_circom_g1([
217            "501195541410525737371980194958674422793469475773065719916327137354779402600",
218            "13527631693157515024233848630878973193664410306029731429350155106228769355415",
219            "1",
220        ]),
221    ];
222
223    let vk = ark_groth16::VerifyingKey {
224        alpha_g1: circom_to_arkworks_g1(&CIRCOM_ALPHA_G1).unwrap(),
225        beta_g2: circom_to_arkworks_g2(&CIRCOM_BETA_G2).unwrap(),
226        gamma_g2: circom_to_arkworks_g2(&CIRCOM_GAMMA_G2).unwrap(),
227        delta_g2: circom_to_arkworks_g2(&CIRCOM_DELTA_G2).unwrap(),
228        gamma_abc_g1: CIRCOM_GAMMA_ABC_G1
229            .iter()
230            .map(circom_to_arkworks_g1)
231            .collect::<Result<_, _>>()
232            .unwrap(),
233    };
234
235    VerifyingKey::new(PreparedVerifyingKey::from(vk))
236}
237
238/// V2 verifying key for test environments, based on zklogin-circuits v2-main branch
239/// artifacts/dev/zkLogin.vkey (finalized v2 circuit). This is based on a local setup and should
240/// not be used in production.
241fn dev_verifying_key_v2() -> VerifyingKey {
242    const CIRCOM_ALPHA_G1: CircomG1 = build_circom_g1([
243        "20491192805390485299153009773594534940189261866228447918068658471970481763042",
244        "9383485363053290200918347156157836566562967994039712273449902621266178545958",
245        "1",
246    ]);
247
248    const CIRCOM_BETA_G2: CircomG2 = build_circom_g2([
249        [
250            "6375614351688725206403948262868962793625744043794305715222011528459656738731",
251            "4252822878758300859123897981450591353533073413197771768651442665752259397132",
252        ],
253        [
254            "10505242626370262277552901082094356697409835680220590971873171140371331206856",
255            "21847035105528745403288232691147584728191162732299865338377159692350059136679",
256        ],
257        ["1", "0"],
258    ]);
259
260    const CIRCOM_GAMMA_G2: CircomG2 = build_circom_g2([
261        [
262            "10857046999023057135944570762232829481370756359578518086990519993285655852781",
263            "11559732032986387107991004021392285783925812861821192530917403151452391805634",
264        ],
265        [
266            "8495653923123431417604973247489272438418190587263600148770280649306958101930",
267            "4082367875863433681332203403145435568316851327593401208105741076214120093531",
268        ],
269        ["1", "0"],
270    ]);
271
272    const CIRCOM_DELTA_G2: CircomG2 = build_circom_g2([
273        [
274            "10857046999023057135944570762232829481370756359578518086990519993285655852781",
275            "11559732032986387107991004021392285783925812861821192530917403151452391805634",
276        ],
277        [
278            "8495653923123431417604973247489272438418190587263600148770280649306958101930",
279            "4082367875863433681332203403145435568316851327593401208105741076214120093531",
280        ],
281        ["1", "0"],
282    ]);
283
284    const CIRCOM_GAMMA_ABC_G1: [CircomG1; 2] = [
285        build_circom_g1([
286            "7494019946711010262111829149650251402606293243816947408234427261113297488209",
287            "15277037309547978131462479061393755632702482465205499508808087107719455935457",
288            "1",
289        ]),
290        build_circom_g1([
291            "11337299590256247592846534480065240790944417381563410255189124427241414159992",
292            "10513214342948426742825684049934611922239382370302051503558098869981509341761",
293            "1",
294        ]),
295    ];
296
297    let vk = ark_groth16::VerifyingKey {
298        alpha_g1: circom_to_arkworks_g1(&CIRCOM_ALPHA_G1).unwrap(),
299        beta_g2: circom_to_arkworks_g2(&CIRCOM_BETA_G2).unwrap(),
300        gamma_g2: circom_to_arkworks_g2(&CIRCOM_GAMMA_G2).unwrap(),
301        delta_g2: circom_to_arkworks_g2(&CIRCOM_DELTA_G2).unwrap(),
302        gamma_abc_g1: CIRCOM_GAMMA_ABC_G1
303            .iter()
304            .map(circom_to_arkworks_g1)
305            .collect::<Result<_, _>>()
306            .unwrap(),
307    };
308
309    VerifyingKey::new(PreparedVerifyingKey::from(vk))
310}
311
312impl VerifyingKey {
313    fn new(inner: PreparedVerifyingKey<ark_bn254::Bn254>) -> Self {
314        Self { inner }
315    }
316
317    pub fn new_mainnet() -> Self {
318        mainnet_verifying_key()
319    }
320
321    pub fn new_dev_for(version: CircuitVersion) -> Self {
322        match version {
323            CircuitVersion::V1 => dev_verifying_key(),
324            CircuitVersion::V2 => dev_verifying_key_v2(),
325        }
326    }
327
328    pub fn verify_zklogin(
329        &self,
330        jwk: &Jwk,
331        inputs: &ZkLoginInputs,
332        signature: &SimpleSignature,
333        max_epoch: u64,
334        circuit_version: CircuitVersion,
335    ) -> Result<(), SignatureError> {
336        use base64ct::Base64UrlUnpadded;
337        use base64ct::Encoding;
338        // Decode modulus to bytes.
339        let modulus = Base64UrlUnpadded::decode_vec(&jwk.n)
340            .map_err(|e| SignatureError::from_source(e.to_string()))?;
341
342        let proof = zklogin_proof_to_arkworks(inputs.proof_points())?;
343        let input_hash =
344            calculate_all_inputs_hash(inputs, signature, &modulus, max_epoch, circuit_version)?;
345
346        self.verify_proof(&proof, &[input_hash])
347    }
348
349    fn verify_proof(
350        &self,
351        proof: &Proof<ark_bn254::Bn254>,
352        public_inputs: &[ark_bn254::Fr],
353    ) -> Result<(), SignatureError> {
354        use ark_snark::SNARK;
355
356        if ark_groth16::Groth16::<ark_bn254::Bn254>::verify_with_processed_vk(
357            &self.inner,
358            public_inputs,
359            proof,
360        )
361        .map_err(|e| SignatureError::from_source(e.to_string()))?
362        {
363            Ok(())
364        } else {
365            Err(SignatureError::from_source("Groth16 proof verify failed"))
366        }
367    }
368}
369
370fn zklogin_proof_to_arkworks(
371    proof: &ZkLoginProof,
372) -> Result<Proof<ark_bn254::Bn254>, SignatureError> {
373    Ok(Proof {
374        a: circom_to_arkworks_g1(&proof.a)?,
375        b: circom_to_arkworks_g2(&proof.b)?,
376        c: circom_to_arkworks_g1(&proof.c)?,
377    })
378}
379
380/// Given a SimpleSignature convert the corrisponding public key, prefixed with the signature
381/// scheme flag, to two Bn254Frs
382fn public_key_to_frs(signature: &SimpleSignature) -> Result<(Fr, Fr), SignatureError> {
383    // buf length of the longest public key secp256r1/secp256k1 of 33 bytes plus 1 byte for the
384    // scheme
385    let mut buf = [0u8; 34];
386
387    buf[0] = signature.scheme().to_u8();
388
389    let buf = match signature {
390        SimpleSignature::Ed25519 { public_key, .. } => {
391            buf[1..Ed25519PublicKey::LENGTH + 1].copy_from_slice(public_key.inner());
392            &buf[..Ed25519PublicKey::LENGTH + 1]
393        }
394        SimpleSignature::Secp256k1 { public_key, .. } => {
395            buf[1..Secp256k1PublicKey::LENGTH + 1].copy_from_slice(public_key.inner());
396            &buf[..Secp256k1PublicKey::LENGTH + 1]
397        }
398        SimpleSignature::Secp256r1 { public_key, .. } => {
399            buf[1..Secp256r1PublicKey::LENGTH + 1].copy_from_slice(public_key.inner());
400            &buf[..Secp256r1PublicKey::LENGTH + 1]
401        }
402        _ => return Err(SignatureError::from_source("unknown signature scheme")),
403    };
404
405    //TODO this comment is wrong...
406    // Split the bytes deterministically such that the first element contains the first 128
407    // bits of the hash, and the second element contains the latter ones.
408    let (first_half, second_half) = buf.split_at(buf.len() - 16);
409
410    let eph_public_key_0 = Fr::from_be_bytes_mod_order(first_half);
411    let eph_public_key_1 = Fr::from_be_bytes_mod_order(second_half);
412    Ok((eph_public_key_0, eph_public_key_1))
413}
414
415pub(crate) type U8192 = bnum::BUintD8<1024>;
416
417const PACK_WIDTH: u8 = 248;
418const V1_MAX_EXT_ISS_LEN: u16 = 165;
419const V2_MAX_EXT_ISS_LEN: u16 = 186;
420
421/// Circuit version, which determines the layout of the `all_inputs_hash` public input.
422#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
423pub enum CircuitVersion {
424    /// V1 circuit: the iss claim is folded in as its *base64* value and `index_mod_4` is folded
425    /// into the hash. There is no `rsa_num_bits` field.
426    V1,
427    /// V2 circuit: the iss claim is folded in as its *decoded* extended claim, `index_mod_4` is
428    /// dropped, and the actual RSA modulus bit length (`rsa_num_bits`) is folded in instead.
429    V2,
430}
431
432/// Circuit-specific parameters, obtained via [`CircuitVersion::config`].
433struct CircuitConfig {
434    /// Maximum header length in base64.
435    max_header_len_b64: u16,
436    /// Maximum length passed to the iss hash. For [`CircuitVersion::V1`] this is the base64
437    /// length of the iss claim; for [`CircuitVersion::V2`] it is the byte length of the *decoded*
438    /// extended iss claim (the circuit's `maxExtIssLength`).
439    max_iss_len: u16,
440    /// Maximum RSA key size in bits. The modulus is padded to this size before hashing.
441    max_rsa_bits: u16,
442}
443
444impl CircuitVersion {
445    fn config(&self) -> CircuitConfig {
446        match self {
447            CircuitVersion::V1 => CircuitConfig {
448                max_header_len_b64: 248,
449                max_iss_len: 4 * (1 + V1_MAX_EXT_ISS_LEN / 3),
450                max_rsa_bits: 2048,
451            },
452            CircuitVersion::V2 => CircuitConfig {
453                max_header_len_b64: 279,
454                max_iss_len: V2_MAX_EXT_ISS_LEN,
455                max_rsa_bits: 8192,
456            },
457        }
458    }
459}
460
461/// Pads a stream of bytes and maps it to a field element
462pub fn hash_ascii_str_to_field(s: &str, max_size: u16) -> Result<Fr, SignatureError> {
463    let str_padded = str_to_padded_char_codes(s, max_size)?;
464    hash_to_field(&str_padded, 8, PACK_WIDTH)
465}
466
467fn str_to_padded_char_codes(s: &str, max_len: u16) -> Result<Vec<U256>, SignatureError> {
468    let arr: Vec<U256> = s.bytes().map(U256::from).collect();
469    pad_with_zeroes(arr, max_len)
470}
471
472fn pad_with_zeroes(in_arr: Vec<U256>, out_count: u16) -> Result<Vec<U256>, SignatureError> {
473    if in_arr.len() > out_count as usize {
474        return Err(SignatureError::from_source("in_arr too long"));
475    }
476    let mut padded = in_arr;
477    padded.resize(out_count as usize, U256::ZERO);
478    Ok(padded)
479}
480
481/// Maps a stream of bigints to a single field element. First we convert the base from
482/// inWidth to packWidth. Then we compute the poseidon hash of the "packed" input.
483/// input is the input vector containing equal-width big ints. inWidth is the width of
484/// each input element.
485fn hash_to_field<T: ToBits>(
486    input: &[T],
487    in_width: u16,
488    pack_width: u8,
489) -> Result<Fr, SignatureError> {
490    let packed = convert_base(input, in_width, pack_width)?;
491
492    POSEIDON.hash(&packed).map_err(SignatureError::from_source)
493}
494
495/// Helper function to pack field elements from big ints.
496fn convert_base<T: ToBits>(
497    in_arr: &[T],
498    in_width: u16,
499    out_width: u8,
500) -> Result<Vec<Fr>, SignatureError> {
501    if out_width == 0 {
502        return Err(SignatureError::from_source("invalid input"));
503    }
504    let bits = big_int_array_to_bits(in_arr, in_width as usize)?;
505    let mut packed: Vec<Fr> = bits
506        .rchunks(out_width as usize)
507        .map(|chunk| {
508            U256::from_radix_be(chunk, 2)
509                .map(|v| Fr::from_be_bytes_mod_order(v.to_be().digits()))
510                .ok_or_else(|| SignatureError::from_source("invalid radix-2 conversion"))
511        })
512        .collect::<Result<_, _>>()?;
513    packed.reverse();
514    match packed.len() != (in_arr.len() * in_width as usize).div_ceil(out_width as usize) {
515        true => Err(SignatureError::from_source("invalid input")),
516        false => Ok(packed),
517    }
518}
519
520/// Convert a big int array to a bit array with 0 paddings.
521fn big_int_array_to_bits<T: ToBits>(
522    integers: &[T],
523    intended_size: usize,
524) -> Result<Vec<u8>, SignatureError> {
525    use itertools::Itertools;
526    use std::cmp::Ordering::Equal;
527    use std::cmp::Ordering::Greater;
528    use std::cmp::Ordering::Less;
529
530    integers
531        .iter()
532        .map(|integer| {
533            let bits = integer.to_bits();
534            match bits.len().cmp(&intended_size) {
535                Less => {
536                    let extra_bits = intended_size - bits.len();
537                    let mut padded = vec![0; extra_bits];
538                    padded.extend(bits);
539                    Ok(padded)
540                }
541                Equal => Ok(bits),
542                Greater => Err(SignatureError::from_source("invalid input")),
543            }
544        })
545        .flatten_ok()
546        .collect()
547}
548
549trait ToBits {
550    fn to_bits(&self) -> Vec<u8>;
551}
552
553impl ToBits for U256 {
554    fn to_bits(&self) -> Vec<u8> {
555        self.to_radix_be(2)
556    }
557}
558
559impl ToBits for U8192 {
560    fn to_bits(&self) -> Vec<u8> {
561        self.to_radix_be(2)
562    }
563}
564
565/// Calculate the poseidon hash from selected fields from inputs, along with the ephemeral pubkey.
566pub fn calculate_all_inputs_hash(
567    inputs: &ZkLoginInputs,
568    signature: &SimpleSignature,
569    modulus: &[u8],
570    max_epoch: u64,
571    version: CircuitVersion,
572) -> Result<Fr, SignatureError> {
573    let config = version.config();
574    if inputs.header_base64().len() > config.max_header_len_b64 as usize {
575        return Err(SignatureError::from_source("header too long"));
576    }
577
578    let (first, second) = public_key_to_frs(signature)?;
579
580    let address_seed = bn254_to_fr(inputs.address_seed());
581    let max_epoch_f = Fr::from_be_bytes_mod_order(U256::from(max_epoch).to_be().digits());
582    let header_f = hash_ascii_str_to_field(inputs.header_base64(), config.max_header_len_b64)?;
583    let modulus_f = modulus_to_field(modulus, config.max_rsa_bits)?;
584
585    match version {
586        CircuitVersion::V1 => {
587            let index_mod_4_f = Fr::from_be_bytes_mod_order(
588                U256::from(inputs.iss_base64_details().index_mod_4)
589                    .to_be()
590                    .digits(),
591            );
592            let iss_base64_f =
593                hash_ascii_str_to_field(&inputs.iss_base64_details().value, config.max_iss_len)?;
594
595            POSEIDON
596                .hash(&[
597                    first,
598                    second,
599                    address_seed,
600                    max_epoch_f,
601                    iss_base64_f,
602                    index_mod_4_f,
603                    header_f,
604                    modulus_f,
605                ])
606                .map_err(SignatureError::from_source)
607        }
608        CircuitVersion::V2 => {
609            let iss_details = inputs.iss_base64_details();
610            let ext_iss = iss_details
611                .decoded_extended_claim()
612                .map_err(SignatureError::from_source)?;
613            let iss_f = hash_ascii_str_to_field(&ext_iss, config.max_iss_len)?;
614            let rsa_num_bits_f = Fr::from(rsa_num_bits(modulus));
615
616            POSEIDON
617                .hash(&[
618                    first,
619                    second,
620                    address_seed,
621                    max_epoch_f,
622                    iss_f,
623                    header_f,
624                    modulus_f,
625                    rsa_num_bits_f,
626                ])
627                .map_err(SignatureError::from_source)
628        }
629    }
630}
631
632/// Cache of modulus hashes, keyed by the modulus bytes and the bit size it is padded to.
633/// Hashing a modulus is expensive (up to 8192 bits packed and poseidon-hashed) and the set of
634/// active JWKs is small, so hashes are memoized like fastcrypto's `cached_modulus_hash`.
635type ModulusHashKey = (Vec<u8>, u16);
636
637static MODULUS_HASH_CACHE: LazyLock<RwLock<HashMap<ModulusHashKey, Fr>>> =
638    LazyLock::new(|| RwLock::new(HashMap::new()));
639
640/// Maximum number of entries in the modulus hash cache.
641const MODULUS_HASH_CACHE_MAX_SIZE: usize = 100;
642
643/// Hash the modulus padded to `max_rsa_bits` bits to a field element; errors if the modulus
644/// does not fit in `max_rsa_bits` bits.
645///
646/// Equivalent to fastcrypto's `cached_modulus_hash`, with the fixed-width `U8192` in place of
647/// `BigUint` to avoid a num-bigint dependency.
648fn modulus_to_field(modulus: &[u8], max_rsa_bits: u16) -> Result<Fr, SignatureError> {
649    if let Some(f) = MODULUS_HASH_CACHE
650        .read()
651        .ok()
652        .and_then(|cache| cache.get(&(modulus.to_vec(), max_rsa_bits)).copied())
653    {
654        return Ok(f);
655    }
656    let f = hash_to_field(
657        &[U8192::from_be_slice(modulus)
658            .ok_or_else(|| SignatureError::from_source("JWK modulus too large for U8192"))?],
659        max_rsa_bits,
660        PACK_WIDTH,
661    )?;
662    if let Ok(mut cache) = MODULUS_HASH_CACHE.write() {
663        if cache.len() >= MODULUS_HASH_CACHE_MAX_SIZE {
664            cache.clear();
665        }
666        cache.insert((modulus.to_vec(), max_rsa_bits), f);
667    }
668    Ok(f)
669}
670
671/// Number of significant bits in the big-endian `modulus` (0 for an all-zero modulus).
672///
673/// Equivalent to fastcrypto's `BigUint::from_bytes_be(modulus).bits()`, computed directly on
674/// the byte slice to avoid a num-bigint dependency.
675fn rsa_num_bits(modulus: &[u8]) -> u64 {
676    let mut iter = modulus.iter().skip_while(|b| **b == 0);
677    match iter.next() {
678        Some(first) => (u8::BITS - first.leading_zeros()) as u64 + iter.count() as u64 * 8,
679        None => 0,
680    }
681}
682
683/// Calculate the Sui address based on address seed and address params.
684#[allow(unused)]
685fn gen_address_seed(
686    salt: &str,
687    name: &str,  // i.e. "sub"
688    value: &str, // i.e. the sub value
689    aud: &str,   // i.e. the client ID
690) -> Result<String, SignatureError> {
691    let salt_hash = POSEIDON
692        .hash(&[bn254_to_fr(
693            &Bn254FieldElement::from_str(salt).map_err(SignatureError::from_source)?,
694        )])
695        .map_err(SignatureError::from_source)?;
696    gen_address_seed_with_salt_hash(salt_hash, name, value, aud)
697}
698
699const MAX_KEY_CLAIM_NAME_LENGTH: u16 = 32;
700const MAX_KEY_CLAIM_VALUE_LENGTH: u16 = 115;
701const MAX_AUD_VALUE_LENGTH: u16 = 145;
702
703/// Same as [`gen_address_seed`] but takes the poseidon hash of the salt as input instead of the salt.
704pub(crate) fn gen_address_seed_with_salt_hash(
705    salt_hash: Fr,
706    name: &str,  // i.e. "sub"
707    value: &str, // i.e. the sub value
708    aud: &str,   // i.e. the client ID
709) -> Result<String, SignatureError> {
710    Ok(POSEIDON
711        .hash(&[
712            hash_ascii_str_to_field(name, MAX_KEY_CLAIM_NAME_LENGTH)?,
713            hash_ascii_str_to_field(value, MAX_KEY_CLAIM_VALUE_LENGTH)?,
714            hash_ascii_str_to_field(aud, MAX_AUD_VALUE_LENGTH)?,
715            salt_hash,
716        ])
717        .map_err(SignatureError::from_source)?
718        .to_string())
719}
720
721#[cfg(test)]
722mod test {
723    use super::*;
724
725    #[cfg(test)]
726    #[cfg(target_arch = "wasm32")]
727    use wasm_bindgen_test::wasm_bindgen_test as test;
728
729    #[test]
730    fn test_verify_zklogin_google() {
731        let user_salt = "206703048842351542647799591018316385612";
732
733        let signature = crate::zklogin::tests::test_eph_simple_signature();
734
735        // Get the address seed.
736        let address_seed = gen_address_seed(
737            user_salt,
738            "sub",
739            "106294049240999307923",
740            "25769832374-famecqrhe2gkebt5fvqms2263046lj96.apps.googleusercontent.com",
741        )
742        .unwrap();
743
744        let inputs = serde_json::json!({
745            "proof_points": {
746                "a": [
747                    "8247215875293406890829839156897863742504615191361518281091302475904551111016",
748                    "6872980335748205979379321982220498484242209225765686471076081944034292159666",
749                    "1"
750                ],
751                "b": [
752                    [
753                        "21419680064642047510915171723230639588631899775315750803416713283740137406807",
754                        "21566716915562037737681888858382287035712341650647439119820808127161946325890"
755                    ],
756                    [
757                        "17867714710686394159919998503724240212517838710399045289784307078087926404555",
758                        "21812769875502013113255155836896615164559280911997219958031852239645061854221"
759                    ],
760                    ["1","0"]
761                ],
762                "c": [
763                    "7530826803702928198368421787278524256623871560746240215547076095911132653214",
764                    "16244547936249959771862454850485726883972969173921727256151991751860694123976",
765                    "1"
766                ]
767            },
768            "iss_base64_details": {
769                "value": "yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC",
770                "index_mod_4": 1
771            },
772            "header_base64": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjZmNzI1NDEwMWY1NmU0MWNmMzVjOTkyNmRlODRhMmQ1NTJiNGM2ZjEiLCJ0eXAiOiJKV1QifQ",
773            "address_seed": address_seed
774        });
775
776        let zklogin_inputs: ZkLoginInputs = serde_json::from_value(inputs).unwrap();
777
778        let jwk = Jwk {
779            kty: "RSA".to_string(),
780            e: "AQAB".to_string(),
781            n: "oUriU8GqbRw-avcMn95DGW1cpZR1IoM6L7krfrWvLSSCcSX6Ig117o25Yk7QWBiJpaPV0FbP7Y5-DmThZ3SaF0AXW-3BsKPEXfFfeKVc6vBqk3t5mKlNEowjdvNTSzoOXO5UIHwsXaxiJlbMRalaFEUm-2CKgmXl1ss_yGh1OHkfnBiGsfQUndKoHiZuDzBMGw8Sf67am_Ok-4FShK0NuR3-q33aB_3Z7obC71dejSLWFOEcKUVCaw6DGVuLog3x506h1QQ1r0FXKOQxnmqrRgpoHqGSouuG35oZve1vgCU4vLZ6EAgBAbC0KL35I7_0wUDSMpiAvf7iZxzJVbspkQ".to_string(),
782            alg: "RS256".to_string(),
783        };
784
785        VerifyingKey::new_mainnet()
786            .verify_zklogin(&jwk, &zklogin_inputs, &signature, 10, CircuitVersion::V1)
787            .unwrap();
788    }
789
790    #[test]
791    fn test_public_key_to_frs() {
792        let signature = crate::zklogin::tests::test_eph_simple_signature();
793        let (actual_0, actual_1) = public_key_to_frs(&signature).unwrap();
794        let expect_0 = Fr::from(ark_ff::BigInt([
795            1244302228903607218,
796            13386648721483054705,
797            0,
798            0,
799        ]));
800
801        let expect_1 = Fr::from(ark_ff::BigInt([
802            18225592963892023808,
803            2904666130704426303,
804            0,
805            0,
806        ]));
807        assert_eq!(actual_0, expect_0);
808        assert_eq!(actual_1, expect_1);
809    }
810
811    #[test]
812    fn test_hash_ascii_str_to_field() {
813        let actual = hash_ascii_str_to_field("sub", 32).unwrap();
814        let expect = Fr::from(ark_ff::BigInt([
815            9420274050661827129,
816            9736100402995345242,
817            10431892319505233812,
818            1450152190758097105,
819        ]));
820        assert_eq!(actual, expect);
821
822        let actual = hash_ascii_str_to_field("106294049240999307923", 115).unwrap();
823        let expect = Fr::from(ark_ff::BigInt([
824            1616959301818912987,
825            17318965991705091209,
826            15303466056770245354,
827            1596136658728187659,
828        ]));
829        assert_eq!(actual, expect);
830
831        let actual = hash_ascii_str_to_field(
832            "25769832374-famecqrhe2gkebt5fvqms2263046lj96.apps.googleusercontent.com",
833            145,
834        )
835        .unwrap();
836        let expect = Fr::from(ark_ff::BigInt([
837            5030944271044826582,
838            8577618269522081956,
839            6962871209781429610,
840            2149811477348923117,
841        ]));
842        assert_eq!(actual, expect);
843
844        let actual =
845            hash_ascii_str_to_field("yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC", 224)
846                .unwrap();
847        let expect = Fr::from(ark_ff::BigInt([
848            6021918591354572765,
849            14069258108381575504,
850            1736509627917450843,
851            2767185135515367512,
852        ]));
853        assert_eq!(actual, expect);
854
855        let actual = hash_ascii_str_to_field(
856        "eyJhbGciOiJSUzI1NiIsImtpZCI6IjZmNzI1NDEwMWY1NmU0MWNmMzVjOTkyNmRlODRhMmQ1NTJiNGM2ZjEiLCJ0eXAiOiJKV1QifQ",
857        248,
858    ).unwrap();
859        let expect = Fr::from(ark_ff::BigInt([
860            4239129243150064016,
861            15469804315138207306,
862            17534492051703966556,
863            2100329545252322607,
864        ]));
865        assert_eq!(actual, expect);
866    }
867}