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