Skip to main content

sui_sdk_types/crypto/
zklogin.rs

1use super::SimpleSignature;
2use crate::checkpoint::EpochId;
3use crate::u256::U256;
4
5/// A zklogin authenticator
6///
7/// # BCS
8///
9/// The BCS serialized form for this type is defined by the following ABNF:
10///
11/// ```text
12/// zklogin-bcs = bytes             ; contents are defined by <zklogin-authenticator>
13/// zklogin     = zklogin-flag
14///               zklogin-inputs
15///               u64               ; max epoch
16///               simple-signature    
17/// ```
18///
19/// Note: Due to historical reasons, signatures are serialized slightly different from the majority
20/// of the types in Sui. In particular if a signature is ever embedded in another structure it
21/// generally is serialized as `bytes` meaning it has a length prefix that defines the length of
22/// the completely serialized signature.
23#[derive(Debug, Clone, PartialEq, Eq)]
24#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
25pub struct ZkLoginAuthenticator {
26    /// Zklogin proof and inputs required to perform proof verification.
27    pub inputs: ZkLoginInputs,
28
29    /// Maximum epoch for which the proof is valid.
30    pub max_epoch: EpochId,
31
32    /// User signature with the pubkey attested to by the provided proof.
33    pub signature: SimpleSignature,
34}
35
36/// A zklogin groth16 proof and the required inputs to perform proof verification.
37///
38/// # BCS
39///
40/// The BCS serialized form for this type is defined by the following ABNF:
41///
42/// ```text
43/// zklogin-inputs = zklogin-proof
44///                  zklogin-claim
45///                  string              ; base64url-unpadded encoded JwtHeader
46///                  bn254-field-element ; address_seed
47/// ```
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ZkLoginInputs {
50    proof_points: ZkLoginProof,
51    iss_base64_details: ZkLoginClaim,
52    header_base64: String,
53
54    jwt_header: JwtHeader,
55    jwk_id: JwkId,
56    public_identifier: ZkLoginPublicIdentifier,
57}
58
59impl ZkLoginInputs {
60    #[cfg(feature = "serde")]
61    #[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
62    pub fn new(
63        proof_points: ZkLoginProof,
64        iss_base64_details: ZkLoginClaim,
65        header_base64: String,
66        address_seed: Bn254FieldElement,
67    ) -> Result<Self, InvalidZkLoginAuthenticatorError> {
68        let iss = {
69            const ISS: &str = "iss";
70
71            let iss = iss_base64_details.verify_extended_claim(ISS)?;
72
73            if iss.len() > 255 {
74                return Err(InvalidZkLoginAuthenticatorError::new(
75                    "invalid iss: too long",
76                ));
77            }
78            iss
79        };
80
81        let jwt_header = JwtHeader::from_base64(&header_base64)?;
82        let jwk_id = JwkId {
83            iss: iss.clone(),
84            kid: jwt_header.kid.clone(),
85        };
86
87        let public_identifier = ZkLoginPublicIdentifier { iss, address_seed };
88
89        Ok(Self {
90            proof_points,
91            iss_base64_details,
92            header_base64,
93            jwt_header,
94            jwk_id,
95            public_identifier,
96        })
97    }
98
99    pub fn proof_points(&self) -> &ZkLoginProof {
100        &self.proof_points
101    }
102
103    pub fn iss_base64_details(&self) -> &ZkLoginClaim {
104        &self.iss_base64_details
105    }
106
107    pub fn header_base64(&self) -> &str {
108        &self.header_base64
109    }
110
111    pub fn address_seed(&self) -> &Bn254FieldElement {
112        &self.public_identifier.address_seed
113    }
114
115    pub fn jwk_id(&self) -> &JwkId {
116        &self.jwk_id
117    }
118
119    pub fn iss(&self) -> &str {
120        &self.public_identifier.iss
121    }
122
123    pub fn public_identifier(&self) -> &ZkLoginPublicIdentifier {
124        &self.public_identifier
125    }
126}
127
128#[cfg(feature = "proptest")]
129impl proptest::arbitrary::Arbitrary for ZkLoginInputs {
130    type Parameters = ();
131    type Strategy = proptest::strategy::BoxedStrategy<Self>;
132
133    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
134        use proptest::prelude::*;
135
136        (any::<ZkLoginProof>(), any::<Bn254FieldElement>())
137            .prop_map(|(proof_points, address_seed)| {
138                //TODO implement Arbitrary for real for ZkLoginClaim and header_base64 values
139                let iss_base64_details = ZkLoginClaim {
140                    value: "wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw".to_owned(),
141                    index_mod_4: 2,
142                };
143                let header_base64 = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ".to_owned();
144                Self::new(
145                    proof_points,
146                    iss_base64_details,
147                    header_base64,
148                    address_seed,
149                )
150                .unwrap()
151            })
152            .boxed()
153    }
154}
155
156/// A claim of the iss in a zklogin proof
157///
158/// # BCS
159///
160/// The BCS serialized form for this type is defined by the following ABNF:
161///
162/// ```text
163/// zklogin-claim = string u8
164/// ```
165#[derive(Debug, Clone, PartialEq, Eq)]
166#[cfg_attr(
167    feature = "serde",
168    derive(serde_derive::Serialize, serde_derive::Deserialize)
169)]
170#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
171pub struct ZkLoginClaim {
172    pub value: String,
173    pub index_mod_4: u8,
174}
175
176#[derive(Debug)]
177pub struct InvalidZkLoginAuthenticatorError(String);
178
179#[cfg(feature = "serde")]
180#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
181impl InvalidZkLoginAuthenticatorError {
182    fn new<T: Into<String>>(err: T) -> Self {
183        Self(err.into())
184    }
185}
186
187impl std::fmt::Display for InvalidZkLoginAuthenticatorError {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        write!(f, "invalid zklogin claim: {}", self.0)
190    }
191}
192
193impl std::error::Error for InvalidZkLoginAuthenticatorError {}
194
195#[cfg(feature = "serde")]
196#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
197impl ZkLoginClaim {
198    /// Base64-decode `value` at its `index_mod_4` offset within the JWT payload, yielding
199    /// the decoded extended claim string (e.g. `"iss":"https://accounts.google.com",`).
200    pub fn decoded_extended_claim(&self) -> Result<String, InvalidZkLoginAuthenticatorError> {
201        decode_base64_url(&self.value, &self.index_mod_4)
202    }
203
204    fn verify_extended_claim(
205        &self,
206        expected_key: &str,
207    ) -> Result<String, InvalidZkLoginAuthenticatorError> {
208        let extended_claim = self.decoded_extended_claim()?;
209
210        // Last character of each extracted_claim must be '}' or ','
211        if !(extended_claim.ends_with('}') || extended_claim.ends_with(',')) {
212            return Err(InvalidZkLoginAuthenticatorError::new(
213                "Invalid extended claim",
214            ));
215        }
216
217        let json_str = format!("{{{}}}", &extended_claim[..extended_claim.len() - 1]);
218
219        serde_json::from_str::<serde_json::Value>(&json_str)
220            .map_err(|e| InvalidZkLoginAuthenticatorError::new(e.to_string()))?
221            .as_object_mut()
222            .and_then(|o| o.get_mut(expected_key))
223            .map(serde_json::Value::take)
224            .and_then(|v| match v {
225                serde_json::Value::String(s) => Some(s),
226                _ => None,
227            })
228            .ok_or_else(|| InvalidZkLoginAuthenticatorError::new("invalid extended claim"))
229    }
230}
231
232#[cfg(feature = "serde")]
233/// Map a base64 string to a bit array by taking each char's index and convert it to binary form with one bit per u8
234/// element in the output. Returns InvalidZkLoginClaimError if one of the characters is not in the base64 charset.
235fn base64_to_bitarray(input: &str) -> Result<Vec<u8>, InvalidZkLoginAuthenticatorError> {
236    use itertools::Itertools;
237
238    const BASE64_URL_CHARSET: &str =
239        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
240
241    input
242        .chars()
243        .map(|c| {
244            BASE64_URL_CHARSET
245                .find(c)
246                .map(|index| index as u8)
247                .map(|index| (0..6).rev().map(move |i| (index >> i) & 1))
248                .ok_or_else(|| {
249                    InvalidZkLoginAuthenticatorError::new("base64_to_bitarry invalid input")
250                })
251        })
252        .flatten_ok()
253        .collect()
254}
255
256#[cfg(feature = "serde")]
257/// Convert a bitarray (each bit is represented by a u8) to a byte array by taking each 8 bits as a
258/// byte in big-endian format.
259fn bitarray_to_bytearray(bits: &[u8]) -> Result<Vec<u8>, InvalidZkLoginAuthenticatorError> {
260    #[expect(clippy::manual_is_multiple_of)]
261    if bits.len() % 8 != 0 {
262        return Err(InvalidZkLoginAuthenticatorError::new(
263            "bitarray_to_bytearray invalid input",
264        ));
265    }
266    Ok(bits
267        .chunks(8)
268        .map(|chunk| {
269            let mut byte = 0u8;
270            for (i, bit) in chunk.iter().rev().enumerate() {
271                byte |= bit << i;
272            }
273            byte
274        })
275        .collect())
276}
277
278#[cfg(feature = "serde")]
279/// Parse the base64 string, add paddings based on offset, and convert to a bytearray.
280fn decode_base64_url(
281    s: &str,
282    index_mod_4: &u8,
283) -> Result<String, InvalidZkLoginAuthenticatorError> {
284    if s.len() < 2 {
285        return Err(InvalidZkLoginAuthenticatorError::new(
286            "Base64 string smaller than 2",
287        ));
288    }
289    let mut bits = base64_to_bitarray(s)?;
290    match index_mod_4 {
291        0 => {}
292        1 => {
293            bits.drain(..2);
294        }
295        2 => {
296            bits.drain(..4);
297        }
298        _ => {
299            return Err(InvalidZkLoginAuthenticatorError::new(
300                "Invalid first_char_offset",
301            ));
302        }
303    }
304
305    // Compute the offset in `usize` so that an `s.len()` past
306    // `u8::MAX` cannot wrap to a small value (or underflow when
307    // combined with the `- 1`). The earlier match has already
308    // narrowed `*index_mod_4` to `0..=2`, and `s.len() >= 2`,
309    // so the unsigned subtraction never underflows here.
310    let last_char_offset = (*index_mod_4 as usize + s.len() - 1) % 4;
311    match last_char_offset {
312        3 => {}
313        2 => {
314            bits.drain(bits.len() - 2..);
315        }
316        1 => {
317            bits.drain(bits.len() - 4..);
318        }
319        _ => {
320            return Err(InvalidZkLoginAuthenticatorError::new(
321                "Invalid last_char_offset",
322            ));
323        }
324    }
325
326    if bits.len() % 8 != 0 {
327        return Err(InvalidZkLoginAuthenticatorError::new("Invalid bits length"));
328    }
329
330    Ok(std::str::from_utf8(&bitarray_to_bytearray(&bits)?)
331        .map_err(|_| InvalidZkLoginAuthenticatorError::new("Invalid UTF8 string"))?
332        .to_owned())
333}
334
335/// Struct that represents a standard JWT header according to
336/// https://openid.net/specs/openid-connect-core-1_0.html
337#[derive(Debug, Clone, PartialEq, Eq)]
338struct JwtHeader {
339    alg: String,
340    kid: String,
341    typ: Option<String>,
342}
343
344impl JwtHeader {
345    #[cfg(feature = "serde")]
346    fn from_base64(s: &str) -> Result<Self, InvalidZkLoginAuthenticatorError> {
347        use base64ct::Base64UrlUnpadded;
348        use base64ct::Encoding;
349
350        #[derive(serde_derive::Serialize, serde_derive::Deserialize)]
351        struct Header {
352            alg: String,
353            kid: String,
354            #[serde(skip_serializing_if = "Option::is_none")]
355            typ: Option<String>,
356        }
357
358        let header_bytes = Base64UrlUnpadded::decode_vec(s)
359            .map_err(|e| InvalidZkLoginAuthenticatorError::new(format!("invalid base64: {e}")))?;
360        let Header { alg, kid, typ } = serde_json::from_slice(&header_bytes)
361            .map_err(|e| InvalidZkLoginAuthenticatorError::new(format!("invalid json: {e}")))?;
362        if alg != "RS256" {
363            return Err(InvalidZkLoginAuthenticatorError::new(
364                "jwt alg must be RS256",
365            ));
366        }
367        Ok(Self { alg, kid, typ })
368    }
369}
370
371/// A zklogin groth16 proof
372///
373/// # BCS
374///
375/// The BCS serialized form for this type is defined by the following ABNF:
376///
377/// ```text
378/// zklogin-proof = circom-g1 circom-g2 circom-g1
379/// ```
380#[derive(Debug, Clone, PartialEq, Eq)]
381#[cfg_attr(
382    feature = "serde",
383    derive(serde_derive::Serialize, serde_derive::Deserialize)
384)]
385#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
386pub struct ZkLoginProof {
387    pub a: CircomG1,
388    pub b: CircomG2,
389    pub c: CircomG1,
390}
391
392/// A G1 point
393///
394/// This represents the canonical decimal representation of the projective coordinates in Fq.
395///
396/// # BCS
397///
398/// The BCS serialized form for this type is defined by the following ABNF:
399///
400/// ```text
401/// circom-g1 = %x03 3(bn254-field-element)
402/// ```
403#[derive(Clone, Debug, PartialEq, Eq)]
404#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
405pub struct CircomG1(pub [Bn254FieldElement; 3]);
406
407/// A G2 point
408///
409/// This represents the canonical decimal representation of the coefficients of the projective
410/// coordinates in Fq2.
411///
412/// # BCS
413///
414/// The BCS serialized form for this type is defined by the following ABNF:
415///
416/// ```text
417/// circom-g2 = %x03 3(%x02 2(bn254-field-element))
418/// ```
419#[derive(Clone, Debug, PartialEq, Eq)]
420#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
421pub struct CircomG2(pub [[Bn254FieldElement; 2]; 3]);
422
423/// Public Key equivalent for Zklogin authenticators
424///
425/// A `ZkLoginPublicIdentifier` is the equivalent of a public key for other account authenticators,
426/// and contains the information required to derive the onchain account [`Address`] for a Zklogin
427/// authenticator.
428///
429/// ## Note
430///
431/// Due to a historical bug that was introduced in the Sui Typescript SDK when the zklogin
432/// authenticator was first introduced, there are now possibly two "valid" addresses for each
433/// zklogin authenticator depending on the bit-pattern of the `address_seed` value.
434///
435/// The original bug incorrectly derived a zklogin's address by stripping any leading
436/// zero-bytes that could have been present in the 32-byte length `address_seed` value prior to
437/// hashing, leading to a different derived address. This incorrectly derived address was
438/// presented to users of various wallets, leading them to sending funds to these addresses
439/// that they couldn't access. Instead of letting these users lose any assets that were sent to
440/// these addresses, the Sui network decided to change the protocol to allow for a zklogin
441/// authenticator who's `address_seed` value had leading zero-bytes be authorized to sign for
442/// both the addresses derived from both the unpadded and padded `address_seed` value.
443///
444/// # BCS
445///
446/// The BCS serialized form for this type is defined by the following ABNF:
447///
448/// ```text
449/// zklogin-public-identifier-bcs = bytes ; where the contents are defined by
450///                                       ; <zklogin-public-identifier>
451///
452/// zklogin-public-identifier = zklogin-public-identifier-iss
453///                             address-seed
454///
455/// zklogin-public-identifier-unpadded = zklogin-public-identifier-iss
456///                                      address-seed-unpadded
457///
458/// ; The iss, or issuer, is a utf8 string that is less than 255 bytes long
459/// ; and is serialized with the iss's length in bytes as a u8 followed by
460/// ; the bytes of the iss
461/// zklogin-public-identifier-iss = u8 *255(OCTET)
462///
463/// ; A Bn254FieldElement serialized as a 32-byte big-endian value
464/// address-seed = 32(OCTET)
465///
466/// ; A Bn254FieldElement serialized as a 32-byte big-endian value
467/// ; with any leading zero bytes stripped
468/// address-seed-unpadded = %x00 / %x01-ff *31(OCTET)
469/// ```
470///
471/// [`Address`]: crate::Address
472#[derive(Clone, Debug, PartialEq, Eq)]
473#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
474pub struct ZkLoginPublicIdentifier {
475    iss: String,
476    address_seed: Bn254FieldElement,
477}
478
479impl ZkLoginPublicIdentifier {
480    pub fn new(iss: String, address_seed: Bn254FieldElement) -> Option<Self> {
481        if iss.len() > 255 {
482            None
483        } else {
484            Some(Self { iss, address_seed })
485        }
486    }
487
488    pub fn iss(&self) -> &str {
489        &self.iss
490    }
491
492    pub fn address_seed(&self) -> &Bn254FieldElement {
493        &self.address_seed
494    }
495}
496
497/// A JSON Web Key
498///
499/// Struct that contains info for a JWK. A list of them for different kids can
500/// be retrieved from the JWK endpoint (e.g. <https://www.googleapis.com/oauth2/v3/certs>).
501/// The JWK is used to verify the JWT token.
502///
503/// # BCS
504///
505/// The BCS serialized form for this type is defined by the following ABNF:
506///
507/// ```text
508/// jwk = string string string string
509/// ```
510#[derive(Clone, Debug, PartialEq, Eq, Hash)]
511#[cfg_attr(
512    feature = "serde",
513    derive(serde_derive::Serialize, serde_derive::Deserialize)
514)]
515#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
516pub struct Jwk {
517    /// Key type parameter, <https://datatracker.ietf.org/doc/html/rfc7517#section-4.1>
518    pub kty: String,
519
520    /// RSA public exponent, <https://datatracker.ietf.org/doc/html/rfc7517#section-9.3>
521    pub e: String,
522
523    /// RSA modulus, <https://datatracker.ietf.org/doc/html/rfc7517#section-9.3>
524    pub n: String,
525
526    /// Algorithm parameter, <https://datatracker.ietf.org/doc/html/rfc7517#section-4.4>
527    pub alg: String,
528}
529
530/// Key to uniquely identify a JWK
531///
532/// # BCS
533///
534/// The BCS serialized form for this type is defined by the following ABNF:
535///
536/// ```text
537/// jwk-id = string string
538/// ```
539#[derive(Clone, Debug, PartialEq, Eq, Hash)]
540#[cfg_attr(
541    feature = "serde",
542    derive(serde_derive::Serialize, serde_derive::Deserialize)
543)]
544#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
545pub struct JwkId {
546    /// The issuer or identity of the OIDC provider.
547    pub iss: String,
548
549    /// A key id use to uniquely identify a key from an OIDC provider.
550    pub kid: String,
551}
552
553/// A point on the BN254 elliptic curve.
554///
555/// This is a 32-byte, or 256-bit, value that is generally represented as radix10 when a
556/// human-readable display format is needed, and is represented as a 32-byte big-endian value while
557/// in memory.
558///
559/// # BCS
560///
561/// The BCS serialized form for this type is defined by the following ABNF:
562///
563/// ```text
564/// bn254-field-element = *DIGIT ; which is then interpreted as a radix10 encoded 32-byte value
565/// ```
566#[derive(Clone, Debug, Default, PartialEq, Eq)]
567#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
568pub struct Bn254FieldElement([u8; 32]);
569
570impl Bn254FieldElement {
571    pub const fn new(bytes: [u8; 32]) -> Self {
572        Self(bytes)
573    }
574
575    pub const fn from_str_radix_10(s: &str) -> Result<Self, Bn254FieldElementParseError> {
576        let u256 = match U256::from_str_radix(s, 10) {
577            Ok(u256) => u256,
578            Err(e) => return Err(Bn254FieldElementParseError(e)),
579        };
580        let be = u256.to_be();
581        Ok(Self(*be.digits()))
582    }
583
584    pub fn unpadded(&self) -> &[u8] {
585        let mut buf = self.0.as_slice();
586
587        while !buf.is_empty() && buf[0] == 0 {
588            buf = &buf[1..];
589        }
590
591        // If the value is '0' then just return a slice of length 1 of the final byte
592        if buf.is_empty() { &self.0[31..] } else { buf }
593    }
594
595    pub fn padded(&self) -> &[u8] {
596        &self.0
597    }
598}
599
600impl std::fmt::Display for Bn254FieldElement {
601    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602        let u256 = U256::from_be(U256::from_digits(self.0));
603        let radix10 = u256.to_str_radix(10);
604        f.write_str(&radix10)
605    }
606}
607
608#[derive(Debug)]
609pub struct Bn254FieldElementParseError(crate::U256ParseError);
610
611impl std::fmt::Display for Bn254FieldElementParseError {
612    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613        write!(f, "unable to parse radix10 encoded value {}", self.0)
614    }
615}
616
617impl std::error::Error for Bn254FieldElementParseError {}
618
619impl std::str::FromStr for Bn254FieldElement {
620    type Err = Bn254FieldElementParseError;
621
622    fn from_str(s: &str) -> Result<Self, Self::Err> {
623        let u256 = U256::from_str_radix(s, 10).map_err(Bn254FieldElementParseError)?;
624        let be = u256.to_be();
625        Ok(Self(*be.digits()))
626    }
627}
628
629#[cfg(test)]
630mod test {
631    use super::Bn254FieldElement;
632    use num_bigint::BigUint;
633    use proptest::prelude::*;
634    use std::str::FromStr;
635    use test_strategy::proptest;
636
637    #[cfg(target_arch = "wasm32")]
638    use wasm_bindgen_test::wasm_bindgen_test as test;
639
640    #[test]
641    fn unpadded_slice() {
642        let seed = Bn254FieldElement([0; 32]);
643        let zero: [u8; 1] = [0];
644        assert_eq!(seed.unpadded(), zero.as_slice());
645
646        let mut seed = Bn254FieldElement([1; 32]);
647        seed.0[0] = 0;
648        assert_eq!(seed.unpadded(), [1; 31].as_slice());
649    }
650
651    #[proptest]
652    fn dont_crash_on_large_inputs(
653        #[strategy(proptest::collection::vec(any::<u8>(), 33..1024))] bytes: Vec<u8>,
654    ) {
655        let big_int = BigUint::from_bytes_be(&bytes);
656        let radix10 = big_int.to_str_radix(10);
657
658        // doesn't crash
659        let _ = Bn254FieldElement::from_str(&radix10);
660    }
661
662    #[proptest]
663    fn valid_address_seeds(
664        #[strategy(proptest::collection::vec(any::<u8>(), 1..=32))] bytes: Vec<u8>,
665    ) {
666        let big_int = BigUint::from_bytes_be(&bytes);
667        let radix10 = big_int.to_str_radix(10);
668
669        let seed = Bn254FieldElement::from_str(&radix10).unwrap();
670        assert_eq!(radix10, seed.to_string());
671        // Ensure unpadded doesn't crash
672        seed.unpadded();
673    }
674
675    // Regression test: BCS deserialization used to call
676    // `DisplayFromStr::deserialize_as` directly, which accepts any
677    // radix10 string `bnum::U256::from_str_radix` would parse —
678    // including encodings with leading zeros like `"007"`. Two
679    // distinct BCS byte strings (`0x01 0x37` and `0x03 0x30 0x30 0x37`)
680    // therefore decoded to the same `Bn254FieldElement`, breaking the
681    // canonicality invariant that downstream signature deduplication
682    // and digesting rely on. The deserializer must now reject any
683    // encoding that does not round-trip through `Display`.
684    #[cfg(feature = "serde")]
685    #[test]
686    fn bcs_rejects_non_canonical_radix10_encoding() {
687        let canonical = bcs::to_bytes("7").unwrap();
688        let leading_zero = bcs::to_bytes("007").unwrap();
689        assert_ne!(canonical, leading_zero);
690
691        let parsed: Bn254FieldElement = bcs::from_bytes(&canonical).unwrap();
692        assert_eq!(parsed.to_string(), "7");
693
694        let err = bcs::from_bytes::<Bn254FieldElement>(&leading_zero).unwrap_err();
695        assert!(
696            err.to_string().contains("non-canonical"),
697            "unexpected error: {err}"
698        );
699    }
700
701    // Regression test: `decode_base64_url` used to compute
702    // `(index_mod_4 + s.len() as u8 - 1) % 4` in `u8`. With an
703    // attacker-supplied JWT claim value of length 256 the cast
704    // truncates `s.len()` to `0` and the subtraction underflows,
705    // panicking under `debug_assertions`. The arithmetic must now use
706    // `usize` so that the function returns a structured error rather
707    // than crashing the caller.
708    #[cfg(feature = "serde")]
709    #[test]
710    fn long_claim_value_does_not_panic_on_u8_overflow() {
711        use super::ZkLoginClaim;
712
713        let claim = ZkLoginClaim {
714            value: "A".repeat(256),
715            index_mod_4: 0,
716        };
717        assert!(claim.verify_extended_claim("iss").is_err());
718    }
719}
720
721#[cfg(feature = "serde")]
722#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
723mod serialization {
724    use crate::SignatureScheme;
725
726    use super::*;
727    use serde::Deserialize;
728    use serde::Deserializer;
729    use serde::Serialize;
730    use serde::Serializer;
731    use serde_with::Bytes;
732    use serde_with::DeserializeAs;
733    use serde_with::SerializeAs;
734    use std::borrow::Cow;
735
736    // Serialized format is: iss_bytes_len || iss_bytes || padded_32_byte_address_seed.
737    impl Serialize for ZkLoginPublicIdentifier {
738        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
739        where
740            S: Serializer,
741        {
742            if serializer.is_human_readable() {
743                #[derive(serde_derive::Serialize)]
744                struct Readable<'a> {
745                    iss: &'a str,
746                    address_seed: &'a Bn254FieldElement,
747                }
748                let readable = Readable {
749                    iss: &self.iss,
750                    address_seed: &self.address_seed,
751                };
752                readable.serialize(serializer)
753            } else {
754                let mut buf = Vec::new();
755                let iss_bytes = self.iss.as_bytes();
756                buf.push(iss_bytes.len() as u8);
757                buf.extend(iss_bytes);
758
759                buf.extend(&self.address_seed.0);
760
761                serializer.serialize_bytes(&buf)
762            }
763        }
764    }
765
766    impl<'de> Deserialize<'de> for ZkLoginPublicIdentifier {
767        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
768        where
769            D: Deserializer<'de>,
770        {
771            if deserializer.is_human_readable() {
772                #[derive(serde_derive::Deserialize)]
773                struct Readable {
774                    iss: String,
775                    address_seed: Bn254FieldElement,
776                }
777
778                let Readable { iss, address_seed } = Deserialize::deserialize(deserializer)?;
779                Self::new(iss, address_seed)
780                    .ok_or_else(|| serde::de::Error::custom("invalid zklogin public identifier"))
781            } else {
782                let bytes: Cow<'de, [u8]> = Bytes::deserialize_as(deserializer)?;
783                let iss_len = *bytes
784                    .first()
785                    .ok_or_else(|| serde::de::Error::custom("invalid zklogin public identifier"))?;
786                let iss_bytes = bytes
787                    .get(1..(1 + iss_len as usize))
788                    .ok_or_else(|| serde::de::Error::custom("invalid zklogin public identifier"))?;
789                let iss = std::str::from_utf8(iss_bytes).map_err(serde::de::Error::custom)?;
790                let address_seed_bytes = bytes
791                    .get((1 + iss_len as usize)..)
792                    .ok_or_else(|| serde::de::Error::custom("invalid zklogin public identifier"))?;
793
794                let address_seed = <[u8; 32]>::try_from(address_seed_bytes)
795                    .map_err(serde::de::Error::custom)
796                    .map(Bn254FieldElement)?;
797
798                Self::new(iss.into(), address_seed)
799                    .ok_or_else(|| serde::de::Error::custom("invalid zklogin public identifier"))
800            }
801        }
802    }
803
804    #[derive(serde_derive::Serialize)]
805    struct AuthenticatorRef<'a> {
806        inputs: &'a ZkLoginInputs,
807        max_epoch: EpochId,
808        signature: &'a SimpleSignature,
809    }
810
811    #[derive(serde_derive::Deserialize)]
812    struct Authenticator {
813        inputs: ZkLoginInputs,
814        max_epoch: EpochId,
815        signature: SimpleSignature,
816    }
817
818    impl Serialize for ZkLoginAuthenticator {
819        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
820        where
821            S: Serializer,
822        {
823            if serializer.is_human_readable() {
824                let authenticator_ref = AuthenticatorRef {
825                    inputs: &self.inputs,
826                    max_epoch: self.max_epoch,
827                    signature: &self.signature,
828                };
829
830                authenticator_ref.serialize(serializer)
831            } else {
832                let bytes = self.to_bytes();
833                serializer.serialize_bytes(&bytes)
834            }
835        }
836    }
837
838    impl<'de> Deserialize<'de> for ZkLoginAuthenticator {
839        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
840        where
841            D: Deserializer<'de>,
842        {
843            if deserializer.is_human_readable() {
844                let Authenticator {
845                    inputs,
846                    max_epoch,
847                    signature,
848                } = Authenticator::deserialize(deserializer)?;
849                Ok(Self {
850                    inputs,
851                    max_epoch,
852                    signature,
853                })
854            } else {
855                let bytes: Cow<'de, [u8]> = Bytes::deserialize_as(deserializer)?;
856                Self::from_serialized_bytes(bytes)
857            }
858        }
859    }
860
861    impl ZkLoginAuthenticator {
862        pub(crate) fn to_bytes(&self) -> Vec<u8> {
863            let authenticator_ref = AuthenticatorRef {
864                inputs: &self.inputs,
865                max_epoch: self.max_epoch,
866                signature: &self.signature,
867            };
868
869            let mut buf = Vec::new();
870            buf.push(SignatureScheme::ZkLogin as u8);
871
872            bcs::serialize_into(&mut buf, &authenticator_ref).expect("serialization cannot fail");
873            buf
874        }
875
876        pub(crate) fn from_serialized_bytes<T: AsRef<[u8]>, E: serde::de::Error>(
877            bytes: T,
878        ) -> Result<Self, E> {
879            let bytes = bytes.as_ref();
880            let flag = SignatureScheme::from_byte(
881                *bytes
882                    .first()
883                    .ok_or_else(|| serde::de::Error::custom("missing signature scheme flag"))?,
884            )
885            .map_err(serde::de::Error::custom)?;
886            if flag != SignatureScheme::ZkLogin {
887                return Err(serde::de::Error::custom("invalid zklogin flag"));
888            }
889            let bcs_bytes = &bytes[1..];
890
891            let Authenticator {
892                inputs,
893                max_epoch,
894                signature,
895            } = bcs::from_bytes(bcs_bytes).map_err(serde::de::Error::custom)?;
896            Ok(Self {
897                inputs,
898                max_epoch,
899                signature,
900            })
901        }
902    }
903
904    impl Serialize for ZkLoginInputs {
905        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
906        where
907            S: Serializer,
908        {
909            #[derive(serde_derive::Serialize)]
910            struct Inputs<'a> {
911                proof_points: &'a ZkLoginProof,
912                iss_base64_details: &'a ZkLoginClaim,
913                header_base64: &'a str,
914                address_seed: &'a Bn254FieldElement,
915            }
916
917            Inputs {
918                proof_points: self.proof_points(),
919                iss_base64_details: self.iss_base64_details(),
920                header_base64: self.header_base64(),
921                address_seed: self.address_seed(),
922            }
923            .serialize(serializer)
924        }
925    }
926
927    impl<'de> Deserialize<'de> for ZkLoginInputs {
928        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
929        where
930            D: Deserializer<'de>,
931        {
932            #[derive(serde_derive::Deserialize)]
933            struct Inputs {
934                proof_points: ZkLoginProof,
935                iss_base64_details: ZkLoginClaim,
936                header_base64: String,
937                address_seed: Bn254FieldElement,
938            }
939
940            let Inputs {
941                proof_points,
942                iss_base64_details,
943                header_base64,
944                address_seed,
945            } = Inputs::deserialize(deserializer)?;
946            Self::new(
947                proof_points,
948                iss_base64_details,
949                header_base64,
950                address_seed,
951            )
952            .map_err(serde::de::Error::custom)
953        }
954    }
955
956    // AddressSeed's serialized format is as a radix10 encoded string
957    impl Serialize for Bn254FieldElement {
958        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
959        where
960            S: serde::Serializer,
961        {
962            serde_with::DisplayFromStr::serialize_as(self, serializer)
963        }
964    }
965
966    impl<'de> Deserialize<'de> for Bn254FieldElement {
967        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
968        where
969            D: Deserializer<'de>,
970        {
971            // `Display` strips any leading zeros from the radix10
972            // encoding while `FromStr` (via `bnum::U256::from_str_radix`)
973            // accepts them, so the bare `DisplayFromStr` round-trip is
974            // not canonical: the BCS encodings of e.g. `"7"` and
975            // `"007"` both parse to the same value but differ at the
976            // byte level. Any consumer that keys signature dedup,
977            // replay protection, or content digesting on the BCS
978            // bytes of a `ZkLoginAuthenticator` (which embeds ten
979            // `Bn254FieldElement`s) would treat such pairs as
980            // distinct, so reject any encoding whose `Display`
981            // round-trip differs from the input.
982            let s: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
983            let value = s
984                .parse::<Bn254FieldElement>()
985                .map_err(serde::de::Error::custom)?;
986            if value.to_string() != *s {
987                return Err(serde::de::Error::custom(
988                    "non-canonical Bn254FieldElement encoding",
989                ));
990            }
991            Ok(value)
992        }
993    }
994
995    impl Serialize for CircomG1 {
996        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
997        where
998            S: serde::Serializer,
999        {
1000            use serde::ser::SerializeSeq;
1001            let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
1002            for element in &self.0 {
1003                seq.serialize_element(element)?;
1004            }
1005            seq.end()
1006        }
1007    }
1008
1009    impl<'de> Deserialize<'de> for CircomG1 {
1010        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1011        where
1012            D: Deserializer<'de>,
1013        {
1014            let inner = <Vec<_>>::deserialize(deserializer)?;
1015            Ok(Self(inner.try_into().map_err(|_| {
1016                serde::de::Error::custom("expected array of length 3")
1017            })?))
1018        }
1019    }
1020
1021    impl Serialize for CircomG2 {
1022        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1023        where
1024            S: serde::Serializer,
1025        {
1026            use serde::ser::SerializeSeq;
1027
1028            struct Inner<'a>(&'a [Bn254FieldElement; 2]);
1029
1030            impl Serialize for Inner<'_> {
1031                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1032                where
1033                    S: serde::Serializer,
1034                {
1035                    let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
1036                    for element in self.0 {
1037                        seq.serialize_element(element)?;
1038                    }
1039                    seq.end()
1040                }
1041            }
1042
1043            let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
1044            for element in &self.0 {
1045                seq.serialize_element(&Inner(element))?;
1046            }
1047            seq.end()
1048        }
1049    }
1050
1051    impl<'de> Deserialize<'de> for CircomG2 {
1052        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1053        where
1054            D: Deserializer<'de>,
1055        {
1056            let vecs = <Vec<Vec<Bn254FieldElement>>>::deserialize(deserializer)?;
1057            let mut inner: [[Bn254FieldElement; 2]; 3] = Default::default();
1058
1059            if vecs.len() != 3 {
1060                return Err(serde::de::Error::custom(
1061                    "vector of three vectors each being a vector of two strings",
1062                ));
1063            }
1064
1065            for (i, v) in vecs.into_iter().enumerate() {
1066                if v.len() != 2 {
1067                    return Err(serde::de::Error::custom(
1068                        "vector of three vectors each being a vector of two strings",
1069                    ));
1070                }
1071
1072                for (j, point) in v.into_iter().enumerate() {
1073                    inner[i][j] = point;
1074                }
1075            }
1076
1077            Ok(Self(inner))
1078        }
1079    }
1080}