aip | title | author | discussions-to (*optional) | Status | last-call-end-date (*optional) | type | created | updated (*optional) | requires (*optional) |
---|---|---|---|---|---|---|---|---|---|
103 |
Permissioned Signer |
runtian-zhou wrwg igor-aptos davidiw lightmark |
<a url pointing to the official discussion thread> |
Draft |
<mm/dd/yyyy the last date to leave feedbacks and reviews> |
Framework |
09/26/2024 |
<mm/dd/yyyy> |
<AIP number(s)> |
Improve the security and usability of signer by allowing to create a permissioned mode signer where users can specify the permissions associated with those signers. Operations that violates those permission setups would be rejected in the transaction.
We are trying to define a permission standard to specify permissions in the current aptos framework. Despite such standard could be extended to any modules on blockchain, we will be limiting our focus on the aptos framework only so that we can iterate on this standard faster.
The current solution is intended to be used by Aptos Framework only. We are open to encoperate the changes to the Move on Aptos language extensions in the future if we want to extend the permission system to the general public.
Right now Move on Aptos has one single permission that is represented as signer.
- Smart contract uses
signer
to identify who authenticate this operation. Typical code would look likelet addr = signer::address_of(x)
and useaddr
as the issuer for this operation.- e.g: framework code will use
signer
to determine which account issued the withdraw
- e.g: framework code will use
- Smart contract needs
signer
to move items into/away from account.
This results in a huge security concern:
- Different smart contract uses the same
signer
to authenticate an operation. This means that a malicious smart contract can pass thesigner
to another module and perform the operation on user’s behalf without being acknowledged by the user. signer
could also be abused to inflate your account.
We need to design a solution such that:
- Move modules should be able to
- define permissions
- Framework/transaction can set those permissions
- Transactions that tries to execute operations that are not included in the list needs to be aborted and rejected by the Aptos VM.
- Work with our existing signer module so that users can still set constraints on what transactions invoking existing on chain move functions could invoke.
- Transaction that violates those permissions will need to be aborted, even for those existing on chain modules.
This would be a full compatible change to the framework. Existing on chain move modules wouldn't be impacted. We will add those constraints to the Aptos Framework around popular asset types, i.e: Coin/FungibleAsset/Object/NFT.
Another way to enforce permission is to leverage AIP-56 the runtime access control engine in Move. However, the runtime access control is more suitable to provide coarse grained permissions around modules but it's hard to represent permissions such as withdrawing X APT from a signer.
Currently, a signer is a single wrapper around an address to represent that the signature of the transaction passes the address authentication logic. We would like to extend the notion of existing signers in the Move VM so that we could provide APIs to manage contextual data embedded in such extended signer.
The lifecycle of the new signer will look like the following:
module aptos_framework::permissioned_signer {
struct PermissionedHandle {
master_addr: address,
permission_addr: address,
}
public fun create_permissioned_handle(
master: &signer,
): PermissionedHandle;
public fun signer_from_permissioned(p: &PermissionedHandle): signer;
public fun destroy_permissioned_handle(p: PermissionedHandle);
public fun is_permissioned_signer(s: &signer): bool;
}
We would introduce a new struct called PermissionedHandle
that is derived from the original signer. The handle is generated by create_permissioned_handle
API, which takes an original signer. In that handle, we will generate a fresh address using auid API to store the permission information that would be needed by the management API.
Once we have the handle, we can then invoke signer_from_permissioned
to obtain a permissioned signer value. The invariant would be that the address of the permissioned signer should be exactly the same as the existing master signer that is used to derive the handle. The handle is also not droppable, so that we can enforce the destruction logic by using destroy_permissioned_handle
API.
Note that this handle does not have store capability. So that the permission handle can only be used transiently within the lifetime of a transaction.
We implemented a storable version of a handle:
module aptos_framework::permissioned_signer {
struct StorablePermissionedHandle has store {
master_addr: address,
permission_addr: address,
expiration_time: u64,
}
public(friend) fun create_storable_permissioned_handle(
master: &signer,
expiration_time: u64
): StorablePermissionedHandle
public fun signer_from_storable_permissioned(p: &StorablePermissionedHandle): signer
public fun destroy_storable_permissioned_handle(p: StorablePermissionedHandle)
public fun revoke_permission_handle(s: &signer, permission_addr: address)
public fun revoke_all_handles(s: &signer)
public fun permission_address(p: &StorablePermissionedHandle)
}
To create a StorablePermissionedHandle
, the original signer will also need to provide an expiry time, and the signer_from_storable_permissioned
can only be invoked before that expiry time. The original signer also has the ability to revoke any of the issued StorablePermissionedHandle
by invoking revoke_permission_handle
or revoke_all_handles
. The creation of StorablePermissionedHandle
should be treated very cautiously because it can be viewed as account delegation as well and thus is only provided as a friend function that can only be invoked by other framework functionalities.
Since the signer is not storable, we should have the freedom to change the internal representation of a signer. The new signer will roughly look like the following in the VM:
enum Signer {
Master(AccountAddress),
Permissioned {
master_addr: address,
permission_addr: address,
}
}
Note that we did change the serialization rules for signer which would be a non-backward compatible change. This should be fine in general as signer should not be storable. The only problem is we exposes size_of_val
API in our framework. Since we changed the serialization, this would break the existing semantics for this API. To mitigate this, we will manually make sure that size_of_val
would calculate the size of signer to be a single account address instead of an address plus a variant. Another way to address this issue is to use a feature flag to make sure the latest binary will actually abort when trying to serialize a struct with signer.
The management API would look like the following:
module aptos_framework::permissioned_signer {
public fun authorize<PermKey: copy + drop + store>(
master: &signer,
permissioned: &signer,
capacity: u256,
perm: PermKey
);
public fun check_permission_exists<PermKey: copy + drop + store>(s: &signer, perm: PermKey)
public fun check_permission_capacity_above<PermKey: copy + drop + store>(
s: &signer, threshold: u256, perm: PermKey
)
public fun check_permission_consume<PermKey: copy + drop + store>(
s: &signer, weight: u256, perm: PermKey
)
}
The master signer can use authorize
to grant permissions to a permissioned signer. The smart contracts would use the check_
APIs to make sure if a permissioned signer is provided, it needs to have the right permission granted by the master signer to perform the corresponding operation.
In the current design, the permission storage is a CopyableAny -> u256
mapping stored in the global storage. Smart contract can define the key themselves to create proper domain separation and act as witness pattern.
The master signer should be able to revoke the permission granted to a permissioned signer at any time. The API would look like the following:
module aptos_framework::permissioned_signer {
public fun revoke_permission_handle(master: &signer, permission_address: address);
public fun revoke_all_handles(s: &signer);
}
The signer_from_permissioned
would abort on signers derived from a revoked permission handle.
module aptos_framework::fungible_asset {
struct WithdrawPermission has store {
token_addr: address,
}
public fun withdraw<T: key>(owner: &signer, store: Object<T>, amount: u64): FungibleAsset {
assert!(
permissioned_signer::check_permission(
owner,
amount as u256,
WithdrawPermission {
metadata_address: object::object_address(&borrow_store_resource(&store).metadata)
}
),
error::permission_denied(EWITHDRAW_PERMISSION_DENIED)
);
//… regular withdraw logic
}
public fun grant_permission(
master: &signer,
permissioned: &signer,
token_type: Object<Metadata>,
amount: u64
) {
permissioned_signer::authorize(
master,
permissioned,
amount as u256,
WithdrawPermission { metadata_address: object::object_address(&token_type)}
)
}
/// Removing permissions from permissioned signer.
public fun revoke_permission(permissioned: &signer, token_type: Object<Metadata>) {
permissioned_signer::revoke_permission(permissioned, WithdrawPermission {
metadata_address: object::object_address(&token_type),
})
}
}
We have a stacked PRs in aptos-labs/aptos-core#14605. In the stack, we have:
- Implemented the core library that generates and manages the permissioned signer.
- including Move and Rust components.
- Sweeped throught Aptos Framework codebase with following permissions implemented:
- how much fungible asset/coin can be withdrawed from each permissioned signer.
- transfer Object.
- manipulate NFT object/collection.
- operations on account.
- allowed to stake
- Privileged operation where master signer is required.
Here's an example change for voting.move, where a master signer is required:
diff --git a/aptos-move/framework/aptos-framework/sources/voting.move b/aptos-move/framework/aptos-framework/sources/voting.move
index a10e795b7369f..4dd70ad10e580 100644
--- a/aptos-move/framework/aptos-framework/sources/voting.move
+++ b/aptos-move/framework/aptos-framework/sources/voting.move
@@ -34,6 +34,7 @@ module aptos_framework::voting {
use aptos_framework::account;
use aptos_framework::event::{Self, EventHandle};
+ use aptos_framework::permissioned_signer;
use aptos_framework::timestamp;
use aptos_framework::transaction_context;
use aptos_std::from_bcs;
@@ -189,6 +190,7 @@ module aptos_framework::voting {
}
public fun register<ProposalType: store>(account: &signer) {
+ permissioned_signer::assert_master_signer(account);
let addr = signer::address_of(account);
assert!(!exists<VotingForum<ProposalType>>(addr), error::already_exists(EVOTING_FORUM_ALREADY_REGISTERED));
Framework unit tests.
The performance in the current code might not be good enough. We need to look into more performance storage data structure for storing permissions.
It is crucial for us to review whether the permission checks in Aptos Framework is exhaustive. Otherwise, a permissioned signer can perform operations that escaped the intention at creation process.
We might want to enable the permissioned signer framework to the general on chain move modules so that they could use this mechanism to develope their own authorization schema.
Implementation done. Under review now.
We will need https://github.com/aptos-foundation/AIPs/pull/448/files for SDK side support.
Looking into merging into Aptos Framework towards end of October 24.