Skip to content

Commit

Permalink
passkey: nonce check (#2039)
Browse files Browse the repository at this point in the history
# Goal
The goal of this PR is to verify that the nonce is correct for the
account_id and increase the nonce. And to restrict the transactions that
can be called inside a passkey

Closes #2030 


# Checklist
- [x] Chain spec updated
- [x] Tests added
- [x] Benchmarks added
- [x] Weights updated
  • Loading branch information
aramikm authored Jun 27, 2024
1 parent c9de0d9 commit acefb8e
Show file tree
Hide file tree
Showing 8 changed files with 539 additions and 75 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions pallets/passkey/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ log = { workspace = true, default-features = false }

# Frequency related dependencies
common-primitives = { default-features = false, path = "../../common/primitives" }
common-runtime = { path = "../../runtime/common", default-features = false }

[dev-dependencies]
common-runtime = { path = "../../runtime/common", default-features = false }
pallet-balances = { workspace = true }
sp-keystore = { workspace = true }
sp-keyring = { workspace = true }

[features]
default = ["std"]
Expand All @@ -36,15 +38,20 @@ std = [
"frame-support/std",
"frame-system/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"frame-benchmarking/std",
"common-primitives/std",
"common-runtime/std",
]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"frame-benchmarking/runtime-benchmarks"
"frame-benchmarking/runtime-benchmarks",
"common-primitives/runtime-benchmarks",
"common-runtime/runtime-benchmarks",
]
try-runtime = ["frame-support/try-runtime", "frame-system/try-runtime"]
61 changes: 55 additions & 6 deletions pallets/passkey/src/benchmarking.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,72 @@
#![allow(clippy::unwrap_used)]
use super::*;

use crate::types::*;
#[allow(unused)]
use crate::Pallet as Passkey;
use frame_benchmarking::{benchmarks, whitelisted_caller};
use frame_benchmarking::benchmarks;
use frame_support::assert_ok;
use sp_core::{crypto::KeyTypeId, Encode};
use sp_runtime::{traits::Zero, MultiSignature, RuntimeAppPublic};
use sp_std::prelude::*;

pub const TEST_KEY_TYPE_ID: KeyTypeId = KeyTypeId(*b"test");

mod app_sr25519 {
use super::TEST_KEY_TYPE_ID;
use sp_core::sr25519;
use sp_runtime::app_crypto::app_crypto;
app_crypto!(sr25519, TEST_KEY_TYPE_ID);
}

type SignerId = app_sr25519::Public;

fn generate_payload<T: Config>() -> PasskeyPayload<T> {
let test_account_1_pk = SignerId::generate_pair(None);
let passkey_public_key = [0u8; 33];
let wrapped_binary = wrap_binary_data(passkey_public_key.to_vec());
let signature: MultiSignature =
MultiSignature::Sr25519(test_account_1_pk.sign(&wrapped_binary).unwrap().into());

let inner_call: <T as Config>::RuntimeCall =
frame_system::Call::<T>::remark { remark: vec![] }.into();

let call: PasskeyCall<T> = PasskeyCall {
account_id: T::AccountId::decode(&mut &test_account_1_pk.encode()[..]).unwrap(),
account_nonce: T::Nonce::zero(),
account_ownership_proof: signature,
call: Box::new(inner_call),
};
let payload = PasskeyPayload {
passkey_public_key,
verifiable_passkey_signature: VerifiablePasskeySignature {
signature: PasskeySignature::default(),
client_data_json: PasskeyClientDataJson::default(),
authenticator_data: PasskeyAuthenticatorData::default(),
},
passkey_call: call,
};
payload
}

benchmarks! {
proxy {
let caller: T::AccountId = whitelisted_caller();
where_clause { where <T as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo> }

validate {
let payload = generate_payload::<T>();
}: {
//TODO: should calculate overhead after applying all validations
assert_ok!(Passkey::validate_unsigned(TransactionSource::InBlock, &Call::proxy { payload }));
}
verify {

pre_dispatch {
let payload = generate_payload::<T>();
}: {
assert_ok!(Passkey::pre_dispatch(&Call::proxy { payload }));
}

impl_benchmark_test_suite!(
Passkey,
crate::mock::new_test_ext(),
crate::mock::new_test_ext_keystore(),
crate::mock::Test
);
}
157 changes: 106 additions & 51 deletions pallets/passkey/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@
missing_docs
)]

