Files
arbiter/ARCHITECTURE.md
2026-02-13 17:18:50 +01:00

6.5 KiB

Arbiter

Project dedicated to permissioned access to cryptocurrency wallets. It's designed to run as a background service on the user's machine with an optional application to manage the vault. This vault NEVER exposes key material, it only hands signatures if it deems it appropriate (defined by policies).

Specification

It makes distinction of two peer types:

  • User Agent. Client application for user to manage vault
  • SDK Client. Consumers of the keys, usually automation tools. In the future, possibly a browser-based wallet (would require rethinking usage of gRPC, because grpc-web doesn't support bidirectional streams yet. Maybe protojson over websocket?)

Peers are authenticated using public-key cryptography, specifically ed25519. The flow goes like this:

  • Peer requests a challenge
  • Server checks the database for whether such a key is registered. If so, it increments the nonce and generates a challenge (replay attack protection)
  • Peer signs the challenge with their key
  • Server verifies challenge:
    • If challenge passed, connection is considered authenticated
    • If not passed, server closes connection

For initial bootstrap, user clients are also supposed to present a bootstrap token. This token is generated by the server only if there are no user agents registered in the database. It places it in a special file at path ~/.arbiter/bootstrap_token and prints it to the console. The former is for automatic discovery by the user agent client in case of a local setup, and the latter is for remote setup.

There are no bootstrap mechanisms for SDK clients; instead, they are supposed to be approved by the user.

Server Identity

Server proves its identity using a TLS public key and certificate. The TLS certificate is self-signed with a private key generated by the server on first run. This private key is long-term, and currently no rotation mechanism is present due to the complexity of multi-peer coordination.

SDK Clients are supposed to receive the public key fingerprint using the ServerInfo protobuf struct. That's how they know that they are connecting to the right server.

The User Agent handles this automatically in case of a local setup using the bootstrap token, and requires ServerInfo too in case of remote setup.

A mechanism for easy setup using a single string is yet to be introduced.

Keys and their relationships

The bound database stores everything in encrypted format. This includes the root key.

All keys used for encryption use symmetric cryptography and an AEAD scheme. Currently, this project uses XChaCha20-Poly1305, but this may change in the future. For this reason each aead_encrypted entry in database has additional scheme field, which denotes version. If we increment the application-level encryption scheme version, Arbiter is supposed to migrate them when the vault unseals.

First of all, there are three kinds of keys and their relationships:

  • User key a.k.a password
  • Root key
  • Wallet key

User key encrypts root key, and root key encrypts wallet keys. This separation is made to:

  • Allow user key (password) rotation without re-encrypting the whole database
  • Allow root key rotation without asking the user to change their password

Vault seal

When the server boots, the root key is encrypted. Therefore, the server is unable to perform any operations with key material. This state is called "Sealed".

This requires the user to complete the unseal: they need to provide the User key for root key decryption. This requires implementing a hardened memory-cell to avoid leaking keys when:

  • Memory dump is taken
  • Pages are swapped
  • Computer goes into sleep mode and creates hibernation file We plan to make a custom implementation (mlock and VirtualProtect based), but currently we are using the crate memsafe.

The unseal flow goes as follows:

  • User Agent makes an unseal request
  • Server responds with a one-time pubkey
  • User Agent asks the user for a password, encrypts the password with the one-time pubkey, and sends the result to the server
  • Server verifies the key.
    • If ok, the hardened memory-cell is initialized and the server transitions into Unsealed status. Additionally, entries pending encryption scheme version bumps are re-encrypted
    • If not ok, the server sends an error indicating that the key doesn't match. Flow ends here

Permission engine

Fundamental rules:

  • SDK clients don't have any access by default
  • Access is granted by the User Agent explicitly
  • SDK clients are granted access only to specific wallets and under certain conditions (policies)

Each blockchain requires its own dedicated policy system (because of differences in static transaction analysis). Currently, this project focuses on EVM only, but Solana is planned as well.

Another responsibility of Arbiter in this case is to make sure that the nonce never gets reused.

EVM Policies

Each grant is given on the basis of:

  • Wallet
  • Chain ID

Those are present in each grant.

Transaction sub-grants

Overall, these can be separated into three categories depending on what clients want to do and whether we know the contract. Arbiter shall maintain an ever-expanding database of contracts and their ABIs.

So there are three cases:

  • We know the contract the client wants to interact with and have its ABI. This means that we can provide semantic meaning to the transaction, e.g. if the client wants to make a USDT transfer, then we can notify the user that "client X wants to make a transfer of Y USDT tokens to address Z". Semantic meaning varies depending on the function and contract called.

    • Additionally, these kinds of transactions can be subject to additional rules. In case of tokens, the user can set temporal and volume-based limitations: "No more than 10k ever", "No more than 100 tokens per hour"
  • We don't have this contract in the database and therefore are unable to understand what it does. It can do anything, including transferring all tokens to another address. Therefore, we warn the user and grant access to all interactions with this contract (these transactions are matched by the to field)

    • These transactions, however, can also be subject to temporal & volume limits. For instance, "no more than 5 transactions per hour", "no more than 100 transactions ever"
  • Plain ether transfers. These transactions don't have calldata, hence can never interact with contracts. They can be subject to additional restrictions similar to those defined above

Additional limits not specific to grant sub-type should be available as well. Examples:

  • gas limit
  • time interval limits (available only 08-20 on Mondays and Thursdays)