Skip to main content

iota_transaction_checks/
deny.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use iota_sdk_types::{Command, ObjectId, ObjectReference, Transaction, UserSignature};
6use iota_types::{
7    account_abstraction::authenticator_function::AuthenticatorFunctionRef,
8    deny_rule_governance::DenyRuleConfig,
9    error::{IotaError, IotaResult, UserInputError},
10    storage::BackingPackageStore,
11    transaction::{InputObjectKind, TransactionAPI, TransactionKindExt},
12};
13use tracing::instrument;
14macro_rules! deny_if_true {
15    ($cond:expr, $msg:expr) => {
16        if ($cond) {
17            return Err(IotaError::UserInput {
18                error: UserInputError::TransactionDenied {
19                    error: $msg.to_string(),
20                },
21            });
22        }
23    };
24}
25
26/// Check that the provided transaction is allowed to be signed according to the
27/// deny config.
28#[instrument(level = "trace", skip_all, fields(tx_digest = ?tx.digest()))]
29pub fn check_transaction_for_validation(
30    tx: &Transaction,
31    tx_signatures: &[UserSignature],
32    input_object_kinds: &[InputObjectKind],
33    receiving_objects: &[ObjectReference],
34    filter_config: &dyn DenyRuleConfig,
35    package_store: &dyn BackingPackageStore,
36) -> IotaResult {
37    check_disabled_features(filter_config, tx, tx_signatures)?;
38
39    check_signers(filter_config, tx)?;
40
41    check_input_objects(filter_config, input_object_kinds)?;
42
43    check_package_dependencies(filter_config, tx, package_store)?;
44
45    check_receiving_objects(filter_config, receiving_objects)?;
46
47    Ok(())
48}
49
50#[instrument(level = "trace", skip_all)]
51fn check_receiving_objects(
52    filter_config: &dyn DenyRuleConfig,
53    receiving_objects: &[ObjectReference],
54) -> IotaResult {
55    deny_if_true!(
56        filter_config.receiving_objects_disabled() && !receiving_objects.is_empty(),
57        "Receiving objects is temporarily disabled".to_string()
58    );
59    if !filter_config.has_denied_objects() {
60        return Ok(());
61    }
62    for receiving_object in receiving_objects {
63        deny_if_true!(
64            filter_config.is_object_denied(&receiving_object.object_id),
65            format!(
66                "Access to object {:?} is temporarily disabled",
67                receiving_object.object_id
68            )
69        );
70    }
71    Ok(())
72}
73
74#[instrument(level = "trace", skip_all)]
75fn check_disabled_features(
76    filter_config: &dyn DenyRuleConfig,
77    tx: &Transaction,
78    tx_signatures: &[UserSignature],
79) -> IotaResult {
80    deny_if_true!(
81        filter_config.user_transaction_disabled(),
82        "Transaction signing is temporarily disabled"
83    );
84
85    tx_signatures.iter().try_for_each(|s| {
86        if let UserSignature::MoveAuthenticator(_) = s {
87            deny_if_true!(
88                filter_config.move_authenticator_disabled(),
89                "MoveAuthenticator is temporarily disabled"
90            );
91        }
92        Ok(())
93    })?;
94
95    if !filter_config.package_publish_disabled() && !filter_config.package_upgrade_disabled() {
96        return Ok(());
97    }
98
99    for command in tx.kind().iter_commands() {
100        deny_if_true!(
101            filter_config.package_publish_disabled() && matches!(command, Command::Publish(..)),
102            "Package publish is temporarily disabled"
103        );
104        deny_if_true!(
105            filter_config.package_upgrade_disabled() && matches!(command, Command::Upgrade(..)),
106            "Package upgrade is temporarily disabled"
107        );
108    }
109    Ok(())
110}
111
112#[instrument(level = "trace", skip_all)]
113fn check_signers(filter_config: &dyn DenyRuleConfig, tx: &Transaction) -> IotaResult {
114    if !filter_config.has_denied_addresses() {
115        return Ok(());
116    }
117    for signer in tx.signers() {
118        deny_if_true!(
119            filter_config.is_address_denied(&signer),
120            format!(
121                "Access to account address {:?} is temporarily disabled",
122                signer
123            )
124        );
125    }
126    Ok(())
127}
128
129#[instrument(level = "trace", skip_all)]
130fn check_input_objects(
131    filter_config: &dyn DenyRuleConfig,
132    input_object_kinds: &[InputObjectKind],
133) -> IotaResult {
134    let shared_object_disabled = filter_config.shared_object_disabled();
135    if !filter_config.has_denied_objects() && !shared_object_disabled {
136        // No need to iterate through the input objects if no relevant policy is set.
137        return Ok(());
138    }
139    for input_object_kind in input_object_kinds {
140        let id = input_object_kind.object_id();
141        deny_if_true!(
142            filter_config.is_object_denied(&id),
143            format!("Access to input object {id} is temporarily disabled")
144        );
145        deny_if_true!(
146            shared_object_disabled && input_object_kind.is_shared_object(),
147            "Usage of shared object in transactions is temporarily disabled"
148        );
149    }
150    Ok(())
151}
152
153#[instrument(level = "trace", skip_all)]
154fn check_package_dependencies(
155    filter_config: &dyn DenyRuleConfig,
156    tx: &Transaction,
157    package_store: &dyn BackingPackageStore,
158) -> IotaResult {
159    if !filter_config.has_denied_packages() {
160        return Ok(());
161    }
162    let mut dependencies = vec![];
163    for command in tx.kind().iter_commands() {
164        match command {
165            Command::Publish(cmd) => {
166                // It is possible that the deps list is inaccurate since it's provided
167                // by the user. But that's OK because this publish transaction will fail
168                // to execute in the end. Similar reasoning for Upgrade.
169                dependencies.extend(cmd.dependencies.iter().copied());
170            }
171            Command::Upgrade(cmd) => {
172                dependencies.extend(cmd.dependencies.iter().copied());
173                // It's crucial that we don't allow upgrading a package in the deny list,
174                // otherwise one can bypass the deny list by upgrading a package.
175                dependencies.push(cmd.package);
176            }
177            Command::MoveCall(cmd) => {
178                dependencies.extend(package_and_dependency_ids(cmd.package, package_store)?);
179            }
180            Command::TransferObjects(..)
181            | Command::SplitCoins(..)
182            | Command::MergeCoins(..)
183            | Command::MakeMoveVector(..) => {}
184            _ => unimplemented!("a new Command enum variant was added and needs to be handled"),
185        }
186    }
187    for dep in dependencies {
188        deny_if_true!(
189            filter_config.is_package_denied(&dep),
190            format!("Access to package {dep} is temporarily disabled")
191        );
192    }
193    Ok(())
194}
195
196/// Check that no `MoveAuthenticator` authenticates through a denied package.
197///
198/// The package holding the authenticate function is named only by the account's
199/// `AuthenticatorFunctionRef`: the call is assembled during execution, and the
200/// reference is a runtime one, so the package appears in neither the
201/// transaction's commands nor any linkage table. It is therefore checked here
202/// rather than alongside a transaction's commands in
203/// [`check_transaction_for_validation`].
204///
205/// Call it once the references have been loaded, over every authenticator the
206/// transaction carries, and before any authenticator runs.
207#[instrument(level = "trace", skip_all)]
208pub fn check_authenticator_packages<'a>(
209    filter_config: &dyn DenyRuleConfig,
210    authenticator_function_refs: impl IntoIterator<Item = &'a AuthenticatorFunctionRef>,
211    package_store: &dyn BackingPackageStore,
212) -> IotaResult {
213    if !filter_config.has_denied_packages() {
214        return Ok(());
215    }
216    for authenticator_function_ref in authenticator_function_refs {
217        let package_id = match authenticator_function_ref {
218            AuthenticatorFunctionRef::V1(v1) => v1.package,
219        };
220        for dep in package_and_dependency_ids(package_id, package_store)? {
221            deny_if_true!(
222                filter_config.is_package_denied(&dep),
223                format!(
224                    "Access to package {dep} from a Move authenticator is temporarily disabled"
225                )
226            );
227        }
228    }
229    Ok(())
230}
231
232/// The ids the deny list has to see for a call into `package_id`: the package
233/// itself, and the upgraded id of each of its dependencies.
234///
235/// `linkage_table` maps the original id of a dependency to the id actually
236/// loaded, and only the latter is returned. So this establishes that a denied
237/// package is not in current use, and denying one version of a package still
238/// permits a newer one.
239fn package_and_dependency_ids(
240    package_id: ObjectId,
241    package_store: &dyn BackingPackageStore,
242) -> IotaResult<Vec<ObjectId>> {
243    let package = package_store
244        .get_package_object(&package_id)?
245        .ok_or(IotaError::UserInput {
246            error: UserInputError::ObjectNotFound {
247                object_id: package_id,
248                version: None,
249            },
250        })?;
251    let package = package.move_package();
252    Ok(package
253        .linkage_table()
254        .values()
255        .map(|upgrade_info| upgrade_info.upgraded_id)
256        .chain(std::iter::once(package.id()))
257        .collect())
258}
259
260#[cfg(test)]
261mod tests {
262    use std::collections::{BTreeMap, HashMap};
263
264    use iota_sdk_types::{
265        DenyRuleSet, MovePackage, ObjectId, TransactionDigest, UpgradeInfo, Version,
266    };
267    use iota_types::{
268        account_abstraction::authenticator_function::{
269            AuthenticatorFunctionRef, AuthenticatorFunctionRefV1,
270        },
271        object::Object,
272        storage::PackageObject,
273    };
274
275    use super::*;
276
277    /// A package store holding only the packages a test puts in it; anything
278    /// else reads back as missing.
279    #[derive(Default)]
280    struct TestPackageStore(HashMap<ObjectId, PackageObject>);
281
282    impl BackingPackageStore for TestPackageStore {
283        fn get_package_object(&self, package_id: &ObjectId) -> IotaResult<Option<PackageObject>> {
284            Ok(self.0.get(package_id).cloned())
285        }
286    }
287
288    impl TestPackageStore {
289        /// Adds a module-less package whose linkage table maps each dependency
290        /// onto itself, which is what a package that has never been upgraded
291        /// past its dependencies looks like.
292        fn with_package(
293            mut self,
294            id: ObjectId,
295            dependencies: impl IntoIterator<Item = ObjectId>,
296        ) -> Self {
297            let linkage_table: BTreeMap<_, _> = dependencies
298                .into_iter()
299                .map(|dep| {
300                    (
301                        dep,
302                        UpgradeInfo {
303                            upgraded_id: dep,
304                            upgraded_version: Version::OBJECT_START,
305                        },
306                    )
307                })
308                .collect();
309            let package = MovePackage::new(
310                id,
311                Version::OBJECT_START,
312                BTreeMap::new(),
313                vec![],
314                linkage_table,
315            );
316            self.0.insert(
317                id,
318                PackageObject::new(Object::new_from_package(
319                    package,
320                    TransactionDigest::genesis_marker(),
321                )),
322            );
323            self
324        }
325    }
326
327    fn id(byte: u8) -> ObjectId {
328        ObjectId::new([byte; 32])
329    }
330
331    fn authenticator_ref(package: ObjectId) -> AuthenticatorFunctionRef {
332        AuthenticatorFunctionRef::V1(AuthenticatorFunctionRefV1 {
333            package,
334            module: "account".to_string(),
335            function: "authenticate".to_string(),
336        })
337    }
338
339    fn deny_packages(packages: impl IntoIterator<Item = ObjectId>) -> DenyRuleSet {
340        DenyRuleSet {
341            denied_packages: packages.into_iter().collect(),
342            ..Default::default()
343        }
344    }
345
346    fn assert_denied(result: IotaResult) {
347        assert!(matches!(
348            result.unwrap_err(),
349            IotaError::UserInput {
350                error: UserInputError::TransactionDenied { .. }
351            }
352        ));
353    }
354
355    #[test]
356    fn denies_the_authenticator_package_itself() {
357        let package = id(1);
358        let store = TestPackageStore::default().with_package(package, []);
359
360        assert_denied(check_authenticator_packages(
361            &deny_packages([package]),
362            &[authenticator_ref(package)],
363            &store,
364        ));
365    }
366
367    #[test]
368    fn denies_a_dependency_of_the_authenticator_package() {
369        let (package, dependency) = (id(1), id(2));
370        let store = TestPackageStore::default().with_package(package, [dependency]);
371
372        assert_denied(check_authenticator_packages(
373            &deny_packages([dependency]),
374            &[authenticator_ref(package)],
375            &store,
376        ));
377    }
378
379    #[test]
380    fn denies_when_any_one_of_several_authenticators_is_denied() {
381        // A sponsored transaction carries one authenticator per signer, and the
382        // sender's must be judged even when the sponsor's is fine.
383        let (sender_package, sponsor_package) = (id(1), id(2));
384        let store = TestPackageStore::default()
385            .with_package(sender_package, [])
386            .with_package(sponsor_package, []);
387
388        assert_denied(check_authenticator_packages(
389            &deny_packages([sender_package]),
390            &[
391                authenticator_ref(sponsor_package),
392                authenticator_ref(sender_package),
393            ],
394            &store,
395        ));
396    }
397
398    #[test]
399    fn allows_a_package_that_is_not_denied() {
400        let (package, dependency, denied) = (id(1), id(2), id(3));
401        let store = TestPackageStore::default().with_package(package, [dependency]);
402
403        check_authenticator_packages(
404            &deny_packages([denied]),
405            &[authenticator_ref(package)],
406            &store,
407        )
408        .unwrap();
409    }
410
411    #[test]
412    fn reads_no_package_when_nothing_is_denied() {
413        // The empty store would report the package as missing, so an error here
414        // would mean the early exit failed to spare the read.
415        check_authenticator_packages(
416            &DenyRuleSet::default(),
417            &[authenticator_ref(id(1))],
418            &TestPackageStore::default(),
419        )
420        .unwrap();
421    }
422
423    #[test]
424    fn reports_a_missing_authenticator_package() {
425        let missing = id(1);
426
427        assert!(matches!(
428            check_authenticator_packages(
429                &deny_packages([id(9)]),
430                &[authenticator_ref(missing)],
431                &TestPackageStore::default(),
432            )
433            .unwrap_err(),
434            IotaError::UserInput {
435                error: UserInputError::ObjectNotFound { object_id, .. }
436            } if object_id == missing
437        ));
438    }
439}