Skip to main content

sui_core/
signature_verifier.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use fastcrypto_zkp::bn254::zk_login::JwkId;
5use fastcrypto_zkp::bn254::zk_login::{JWK, OIDCProvider};
6use fastcrypto_zkp::bn254::zk_login_api::ZkLoginEnv;
7use imbl::hashmap::HashMap as ImHashMap;
8use itertools::Itertools as _;
9use mysten_common::debug_fatal;
10use nonempty::NonEmpty;
11use parking_lot::RwLock;
12use prometheus::{IntCounter, Registry, register_int_counter_with_registry};
13use shared_crypto::intent::Intent;
14use std::sync::Arc;
15use sui_types::address_alias;
16use sui_types::base_types::{SequenceNumber, SuiAddress};
17use sui_types::digests::SenderSignedDataDigest;
18use sui_types::digests::ZKLoginInputsDigest;
19use sui_types::signature_verification::{
20    VerifiedDigestCache, verify_sender_signed_data_message_signatures,
21};
22use sui_types::storage::ObjectStore;
23use sui_types::transaction::{SenderSignedData, TransactionDataAPI};
24use sui_types::{
25    committee::Committee,
26    crypto::{AuthoritySignInfoTrait, VerificationObligation},
27    error::{SuiErrorKind, SuiResult},
28    message_envelope::Message,
29    messages_checkpoint::SignedCheckpointSummary,
30    signature::VerifyParams,
31};
32use tracing::debug;
33
34/// Verifies signatures in ways that are faster than verifying each signature individually.
35/// - BLS signatures (checkpoints) - batch verification.
36/// - User signed data - caching.
37pub struct SignatureVerifier {
38    committee: Arc<Committee>,
39    object_store: Arc<dyn ObjectStore + Send + Sync>,
40    signed_data_cache: VerifiedDigestCache<SenderSignedDataDigest, Vec<u8>>,
41    zklogin_inputs_cache: Arc<VerifiedDigestCache<ZKLoginInputsDigest>>,
42
43    /// Map from JwkId (iss, kid) to the fetched JWK for that key.
44    /// We use an immutable data structure because verification of ZKLogins may be slow, so we
45    /// don't want to pass a reference to the map to the verify method, since that would lead to a
46    /// lengthy critical section. Instead, we use an immutable data structure which can be cloned
47    /// very cheaply.
48    jwks: RwLock<ImHashMap<JwkId, JWK>>,
49
50    /// Params that contains a list of supported providers for ZKLogin and the environment (prod/test) the code runs in.
51    zk_login_params: ZkLoginParams,
52
53    /// If true, uses address aliases during signature verification.
54    enable_address_aliases: bool,
55
56    pub metrics: Arc<SignatureVerifierMetrics>,
57}
58
59/// Contains two parameters to pass in to verify a ZkLogin signature.
60#[derive(Clone)]
61struct ZkLoginParams {
62    /// A list of supported OAuth providers for ZkLogin.
63    pub supported_providers: Vec<OIDCProvider>,
64    /// The environment (prod/test) the code runs in. It decides which verifying key to use in fastcrypto.
65    pub env: ZkLoginEnv,
66    /// zkLogin circuit verify mode: 0 = v1 circuit only, 1 = v2 circuit with
67    /// fallback to v1, 2 = v2 circuit only.
68    pub zklogin_circuit_mode: u64,
69    /// Flag to determine whether legacy address (derived from padded address seed) should be verified.
70    pub verify_legacy_zklogin_address: bool,
71    // Flag to determine whether zkLogin inside multisig is accepted.
72    pub accept_zklogin_in_multisig: bool,
73    // Flag to determine whether passkey inside multisig is accepted.
74    pub accept_passkey_in_multisig: bool,
75    /// Flag to determine whether ML-DSA-65 inside multisig is accepted; fed by
76    /// the `mldsa65_auth` protocol flag, which covers standalone use as well.
77    pub accept_mldsa65_in_multisig: bool,
78    /// Value that sets the upper bound for max_epoch in zkLogin signature.
79    pub zklogin_max_epoch_upper_bound_delta: Option<u64>,
80    /// Flag to determine whether additional multisig checks are performed.
81    pub additional_multisig_checks: bool,
82    /// Flag to determine whether additional zkLogin public identifier structure is validated.
83    pub validate_zklogin_public_identifier: bool,
84}
85
86impl SignatureVerifier {
87    pub fn new(
88        committee: Arc<Committee>,
89        object_store: Arc<dyn ObjectStore + Send + Sync>,
90        metrics: Arc<SignatureVerifierMetrics>,
91        supported_providers: Vec<OIDCProvider>,
92        zklogin_env: ZkLoginEnv,
93        zklogin_circuit_mode: u64,
94        verify_legacy_zklogin_address: bool,
95        accept_zklogin_in_multisig: bool,
96        accept_passkey_in_multisig: bool,
97        accept_mldsa65_in_multisig: bool,
98        zklogin_max_epoch_upper_bound_delta: Option<u64>,
99        additional_multisig_checks: bool,
100        validate_zklogin_public_identifier: bool,
101        enable_address_aliases: bool,
102    ) -> Self {
103        Self {
104            committee,
105            object_store,
106            signed_data_cache: VerifiedDigestCache::new(
107                metrics.signed_data_cache_hits.clone(),
108                metrics.signed_data_cache_misses.clone(),
109                metrics.signed_data_cache_evictions.clone(),
110            ),
111            zklogin_inputs_cache: Arc::new(VerifiedDigestCache::new(
112                metrics.zklogin_inputs_cache_hits.clone(),
113                metrics.zklogin_inputs_cache_misses.clone(),
114                metrics.zklogin_inputs_cache_evictions.clone(),
115            )),
116            jwks: Default::default(),
117            enable_address_aliases,
118            metrics,
119            zk_login_params: ZkLoginParams {
120                supported_providers,
121                env: zklogin_env,
122                zklogin_circuit_mode,
123                verify_legacy_zklogin_address,
124                accept_zklogin_in_multisig,
125                accept_passkey_in_multisig,
126                accept_mldsa65_in_multisig,
127                zklogin_max_epoch_upper_bound_delta,
128                additional_multisig_checks,
129                validate_zklogin_public_identifier,
130            },
131        }
132    }
133
134    /// Insert a JWK into the verifier state. Pre-existing entries for a given JwkId will not be
135    /// overwritten.
136    pub(crate) fn insert_jwk(&self, jwk_id: &JwkId, jwk: &JWK) {
137        let mut jwks = self.jwks.write();
138        match jwks.entry(jwk_id.clone()) {
139            imbl::hashmap::Entry::Occupied(_) => {
140                debug!("JWK with kid {:?} already exists", jwk_id);
141            }
142            imbl::hashmap::Entry::Vacant(entry) => {
143                debug!("inserting JWK with kid: {:?}", jwk_id);
144                entry.insert(jwk.clone());
145            }
146        }
147    }
148
149    pub fn has_jwk(&self, jwk_id: &JwkId, jwk: &JWK) -> bool {
150        let jwks = self.jwks.read();
151        jwks.get(jwk_id) == Some(jwk)
152    }
153
154    pub fn get_jwks(&self) -> ImHashMap<JwkId, JWK> {
155        self.jwks.read().clone()
156    }
157
158    // For each required signer in the transaction, returns the signature index and
159    // version of the AddressAliases object used to verify it.
160    pub fn verify_tx_with_current_aliases(
161        &self,
162        signed_tx: &SenderSignedData,
163    ) -> SuiResult<NonEmpty<(u8, Option<SequenceNumber>)>> {
164        let mut alias_versions_by_signer = Vec::new();
165        let mut aliases = Vec::new();
166
167        // Look up aliases for each address at the current version.
168        let signers = signed_tx.intent_message().value.required_signers();
169        for signer in signers {
170            if !self.enable_address_aliases {
171                alias_versions_by_signer.push((signer, None));
172                aliases.push((signer, NonEmpty::singleton(signer)));
173            } else {
174                // Look up aliases for the signer using the derived object address.
175                let address_aliases =
176                    address_alias::get_address_aliases_from_store(&self.object_store, signer)?;
177
178                alias_versions_by_signer.push((signer, address_aliases.as_ref().map(|(_, v)| *v)));
179                aliases.push((
180                    signer,
181                    address_aliases
182                        .map(|(aliases, _)| {
183                            NonEmpty::from_vec(aliases.aliases.contents.clone()).unwrap_or_else(
184                                || {
185                                    debug_fatal!(
186                                    "AddressAliases struct has empty aliases field for signer {}",
187                                    signer
188                                );
189                                    NonEmpty::singleton(signer)
190                                },
191                            )
192                        })
193                        .unwrap_or(NonEmpty::singleton(signer)),
194                ));
195            }
196        }
197
198        // Verify and get the signature indices for each required signer.
199        let sig_indices = self.verify_tx(signed_tx, &alias_versions_by_signer, aliases)?;
200
201        // Combine signature indices with alias versions.
202        let result: Vec<(u8, Option<SequenceNumber>)> = sig_indices
203            .into_iter()
204            .zip_eq(alias_versions_by_signer.into_iter().map(|(_, seq)| seq))
205            .collect();
206
207        Ok(NonEmpty::from_vec(result).expect("must have at least one required_signer"))
208    }
209
210    pub fn verify_tx_require_no_aliases(&self, signed_tx: &SenderSignedData) -> SuiResult {
211        let current_aliases = self.verify_tx_with_current_aliases(signed_tx)?;
212        for (_, version) in current_aliases {
213            if version.is_some() {
214                return Err(SuiErrorKind::AliasesChanged.into());
215            }
216        }
217        Ok(())
218    }
219
220    fn verify_tx(
221        &self,
222        signed_tx: &SenderSignedData,
223        alias_versions: &Vec<(SuiAddress, Option<SequenceNumber>)>,
224        aliased_addresses: Vec<(SuiAddress, NonEmpty<SuiAddress>)>,
225    ) -> SuiResult<Vec<u8>> {
226        let digest = signed_tx.full_message_digest_with_alias_versions(alias_versions);
227
228        if let Some(indices) = self.signed_data_cache.get_cached(&digest) {
229            return Ok(indices);
230        }
231
232        let jwks = self.jwks.read().clone();
233        let verify_params = VerifyParams::new(
234            jwks,
235            self.zk_login_params.supported_providers.clone(),
236            self.zk_login_params.env,
237            self.zk_login_params.zklogin_circuit_mode,
238            self.zk_login_params.verify_legacy_zklogin_address,
239            self.zk_login_params.accept_zklogin_in_multisig,
240            self.zk_login_params.accept_passkey_in_multisig,
241            self.zk_login_params.zklogin_max_epoch_upper_bound_delta,
242            self.zk_login_params.additional_multisig_checks,
243            self.zk_login_params.validate_zklogin_public_identifier,
244        )
245        .with_mldsa65_in_multisig(self.zk_login_params.accept_mldsa65_in_multisig);
246        let indices = verify_sender_signed_data_message_signatures(
247            signed_tx,
248            self.committee.epoch(),
249            &verify_params,
250            self.zklogin_inputs_cache.clone(),
251            aliased_addresses,
252        )?;
253
254        self.signed_data_cache
255            .cache_with_value(digest, indices.clone());
256        Ok(indices)
257    }
258
259    pub fn clear_signature_cache(&self) {
260        self.signed_data_cache.clear();
261        self.zklogin_inputs_cache.clear();
262    }
263}
264
265pub struct SignatureVerifierMetrics {
266    pub signed_data_cache_hits: IntCounter,
267    pub signed_data_cache_misses: IntCounter,
268    pub signed_data_cache_evictions: IntCounter,
269    pub zklogin_inputs_cache_hits: IntCounter,
270    pub zklogin_inputs_cache_misses: IntCounter,
271    pub zklogin_inputs_cache_evictions: IntCounter,
272}
273
274impl SignatureVerifierMetrics {
275    pub fn new(registry: &Registry) -> Arc<Self> {
276        Arc::new(Self {
277            signed_data_cache_hits: register_int_counter_with_registry!(
278                "signed_data_cache_hits",
279                "Number of signed data which were known to be verified because of signature cache.",
280                registry
281            )
282            .unwrap(),
283            signed_data_cache_misses: register_int_counter_with_registry!(
284                "signed_data_cache_misses",
285                "Number of signed data which missed the signature cache.",
286                registry
287            )
288            .unwrap(),
289            signed_data_cache_evictions: register_int_counter_with_registry!(
290                "signed_data_cache_evictions",
291                "Number of times we evict a pre-existing signed data were known to be verified because of signature cache.",
292                registry
293            )
294                .unwrap(),
295                zklogin_inputs_cache_hits: register_int_counter_with_registry!(
296                    "zklogin_inputs_cache_hits",
297                    "Number of zklogin signature which were known to be partially verified because of zklogin inputs cache.",
298                    registry
299                )
300                .unwrap(),
301                zklogin_inputs_cache_misses: register_int_counter_with_registry!(
302                    "zklogin_inputs_cache_misses",
303                    "Number of zklogin signatures which missed the zklogin inputs cache.",
304                    registry
305                )
306                .unwrap(),
307                zklogin_inputs_cache_evictions: register_int_counter_with_registry!(
308                    "zklogin_inputs_cache_evictions",
309                    "Number of times we evict a pre-existing zklogin inputs digest that was known to be verified because of zklogin inputs cache.",
310                    registry
311                )
312                .unwrap(),
313        })
314    }
315}
316
317/// Batch-verifies checkpoint signatures - if any fail return error.
318pub(crate) fn batch_verify_checkpoints(
319    committee: &Committee,
320    checkpoints: &[&SignedCheckpointSummary],
321) -> SuiResult {
322    for ckpt in checkpoints {
323        ckpt.data().verify_epoch(committee.epoch())?;
324    }
325
326    let mut obligation = VerificationObligation::default();
327
328    for ckpt in checkpoints {
329        let idx = obligation.add_message(ckpt.data(), ckpt.epoch(), Intent::sui_app(ckpt.scope()));
330        ckpt.auth_sig()
331            .add_to_verification_obligation(committee, &mut obligation, idx)?;
332    }
333
334    obligation.verify_all()
335}