Skip to content

Latest commit

 

History

History
290 lines (207 loc) · 12.7 KB

aip-103.md

File metadata and controls

290 lines (207 loc) · 12.7 KB
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)>

AIP-103 - Permissioned Signer

Summary

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.

Out of scope

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.

High-level Overview

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 like let addr = signer::address_of(x) and use addr as the issuer for this operation.
    • e.g: framework code will use signer to determine which account issued the withdraw
  • 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 the signer 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.

Impact

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.

Alternative Solutions

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.

Specification and Implementation Details

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.

Lifecycle of a permissioned handle

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.

VM Representation of the new signer

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.

Managing permissions

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.

Revoking Permission

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.

Example: Fungible Asset Transfer Limit

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),
        })
    }

}

Reference Implementation

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));

Testing

Framework unit tests.

Risks and Drawbacks

The performance in the current code might not be good enough. We need to look into more performance storage data structure for storing permissions.

Security Considerations

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.

Future Potential

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.

Timeline

Suggested implementation timeline

Implementation done. Under review now.

Suggested developer platform support timeline

We will need https://github.com/aptos-foundation/AIPs/pull/448/files for SDK side support.

Suggested deployment timeline

Looking into merging into Aptos Framework towards end of October 24.

Open Questions (Optional)