use common_primitives::utils::wrap_binary_data;
use frame_support::{
dispatch::{GetDispatchInfo, PostDispatchInfo},
dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo},
pallet_prelude::*,
traits::IsSubType,
traits::Contains,
};
use frame_system::pallet_prelude::*;
use sp_runtime::traits::{Convert, Dispatchable, Verify};
use sp_std::vec::Vec;
use sp_runtime::{
traits::{Convert, Dispatchable, SignedExtension, Verify},
AccountId32, MultiSignature,
};
use sp_std::{vec, vec::Vec};

use common_runtime::extensions::check_nonce::CheckNonce;

#[cfg(test)]
mod mock;
Expand All @@ -45,8 +51,6 @@ pub use module::*;

#[frame_support::pallet]
pub mod module {
use common_primitives::utils::wrap_binary_data;
use sp_runtime::{AccountId32, MultiSignature};

use super::*;

Expand All @@ -63,14 +67,16 @@ pub mod module {
+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
+ GetDispatchInfo
+ From<frame_system::Call<Self>>
+ IsSubType<Call<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeCall>;

/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;

/// AccountId truncated to 32 bytes
type ConvertIntoAccountId32: Convert<Self::AccountId, AccountId32>;

/// Filters the inner calls for passkey which is set in runtime
type PasskeyCallFilter: Contains<<Self as Config>::RuntimeCall>;
}

#[pallet::error]
Expand Down Expand Up @@ -99,8 +105,7 @@ pub mod module {
#[pallet::call_index(0)]
#[pallet::weight({
let dispatch_info = payload.passkey_call.call.get_dispatch_info();
// TODO: calculate overhead after all validations
let overhead = T::WeightInfo::proxy();
let overhead = T::WeightInfo::pre_dispatch();
let total = overhead.saturating_add(dispatch_info.weight);
(total, dispatch_info.class)
})]
Expand All @@ -124,56 +129,106 @@ pub mod module {
}

#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
impl<T: Config> ValidateUnsigned for Pallet<T>
where
<T as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo>,
{
type Call = Call<T>;
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
Self::validate_signatures(call)?;
Ok(ValidTransaction::default())
let payload = Self::filter_valid_calls(&call)?;
Self::validate_signatures(&payload)?;

let nonce_check = PasskeyNonce::new(payload.passkey_call.clone());
nonce_check.validate()
}

fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::validate_unsigned(TransactionSource::InBlock, call)?;

let payload = Self::filter_valid_calls(&call)?;
let nonce_check = PasskeyNonce::new(payload.passkey_call.clone());
nonce_check.pre_dispatch()
}
}
}

