diff --git a/runtime-sdk/modules/evm/src/precompile/subcall.rs b/runtime-sdk/modules/evm/src/precompile/subcall.rs index f0221e8e2a..1fa93385f0 100644 --- a/runtime-sdk/modules/evm/src/precompile/subcall.rs +++ b/runtime-sdk/modules/evm/src/precompile/subcall.rs @@ -215,6 +215,7 @@ mod test { gas: 1_000_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -384,6 +385,7 @@ mod test { gas: 130_000, ..Default::default() }, + ..Default::default() }, ); assert!( @@ -413,6 +415,7 @@ mod test { gas: 120_000, ..Default::default() }, + ..Default::default() }, ); if let module::CallResult::Failed { @@ -464,6 +467,7 @@ mod test { gas: 127_710, ..Default::default() }, + ..Default::default() }, ); if let module::CallResult::Failed { diff --git a/runtime-sdk/modules/evm/src/test.rs b/runtime-sdk/modules/evm/src/test.rs index d32a135b9d..6e3b794087 100644 --- a/runtime-sdk/modules/evm/src/test.rs +++ b/runtime-sdk/modules/evm/src/test.rs @@ -282,18 +282,21 @@ fn test_evm_calls() { #[test] fn test_c10l_evm_calls_enc() { + let _guard = crypto::signature::context::test_using_chain_context(); crypto::signature::context::set_chain_context(Default::default(), "test"); do_test_evm_calls::(false); } #[test] fn test_c10l_evm_calls_plain() { + let _guard = crypto::signature::context::test_using_chain_context(); crypto::signature::context::set_chain_context(Default::default(), "test"); do_test_evm_calls::(true /* force_plain */); } #[test] fn test_c10l_evm_balance_transfer() { + let _guard = crypto::signature::context::test_using_chain_context(); crypto::signature::context::set_chain_context(Default::default(), "test"); let mut mock = mock::Mock::default(); let ctx = mock.create_ctx(); @@ -791,6 +794,7 @@ fn test_evm_runtime() { #[test] fn test_c10l_evm_runtime() { + let _guard = crypto::signature::context::test_using_chain_context(); crypto::signature::context::set_chain_context(Default::default(), "test"); do_test_evm_runtime::(); } @@ -913,6 +917,7 @@ fn test_fee_refunds() { gas: 100_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -965,6 +970,7 @@ fn test_fee_refunds() { gas: 100_000, ..Default::default() }, + ..Default::default() }, ); if let module::CallResult::Failed { @@ -1046,6 +1052,7 @@ fn test_transfer_event() { gas: 100_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -1134,6 +1141,7 @@ fn test_return_value_limits() { gas: 100_000, ..Default::default() }, + ..Default::default() }, ); let result: Vec = cbor::from_value(dispatch_result.result.unwrap()).unwrap(); @@ -1155,6 +1163,7 @@ fn test_return_value_limits() { gas: 100_000, ..Default::default() }, + ..Default::default() }, ); if let module::CallResult::Failed { diff --git a/runtime-sdk/src/crypto/signature/context.rs b/runtime-sdk/src/crypto/signature/context.rs index fce3821aff..8a5d008737 100644 --- a/runtime-sdk/src/crypto/signature/context.rs +++ b/runtime-sdk/src/crypto/signature/context.rs @@ -69,20 +69,32 @@ pub fn set_chain_context(runtime_id: Namespace, consensus_chain_context: &str) { *guard = Some(ctx.into_bytes()); } +/// Test helper to serialize unit tests using the global chain context. The chain context is reset +/// when this method is called. +/// +/// # Example +/// +/// ```rust +/// # use oasis_runtime_sdk::crypto::signature::context::test_using_chain_context; +/// let _guard = test_using_chain_context(); +/// // ... rest of the test code follows ... +/// ``` +#[cfg(any(test, feature = "test"))] +pub fn test_using_chain_context() -> std::sync::MutexGuard<'static, ()> { + static TEST_USING_CHAIN_CONTEXT: Lazy> = Lazy::new(Default::default); + let guard = TEST_USING_CHAIN_CONTEXT.lock().unwrap(); + *CHAIN_CONTEXT.lock().unwrap() = None; + + guard +} + #[cfg(test)] mod test { use super::*; - static TEST_GUARD: Lazy> = Lazy::new(Default::default); - - fn reset_chain_context() { - *CHAIN_CONTEXT.lock().unwrap() = None; - } - #[test] fn test_chain_context() { - let _guard = TEST_GUARD.lock().unwrap(); - reset_chain_context(); + let _guard = test_using_chain_context(); set_chain_context( "8000000000000000000000000000000000000000000000000000000000000000".into(), "643fb06848be7e970af3b5b2d772eb8cfb30499c8162bc18ac03df2f5e22520e", @@ -94,8 +106,7 @@ mod test { #[test] fn test_chain_context_not_configured() { - let _guard = TEST_GUARD.lock().unwrap(); - reset_chain_context(); + let _guard = test_using_chain_context(); let result = std::panic::catch_unwind(|| get_chain_context_for(b"test")); assert!(result.is_err()); @@ -103,8 +114,7 @@ mod test { #[test] fn test_chain_context_already_configured() { - let _guard = TEST_GUARD.lock().unwrap(); - reset_chain_context(); + let _guard = test_using_chain_context(); set_chain_context( "8000000000000000000000000000000000000000000000000000000000000000".into(), "643fb06848be7e970af3b5b2d772eb8cfb30499c8162bc18ac03df2f5e22520e", diff --git a/runtime-sdk/src/dispatcher.rs b/runtime-sdk/src/dispatcher.rs index 78ce1f404a..8d8f3f17ee 100644 --- a/runtime-sdk/src/dispatcher.rs +++ b/runtime-sdk/src/dispatcher.rs @@ -250,6 +250,10 @@ impl Dispatcher { } } + if let Err(e) = R::Modules::before_authorized_call_dispatch(ctx, &call) { + return (e.into_call_result(), call_format_metadata); + } + let result = match R::Modules::dispatch_call(ctx, &call.method, call.body) { module::DispatchResult::Handled(result) => result, module::DispatchResult::Unhandled(_) => { diff --git a/runtime-sdk/src/module.rs b/runtime-sdk/src/module.rs index d29142f065..bff385ce14 100644 --- a/runtime-sdk/src/module.rs +++ b/runtime-sdk/src/module.rs @@ -396,6 +396,18 @@ pub trait TransactionHandler { Ok(()) } + /// Perform any action after authentication and decoding, within the transaction context. + /// + /// At this point, the call has been decoded according to the call format, + /// and method authorizers have run. + fn before_authorized_call_dispatch( + _ctx: &C, + _call: &Call, + ) -> Result<(), modules::core::Error> { + // Default implementation doesn't do anything. + Ok(()) + } + /// Perform any action after call, within the transaction context. /// /// If an error is returned the transaction call fails and updates are rolled back. @@ -461,6 +473,14 @@ impl TransactionHandler for Tuple { Ok(()) } + fn before_authorized_call_dispatch( + ctx: &C, + call: &Call, + ) -> Result<(), modules::core::Error> { + for_tuples!( #( Tuple::before_authorized_call_dispatch(ctx, call)?; )* ); + Ok(()) + } + fn after_handle_call( ctx: &C, mut result: CallResult, diff --git a/runtime-sdk/src/modules/access/mod.rs b/runtime-sdk/src/modules/access/mod.rs new file mode 100644 index 0000000000..22c662ba7c --- /dev/null +++ b/runtime-sdk/src/modules/access/mod.rs @@ -0,0 +1,71 @@ +//! Method access control module. +use once_cell::unsync::Lazy; +use thiserror::Error; + +use crate::{ + context::Context, + module::{self, Module as _}, + modules, sdk_derive, + state::CurrentState, + types::transaction, +}; + +#[cfg(test)] +mod test; +pub mod types; + +/// Unique module name. +const MODULE_NAME: &str = "access"; + +/// Errors emitted by the access module. +#[derive(Error, Debug, oasis_runtime_sdk_macros::Error)] +pub enum Error { + #[error("caller is not authorized to call method")] + #[sdk_error(code = 1)] + NotAuthorized, +} + +/// Module configuration. +#[allow(clippy::declare_interior_mutable_const)] +pub trait Config: 'static { + /// To filter methods by caller address, add them to this mapping. + /// + /// If the mapping is empty, no method is filtered. + const METHOD_AUTHORIZATIONS: Lazy = Lazy::new(types::Authorization::new); +} + +/// The method access control module. +pub struct Module { + _cfg: std::marker::PhantomData, +} + +#[sdk_derive(Module)] +impl Module { + const NAME: &'static str = MODULE_NAME; + const VERSION: u32 = 1; + type Error = Error; + type Event = (); + type Parameters = (); + type Genesis = (); +} + +impl module::TransactionHandler for Module { + fn before_authorized_call_dispatch( + _ctx: &C, + call: &transaction::Call, + ) -> Result<(), modules::core::Error> { + let tx_caller_address = CurrentState::with_env(|env| env.tx_caller_address()); + #[allow(clippy::borrow_interior_mutable_const)] + if Cfg::METHOD_AUTHORIZATIONS.is_authorized(&call.method, &tx_caller_address) { + Ok(()) + } else { + Err(modules::core::Error::InvalidArgument( + Error::NotAuthorized.into(), + )) + } + } +} + +impl module::BlockHandler for Module {} + +impl module::InvariantHandler for Module {} diff --git a/runtime-sdk/src/modules/access/test.rs b/runtime-sdk/src/modules/access/test.rs new file mode 100644 index 0000000000..09e6a43622 --- /dev/null +++ b/runtime-sdk/src/modules/access/test.rs @@ -0,0 +1,272 @@ +//! Tests for the method access control module. +use std::collections::BTreeMap; + +use once_cell::unsync::Lazy; + +use crate::{ + context::Context, + crypto::signature::context as signature_context, + handler, + module::{self, Module}, + modules::{self, core}, + sdk_derive, + testing::{keys, mock}, + types::{ + token::{BaseUnits, Denomination}, + transaction, + }, + Runtime, Version, +}; + +use super::{ + types::{Authorization, MethodAuthorization}, + Error as AccessError, +}; + +struct TestConfig; + +impl core::Config for TestConfig {} + +impl modules::access::Config for TestConfig { + const METHOD_AUTHORIZATIONS: Lazy = Lazy::new(|| { + Authorization::with_filtered_methods([( + "test.FilteredMethod", + MethodAuthorization::allow_from([keys::alice::address()]), + )]) + }); +} + +/// Test runtime. +struct TestRuntime; + +impl Runtime for TestRuntime { + const VERSION: Version = Version::new(0, 0, 0); + + type Core = modules::core::Module; + type Accounts = modules::accounts::Module; + + type Modules = ( + modules::core::Module, + modules::accounts::Module, + modules::access::Module, + TestModule, + ); + + fn genesis_state() -> ::Genesis { + ( + core::Genesis { + parameters: core::Parameters { + max_batch_gas: 10_000_000, + min_gas_price: BTreeMap::from([(Denomination::NATIVE, 0)]), + ..Default::default() + }, + }, + modules::accounts::Genesis { + balances: BTreeMap::from([ + ( + keys::alice::address(), + BTreeMap::from([(Denomination::NATIVE, 1_000_000)]), + ), + ( + keys::bob::address(), + BTreeMap::from([(Denomination::NATIVE, 1_000_000)]), + ), + ]), + total_supplies: BTreeMap::from([(Denomination::NATIVE, 2_000_000)]), + ..Default::default() + }, + (), // Access module has no genesis. + (), // Test module has no genesis. + ) + } +} + +/// A module with multiple no-op methods; intended for testing routing. +struct TestModule; + +#[sdk_derive(Module)] +impl TestModule { + const NAME: &'static str = "test"; + type Error = core::Error; + type Event = (); + type Parameters = (); + type Genesis = (); + + #[handler(call = "test.FilteredMethod")] + fn filtered_method(_ctx: &C, fail: bool) -> Result { + Ok(42) + } + + #[handler(call = "test.AllowedMethod")] + fn allowed_method(ctx: &C, _args: ()) -> Result { + Ok(42) + } +} + +impl module::BlockHandler for TestModule {} +impl module::TransactionHandler for TestModule {} +impl module::InvariantHandler for TestModule {} + +fn dispatch_test( + ctx: &C, + signer: &mut mock::Signer, + meth: &str, + encrypted: bool, + should_fail: bool, +) { + let dispatch_result = signer.call_opts( + ctx, + meth, + (), + mock::CallOptions { + fee: transaction::Fee { + amount: BaseUnits::new(1_500, Denomination::NATIVE), + gas: 1_500, + ..Default::default() + }, + encrypted, + ..Default::default() + }, + ); + if should_fail { + let err = core::Error::InvalidArgument(AccessError::NotAuthorized.into()); + assert!( + matches!( + dispatch_result.result, + module::CallResult::Failed { module: _, code: _, message: m } if m == format!("{}", err), + ), + "method call should be blocked", + ); + } else { + assert!( + dispatch_result.result.is_success(), + "method call should succeed but failed with: {:?}", + dispatch_result.result, + ); + let unmarshalled: u64 = + cbor::from_value(dispatch_result.result.unwrap()).expect("result should be decodable"); + assert_eq!(unmarshalled, 42); + } +} + +#[test] +fn test_access_module() { + let _guard = signature_context::test_using_chain_context(); + signature_context::set_chain_context(Default::default(), "test"); + let mut mock = mock::Mock::default(); + let ctx = mock.create_ctx_for_runtime::(true); + + let mut alice = mock::Signer::new(0, keys::alice::sigspec()); + let mut bob = mock::Signer::new(0, keys::bob::sigspec()); + + TestRuntime::migrate(&ctx); + + let filtered = "test.FilteredMethod"; + let allowed = "test.AllowedMethod"; + + // Test plain calls. + + dispatch_test(&ctx, &mut alice, filtered, false, false); + dispatch_test(&ctx, &mut alice, allowed, false, false); + + dispatch_test(&ctx, &mut bob, filtered, false, true); + dispatch_test(&ctx, &mut bob, allowed, false, false); + + // Test encrypted calls. + + dispatch_test(&ctx, &mut alice, filtered, true, false); + dispatch_test(&ctx, &mut alice, allowed, true, false); + + dispatch_test(&ctx, &mut bob, filtered, true, true); + dispatch_test(&ctx, &mut bob, allowed, true, false); +} + +#[test] +fn test_method_authorization() { + let alice = keys::alice::address(); + let bob = keys::bob::address(); + let charlie = keys::charlie::address(); + + // An empty authorizer shouldn't let anybody through. + let empty = MethodAuthorization::allow_from([]); + assert_eq!(empty.is_authorized(&alice), false); + assert_eq!(empty.is_authorized(&bob), false); + assert_eq!(empty.is_authorized(&charlie), false); + + // An authorizer with some addresses should only let those through. + let for_alice = MethodAuthorization::allow_from([alice]); + assert_eq!(for_alice.is_authorized(&alice), true); + assert_eq!(for_alice.is_authorized(&bob), false); + assert_eq!(for_alice.is_authorized(&charlie), false); + + let for_bob = MethodAuthorization::allow_from([bob]); + assert_eq!(for_bob.is_authorized(&alice), false); + assert_eq!(for_bob.is_authorized(&bob), true); + assert_eq!(for_bob.is_authorized(&charlie), false); +} + +#[test] +fn test_authorization() { + let alice = keys::alice::address(); + let bob = keys::bob::address(); + let charlie = keys::charlie::address(); + let dave = keys::dave::address(); + + let authorization = Authorization::with_filtered_methods([ + ("test.Nobody", MethodAuthorization::allow_from([])), + ("test.Alice", MethodAuthorization::allow_from([alice])), + ("test.Bob", MethodAuthorization::allow_from([bob])), + ("test.Both", MethodAuthorization::allow_from([alice, bob])), + ( + "test.AliceAndCharlie", + MethodAuthorization::allow_from([alice, charlie]), + ), + ]); + + // Alice should be able to access some filtered methods and all unfiltered ones. + assert_eq!(authorization.is_authorized("test.Nobody", &alice), false); + assert_eq!(authorization.is_authorized("test.Alice", &alice), true); + assert_eq!(authorization.is_authorized("test.Bob", &alice), false); + assert_eq!(authorization.is_authorized("test.Both", &alice), true); + assert_eq!( + authorization.is_authorized("test.AliceAndCharlie", &alice), + true + ); + assert_eq!(authorization.is_authorized("test.Everybody", &alice), true); + + // Bob should be able to access some filtered methods and all unfiltered ones. + assert_eq!(authorization.is_authorized("test.Nobody", &bob), false); + assert_eq!(authorization.is_authorized("test.Alice", &bob), false); + assert_eq!(authorization.is_authorized("test.Bob", &bob), true); + assert_eq!(authorization.is_authorized("test.Both", &bob), true); + assert_eq!( + authorization.is_authorized("test.AliceAndCharlie", &bob), + false + ); + assert_eq!(authorization.is_authorized("test.Everybody", &bob), true); + + // Charlie should be able to access some filtered methods and all unfiltered ones. + assert_eq!(authorization.is_authorized("test.Nobody", &charlie), false); + assert_eq!(authorization.is_authorized("test.Alice", &charlie), false); + assert_eq!(authorization.is_authorized("test.Bob", &charlie), false); + assert_eq!(authorization.is_authorized("test.Both", &charlie), false); + assert_eq!( + authorization.is_authorized("test.AliceAndCharlie", &charlie), + true + ); + assert_eq!( + authorization.is_authorized("test.Everybody", &charlie), + true + ); + + // Dave is left out of everything, so should only be able to access unfiltered methods. + assert_eq!(authorization.is_authorized("test.Nobody", &dave), false); + assert_eq!(authorization.is_authorized("test.Alice", &dave), false); + assert_eq!(authorization.is_authorized("test.Bob", &dave), false); + assert_eq!(authorization.is_authorized("test.Both", &dave), false); + assert_eq!( + authorization.is_authorized("test.AliceAndCharlie", &dave), + false + ); + assert_eq!(authorization.is_authorized("test.Everybody", &dave), true); +} diff --git a/runtime-sdk/src/modules/access/types.rs b/runtime-sdk/src/modules/access/types.rs new file mode 100644 index 0000000000..8794c66f78 --- /dev/null +++ b/runtime-sdk/src/modules/access/types.rs @@ -0,0 +1,72 @@ +//! Method access control module types. +use std::collections::{BTreeMap, BTreeSet}; + +use crate::types::address::Address; + +/// A set of addresses that can be used to define access control for a particular method. +pub type Addresses = BTreeSet
; + +/// A specific kind of method authorization. +pub enum MethodAuthorization { + /// Only allow method calls from these addresses; + /// for other callers, the method call will fail. + AllowFrom(Addresses), +} + +impl MethodAuthorization { + /// Helper for creating a method authorization type that + /// only allows callers with the given addresses. + pub fn allow_from>(it: I) -> Self { + Self::AllowFrom(BTreeSet::from_iter(it)) + } + + pub(super) fn is_authorized(&self, address: &Address) -> bool { + match self { + Self::AllowFrom(addrs) => addrs.contains(address), + } + } +} + +/// A set of methods that are subject to access control. +pub type Methods = BTreeMap; + +/// A specific kind of access control. +pub enum Authorization { + /// Control a statically configured set of methods, each with a + /// statically configured set of addresses that are allowed to call it. + FilterOnly(Methods), +} + +impl Authorization { + /// Return a new access control configuration. + pub fn new() -> Self { + Self::FilterOnly(BTreeMap::new()) + } + + /// Helper for creating a static access control configuration. + pub fn with_filtered_methods(it: I) -> Self + where + S: AsRef, + I: IntoIterator, + { + Self::FilterOnly(BTreeMap::from_iter( + it.into_iter() + .map(|(name, authz)| (name.as_ref().to_string(), authz)), + )) + } + + pub(super) fn is_authorized(&self, method: &str, address: &Address) -> bool { + match self { + Self::FilterOnly(meths) => meths + .get(method) + .map(|authz| authz.is_authorized(address)) + .unwrap_or(true), + } + } +} + +impl Default for Authorization { + fn default() -> Self { + Self::FilterOnly(BTreeMap::default()) + } +} diff --git a/runtime-sdk/src/modules/accounts/test.rs b/runtime-sdk/src/modules/accounts/test.rs index f75b695251..9912d89975 100644 --- a/runtime-sdk/src/modules/accounts/test.rs +++ b/runtime-sdk/src/modules/accounts/test.rs @@ -1277,6 +1277,7 @@ fn test_fee_disbursement_2() { gas: 1_500, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -1336,6 +1337,7 @@ fn test_fee_refund() { gas: 100_000, ..Default::default() }, + ..Default::default() }, ); @@ -1391,6 +1393,7 @@ fn test_fee_refund_subcall() { gas: 100_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -1455,6 +1458,7 @@ fn test_fee_proxy() { }), ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -1507,6 +1511,7 @@ fn test_fee_proxy() { }), ..Default::default() }, + ..Default::default() }, ); assert!(!dispatch_result.result.is_success(), "call should fail"); diff --git a/runtime-sdk/src/modules/core/test.rs b/runtime-sdk/src/modules/core/test.rs index ed5d60feef..4ec87fa9ed 100644 --- a/runtime-sdk/src/modules/core/test.rs +++ b/runtime-sdk/src/modules/core/test.rs @@ -1391,6 +1391,7 @@ fn test_storage_gas() { gas: 10_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -1419,6 +1420,7 @@ fn test_storage_gas() { gas: 10_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -1439,6 +1441,7 @@ fn test_storage_gas() { gas: 10_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -1484,6 +1487,7 @@ fn test_message_gas() { gas: 10_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); @@ -1516,6 +1520,7 @@ fn test_message_gas() { gas: 10_000, ..Default::default() }, + ..Default::default() }, ); assert!(dispatch_result.result.is_success(), "call should succeed"); diff --git a/runtime-sdk/src/modules/mod.rs b/runtime-sdk/src/modules/mod.rs index 3624df4770..cb9d5346e4 100644 --- a/runtime-sdk/src/modules/mod.rs +++ b/runtime-sdk/src/modules/mod.rs @@ -1,5 +1,6 @@ //! Runtime modules included with the SDK. +pub mod access; pub mod accounts; pub mod consensus; pub mod consensus_accounts; diff --git a/runtime-sdk/src/testing/mock.rs b/runtime-sdk/src/testing/mock.rs index c35c16e83f..275cfeb8f1 100644 --- a/runtime-sdk/src/testing/mock.rs +++ b/runtime-sdk/src/testing/mock.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use oasis_core_runtime::{ - common::{namespace::Namespace, version::Version}, + common::{crypto::mrae::deoxysii, namespace::Namespace, version::Version}, consensus::{beacon, roothash, state::ConsensusState, Event}, protocol::HostInfo, storage::mkvs, @@ -10,6 +10,7 @@ use oasis_core_runtime::{ }; use crate::{ + callformat, context::{Context, RuntimeBatchContext}, dispatcher, error::RuntimeError, @@ -21,7 +22,7 @@ use crate::{ state::{self, CurrentState, TransactionResult}, storage::MKVSStore, testing::{configmap, keymanager::MockKeyManagerClient}, - types::{address::SignatureAddressSpec, transaction}, + types::{self, address::SignatureAddressSpec, transaction}, }; pub struct Config; @@ -179,6 +180,8 @@ pub fn transaction() -> transaction::Transaction { pub struct CallOptions { /// Transaction fee. pub fee: transaction::Fee, + /// Should the call be encrypted. + pub encrypted: bool, } impl Default for CallOptions { @@ -190,6 +193,7 @@ impl Default for CallOptions { consensus_messages: 0, ..Default::default() }, + encrypted: false, } } } @@ -232,14 +236,43 @@ impl Signer { C: Context, B: cbor::Encode, { + let mut call = transaction::Call { + format: transaction::CallFormat::Plain, + method: method.to_owned(), + body: cbor::to_value(body), + ..Default::default() + }; + if opts.encrypted { + let key_pair = deoxysii::generate_key_pair(); + let nonce = [0u8; deoxysii::NONCE_SIZE]; + let km = ctx.key_manager().unwrap(); + let epoch = ctx.epoch(); + let runtime_keypair = km + .get_or_create_ephemeral_keys(callformat::get_key_pair_id(epoch), epoch) + .unwrap(); + let runtime_pk = runtime_keypair.input_keypair.pk; + call = transaction::Call { + format: transaction::CallFormat::EncryptedX25519DeoxysII, + method: "".to_owned(), + body: cbor::to_value(types::callformat::CallEnvelopeX25519DeoxysII { + pk: key_pair.0.into(), + nonce, + epoch, + data: deoxysii::box_seal( + &nonce, + cbor::to_vec(call), + vec![], + &runtime_pk.0, + &key_pair.1, + ) + .unwrap(), + }), + ..Default::default() + } + }; let tx = transaction::Transaction { version: 1, - call: transaction::Call { - format: transaction::CallFormat::Plain, - method: method.to_owned(), - body: cbor::to_value(body), - ..Default::default() - }, + call, auth_info: transaction::AuthInfo { signer_info: vec![transaction::SignerInfo::new_sigspec( self.sigspec.clone(),