Skip to content

Commit ce2b6b4

Browse files
committed
sui-crypto: move logic for deriving ZkLoginPublicIdentifier to sui-sdk-types
1 parent 5b61c62 commit ce2b6b4

File tree

4 files changed

+175
-129
lines changed

4 files changed

+175
-129
lines changed

crates/sui-crypto/src/multisig.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ impl MultisigVerifier {
7272

7373
// verify that the member identifier and the authenticator match
7474
if zklogin_identifier
75-
!= &crate::zklogin::zklogin_identifier_from_inputs(
76-
&zklogin_authenticator.inputs,
77-
)?
75+
!= &zklogin_authenticator
76+
.inputs
77+
.public_identifier()
78+
.map_err(SignatureError::from_source)?
7879
{
7980
return Err(SignatureError::from_source(
8081
"member zklogin identifier does not match signature",
@@ -390,8 +391,10 @@ fn multisig_pubkey_and_signature_from_user_signature(
390391
)),
391392
#[cfg(feature = "zklogin")]
392393
UserSignature::ZkLogin(zklogin_authenticator) => {
393-
let zklogin_identifier =
394-
crate::zklogin::zklogin_identifier_from_inputs(&zklogin_authenticator.inputs)?;
394+
let zklogin_identifier = zklogin_authenticator
395+
.inputs
396+
.public_identifier()
397+
.map_err(SignatureError::from_source)?;
395398
Ok((
396399
MultisigMemberPublicKey::ZkLogin(zklogin_identifier),
397400
MultisigMemberSignature::ZkLogin(zklogin_authenticator),

crates/sui-crypto/src/zklogin/mod.rs

Lines changed: 1 addition & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use sui_sdk_types::Jwk;
77
use sui_sdk_types::JwkId;
88
use sui_sdk_types::UserSignature;
99
use sui_sdk_types::ZkLoginAuthenticator;
10-
use sui_sdk_types::ZkLoginClaim;
1110
use sui_sdk_types::ZkLoginInputs;
1211

1312
mod poseidon;
@@ -93,11 +92,9 @@ struct JwtDetails {
9392

9493
impl JwtDetails {
9594
fn from_zklogin_inputs(inputs: &ZkLoginInputs) -> Result<Self, SignatureError> {
96-
const ISS: &str = "iss";
97-
9895
let header = JwtHeader::from_base64(&inputs.header_base64)?;
9996
let id = JwkId {
100-
iss: verify_extended_claim(&inputs.iss_base64_details, ISS)?,
97+
iss: inputs.iss().map_err(SignatureError::from_source)?,
10198
kid: header.kid.clone(),
10299
};
103100
Ok(JwtDetails { header, id })
@@ -136,122 +133,3 @@ impl JwtHeader {
136133
Ok(Self { alg, kid, typ })
137134
}
138135
}
139-
140-
/// Parse the extended claim json value to its claim value, using the expected claim key.
141-
fn verify_extended_claim(
142-
claim: &ZkLoginClaim,
143-
expected_key: &str,
144-
) -> Result<String, SignatureError> {
145-
/// 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
146-
/// element in the output. Returns SignatureError if one of the characters is not in the base64 charset.
147-
fn base64_to_bitarray(input: &str) -> Result<Vec<u8>, SignatureError> {
148-
use itertools::Itertools;
149-
150-
const BASE64_URL_CHARSET: &str =
151-
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
152-
153-
input
154-
.chars()
155-
.map(|c| {
156-
BASE64_URL_CHARSET
157-
.find(c)
158-
.map(|index| index as u8)
159-
.map(|index| (0..6).rev().map(move |i| (index >> i) & 1))
160-
.ok_or_else(|| SignatureError::from_source("base64_to_bitarry invalid input"))
161-
})
162-
.flatten_ok()
163-
.collect()
164-
}
165-
166-
/// Convert a bitarray (each bit is represented by a u8) to a byte array by taking each 8 bits as a
167-
/// byte in big-endian format.
168-
fn bitarray_to_bytearray(bits: &[u8]) -> Result<Vec<u8>, SignatureError> {
169-
if bits.len() % 8 != 0 {
170-
return Err(SignatureError::from_source(
171-
"bitarray_to_bytearray invalid input",
172-
));
173-
}
174-
Ok(bits
175-
.chunks(8)
176-
.map(|chunk| {
177-
let mut byte = 0u8;
178-
for (i, bit) in chunk.iter().rev().enumerate() {
179-
byte |= bit << i;
180-
}
181-
byte
182-
})
183-
.collect())
184-
}
185-
186-
/// Parse the base64 string, add paddings based on offset, and convert to a bytearray.
187-
fn decode_base64_url(s: &str, index_mod_4: &u8) -> Result<String, SignatureError> {
188-
if s.len() < 2 {
189-
return Err(SignatureError::from_source("Base64 string smaller than 2"));
190-
}
191-
let mut bits = base64_to_bitarray(s)?;
192-
match index_mod_4 {
193-
0 => {}
194-
1 => {
195-
bits.drain(..2);
196-
}
197-
2 => {
198-
bits.drain(..4);
199-
}
200-
_ => {
201-
return Err(SignatureError::from_source("Invalid first_char_offset"));
202-
}
203-
}
204-
205-
let last_char_offset = (index_mod_4 + s.len() as u8 - 1) % 4;
206-
match last_char_offset {
207-
3 => {}
208-
2 => {
209-
bits.drain(bits.len() - 2..);
210-
}
211-
1 => {
212-
bits.drain(bits.len() - 4..);
213-
}
214-
_ => {
215-
return Err(SignatureError::from_source("Invalid last_char_offset"));
216-
}
217-
}
218-
219-
if bits.len() % 8 != 0 {
220-
return Err(SignatureError::from_source("Invalid bits length"));
221-
}
222-
223-
Ok(std::str::from_utf8(&bitarray_to_bytearray(&bits)?)
224-
.map_err(|_| SignatureError::from_source("Invalid UTF8 string"))?
225-
.to_owned())
226-
}
227-
228-
let extended_claim = decode_base64_url(&claim.value, &claim.index_mod_4)?;
229-
230-
// Last character of each extracted_claim must be '}' or ','
231-
if !(extended_claim.ends_with('}') || extended_claim.ends_with(',')) {
232-
return Err(SignatureError::from_source("Invalid extended claim"));
233-
}
234-
235-
let json_str = format!("{{{}}}", &extended_claim[..extended_claim.len() - 1]);
236-
237-
serde_json::from_str::<serde_json::Value>(&json_str)
238-
.map_err(SignatureError::from_source)?
239-
.as_object_mut()
240-
.and_then(|o| o.get_mut(expected_key))
241-
.map(serde_json::Value::take)
242-
.and_then(|v| match v {
243-
serde_json::Value::String(s) => Some(s),
244-
_ => None,
245-
})
246-
.ok_or_else(|| SignatureError::from_source("invalid extended claim"))
247-
}
248-
249-
pub(crate) fn zklogin_identifier_from_inputs(
250-
inputs: &ZkLoginInputs,
251-
) -> Result<sui_sdk_types::ZkLoginPublicIdentifier, SignatureError> {
252-
const ISS: &str = "iss";
253-
254-
let iss = verify_extended_claim(&inputs.iss_base64_details, ISS)?;
255-
sui_sdk_types::ZkLoginPublicIdentifier::new(iss, inputs.address_seed.clone())
256-
.ok_or_else(|| SignatureError::from_source("invalid iss"))
257-
}

crates/sui-sdk-types/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ rustdoc-args = [
2323

2424
[features]
2525
default = []
26-
serde = ["dep:serde", "dep:serde_derive", "dep:serde_with", "dep:bcs", "dep:serde_json", "roaring/std"]
26+
serde = ["dep:serde", "dep:serde_derive", "dep:serde_with", "dep:bcs", "dep:serde_json", "roaring/std", "dep:itertools"]
2727
rand = ["dep:rand_core"]
2828
hash = ["dep:blake2"]
2929
proptest = ["dep:proptest", "dep:test-strategy", "serde"]
@@ -42,6 +42,7 @@ serde_derive = { version = "1.0.210", optional = true }
4242
serde_with = { version = "3.9", default-features = false, features = ["alloc"], optional = true }
4343
bcs = { version = "0.1.6", optional = true }
4444
serde_json = { version = "1.0.128", optional = true }
45+
itertools = { version = "0.13.0", optional = true }
4546

4647
# RNG support
4748
rand_core = { version = "0.6.4", optional = true }

crates/sui-sdk-types/src/crypto/zklogin.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,32 @@ pub struct ZkLoginInputs {
5858
pub address_seed: Bn254FieldElement,
5959
}
6060

61+
impl ZkLoginInputs {
62+
#[cfg(feature = "serde")]
63+
#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
64+
pub fn iss(&self) -> Result<String, InvalidZkLoginClaimError> {
65+
const ISS: &str = "iss";
66+
67+
let iss = self.iss_base64_details.verify_extended_claim(ISS)?;
68+
69+
if iss.len() > 255 {
70+
Err(InvalidZkLoginClaimError::new("invalid iss: too long"))
71+
} else {
72+
Ok(iss)
73+
}
74+
}
75+
76+
#[cfg(feature = "serde")]
77+
#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
78+
pub fn public_identifier(&self) -> Result<ZkLoginPublicIdentifier, InvalidZkLoginClaimError> {
79+
let iss = self.iss()?;
80+
Ok(ZkLoginPublicIdentifier {
81+
iss,
82+
address_seed: self.address_seed.clone(),
83+
})
84+
}
85+
}
86+
6187
/// A claim of the iss in a zklogin proof
6288
///
6389
/// # BCS
@@ -78,6 +104,144 @@ pub struct ZkLoginClaim {
78104
pub index_mod_4: u8,
79105
}
80106

107+
#[derive(Debug)]
108+
pub struct InvalidZkLoginClaimError(String);
109+
110+
#[cfg(feature = "serde")]
111+
#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
112+
impl InvalidZkLoginClaimError {
113+
fn new<T: Into<String>>(err: T) -> Self {
114+
Self(err.into())
115+
}
116+
}
117+
118+
impl std::fmt::Display for InvalidZkLoginClaimError {
119+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120+
write!(f, "invalid zklogin claim: {}", self.0)
121+
}
122+
}
123+
124+
impl std::error::Error for InvalidZkLoginClaimError {}
125+
126+
#[cfg(feature = "serde")]
127+
#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
128+
impl ZkLoginClaim {
129+
fn verify_extended_claim(
130+
&self,
131+
expected_key: &str,
132+
) -> Result<String, InvalidZkLoginClaimError> {
133+
/// 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
134+
/// element in the output. Returns InvalidZkLoginClaimError if one of the characters is not in the base64 charset.
135+
fn base64_to_bitarray(input: &str) -> Result<Vec<u8>, InvalidZkLoginClaimError> {
136+
use itertools::Itertools;
137+
138+
const BASE64_URL_CHARSET: &str =
139+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
140+
141+
input
142+
.chars()
143+
.map(|c| {
144+
BASE64_URL_CHARSET
145+
.find(c)
146+
.map(|index| index as u8)
147+
.map(|index| (0..6).rev().map(move |i| (index >> i) & 1))
148+
.ok_or_else(|| {
149+
InvalidZkLoginClaimError::new("base64_to_bitarry invalid input")
150+
})
151+
})
152+
.flatten_ok()
153+
.collect()
154+
}
155+
156+
/// Convert a bitarray (each bit is represented by a u8) to a byte array by taking each 8 bits as a
157+
/// byte in big-endian format.
158+
fn bitarray_to_bytearray(bits: &[u8]) -> Result<Vec<u8>, InvalidZkLoginClaimError> {
159+
if bits.len() % 8 != 0 {
160+
return Err(InvalidZkLoginClaimError::new(
161+
"bitarray_to_bytearray invalid input",
162+
));
163+
}
164+
Ok(bits
165+
.chunks(8)
166+
.map(|chunk| {
167+
let mut byte = 0u8;
168+
for (i, bit) in chunk.iter().rev().enumerate() {
169+
byte |= bit << i;
170+
}
171+
byte
172+
})
173+
.collect())
174+
}
175+
176+
/// Parse the base64 string, add paddings based on offset, and convert to a bytearray.
177+
fn decode_base64_url(
178+
s: &str,
179+
index_mod_4: &u8,
180+
) -> Result<String, InvalidZkLoginClaimError> {
181+
if s.len() < 2 {
182+
return Err(InvalidZkLoginClaimError::new(
183+
"Base64 string smaller than 2",
184+
));
185+
}
186+
let mut bits = base64_to_bitarray(s)?;
187+
match index_mod_4 {
188+
0 => {}
189+
1 => {
190+
bits.drain(..2);
191+
}
192+
2 => {
193+
bits.drain(..4);
194+
}
195+
_ => {
196+
return Err(InvalidZkLoginClaimError::new("Invalid first_char_offset"));
197+
}
198+
}
199+
200+
let last_char_offset = (index_mod_4 + s.len() as u8 - 1) % 4;
201+
match last_char_offset {
202+
3 => {}
203+
2 => {
204+
bits.drain(bits.len() - 2..);
205+
}
206+
1 => {
207+
bits.drain(bits.len() - 4..);
208+
}
209+
_ => {
210+
return Err(InvalidZkLoginClaimError::new("Invalid last_char_offset"));
211+
}
212+
}
213+
214+
if bits.len() % 8 != 0 {
215+
return Err(InvalidZkLoginClaimError::new("Invalid bits length"));
216+
}
217+
218+
Ok(std::str::from_utf8(&bitarray_to_bytearray(&bits)?)
219+
.map_err(|_| InvalidZkLoginClaimError::new("Invalid UTF8 string"))?
220+
.to_owned())
221+
}
222+
223+
let extended_claim = decode_base64_url(&self.value, &self.index_mod_4)?;
224+
225+
// Last character of each extracted_claim must be '}' or ','
226+
if !(extended_claim.ends_with('}') || extended_claim.ends_with(',')) {
227+
return Err(InvalidZkLoginClaimError::new("Invalid extended claim"));
228+
}
229+
230+
let json_str = format!("{{{}}}", &extended_claim[..extended_claim.len() - 1]);
231+
232+
serde_json::from_str::<serde_json::Value>(&json_str)
233+
.map_err(|e| InvalidZkLoginClaimError::new(e.to_string()))?
234+
.as_object_mut()
235+
.and_then(|o| o.get_mut(expected_key))
236+
.map(serde_json::Value::take)
237+
.and_then(|v| match v {
238+
serde_json::Value::String(s) => Some(s),
239+
_ => None,
240+
})
241+
.ok_or_else(|| InvalidZkLoginClaimError::new("invalid extended claim"))
242+
}
243+
}
244+
81245
/// A zklogin groth16 proof
82246
///
83247
/// # BCS

0 commit comments

Comments
 (0)