impl<T: Config> Pallet<T> {
fn validate_signatures(call: &Call<T>) -> TransactionValidity {
match call {
Call::proxy { payload } => {
let signed_data = payload.passkey_public_key;
let signature = payload.passkey_call.account_ownership_proof.clone();
let signer = &payload.passkey_call.account_id;
match Self::check_account_signature(signer, &signed_data.into(), &signature) {
Ok(_) => Ok(ValidTransaction::default()),
Err(_e) => InvalidTransaction::BadSigner.into(),
}
},
_ => InvalidTransaction::Call.into(),
}
impl<T: Config> Pallet<T> {
fn filter_valid_calls(call: &Call<T>) -> Result<PasskeyPayload<T>, TransactionValidityError> {
match call {
Call::proxy { payload }
if T::PasskeyCallFilter::contains(&payload.clone().passkey_call.call) =>
return Ok(payload.clone()),
_ => return Err(InvalidTransaction::Call.into()),
}
}

/// Check the signature on passkey public key by the account id
/// Returns Ok(()) if the signature is valid
/// Returns Err(InvalidAccountSignature) if the signature is invalid
/// # Arguments
/// * `signer` - The account id of the signer
/// * `signed_data` - The signed data
/// * `signature` - The signature
/// # Return
/// * `Ok(())` if the signature is valid
/// * `Err(InvalidAccountSignature)` if the signature is invalid
fn check_account_signature(
signer: &T::AccountId,
signed_data: &Vec<u8>,
signature: &MultiSignature,
) -> DispatchResult {
let key: AccountId32 = T::ConvertIntoAccountId32::convert((*signer).clone());
let signed_payload: Vec<u8> = wrap_binary_data(signed_data.clone().into());

let verified = signature.verify(&signed_payload[..], &key);
if verified {
Ok(())
} else {
Err(Error::<T>::InvalidAccountSignature.into())
}
fn validate_signatures(payload: &PasskeyPayload<T>) -> TransactionValidity {
let signed_data = payload.passkey_public_key;
let signature = payload.passkey_call.account_ownership_proof.clone();
let signer = &payload.passkey_call.account_id;
match Self::check_account_signature(signer, &signed_data.into(), &signature) {
Ok(_) => Ok(ValidTransaction::default()),
Err(_e) => InvalidTransaction::BadSigner.into(),
}
}

/// Check the signature on passkey public key by the account id
/// Returns Ok(()) if the signature is valid
/// Returns Err(InvalidAccountSignature) if the signature is invalid
/// # Arguments
/// * `signer` - The account id of the signer
/// * `signed_data` - The signed data
/// * `signature` - The signature
/// # Return
/// * `Ok(())` if the signature is valid
/// * `Err(InvalidAccountSignature)` if the signature is invalid
fn check_account_signature(
signer: &T::AccountId,
signed_data: &Vec<u8>,
signature: &MultiSignature,
) -> DispatchResult {
let key = T::ConvertIntoAccountId32::convert((*signer).clone());
let signed_payload: Vec<u8> = wrap_binary_data(signed_data.clone().into());

let verified = signature.verify(&signed_payload[..], &key);
if verified {
Ok(())
} else {
Err(Error::<T>::InvalidAccountSignature.into())
}
}
}

impl<T: Config> Pallet<T> {}
/// Passkey specific nonce check
#[derive(Encode, Decode, Clone, TypeInfo)]
#[scale_info(skip_type_params(T))]
struct PasskeyNonce<T: Config>(pub PasskeyCall<T>);

impl<T: Config> PasskeyNonce<T>
where
<T as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo>,
{
pub fn new(passkey_call: PasskeyCall<T>) -> Self {
Self(passkey_call)
}

pub fn validate(&self) -> TransactionValidity {
let who = self.0.account_id.clone();
let nonce = self.0.account_nonce;
let some_call: &<T as Config>::RuntimeCall = &self.0.call;
let info = &some_call.get_dispatch_info();

let passkey_nonce = CheckNonce::<T>::from(nonce);
passkey_nonce.validate(&who, &some_call.clone().into(), info, 0usize)
}

pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
let who = self.0.account_id.clone();
let nonce = self.0.account_nonce;
let some_call: &<T as Config>::RuntimeCall = &self.0.call;
let info = &some_call.get_dispatch_info();

let passkey_nonce = CheckNonce::<T>::from(nonce);
passkey_nonce.pre_dispatch(&who, &some_call.clone().into(), info, 0usize)
}
}
31 changes: 29 additions & 2 deletions pallets/passkey/src/mock.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Mocks for the Passkey module.
use frame_support::{
construct_runtime,
traits::{ConstU32, ConstU64, Everything},
traits::{ConstU32, ConstU64, Contains, Everything},
};
use sp_core::H256;
use sp_runtime::{
Expand All @@ -20,7 +20,7 @@ construct_runtime!(
{
System: frame_system::{Pallet, Call, Storage, Config<T>, Event<T>},
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
Passkey: pallet_passkey::{Pallet, Storage, Call, Event<T>},
Passkey: pallet_passkey::{Pallet, Storage, Call, Event<T>, ValidateUnsigned},
}
);

Expand Down Expand Up @@ -55,6 +55,7 @@ impl pallet_passkey::Config for Test {
type WeightInfo = ();
type RuntimeCall = RuntimeCall;
type ConvertIntoAccountId32 = ConvertInto;
type PasskeyCallFilter = MockPasskeyCallFilter;
}

impl pallet_balances::Config for Test {
Expand All @@ -74,9 +75,35 @@ impl pallet_balances::Config for Test {
type RuntimeHoldReason = ();
}

pub struct MockPasskeyCallFilter;

impl Contains<RuntimeCall> for MockPasskeyCallFilter {
fn contains(call: &RuntimeCall) -> bool {
match call {
RuntimeCall::System(frame_system::Call::remark { .. }) |
RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { .. }) |
RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { .. }) |
RuntimeCall::Balances(pallet_balances::Call::transfer_all { .. }) => true,
_ => false,
}
}
}

pub fn new_test_ext() -> sp_io::TestExternalities {
let mut ext: sp_io::TestExternalities =
frame_system::GenesisConfig::<Test>::default().build_storage().unwrap().into();
ext.execute_with(|| System::set_block_number(1));
ext
}

#[cfg(feature = "runtime-benchmarks")]
pub fn new_test_ext_keystore() -> sp_io::TestExternalities {
use sp_keystore::{testing::MemoryKeystore, KeystoreExt, KeystorePtr};
use sp_std::sync::Arc;

let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.register_extension(KeystoreExt(Arc::new(MemoryKeystore::new()) as KeystorePtr));

ext
}
Loading

0 comments on commit acefb8e

Please sign in to comment.