misc: spec refactor :)

This commit is contained in:
hdbg
2026-02-13 17:20:20 +01:00
parent 056cd4af40
commit 8fb7a04102
2 changed files with 164 additions and 83 deletions

View File

@@ -1,110 +1,156 @@
# 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?)
Arbiter is a permissioned signing service for cryptocurrency wallets. It runs as a background service on the user's machine with an optional client application for vault management.
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
**Core principle:** The vault NEVER exposes key material. It only produces signatures when a request satisfies the configured policies.
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.
## 1. Peer Types
### 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.
Arbiter distinguishes two kinds of peers:
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.
- **User Agent** — A client application used by the owner to manage the vault (create wallets, approve SDK clients, configure policies).
- **SDK Client** — A consumer of signing capabilities, typically an automation tool. In the future, this could include a browser-based wallet.
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.
## 2. Authentication
### Keys and their relationships
The bound database stores everything in encrypted format. This includes the root key.
### 2.1 Challenge-Response
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.
All peers authenticate via public-key cryptography using a challenge-response protocol:
First of all, there are three kinds of keys and their relationships:
- User key a.k.a password
- Root key
- Wallet key
1. The peer sends its public key and requests a challenge.
2. The server looks up the key in its database. If found, it increments the nonce and returns a challenge (replay-attack protection).
3. The peer signs the challenge with its private key and sends the signature back.
4. The server verifies the signature:
- **Pass:** The connection is considered authenticated.
- **Fail:** The server closes the connection.
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
### 2.2 User Agent Bootstrap
On first run — when no User Agents are registered — the server generates a one-time bootstrap token. It is made available in two ways:
### 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".
- **Local setup:** Written to `~/.arbiter/bootstrap_token` for automatic discovery by a co-located User Agent.
- **Remote setup:** Printed to the server's console output.
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 first User Agent must present this token alongside the standard challenge-response to complete registration.
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
### 2.3 SDK Client Registration
There is no bootstrap mechanism for SDK clients. They must be explicitly approved by an already-registered User Agent.
### 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.
## 3. Server Identity
Another responsibility of Arbiter in this case is to make sure that the nonce never gets reused.
The server proves its identity using TLS with a self-signed certificate. The TLS private key is generated on first run and is long-term; no rotation mechanism exists yet due to the complexity of multi-peer coordination.
#### EVM Policies
Each grant is given on the basis of:
- Wallet
- Chain ID
Peers verify the server by its **public key fingerprint**:
Those are present in each grant.
- **User Agent (local):** Receives the fingerprint automatically through the bootstrap token.
- **User Agent (remote) / SDK Client:** Must receive the fingerprint out-of-band.
##### 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.
> A streamlined setup mechanism using a single connection string is planned but not yet implemented.
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
## 4. Key Management
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)
### 4.1 Key Hierarchy
There are three layers of keys:
| Key | Encrypts | Encrypted by |
|---|---|---|
| **User key** (password) | Root key | — (derived from user input) |
| **Root key** | Wallet keys | User key |
| **Wallet keys** | — (used for signing) | Root key |
This layered design enables:
- **Password rotation** without re-encrypting every wallet key (only the root key is re-encrypted).
- **Root key rotation** without requiring the user to change their password.
### 4.2 Encryption at Rest
The database stores everything in encrypted form using symmetric AEAD. The encryption scheme is versioned to support transparent migration — when the vault unseals, Arbiter automatically re-encrypts any entries that are behind the current scheme version. See [IMPLEMENTATION.md](IMPLEMENTATION.md) for the specific scheme and versioning mechanism.
---
## 5. Vault Lifecycle
### 5.1 Sealed State
On boot, the root key is encrypted and the server cannot perform any signing operations. This state is called **Sealed**.
### 5.2 Unseal Flow
To transition to the **Unsealed** state, a User Agent must provide the password:
1. The User Agent initiates an unseal request.
2. The server generates a one-time key pair and returns the public key.
3. The User Agent encrypts the user's password with this one-time public key and sends the ciphertext to the server.
4. The server decrypts and verifies the password:
- **Success:** The root key is decrypted and placed into a hardened memory cell. The server transitions to `Unsealed`. Any entries pending encryption scheme migration are re-encrypted.
- **Failure:** The server returns an error indicating the password is incorrect.
### 5.3 Memory Protection
Once unsealed, the root key must be protected in memory against:
- Memory dumps
- Page swaps to disk
- Hibernation files
See [IMPLEMENTATION.md](IMPLEMENTATION.md) for the current and planned memory protection approaches.
---
## 6. Permission Engine
### 6.1 Fundamental Rules
- SDK clients have **no access by default**.
- Access is granted **explicitly** by a User Agent.
- Grants are scoped to **specific wallets** and governed by **policies**.
Each blockchain requires its own policy system due to differences in static transaction analysis. Currently, only EVM is supported; Solana support is planned.
Arbiter is also responsible for ensuring that **transaction nonces are never reused**.
### 6.2 EVM Policies
Every EVM grant is scoped to a specific **wallet** and **chain ID**.
#### 6.2.1 Transaction Sub-Grants
Arbiter maintains an ever-expanding database of known contracts and their ABIs. Based on contract knowledge, transaction requests fall into three categories:
**1. Known contract (ABI available)**
The transaction can be decoded and presented with semantic meaning. For example: *"Client X wants to transfer Y USDT to address Z."*
Available restrictions:
- Volume limits (e.g., "no more than 10,000 tokens ever")
- Rate limits (e.g., "no more than 100 tokens per hour")
**2. Unknown contract (no ABI)**
The transaction cannot be decoded, so its effects are opaque — it could do anything, including draining all tokens. The user is warned, and if approved, access is granted to all interactions with the contract (matched by the `to` field).
Available restrictions:
- Transaction count limits (e.g., "no more than 100 transactions ever")
- Rate limits (e.g., "no more than 5 transactions per hour")
**3. Plain ether transfer (no calldata)**
These transactions have no `calldata` and therefore cannot interact with contracts. They can be subject to the same volume and rate restrictions as above.
#### 6.2.2 Global Limits
In addition to sub-grant-specific restrictions, the following limits can be applied across all grant types:
- **Gas limit** — Maximum gas per transaction.
- **Time-window restrictions** — e.g., signing allowed only 08:0020:00 on Mondays and Thursdays.