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 im::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    /// Value that sets the upper bound for max_epoch in zkLogin signature.
76    pub zklogin_max_epoch_upper_bound_delta: Option<u64>,
77    /// Flag to determine whether additional multisig checks are performed.
78    pub additional_multisig_checks: bool,
79    /// Flag to determine whether additional zkLogin public identifier structure is validated.
80    pub validate_zklogin_public_identifier: bool,
81}
82
83impl SignatureVerifier {
84    pub fn new(
85        committee: Arc<Committee>,
86        object_store: Arc<dyn ObjectStore + Send + Sync>,
87        metrics: Arc<SignatureVerifierMetrics>,
88        supported_providers: Vec<OIDCProvider>,
89        zklogin_env: ZkLoginEnv,
90        zklogin_circuit_mode: u64,
91        verify_legacy_zklogin_address: bool,
92        accept_zklogin_in_multisig: bool,
93        accept_passkey_in_multisig: bool,
94        zklogin_max_epoch_upper_bound_delta: Option<u64>,
95        additional_multisig_checks: bool,
96        validate_zklogin_public_identifier: bool,
97        enable_address_aliases: bool,
98    ) -> Self {
99        Self {
100            committee,
101            object_store,
102            signed_data_cache: VerifiedDigestCache::new(
103                metrics.signed_data_cache_hits.clone(),
104                metrics.signed_data_cache_misses.clone(),
105                metrics.signed_data_cache_evictions.clone(),
106            ),
107            zklogin_inputs_cache: Arc::new(VerifiedDigestCache::new(
108                metrics.zklogin_inputs_cache_hits.clone(),
109                metrics.zklogin_inputs_cache_misses.clone(),
110                metrics.zklogin_inputs_cache_evictions.clone(),
111            )),
112            jwks: Default::default(),
113            enable_address_aliases,
114            metrics,
115            zk_login_params: ZkLoginParams {
116                supported_providers,
117                env: zklogin_env,
118                zklogin_circuit_mode,
119                verify_legacy_zklogin_address,
120                accept_zklogin_in_multisig,
121                accept_passkey_in_multisig,
122                zklogin_max_epoch_upper_bound_delta,
123                additional_multisig_checks,
124                validate_zklogin_public_identifier,
125            },
126        }
127    }
128
129    /// Insert a JWK into the verifier state. Pre-existing entries for a given JwkId will not be
130    /// overwritten.
131    pub(crate) fn insert_jwk(&self, jwk_id: &JwkId, jwk: &JWK) {
132        let mut jwks = self.jwks.write();
133        match jwks.entry(jwk_id.clone()) {
134            im::hashmap::Entry::Occupied(_) => {
135                debug!("JWK with kid {:?} already exists", jwk_id);
136            }
137            im::hashmap::Entry::Vacant(entry) => {
138                debug!("inserting JWK with kid: {:?}", jwk_id);
139                entry.insert(jwk.clone());
140            }
141        }
142    }
143
144    pub fn has_jwk(&self, jwk_id: &JwkId, jwk: &JWK) -> bool {
145        let jwks = self.jwks.read();
146        jwks.get(jwk_id) == Some(jwk)
147    }
148
149    pub fn get_jwks(&self) -> ImHashMap<JwkId, JWK> {
150        self.jwks.read().clone()
151    }
152
153    // For each required signer in the transaction, returns the signature index and
154    // version of the AddressAliases object used to verify it.
155    pub fn verify_tx_with_current_aliases(
156        &self,
157        signed_tx: &SenderSignedData,
158    ) -> SuiResult<NonEmpty<(u8, Option<SequenceNumber>)>> {
159        let mut alias_versions_by_signer = Vec::new();
160        let mut aliases = Vec::new();
161
162        // Look up aliases for each address at the current version.
163        let signers = signed_tx.intent_message().value.required_signers();
164        for signer in signers {
165            if !self.enable_address_aliases {
166                alias_versions_by_signer.push((signer, None));
167                aliases.push((signer, NonEmpty::singleton(signer)));
168            } else {
169                // Look up aliases for the signer using the derived object address.
170                let address_aliases =
171                    address_alias::get_address_aliases_from_store(&self.object_store, signer)?;
172
173                alias_versions_by_signer.push((signer, address_aliases.as_ref().map(|(_, v)| *v)));
174                aliases.push((
175                    signer,
176                    address_aliases
177                        .map(|(aliases, _)| {
178                            NonEmpty::from_vec(aliases.aliases.contents.clone()).unwrap_or_else(
179                                || {
180                                    debug_fatal!(
181                                    "AddressAliases struct has empty aliases field for signer {}",
182                                    signer
183                                );
184                                    NonEmpty::singleton(signer)
185                                },
186                            )
187                        })
188                        .unwrap_or(NonEmpty::singleton(signer)),
189                ));
190            }
191        }
192
193        // Verify and get the signature indices for each required signer.
194        let sig_indices = self.verify_tx(signed_tx, &alias_versions_by_signer, aliases)?;
195
196        // Combine signature indices with alias versions.
197        let result: Vec<(u8, Option<SequenceNumber>)> = sig_indices
198            .into_iter()
199            .zip_eq(alias_versions_by_signer.into_iter().map(|(_, seq)| seq))
200            .collect();
201
202        Ok(NonEmpty::from_vec(result).expect("must have at least one required_signer"))
203    }
204
205    pub fn verify_tx_require_no_aliases(&self, signed_tx: &SenderSignedData) -> SuiResult {
206        let current_aliases = self.verify_tx_with_current_aliases(signed_tx)?;
207        for (_, version) in current_aliases {
208            if version.is_some() {
209                return Err(SuiErrorKind::AliasesChanged.into());
210            }
211        }
212        Ok(())
213    }
214
215    fn verify_tx(
216        &self,
217        signed_tx: &SenderSignedData,
218        alias_versions: &Vec<(SuiAddress, Option<SequenceNumber>)>,
219        aliased_addresses: Vec<(SuiAddress, NonEmpty<SuiAddress>)>,
220    ) -> SuiResult<Vec<u8>> {
221        let digest = signed_tx.full_message_digest_with_alias_versions(alias_versions);
222
223        if let Some(indices) = self.signed_data_cache.get_cached(&digest) {
224            return Ok(indices);
225        }
226
227        let jwks = self.jwks.read().clone();
228        let verify_params = VerifyParams::new(
229            jwks,
230            self.zk_login_params.supported_providers.clone(),
231            self.zk_login_params.env,
232            self.zk_login_params.zklogin_circuit_mode,
233            self.zk_login_params.verify_legacy_zklogin_address,
234            self.zk_login_params.accept_zklogin_in_multisig,
235            self.zk_login_params.accept_passkey_in_multisig,
236            self.zk_login_params.zklogin_max_epoch_upper_bound_delta,
237            self.zk_login_params.additional_multisig_checks,
238            self.zk_login_params.validate_zklogin_public_identifier,
239        );
240        let indices = verify_sender_signed_data_message_signatures(
241            signed_tx,
242            self.committee.epoch(),
243            &verify_params,
244            self.zklogin_inputs_cache.clone(),
245            aliased_addresses,
246        )?;
247
248        self.signed_data_cache
249            .cache_with_value(digest, indices.clone());
250        Ok(indices)
251    }
252
253    pub fn clear_signature_cache(&self) {
254        self.signed_data_cache.clear();
255        self.zklogin_inputs_cache.clear();
256    }
257}
258
259pub struct SignatureVerifierMetrics {
260    pub signed_data_cache_hits: IntCounter,
261    pub signed_data_cache_misses: IntCounter,
262    pub signed_data_cache_evictions: IntCounter,
263    pub zklogin_inputs_cache_hits: IntCounter,
264    pub zklogin_inputs_cache_misses: IntCounter,
265    pub zklogin_inputs_cache_evictions: IntCounter,
266}
267
268impl SignatureVerifierMetrics {
269    pub fn new(registry: &Registry) -> Arc<Self> {
270        Arc::new(Self {
271            signed_data_cache_hits: register_int_counter_with_registry!(
272                "signed_data_cache_hits",
273                "Number of signed data which were known to be verified because of signature cache.",
274                registry
275            )
276            .unwrap(),
277            signed_data_cache_misses: register_int_counter_with_registry!(
278                "signed_data_cache_misses",
279                "Number of signed data which missed the signature cache.",
280                registry
281            )
282            .unwrap(),
283            signed_data_cache_evictions: register_int_counter_with_registry!(
284                "signed_data_cache_evictions",
285                "Number of times we evict a pre-existing signed data were known to be verified because of signature cache.",
286                registry
287            )
288                .unwrap(),
289                zklogin_inputs_cache_hits: register_int_counter_with_registry!(
290                    "zklogin_inputs_cache_hits",
291                    "Number of zklogin signature which were known to be partially verified because of zklogin inputs cache.",
292                    registry
293                )
294                .unwrap(),
295                zklogin_inputs_cache_misses: register_int_counter_with_registry!(
296                    "zklogin_inputs_cache_misses",
297                    "Number of zklogin signatures which missed the zklogin inputs cache.",
298                    registry
299                )
300                .unwrap(),
301                zklogin_inputs_cache_evictions: register_int_counter_with_registry!(
302                    "zklogin_inputs_cache_evictions",
303                    "Number of times we evict a pre-existing zklogin inputs digest that was known to be verified because of zklogin inputs cache.",
304                    registry
305                )
306                .unwrap(),
307        })
308    }
309}
310
311/// Batch-verifies checkpoint signatures - if any fail return error.
312pub(crate) fn batch_verify_checkpoints(
313    committee: &Committee,
314    checkpoints: &[&SignedCheckpointSummary],
315) -> SuiResult {
316    for ckpt in checkpoints {
317        ckpt.data().verify_epoch(committee.epoch())?;
318    }
319
320    let mut obligation = VerificationObligation::default();
321
322    for ckpt in checkpoints {
323        let idx = obligation.add_message(ckpt.data(), ckpt.epoch(), Intent::sui_app(ckpt.scope()));
324        ckpt.auth_sig()
325            .add_to_verification_obligation(committee, &mut obligation, idx)?;
326    }
327
328    obligation.verify_all()
329}