Pallet Pass

Allows dispatching calls on behalf of a keyless account using an authenticator that resolves alternative signing methods.

Overview

Historically, having an account on the Web3 Ecosystem and using it to send transactions over a network has been as easy as having a private key which can sign those. Now, this ease comes with some tradeoffs. One of the most important is securely storing that key, to support signature operations.

Nonetheless, transactions are endorsed by accounts, not keys. A key is merely one means to get an Account. This pallet provides mechanisms for an Account to exist without necesarily hold a private key as traditionally known. This is done by introducing the concept of an Authenticator, very well known in the Web2 Ecosystem.

Terminology

  • An Authenticator is a handler that allows processing Challenges to either gather access to the Account, or to register a new Device against the Account.
  • A Challenge is the process where an Authenticator gathers a cryptographically verifiable input from a Device, and decides whether to provide access to an Account.
  • The Gas is an unique handler that enables an Account to dispatch calls without it paying fees.
  • A Recovery is a handler that allows registering a new Device when an User loses access to all their Devices.
  • A Registrar is a handler to create a new Account. The rules for registering a new Account are defined by an Origin.
  • A Session is the result of being granted to use an Account via using a Device on an Authenticator. Can be either:
    • A short-lived session, where the access is granted a single time via a dispatchable that processes the authentication and dispatches the call, or
    • A long-lived session, where an ephemeral key that is registered against the pallet and can function as a valid authentication method to gather access to an Account meanwhile the duration is not exceeded. The duration will be at most MaxDuration.
  • An User is an entity that owns Devices to gather access to an Account. Users can recover an Account whenever a Recovery is enabled.

Additionally, there are some concepts bound to implementation mechanisms, like:

  • A handler is a structure that implements a trait to enable one of the functions that the pallet must provide.
  • An Account is an AccountId derived from a registering process (typically, an username), that can be used to dispatch calls on its behalf.

Goals

This pallet supports four main purposes:

  • Registering an Account, and providing access to it via an Authenticator.
  • Dispatching calls to either unsigned extrinsics that pass the challenge for a given Authenticator (we call this short-live Session), or signed extrinsics previously registered on behalf of the pallet to control the Account for a limited time (we'll call this extended-sessions, and these signing keys Devices).
  • Determining whether calling an extrinsic won't pay fees via the Gas handler.
  • Recovering access to an Account via a Recovery.

Interface

Dispatchable Functions

  • register/claim: Given the data requested by the Registrar, generates a unique hash for derivating an Account.
    • account_name: An unique name for identifying the Account. Generally, an username.
    • authenticator: An Authenticator registered on the pallet.
    • device_id: An unique identification for the first Device attached to the Account against which to validate the authentication information.
    • maybe_session: An optional (ChallengePayload, SessionKey)
  • authenticate: Opens a long-lived Session using a Device and an Authenticator. Once it's opened, registers a Session against the Account.
    • account_name: An unique name of the Account. Generally, an username.
    • authenticator: An Authenticator registered on the pallet.
    • device_id: An descriptor for the Device attached to the Account against which to validate the authentication information, that decodes to the type of device accepted by the Authenticator.
    • challenge_payload: An encoded BoundedVec<u8, T::MaxPayloadSize> with the information required by the authenticator to validate the device.
    • session_key: An AccountId to identify the Session.
  • add_device: Requires being signed by an AccountId registered as a valid Session for the Account. Receives the information of a new device.
    • authenticator: An Authenticator registered on the pallet.
    • device: An descriptor for the Device attached to the Account against which to validate the authentication information, that decodes to the type of device accepted by the Authenticator.
  • dispatch: Dispatches a call on behalf of an Account if the signer is a valid Session or the authentication details are valid. The fees, if any, will be paid by the Account, or by the signer in case there's not an Account tied to a signed origin.
    • call: A valid RuntimeCall that can be dispatched on the runtime.
    • maybe_authentication: An optional (AccountName, Authenticator, DeviceId, ) tuple, sent to authenticate a device on-the-fly, producing a short-lived session.
    • maybe_next_session_key: An optional AccountId for the next Session key (AccountId) that should be registered on behalf of the Account.

Usage

Registrar

pub trait Registrar<AccountId, AccountName> { // Required methods fn is_claimable(account_name: &AccountName, claimer: &AccountId) -> types::RegistrarResult; fn claim(account_name: &AccountName, claimer: &AccountId) -> types::RegistrarResult; fn claimer_pays_fees(account_name: &AccountName, claimer: &AccountId) -> bool; }

Authenticator

pub trait Authenticator<AccountId, AccountName, DeviceId> { type Error: Parameter; type Device: Parameter; type Challenge: Parameter; fn register_device( &self, account_name: AccountName, device: &Device, ) -> DeviceId; fn authenticate( &self, account_name: AccountName, device_id: &DeviceId, challenge: &Challenge, ) -> Result<(), Error>; }

Gas Handling

pub trait GasHandler<AccountId> { fn pays_fees(account_id: AccountId) -> bool; }
// ... #[pallet::feeless_if(|origin: &OriginFor<T>, | -> bool { let account_id = Self::authenticate(origin, ...); !T::GasHandler::pays_fees(account_id) })] fn dispatch( origin: OriginFor<T>, call: Box<RuntimeCallFor<T>>, ) -> DispatchResult { // ... } // ...

More info: #[pallet:feeless_if]

pub struct MembershipGasHandler<T>(PhantomData<T>); impl<T: pallet_communities::Config> GasHandler<AccountIdOf<T>> for MembershipGasHandler<T> { fn pays_fees(account_id: AccountIdOf<T>) -> bool { // bring the membership for this account, if any match T::Memberships::memberships_of(account_id) { Some(membership) => { // check the membership has a fe... } None => false } } }
impl pallet_pass::Config for Runtime { // ... type GasHandler = MembershipGasHandler<Self>; // ... }

Prerequisites

Assumptions