Compare commits
64 Commits
win-servic
...
enforcing-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
763058b014 | ||
|
|
1497884ce6 | ||
|
|
b3464cf8a6 | ||
|
|
46d1318b6f | ||
| 9c80d51d45 | |||
|
|
33456a644d | ||
|
|
5bc0c42cc7 | ||
|
|
f6b62ab884 | ||
|
|
2dd5a3f32f | ||
|
|
1aca9d4007 | ||
| 5ee1b49c43 | |||
|
|
00745bb381 | ||
|
|
b122aa464c | ||
|
|
9fab945a00 | ||
|
|
aeed664e9a | ||
|
|
4057c1fc12 | ||
|
|
f5eb51978d | ||
|
|
d997e0f843 | ||
|
|
7aca281a81 | ||
| 0daad1dd37 | |||
| 9ea474e1b2 | |||
|
|
c6f440fdad | ||
| e17c25a604 | |||
|
|
01b12515bd | ||
|
|
4a50daa7ea | ||
|
|
352ee3ee63 | ||
|
|
dd51d756da | ||
|
|
0bb6e596ac | ||
|
|
083ff66af2 | ||
|
|
881f16bb1a | ||
|
|
78895bca5b | ||
| 1495fbe754 | |||
| ab8cf877d7 | |||
|
|
146f7a419e | ||
|
|
0362044b83 | ||
| 72618c186f | |||
|
|
e47ccc3108 | ||
| 90d8ae3c6c | |||
| 4af172e49a | |||
|
|
bc45b9b9ce | ||
|
|
5bce9fd68e | ||
|
|
63a4875fdb | ||
|
|
d5ec303b9a | ||
|
|
82b5b85f52 | ||
|
|
e2d8b7841b | ||
|
|
8feda7990c | ||
|
|
16f0e67d02 | ||
|
|
b5507e7d0f | ||
|
|
0388fa2c8b | ||
|
|
cfe01ba1ad | ||
|
|
59c7091cba | ||
|
|
523bf783ac | ||
|
|
643f251419 | ||
|
|
bce6ecd409 | ||
|
|
f32728a277 | ||
|
|
32743741e1 | ||
|
|
54b2183be5 | ||
|
|
ca35b9fed7 | ||
|
|
27428f709a | ||
|
|
78006e90f2 | ||
|
|
29cc4d9e5b | ||
|
|
7f8b9cc63e | ||
|
|
a02ef68a70 | ||
|
|
6987e5f70f |
@@ -24,4 +24,4 @@ steps:
|
|||||||
- mise install rust
|
- mise install rust
|
||||||
- mise install protoc
|
- mise install protoc
|
||||||
- mise install cargo:cargo-nextest
|
- mise install cargo:cargo-nextest
|
||||||
- mise exec cargo:cargo-nextest -- cargo nextest run --no-fail-fast
|
- mise exec cargo:cargo-nextest -- cargo nextest run --no-fail-fast --all-features
|
||||||
205
ARCHITECTURE.md
205
ARCHITECTURE.md
@@ -11,6 +11,7 @@ Arbiter distinguishes two kinds of peers:
|
|||||||
|
|
||||||
- **User Agent** — A client application used by the owner to manage the vault (create wallets, approve SDK clients, configure policies).
|
- **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.
|
- **SDK Client** — A consumer of signing capabilities, typically an automation tool. In the future, this could include a browser-based wallet.
|
||||||
|
- **Recovery Operator** — A dormant recovery participant with narrowly scoped authority used only for custody recovery and operator replacement.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,7 +43,149 @@ There is no bootstrap mechanism for SDK clients. They must be explicitly approve
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Server Identity
|
## 3. Multi-Operator Governance
|
||||||
|
|
||||||
|
When more than one User Agent is registered, the vault is treated as having multiple operators. In that mode, sensitive actions are governed by voting rather than by a single operator decision.
|
||||||
|
|
||||||
|
### 3.1 Voting Rules
|
||||||
|
|
||||||
|
Voting is based on the total number of registered operators:
|
||||||
|
|
||||||
|
- **1 operator:** no vote is needed; the single operator decides directly.
|
||||||
|
- **2 operators:** full consensus is required; both operators must approve.
|
||||||
|
- **3 or more operators:** quorum is `floor(N / 2) + 1`.
|
||||||
|
|
||||||
|
For a decision to count, the operator's approval or rejection must be signed by that operator's associated key. Unsigned votes, or votes that fail signature verification, are ignored.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- **3 operators:** 2 approvals required
|
||||||
|
- **4 operators:** 3 approvals required
|
||||||
|
|
||||||
|
### 3.2 Actions Requiring a Vote
|
||||||
|
|
||||||
|
In multi-operator mode, a successful vote is required for:
|
||||||
|
|
||||||
|
- approving new SDK clients
|
||||||
|
- granting an SDK client visibility to a wallet
|
||||||
|
- approving a one-off transaction
|
||||||
|
- approving creation of a persistent grant
|
||||||
|
- approving operator replacement
|
||||||
|
- approving server updates
|
||||||
|
- updating Shamir secret-sharing parameters
|
||||||
|
|
||||||
|
### 3.3 Special Rule for Key Rotation
|
||||||
|
|
||||||
|
Key rotation always requires full quorum, regardless of the normal voting threshold.
|
||||||
|
|
||||||
|
This is stricter than ordinary governance actions because rotating the root key requires every operator to participate in coordinated share refresh/update steps. The root key itself is not redistributed directly, but each operator's share material must be changed consistently.
|
||||||
|
|
||||||
|
### 3.4 Root Key Custody
|
||||||
|
|
||||||
|
When the vault has multiple operators, the vault root key is protected using Shamir secret sharing.
|
||||||
|
|
||||||
|
The vault root key is encrypted in a way that requires reconstruction from user-held shares rather than from a single shared password.
|
||||||
|
|
||||||
|
For ordinary operators, the Shamir threshold matches the ordinary governance quorum. For example:
|
||||||
|
|
||||||
|
- **2 operators:** `2-of-2`
|
||||||
|
- **3 operators:** `2-of-3`
|
||||||
|
- **4 operators:** `3-of-4`
|
||||||
|
|
||||||
|
In practice, the Shamir share set also includes Recovery Operator shares. This means the effective Shamir parameters are computed over the combined share pool while keeping the same threshold. For example:
|
||||||
|
|
||||||
|
- **3 ordinary operators + 2 recovery shares:** `2-of-5`
|
||||||
|
|
||||||
|
This ensures that the normal custody threshold follows the ordinary operator quorum, while still allowing dormant recovery shares to exist for break-glass recovery flows.
|
||||||
|
|
||||||
|
### 3.5 Recovery Operators
|
||||||
|
|
||||||
|
Recovery Operators are a separate peer type from ordinary vault operators.
|
||||||
|
|
||||||
|
Their role is intentionally narrow. They can only:
|
||||||
|
|
||||||
|
- participate in unsealing the vault
|
||||||
|
- vote for operator replacement
|
||||||
|
|
||||||
|
Recovery Operators do not participate in routine governance such as approving SDK clients, granting wallet visibility, approving transactions, creating grants, approving server updates, or changing Shamir parameters.
|
||||||
|
|
||||||
|
### 3.6 Sleeping and Waking Recovery Operators
|
||||||
|
|
||||||
|
By default, Recovery Operators are **sleeping** and do not participate in any active flow.
|
||||||
|
|
||||||
|
Any ordinary operator may request that Recovery Operators **wake up**.
|
||||||
|
|
||||||
|
Any ordinary operator may also cancel a pending wake-up request.
|
||||||
|
|
||||||
|
This creates a dispute window before recovery powers become active. The default wake-up delay is **14 days**.
|
||||||
|
|
||||||
|
Recovery Operators are therefore part of the break-glass recovery path rather than the normal operating quorum.
|
||||||
|
|
||||||
|
The high-level recovery flow is:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor Op as Ordinary Operator
|
||||||
|
participant Server
|
||||||
|
actor Other as Other Operator
|
||||||
|
actor Rec as Recovery Operator
|
||||||
|
|
||||||
|
Op->>Server: Request recovery wake-up
|
||||||
|
Server-->>Op: Wake-up pending
|
||||||
|
Note over Server: Default dispute window: 14 days
|
||||||
|
|
||||||
|
alt Wake-up cancelled during dispute window
|
||||||
|
Other->>Server: Cancel wake-up
|
||||||
|
Server-->>Op: Recovery cancelled
|
||||||
|
Server-->>Rec: Stay sleeping
|
||||||
|
else No cancellation for 14 days
|
||||||
|
Server-->>Rec: Wake up
|
||||||
|
Rec->>Server: Join recovery flow
|
||||||
|
critical Recovery authority
|
||||||
|
Rec->>Server: Participate in unseal
|
||||||
|
Rec->>Server: Vote on operator replacement
|
||||||
|
end
|
||||||
|
Server-->>Op: Recovery mode active
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 Committee Formation
|
||||||
|
|
||||||
|
There are two ways to form a multi-operator committee:
|
||||||
|
|
||||||
|
- convert an existing single-operator vault by adding new operators
|
||||||
|
- bootstrap an unbootstrapped vault directly into multi-operator mode
|
||||||
|
|
||||||
|
In both cases, committee formation is a coordinated process. Arbiter does not allow multi-operator custody to emerge implicitly from unrelated registrations.
|
||||||
|
|
||||||
|
### 3.8 Bootstrapping an Unbootstrapped Vault into Multi-Operator Mode
|
||||||
|
|
||||||
|
When an unbootstrapped vault is initialized as a multi-operator vault, the setup proceeds as follows:
|
||||||
|
|
||||||
|
1. An operator connects to the unbootstrapped vault using a User Agent and the bootstrap token.
|
||||||
|
2. During bootstrap setup, that operator declares:
|
||||||
|
- the total number of ordinary operators
|
||||||
|
- the total number of Recovery Operators
|
||||||
|
3. The vault enters **multi-bootstrap mode**.
|
||||||
|
4. While in multi-bootstrap mode:
|
||||||
|
- every ordinary operator must connect with a User Agent using the bootstrap token
|
||||||
|
- every Recovery Operator must also connect using the bootstrap token
|
||||||
|
- each participant is registered individually
|
||||||
|
- each participant's share is created and protected with that participant's credentials
|
||||||
|
5. The vault is considered fully bootstrapped only after all declared operator and recovery-share registrations have completed successfully.
|
||||||
|
|
||||||
|
This means the operator and recovery set is fixed at bootstrap completion time, based on the counts declared when multi-bootstrap mode was entered.
|
||||||
|
|
||||||
|
### 3.9 Special Bootstrap Constraint for Two-Operator Vaults
|
||||||
|
|
||||||
|
If a vault is declared with exactly **2 ordinary operators**, Arbiter requires at least **1 Recovery Operator** to be configured during bootstrap.
|
||||||
|
|
||||||
|
This prevents the worst-case custody failure in which a `2-of-2` operator set becomes permanently unrecoverable after loss of a single operator.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Server Identity
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -55,9 +198,9 @@ Peers verify the server by its **public key fingerprint**:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Key Management
|
## 5. Key Management
|
||||||
|
|
||||||
### 4.1 Key Hierarchy
|
### 5.1 Key Hierarchy
|
||||||
|
|
||||||
There are three layers of keys:
|
There are three layers of keys:
|
||||||
|
|
||||||
@@ -72,19 +215,19 @@ This layered design enables:
|
|||||||
- **Password rotation** without re-encrypting every wallet key (only the root key is re-encrypted).
|
- **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.
|
- **Root key rotation** without requiring the user to change their password.
|
||||||
|
|
||||||
### 4.2 Encryption at Rest
|
### 5.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.
|
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
|
## 6. Vault Lifecycle
|
||||||
|
|
||||||
### 5.1 Sealed State
|
### 6.1 Sealed State
|
||||||
|
|
||||||
On boot, the root key is encrypted and the server cannot perform any signing operations. This state is called **Sealed**.
|
On boot, the root key is encrypted and the server cannot perform any signing operations. This state is called **Sealed**.
|
||||||
|
|
||||||
### 5.2 Unseal Flow
|
### 6.2 Unseal Flow
|
||||||
|
|
||||||
To transition to the **Unsealed** state, a User Agent must provide the password:
|
To transition to the **Unsealed** state, a User Agent must provide the password:
|
||||||
|
|
||||||
@@ -95,7 +238,7 @@ To transition to the **Unsealed** state, a User Agent must provide 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.
|
- **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.
|
- **Failure:** The server returns an error indicating the password is incorrect.
|
||||||
|
|
||||||
### 5.3 Memory Protection
|
### 6.3 Memory Protection
|
||||||
|
|
||||||
Once unsealed, the root key must be protected in memory against:
|
Once unsealed, the root key must be protected in memory against:
|
||||||
|
|
||||||
@@ -107,9 +250,9 @@ See [IMPLEMENTATION.md](IMPLEMENTATION.md) for the current and planned memory pr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Permission Engine
|
## 7. Permission Engine
|
||||||
|
|
||||||
### 6.1 Fundamental Rules
|
### 7.1 Fundamental Rules
|
||||||
|
|
||||||
- SDK clients have **no access by default**.
|
- SDK clients have **no access by default**.
|
||||||
- Access is granted **explicitly** by a User Agent.
|
- Access is granted **explicitly** by a User Agent.
|
||||||
@@ -119,11 +262,45 @@ Each blockchain requires its own policy system due to differences in static tran
|
|||||||
|
|
||||||
Arbiter is also responsible for ensuring that **transaction nonces are never reused**.
|
Arbiter is also responsible for ensuring that **transaction nonces are never reused**.
|
||||||
|
|
||||||
### 6.2 EVM Policies
|
### 7.2 EVM Policies
|
||||||
|
|
||||||
Every EVM grant is scoped to a specific **wallet** and **chain ID**.
|
Every EVM grant is scoped to a specific **wallet** and **chain ID**.
|
||||||
|
|
||||||
#### 6.2.1 Transaction Sub-Grants
|
#### 7.2.0 Transaction Signing Sequence
|
||||||
|
|
||||||
|
The high-level interaction order is:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor SDK as SDK Client
|
||||||
|
participant Server
|
||||||
|
participant UA as User Agent
|
||||||
|
|
||||||
|
SDK->>Server: SignTransactionRequest
|
||||||
|
Server->>Server: Resolve wallet and wallet visibility
|
||||||
|
alt Visibility approval required
|
||||||
|
Server->>UA: Ask for wallet visibility approval
|
||||||
|
UA-->>Server: Vote result
|
||||||
|
end
|
||||||
|
Server->>Server: Evaluate transaction
|
||||||
|
Server->>Server: Load grant and limits context
|
||||||
|
alt Grant approval required
|
||||||
|
Server->>UA: Ask for execution / grant approval
|
||||||
|
UA-->>Server: Vote result
|
||||||
|
opt Create persistent grant
|
||||||
|
Server->>Server: Create and store grant
|
||||||
|
end
|
||||||
|
Server->>Server: Retry evaluation
|
||||||
|
end
|
||||||
|
critical Final authorization path
|
||||||
|
Server->>Server: Check limits and record execution
|
||||||
|
Server-->>Server: Signature or evaluation error
|
||||||
|
end
|
||||||
|
Server-->>SDK: Signature or error
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.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:
|
Arbiter maintains an ever-expanding database of known contracts and their ABIs. Based on contract knowledge, transaction requests fall into three categories:
|
||||||
|
|
||||||
@@ -147,9 +324,9 @@ Available restrictions:
|
|||||||
|
|
||||||
These transactions have no `calldata` and therefore cannot interact with contracts. They can be subject to the same volume and rate restrictions as above.
|
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
|
#### 7.2.2 Global Limits
|
||||||
|
|
||||||
In addition to sub-grant-specific restrictions, the following limits can be applied across all grant types:
|
In addition to sub-grant-specific restrictions, the following limits can be applied across all grant types:
|
||||||
|
|
||||||
- **Gas limit** — Maximum gas per transaction.
|
- **Gas limit** — Maximum gas per transaction.
|
||||||
- **Time-window restrictions** — e.g., signing allowed only 08:00–20:00 on Mondays and Thursdays.
|
- **Time-window restrictions** — e.g., signing allowed only 08:00–20:00 on Mondays and Thursdays.
|
||||||
|
|||||||
@@ -67,7 +67,18 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i
|
|||||||
## Cryptography
|
## Cryptography
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
- **Signature scheme:** ed25519
|
- **Client protocol:** ed25519
|
||||||
|
|
||||||
|
### User-Agent Authentication
|
||||||
|
|
||||||
|
User-agent authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware.
|
||||||
|
|
||||||
|
- **Supported schemes:** RSA, Ed25519, ECDSA (secp256k1)
|
||||||
|
- **Why:** the user agent authenticates with keys backed by platform facilities, and those facilities differ by platform
|
||||||
|
- **Apple Silicon Secure Enclave / Secure Element:** ECDSA-only in practice
|
||||||
|
- **Windows Hello / TPM 2.0:** currently RSA-backed in our integration
|
||||||
|
|
||||||
|
This is why the user-agent auth protocol carries an explicit `KeyType`, while the SDK client protocol remains fixed to ed25519.
|
||||||
|
|
||||||
### Encryption at Rest
|
### Encryption at Rest
|
||||||
- **Scheme:** Symmetric AEAD — currently **XChaCha20-Poly1305**
|
- **Scheme:** Symmetric AEAD — currently **XChaCha20-Poly1305**
|
||||||
@@ -117,6 +128,52 @@ The central abstraction is the `Policy` trait. Each implementation handles one s
|
|||||||
4. **Evaluate** — `Policy::evaluate` checks the decoded meaning against the grant's policy-specific constraints and returns any violations.
|
4. **Evaluate** — `Policy::evaluate` checks the decoded meaning against the grant's policy-specific constraints and returns any violations.
|
||||||
5. **Record** — If `RunKind::Execution` and there are no violations, the engine writes to `evm_transaction_log` and calls `Policy::record_transaction` for any policy-specific logging (e.g., token transfer volume).
|
5. **Record** — If `RunKind::Execution` and there are no violations, the engine writes to `evm_transaction_log` and calls `Policy::record_transaction` for any policy-specific logging (e.g., token transfer volume).
|
||||||
|
|
||||||
|
The detailed branch structure is shown below:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[SDK Client sends sign transaction request] --> B[Server resolves wallet]
|
||||||
|
B --> C{Wallet exists?}
|
||||||
|
|
||||||
|
C -- No --> Z1[Return wallet not found error]
|
||||||
|
C -- Yes --> D[Check SDK client wallet visibility]
|
||||||
|
|
||||||
|
D --> E{Wallet visible to SDK client?}
|
||||||
|
E -- No --> F[Start wallet visibility voting flow]
|
||||||
|
F --> G{Vote approved?}
|
||||||
|
G -- No --> Z2[Return wallet access denied error]
|
||||||
|
G -- Yes --> H[Persist wallet visibility]
|
||||||
|
E -- Yes --> I[Classify transaction meaning]
|
||||||
|
H --> I
|
||||||
|
|
||||||
|
I --> J{Meaning supported?}
|
||||||
|
J -- No --> Z3[Return unsupported transaction error]
|
||||||
|
J -- Yes --> K[Find matching grant]
|
||||||
|
|
||||||
|
K --> L{Grant exists?}
|
||||||
|
L -- Yes --> M[Check grant limits]
|
||||||
|
L -- No --> N[Start execution or grant voting flow]
|
||||||
|
|
||||||
|
N --> O{User-agent decision}
|
||||||
|
O -- Reject --> Z4[Return no matching grant error]
|
||||||
|
O -- Allow once --> M
|
||||||
|
O -- Create grant --> P[Create grant with user-selected limits]
|
||||||
|
P --> Q[Persist grant]
|
||||||
|
Q --> M
|
||||||
|
|
||||||
|
M --> R{Limits exceeded?}
|
||||||
|
R -- Yes --> Z5[Return evaluation error]
|
||||||
|
R -- No --> S[Record transaction in logs]
|
||||||
|
S --> T[Produce signature]
|
||||||
|
T --> U[Return signature to SDK client]
|
||||||
|
|
||||||
|
note1[Limit checks include volume, count, and gas constraints.]
|
||||||
|
note2[Grant lookup depends on classified meaning, such as ether transfer or token transfer.]
|
||||||
|
|
||||||
|
K -. uses .-> note2
|
||||||
|
M -. checks .-> note1
|
||||||
|
```
|
||||||
|
|
||||||
### Policy Trait
|
### Policy Trait
|
||||||
|
|
||||||
| Method | Purpose |
|
| Method | Purpose |
|
||||||
@@ -148,7 +205,7 @@ The central abstraction is the `Policy` trait. Each implementation handles one s
|
|||||||
Every grant has two layers:
|
Every grant has two layers:
|
||||||
|
|
||||||
- **Shared (`evm_basic_grant`)** — wallet, chain, validity period, gas fee caps, transaction count rate limit. One row per grant regardless of type.
|
- **Shared (`evm_basic_grant`)** — wallet, chain, validity period, gas fee caps, transaction count rate limit. One row per grant regardless of type.
|
||||||
- **Specific** — policy-owned tables (`evm_ether_transfer_grant`, `evm_token_transfer_grant`, etc.) holding type-specific configuration.
|
- **Specific** — policy-owned tables (`evm_ether_transfer_grant`, `evm_token_transfer_grant`) holding type-specific configuration.
|
||||||
|
|
||||||
`find_all_grants` uses a `#[diesel::auto_type]` base join between the specific and shared tables, then batch-loads related rows (targets, volume limits) in two additional queries to avoid N+1.
|
`find_all_grants` uses a `#[diesel::auto_type]` base join between the specific and shared tables, then batch-loads related rows (targets, volume limits) in two additional queries to avoid N+1.
|
||||||
|
|
||||||
@@ -171,7 +228,6 @@ These are checked centrally in `check_shared_constraints` before policy evaluati
|
|||||||
- **Only EIP-1559 transactions are supported.** Legacy and EIP-2930 types are rejected outright.
|
- **Only EIP-1559 transactions are supported.** Legacy and EIP-2930 types are rejected outright.
|
||||||
- **No opaque-calldata (unknown contract) grant type.** The architecture describes a category for unrecognised contracts, but no policy implements it yet. Any transaction that is not a plain ETH transfer or a known ERC-20 transfer is unconditionally rejected.
|
- **No opaque-calldata (unknown contract) grant type.** The architecture describes a category for unrecognised contracts, but no policy implements it yet. Any transaction that is not a plain ETH transfer or a known ERC-20 transfer is unconditionally rejected.
|
||||||
- **Token registry is static.** Tokens are recognised only if they appear in the hard-coded `arbiter_tokens_registry` crate. There is no mechanism to register additional contracts at runtime.
|
- **Token registry is static.** Tokens are recognised only if they appear in the hard-coded `arbiter_tokens_registry` crate. There is no mechanism to register additional contracts at runtime.
|
||||||
- **Nonce management is not implemented.** The architecture lists nonce deduplication as a core responsibility, but no nonce tracking or enforcement exists yet.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -179,5 +235,5 @@ These are checked centrally in `check_shared_constraints` before policy evaluati
|
|||||||
|
|
||||||
The unsealed root key must be held in a hardened memory cell resistant to dumps, page swaps, and hibernation.
|
The unsealed root key must be held in a hardened memory cell resistant to dumps, page swaps, and hibernation.
|
||||||
|
|
||||||
- **Current:** Using the `memsafe` crate as an interim solution
|
- **Current:** A dedicated memory-protection abstraction is in place, with `memsafe` used behind that abstraction today
|
||||||
- **Planned:** Custom implementation based on `mlock` (Unix) and `VirtualProtect` (Windows)
|
- **Planned:** Additional backends can be introduced behind the same abstraction, including a custom implementation based on `mlock` (Unix) and `VirtualProtect` (Windows)
|
||||||
|
|||||||
73
mise.lock
73
mise.lock
@@ -8,10 +8,18 @@ backend = "aqua:ast-grep/ast-grep"
|
|||||||
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
||||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
[tools.ast-grep."platforms.linux-x64"]
|
[tools.ast-grep."platforms.linux-x64"]
|
||||||
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
||||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
[tools.ast-grep."platforms.macos-arm64"]
|
[tools.ast-grep."platforms.macos-arm64"]
|
||||||
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620"
|
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620"
|
||||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"
|
||||||
@@ -32,10 +40,6 @@ backend = "cargo:cargo-audit"
|
|||||||
version = "0.13.9"
|
version = "0.13.9"
|
||||||
backend = "cargo:cargo-edit"
|
backend = "cargo:cargo-edit"
|
||||||
|
|
||||||
[[tools."cargo:cargo-features"]]
|
|
||||||
version = "1.0.0"
|
|
||||||
backend = "cargo:cargo-features"
|
|
||||||
|
|
||||||
[[tools."cargo:cargo-features-manager"]]
|
[[tools."cargo:cargo-features-manager"]]
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
backend = "cargo:cargo-features-manager"
|
backend = "cargo:cargo-features-manager"
|
||||||
@@ -44,26 +48,22 @@ backend = "cargo:cargo-features-manager"
|
|||||||
version = "1.46.3"
|
version = "1.46.3"
|
||||||
backend = "cargo:cargo-insta"
|
backend = "cargo:cargo-insta"
|
||||||
|
|
||||||
|
[[tools."cargo:cargo-mutants"]]
|
||||||
|
version = "27.0.0"
|
||||||
|
backend = "cargo:cargo-mutants"
|
||||||
|
|
||||||
[[tools."cargo:cargo-nextest"]]
|
[[tools."cargo:cargo-nextest"]]
|
||||||
version = "0.9.126"
|
version = "0.9.126"
|
||||||
backend = "cargo:cargo-nextest"
|
backend = "cargo:cargo-nextest"
|
||||||
|
|
||||||
[[tools."cargo:cargo-shear"]]
|
[[tools."cargo:cargo-shear"]]
|
||||||
version = "1.9.1"
|
version = "1.11.2"
|
||||||
backend = "cargo:cargo-shear"
|
backend = "cargo:cargo-shear"
|
||||||
|
|
||||||
[[tools."cargo:cargo-vet"]]
|
[[tools."cargo:cargo-vet"]]
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
backend = "cargo:cargo-vet"
|
backend = "cargo:cargo-vet"
|
||||||
|
|
||||||
[[tools."cargo:diesel-cli"]]
|
|
||||||
version = "2.3.6"
|
|
||||||
backend = "cargo:diesel-cli"
|
|
||||||
|
|
||||||
[tools."cargo:diesel-cli".options]
|
|
||||||
default-features = "false"
|
|
||||||
features = "sqlite,sqlite-bundled"
|
|
||||||
|
|
||||||
[[tools."cargo:diesel_cli"]]
|
[[tools."cargo:diesel_cli"]]
|
||||||
version = "2.3.6"
|
version = "2.3.6"
|
||||||
backend = "cargo:diesel_cli"
|
backend = "cargo:diesel_cli"
|
||||||
@@ -72,10 +72,6 @@ backend = "cargo:diesel_cli"
|
|||||||
default-features = "false"
|
default-features = "false"
|
||||||
features = "sqlite,sqlite-bundled"
|
features = "sqlite,sqlite-bundled"
|
||||||
|
|
||||||
[[tools."cargo:rinf_cli"]]
|
|
||||||
version = "8.9.1"
|
|
||||||
backend = "cargo:rinf_cli"
|
|
||||||
|
|
||||||
[[tools.flutter]]
|
[[tools.flutter]]
|
||||||
version = "3.38.9-stable"
|
version = "3.38.9-stable"
|
||||||
backend = "asdf:flutter"
|
backend = "asdf:flutter"
|
||||||
@@ -88,10 +84,18 @@ backend = "aqua:protocolbuffers/protobuf/protoc"
|
|||||||
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||||
|
|
||||||
[tools.protoc."platforms.linux-x64"]
|
[tools.protoc."platforms.linux-x64"]
|
||||||
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||||
|
|
||||||
[tools.protoc."platforms.macos-arm64"]
|
[tools.protoc."platforms.macos-arm64"]
|
||||||
checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed"
|
checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed"
|
||||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"
|
||||||
@@ -109,24 +113,39 @@ version = "3.14.3"
|
|||||||
backend = "core:python"
|
backend = "core:python"
|
||||||
|
|
||||||
[tools.python."platforms.linux-arm64"]
|
[tools.python."platforms.linux-arm64"]
|
||||||
checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625"
|
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.python."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.linux-x64"]
|
[tools.python."platforms.linux-x64"]
|
||||||
checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0"
|
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
|
[tools.python."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.macos-arm64"]
|
[tools.python."platforms.macos-arm64"]
|
||||||
checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb"
|
checksum = "sha256:c43aecde4a663aebff99b9b83da0efec506479f1c3f98331442f33d2c43501f9"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.macos-x64"]
|
[tools.python."platforms.macos-x64"]
|
||||||
checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7"
|
checksum = "sha256:9ab41dbc2f100a2a45d1833b9c11165f51051c558b5213eda9a9731d5948a0c0"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[tools.python."platforms.windows-x64"]
|
[tools.python."platforms.windows-x64"]
|
||||||
checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0"
|
checksum = "sha256:bbe19034b35b0267176a7442575ae7dc6343480fd4d35598cb7700173d431e09"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||||
|
provenance = "github-attestations"
|
||||||
|
|
||||||
[[tools.rust]]
|
[[tools.rust]]
|
||||||
version = "1.93.0"
|
version = "1.93.0"
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ protoc = "29.6"
|
|||||||
python = "3.14.3"
|
python = "3.14.3"
|
||||||
ast-grep = "0.42.0"
|
ast-grep = "0.42.0"
|
||||||
"cargo:cargo-edit" = "0.13.9"
|
"cargo:cargo-edit" = "0.13.9"
|
||||||
|
"cargo:cargo-mutants" = "27.0.0"
|
||||||
|
|
||||||
[tasks.codegen]
|
[tasks.codegen]
|
||||||
sources = ['protobufs/*.proto']
|
sources = ['protobufs/*.proto', 'protobufs/**/*.proto']
|
||||||
outputs = ['useragent/lib/proto/*']
|
outputs = ['useragent/lib/proto/**']
|
||||||
run = '''
|
run = '''
|
||||||
dart pub global activate protoc_plugin && \
|
dart pub global activate protoc_plugin && \
|
||||||
protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ protobufs/*.proto
|
protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ $(find protobufs -name '*.proto' | sort)
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -2,63 +2,24 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package arbiter.client;
|
package arbiter.client;
|
||||||
|
|
||||||
import "evm.proto";
|
import "client/auth.proto";
|
||||||
import "google/protobuf/empty.proto";
|
import "client/evm.proto";
|
||||||
|
import "client/vault.proto";
|
||||||
message ClientInfo {
|
|
||||||
string name = 1;
|
|
||||||
optional string description = 2;
|
|
||||||
optional string version = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthChallengeRequest {
|
|
||||||
bytes pubkey = 1;
|
|
||||||
ClientInfo client_info = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthChallenge {
|
|
||||||
bytes pubkey = 1;
|
|
||||||
int32 nonce = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthChallengeSolution {
|
|
||||||
bytes signature = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AuthResult {
|
|
||||||
AUTH_RESULT_UNSPECIFIED = 0;
|
|
||||||
AUTH_RESULT_SUCCESS = 1;
|
|
||||||
AUTH_RESULT_INVALID_KEY = 2;
|
|
||||||
AUTH_RESULT_INVALID_SIGNATURE = 3;
|
|
||||||
AUTH_RESULT_APPROVAL_DENIED = 4;
|
|
||||||
AUTH_RESULT_NO_USER_AGENTS_ONLINE = 5;
|
|
||||||
AUTH_RESULT_INTERNAL = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum VaultState {
|
|
||||||
VAULT_STATE_UNSPECIFIED = 0;
|
|
||||||
VAULT_STATE_UNBOOTSTRAPPED = 1;
|
|
||||||
VAULT_STATE_SEALED = 2;
|
|
||||||
VAULT_STATE_UNSEALED = 3;
|
|
||||||
VAULT_STATE_ERROR = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ClientRequest {
|
message ClientRequest {
|
||||||
int32 request_id = 4;
|
int32 request_id = 4;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallengeRequest auth_challenge_request = 1;
|
auth.Request auth = 1;
|
||||||
AuthChallengeSolution auth_challenge_solution = 2;
|
vault.Request vault = 2;
|
||||||
google.protobuf.Empty query_vault_state = 3;
|
evm.Request evm = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClientResponse {
|
message ClientResponse {
|
||||||
optional int32 request_id = 7;
|
optional int32 request_id = 7;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallenge auth_challenge = 1;
|
auth.Response auth = 1;
|
||||||
AuthResult auth_result = 2;
|
vault.Response vault = 2;
|
||||||
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 3;
|
evm.Response evm = 3;
|
||||||
arbiter.evm.EvmAnalyzeTransactionResponse evm_analyze_transaction = 4;
|
|
||||||
VaultState vault_state = 6;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
protobufs/client/auth.proto
Normal file
43
protobufs/client/auth.proto
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.client.auth;
|
||||||
|
|
||||||
|
import "shared/client.proto";
|
||||||
|
|
||||||
|
message AuthChallengeRequest {
|
||||||
|
bytes pubkey = 1;
|
||||||
|
arbiter.shared.ClientInfo client_info = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthChallenge {
|
||||||
|
bytes pubkey = 1;
|
||||||
|
int32 nonce = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthChallengeSolution {
|
||||||
|
bytes signature = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthResult {
|
||||||
|
AUTH_RESULT_UNSPECIFIED = 0;
|
||||||
|
AUTH_RESULT_SUCCESS = 1;
|
||||||
|
AUTH_RESULT_INVALID_KEY = 2;
|
||||||
|
AUTH_RESULT_INVALID_SIGNATURE = 3;
|
||||||
|
AUTH_RESULT_APPROVAL_DENIED = 4;
|
||||||
|
AUTH_RESULT_NO_USER_AGENTS_ONLINE = 5;
|
||||||
|
AUTH_RESULT_INTERNAL = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
oneof payload {
|
||||||
|
AuthChallengeRequest challenge_request = 1;
|
||||||
|
AuthChallengeSolution challenge_solution = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
oneof payload {
|
||||||
|
AuthChallenge challenge = 1;
|
||||||
|
AuthResult result = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
protobufs/client/evm.proto
Normal file
19
protobufs/client/evm.proto
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.client.evm;
|
||||||
|
|
||||||
|
import "evm.proto";
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
oneof payload {
|
||||||
|
arbiter.evm.EvmSignTransactionRequest sign_transaction = 1;
|
||||||
|
arbiter.evm.EvmAnalyzeTransactionRequest analyze_transaction = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
oneof payload {
|
||||||
|
arbiter.evm.EvmSignTransactionResponse sign_transaction = 1;
|
||||||
|
arbiter.evm.EvmAnalyzeTransactionResponse analyze_transaction = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
protobufs/client/vault.proto
Normal file
18
protobufs/client/vault.proto
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.client.vault;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "shared/vault.proto";
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
oneof payload {
|
||||||
|
google.protobuf.Empty query_state = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
oneof payload {
|
||||||
|
arbiter.shared.VaultState state = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ package arbiter.evm;
|
|||||||
|
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "shared/evm.proto";
|
||||||
|
|
||||||
enum EvmError {
|
enum EvmError {
|
||||||
EVM_ERROR_UNSPECIFIED = 0;
|
EVM_ERROR_UNSPECIFIED = 0;
|
||||||
@@ -74,70 +75,6 @@ message SpecificGrant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message EtherTransferMeaning {
|
|
||||||
bytes to = 1; // 20-byte Ethereum address
|
|
||||||
bytes value = 2; // U256 as big-endian bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
message TokenInfo {
|
|
||||||
string symbol = 1;
|
|
||||||
bytes address = 2; // 20-byte Ethereum address
|
|
||||||
uint64 chain_id = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirror of token_transfers::Meaning
|
|
||||||
message TokenTransferMeaning {
|
|
||||||
TokenInfo token = 1;
|
|
||||||
bytes to = 2; // 20-byte Ethereum address
|
|
||||||
bytes value = 3; // U256 as big-endian bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirror of policies::SpecificMeaning
|
|
||||||
message SpecificMeaning {
|
|
||||||
oneof meaning {
|
|
||||||
EtherTransferMeaning ether_transfer = 1;
|
|
||||||
TokenTransferMeaning token_transfer = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Eval error types ---
|
|
||||||
message GasLimitExceededViolation {
|
|
||||||
optional bytes max_gas_fee_per_gas = 1; // U256 as big-endian bytes
|
|
||||||
optional bytes max_priority_fee_per_gas = 2; // U256 as big-endian bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
message EvalViolation {
|
|
||||||
oneof kind {
|
|
||||||
bytes invalid_target = 1; // 20-byte Ethereum address
|
|
||||||
GasLimitExceededViolation gas_limit_exceeded = 2;
|
|
||||||
google.protobuf.Empty rate_limit_exceeded = 3;
|
|
||||||
google.protobuf.Empty volumetric_limit_exceeded = 4;
|
|
||||||
google.protobuf.Empty invalid_time = 5;
|
|
||||||
google.protobuf.Empty invalid_transaction_type = 6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction was classified but no grant covers it
|
|
||||||
message NoMatchingGrantError {
|
|
||||||
SpecificMeaning meaning = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction was classified and a grant was found, but constraints were violated
|
|
||||||
message PolicyViolationsError {
|
|
||||||
SpecificMeaning meaning = 1;
|
|
||||||
repeated EvalViolation violations = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// top-level error returned when transaction evaluation fails
|
|
||||||
message TransactionEvalError {
|
|
||||||
oneof kind {
|
|
||||||
google.protobuf.Empty contract_creation_not_supported = 1;
|
|
||||||
google.protobuf.Empty unsupported_transaction_type = 2;
|
|
||||||
NoMatchingGrantError no_matching_grant = 3;
|
|
||||||
PolicyViolationsError policy_violations = 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- UserAgent grant management ---
|
// --- UserAgent grant management ---
|
||||||
message EvmGrantCreateRequest {
|
message EvmGrantCreateRequest {
|
||||||
SharedSettings shared = 1;
|
SharedSettings shared = 1;
|
||||||
@@ -197,7 +134,7 @@ message EvmSignTransactionRequest {
|
|||||||
message EvmSignTransactionResponse {
|
message EvmSignTransactionResponse {
|
||||||
oneof result {
|
oneof result {
|
||||||
bytes signature = 1; // 65-byte signature: r[32] || s[32] || v[1]
|
bytes signature = 1; // 65-byte signature: r[32] || s[32] || v[1]
|
||||||
TransactionEvalError eval_error = 2;
|
arbiter.shared.evm.TransactionEvalError eval_error = 2;
|
||||||
EvmError error = 3;
|
EvmError error = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,8 +146,8 @@ message EvmAnalyzeTransactionRequest {
|
|||||||
|
|
||||||
message EvmAnalyzeTransactionResponse {
|
message EvmAnalyzeTransactionResponse {
|
||||||
oneof result {
|
oneof result {
|
||||||
SpecificMeaning meaning = 1;
|
arbiter.shared.evm.SpecificMeaning meaning = 1;
|
||||||
TransactionEvalError eval_error = 2;
|
arbiter.shared.evm.TransactionEvalError eval_error = 2;
|
||||||
EvmError error = 3;
|
EvmError error = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
protobufs/shared/client.proto
Normal file
9
protobufs/shared/client.proto
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.shared;
|
||||||
|
|
||||||
|
message ClientInfo {
|
||||||
|
string name = 1;
|
||||||
|
optional string description = 2;
|
||||||
|
optional string version = 3;
|
||||||
|
}
|
||||||
74
protobufs/shared/evm.proto
Normal file
74
protobufs/shared/evm.proto
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.shared.evm;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
message EtherTransferMeaning {
|
||||||
|
bytes to = 1; // 20-byte Ethereum address
|
||||||
|
bytes value = 2; // U256 as big-endian bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
message TokenInfo {
|
||||||
|
string symbol = 1;
|
||||||
|
bytes address = 2; // 20-byte Ethereum address
|
||||||
|
uint64 chain_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror of token_transfers::Meaning
|
||||||
|
message TokenTransferMeaning {
|
||||||
|
TokenInfo token = 1;
|
||||||
|
bytes to = 2; // 20-byte Ethereum address
|
||||||
|
bytes value = 3; // U256 as big-endian bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror of policies::SpecificMeaning
|
||||||
|
message SpecificMeaning {
|
||||||
|
oneof meaning {
|
||||||
|
EtherTransferMeaning ether_transfer = 1;
|
||||||
|
TokenTransferMeaning token_transfer = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GasLimitExceededViolation {
|
||||||
|
optional bytes max_gas_fee_per_gas = 1; // U256 as big-endian bytes
|
||||||
|
optional bytes max_priority_fee_per_gas = 2; // U256 as big-endian bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
message EvalViolation {
|
||||||
|
message ChainIdMismatch {
|
||||||
|
uint64 expected = 1;
|
||||||
|
uint64 actual = 2;
|
||||||
|
}
|
||||||
|
oneof kind {
|
||||||
|
bytes invalid_target = 1; // 20-byte Ethereum address
|
||||||
|
GasLimitExceededViolation gas_limit_exceeded = 2;
|
||||||
|
google.protobuf.Empty rate_limit_exceeded = 3;
|
||||||
|
google.protobuf.Empty volumetric_limit_exceeded = 4;
|
||||||
|
google.protobuf.Empty invalid_time = 5;
|
||||||
|
google.protobuf.Empty invalid_transaction_type = 6;
|
||||||
|
|
||||||
|
ChainIdMismatch chain_id_mismatch = 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction was classified but no grant covers it
|
||||||
|
message NoMatchingGrantError {
|
||||||
|
SpecificMeaning meaning = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction was classified and a grant was found, but constraints were violated
|
||||||
|
message PolicyViolationsError {
|
||||||
|
SpecificMeaning meaning = 1;
|
||||||
|
repeated EvalViolation violations = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// top-level error returned when transaction evaluation fails
|
||||||
|
message TransactionEvalError {
|
||||||
|
oneof kind {
|
||||||
|
google.protobuf.Empty contract_creation_not_supported = 1;
|
||||||
|
google.protobuf.Empty unsupported_transaction_type = 2;
|
||||||
|
NoMatchingGrantError no_matching_grant = 3;
|
||||||
|
PolicyViolationsError policy_violations = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
protobufs/shared/vault.proto
Normal file
11
protobufs/shared/vault.proto
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.shared;
|
||||||
|
|
||||||
|
enum VaultState {
|
||||||
|
VAULT_STATE_UNSPECIFIED = 0;
|
||||||
|
VAULT_STATE_UNBOOTSTRAPPED = 1;
|
||||||
|
VAULT_STATE_SEALED = 2;
|
||||||
|
VAULT_STATE_UNSEALED = 3;
|
||||||
|
VAULT_STATE_ERROR = 4;
|
||||||
|
}
|
||||||
@@ -2,198 +2,27 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package arbiter.user_agent;
|
package arbiter.user_agent;
|
||||||
|
|
||||||
import "client.proto";
|
import "user_agent/auth.proto";
|
||||||
import "evm.proto";
|
import "user_agent/evm.proto";
|
||||||
import "google/protobuf/empty.proto";
|
import "user_agent/sdk_client.proto";
|
||||||
|
import "user_agent/vault/vault.proto";
|
||||||
enum KeyType {
|
|
||||||
KEY_TYPE_UNSPECIFIED = 0;
|
|
||||||
KEY_TYPE_ED25519 = 1;
|
|
||||||
KEY_TYPE_ECDSA_SECP256K1 = 2;
|
|
||||||
KEY_TYPE_RSA = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- SDK client management ---
|
|
||||||
|
|
||||||
enum SdkClientError {
|
|
||||||
SDK_CLIENT_ERROR_UNSPECIFIED = 0;
|
|
||||||
SDK_CLIENT_ERROR_ALREADY_EXISTS = 1;
|
|
||||||
SDK_CLIENT_ERROR_NOT_FOUND = 2;
|
|
||||||
SDK_CLIENT_ERROR_HAS_RELATED_DATA = 3; // hard-delete blocked by FK (client has grants or transaction logs)
|
|
||||||
SDK_CLIENT_ERROR_INTERNAL = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientRevokeRequest {
|
|
||||||
int32 client_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientEntry {
|
|
||||||
int32 id = 1;
|
|
||||||
bytes pubkey = 2;
|
|
||||||
arbiter.client.ClientInfo info = 3;
|
|
||||||
int32 created_at = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientList {
|
|
||||||
repeated SdkClientEntry clients = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientRevokeResponse {
|
|
||||||
oneof result {
|
|
||||||
google.protobuf.Empty ok = 1;
|
|
||||||
SdkClientError error = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientListResponse {
|
|
||||||
oneof result {
|
|
||||||
SdkClientList clients = 1;
|
|
||||||
SdkClientError error = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthChallengeRequest {
|
|
||||||
bytes pubkey = 1;
|
|
||||||
optional string bootstrap_token = 2;
|
|
||||||
KeyType key_type = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthChallenge {
|
|
||||||
int32 nonce = 2;
|
|
||||||
reserved 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthChallengeSolution {
|
|
||||||
bytes signature = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AuthResult {
|
|
||||||
AUTH_RESULT_UNSPECIFIED = 0;
|
|
||||||
AUTH_RESULT_SUCCESS = 1;
|
|
||||||
AUTH_RESULT_INVALID_KEY = 2;
|
|
||||||
AUTH_RESULT_INVALID_SIGNATURE = 3;
|
|
||||||
AUTH_RESULT_BOOTSTRAP_REQUIRED = 4;
|
|
||||||
AUTH_RESULT_TOKEN_INVALID = 5;
|
|
||||||
AUTH_RESULT_INTERNAL = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UnsealStart {
|
|
||||||
bytes client_pubkey = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UnsealStartResponse {
|
|
||||||
bytes server_pubkey = 1;
|
|
||||||
}
|
|
||||||
message UnsealEncryptedKey {
|
|
||||||
bytes nonce = 1;
|
|
||||||
bytes ciphertext = 2;
|
|
||||||
bytes associated_data = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message BootstrapEncryptedKey {
|
|
||||||
bytes nonce = 1;
|
|
||||||
bytes ciphertext = 2;
|
|
||||||
bytes associated_data = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum UnsealResult {
|
|
||||||
UNSEAL_RESULT_UNSPECIFIED = 0;
|
|
||||||
UNSEAL_RESULT_SUCCESS = 1;
|
|
||||||
UNSEAL_RESULT_INVALID_KEY = 2;
|
|
||||||
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum BootstrapResult {
|
|
||||||
BOOTSTRAP_RESULT_UNSPECIFIED = 0;
|
|
||||||
BOOTSTRAP_RESULT_SUCCESS = 1;
|
|
||||||
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2;
|
|
||||||
BOOTSTRAP_RESULT_INVALID_KEY = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum VaultState {
|
|
||||||
VAULT_STATE_UNSPECIFIED = 0;
|
|
||||||
VAULT_STATE_UNBOOTSTRAPPED = 1;
|
|
||||||
VAULT_STATE_SEALED = 2;
|
|
||||||
VAULT_STATE_UNSEALED = 3;
|
|
||||||
VAULT_STATE_ERROR = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientConnectionRequest {
|
|
||||||
bytes pubkey = 1;
|
|
||||||
arbiter.client.ClientInfo info = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientConnectionResponse {
|
|
||||||
bool approved = 1;
|
|
||||||
bytes pubkey = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientConnectionCancel {
|
|
||||||
bytes pubkey = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message WalletAccess {
|
|
||||||
int32 wallet_id = 1;
|
|
||||||
int32 sdk_client_id = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientWalletAccess {
|
|
||||||
int32 id = 1;
|
|
||||||
WalletAccess access = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientGrantWalletAccess {
|
|
||||||
repeated WalletAccess accesses = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SdkClientRevokeWalletAccess {
|
|
||||||
repeated int32 accesses = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ListWalletAccessResponse {
|
|
||||||
repeated SdkClientWalletAccess accesses = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UserAgentRequest {
|
message UserAgentRequest {
|
||||||
int32 id = 16;
|
int32 id = 16;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallengeRequest auth_challenge_request = 1;
|
auth.Request auth = 1;
|
||||||
AuthChallengeSolution auth_challenge_solution = 2;
|
vault.Request vault = 2;
|
||||||
UnsealStart unseal_start = 3;
|
evm.Request evm = 3;
|
||||||
UnsealEncryptedKey unseal_encrypted_key = 4;
|
sdk_client.Request sdk_client = 4;
|
||||||
google.protobuf.Empty query_vault_state = 5;
|
|
||||||
google.protobuf.Empty evm_wallet_create = 6;
|
|
||||||
google.protobuf.Empty evm_wallet_list = 7;
|
|
||||||
arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8;
|
|
||||||
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
|
|
||||||
arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
|
|
||||||
SdkClientConnectionResponse sdk_client_connection_response = 11;
|
|
||||||
SdkClientRevokeRequest sdk_client_revoke = 12;
|
|
||||||
google.protobuf.Empty sdk_client_list = 13;
|
|
||||||
BootstrapEncryptedKey bootstrap_encrypted_key = 14;
|
|
||||||
SdkClientGrantWalletAccess grant_wallet_access = 15;
|
|
||||||
SdkClientRevokeWalletAccess revoke_wallet_access = 17;
|
|
||||||
google.protobuf.Empty list_wallet_access = 18;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message UserAgentResponse {
|
message UserAgentResponse {
|
||||||
optional int32 id = 16;
|
optional int32 id = 16;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallenge auth_challenge = 1;
|
auth.Response auth = 1;
|
||||||
AuthResult auth_result = 2;
|
vault.Response vault = 2;
|
||||||
UnsealStartResponse unseal_start_response = 3;
|
evm.Response evm = 3;
|
||||||
UnsealResult unseal_result = 4;
|
sdk_client.Response sdk_client = 4;
|
||||||
VaultState vault_state = 5;
|
|
||||||
arbiter.evm.WalletCreateResponse evm_wallet_create = 6;
|
|
||||||
arbiter.evm.WalletListResponse evm_wallet_list = 7;
|
|
||||||
arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8;
|
|
||||||
arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9;
|
|
||||||
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
|
|
||||||
SdkClientConnectionRequest sdk_client_connection_request = 11;
|
|
||||||
SdkClientConnectionCancel sdk_client_connection_cancel = 12;
|
|
||||||
SdkClientRevokeResponse sdk_client_revoke_response = 13;
|
|
||||||
SdkClientListResponse sdk_client_list_response = 14;
|
|
||||||
BootstrapResult bootstrap_result = 15;
|
|
||||||
ListWalletAccessResponse list_wallet_access_response = 17;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
protobufs/user_agent/auth.proto
Normal file
48
protobufs/user_agent/auth.proto
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.user_agent.auth;
|
||||||
|
|
||||||
|
enum KeyType {
|
||||||
|
KEY_TYPE_UNSPECIFIED = 0;
|
||||||
|
KEY_TYPE_ED25519 = 1;
|
||||||
|
KEY_TYPE_ECDSA_SECP256K1 = 2;
|
||||||
|
KEY_TYPE_RSA = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthChallengeRequest {
|
||||||
|
bytes pubkey = 1;
|
||||||
|
optional string bootstrap_token = 2;
|
||||||
|
KeyType key_type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthChallenge {
|
||||||
|
int32 nonce = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthChallengeSolution {
|
||||||
|
bytes signature = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthResult {
|
||||||
|
AUTH_RESULT_UNSPECIFIED = 0;
|
||||||
|
AUTH_RESULT_SUCCESS = 1;
|
||||||
|
AUTH_RESULT_INVALID_KEY = 2;
|
||||||
|
AUTH_RESULT_INVALID_SIGNATURE = 3;
|
||||||
|
AUTH_RESULT_BOOTSTRAP_REQUIRED = 4;
|
||||||
|
AUTH_RESULT_TOKEN_INVALID = 5;
|
||||||
|
AUTH_RESULT_INTERNAL = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
oneof payload {
|
||||||
|
AuthChallengeRequest challenge_request = 1;
|
||||||
|
AuthChallengeSolution challenge_solution = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
oneof payload {
|
||||||
|
AuthChallenge challenge = 1;
|
||||||
|
AuthResult result = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
protobufs/user_agent/evm.proto
Normal file
33
protobufs/user_agent/evm.proto
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.user_agent.evm;
|
||||||
|
|
||||||
|
import "evm.proto";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
message SignTransactionRequest {
|
||||||
|
int32 client_id = 1;
|
||||||
|
arbiter.evm.EvmSignTransactionRequest request = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
oneof payload {
|
||||||
|
google.protobuf.Empty wallet_create = 1;
|
||||||
|
google.protobuf.Empty wallet_list = 2;
|
||||||
|
arbiter.evm.EvmGrantCreateRequest grant_create = 3;
|
||||||
|
arbiter.evm.EvmGrantDeleteRequest grant_delete = 4;
|
||||||
|
arbiter.evm.EvmGrantListRequest grant_list = 5;
|
||||||
|
SignTransactionRequest sign_transaction = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
oneof payload {
|
||||||
|
arbiter.evm.WalletCreateResponse wallet_create = 1;
|
||||||
|
arbiter.evm.WalletListResponse wallet_list = 2;
|
||||||
|
arbiter.evm.EvmGrantCreateResponse grant_create = 3;
|
||||||
|
arbiter.evm.EvmGrantDeleteResponse grant_delete = 4;
|
||||||
|
arbiter.evm.EvmGrantListResponse grant_list = 5;
|
||||||
|
arbiter.evm.EvmSignTransactionResponse sign_transaction = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
protobufs/user_agent/sdk_client.proto
Normal file
100
protobufs/user_agent/sdk_client.proto
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.user_agent.sdk_client;
|
||||||
|
|
||||||
|
import "shared/client.proto";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
enum Error {
|
||||||
|
ERROR_UNSPECIFIED = 0;
|
||||||
|
ERROR_ALREADY_EXISTS = 1;
|
||||||
|
ERROR_NOT_FOUND = 2;
|
||||||
|
ERROR_HAS_RELATED_DATA = 3; // hard-delete blocked by FK (client has grants or transaction logs)
|
||||||
|
ERROR_INTERNAL = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RevokeRequest {
|
||||||
|
int32 client_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Entry {
|
||||||
|
int32 id = 1;
|
||||||
|
bytes pubkey = 2;
|
||||||
|
arbiter.shared.ClientInfo info = 3;
|
||||||
|
int32 created_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message List {
|
||||||
|
repeated Entry clients = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RevokeResponse {
|
||||||
|
oneof result {
|
||||||
|
google.protobuf.Empty ok = 1;
|
||||||
|
Error error = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListResponse {
|
||||||
|
oneof result {
|
||||||
|
List clients = 1;
|
||||||
|
Error error = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ConnectionRequest {
|
||||||
|
bytes pubkey = 1;
|
||||||
|
arbiter.shared.ClientInfo info = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ConnectionResponse {
|
||||||
|
bool approved = 1;
|
||||||
|
bytes pubkey = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ConnectionCancel {
|
||||||
|
bytes pubkey = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WalletAccess {
|
||||||
|
int32 wallet_id = 1;
|
||||||
|
int32 sdk_client_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WalletAccessEntry {
|
||||||
|
int32 id = 1;
|
||||||
|
WalletAccess access = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GrantWalletAccess {
|
||||||
|
repeated WalletAccess accesses = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RevokeWalletAccess {
|
||||||
|
repeated int32 accesses = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListWalletAccessResponse {
|
||||||
|
repeated WalletAccessEntry accesses = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
oneof payload {
|
||||||
|
ConnectionResponse connection_response = 1;
|
||||||
|
RevokeRequest revoke = 2;
|
||||||
|
google.protobuf.Empty list = 3;
|
||||||
|
GrantWalletAccess grant_wallet_access = 4;
|
||||||
|
RevokeWalletAccess revoke_wallet_access = 5;
|
||||||
|
google.protobuf.Empty list_wallet_access = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
oneof payload {
|
||||||
|
ConnectionRequest connection_request = 1;
|
||||||
|
ConnectionCancel connection_cancel = 2;
|
||||||
|
RevokeResponse revoke = 3;
|
||||||
|
ListResponse list = 4;
|
||||||
|
ListWalletAccessResponse list_wallet_access = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
protobufs/user_agent/vault/bootstrap.proto
Normal file
24
protobufs/user_agent/vault/bootstrap.proto
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.user_agent.vault.bootstrap;
|
||||||
|
|
||||||
|
message BootstrapEncryptedKey {
|
||||||
|
bytes nonce = 1;
|
||||||
|
bytes ciphertext = 2;
|
||||||
|
bytes associated_data = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BootstrapResult {
|
||||||
|
BOOTSTRAP_RESULT_UNSPECIFIED = 0;
|
||||||
|
BOOTSTRAP_RESULT_SUCCESS = 1;
|
||||||
|
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2;
|
||||||
|
BOOTSTRAP_RESULT_INVALID_KEY = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
BootstrapEncryptedKey encrypted_key = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
BootstrapResult result = 1;
|
||||||
|
}
|
||||||
37
protobufs/user_agent/vault/unseal.proto
Normal file
37
protobufs/user_agent/vault/unseal.proto
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.user_agent.vault.unseal;
|
||||||
|
|
||||||
|
message UnsealStart {
|
||||||
|
bytes client_pubkey = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnsealStartResponse {
|
||||||
|
bytes server_pubkey = 1;
|
||||||
|
}
|
||||||
|
message UnsealEncryptedKey {
|
||||||
|
bytes nonce = 1;
|
||||||
|
bytes ciphertext = 2;
|
||||||
|
bytes associated_data = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UnsealResult {
|
||||||
|
UNSEAL_RESULT_UNSPECIFIED = 0;
|
||||||
|
UNSEAL_RESULT_SUCCESS = 1;
|
||||||
|
UNSEAL_RESULT_INVALID_KEY = 2;
|
||||||
|
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
oneof payload {
|
||||||
|
UnsealStart start = 1;
|
||||||
|
UnsealEncryptedKey encrypted_key = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
oneof payload {
|
||||||
|
UnsealStartResponse start = 1;
|
||||||
|
UnsealResult result = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
protobufs/user_agent/vault/vault.proto
Normal file
24
protobufs/user_agent/vault/vault.proto
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package arbiter.user_agent.vault;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "shared/vault.proto";
|
||||||
|
import "user_agent/vault/bootstrap.proto";
|
||||||
|
import "user_agent/vault/unseal.proto";
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
oneof payload {
|
||||||
|
google.protobuf.Empty query_state = 1;
|
||||||
|
unseal.Request unseal = 2;
|
||||||
|
bootstrap.Request bootstrap = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
oneof payload {
|
||||||
|
arbiter.shared.VaultState state = 1;
|
||||||
|
unseal.Response unseal = 2;
|
||||||
|
bootstrap.Response bootstrap = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
server/.cargo/mutants.toml
Normal file
1
server/.cargo/mutants.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test_tool = "nextest"
|
||||||
2
server/.gitignore
vendored
Normal file
2
server/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mutants.out/
|
||||||
|
mutants.out.old/
|
||||||
33
server/Cargo.lock
generated
33
server/Cargo.lock
generated
@@ -724,6 +724,7 @@ name = "arbiter-server"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy",
|
"alloy",
|
||||||
|
"anyhow",
|
||||||
"arbiter-proto",
|
"arbiter-proto",
|
||||||
"arbiter-tokens-registry",
|
"arbiter-tokens-registry",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -737,23 +738,29 @@ dependencies = [
|
|||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"fatality",
|
"fatality",
|
||||||
"futures",
|
"futures",
|
||||||
|
"hmac",
|
||||||
"insta",
|
"insta",
|
||||||
"k256",
|
"k256",
|
||||||
"kameo",
|
"kameo",
|
||||||
"memsafe",
|
"memsafe",
|
||||||
"miette",
|
"mutants",
|
||||||
"pem",
|
"pem",
|
||||||
|
"proptest",
|
||||||
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"restructed",
|
"restructed",
|
||||||
"rsa",
|
"rsa",
|
||||||
|
"rstest",
|
||||||
"rustls",
|
"rustls",
|
||||||
"secrecy",
|
"secrecy",
|
||||||
|
"serde_with",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"smlang",
|
"smlang",
|
||||||
"spki",
|
"spki",
|
||||||
"strum 0.28.0",
|
"strum 0.28.0",
|
||||||
|
"subtle",
|
||||||
"test-log",
|
"test-log",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1954,6 +1961,7 @@ version = "3.0.0-rc.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890"
|
checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"serde",
|
||||||
"signature 3.0.0-rc.10",
|
"signature 3.0.0-rc.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1966,6 +1974,7 @@ dependencies = [
|
|||||||
"curve25519-dalek 5.0.0-pre.6",
|
"curve25519-dalek 5.0.0-pre.6",
|
||||||
"ed25519",
|
"ed25519",
|
||||||
"rand_core 0.10.0",
|
"rand_core 0.10.0",
|
||||||
|
"serde",
|
||||||
"sha2 0.11.0-rc.5",
|
"sha2 0.11.0-rc.5",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -2051,7 +2060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3170,6 +3179,12 @@ version = "0.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mutants"
|
||||||
|
version = "0.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "add0ac067452ff1aca8c5002111bd6b1c895baee6e45fcbc44e0193aea17be56"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@@ -3186,7 +3201,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3211,6 +3226,7 @@ dependencies = [
|
|||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -3633,9 +3649,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proptest"
|
name = "proptest"
|
||||||
version = "1.10.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532"
|
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set",
|
"bit-set",
|
||||||
"bit-vec",
|
"bit-vec",
|
||||||
@@ -4150,6 +4166,7 @@ dependencies = [
|
|||||||
"pkcs1",
|
"pkcs1",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
|
"serde",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"signature 2.2.0",
|
"signature 2.2.0",
|
||||||
"spki",
|
"spki",
|
||||||
@@ -4285,7 +4302,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4701,7 +4718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4895,7 +4912,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ chrono = { version = "0.4.44", features = ["serde"] }
|
|||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
||||||
smlang = "0.8.0"
|
smlang = "0.8.0"
|
||||||
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
@@ -43,3 +42,6 @@ k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
|
|||||||
rsa = { version = "0.9", features = ["sha2"] }
|
rsa = { version = "0.9", features = ["sha2"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
spki = "0.7"
|
spki = "0.7"
|
||||||
|
prost = "0.14.3"
|
||||||
|
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
||||||
|
mutants = "0.0.4"
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ disallowed-methods = [
|
|||||||
{ path = "rsa::RsaPrivateKey::decrypt_blinded", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." },
|
{ path = "rsa::RsaPrivateKey::decrypt_blinded", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." },
|
||||||
{ path = "rsa::traits::Decryptor::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
|
{ path = "rsa::traits::Decryptor::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
|
||||||
{ path = "rsa::traits::RandomizedDecryptor::decrypt_with_rng", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt_with_rng() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
|
{ path = "rsa::traits::RandomizedDecryptor::decrypt_with_rng", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt_with_rng() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
|
||||||
|
|
||||||
|
{ path = "arbiter_server::crypto::integrity::v1::lookup_verified_allow_unavailable", reason = "This function allows integrity checks to be bypassed when vault key material is unavailable, which can lead to silent security failures if used incorrectly. It should only be used in specific contexts where this behavior is acceptable, and its use should be carefully audited." },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata, format_challenge,
|
ClientMetadata, format_challenge,
|
||||||
proto::client::{
|
proto::{
|
||||||
AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientInfo as ProtoClientInfo,
|
client::{
|
||||||
ClientRequest, client_request::Payload as ClientRequestPayload,
|
ClientRequest,
|
||||||
client_response::Payload as ClientResponsePayload,
|
auth::{
|
||||||
|
self as proto_auth, AuthChallenge, AuthChallengeRequest, AuthChallengeSolution,
|
||||||
|
AuthResult, request::Payload as AuthRequestPayload,
|
||||||
|
response::Payload as AuthResponsePayload,
|
||||||
|
},
|
||||||
|
client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
},
|
||||||
|
shared::ClientInfo as ProtoClientInfo,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ed25519_dalek::Signer as _;
|
use ed25519_dalek::Signer as _;
|
||||||
@@ -51,16 +59,16 @@ async fn send_auth_challenge_request(
|
|||||||
transport
|
transport
|
||||||
.send(ClientRequest {
|
.send(ClientRequest {
|
||||||
request_id: next_request_id(),
|
request_id: next_request_id(),
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
payload: Some(ClientRequestPayload::Auth(proto_auth::Request {
|
||||||
AuthChallengeRequest {
|
payload: Some(AuthRequestPayload::ChallengeRequest(AuthChallengeRequest {
|
||||||
pubkey: key.verifying_key().to_bytes().to_vec(),
|
pubkey: key.verifying_key().to_bytes().to_vec(),
|
||||||
client_info: Some(ProtoClientInfo {
|
client_info: Some(ProtoClientInfo {
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
version: metadata.version,
|
version: metadata.version,
|
||||||
}),
|
}),
|
||||||
},
|
})),
|
||||||
)),
|
})),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| AuthError::UnexpectedAuthResponse)
|
.map_err(|_| AuthError::UnexpectedAuthResponse)
|
||||||
@@ -68,7 +76,7 @@ async fn send_auth_challenge_request(
|
|||||||
|
|
||||||
async fn receive_auth_challenge(
|
async fn receive_auth_challenge(
|
||||||
transport: &mut ClientTransport,
|
transport: &mut ClientTransport,
|
||||||
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, AuthError> {
|
) -> std::result::Result<AuthChallenge, AuthError> {
|
||||||
let response = transport
|
let response = transport
|
||||||
.recv()
|
.recv()
|
||||||
.await
|
.await
|
||||||
@@ -76,8 +84,11 @@ async fn receive_auth_challenge(
|
|||||||
|
|
||||||
let payload = response.payload.ok_or(AuthError::MissingAuthChallenge)?;
|
let payload = response.payload.ok_or(AuthError::MissingAuthChallenge)?;
|
||||||
match payload {
|
match payload {
|
||||||
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
|
ClientResponsePayload::Auth(response) => match response.payload {
|
||||||
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)),
|
Some(AuthResponsePayload::Challenge(challenge)) => Ok(challenge),
|
||||||
|
Some(AuthResponsePayload::Result(result)) => Err(map_auth_result(result)),
|
||||||
|
None => Err(AuthError::MissingAuthChallenge),
|
||||||
|
},
|
||||||
_ => Err(AuthError::UnexpectedAuthResponse),
|
_ => Err(AuthError::UnexpectedAuthResponse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +96,7 @@ async fn receive_auth_challenge(
|
|||||||
async fn send_auth_challenge_solution(
|
async fn send_auth_challenge_solution(
|
||||||
transport: &mut ClientTransport,
|
transport: &mut ClientTransport,
|
||||||
key: &ed25519_dalek::SigningKey,
|
key: &ed25519_dalek::SigningKey,
|
||||||
challenge: arbiter_proto::proto::client::AuthChallenge,
|
challenge: AuthChallenge,
|
||||||
) -> std::result::Result<(), AuthError> {
|
) -> std::result::Result<(), AuthError> {
|
||||||
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
||||||
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
||||||
@@ -93,9 +104,11 @@ async fn send_auth_challenge_solution(
|
|||||||
transport
|
transport
|
||||||
.send(ClientRequest {
|
.send(ClientRequest {
|
||||||
request_id: next_request_id(),
|
request_id: next_request_id(),
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeSolution(
|
payload: Some(ClientRequestPayload::Auth(proto_auth::Request {
|
||||||
AuthChallengeSolution { signature },
|
payload: Some(AuthRequestPayload::ChallengeSolution(
|
||||||
)),
|
AuthChallengeSolution { signature },
|
||||||
|
)),
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| AuthError::UnexpectedAuthResponse)
|
.map_err(|_| AuthError::UnexpectedAuthResponse)
|
||||||
@@ -109,16 +122,17 @@ async fn receive_auth_confirmation(
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| AuthError::UnexpectedAuthResponse)?;
|
.map_err(|_| AuthError::UnexpectedAuthResponse)?;
|
||||||
|
|
||||||
let payload = response
|
let payload = response.payload.ok_or(AuthError::UnexpectedAuthResponse)?;
|
||||||
.payload
|
|
||||||
.ok_or(AuthError::UnexpectedAuthResponse)?;
|
|
||||||
match payload {
|
match payload {
|
||||||
ClientResponsePayload::AuthResult(result)
|
ClientResponsePayload::Auth(response) => match response.payload {
|
||||||
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
|
Some(AuthResponsePayload::Result(result))
|
||||||
{
|
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
|
||||||
Ok(())
|
{
|
||||||
}
|
Ok(())
|
||||||
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)),
|
}
|
||||||
|
Some(AuthResponsePayload::Result(result)) => Err(map_auth_result(result)),
|
||||||
|
_ => Err(AuthError::UnexpectedAuthResponse),
|
||||||
|
},
|
||||||
_ => Err(AuthError::UnexpectedAuthResponse),
|
_ => Err(AuthError::UnexpectedAuthResponse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
use arbiter_client::ArbiterClient;
|
use arbiter_client::ArbiterClient;
|
||||||
use arbiter_proto::{ClientMetadata, url::ArbiterUrl};
|
use arbiter_proto::{ClientMetadata, url::ArbiterUrl};
|
||||||
use tonic::ConnectError;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -23,8 +21,6 @@ async fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let url = match ArbiterUrl::try_from(input) {
|
let url = match ArbiterUrl::try_from(input) {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -33,7 +29,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{:#?}", url);
|
println!("{:#?}", url);
|
||||||
|
|
||||||
let metadata = ClientMetadata {
|
let metadata = ClientMetadata {
|
||||||
name: "arbiter-client test_connect".to_string(),
|
name: "arbiter-client test_connect".to_string(),
|
||||||
@@ -45,4 +41,4 @@ async fn main() {
|
|||||||
Ok(_) => println!("Connected and authenticated successfully."),
|
Ok(_) => println!("Connected and authenticated successfully."),
|
||||||
Err(err) => eprintln!("Failed to connect: {:#?}", err),
|
Err(err) => eprintln!("Failed to connect: {:#?}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
use arbiter_proto::{ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
|
use arbiter_proto::{
|
||||||
|
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
|
||||||
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{Mutex, mpsc};
|
use tokio::sync::{Mutex, mpsc};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tonic::transport::ClientTlsConfig;
|
use tonic::transport::ClientTlsConfig;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport}
|
StorageError,
|
||||||
|
auth::{AuthError, authenticate},
|
||||||
|
storage::{FileSigningKeyStorage, SigningKeyStorage},
|
||||||
|
transport::{BUFFER_LENGTH, ClientTransport},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "evm")]
|
#[cfg(feature = "evm")]
|
||||||
@@ -30,7 +35,6 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("Storage error")]
|
#[error("Storage error")]
|
||||||
Storage(#[from] StorageError),
|
Storage(#[from] StorageError),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ArbiterClient {
|
pub struct ArbiterClient {
|
||||||
@@ -61,10 +65,11 @@ impl ArbiterClient {
|
|||||||
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
||||||
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
||||||
|
|
||||||
let channel = tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
|
let channel =
|
||||||
.tls_config(tls)?
|
tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
|
||||||
.connect()
|
.tls_config(tls)?
|
||||||
.await?;
|
.connect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut client = ArbiterServiceClient::new(channel);
|
let mut client = ArbiterServiceClient::new(channel);
|
||||||
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
|
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ pub use client::{ArbiterClient, Error};
|
|||||||
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
||||||
|
|
||||||
#[cfg(feature = "evm")]
|
#[cfg(feature = "evm")]
|
||||||
pub use wallets::evm::ArbiterEvmWallet;
|
pub use wallets::evm::{ArbiterEvmSignTransactionError, ArbiterEvmWallet};
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use arbiter_proto::proto::{
|
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
|
||||||
client::{ClientRequest, ClientResponse},
|
|
||||||
};
|
|
||||||
use std::sync::atomic::{AtomicI32, Ordering};
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
@@ -36,9 +34,7 @@ impl ClientTransport {
|
|||||||
.map_err(|_| ClientSignError::ChannelClosed)
|
.map_err(|_| ClientSignError::ChannelClosed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn recv(
|
pub(crate) async fn recv(&mut self) -> std::result::Result<ClientResponse, ClientSignError> {
|
||||||
&mut self,
|
|
||||||
) -> std::result::Result<ClientResponse, ClientSignError> {
|
|
||||||
match self.receiver.message().await {
|
match self.receiver.message().await {
|
||||||
Ok(Some(resp)) => Ok(resp),
|
Ok(Some(resp)) => Ok(resp),
|
||||||
Ok(None) => Err(ClientSignError::ConnectionClosed),
|
Ok(None) => Err(ClientSignError::ConnectionClosed),
|
||||||
|
|||||||
@@ -8,7 +8,49 @@ use async_trait::async_trait;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::transport::ClientTransport;
|
use arbiter_proto::proto::{
|
||||||
|
client::{
|
||||||
|
ClientRequest,
|
||||||
|
client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
evm::{
|
||||||
|
self as proto_evm, request::Payload as EvmRequestPayload,
|
||||||
|
response::Payload as EvmResponsePayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
evm::{
|
||||||
|
EvmSignTransactionRequest,
|
||||||
|
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||||
|
},
|
||||||
|
shared::evm::TransactionEvalError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::transport::{ClientTransport, next_request_id};
|
||||||
|
|
||||||
|
/// A typed error payload returned by [`ArbiterEvmWallet`] transaction signing.
|
||||||
|
///
|
||||||
|
/// This is wrapped into `alloy::signers::Error::Other`, so consumers can downcast by [`TryFrom`] and
|
||||||
|
/// interpret the concrete policy evaluation failure instead of parsing strings.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ArbiterEvmSignTransactionError {
|
||||||
|
#[error("transaction rejected by policy: {0:?}")]
|
||||||
|
PolicyEval(TransactionEvalError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a Error> for &'a ArbiterEvmSignTransactionError {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &'a Error) -> Result<Self, Self::Error> {
|
||||||
|
if let Error::Other(inner) = value
|
||||||
|
&& let Some(eval_error) = inner.downcast_ref()
|
||||||
|
{
|
||||||
|
Ok(eval_error)
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ArbiterEvmWallet {
|
pub struct ArbiterEvmWallet {
|
||||||
transport: Arc<Mutex<ClientTransport>>,
|
transport: Arc<Mutex<ClientTransport>>,
|
||||||
@@ -17,6 +59,10 @@ pub struct ArbiterEvmWallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ArbiterEvmWallet {
|
impl ArbiterEvmWallet {
|
||||||
|
#[expect(
|
||||||
|
dead_code,
|
||||||
|
reason = "constructor may be used in future extensions, e.g. to support wallet listing"
|
||||||
|
)]
|
||||||
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
||||||
Self {
|
Self {
|
||||||
transport,
|
transport,
|
||||||
@@ -79,11 +125,72 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
|
|||||||
&self,
|
&self,
|
||||||
tx: &mut dyn SignableTransaction<Signature>,
|
tx: &mut dyn SignableTransaction<Signature>,
|
||||||
) -> Result<Signature> {
|
) -> Result<Signature> {
|
||||||
let _transport = self.transport.lock().await;
|
|
||||||
self.validate_chain_id(tx)?;
|
self.validate_chain_id(tx)?;
|
||||||
|
|
||||||
Err(Error::other(
|
let mut transport = self.transport.lock().await;
|
||||||
"transaction signing is not supported by current arbiter.client protocol",
|
let request_id = next_request_id();
|
||||||
))
|
let rlp_transaction = tx.encoded_for_signing();
|
||||||
|
|
||||||
|
transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
request_id,
|
||||||
|
payload: Some(ClientRequestPayload::Evm(proto_evm::Request {
|
||||||
|
payload: Some(EvmRequestPayload::SignTransaction(
|
||||||
|
EvmSignTransactionRequest {
|
||||||
|
wallet_address: self.address.to_vec(),
|
||||||
|
rlp_transaction,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::other("failed to send evm sign transaction request"))?;
|
||||||
|
|
||||||
|
let response = transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::other("failed to receive evm sign transaction response"))?;
|
||||||
|
|
||||||
|
if response.request_id != Some(request_id) {
|
||||||
|
return Err(Error::other(
|
||||||
|
"received mismatched response id for evm sign transaction",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = response
|
||||||
|
.payload
|
||||||
|
.ok_or_else(|| Error::other("missing evm sign transaction response payload"))?;
|
||||||
|
|
||||||
|
let ClientResponsePayload::Evm(proto_evm::Response {
|
||||||
|
payload: Some(payload),
|
||||||
|
}) = payload
|
||||||
|
else {
|
||||||
|
return Err(Error::other(
|
||||||
|
"unexpected response payload for evm sign transaction request",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let EvmResponsePayload::SignTransaction(response) = payload else {
|
||||||
|
return Err(Error::other(
|
||||||
|
"unexpected evm response payload for sign transaction request",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = response
|
||||||
|
.result
|
||||||
|
.ok_or_else(|| Error::other("missing evm sign transaction result"))?;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
EvmSignTransactionResult::Signature(signature) => {
|
||||||
|
Signature::try_from(signature.as_slice())
|
||||||
|
.map_err(|_| Error::other("invalid signature returned by server"))
|
||||||
|
}
|
||||||
|
EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(
|
||||||
|
ArbiterEvmSignTransactionError::PolicyEval(eval_error),
|
||||||
|
)),
|
||||||
|
EvmSignTransactionResult::Error(code) => Err(Error::other(format!(
|
||||||
|
"server failed to sign transaction with error code {code}"
|
||||||
|
))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ tokio.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
tonic-prost = "0.14.5"
|
tonic-prost = "0.14.5"
|
||||||
prost = "0.14.3"
|
prost.workspace = true
|
||||||
kameo.workspace = true
|
kameo.workspace = true
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
miette.workspace = true
|
miette.workspace = true
|
||||||
|
|||||||
@@ -6,12 +6,56 @@ use base64::{Engine, prelude::BASE64_STANDARD};
|
|||||||
pub mod proto {
|
pub mod proto {
|
||||||
tonic::include_proto!("arbiter");
|
tonic::include_proto!("arbiter");
|
||||||
|
|
||||||
|
pub mod shared {
|
||||||
|
tonic::include_proto!("arbiter.shared");
|
||||||
|
|
||||||
|
pub mod evm {
|
||||||
|
tonic::include_proto!("arbiter.shared.evm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod user_agent {
|
pub mod user_agent {
|
||||||
tonic::include_proto!("arbiter.user_agent");
|
tonic::include_proto!("arbiter.user_agent");
|
||||||
|
|
||||||
|
pub mod auth {
|
||||||
|
tonic::include_proto!("arbiter.user_agent.auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod evm {
|
||||||
|
tonic::include_proto!("arbiter.user_agent.evm");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod sdk_client {
|
||||||
|
tonic::include_proto!("arbiter.user_agent.sdk_client");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod vault {
|
||||||
|
tonic::include_proto!("arbiter.user_agent.vault");
|
||||||
|
|
||||||
|
pub mod bootstrap {
|
||||||
|
tonic::include_proto!("arbiter.user_agent.vault.bootstrap");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod unseal {
|
||||||
|
tonic::include_proto!("arbiter.user_agent.vault.unseal");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod client {
|
pub mod client {
|
||||||
tonic::include_proto!("arbiter.client");
|
tonic::include_proto!("arbiter.client");
|
||||||
|
|
||||||
|
pub mod auth {
|
||||||
|
tonic::include_proto!("arbiter.client.auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod evm {
|
||||||
|
tonic::include_proto!("arbiter.client.evm");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod vault {
|
||||||
|
tonic::include_proto!("arbiter.client.vault");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod evm {
|
pub mod evm {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const ARBITER_URL_SCHEME: &str = "arbiter";
|
|||||||
const CERT_QUERY_KEY: &str = "cert";
|
const CERT_QUERY_KEY: &str = "cert";
|
||||||
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
|
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ArbiterUrl {
|
pub struct ArbiterUrl {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ diesel-async = { version = "0.8.0", features = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
] }
|
] }
|
||||||
ed25519-dalek.workspace = true
|
ed25519-dalek.workspace = true
|
||||||
|
ed25519-dalek.features = ["serde"]
|
||||||
arbiter-proto.path = "../arbiter-proto"
|
arbiter-proto.path = "../arbiter-proto"
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
@@ -25,7 +26,6 @@ tonic.features = ["tls-aws-lc"]
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
rustls.workspace = true
|
rustls.workspace = true
|
||||||
smlang.workspace = true
|
smlang.workspace = true
|
||||||
miette.workspace = true
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
fatality = "0.1.1"
|
fatality = "0.1.1"
|
||||||
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
|
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
|
||||||
@@ -47,13 +47,23 @@ restructed = "0.2.2"
|
|||||||
strum = { version = "0.28.0", features = ["derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
pem = "3.0.6"
|
pem = "3.0.6"
|
||||||
k256.workspace = true
|
k256.workspace = true
|
||||||
|
k256.features = ["serde"]
|
||||||
rsa.workspace = true
|
rsa.workspace = true
|
||||||
|
rsa.features = ["serde"]
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
|
hmac = "0.12"
|
||||||
spki.workspace = true
|
spki.workspace = true
|
||||||
alloy.workspace = true
|
alloy.workspace = true
|
||||||
prost-types.workspace = true
|
prost-types.workspace = true
|
||||||
|
prost.workspace = true
|
||||||
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
serde_with = "3.18.0"
|
||||||
|
mutants.workspace = true
|
||||||
|
subtle = "2.6.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.46.3"
|
insta = "1.46.3"
|
||||||
|
proptest = "1.11.0"
|
||||||
|
rstest.workspace = true
|
||||||
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
||||||
|
|||||||
@@ -191,3 +191,19 @@ create table if not exists evm_ether_transfer_grant_target (
|
|||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
create unique index if not exists uniq_ether_transfer_target on evm_ether_transfer_grant_target (grant_id, address);
|
create unique index if not exists uniq_ether_transfer_target on evm_ether_transfer_grant_target (grant_id, address);
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- Integrity Envelopes
|
||||||
|
-- ===============================
|
||||||
|
create table if not exists integrity_envelope (
|
||||||
|
id integer not null primary key,
|
||||||
|
entity_kind text not null,
|
||||||
|
entity_id blob not null,
|
||||||
|
payload_version integer not null,
|
||||||
|
key_version integer not null,
|
||||||
|
mac blob not null, -- 20-byte recipient address
|
||||||
|
signed_at integer not null default(unixepoch ('now')),
|
||||||
|
created_at integer not null default(unixepoch ('now'))
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
create unique index if not exists uniq_integrity_envelope_entity on integrity_envelope (entity_kind, entity_id);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ use arbiter_proto::{BOOTSTRAP_PATH, home_path};
|
|||||||
use diesel::QueryDsl;
|
use diesel::QueryDsl;
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::{Actor, messages};
|
use kameo::{Actor, messages};
|
||||||
use miette::Diagnostic;
|
|
||||||
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
|
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
|
||||||
|
use subtle::ConstantTimeEq as _;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::db::{self, DatabasePool, schema};
|
use crate::db::{self, DatabasePool, schema};
|
||||||
@@ -25,18 +26,15 @@ pub async fn generate_token() -> Result<String, std::io::Error> {
|
|||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::bootstrap::database))]
|
|
||||||
Database(#[from] db::PoolError),
|
Database(#[from] db::PoolError),
|
||||||
|
|
||||||
#[error("Database query error: {0}")]
|
#[error("Database query error: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::bootstrap::database_query))]
|
|
||||||
Query(#[from] diesel::result::Error),
|
Query(#[from] diesel::result::Error),
|
||||||
|
|
||||||
#[error("I/O error: {0}")]
|
#[error("I/O error: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::bootstrap::io))]
|
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,14 +45,14 @@ pub struct Bootstrapper {
|
|||||||
|
|
||||||
impl Bootstrapper {
|
impl Bootstrapper {
|
||||||
pub async fn new(db: &DatabasePool) -> Result<Self, Error> {
|
pub async fn new(db: &DatabasePool) -> Result<Self, Error> {
|
||||||
let mut conn = db.get().await?;
|
let row_count: i64 = {
|
||||||
|
let mut conn = db.get().await?;
|
||||||
|
|
||||||
let row_count: i64 = schema::useragent_client::table
|
schema::useragent_client::table
|
||||||
.count()
|
.count()
|
||||||
.get_result(&mut conn)
|
.get_result(&mut conn)
|
||||||
.await?;
|
.await?
|
||||||
|
};
|
||||||
drop(conn);
|
|
||||||
|
|
||||||
let token = if row_count == 0 {
|
let token = if row_count == 0 {
|
||||||
let token = generate_token().await?;
|
let token = generate_token().await?;
|
||||||
@@ -72,7 +70,13 @@ impl Bootstrapper {
|
|||||||
#[message]
|
#[message]
|
||||||
pub fn is_correct_token(&self, token: String) -> bool {
|
pub fn is_correct_token(&self, token: String) -> bool {
|
||||||
match &self.token {
|
match &self.token {
|
||||||
Some(expected) => *expected == token,
|
Some(expected) => {
|
||||||
|
let expected_bytes = expected.as_bytes();
|
||||||
|
let token_bytes = token.as_bytes();
|
||||||
|
|
||||||
|
let choice = expected_bytes.ct_eq(token_bytes);
|
||||||
|
bool::from(choice)
|
||||||
|
}
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata, format_challenge, transport::{Bi, expect_message}
|
ClientMetadata, format_challenge,
|
||||||
|
transport::{Bi, expect_message},
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
@@ -8,14 +9,16 @@ use diesel::{
|
|||||||
};
|
};
|
||||||
use diesel_async::RunQueryDsl as _;
|
use diesel_async::RunQueryDsl as _;
|
||||||
use ed25519_dalek::{Signature, VerifyingKey};
|
use ed25519_dalek::{Signature, VerifyingKey};
|
||||||
use kameo::error::SendError;
|
use kameo::{actor::ActorRef, error::SendError};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
client::{ClientConnection, ClientProfile},
|
client::{ClientConnection, ClientCredentials, ClientProfile},
|
||||||
flow_coordinator::{self, RequestClientApproval},
|
flow_coordinator::{self, RequestClientApproval},
|
||||||
|
keyholder::KeyHolder,
|
||||||
},
|
},
|
||||||
|
crypto::integrity::{self},
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
models::{ProgramClientMetadata, SqliteTimestamp},
|
models::{ProgramClientMetadata, SqliteTimestamp},
|
||||||
@@ -29,6 +32,8 @@ pub enum Error {
|
|||||||
DatabasePoolUnavailable,
|
DatabasePoolUnavailable,
|
||||||
#[error("Database operation failed")]
|
#[error("Database operation failed")]
|
||||||
DatabaseOperationFailed,
|
DatabaseOperationFailed,
|
||||||
|
#[error("Integrity check failed")]
|
||||||
|
IntegrityCheckFailed,
|
||||||
#[error("Invalid challenge solution")]
|
#[error("Invalid challenge solution")]
|
||||||
InvalidChallengeSolution,
|
InvalidChallengeSolution,
|
||||||
#[error("Client approval request failed")]
|
#[error("Client approval request failed")]
|
||||||
@@ -37,6 +42,13 @@ pub enum Error {
|
|||||||
Transport,
|
Transport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for Error {
|
||||||
|
fn from(e: diesel::result::Error) -> Self {
|
||||||
|
error!(?e, "Database error");
|
||||||
|
Self::DatabaseOperationFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ApproveError {
|
pub enum ApproveError {
|
||||||
#[error("Internal error")]
|
#[error("Internal error")]
|
||||||
@@ -64,17 +76,69 @@ pub enum Outbound {
|
|||||||
AuthSuccess,
|
AuthSuccess,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClientInfo {
|
/// Returns the current nonce and client ID for a registered client.
|
||||||
pub id: i32,
|
|
||||||
pub current_nonce: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Atomically reads and increments the nonce for a known client.
|
|
||||||
/// Returns `None` if the pubkey is not registered.
|
/// Returns `None` if the pubkey is not registered.
|
||||||
async fn get_client_and_nonce(
|
async fn get_current_nonce_and_id(
|
||||||
db: &db::DatabasePool,
|
db: &db::DatabasePool,
|
||||||
pubkey: &VerifyingKey,
|
pubkey: &VerifyingKey,
|
||||||
) -> Result<Option<ClientInfo>, Error> {
|
) -> Result<Option<(i32, i32)>, Error> {
|
||||||
|
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||||
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
|
error!(error = ?e, "Database pool error");
|
||||||
|
Error::DatabasePoolUnavailable
|
||||||
|
})?;
|
||||||
|
program_client::table
|
||||||
|
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||||
|
.select((program_client::id, program_client::nonce))
|
||||||
|
.first::<(i32, i32)>(&mut conn)
|
||||||
|
.await
|
||||||
|
.optional()
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = ?e, "Database error");
|
||||||
|
Error::DatabaseOperationFailed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_integrity(
|
||||||
|
db: &db::DatabasePool,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
pubkey: &VerifyingKey,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut db_conn = db.get().await.map_err(|e| {
|
||||||
|
error!(error = ?e, "Database pool error");
|
||||||
|
Error::DatabasePoolUnavailable
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| {
|
||||||
|
error!("Client not found during integrity verification");
|
||||||
|
Error::DatabaseOperationFailed
|
||||||
|
})?;
|
||||||
|
|
||||||
|
integrity::verify_entity(
|
||||||
|
&mut db_conn,
|
||||||
|
keyholder,
|
||||||
|
&ClientCredentials {
|
||||||
|
pubkey: *pubkey,
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(?e, "Integrity verification failed");
|
||||||
|
Error::IntegrityCheckFailed
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atomically increments the nonce and re-signs the integrity envelope.
|
||||||
|
/// Returns the new nonce, which is used as the challenge nonce.
|
||||||
|
async fn create_nonce(
|
||||||
|
db: &db::DatabasePool,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
pubkey: &VerifyingKey,
|
||||||
|
) -> Result<i32, Error> {
|
||||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
@@ -83,35 +147,34 @@ async fn get_client_and_nonce(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
conn.exclusive_transaction(|conn| {
|
conn.exclusive_transaction(|conn| {
|
||||||
let pubkey_bytes = pubkey_bytes.clone();
|
let keyholder = keyholder.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let Some((client_id, current_nonce)) = program_client::table
|
let (id, new_nonce): (i32, i32) = update(program_client::table)
|
||||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||||
.select((program_client::id, program_client::nonce))
|
.set(program_client::nonce.eq(program_client::nonce + 1))
|
||||||
.first::<(i32, i32)>(conn)
|
.returning((program_client::id, program_client::nonce))
|
||||||
.await
|
.get_result(conn)
|
||||||
.optional()?
|
|
||||||
else {
|
|
||||||
return Result::<_, diesel::result::Error>::Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
update(program_client::table)
|
|
||||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
|
||||||
.set(program_client::nonce.eq(current_nonce + 1))
|
|
||||||
.execute(conn)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Some(ClientInfo {
|
integrity::sign_entity(
|
||||||
id: client_id,
|
conn,
|
||||||
current_nonce,
|
&keyholder,
|
||||||
}))
|
&ClientCredentials {
|
||||||
|
pubkey: *pubkey,
|
||||||
|
nonce: new_nonce,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(?e, "Integrity sign failed after nonce update");
|
||||||
|
Error::DatabaseOperationFailed
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(new_nonce)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
|
||||||
error!(error = ?e, "Database error");
|
|
||||||
Error::DatabaseOperationFailed
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn approve_new_client(
|
async fn approve_new_client(
|
||||||
@@ -139,45 +202,63 @@ async fn approve_new_client(
|
|||||||
|
|
||||||
async fn insert_client(
|
async fn insert_client(
|
||||||
db: &db::DatabasePool,
|
db: &db::DatabasePool,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &VerifyingKey,
|
pubkey: &VerifyingKey,
|
||||||
metadata: &ClientMetadata,
|
metadata: &ClientMetadata,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
use crate::db::schema::{client_metadata, program_client};
|
use crate::db::schema::{client_metadata, program_client};
|
||||||
|
let metadata = metadata.clone();
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
Error::DatabasePoolUnavailable
|
Error::DatabasePoolUnavailable
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let metadata_id = insert_into(client_metadata::table)
|
conn.exclusive_transaction(|conn| {
|
||||||
.values((
|
let keyholder = keyholder.clone();
|
||||||
client_metadata::name.eq(&metadata.name),
|
Box::pin(async move {
|
||||||
client_metadata::description.eq(&metadata.description),
|
const NONCE_START: i32 = 1;
|
||||||
client_metadata::version.eq(&metadata.version),
|
|
||||||
))
|
|
||||||
.returning(client_metadata::id)
|
|
||||||
.get_result::<i32>(&mut conn)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(error = ?e, "Failed to insert client metadata");
|
|
||||||
Error::DatabaseOperationFailed
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let client_id = insert_into(program_client::table)
|
let metadata_id = insert_into(client_metadata::table)
|
||||||
.values((
|
.values((
|
||||||
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
client_metadata::name.eq(&metadata.name),
|
||||||
program_client::metadata_id.eq(metadata_id),
|
client_metadata::description.eq(&metadata.description),
|
||||||
program_client::nonce.eq(1), // pre-incremented; challenge uses 0
|
client_metadata::version.eq(&metadata.version),
|
||||||
))
|
))
|
||||||
.on_conflict_do_nothing()
|
.returning(client_metadata::id)
|
||||||
.returning(program_client::id)
|
.get_result::<i32>(conn)
|
||||||
.get_result::<i32>(&mut conn)
|
.await?;
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(error = ?e, "Failed to insert client metadata");
|
|
||||||
Error::DatabaseOperationFailed
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(client_id)
|
let client_id = insert_into(program_client::table)
|
||||||
|
.values((
|
||||||
|
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
||||||
|
program_client::metadata_id.eq(metadata_id),
|
||||||
|
program_client::nonce.eq(NONCE_START),
|
||||||
|
))
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.returning(program_client::id)
|
||||||
|
.get_result::<i32>(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
integrity::sign_entity(
|
||||||
|
conn,
|
||||||
|
&keyholder,
|
||||||
|
&ClientCredentials {
|
||||||
|
pubkey: *pubkey,
|
||||||
|
nonce: NONCE_START,
|
||||||
|
},
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = ?e, "Failed to sign integrity tag for new client key");
|
||||||
|
Error::DatabaseOperationFailed
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(client_id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_client_metadata(
|
async fn sync_client_metadata(
|
||||||
@@ -287,10 +368,7 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate<T>(
|
pub async fn authenticate<T>(props: &mut ClientConnection, transport: &mut T) -> Result<i32, Error>
|
||||||
props: &mut ClientConnection,
|
|
||||||
transport: &mut T,
|
|
||||||
) -> Result<VerifyingKey, Error>
|
|
||||||
where
|
where
|
||||||
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
||||||
{
|
{
|
||||||
@@ -298,8 +376,11 @@ where
|
|||||||
return Err(Error::Transport);
|
return Err(Error::Transport);
|
||||||
};
|
};
|
||||||
|
|
||||||
let info = match get_client_and_nonce(&props.db, &pubkey).await? {
|
let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? {
|
||||||
Some(nonce) => nonce,
|
Some((id, _)) => {
|
||||||
|
verify_integrity(&props.db, &props.actors.key_holder, &pubkey).await?;
|
||||||
|
id
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
approve_new_client(
|
approve_new_client(
|
||||||
&props.actors,
|
&props.actors,
|
||||||
@@ -309,18 +390,14 @@ where
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let client_id = insert_client(&props.db, &pubkey, &metadata).await?;
|
insert_client(&props.db, &props.actors.key_holder, &pubkey, &metadata).await?
|
||||||
ClientInfo {
|
|
||||||
id: client_id,
|
|
||||||
current_nonce: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sync_client_metadata(&props.db, info.id, &metadata).await?;
|
sync_client_metadata(&props.db, client_id, &metadata).await?;
|
||||||
|
let challenge_nonce = create_nonce(&props.db, &props.actors.key_holder, &pubkey).await?;
|
||||||
|
challenge_client(transport, pubkey, challenge_nonce).await?;
|
||||||
|
|
||||||
challenge_client(transport, pubkey, info.current_nonce).await?;
|
|
||||||
|
|
||||||
transport
|
transport
|
||||||
.send(Ok(Outbound::AuthSuccess))
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
.await
|
.await
|
||||||
@@ -329,5 +406,5 @@ where
|
|||||||
Error::Transport
|
Error::Transport
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(pubkey)
|
Ok(client_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use kameo::actor::Spawn;
|
|||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, client::{ session::ClientSession}},
|
actors::{GlobalActors, client::session::ClientSession},
|
||||||
|
crypto::integrity::{Integrable, hashing::Hashable},
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,6 +14,22 @@ pub struct ClientProfile {
|
|||||||
pub metadata: ClientMetadata,
|
pub metadata: ClientMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ClientCredentials {
|
||||||
|
pub pubkey: ed25519_dalek::VerifyingKey,
|
||||||
|
pub nonce: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Integrable for ClientCredentials {
|
||||||
|
const KIND: &'static str = "client_credentials";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for ClientCredentials {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.pubkey.as_bytes());
|
||||||
|
self.nonce.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ClientConnection {
|
pub struct ClientConnection {
|
||||||
pub(crate) db: db::DatabasePool,
|
pub(crate) db: db::DatabasePool,
|
||||||
pub(crate) actors: GlobalActors,
|
pub(crate) actors: GlobalActors,
|
||||||
@@ -32,8 +49,8 @@ where
|
|||||||
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
|
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
|
||||||
{
|
{
|
||||||
match auth::authenticate(&mut props, transport).await {
|
match auth::authenticate(&mut props, transport).await {
|
||||||
Ok(_pubkey) => {
|
Ok(client_id) => {
|
||||||
ClientSession::spawn(ClientSession::new(props));
|
ClientSession::spawn(ClientSession::new(props, client_id));
|
||||||
info!("Client authenticated, session started");
|
info!("Client authenticated, session started");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
use kameo::{Actor, messages};
|
use kameo::{Actor, messages};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors, client::ClientConnection, flow_coordinator::RegisterClient,
|
GlobalActors,
|
||||||
|
client::ClientConnection,
|
||||||
|
evm::{ClientSignTransaction, SignTransactionError},
|
||||||
|
flow_coordinator::RegisterClient,
|
||||||
keyholder::KeyHolderState,
|
keyholder::KeyHolderState,
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
|
evm::VetError,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ClientSession {
|
pub struct ClientSession {
|
||||||
props: ClientConnection,
|
props: ClientConnection,
|
||||||
|
client_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
pub(crate) fn new(props: ClientConnection) -> Self {
|
pub(crate) fn new(props: ClientConnection, client_id: i32) -> Self {
|
||||||
Self { props }
|
Self { props, client_id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +42,34 @@ impl ClientSession {
|
|||||||
|
|
||||||
Ok(vault_state)
|
Ok(vault_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub(crate) async fn handle_sign_transaction(
|
||||||
|
&mut self,
|
||||||
|
wallet_address: Address,
|
||||||
|
transaction: TxEip1559,
|
||||||
|
) -> Result<Signature, SignTransactionRpcError> {
|
||||||
|
match self
|
||||||
|
.props
|
||||||
|
.actors
|
||||||
|
.evm
|
||||||
|
.ask(ClientSignTransaction {
|
||||||
|
client_id: self.client_id,
|
||||||
|
wallet_address,
|
||||||
|
transaction,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(signature) => Ok(signature),
|
||||||
|
Err(kameo::error::SendError::HandlerError(SignTransactionError::Vet(vet_error))) => {
|
||||||
|
Err(SignTransactionRpcError::Vet(vet_error))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, "Failed to sign EVM transaction in client session");
|
||||||
|
Err(SignTransactionRpcError::Internal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Actor for ClientSession {
|
impl Actor for ClientSession {
|
||||||
@@ -59,7 +94,10 @@ impl Actor for ClientSession {
|
|||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||||
let props = ClientConnection::new(db, actors);
|
let props = ClientConnection::new(db, actors);
|
||||||
Self { props }
|
Self {
|
||||||
|
props,
|
||||||
|
client_id: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,3 +108,12 @@ pub enum Error {
|
|||||||
#[error("Internal error")]
|
#[error("Internal error")]
|
||||||
Internal,
|
Internal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SignTransactionRpcError {
|
||||||
|
#[error("Policy evaluation failed")]
|
||||||
|
Vet(#[from] VetError),
|
||||||
|
|
||||||
|
#[error("Internal error")]
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ use rand::{SeedableRng, rng, rngs::StdRng};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||||
|
crypto::integrity,
|
||||||
db::{
|
db::{
|
||||||
self, DatabaseError, DatabasePool,
|
DatabaseError, DatabasePool,
|
||||||
models::{self, SqliteTimestamp},
|
models::{self},
|
||||||
schema,
|
schema,
|
||||||
},
|
},
|
||||||
evm::{
|
evm::{
|
||||||
self, RunKind,
|
self, ListError, RunKind,
|
||||||
policies::{
|
policies::{
|
||||||
FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||||
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -25,46 +26,40 @@ use crate::{
|
|||||||
|
|
||||||
pub use crate::evm::safe_signer;
|
pub use crate::evm::safe_signer;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SignTransactionError {
|
pub enum SignTransactionError {
|
||||||
#[error("Wallet not found")]
|
#[error("Wallet not found")]
|
||||||
#[diagnostic(code(arbiter::evm::sign::wallet_not_found))]
|
|
||||||
WalletNotFound,
|
WalletNotFound,
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::sign::database))]
|
|
||||||
Database(#[from] DatabaseError),
|
Database(#[from] DatabaseError),
|
||||||
|
|
||||||
#[error("Keyholder error: {0}")]
|
#[error("Keyholder error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::sign::keyholder))]
|
|
||||||
Keyholder(#[from] crate::actors::keyholder::Error),
|
Keyholder(#[from] crate::actors::keyholder::Error),
|
||||||
|
|
||||||
#[error("Keyholder mailbox error")]
|
#[error("Keyholder mailbox error")]
|
||||||
#[diagnostic(code(arbiter::evm::sign::keyholder_send))]
|
|
||||||
KeyholderSend,
|
KeyholderSend,
|
||||||
|
|
||||||
#[error("Signing error: {0}")]
|
#[error("Signing error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::sign::signing))]
|
|
||||||
Signing(#[from] alloy::signers::Error),
|
Signing(#[from] alloy::signers::Error),
|
||||||
|
|
||||||
#[error("Policy error: {0}")]
|
#[error("Policy error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::sign::vet))]
|
|
||||||
Vet(#[from] evm::VetError),
|
Vet(#[from] evm::VetError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Keyholder error: {0}")]
|
#[error("Keyholder error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::keyholder))]
|
|
||||||
Keyholder(#[from] crate::actors::keyholder::Error),
|
Keyholder(#[from] crate::actors::keyholder::Error),
|
||||||
|
|
||||||
#[error("Keyholder mailbox error")]
|
#[error("Keyholder mailbox error")]
|
||||||
#[diagnostic(code(arbiter::evm::keyholder_send))]
|
|
||||||
KeyholderSend,
|
KeyholderSend,
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::database))]
|
|
||||||
Database(#[from] DatabaseError),
|
Database(#[from] DatabaseError),
|
||||||
|
|
||||||
|
#[error("Integrity violation: {0}")]
|
||||||
|
Integrity(#[from] integrity::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Actor)]
|
#[derive(Actor)]
|
||||||
@@ -80,7 +75,7 @@ impl EvmActor {
|
|||||||
// is it safe to seed rng from system once?
|
// is it safe to seed rng from system once?
|
||||||
// todo: audit
|
// todo: audit
|
||||||
let rng = StdRng::from_rng(&mut rng());
|
let rng = StdRng::from_rng(&mut rng());
|
||||||
let engine = evm::Engine::new(db.clone());
|
let engine = evm::Engine::new(db.clone(), keyholder.clone());
|
||||||
Self {
|
Self {
|
||||||
keyholder,
|
keyholder,
|
||||||
db,
|
db,
|
||||||
@@ -141,46 +136,59 @@ impl EvmActor {
|
|||||||
&mut self,
|
&mut self,
|
||||||
basic: SharedGrantSettings,
|
basic: SharedGrantSettings,
|
||||||
grant: SpecificGrant,
|
grant: SpecificGrant,
|
||||||
) -> Result<i32, DatabaseError> {
|
) -> Result<integrity::Verified<i32>, Error> {
|
||||||
match grant {
|
match grant {
|
||||||
SpecificGrant::EtherTransfer(settings) => {
|
SpecificGrant::EtherTransfer(settings) => self
|
||||||
self.engine
|
.engine
|
||||||
.create_grant::<EtherTransfer>(FullGrant {
|
.create_grant::<EtherTransfer>(CombinedSettings {
|
||||||
basic,
|
shared: basic,
|
||||||
specific: settings,
|
specific: settings,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
.map_err(Error::from),
|
||||||
SpecificGrant::TokenTransfer(settings) => {
|
SpecificGrant::TokenTransfer(settings) => self
|
||||||
self.engine
|
.engine
|
||||||
.create_grant::<TokenTransfer>(FullGrant {
|
.create_grant::<TokenTransfer>(CombinedSettings {
|
||||||
basic,
|
shared: basic,
|
||||||
specific: settings,
|
specific: settings,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
.map_err(Error::from),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
|
||||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
diesel::update(schema::evm_basic_grant::table)
|
// let keyholder = self.keyholder.clone();
|
||||||
.filter(schema::evm_basic_grant::id.eq(grant_id))
|
|
||||||
.set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
// diesel_async::AsyncConnection::transaction(&mut conn, |conn| {
|
||||||
.execute(&mut conn)
|
// Box::pin(async move {
|
||||||
.await
|
// diesel::update(schema::evm_basic_grant::table)
|
||||||
.map_err(DatabaseError::from)?;
|
// .filter(schema::evm_basic_grant::id.eq(grant_id))
|
||||||
Ok(())
|
// .set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
||||||
|
// .execute(conn)
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
// let signed = integrity::evm::load_signed_grant_by_basic_id(conn, grant_id).await?;
|
||||||
|
|
||||||
|
// diesel::result::QueryResult::Ok(())
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// .await
|
||||||
|
// .map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
|
// Ok(())
|
||||||
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
||||||
Ok(self
|
match self.engine.list_all_grants().await {
|
||||||
.engine
|
Ok(grants) => Ok(grants),
|
||||||
.list_all_grants()
|
Err(ListError::Database(db_err)) => Err(Error::Database(db_err)),
|
||||||
.await
|
Err(ListError::Integrity(integrity_err)) => Err(Error::Integrity(integrity_err)),
|
||||||
.map_err(DatabaseError::from)?)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::actors::{
|
|||||||
pub struct Args {
|
pub struct Args {
|
||||||
pub client: ClientProfile,
|
pub client: ClientProfile,
|
||||||
pub user_agents: Vec<ActorRef<UserAgentSession>>,
|
pub user_agents: Vec<ActorRef<UserAgentSession>>,
|
||||||
pub reply: ReplySender<Result<bool, ApprovalError>>
|
pub reply: ReplySender<Result<bool, ApprovalError>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClientApprovalController {
|
pub struct ClientApprovalController {
|
||||||
@@ -39,7 +39,11 @@ impl Actor for ClientApprovalController {
|
|||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
async fn on_start(
|
async fn on_start(
|
||||||
Args { client, mut user_agents, reply }: Self::Args,
|
Args {
|
||||||
|
client,
|
||||||
|
mut user_agents,
|
||||||
|
reply,
|
||||||
|
}: Self::Args,
|
||||||
actor_ref: ActorRef<Self>,
|
actor_ref: ActorRef<Self>,
|
||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
let this = Self {
|
let this = Self {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
pub mod v1;
|
|
||||||
@@ -4,11 +4,19 @@ use diesel::{
|
|||||||
dsl::{insert_into, update},
|
dsl::{insert_into, update},
|
||||||
};
|
};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
use hmac::Mac as _;
|
||||||
use kameo::{Actor, Reply, messages};
|
use kameo::{Actor, Reply, messages};
|
||||||
use strum::{EnumDiscriminants, IntoDiscriminant};
|
use strum::{EnumDiscriminants, IntoDiscriminant};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::safe_cell::SafeCell;
|
use crate::{
|
||||||
|
crypto::{
|
||||||
|
KeyCell, derive_key,
|
||||||
|
encryption::v1::{self, Nonce},
|
||||||
|
integrity::v1::HmacSha256,
|
||||||
|
},
|
||||||
|
safe_cell::SafeCell,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
@@ -17,9 +25,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
safe_cell::SafeCellHandle as _,
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
use encryption::v1::{self, KeyCell, Nonce};
|
|
||||||
|
|
||||||
pub mod encryption;
|
|
||||||
|
|
||||||
#[derive(Default, EnumDiscriminants)]
|
#[derive(Default, EnumDiscriminants)]
|
||||||
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
||||||
@@ -35,36 +40,28 @@ enum State {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Keyholder is already bootstrapped")]
|
#[error("Keyholder is already bootstrapped")]
|
||||||
#[diagnostic(code(arbiter::keyholder::already_bootstrapped))]
|
|
||||||
AlreadyBootstrapped,
|
AlreadyBootstrapped,
|
||||||
#[error("Keyholder is not bootstrapped")]
|
#[error("Keyholder is not bootstrapped")]
|
||||||
#[diagnostic(code(arbiter::keyholder::not_bootstrapped))]
|
|
||||||
NotBootstrapped,
|
NotBootstrapped,
|
||||||
#[error("Invalid key provided")]
|
#[error("Invalid key provided")]
|
||||||
#[diagnostic(code(arbiter::keyholder::invalid_key))]
|
|
||||||
InvalidKey,
|
InvalidKey,
|
||||||
|
|
||||||
#[error("Requested aead entry not found")]
|
#[error("Requested aead entry not found")]
|
||||||
#[diagnostic(code(arbiter::keyholder::aead_not_found))]
|
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
#[error("Encryption error: {0}")]
|
#[error("Encryption error: {0}")]
|
||||||
#[diagnostic(code(arbiter::keyholder::encryption_error))]
|
|
||||||
Encryption(#[from] chacha20poly1305::aead::Error),
|
Encryption(#[from] chacha20poly1305::aead::Error),
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
#[diagnostic(code(arbiter::keyholder::database_error))]
|
|
||||||
DatabaseConnection(#[from] db::PoolError),
|
DatabaseConnection(#[from] db::PoolError),
|
||||||
|
|
||||||
#[error("Database transaction error: {0}")]
|
#[error("Database transaction error: {0}")]
|
||||||
#[diagnostic(code(arbiter::keyholder::database_transaction_error))]
|
|
||||||
DatabaseTransaction(#[from] diesel::result::Error),
|
DatabaseTransaction(#[from] diesel::result::Error),
|
||||||
|
|
||||||
#[error("Broken database")]
|
#[error("Broken database")]
|
||||||
#[diagnostic(code(arbiter::keyholder::broken_database))]
|
|
||||||
BrokenDatabase,
|
BrokenDatabase,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,14 +111,13 @@ impl KeyHolder {
|
|||||||
.first(conn)
|
.first(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut nonce =
|
let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
||||||
v1::Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
error!(
|
||||||
error!(
|
"Broken database: invalid nonce for root key history id={}",
|
||||||
"Broken database: invalid nonce for root key history id={}",
|
root_key_id
|
||||||
root_key_id
|
);
|
||||||
);
|
Error::BrokenDatabase
|
||||||
Error::BrokenDatabase
|
})?;
|
||||||
})?;
|
|
||||||
nonce.increment();
|
nonce.increment();
|
||||||
|
|
||||||
update(schema::root_key_history::table)
|
update(schema::root_key_history::table)
|
||||||
@@ -144,12 +140,12 @@ impl KeyHolder {
|
|||||||
return Err(Error::AlreadyBootstrapped);
|
return Err(Error::AlreadyBootstrapped);
|
||||||
}
|
}
|
||||||
let salt = v1::generate_salt();
|
let salt = v1::generate_salt();
|
||||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
let mut seal_key = derive_key(seal_key_raw, &salt);
|
||||||
let mut root_key = KeyCell::new_secure_random();
|
let mut root_key = KeyCell::new_secure_random();
|
||||||
|
|
||||||
// Zero nonces are fine because they are one-time
|
// Zero nonces are fine because they are one-time
|
||||||
let root_key_nonce = v1::Nonce::default();
|
let root_key_nonce = Nonce::default();
|
||||||
let data_encryption_nonce = v1::Nonce::default();
|
let data_encryption_nonce = Nonce::default();
|
||||||
|
|
||||||
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
||||||
let root_key_reader = reader.as_slice();
|
let root_key_reader = reader.as_slice();
|
||||||
@@ -214,7 +210,6 @@ impl KeyHolder {
|
|||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await?;
|
||||||
schema::root_key_history::table
|
schema::root_key_history::table
|
||||||
.filter(schema::root_key_history::id.eq(*root_key_history_id))
|
.filter(schema::root_key_history::id.eq(*root_key_history_id))
|
||||||
.select(schema::root_key_history::data_encryption_nonce)
|
|
||||||
.select(RootKeyHistory::as_select())
|
.select(RootKeyHistory::as_select())
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await?
|
.await?
|
||||||
@@ -225,7 +220,7 @@ impl KeyHolder {
|
|||||||
error!("Broken database: invalid salt for root key");
|
error!("Broken database: invalid salt for root key");
|
||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?;
|
})?;
|
||||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
let mut seal_key = derive_key(seal_key_raw, &salt);
|
||||||
|
|
||||||
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
||||||
|
|
||||||
@@ -245,7 +240,7 @@ impl KeyHolder {
|
|||||||
|
|
||||||
self.state = State::Unsealed {
|
self.state = State::Unsealed {
|
||||||
root_key_history_id: current_key.id,
|
root_key_history_id: current_key.id,
|
||||||
root_key: v1::KeyCell::try_from(root_key).map_err(|err| {
|
root_key: KeyCell::try_from(root_key).map_err(|err| {
|
||||||
error!(?err, "Broken database: invalid encryption key size");
|
error!(?err, "Broken database: invalid encryption key size");
|
||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?,
|
})?,
|
||||||
@@ -256,7 +251,6 @@ impl KeyHolder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
||||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||||
@@ -292,6 +286,7 @@ impl KeyHolder {
|
|||||||
let State::Unsealed {
|
let State::Unsealed {
|
||||||
root_key,
|
root_key,
|
||||||
root_key_history_id,
|
root_key_history_id,
|
||||||
|
..
|
||||||
} = &mut self.state
|
} = &mut self.state
|
||||||
else {
|
else {
|
||||||
return Err(Error::NotBootstrapped);
|
return Err(Error::NotBootstrapped);
|
||||||
@@ -329,6 +324,60 @@ impl KeyHolder {
|
|||||||
self.state.discriminant()
|
self.state.discriminant()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub fn sign_integrity(&mut self, mac_input: Vec<u8>) -> Result<(i32, Vec<u8>), Error> {
|
||||||
|
let State::Unsealed {
|
||||||
|
root_key,
|
||||||
|
root_key_history_id,
|
||||||
|
} = &mut self.state
|
||||||
|
else {
|
||||||
|
return Err(Error::NotBootstrapped);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hmac = root_key
|
||||||
|
.0
|
||||||
|
.read_inline(|k| match HmacSha256::new_from_slice(k) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
||||||
|
});
|
||||||
|
hmac.update(&root_key_history_id.to_be_bytes());
|
||||||
|
hmac.update(&mac_input);
|
||||||
|
|
||||||
|
let mac = hmac.finalize().into_bytes().to_vec();
|
||||||
|
Ok((*root_key_history_id, mac))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub fn verify_integrity(
|
||||||
|
&mut self,
|
||||||
|
mac_input: Vec<u8>,
|
||||||
|
expected_mac: Vec<u8>,
|
||||||
|
key_version: i32,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let State::Unsealed {
|
||||||
|
root_key,
|
||||||
|
root_key_history_id,
|
||||||
|
} = &mut self.state
|
||||||
|
else {
|
||||||
|
return Err(Error::NotBootstrapped);
|
||||||
|
};
|
||||||
|
|
||||||
|
if *root_key_history_id != key_version {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hmac = root_key
|
||||||
|
.0
|
||||||
|
.read_inline(|k| match HmacSha256::new_from_slice(k) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
||||||
|
});
|
||||||
|
hmac.update(&key_version.to_be_bytes());
|
||||||
|
hmac.update(&mac_input);
|
||||||
|
|
||||||
|
Ok(hmac.verify_slice(&expected_mac).is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub fn seal(&mut self) -> Result<(), Error> {
|
pub fn seal(&mut self) -> Result<(), Error> {
|
||||||
let State::Unsealed {
|
let State::Unsealed {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use kameo::actor::{ActorRef, Spawn};
|
use kameo::actor::{ActorRef, Spawn};
|
||||||
use miette::Diagnostic;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -17,14 +16,12 @@ pub mod flow_coordinator;
|
|||||||
pub mod keyholder;
|
pub mod keyholder;
|
||||||
pub mod user_agent;
|
pub mod user_agent;
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug)]
|
||||||
pub enum SpawnError {
|
pub enum SpawnError {
|
||||||
#[error("Failed to spawn Bootstrapper actor")]
|
#[error("Failed to spawn Bootstrapper actor")]
|
||||||
#[diagnostic(code(SpawnError::Bootstrapper))]
|
|
||||||
Bootstrapper(#[from] bootstrap::Error),
|
Bootstrapper(#[from] bootstrap::Error),
|
||||||
|
|
||||||
#[error("Failed to spawn KeyHolder actor")]
|
#[error("Failed to spawn KeyHolder actor")]
|
||||||
#[diagnostic(code(SpawnError::KeyHolder))]
|
|
||||||
KeyHolder(#[from] keyholder::Error),
|
KeyHolder(#[from] keyholder::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,26 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
fn internal(details: impl Into<String>) -> Self {
|
#[track_caller]
|
||||||
Self::Internal {
|
pub(super) fn internal(details: impl Into<String>, err: &impl std::fmt::Debug) -> Self {
|
||||||
details: details.into(),
|
let details = details.into();
|
||||||
}
|
let caller = std::panic::Location::caller();
|
||||||
|
error!(
|
||||||
|
caller_file = %caller.file(),
|
||||||
|
caller_line = caller.line(),
|
||||||
|
caller_column = caller.column(),
|
||||||
|
details = %details,
|
||||||
|
error = ?err,
|
||||||
|
"Internal error"
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::Internal { details }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for Error {
|
||||||
|
fn from(e: diesel::result::Error) -> Self {
|
||||||
|
Self::internal("Database error", &e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::Bi;
|
||||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
use kameo::actor::ActorRef;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
bootstrap::ConsumeToken,
|
bootstrap::ConsumeToken,
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
keyholder::KeyHolder,
|
||||||
|
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
||||||
},
|
},
|
||||||
db::schema,
|
crypto::integrity,
|
||||||
|
db::{DatabasePool, schema::useragent_client},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ChallengeRequest {
|
pub struct ChallengeRequest {
|
||||||
@@ -40,61 +43,166 @@ smlang::statemachine!(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
|
/// Returns the current nonce, ready to use for the challenge nonce.
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
async fn get_current_nonce_and_id(
|
||||||
error!(error = ?e, "Database pool error");
|
db: &DatabasePool,
|
||||||
Error::internal("Database unavailable")
|
key: &AuthPublicKey,
|
||||||
})?;
|
) -> Result<(i32, i32), Error> {
|
||||||
|
let mut db_conn = db
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
db_conn
|
db_conn
|
||||||
.exclusive_transaction(|conn| {
|
.exclusive_transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let current_nonce = schema::useragent_client::table
|
useragent_client::table
|
||||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
.filter(useragent_client::public_key.eq(key.to_stored_bytes()))
|
||||||
.select(schema::useragent_client::nonce)
|
.filter(useragent_client::key_type.eq(key.key_type()))
|
||||||
.first::<i32>(conn)
|
.select((useragent_client::id, useragent_client::nonce))
|
||||||
.await?;
|
.first::<(i32, i32)>(conn)
|
||||||
|
.await
|
||||||
update(schema::useragent_client::table)
|
|
||||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
|
||||||
.set(schema::useragent_client::nonce.eq(current_nonce + 1))
|
|
||||||
.execute(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Result::<_, diesel::result::Error>::Ok(current_nonce)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Database operation failed", &e))?
|
||||||
error!(error = ?e, "Database error");
|
|
||||||
Error::internal("Database operation failed")
|
|
||||||
})?
|
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error!(?pubkey_bytes, "Public key not found in database");
|
error!(?key, "Public key not found in database");
|
||||||
Error::UnregisteredPublicKey
|
Error::UnregisteredPublicKey
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> {
|
async fn verify_integrity(
|
||||||
|
db: &DatabasePool,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
pubkey: &AuthPublicKey,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut db_conn = db
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
|
|
||||||
|
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
|
||||||
|
|
||||||
|
let attestation_status = integrity::check_entity_attestation(
|
||||||
|
&mut db_conn,
|
||||||
|
keyholder,
|
||||||
|
&UserAgentCredentials {
|
||||||
|
pubkey: pubkey.clone(),
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::internal("Integrity verification failed", &e))?;
|
||||||
|
|
||||||
|
use integrity::AttestationStatus as AS;
|
||||||
|
// SAFETY (policy): challenge auth must work in both vault states.
|
||||||
|
// While sealed, integrity checks can only report `Unavailable` because key material is not
|
||||||
|
// accessible. While unsealed, the same check can report `Attested`.
|
||||||
|
// This path intentionally accepts both outcomes to keep challenge auth available across state
|
||||||
|
// transitions; stricter verification is enforced in sensitive post-auth flows.
|
||||||
|
match attestation_status {
|
||||||
|
AS::Attested | AS::Unavailable => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_nonce(
|
||||||
|
db: &DatabasePool,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
pubkey: &AuthPublicKey,
|
||||||
|
) -> Result<i32, Error> {
|
||||||
|
let mut db_conn = db
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
|
let new_nonce = db_conn
|
||||||
|
.exclusive_transaction(|conn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let (id, new_nonce): (i32, i32) = update(useragent_client::table)
|
||||||
|
.filter(useragent_client::public_key.eq(pubkey.to_stored_bytes()))
|
||||||
|
.filter(useragent_client::key_type.eq(pubkey.key_type()))
|
||||||
|
.set(useragent_client::nonce.eq(useragent_client::nonce + 1))
|
||||||
|
.returning((useragent_client::id, useragent_client::nonce))
|
||||||
|
.get_result(conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||||
|
|
||||||
|
integrity::sign_entity(
|
||||||
|
conn,
|
||||||
|
keyholder,
|
||||||
|
&UserAgentCredentials {
|
||||||
|
pubkey: pubkey.clone(),
|
||||||
|
nonce: new_nonce,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::internal("Database error", &e))?;
|
||||||
|
|
||||||
|
Result::<_, Error>::Ok(new_nonce)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(new_nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_key(
|
||||||
|
db: &DatabasePool,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
pubkey: &AuthPublicKey,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||||
let key_type = pubkey.key_type();
|
let key_type = pubkey.key_type();
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db
|
||||||
error!(error = ?e, "Database pool error");
|
.get()
|
||||||
Error::internal("Database unavailable")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
diesel::insert_into(schema::useragent_client::table)
|
|
||||||
.values((
|
|
||||||
schema::useragent_client::public_key.eq(pubkey_bytes),
|
|
||||||
schema::useragent_client::nonce.eq(1),
|
|
||||||
schema::useragent_client::key_type.eq(key_type),
|
|
||||||
))
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
error!(error = ?e, "Database error");
|
|
||||||
Error::internal("Database operation failed")
|
conn.transaction(|conn| {
|
||||||
})?;
|
Box::pin(async move {
|
||||||
|
const NONCE_START: i32 = 1;
|
||||||
|
|
||||||
|
let id: i32 = diesel::insert_into(useragent_client::table)
|
||||||
|
.values((
|
||||||
|
useragent_client::public_key.eq(pubkey_bytes),
|
||||||
|
useragent_client::nonce.eq(NONCE_START),
|
||||||
|
useragent_client::key_type.eq(key_type),
|
||||||
|
))
|
||||||
|
.returning(useragent_client::id)
|
||||||
|
.get_result(conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||||
|
|
||||||
|
if let Err(e) = integrity::sign_entity(
|
||||||
|
conn,
|
||||||
|
keyholder,
|
||||||
|
&UserAgentCredentials {
|
||||||
|
pubkey: pubkey.clone(),
|
||||||
|
nonce: NONCE_START,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
match e {
|
||||||
|
integrity::Error::Keyholder(
|
||||||
|
crate::actors::keyholder::Error::NotBootstrapped,
|
||||||
|
) => {
|
||||||
|
// IMPORTANT: bootstrap-token auth must work before the vault has a root key.
|
||||||
|
// We intentionally allow creating the DB row first and backfill envelopes
|
||||||
|
// after bootstrap/unseal to keep the bootstrap flow possible.
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(Error::internal("Failed to register public key", &other));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result::<_, Error>::Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -120,8 +228,9 @@ where
|
|||||||
&mut self,
|
&mut self,
|
||||||
ChallengeRequest { pubkey }: ChallengeRequest,
|
ChallengeRequest { pubkey }: ChallengeRequest,
|
||||||
) -> Result<ChallengeContext, Self::Error> {
|
) -> Result<ChallengeContext, Self::Error> {
|
||||||
let stored_bytes = pubkey.to_stored_bytes();
|
verify_integrity(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
|
||||||
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
|
|
||||||
|
let nonce = create_nonce(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
|
||||||
|
|
||||||
self.transport
|
self.transport
|
||||||
.send(Ok(Outbound::AuthChallenge { nonce }))
|
.send(Ok(Outbound::AuthChallenge { nonce }))
|
||||||
@@ -151,24 +260,31 @@ where
|
|||||||
token: token.clone(),
|
token: token.clone(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Failed to consume bootstrap token", &e))?;
|
||||||
error!(?e, "Failed to consume bootstrap token");
|
|
||||||
Error::internal("Failed to consume bootstrap token")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !token_ok {
|
if !token_ok {
|
||||||
error!("Invalid bootstrap token provided");
|
error!("Invalid bootstrap token provided");
|
||||||
return Err(Error::InvalidBootstrapToken);
|
return Err(Error::InvalidBootstrapToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
register_key(&self.conn.db, &pubkey).await?;
|
match token_ok {
|
||||||
|
true => {
|
||||||
self.transport
|
register_key(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
|
||||||
.send(Ok(Outbound::AuthSuccess))
|
self.transport
|
||||||
.await
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
.map_err(|_| Error::Transport)?;
|
.await
|
||||||
|
.map_err(|_| Error::Transport)?;
|
||||||
Ok(pubkey)
|
Ok(pubkey)
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
error!("Invalid bootstrap token provided");
|
||||||
|
self.transport
|
||||||
|
.send(Err(Error::InvalidBootstrapToken))
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Transport)?;
|
||||||
|
Err(Error::InvalidBootstrapToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
@@ -210,13 +326,21 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if valid {
|
match valid {
|
||||||
self.transport
|
true => {
|
||||||
.send(Ok(Outbound::AuthSuccess))
|
self.transport
|
||||||
.await
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
.map_err(|_| Error::Transport)?;
|
.await
|
||||||
|
.map_err(|_| Error::Transport)?;
|
||||||
|
Ok(key.clone())
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
self.transport
|
||||||
|
.send(Err(Error::InvalidChallengeSolution))
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Transport)?;
|
||||||
|
Err(Error::InvalidChallengeSolution)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(key.clone())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, client::ClientProfile},
|
actors::{GlobalActors, client::ClientProfile},
|
||||||
|
crypto::integrity::Integrable,
|
||||||
db::{self, models::KeyType},
|
db::{self, models::KeyType},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,6 +14,16 @@ pub enum AuthPublicKey {
|
|||||||
Rsa(rsa::RsaPublicKey),
|
Rsa(rsa::RsaPublicKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UserAgentCredentials {
|
||||||
|
pub pubkey: AuthPublicKey,
|
||||||
|
pub nonce: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Integrable for UserAgentCredentials {
|
||||||
|
const KIND: &'static str = "useragent_credentials";
|
||||||
|
}
|
||||||
|
|
||||||
impl AuthPublicKey {
|
impl AuthPublicKey {
|
||||||
/// Canonical bytes stored in DB and echoed back in the challenge.
|
/// Canonical bytes stored in DB and echoed back in the challenge.
|
||||||
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
|
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
|
||||||
@@ -92,3 +103,18 @@ pub mod session;
|
|||||||
|
|
||||||
pub use auth::authenticate;
|
pub use auth::authenticate;
|
||||||
pub use session::UserAgentSession;
|
pub use session::UserAgentSession;
|
||||||
|
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
impl Hashable for AuthPublicKey {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.to_stored_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for UserAgentCredentials {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.pubkey.hash(hasher);
|
||||||
|
self.nonce.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,40 +1,97 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use alloy::primitives::Address;
|
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
use diesel::sql_types::ops::Add;
|
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
|
||||||
use diesel::{BoolExpressionMethods as _, ExpressionMethods as _, QueryDsl as _, SelectableHelper};
|
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use kameo::error::SendError;
|
use kameo::error::SendError;
|
||||||
|
use kameo::messages;
|
||||||
use kameo::prelude::Context;
|
use kameo::prelude::Context;
|
||||||
use kameo::{message, messages};
|
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
|
||||||
use crate::actors::keyholder::KeyHolderState;
|
use crate::actors::keyholder::KeyHolderState;
|
||||||
use crate::actors::user_agent::session::Error;
|
use crate::actors::user_agent::session::Error;
|
||||||
use crate::db::models::{
|
use crate::db::models::{
|
||||||
CoreEvmWalletAccess, EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
||||||
};
|
};
|
||||||
use crate::db::schema::evm_wallet_access;
|
|
||||||
use crate::evm::policies::{Grant, SpecificGrant};
|
use crate::evm::policies::{Grant, SpecificGrant};
|
||||||
use crate::safe_cell::SafeCell;
|
use crate::safe_cell::SafeCell;
|
||||||
|
use crate::{
|
||||||
|
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
|
||||||
|
crypto::integrity::{self, Verified},
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
evm::{
|
evm::{
|
||||||
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
|
||||||
|
UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
||||||
},
|
},
|
||||||
keyholder::{self, Bootstrap, TryUnseal},
|
keyholder::{self, Bootstrap, TryUnseal},
|
||||||
user_agent::session::{
|
user_agent::session::{
|
||||||
UserAgentSession,
|
UserAgentSession,
|
||||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||||
},
|
},
|
||||||
|
user_agent::{AuthPublicKey, UserAgentCredentials},
|
||||||
},
|
},
|
||||||
|
db::schema::useragent_client,
|
||||||
safe_cell::SafeCellHandle as _,
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn is_vault_sealed_from_evm<M>(err: &SendError<M, crate::actors::evm::Error>) -> bool {
|
||||||
|
matches!(
|
||||||
|
err,
|
||||||
|
SendError::HandlerError(crate::actors::evm::Error::Keyholder(
|
||||||
|
keyholder::Error::NotBootstrapped
|
||||||
|
)) | SendError::HandlerError(crate::actors::evm::Error::Integrity(
|
||||||
|
crate::crypto::integrity::Error::Keyholder(keyholder::Error::NotBootstrapped)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
|
async fn backfill_useragent_integrity(&self) -> Result<(), Error> {
|
||||||
|
let mut conn = self.props.db.get().await?;
|
||||||
|
let keyholder = self.props.actors.key_holder.clone();
|
||||||
|
|
||||||
|
conn.transaction(|conn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let rows: Vec<(i32, i32, Vec<u8>, crate::db::models::KeyType)> =
|
||||||
|
useragent_client::table
|
||||||
|
.select((
|
||||||
|
useragent_client::id,
|
||||||
|
useragent_client::nonce,
|
||||||
|
useragent_client::public_key,
|
||||||
|
useragent_client::key_type,
|
||||||
|
))
|
||||||
|
.load(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for (id, nonce, public_key, key_type) in rows {
|
||||||
|
let pubkey = AuthPublicKey::try_from((key_type, public_key)).map_err(|e| {
|
||||||
|
Error::internal(format!("Invalid user-agent key in db: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
integrity::sign_entity(
|
||||||
|
conn,
|
||||||
|
&keyholder,
|
||||||
|
&UserAgentCredentials { pubkey, nonce },
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::internal(format!("Failed to backfill user-agent integrity: {e}"))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result::<_, Error>::Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
||||||
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
||||||
error!("Received encrypted key in invalid state");
|
error!("Received encrypted key in invalid state");
|
||||||
@@ -112,6 +169,24 @@ pub enum BootstrapError {
|
|||||||
General(#[from] super::Error),
|
General(#[from] super::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SignTransactionError {
|
||||||
|
#[error("Policy evaluation failed")]
|
||||||
|
Vet(#[from] crate::evm::VetError),
|
||||||
|
|
||||||
|
#[error("Internal signing error")]
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GrantMutationError {
|
||||||
|
#[error("Vault is sealed")]
|
||||||
|
VaultSealed,
|
||||||
|
|
||||||
|
#[error("Internal grant mutation error")]
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
#[message]
|
#[message]
|
||||||
@@ -174,6 +249,7 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
self.backfill_useragent_integrity().await?;
|
||||||
info!("Successfully unsealed key with client-provided key");
|
info!("Successfully unsealed key with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -235,6 +311,7 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
self.backfill_useragent_integrity().await?;
|
||||||
info!("Successfully bootstrapped vault with client-provided key");
|
info!("Successfully bootstrapped vault with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -308,12 +385,15 @@ impl UserAgentSession {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
#[message]
|
#[message]
|
||||||
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
pub(crate) async fn handle_grant_list(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Vec<Grant<SpecificGrant>>, GrantMutationError> {
|
||||||
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
||||||
Ok(grants) => Ok(grants),
|
Ok(grants) => Ok(grants),
|
||||||
|
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant list failed");
|
error!(?err, "EVM grant list failed");
|
||||||
Err(Error::internal("Failed to list EVM grants"))
|
Err(GrantMutationError::Internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,7 +403,7 @@ impl UserAgentSession {
|
|||||||
&mut self,
|
&mut self,
|
||||||
basic: crate::evm::policies::SharedGrantSettings,
|
basic: crate::evm::policies::SharedGrantSettings,
|
||||||
grant: crate::evm::policies::SpecificGrant,
|
grant: crate::evm::policies::SpecificGrant,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<Verified<i32>, GrantMutationError> {
|
||||||
match self
|
match self
|
||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
@@ -332,26 +412,62 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(grant_id) => Ok(grant_id),
|
Ok(grant_id) => Ok(grant_id),
|
||||||
|
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant create failed");
|
error!(?err, "EVM grant create failed");
|
||||||
Err(Error::internal("Failed to create EVM grant"))
|
Err(GrantMutationError::Internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub(crate) async fn handle_grant_delete(&mut self, grant_id: i32) -> Result<(), Error> {
|
pub(crate) async fn handle_grant_delete(
|
||||||
|
&mut self,
|
||||||
|
grant_id: i32,
|
||||||
|
) -> Result<(), GrantMutationError> {
|
||||||
match self
|
match self
|
||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
.evm
|
.evm
|
||||||
.ask(UseragentDeleteGrant { grant_id })
|
.ask(UseragentDeleteGrant {
|
||||||
|
_grant_id: grant_id,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant delete failed");
|
error!(?err, "EVM grant delete failed");
|
||||||
Err(Error::internal("Failed to delete EVM grant"))
|
Err(GrantMutationError::Internal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub(crate) async fn handle_sign_transaction(
|
||||||
|
&mut self,
|
||||||
|
client_id: i32,
|
||||||
|
wallet_address: Address,
|
||||||
|
transaction: TxEip1559,
|
||||||
|
) -> Result<Signature, SignTransactionError> {
|
||||||
|
match self
|
||||||
|
.props
|
||||||
|
.actors
|
||||||
|
.evm
|
||||||
|
.ask(ClientSignTransaction {
|
||||||
|
client_id,
|
||||||
|
wallet_address,
|
||||||
|
transaction,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(signature) => Ok(signature),
|
||||||
|
Err(SendError::HandlerError(EvmSignError::Vet(vet_error))) => {
|
||||||
|
Err(SignTransactionError::Vet(vet_error))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, "EVM sign transaction failed in user-agent session");
|
||||||
|
Err(SignTransactionError::Internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use miette::Diagnostic;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -11,30 +10,24 @@ use crate::{
|
|||||||
|
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[error("Database setup failed: {0}")]
|
#[error("Database setup failed: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::init::database_setup))]
|
|
||||||
DatabaseSetup(#[from] db::DatabaseSetupError),
|
DatabaseSetup(#[from] db::DatabaseSetupError),
|
||||||
|
|
||||||
#[error("Connection acquire failed: {0}")]
|
#[error("Connection acquire failed: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::init::database_pool))]
|
|
||||||
DatabasePool(#[from] db::PoolError),
|
DatabasePool(#[from] db::PoolError),
|
||||||
|
|
||||||
#[error("Database query error: {0}")]
|
#[error("Database query error: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::init::database_query))]
|
|
||||||
DatabaseQuery(#[from] diesel::result::Error),
|
DatabaseQuery(#[from] diesel::result::Error),
|
||||||
|
|
||||||
#[error("TLS initialization failed: {0}")]
|
#[error("TLS initialization failed: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::init::tls_init))]
|
|
||||||
Tls(#[from] tls::InitError),
|
Tls(#[from] tls::InitError),
|
||||||
|
|
||||||
#[error("Actor spawn failed: {0}")]
|
#[error("Actor spawn failed: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::init::actor_spawn))]
|
|
||||||
ActorSpawn(#[from] crate::actors::SpawnError),
|
ActorSpawn(#[from] crate::actors::SpawnError),
|
||||||
|
|
||||||
#[error("I/O Error: {0}")]
|
#[error("I/O Error: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::init::io))]
|
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{net::IpAddr, string::FromUtf8Error};
|
use std::{net::Ipv4Addr, string::FromUtf8Error};
|
||||||
|
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
|
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use miette::Diagnostic;
|
|
||||||
use pem::Pem;
|
use pem::Pem;
|
||||||
use rcgen::{
|
use rcgen::{
|
||||||
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
||||||
@@ -29,30 +29,24 @@ const ENCODE_CONFIG: pem::EncodeConfig = {
|
|||||||
pem::EncodeConfig::new().set_line_ending(line_ending)
|
pem::EncodeConfig::new().set_line_ending(line_ending)
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[error("Key generation error during TLS initialization: {0}")]
|
#[error("Key generation error during TLS initialization: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::tls_init::key_generation))]
|
|
||||||
KeyGeneration(#[from] rcgen::Error),
|
KeyGeneration(#[from] rcgen::Error),
|
||||||
|
|
||||||
#[error("Key invalid format: {0}")]
|
#[error("Key invalid format: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::tls_init::key_invalid_format))]
|
|
||||||
KeyInvalidFormat(#[from] FromUtf8Error),
|
KeyInvalidFormat(#[from] FromUtf8Error),
|
||||||
|
|
||||||
#[error("Key deserialization error: {0}")]
|
#[error("Key deserialization error: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::tls_init::key_deserialization))]
|
|
||||||
KeyDeserializationError(rcgen::Error),
|
KeyDeserializationError(rcgen::Error),
|
||||||
|
|
||||||
#[error("Database error during TLS initialization: {0}")]
|
#[error("Database error during TLS initialization: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::tls_init::database_error))]
|
|
||||||
DatabaseError(#[from] diesel::result::Error),
|
DatabaseError(#[from] diesel::result::Error),
|
||||||
|
|
||||||
#[error("Pem deserialization error during TLS initialization: {0}")]
|
#[error("Pem deserialization error during TLS initialization: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::tls_init::pem_deserialization))]
|
|
||||||
PemDeserializationError(#[from] rustls::pki_types::pem::Error),
|
PemDeserializationError(#[from] rustls::pki_types::pem::Error),
|
||||||
|
|
||||||
#[error("Database pool acquire error during TLS initialization: {0}")]
|
#[error("Database pool acquire error during TLS initialization: {0}")]
|
||||||
#[diagnostic(code(arbiter_server::tls_init::database_pool_acquire))]
|
|
||||||
DatabasePoolAcquire(#[from] db::PoolError),
|
DatabasePoolAcquire(#[from] db::PoolError),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,9 +110,7 @@ impl TlsCa {
|
|||||||
];
|
];
|
||||||
params
|
params
|
||||||
.subject_alt_names
|
.subject_alt_names
|
||||||
.push(SanType::IpAddress(IpAddr::from([
|
.push(SanType::IpAddress(Ipv4Addr::LOCALHOST.into()));
|
||||||
127, 0, 0, 1,
|
|
||||||
])));
|
|
||||||
|
|
||||||
let mut dn = DistinguishedName::new();
|
let mut dn = DistinguishedName::new();
|
||||||
dn.push(DnType::CommonName, "Arbiter Instance Leaf");
|
dn.push(DnType::CommonName, "Arbiter Instance Leaf");
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod v1;
|
||||||
|
|
||||||
|
pub use v1::*;
|
||||||
109
server/crates/arbiter-server/src/crypto/encryption/v1.rs
Normal file
109
server/crates/arbiter-server/src/crypto/encryption/v1.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
use argon2::password_hash::Salt as ArgonSalt;
|
||||||
|
|
||||||
|
use rand::{
|
||||||
|
Rng as _, SeedableRng,
|
||||||
|
rngs::{StdRng, SysRng},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
|
||||||
|
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
|
||||||
|
|
||||||
|
pub const NONCE_LENGTH: usize = 24;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Nonce(pub [u8; NONCE_LENGTH]);
|
||||||
|
impl Nonce {
|
||||||
|
pub fn increment(&mut self) {
|
||||||
|
for i in (0..self.0.len()).rev() {
|
||||||
|
if self.0[i] == 0xFF {
|
||||||
|
self.0[i] = 0;
|
||||||
|
} else {
|
||||||
|
self.0[i] += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vec(&self) -> Vec<u8> {
|
||||||
|
self.0.to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> TryFrom<&'a [u8]> for Nonce {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != NONCE_LENGTH {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let mut nonce = [0u8; NONCE_LENGTH];
|
||||||
|
nonce.copy_from_slice(value);
|
||||||
|
Ok(Self(nonce))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH];
|
||||||
|
|
||||||
|
pub fn generate_salt() -> Salt {
|
||||||
|
let mut salt = Salt::default();
|
||||||
|
#[allow(
|
||||||
|
clippy::unwrap_used,
|
||||||
|
reason = "Rng failure is unrecoverable and should panic"
|
||||||
|
)]
|
||||||
|
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
|
||||||
|
rng.fill_bytes(&mut salt);
|
||||||
|
salt
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::ops::Deref as _;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
crypto::derive_key,
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn derive_seal_key_deterministic() {
|
||||||
|
static PASSWORD: &[u8] = b"password";
|
||||||
|
let password = SafeCell::new(PASSWORD.to_vec());
|
||||||
|
let password2 = SafeCell::new(PASSWORD.to_vec());
|
||||||
|
let salt = generate_salt();
|
||||||
|
|
||||||
|
let mut key1 = derive_key(password, &salt);
|
||||||
|
let mut key2 = derive_key(password2, &salt);
|
||||||
|
|
||||||
|
let key1_reader = key1.0.read();
|
||||||
|
let key2_reader = key2.0.read();
|
||||||
|
|
||||||
|
assert_eq!(key1_reader.deref(), key2_reader.deref());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn successful_derive() {
|
||||||
|
static PASSWORD: &[u8] = b"password";
|
||||||
|
let password = SafeCell::new(PASSWORD.to_vec());
|
||||||
|
let salt = generate_salt();
|
||||||
|
|
||||||
|
let mut key = derive_key(password, &salt);
|
||||||
|
let key_reader = key.0.read();
|
||||||
|
let key_ref = key_reader.deref();
|
||||||
|
|
||||||
|
assert_ne!(key_ref.as_slice(), &[0u8; 32][..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// We should fuzz this
|
||||||
|
pub fn test_nonce_increment() {
|
||||||
|
let mut nonce = Nonce([0u8; NONCE_LENGTH]);
|
||||||
|
nonce.increment();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
nonce.0,
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
server/crates/arbiter-server/src/crypto/integrity/mod.rs
Normal file
3
server/crates/arbiter-server/src/crypto/integrity/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod v1;
|
||||||
|
|
||||||
|
pub use v1::*;
|
||||||
681
server/crates/arbiter-server/src/crypto/integrity/v1.rs
Normal file
681
server/crates/arbiter-server/src/crypto/integrity/v1.rs
Normal file
@@ -0,0 +1,681 @@
|
|||||||
|
use crate::actors::keyholder;
|
||||||
|
use hmac::Hmac;
|
||||||
|
use sha2::Sha256;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
|
||||||
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
use kameo::{actor::ActorRef, error::SendError};
|
||||||
|
use sha2::Digest as _;
|
||||||
|
|
||||||
|
pub mod hashing;
|
||||||
|
use self::hashing::Hashable;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity},
|
||||||
|
db::{
|
||||||
|
self,
|
||||||
|
models::{IntegrityEnvelope as IntegrityEnvelopeRow, NewIntegrityEnvelope},
|
||||||
|
schema::integrity_envelope,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Database(#[from] db::DatabaseError),
|
||||||
|
|
||||||
|
#[error("KeyHolder error: {0}")]
|
||||||
|
Keyholder(#[from] keyholder::Error),
|
||||||
|
|
||||||
|
#[error("KeyHolder mailbox error")]
|
||||||
|
KeyholderSend,
|
||||||
|
|
||||||
|
#[error("Integrity envelope is missing for entity {entity_kind}")]
|
||||||
|
MissingEnvelope { entity_kind: &'static str },
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"Integrity payload version mismatch for entity {entity_kind}: expected {expected}, found {found}"
|
||||||
|
)]
|
||||||
|
PayloadVersionMismatch {
|
||||||
|
entity_kind: &'static str,
|
||||||
|
expected: i32,
|
||||||
|
found: i32,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Integrity MAC mismatch for entity {entity_kind}")]
|
||||||
|
MacMismatch { entity_kind: &'static str },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[must_use]
|
||||||
|
pub enum AttestationStatus {
|
||||||
|
Attested,
|
||||||
|
Unavailable,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Verified<T>(T);
|
||||||
|
|
||||||
|
impl<T> AsRef<T> for Verified<T> {
|
||||||
|
fn as_ref(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Verified<T> {
|
||||||
|
pub fn into_inner(self) -> T {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for Verified<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CURRENT_PAYLOAD_VERSION: i32 = 1;
|
||||||
|
pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
||||||
|
|
||||||
|
pub type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
|
pub trait Integrable: Hashable {
|
||||||
|
const KIND: &'static str;
|
||||||
|
const VERSION: i32 = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
payload.hash(&mut hasher);
|
||||||
|
hasher.finalize().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||||
|
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
||||||
|
out.extend_from_slice(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_mac_input(
|
||||||
|
entity_kind: &str,
|
||||||
|
entity_id: &[u8],
|
||||||
|
payload_version: i32,
|
||||||
|
payload_hash: &[u8; 32],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(8 + entity_kind.len() + entity_id.len() + 32);
|
||||||
|
push_len_prefixed(&mut out, entity_kind.as_bytes());
|
||||||
|
push_len_prefixed(&mut out, entity_id);
|
||||||
|
out.extend_from_slice(&payload_version.to_be_bytes());
|
||||||
|
out.extend_from_slice(payload_hash);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EntityId(Vec<u8>);
|
||||||
|
|
||||||
|
impl Deref for EntityId {
|
||||||
|
type Target = [u8];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for EntityId {
|
||||||
|
fn from(value: i32) -> Self {
|
||||||
|
Self(value.to_be_bytes().to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'_ [u8]> for EntityId {
|
||||||
|
fn from(bytes: &'_ [u8]) -> Self {
|
||||||
|
Self(bytes.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lookup_verified<E, C, F, Fut>(
|
||||||
|
conn: &mut C,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
entity_id: impl Into<EntityId>,
|
||||||
|
load: F,
|
||||||
|
) -> Result<Verified<E>, Error>
|
||||||
|
where
|
||||||
|
C: AsyncConnection<Backend = Sqlite>,
|
||||||
|
E: Integrable,
|
||||||
|
F: FnOnce(&mut C) -> Fut,
|
||||||
|
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
||||||
|
{
|
||||||
|
let entity = load(conn).await?;
|
||||||
|
verify_entity(conn, keyholder, &entity, entity_id).await?;
|
||||||
|
Ok(Verified(entity))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lookup_verified_allow_unavailable<E, C, F, Fut>(
|
||||||
|
conn: &mut C,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
entity_id: impl Into<EntityId>,
|
||||||
|
load: F,
|
||||||
|
) -> Result<Verified<E>, Error>
|
||||||
|
where
|
||||||
|
C: AsyncConnection<Backend = Sqlite>,
|
||||||
|
E: Integrable+ 'static,
|
||||||
|
F: FnOnce(&mut C) -> Fut,
|
||||||
|
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
||||||
|
{
|
||||||
|
let entity = load(conn).await?;
|
||||||
|
match check_entity_attestation(conn, keyholder, &entity, entity_id.into()).await? {
|
||||||
|
// IMPORTANT: allow_unavailable mode must succeed with an unattested result when vault key
|
||||||
|
// material is unavailable, otherwise integrity checks can be silently bypassed while sealed.
|
||||||
|
AttestationStatus::Attested | AttestationStatus::Unavailable => Ok(Verified(entity)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lookup_verified_from_query<E, Id, C, F>(
|
||||||
|
conn: &mut C,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
load: F,
|
||||||
|
) -> Result<Verified<E>, Error>
|
||||||
|
where
|
||||||
|
C: AsyncConnection<Backend = Sqlite> + Send,
|
||||||
|
E: Integrable,
|
||||||
|
Id: Into<EntityId>,
|
||||||
|
F: for<'a> FnOnce(
|
||||||
|
&'a mut C,
|
||||||
|
) -> Pin<
|
||||||
|
Box<dyn Future<Output = Result<(Id, E), db::DatabaseError>> + Send + 'a>,
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
let (entity_id, entity) = load(conn).await?;
|
||||||
|
verify_entity(conn, keyholder, &entity, entity_id).await?;
|
||||||
|
Ok(Verified(entity))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
||||||
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
entity: &E,
|
||||||
|
as_entity_id: Id,
|
||||||
|
) -> Result<Verified<Id>, Error> {
|
||||||
|
let payload_hash = payload_hash(entity);
|
||||||
|
|
||||||
|
let entity_id = as_entity_id.clone().into();
|
||||||
|
|
||||||
|
let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash);
|
||||||
|
|
||||||
|
let (key_version, mac) = keyholder
|
||||||
|
.ask(SignIntegrity { mac_input })
|
||||||
|
.await
|
||||||
|
.map_err(|err| match err {
|
||||||
|
kameo::error::SendError::HandlerError(inner) => Error::Keyholder(inner),
|
||||||
|
_ => Error::KeyholderSend,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
insert_into(integrity_envelope::table)
|
||||||
|
.values(NewIntegrityEnvelope {
|
||||||
|
entity_kind: E::KIND.to_owned(),
|
||||||
|
entity_id: entity_id.to_vec(),
|
||||||
|
payload_version: E::VERSION,
|
||||||
|
key_version,
|
||||||
|
mac: mac.to_vec(),
|
||||||
|
})
|
||||||
|
.on_conflict((
|
||||||
|
integrity_envelope::entity_id,
|
||||||
|
integrity_envelope::entity_kind,
|
||||||
|
))
|
||||||
|
.do_update()
|
||||||
|
.set((
|
||||||
|
integrity_envelope::payload_version.eq(E::VERSION),
|
||||||
|
integrity_envelope::key_version.eq(key_version),
|
||||||
|
integrity_envelope::mac.eq(mac),
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(db::DatabaseError::from)?;
|
||||||
|
|
||||||
|
Ok(Verified(as_entity_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_entity_attestation<E: Integrable>(
|
||||||
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
entity: &E,
|
||||||
|
entity_id: impl Into<EntityId>,
|
||||||
|
) -> Result<AttestationStatus, Error> {
|
||||||
|
let entity_id = entity_id.into();
|
||||||
|
let envelope: IntegrityEnvelopeRow = integrity_envelope::table
|
||||||
|
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
||||||
|
.filter(integrity_envelope::entity_id.eq(&*entity_id))
|
||||||
|
.first(conn)
|
||||||
|
.await
|
||||||
|
.map_err(|err| match err {
|
||||||
|
diesel::result::Error::NotFound => Error::MissingEnvelope {
|
||||||
|
entity_kind: E::KIND,
|
||||||
|
},
|
||||||
|
other => Error::Database(db::DatabaseError::from(other)),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if envelope.payload_version != E::VERSION {
|
||||||
|
return Err(Error::PayloadVersionMismatch {
|
||||||
|
entity_kind: E::KIND,
|
||||||
|
expected: E::VERSION,
|
||||||
|
found: envelope.payload_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload_hash = payload_hash(entity);
|
||||||
|
let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash);
|
||||||
|
|
||||||
|
let result = keyholder
|
||||||
|
.ask(VerifyIntegrity {
|
||||||
|
mac_input,
|
||||||
|
expected_mac: envelope.mac,
|
||||||
|
key_version: envelope.key_version,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(true) => Ok(AttestationStatus::Attested),
|
||||||
|
Ok(false) => Err(Error::MacMismatch {
|
||||||
|
entity_kind: E::KIND,
|
||||||
|
}),
|
||||||
|
Err(SendError::HandlerError(keyholder::Error::NotBootstrapped)) => {
|
||||||
|
Ok(AttestationStatus::Unavailable)
|
||||||
|
}
|
||||||
|
Err(_) => Err(Error::KeyholderSend),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn verify_entity<'a, E: Integrable>(
|
||||||
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
entity: &'a E,
|
||||||
|
entity_id: impl Into<EntityId>,
|
||||||
|
) -> Result<Verified<&'a E>, Error> {
|
||||||
|
match check_entity_attestation::<E>(conn, keyholder, entity, entity_id).await? {
|
||||||
|
AttestationStatus::Attested => Ok(Verified(entity)),
|
||||||
|
AttestationStatus::Unavailable => Err(Error::Keyholder(keyholder::Error::NotBootstrapped)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_envelope<E: Integrable>(
|
||||||
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
|
entity_id: impl Into<EntityId>,
|
||||||
|
) -> Result<usize, Error> {
|
||||||
|
let entity_id = entity_id.into();
|
||||||
|
|
||||||
|
let affected = diesel::delete(
|
||||||
|
integrity_envelope::table
|
||||||
|
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
||||||
|
.filter(integrity_envelope::entity_id.eq(&*entity_id)),
|
||||||
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(db::DatabaseError::from)?;
|
||||||
|
|
||||||
|
Ok(affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::keyholder::{Bootstrap, KeyHolder},
|
||||||
|
db::{self, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::hashing::Hashable;
|
||||||
|
use super::{
|
||||||
|
check_entity_attestation, AttestationStatus, Error, Integrable, lookup_verified,
|
||||||
|
lookup_verified_allow_unavailable, lookup_verified_from_query, sign_entity, verify_entity,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct DummyEntity {
|
||||||
|
payload_version: i32,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for DummyEntity {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
self.payload_version.hash(hasher);
|
||||||
|
self.payload.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Integrable for DummyEntity {
|
||||||
|
const KIND: &'static str = "dummy_entity";
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef<KeyHolder> {
|
||||||
|
let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
actor
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
actor
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sign_writes_envelope_and_verify_passes() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-7";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let count: i64 = schema::integrity_envelope::table
|
||||||
|
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||||
|
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(count, 1, "envelope row must be created exactly once");
|
||||||
|
let _ = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tampered_mac_fails_verification() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-11";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
diesel::update(schema::integrity_envelope::table)
|
||||||
|
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||||
|
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||||
|
.set(schema::integrity_envelope::mac.eq(vec![0u8; 32]))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn changed_payload_fails_verification() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-21";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tampered = DummyEntity {
|
||||||
|
payload: b"payload-v1-but-tampered".to_vec(),
|
||||||
|
..entity
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = check_entity_attestation(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn allow_unavailable_lookup_passes_while_sealed() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-31";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
drop(keyholder);
|
||||||
|
|
||||||
|
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
let status = check_entity_attestation(&mut conn, &sealed_keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(status, AttestationStatus::Unavailable);
|
||||||
|
|
||||||
|
#[expect(clippy::disallowed_methods, reason = "test only")]
|
||||||
|
lookup_verified_allow_unavailable(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||||
|
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn strict_verify_fails_closed_while_sealed() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-41";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
drop(keyholder);
|
||||||
|
|
||||||
|
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
|
||||||
|
let err = verify_entity(&mut conn, &sealed_keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||||
|
));
|
||||||
|
|
||||||
|
let err = lookup_verified(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||||
|
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn lookup_verified_supports_loaded_aggregate() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: i32 = 77;
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| async {
|
||||||
|
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(verified.payload, b"payload-v1".to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn lookup_verified_allow_unavailable_works_while_sealed() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: i32 = 78;
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
drop(keyholder);
|
||||||
|
|
||||||
|
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
|
||||||
|
#[expect(clippy::disallowed_methods, reason = "test only")]
|
||||||
|
lookup_verified_allow_unavailable(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||||
|
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn extension_trait_lookup_verified_required_works() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: i32 = 79;
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| {
|
||||||
|
Box::pin(async {
|
||||||
|
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(verified.payload, b"payload-v1".to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn lookup_verified_from_query_helpers_work() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: i32 = 80;
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let verified = lookup_verified_from_query(&mut conn, &keyholder, |_| {
|
||||||
|
Box::pin(async {
|
||||||
|
Ok::<_, db::DatabaseError>((
|
||||||
|
ENTITY_ID,
|
||||||
|
DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(verified.payload, b"payload-v1".to_vec());
|
||||||
|
|
||||||
|
drop(keyholder);
|
||||||
|
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
|
||||||
|
let err = lookup_verified_from_query(&mut conn, &sealed_keyholder, |_| {
|
||||||
|
Box::pin(async {
|
||||||
|
Ok::<_, db::DatabaseError>((
|
||||||
|
ENTITY_ID,
|
||||||
|
DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
107
server/crates/arbiter-server/src/crypto/integrity/v1/hashing.rs
Normal file
107
server/crates/arbiter-server/src/crypto/integrity/v1/hashing.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use hmac::digest::Digest;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
/// Deterministically hash a value by feeding its fields into the hasher in a consistent order.
|
||||||
|
pub trait Hashable {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_numeric {
|
||||||
|
($($t:ty),*) => {
|
||||||
|
$(
|
||||||
|
impl Hashable for $t {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(&self.to_be_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_numeric!(u8, u16, u32, u64, i8, i16, i32, i64);
|
||||||
|
|
||||||
|
impl Hashable for &[u8] {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for String {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Hashable + PartialOrd> Hashable for Vec<T> {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
let ref_sorted = {
|
||||||
|
let mut sorted = self.iter().collect::<Vec<_>>();
|
||||||
|
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
sorted
|
||||||
|
};
|
||||||
|
for item in ref_sorted {
|
||||||
|
item.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Hashable + PartialOrd> Hashable for HashSet<T> {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
let ref_sorted = {
|
||||||
|
let mut sorted = self.iter().collect::<Vec<_>>();
|
||||||
|
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
sorted
|
||||||
|
};
|
||||||
|
for item in ref_sorted {
|
||||||
|
item.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Hashable> Hashable for Option<T> {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
match self {
|
||||||
|
Some(value) => {
|
||||||
|
hasher.update([1]);
|
||||||
|
value.hash(hasher);
|
||||||
|
}
|
||||||
|
None => hasher.update([0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Hashable> Hashable for Box<T> {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
self.as_ref().hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Hashable> Hashable for &T {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
(*self).hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for alloy::primitives::Address {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.as_slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for alloy::primitives::U256 {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.to_be_bytes::<32>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for chrono::Duration {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.num_seconds().to_be_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for chrono::DateTime<chrono::Utc> {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
hasher.update(self.timestamp_millis().to_be_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,21 @@
|
|||||||
use std::ops::Deref as _;
|
use std::ops::Deref as _;
|
||||||
|
|
||||||
use argon2::{Algorithm, Argon2, password_hash::Salt as ArgonSalt};
|
use argon2::{Algorithm, Argon2};
|
||||||
use chacha20poly1305::{
|
use chacha20poly1305::{
|
||||||
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
|
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
|
||||||
aead::{AeadMut, Error, Payload},
|
aead::{AeadMut, Error, Payload},
|
||||||
};
|
};
|
||||||
use rand::{
|
use rand::{
|
||||||
Rng as _, SeedableRng,
|
Rng as _, SeedableRng as _,
|
||||||
rngs::{StdRng, SysRng},
|
rngs::{StdRng, SysRng},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||||
|
|
||||||
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
|
pub mod encryption;
|
||||||
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
|
pub mod integrity;
|
||||||
|
|
||||||
pub const NONCE_LENGTH: usize = 24;
|
use encryption::v1::{Nonce, Salt};
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Nonce([u8; NONCE_LENGTH]);
|
|
||||||
impl Nonce {
|
|
||||||
pub fn increment(&mut self) {
|
|
||||||
for i in (0..self.0.len()).rev() {
|
|
||||||
if self.0[i] == 0xFF {
|
|
||||||
self.0[i] = 0;
|
|
||||||
} else {
|
|
||||||
self.0[i] += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_vec(&self) -> Vec<u8> {
|
|
||||||
self.0.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> TryFrom<&'a [u8]> for Nonce {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
|
||||||
if value.len() != NONCE_LENGTH {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
let mut nonce = [0u8; NONCE_LENGTH];
|
|
||||||
nonce.copy_from_slice(value);
|
|
||||||
Ok(Self(nonce))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct KeyCell(pub SafeCell<Key>);
|
pub struct KeyCell(pub SafeCell<Key>);
|
||||||
impl From<SafeCell<Key>> for KeyCell {
|
impl From<SafeCell<Key>> for KeyCell {
|
||||||
@@ -133,24 +102,21 @@ impl KeyCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH];
|
|
||||||
|
|
||||||
pub fn generate_salt() -> Salt {
|
|
||||||
let mut salt = Salt::default();
|
|
||||||
#[allow(
|
|
||||||
clippy::unwrap_used,
|
|
||||||
reason = "Rng failure is unrecoverable and should panic"
|
|
||||||
)]
|
|
||||||
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
|
|
||||||
rng.fill_bytes(&mut salt);
|
|
||||||
salt
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User password might be of different length, have not enough entropy, etc...
|
|
||||||
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
|
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
|
||||||
pub fn derive_seal_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
pub fn derive_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
||||||
|
let params = {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
argon2::Params::new(8, 1, 1, None).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
argon2::Params::new(262_144, 3, 4, None).unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
let params = argon2::Params::new(262_144, 3, 4, None).unwrap();
|
|
||||||
let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params);
|
let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params);
|
||||||
let mut key = SafeCell::new(Key::default());
|
let mut key = SafeCell::new(Key::default());
|
||||||
password.read_inline(|password_source| {
|
password.read_inline(|password_source| {
|
||||||
@@ -171,37 +137,11 @@ pub fn derive_seal_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::{
|
||||||
use crate::safe_cell::SafeCell;
|
derive_key,
|
||||||
|
encryption::v1::{Nonce, generate_salt},
|
||||||
#[test]
|
};
|
||||||
pub fn derive_seal_key_deterministic() {
|
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||||
static PASSWORD: &[u8] = b"password";
|
|
||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
|
||||||
let password2 = SafeCell::new(PASSWORD.to_vec());
|
|
||||||
let salt = generate_salt();
|
|
||||||
|
|
||||||
let mut key1 = derive_seal_key(password, &salt);
|
|
||||||
let mut key2 = derive_seal_key(password2, &salt);
|
|
||||||
|
|
||||||
let key1_reader = key1.0.read();
|
|
||||||
let key2_reader = key2.0.read();
|
|
||||||
|
|
||||||
assert_eq!(key1_reader.deref(), key2_reader.deref());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn successful_derive() {
|
|
||||||
static PASSWORD: &[u8] = b"password";
|
|
||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
|
||||||
let salt = generate_salt();
|
|
||||||
|
|
||||||
let mut key = derive_seal_key(password, &salt);
|
|
||||||
let key_reader = key.0.read();
|
|
||||||
let key_ref = key_reader.deref();
|
|
||||||
|
|
||||||
assert_ne!(key_ref.as_slice(), &[0u8; 32][..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn encrypt_decrypt() {
|
pub fn encrypt_decrypt() {
|
||||||
@@ -209,7 +149,7 @@ mod tests {
|
|||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
let password = SafeCell::new(PASSWORD.to_vec());
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
|
|
||||||
let mut key = derive_seal_key(password, &salt);
|
let mut key = derive_key(password, &salt);
|
||||||
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
||||||
let associated_data = b"associated data";
|
let associated_data = b"associated data";
|
||||||
let mut buffer = b"secret data".to_vec();
|
let mut buffer = b"secret data".to_vec();
|
||||||
@@ -226,18 +166,4 @@ mod tests {
|
|||||||
let buffer = buffer.read();
|
let buffer = buffer.read();
|
||||||
assert_eq!(*buffer, b"secret data");
|
assert_eq!(*buffer, b"secret data");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
// We should fuzz this
|
|
||||||
pub fn test_nonce_increment() {
|
|
||||||
let mut nonce = Nonce([0u8; NONCE_LENGTH]);
|
|
||||||
nonce.increment();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
nonce.0,
|
|
||||||
[
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ use diesel_async::{
|
|||||||
sync_connection_wrapper::SyncConnectionWrapper,
|
sync_connection_wrapper::SyncConnectionWrapper,
|
||||||
};
|
};
|
||||||
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
||||||
use miette::Diagnostic;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
@@ -21,26 +21,21 @@ static DB_FILE: &str = "arbiter.sqlite";
|
|||||||
|
|
||||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||||
|
|
||||||
#[derive(Error, Diagnostic, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DatabaseSetupError {
|
pub enum DatabaseSetupError {
|
||||||
#[error("Failed to determine home directory")]
|
#[error("Failed to determine home directory")]
|
||||||
#[diagnostic(code(arbiter::db::home_dir))]
|
|
||||||
HomeDir(std::io::Error),
|
HomeDir(std::io::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
#[diagnostic(code(arbiter::db::connection))]
|
|
||||||
Connection(diesel::ConnectionError),
|
Connection(diesel::ConnectionError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
#[diagnostic(code(arbiter::db::concurrency))]
|
|
||||||
ConcurrencySetup(diesel::result::Error),
|
ConcurrencySetup(diesel::result::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
#[diagnostic(code(arbiter::db::migration))]
|
|
||||||
Migration(Box<dyn std::error::Error + Send + Sync>),
|
Migration(Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
#[diagnostic(code(arbiter::db::pool))]
|
|
||||||
Pool(#[from] PoolInitError),
|
Pool(#[from] PoolInitError),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +133,7 @@ pub async fn create_pool(url: Option<&str>) -> Result<DatabasePool, DatabaseSetu
|
|||||||
Ok(pool)
|
Ok(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[mutants::skip]
|
||||||
pub async fn create_test_pool() -> DatabasePool {
|
pub async fn create_test_pool() -> DatabasePool {
|
||||||
use rand::distr::{Alphanumeric, SampleString as _};
|
use rand::distr::{Alphanumeric, SampleString as _};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::db::schema::{
|
|||||||
self, aead_encrypted, arbiter_settings, evm_basic_grant, evm_ether_transfer_grant,
|
self, aead_encrypted, arbiter_settings, evm_basic_grant, evm_ether_transfer_grant,
|
||||||
evm_ether_transfer_grant_target, evm_ether_transfer_limit, evm_token_transfer_grant,
|
evm_ether_transfer_grant_target, evm_ether_transfer_limit, evm_token_transfer_grant,
|
||||||
evm_token_transfer_log, evm_token_transfer_volume_limit, evm_transaction_log, evm_wallet,
|
evm_token_transfer_log, evm_token_transfer_volume_limit, evm_transaction_log, evm_wallet,
|
||||||
root_key_history, tls_history,
|
integrity_envelope, root_key_history, tls_history,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use diesel::{prelude::*, sqlite::Sqlite};
|
use diesel::{prelude::*, sqlite::Sqlite};
|
||||||
@@ -376,3 +376,22 @@ pub struct EvmTokenTransferLog {
|
|||||||
pub value: Vec<u8>,
|
pub value: Vec<u8>,
|
||||||
pub created_at: SqliteTimestamp,
|
pub created_at: SqliteTimestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Models, Queryable, Debug, Insertable, Selectable)]
|
||||||
|
#[diesel(table_name = integrity_envelope, check_for_backend(Sqlite))]
|
||||||
|
#[view(
|
||||||
|
NewIntegrityEnvelope,
|
||||||
|
derive(Insertable),
|
||||||
|
omit(id, signed_at, created_at),
|
||||||
|
attributes_with = "deriveless"
|
||||||
|
)]
|
||||||
|
pub struct IntegrityEnvelope {
|
||||||
|
pub id: i32,
|
||||||
|
pub entity_kind: String,
|
||||||
|
pub entity_id: Vec<u8>,
|
||||||
|
pub payload_version: i32,
|
||||||
|
pub key_version: i32,
|
||||||
|
pub mac: Vec<u8>,
|
||||||
|
pub signed_at: SqliteTimestamp,
|
||||||
|
pub created_at: SqliteTimestamp,
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,6 +139,19 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
integrity_envelope (id) {
|
||||||
|
id -> Integer,
|
||||||
|
entity_kind -> Text,
|
||||||
|
entity_id -> Binary,
|
||||||
|
payload_version -> Integer,
|
||||||
|
key_version -> Integer,
|
||||||
|
mac -> Binary,
|
||||||
|
signed_at -> Integer,
|
||||||
|
created_at -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
program_client (id) {
|
program_client (id) {
|
||||||
id -> Integer,
|
id -> Integer,
|
||||||
@@ -219,6 +232,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
evm_transaction_log,
|
evm_transaction_log,
|
||||||
evm_wallet,
|
evm_wallet,
|
||||||
evm_wallet_access,
|
evm_wallet_access,
|
||||||
|
integrity_envelope,
|
||||||
program_client,
|
program_client,
|
||||||
root_key_history,
|
root_key_history,
|
||||||
tls_history,
|
tls_history,
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ use alloy::{
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite};
|
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use tracing_subscriber::registry::Data;
|
use kameo::actor::ActorRef;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
actors::keyholder::KeyHolder,
|
||||||
|
crypto::integrity::{self, Verified},
|
||||||
db::{
|
db::{
|
||||||
self, DatabaseError,
|
self, DatabaseError,
|
||||||
models::{
|
models::{
|
||||||
@@ -19,8 +21,8 @@ use crate::{
|
|||||||
schema::{self, evm_transaction_log},
|
schema::{self, evm_transaction_log},
|
||||||
},
|
},
|
||||||
evm::policies::{
|
evm::policies::{
|
||||||
DatabaseID, EvalContext, EvalViolation, FullGrant, Grant, Policy, SharedGrantSettings,
|
CombinedSettings, DatabaseID, EvalContext, EvalViolation, Grant, Policy,
|
||||||
SpecificGrant, SpecificMeaning, ether_transfer::EtherTransfer,
|
SharedGrantSettings, SpecificGrant, SpecificMeaning, ether_transfer::EtherTransfer,
|
||||||
token_transfers::TokenTransfer,
|
token_transfers::TokenTransfer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -29,42 +31,47 @@ pub mod policies;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
/// Errors that can only occur once the transaction meaning is known (during policy evaluation)
|
/// Errors that can only occur once the transaction meaning is known (during policy evaluation)
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum PolicyError {
|
pub enum PolicyError {
|
||||||
#[error("Database error")]
|
#[error("Database error")]
|
||||||
Error(#[from] crate::db::DatabaseError),
|
Database(#[from] crate::db::DatabaseError),
|
||||||
#[error("Transaction violates policy: {0:?}")]
|
#[error("Transaction violates policy: {0:?}")]
|
||||||
#[diagnostic(code(arbiter_server::evm::policy_error::violation))]
|
|
||||||
Violations(Vec<EvalViolation>),
|
Violations(Vec<EvalViolation>),
|
||||||
#[error("No matching grant found")]
|
#[error("No matching grant found")]
|
||||||
#[diagnostic(code(arbiter_server::evm::policy_error::no_matching_grant))]
|
|
||||||
NoMatchingGrant,
|
NoMatchingGrant,
|
||||||
|
|
||||||
|
#[error("Integrity error: {0}")]
|
||||||
|
Integrity(#[from] integrity::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum VetError {
|
pub enum VetError {
|
||||||
#[error("Contract creation transactions are not supported")]
|
#[error("Contract creation transactions are not supported")]
|
||||||
#[diagnostic(code(arbiter_server::evm::vet_error::contract_creation_unsupported))]
|
|
||||||
ContractCreationNotSupported,
|
ContractCreationNotSupported,
|
||||||
#[error("Engine can't classify this transaction")]
|
#[error("Engine can't classify this transaction")]
|
||||||
#[diagnostic(code(arbiter_server::evm::vet_error::unsupported))]
|
|
||||||
UnsupportedTransactionType,
|
UnsupportedTransactionType,
|
||||||
#[error("Policy evaluation failed: {1}")]
|
#[error("Policy evaluation failed: {1}")]
|
||||||
#[diagnostic(code(arbiter_server::evm::vet_error::evaluated))]
|
|
||||||
Evaluated(SpecificMeaning, #[source] PolicyError),
|
Evaluated(SpecificMeaning, #[source] PolicyError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum AnalyzeError {
|
pub enum AnalyzeError {
|
||||||
#[error("Engine doesn't support granting permissions for contract creation")]
|
#[error("Engine doesn't support granting permissions for contract creation")]
|
||||||
#[diagnostic(code(arbiter_server::evm::analyze_error::contract_creation_not_supported))]
|
|
||||||
ContractCreationNotSupported,
|
ContractCreationNotSupported,
|
||||||
|
|
||||||
#[error("Unsupported transaction type")]
|
#[error("Unsupported transaction type")]
|
||||||
#[diagnostic(code(arbiter_server::evm::analyze_error::unsupported_transaction_type))]
|
|
||||||
UnsupportedTransactionType,
|
UnsupportedTransactionType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ListError {
|
||||||
|
#[error("Database error")]
|
||||||
|
Database(#[from] crate::db::DatabaseError),
|
||||||
|
|
||||||
|
#[error("Integrity verification failed for grant")]
|
||||||
|
Integrity(#[from] integrity::Error),
|
||||||
|
}
|
||||||
|
|
||||||
/// Controls whether a transaction should be executed or only validated
|
/// Controls whether a transaction should be executed or only validated
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum RunKind {
|
pub enum RunKind {
|
||||||
@@ -83,6 +90,14 @@ async fn check_shared_constraints(
|
|||||||
let mut violations = Vec::new();
|
let mut violations = Vec::new();
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
|
if shared.chain != context.chain {
|
||||||
|
violations.push(EvalViolation::MismatchingChainId {
|
||||||
|
expected: shared.chain,
|
||||||
|
actual: context.chain,
|
||||||
|
});
|
||||||
|
return Ok(violations);
|
||||||
|
}
|
||||||
|
|
||||||
// Validity window
|
// Validity window
|
||||||
if shared.valid_from.is_some_and(|t| now < t) || shared.valid_until.is_some_and(|t| now > t) {
|
if shared.valid_from.is_some_and(|t| now < t) || shared.valid_until.is_some_and(|t| now > t) {
|
||||||
violations.push(EvalViolation::InvalidTime);
|
violations.push(EvalViolation::InvalidTime);
|
||||||
@@ -123,6 +138,7 @@ async fn check_shared_constraints(
|
|||||||
// Supporting only EIP-1559 transactions for now, but we can easily extend this to support legacy transactions if needed
|
// Supporting only EIP-1559 transactions for now, but we can easily extend this to support legacy transactions if needed
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
db: db::DatabasePool,
|
db: db::DatabasePool,
|
||||||
|
keyholder: ActorRef<KeyHolder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
@@ -131,18 +147,51 @@ impl Engine {
|
|||||||
context: EvalContext,
|
context: EvalContext,
|
||||||
meaning: &P::Meaning,
|
meaning: &P::Meaning,
|
||||||
run_kind: RunKind,
|
run_kind: RunKind,
|
||||||
) -> Result<(), PolicyError> {
|
) -> Result<(), PolicyError>
|
||||||
|
where
|
||||||
|
P::Settings: Clone,
|
||||||
|
{
|
||||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let grant = P::try_find_grant(&context, &mut conn)
|
let verified_settings =
|
||||||
|
match integrity::lookup_verified_from_query(&mut conn, &self.keyholder, |conn| {
|
||||||
|
let context = context.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
let grant = P::try_find_grant(&context, conn)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::from)?
|
||||||
|
.ok_or_else(|| DatabaseError::from(diesel::result::Error::NotFound))?;
|
||||||
|
|
||||||
|
Ok::<_, DatabaseError>((grant.common_settings_id, grant.settings))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(verified) => verified,
|
||||||
|
Err(integrity::Error::Database(DatabaseError::Connection(
|
||||||
|
diesel::result::Error::NotFound,
|
||||||
|
))) => return Err(PolicyError::NoMatchingGrant),
|
||||||
|
Err(err) => return Err(PolicyError::Integrity(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut grant = P::try_find_grant(&context, &mut conn)
|
||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||||
|
|
||||||
let mut violations =
|
// IMPORTANT: policy evaluation uses extra non-integrity fields from Grant
|
||||||
check_shared_constraints(&context, &grant.shared, grant.shared_grant_id, &mut conn)
|
// (e.g., per-policy ids), so we currently reload Grant after the query-native
|
||||||
.await
|
// integrity check over canonicalized settings.
|
||||||
.map_err(DatabaseError::from)?;
|
grant.settings = verified_settings.into_inner();
|
||||||
|
|
||||||
|
let mut violations = check_shared_constraints(
|
||||||
|
&context,
|
||||||
|
&grant.settings.shared,
|
||||||
|
grant.common_settings_id,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::from)?;
|
||||||
violations.extend(
|
violations.extend(
|
||||||
P::evaluate(&context, meaning, &grant, &mut conn)
|
P::evaluate(&context, meaning, &grant, &mut conn)
|
||||||
.await
|
.await
|
||||||
@@ -151,12 +200,14 @@ impl Engine {
|
|||||||
|
|
||||||
if !violations.is_empty() {
|
if !violations.is_empty() {
|
||||||
return Err(PolicyError::Violations(violations));
|
return Err(PolicyError::Violations(violations));
|
||||||
} else if run_kind == RunKind::Execution {
|
}
|
||||||
|
|
||||||
|
if run_kind == RunKind::Execution {
|
||||||
conn.transaction(|conn| {
|
conn.transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let log_id: i32 = insert_into(evm_transaction_log::table)
|
let log_id: i32 = insert_into(evm_transaction_log::table)
|
||||||
.values(&NewEvmTransactionLog {
|
.values(&NewEvmTransactionLog {
|
||||||
grant_id: grant.shared_grant_id,
|
grant_id: grant.common_settings_id,
|
||||||
wallet_access_id: context.target.id,
|
wallet_access_id: context.target.id,
|
||||||
chain_id: context.chain as i32,
|
chain_id: context.chain as i32,
|
||||||
eth_value: utils::u256_to_bytes(context.value).to_vec(),
|
eth_value: utils::u256_to_bytes(context.value).to_vec(),
|
||||||
@@ -180,15 +231,19 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub fn new(db: db::DatabasePool) -> Self {
|
pub fn new(db: db::DatabasePool, keyholder: ActorRef<KeyHolder>) -> Self {
|
||||||
Self { db }
|
Self { db, keyholder }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_grant<P: Policy>(
|
pub async fn create_grant<P: Policy>(
|
||||||
&self,
|
&self,
|
||||||
full_grant: FullGrant<P::Settings>,
|
full_grant: CombinedSettings<P::Settings>,
|
||||||
) -> Result<i32, DatabaseError> {
|
) -> Result<Verified<i32>, DatabaseError>
|
||||||
|
where
|
||||||
|
P::Settings: Clone,
|
||||||
|
{
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await?;
|
||||||
|
let keyholder = self.keyholder.clone();
|
||||||
|
|
||||||
let id = conn
|
let id = conn
|
||||||
.transaction(|conn| {
|
.transaction(|conn| {
|
||||||
@@ -197,25 +252,25 @@ impl Engine {
|
|||||||
|
|
||||||
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
|
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
|
||||||
.values(&NewEvmBasicGrant {
|
.values(&NewEvmBasicGrant {
|
||||||
chain_id: full_grant.basic.chain as i32,
|
chain_id: full_grant.shared.chain as i32,
|
||||||
wallet_access_id: full_grant.basic.wallet_access_id,
|
wallet_access_id: full_grant.shared.wallet_access_id,
|
||||||
valid_from: full_grant.basic.valid_from.map(SqliteTimestamp),
|
valid_from: full_grant.shared.valid_from.map(SqliteTimestamp),
|
||||||
valid_until: full_grant.basic.valid_until.map(SqliteTimestamp),
|
valid_until: full_grant.shared.valid_until.map(SqliteTimestamp),
|
||||||
max_gas_fee_per_gas: full_grant
|
max_gas_fee_per_gas: full_grant
|
||||||
.basic
|
.shared
|
||||||
.max_gas_fee_per_gas
|
.max_gas_fee_per_gas
|
||||||
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
||||||
max_priority_fee_per_gas: full_grant
|
max_priority_fee_per_gas: full_grant
|
||||||
.basic
|
.shared
|
||||||
.max_priority_fee_per_gas
|
.max_priority_fee_per_gas
|
||||||
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
.map(|fee| utils::u256_to_bytes(fee).to_vec()),
|
||||||
rate_limit_count: full_grant
|
rate_limit_count: full_grant
|
||||||
.basic
|
.shared
|
||||||
.rate_limit
|
.rate_limit
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|rl| rl.count as i32),
|
.map(|rl| rl.count as i32),
|
||||||
rate_limit_window_secs: full_grant
|
rate_limit_window_secs: full_grant
|
||||||
.basic
|
.shared
|
||||||
.rate_limit
|
.rate_limit
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|rl| rl.window.num_seconds() as i32),
|
.map(|rl| rl.window.num_seconds() as i32),
|
||||||
@@ -225,7 +280,14 @@ impl Engine {
|
|||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
P::create_grant(&basic_grant, &full_grant.specific, conn).await
|
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
||||||
|
|
||||||
|
let verified_entity_id =
|
||||||
|
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||||
|
|
||||||
|
QueryResult::Ok(verified_entity_id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -233,33 +295,46 @@ impl Engine {
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, DatabaseError> {
|
async fn list_one_kind<Kind: Policy, Y>(
|
||||||
let mut conn = self.db.get().await?;
|
&self,
|
||||||
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
|
) -> Result<Vec<Grant<Y>>, ListError>
|
||||||
|
where
|
||||||
|
Y: From<Kind::Settings>,
|
||||||
|
{
|
||||||
|
let all_grants = Kind::find_all_grants(conn)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
|
let mut verified_grants = Vec::with_capacity(all_grants.len());
|
||||||
|
|
||||||
|
// Verify integrity of all grants before returning any results.
|
||||||
|
for grant in all_grants {
|
||||||
|
integrity::verify_entity(
|
||||||
|
conn,
|
||||||
|
&self.keyholder,
|
||||||
|
&grant.settings,
|
||||||
|
grant.common_settings_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
verified_grants.push(Grant {
|
||||||
|
id: grant.id,
|
||||||
|
common_settings_id: grant.common_settings_id,
|
||||||
|
settings: grant.settings.generalize(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(verified_grants)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListError> {
|
||||||
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let mut grants: Vec<Grant<SpecificGrant>> = Vec::new();
|
let mut grants: Vec<Grant<SpecificGrant>> = Vec::new();
|
||||||
|
|
||||||
grants.extend(
|
grants.extend(self.list_one_kind::<EtherTransfer, _>(&mut conn).await?);
|
||||||
EtherTransfer::find_all_grants(&mut conn)
|
grants.extend(self.list_one_kind::<TokenTransfer, _>(&mut conn).await?);
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|g| Grant {
|
|
||||||
id: g.id,
|
|
||||||
shared_grant_id: g.shared_grant_id,
|
|
||||||
shared: g.shared,
|
|
||||||
settings: SpecificGrant::EtherTransfer(g.settings),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
grants.extend(
|
|
||||||
TokenTransfer::find_all_grants(&mut conn)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|g| Grant {
|
|
||||||
id: g.id,
|
|
||||||
shared_grant_id: g.shared_grant_id,
|
|
||||||
shared: g.shared,
|
|
||||||
settings: SpecificGrant::TokenTransfer(g.settings),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(grants)
|
Ok(grants)
|
||||||
}
|
}
|
||||||
@@ -305,3 +380,255 @@ impl Engine {
|
|||||||
Err(VetError::UnsupportedTransactionType)
|
Err(VetError::UnsupportedTransactionType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use alloy::primitives::{Address, Bytes, U256, address};
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use diesel::{SelectableHelper, insert_into};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use crate::db::{
|
||||||
|
self, DatabaseConnection,
|
||||||
|
models::{
|
||||||
|
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
|
||||||
|
},
|
||||||
|
schema::{evm_basic_grant, evm_transaction_log},
|
||||||
|
};
|
||||||
|
use crate::evm::policies::{
|
||||||
|
EvalContext, EvalViolation, SharedGrantSettings, TransactionRateLimit,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::check_shared_constraints;
|
||||||
|
|
||||||
|
const WALLET_ACCESS_ID: i32 = 1;
|
||||||
|
const CHAIN_ID: u64 = 1;
|
||||||
|
const RECIPIENT: Address = address!("1111111111111111111111111111111111111111");
|
||||||
|
|
||||||
|
fn context() -> EvalContext {
|
||||||
|
EvalContext {
|
||||||
|
target: EvmWalletAccess {
|
||||||
|
id: WALLET_ACCESS_ID,
|
||||||
|
wallet_id: 10,
|
||||||
|
client_id: 20,
|
||||||
|
created_at: SqliteTimestamp(Utc::now()),
|
||||||
|
},
|
||||||
|
chain: CHAIN_ID,
|
||||||
|
to: RECIPIENT,
|
||||||
|
value: U256::ZERO,
|
||||||
|
calldata: Bytes::new(),
|
||||||
|
max_fee_per_gas: 100,
|
||||||
|
max_priority_fee_per_gas: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shared_settings() -> SharedGrantSettings {
|
||||||
|
SharedGrantSettings {
|
||||||
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
|
chain: CHAIN_ID,
|
||||||
|
valid_from: None,
|
||||||
|
valid_until: None,
|
||||||
|
max_gas_fee_per_gas: None,
|
||||||
|
max_priority_fee_per_gas: None,
|
||||||
|
rate_limit: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_basic_grant(
|
||||||
|
conn: &mut DatabaseConnection,
|
||||||
|
shared: &SharedGrantSettings,
|
||||||
|
) -> EvmBasicGrant {
|
||||||
|
insert_into(evm_basic_grant::table)
|
||||||
|
.values(NewEvmBasicGrant {
|
||||||
|
wallet_access_id: shared.wallet_access_id,
|
||||||
|
chain_id: shared.chain as i32,
|
||||||
|
valid_from: shared.valid_from.map(SqliteTimestamp),
|
||||||
|
valid_until: shared.valid_until.map(SqliteTimestamp),
|
||||||
|
max_gas_fee_per_gas: shared
|
||||||
|
.max_gas_fee_per_gas
|
||||||
|
.map(|fee| super::utils::u256_to_bytes(fee).to_vec()),
|
||||||
|
max_priority_fee_per_gas: shared
|
||||||
|
.max_priority_fee_per_gas
|
||||||
|
.map(|fee| super::utils::u256_to_bytes(fee).to_vec()),
|
||||||
|
rate_limit_count: shared.rate_limit.as_ref().map(|limit| limit.count as i32),
|
||||||
|
rate_limit_window_secs: shared
|
||||||
|
.rate_limit
|
||||||
|
.as_ref()
|
||||||
|
.map(|limit| limit.window.num_seconds() as i32),
|
||||||
|
revoked_at: None,
|
||||||
|
})
|
||||||
|
.returning(EvmBasicGrant::as_select())
|
||||||
|
.get_result(conn)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::matching_chain(CHAIN_ID, false)]
|
||||||
|
#[case::mismatching_chain(CHAIN_ID + 1, true)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn check_shared_constraints_enforces_chain_id(
|
||||||
|
#[case] context_chain: u64,
|
||||||
|
#[case] expect_mismatch: bool,
|
||||||
|
) {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
let context = EvalContext {
|
||||||
|
chain: context_chain,
|
||||||
|
..context()
|
||||||
|
};
|
||||||
|
|
||||||
|
let violations = check_shared_constraints(&context, &shared_settings(), 999, &mut *conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
violations
|
||||||
|
.iter()
|
||||||
|
.any(|violation| matches!(violation, EvalViolation::MismatchingChainId { .. })),
|
||||||
|
expect_mismatch
|
||||||
|
);
|
||||||
|
|
||||||
|
if expect_mismatch {
|
||||||
|
assert_eq!(violations.len(), 1);
|
||||||
|
} else {
|
||||||
|
assert!(violations.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::valid_from_in_bounds(Some(Utc::now() - Duration::hours(1)), None, false)]
|
||||||
|
#[case::valid_from_out_of_bounds(Some(Utc::now() + Duration::hours(1)), None, true)]
|
||||||
|
#[case::valid_until_in_bounds(None, Some(Utc::now() + Duration::hours(1)), false)]
|
||||||
|
#[case::valid_until_out_of_bounds(None, Some(Utc::now() - Duration::hours(1)), true)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn check_shared_constraints_enforces_validity_window(
|
||||||
|
#[case] valid_from: Option<chrono::DateTime<Utc>>,
|
||||||
|
#[case] valid_until: Option<chrono::DateTime<Utc>>,
|
||||||
|
#[case] expect_invalid_time: bool,
|
||||||
|
) {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
let shared = SharedGrantSettings {
|
||||||
|
valid_from,
|
||||||
|
valid_until,
|
||||||
|
..shared_settings()
|
||||||
|
};
|
||||||
|
|
||||||
|
let violations = check_shared_constraints(&context(), &shared, 999, &mut *conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
violations
|
||||||
|
.iter()
|
||||||
|
.any(|violation| matches!(violation, EvalViolation::InvalidTime)),
|
||||||
|
expect_invalid_time
|
||||||
|
);
|
||||||
|
|
||||||
|
if expect_invalid_time {
|
||||||
|
assert_eq!(violations.len(), 1);
|
||||||
|
} else {
|
||||||
|
assert!(violations.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::max_fee_within_limit(Some(U256::from(100u64)), None, 100, 10, false)]
|
||||||
|
#[case::max_fee_exceeded(Some(U256::from(99u64)), None, 100, 10, true)]
|
||||||
|
#[case::priority_fee_within_limit(None, Some(U256::from(10u64)), 100, 10, false)]
|
||||||
|
#[case::priority_fee_exceeded(None, Some(U256::from(9u64)), 100, 10, true)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn check_shared_constraints_enforces_gas_fee_caps(
|
||||||
|
#[case] max_gas_fee_per_gas: Option<U256>,
|
||||||
|
#[case] max_priority_fee_per_gas: Option<U256>,
|
||||||
|
#[case] actual_max_fee_per_gas: u128,
|
||||||
|
#[case] actual_max_priority_fee_per_gas: u128,
|
||||||
|
#[case] expect_gas_limit_violation: bool,
|
||||||
|
) {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
let context = EvalContext {
|
||||||
|
max_fee_per_gas: actual_max_fee_per_gas,
|
||||||
|
max_priority_fee_per_gas: actual_max_priority_fee_per_gas,
|
||||||
|
..context()
|
||||||
|
};
|
||||||
|
|
||||||
|
let shared = SharedGrantSettings {
|
||||||
|
max_gas_fee_per_gas,
|
||||||
|
max_priority_fee_per_gas,
|
||||||
|
..shared_settings()
|
||||||
|
};
|
||||||
|
let violations = check_shared_constraints(&context, &shared, 999, &mut *conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
violations
|
||||||
|
.iter()
|
||||||
|
.any(|violation| matches!(violation, EvalViolation::GasLimitExceeded { .. })),
|
||||||
|
expect_gas_limit_violation
|
||||||
|
);
|
||||||
|
|
||||||
|
if expect_gas_limit_violation {
|
||||||
|
assert_eq!(violations.len(), 1);
|
||||||
|
} else {
|
||||||
|
assert!(violations.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::under_rate_limit(2, false)]
|
||||||
|
#[case::at_rate_limit(1, true)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn check_shared_constraints_enforces_rate_limit(
|
||||||
|
#[case] rate_limit_count: u32,
|
||||||
|
#[case] expect_rate_limit_violation: bool,
|
||||||
|
) {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
let shared = SharedGrantSettings {
|
||||||
|
rate_limit: Some(TransactionRateLimit {
|
||||||
|
count: rate_limit_count,
|
||||||
|
window: Duration::hours(1),
|
||||||
|
}),
|
||||||
|
..shared_settings()
|
||||||
|
};
|
||||||
|
|
||||||
|
let basic_grant = insert_basic_grant(&mut conn, &shared).await;
|
||||||
|
|
||||||
|
insert_into(evm_transaction_log::table)
|
||||||
|
.values(NewEvmTransactionLog {
|
||||||
|
grant_id: basic_grant.id,
|
||||||
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
|
chain_id: CHAIN_ID as i32,
|
||||||
|
eth_value: super::utils::u256_to_bytes(U256::ZERO).to_vec(),
|
||||||
|
signed_at: SqliteTimestamp(Utc::now()),
|
||||||
|
})
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let violations = check_shared_constraints(&context(), &shared, basic_grant.id, &mut *conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
violations
|
||||||
|
.iter()
|
||||||
|
.any(|violation| matches!(violation, EvalViolation::RateLimitExceeded)),
|
||||||
|
expect_rate_limit_violation
|
||||||
|
);
|
||||||
|
|
||||||
|
if expect_rate_limit_violation {
|
||||||
|
assert_eq!(violations.len(), 1);
|
||||||
|
} else {
|
||||||
|
assert!(violations.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ use diesel::{
|
|||||||
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
|
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
|
||||||
};
|
};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use miette::Diagnostic;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
crypto::integrity::v1::Integrable,
|
||||||
db::models::{self, EvmBasicGrant, EvmWalletAccess},
|
db::models::{self, EvmBasicGrant, EvmWalletAccess},
|
||||||
evm::utils,
|
evm::utils,
|
||||||
};
|
};
|
||||||
@@ -33,34 +34,31 @@ pub struct EvalContext {
|
|||||||
pub max_priority_fee_per_gas: u128,
|
pub max_priority_fee_per_gas: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error, Diagnostic)]
|
#[derive(Debug, Error)]
|
||||||
pub enum EvalViolation {
|
pub enum EvalViolation {
|
||||||
#[error("This grant doesn't allow transactions to the target address {target}")]
|
#[error("This grant doesn't allow transactions to the target address {target}")]
|
||||||
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_target))]
|
|
||||||
InvalidTarget { target: Address },
|
InvalidTarget { target: Address },
|
||||||
|
|
||||||
#[error("Gas limit exceeded for this grant")]
|
#[error("Gas limit exceeded for this grant")]
|
||||||
#[diagnostic(code(arbiter_server::evm::eval_violation::gas_limit_exceeded))]
|
|
||||||
GasLimitExceeded {
|
GasLimitExceeded {
|
||||||
max_gas_fee_per_gas: Option<U256>,
|
max_gas_fee_per_gas: Option<U256>,
|
||||||
max_priority_fee_per_gas: Option<U256>,
|
max_priority_fee_per_gas: Option<U256>,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Rate limit exceeded for this grant")]
|
#[error("Rate limit exceeded for this grant")]
|
||||||
#[diagnostic(code(arbiter_server::evm::eval_violation::rate_limit_exceeded))]
|
|
||||||
RateLimitExceeded,
|
RateLimitExceeded,
|
||||||
|
|
||||||
#[error("Transaction exceeds volumetric limits of the grant")]
|
#[error("Transaction exceeds volumetric limits of the grant")]
|
||||||
#[diagnostic(code(arbiter_server::evm::eval_violation::volumetric_limit_exceeded))]
|
|
||||||
VolumetricLimitExceeded,
|
VolumetricLimitExceeded,
|
||||||
|
|
||||||
#[error("Transaction is outside of the grant's validity period")]
|
#[error("Transaction is outside of the grant's validity period")]
|
||||||
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_time))]
|
|
||||||
InvalidTime,
|
InvalidTime,
|
||||||
|
|
||||||
#[error("Transaction type is not allowed by this grant")]
|
#[error("Transaction type is not allowed by this grant")]
|
||||||
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_transaction_type))]
|
|
||||||
InvalidTransactionType,
|
InvalidTransactionType,
|
||||||
|
|
||||||
|
#[error("Mismatching chain ID")]
|
||||||
|
MismatchingChainId { expected: ChainId, actual: ChainId },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DatabaseID = i32;
|
pub type DatabaseID = i32;
|
||||||
@@ -68,13 +66,12 @@ pub type DatabaseID = i32;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Grant<PolicySettings> {
|
pub struct Grant<PolicySettings> {
|
||||||
pub id: DatabaseID,
|
pub id: DatabaseID,
|
||||||
pub shared_grant_id: DatabaseID, // ID of the basic grant for shared-logic checks like rate limits and validity periods
|
pub common_settings_id: DatabaseID, // ID of the basic grant for shared-logic checks like rate limits and validity periods
|
||||||
pub shared: SharedGrantSettings,
|
pub settings: CombinedSettings<PolicySettings>,
|
||||||
pub settings: PolicySettings,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Policy: Sized {
|
pub trait Policy: Sized {
|
||||||
type Settings: Send + Sync + 'static + Into<SpecificGrant>;
|
type Settings: Send + Sync + 'static + Into<SpecificGrant> + Integrable;
|
||||||
type Meaning: Display + std::fmt::Debug + Send + Sync + 'static + Into<SpecificMeaning>;
|
type Meaning: Display + std::fmt::Debug + Send + Sync + 'static + Into<SpecificMeaning>;
|
||||||
|
|
||||||
fn analyze(context: &EvalContext) -> Option<Self::Meaning>;
|
fn analyze(context: &EvalContext) -> Option<Self::Meaning>;
|
||||||
@@ -130,13 +127,13 @@ pub enum SpecificMeaning {
|
|||||||
TokenTransfer(token_transfers::Meaning),
|
TokenTransfer(token_transfers::Meaning),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct TransactionRateLimit {
|
pub struct TransactionRateLimit {
|
||||||
pub count: u32,
|
pub count: u32,
|
||||||
pub window: Duration,
|
pub window: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct VolumeRateLimit {
|
pub struct VolumeRateLimit {
|
||||||
pub max_volume: U256,
|
pub max_volume: U256,
|
||||||
pub window: Duration,
|
pub window: Duration,
|
||||||
@@ -157,7 +154,7 @@ pub struct SharedGrantSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SharedGrantSettings {
|
impl SharedGrantSettings {
|
||||||
fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
|
pub(crate) fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
wallet_access_id: model.wallet_access_id,
|
wallet_access_id: model.wallet_access_id,
|
||||||
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
|
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
|
||||||
@@ -203,7 +200,57 @@ pub enum SpecificGrant {
|
|||||||
TokenTransfer(token_transfers::Settings),
|
TokenTransfer(token_transfers::Settings),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FullGrant<PolicyGrant> {
|
#[derive(Debug, Clone)]
|
||||||
pub basic: SharedGrantSettings,
|
pub struct CombinedSettings<PolicyGrant> {
|
||||||
|
pub shared: SharedGrantSettings,
|
||||||
pub specific: PolicyGrant,
|
pub specific: PolicyGrant,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<P> CombinedSettings<P> {
|
||||||
|
pub fn generalize<Y: From<P>>(self) -> CombinedSettings<Y> {
|
||||||
|
CombinedSettings {
|
||||||
|
shared: self.shared,
|
||||||
|
specific: self.specific.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Integrable> Integrable for CombinedSettings<P> {
|
||||||
|
const KIND: &'static str = P::KIND;
|
||||||
|
const VERSION: i32 = P::VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
impl Hashable for TransactionRateLimit {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.count.hash(hasher);
|
||||||
|
self.window.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for VolumeRateLimit {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.max_volume.hash(hasher);
|
||||||
|
self.window.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for SharedGrantSettings {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.wallet_access_id.hash(hasher);
|
||||||
|
self.chain.hash(hasher);
|
||||||
|
self.valid_from.hash(hasher);
|
||||||
|
self.valid_until.hash(hasher);
|
||||||
|
self.max_gas_fee_per_gas.hash(hasher);
|
||||||
|
self.max_priority_fee_per_gas.hash(hasher);
|
||||||
|
self.rate_limit.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Hashable> Hashable for CombinedSettings<P> {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.shared.hash(hasher);
|
||||||
|
self.specific.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ use diesel::sqlite::Sqlite;
|
|||||||
use diesel::{ExpressionMethods, JoinOnDsl, prelude::*};
|
use diesel::{ExpressionMethods, JoinOnDsl, prelude::*};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
|
||||||
|
use crate::crypto::integrity::v1::Integrable;
|
||||||
use crate::db::models::{
|
use crate::db::models::{
|
||||||
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
|
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
|
||||||
NewEvmEtherTransferLimit, SqliteTimestamp,
|
NewEvmEtherTransferLimit, SqliteTimestamp,
|
||||||
};
|
};
|
||||||
use crate::db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log};
|
use crate::db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log};
|
||||||
use crate::evm::policies::{
|
use crate::evm::policies::{
|
||||||
Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
@@ -36,8 +37,8 @@ use super::{DatabaseID, EvalContext, EvalViolation};
|
|||||||
// Plain ether transfer
|
// Plain ether transfer
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct Meaning {
|
pub struct Meaning {
|
||||||
to: Address,
|
pub(crate) to: Address,
|
||||||
value: U256,
|
pub(crate) value: U256,
|
||||||
}
|
}
|
||||||
impl Display for Meaning {
|
impl Display for Meaning {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
@@ -56,6 +57,18 @@ pub struct Settings {
|
|||||||
pub target: Vec<Address>,
|
pub target: Vec<Address>,
|
||||||
pub limit: VolumeRateLimit,
|
pub limit: VolumeRateLimit,
|
||||||
}
|
}
|
||||||
|
impl Integrable for Settings {
|
||||||
|
const KIND: &'static str = "EtherTransfer";
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
impl Hashable for Settings {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.target.hash(hasher);
|
||||||
|
self.limit.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Settings> for SpecificGrant {
|
impl From<Settings> for SpecificGrant {
|
||||||
fn from(val: Settings) -> SpecificGrant {
|
fn from(val: Settings) -> SpecificGrant {
|
||||||
@@ -91,20 +104,22 @@ async fn query_relevant_past_transaction(
|
|||||||
|
|
||||||
async fn check_rate_limits(
|
async fn check_rate_limits(
|
||||||
grant: &Grant<Settings>,
|
grant: &Grant<Settings>,
|
||||||
|
current_transfer_value: U256,
|
||||||
db: &mut impl AsyncConnection<Backend = Sqlite>,
|
db: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
) -> QueryResult<Vec<EvalViolation>> {
|
) -> QueryResult<Vec<EvalViolation>> {
|
||||||
let mut violations = Vec::new();
|
let mut violations = Vec::new();
|
||||||
let window = grant.settings.limit.window;
|
let window = grant.settings.specific.limit.window;
|
||||||
|
|
||||||
let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?;
|
let past_transaction =
|
||||||
|
query_relevant_past_transaction(grant.common_settings_id, window, db).await?;
|
||||||
|
|
||||||
let window_start = chrono::Utc::now() - grant.settings.limit.window;
|
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
||||||
let cumulative_volume: U256 = past_transaction
|
let prospective_cumulative_volume: U256 = past_transaction
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, timestamp)| timestamp >= &window_start)
|
.filter(|(_, timestamp)| timestamp >= &window_start)
|
||||||
.fold(U256::default(), |acc, (value, _)| acc + *value);
|
.fold(current_transfer_value, |acc, (value, _)| acc + *value);
|
||||||
|
|
||||||
if cumulative_volume > grant.settings.limit.max_volume {
|
if prospective_cumulative_volume > grant.settings.specific.limit.max_volume {
|
||||||
violations.push(EvalViolation::VolumetricLimitExceeded);
|
violations.push(EvalViolation::VolumetricLimitExceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,11 +152,11 @@ impl Policy for EtherTransfer {
|
|||||||
let mut violations = Vec::new();
|
let mut violations = Vec::new();
|
||||||
|
|
||||||
// Check if the target address is within the grant's allowed targets
|
// Check if the target address is within the grant's allowed targets
|
||||||
if !grant.settings.target.contains(&meaning.to) {
|
if !grant.settings.specific.target.contains(&meaning.to) {
|
||||||
violations.push(EvalViolation::InvalidTarget { target: meaning.to });
|
violations.push(EvalViolation::InvalidTarget { target: meaning.to });
|
||||||
}
|
}
|
||||||
|
|
||||||
let rate_violations = check_rate_limits(grant, db).await?;
|
let rate_violations = check_rate_limits(grant, meaning.value, db).await?;
|
||||||
violations.extend(rate_violations);
|
violations.extend(rate_violations);
|
||||||
|
|
||||||
Ok(violations)
|
Ok(violations)
|
||||||
@@ -235,20 +250,21 @@ impl Policy for EtherTransfer {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let settings = Settings {
|
|
||||||
target: targets,
|
|
||||||
limit: VolumeRateLimit {
|
|
||||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume)
|
|
||||||
.map_err(|err| diesel::result::Error::DeserializationError(Box::new(err)))?,
|
|
||||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(Grant {
|
Ok(Some(Grant {
|
||||||
id: grant.id,
|
id: grant.id,
|
||||||
shared_grant_id: grant.basic_grant_id,
|
common_settings_id: grant.basic_grant_id,
|
||||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
settings: CombinedSettings {
|
||||||
settings,
|
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||||
|
specific: Settings {
|
||||||
|
target: targets,
|
||||||
|
limit: VolumeRateLimit {
|
||||||
|
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(|err| {
|
||||||
|
diesel::result::Error::DeserializationError(Box::new(err))
|
||||||
|
})?,
|
||||||
|
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,15 +342,17 @@ impl Policy for EtherTransfer {
|
|||||||
|
|
||||||
Ok(Grant {
|
Ok(Grant {
|
||||||
id: specific.id,
|
id: specific.id,
|
||||||
shared_grant_id: specific.basic_grant_id,
|
common_settings_id: specific.basic_grant_id,
|
||||||
shared: SharedGrantSettings::try_from_model(basic)?,
|
settings: CombinedSettings {
|
||||||
settings: Settings {
|
shared: SharedGrantSettings::try_from_model(basic)?,
|
||||||
target: targets,
|
specific: Settings {
|
||||||
limit: VolumeRateLimit {
|
target: targets,
|
||||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(
|
limit: VolumeRateLimit {
|
||||||
|e| diesel::result::Error::DeserializationError(Box::new(e)),
|
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(
|
||||||
)?,
|
|e| diesel::result::Error::DeserializationError(Box::new(e)),
|
||||||
window: Duration::seconds(limit.window_secs as i64),
|
)?,
|
||||||
|
window: Duration::seconds(limit.window_secs as i64),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ use crate::db::{
|
|||||||
schema::{evm_basic_grant, evm_transaction_log},
|
schema::{evm_basic_grant, evm_transaction_log},
|
||||||
};
|
};
|
||||||
use crate::evm::{
|
use crate::evm::{
|
||||||
policies::{EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit},
|
policies::{
|
||||||
|
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
|
||||||
|
VolumeRateLimit,
|
||||||
|
},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,8 +84,6 @@ fn shared() -> SharedGrantSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── analyze ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn analyze_matches_empty_calldata() {
|
fn analyze_matches_empty_calldata() {
|
||||||
let m = EtherTransfer::analyze(&ctx(ALLOWED, U256::from(1_000u64))).unwrap();
|
let m = EtherTransfer::analyze(&ctx(ALLOWED, U256::from(1_000u64))).unwrap();
|
||||||
@@ -99,8 +100,6 @@ fn analyze_rejects_nonempty_calldata() {
|
|||||||
assert!(EtherTransfer::analyze(&context).is_none());
|
assert!(EtherTransfer::analyze(&context).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── evaluate ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn evaluate_passes_for_allowed_target() {
|
async fn evaluate_passes_for_allowed_target() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
@@ -108,9 +107,11 @@ async fn evaluate_passes_for_allowed_target() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: 999,
|
id: 999,
|
||||||
shared_grant_id: 999,
|
common_settings_id: 999,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings: make_settings(vec![ALLOWED], 1_000_000),
|
shared: shared(),
|
||||||
|
specific: make_settings(vec![ALLOWED], 1_000_000),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let context = ctx(ALLOWED, U256::from(100u64));
|
let context = ctx(ALLOWED, U256::from(100u64));
|
||||||
let m = EtherTransfer::analyze(&context).unwrap();
|
let m = EtherTransfer::analyze(&context).unwrap();
|
||||||
@@ -127,9 +128,11 @@ async fn evaluate_rejects_disallowed_target() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: 999,
|
id: 999,
|
||||||
shared_grant_id: 999,
|
common_settings_id: 999,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings: make_settings(vec![ALLOWED], 1_000_000),
|
shared: shared(),
|
||||||
|
specific: make_settings(vec![ALLOWED], 1_000_000),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let context = ctx(OTHER, U256::from(100u64));
|
let context = ctx(OTHER, U256::from(100u64));
|
||||||
let m = EtherTransfer::analyze(&context).unwrap();
|
let m = EtherTransfer::analyze(&context).unwrap();
|
||||||
@@ -167,9 +170,11 @@ async fn evaluate_passes_when_volume_within_limit() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: grant_id,
|
id: grant_id,
|
||||||
shared_grant_id: basic.id,
|
common_settings_id: basic.id,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings,
|
shared: shared(),
|
||||||
|
specific: settings,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let context = ctx(ALLOWED, U256::from(100u64));
|
let context = ctx(ALLOWED, U256::from(100u64));
|
||||||
let m = EtherTransfer::analyze(&context).unwrap();
|
let m = EtherTransfer::analyze(&context).unwrap();
|
||||||
@@ -198,7 +203,7 @@ async fn evaluate_rejects_volume_over_limit() {
|
|||||||
grant_id,
|
grant_id,
|
||||||
wallet_access_id: WALLET_ACCESS_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
eth_value: utils::u256_to_bytes(U256::from(1_001u64)).to_vec(),
|
eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
|
||||||
signed_at: SqliteTimestamp(Utc::now()),
|
signed_at: SqliteTimestamp(Utc::now()),
|
||||||
})
|
})
|
||||||
.execute(&mut *conn)
|
.execute(&mut *conn)
|
||||||
@@ -207,11 +212,13 @@ async fn evaluate_rejects_volume_over_limit() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: grant_id,
|
id: grant_id,
|
||||||
shared_grant_id: basic.id,
|
common_settings_id: basic.id,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings,
|
shared: shared(),
|
||||||
|
specific: settings,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let context = ctx(ALLOWED, U256::from(100u64));
|
let context = ctx(ALLOWED, U256::from(1u64));
|
||||||
let m = EtherTransfer::analyze(&context).unwrap();
|
let m = EtherTransfer::analyze(&context).unwrap();
|
||||||
let v = EtherTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
let v = EtherTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||||
.await
|
.await
|
||||||
@@ -233,13 +240,13 @@ async fn evaluate_passes_at_exactly_volume_limit() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Exactly at the limit — the check is `>`, so this should not violate
|
// Exactly at the limit including current transfer — check is `>`, so this should not violate
|
||||||
insert_into(evm_transaction_log::table)
|
insert_into(evm_transaction_log::table)
|
||||||
.values(NewEvmTransactionLog {
|
.values(NewEvmTransactionLog {
|
||||||
grant_id,
|
grant_id,
|
||||||
wallet_access_id: WALLET_ACCESS_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
|
eth_value: utils::u256_to_bytes(U256::from(900u64)).to_vec(),
|
||||||
signed_at: SqliteTimestamp(Utc::now()),
|
signed_at: SqliteTimestamp(Utc::now()),
|
||||||
})
|
})
|
||||||
.execute(&mut *conn)
|
.execute(&mut *conn)
|
||||||
@@ -248,9 +255,11 @@ async fn evaluate_passes_at_exactly_volume_limit() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: grant_id,
|
id: grant_id,
|
||||||
shared_grant_id: basic.id,
|
common_settings_id: basic.id,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings,
|
shared: shared(),
|
||||||
|
specific: settings,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let context = ctx(ALLOWED, U256::from(100u64));
|
let context = ctx(ALLOWED, U256::from(100u64));
|
||||||
let m = EtherTransfer::analyze(&context).unwrap();
|
let m = EtherTransfer::analyze(&context).unwrap();
|
||||||
@@ -263,8 +272,6 @@ async fn evaluate_passes_at_exactly_volume_limit() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── try_find_grant ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn try_find_grant_roundtrip() {
|
async fn try_find_grant_roundtrip() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
@@ -282,8 +289,11 @@ async fn try_find_grant_roundtrip() {
|
|||||||
|
|
||||||
assert!(found.is_some());
|
assert!(found.is_some());
|
||||||
let g = found.unwrap();
|
let g = found.unwrap();
|
||||||
assert_eq!(g.settings.target, vec![ALLOWED]);
|
assert_eq!(g.settings.specific.target, vec![ALLOWED]);
|
||||||
assert_eq!(g.settings.limit.max_volume, U256::from(1_000_000u64));
|
assert_eq!(
|
||||||
|
g.settings.specific.limit.max_volume,
|
||||||
|
U256::from(1_000_000u64)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -320,7 +330,36 @@ async fn try_find_grant_wrong_target_returns_none() {
|
|||||||
assert!(found.is_none());
|
assert!(found.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── find_all_grants ──────────────────────────────────────────────────────
|
proptest::proptest! {
|
||||||
|
#[test]
|
||||||
|
fn target_order_does_not_affect_hash(
|
||||||
|
raw_addrs in proptest::collection::vec(proptest::prelude::any::<[u8; 20]>(), 0..8),
|
||||||
|
seed in proptest::prelude::any::<u64>(),
|
||||||
|
max_volume in proptest::prelude::any::<u64>(),
|
||||||
|
window_secs in 1i64..=86400,
|
||||||
|
) {
|
||||||
|
use rand::{SeedableRng, seq::SliceRandom};
|
||||||
|
use sha2::Digest;
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
let addrs: Vec<Address> = raw_addrs.iter().map(|b| Address::from(*b)).collect();
|
||||||
|
let mut shuffled = addrs.clone();
|
||||||
|
shuffled.shuffle(&mut rand::rngs::StdRng::seed_from_u64(seed));
|
||||||
|
|
||||||
|
let limit = VolumeRateLimit {
|
||||||
|
max_volume: U256::from(max_volume),
|
||||||
|
window: Duration::seconds(window_secs),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut h1 = sha2::Sha256::new();
|
||||||
|
Settings { target: addrs, limit: limit.clone() }.hash(&mut h1);
|
||||||
|
|
||||||
|
let mut h2 = sha2::Sha256::new();
|
||||||
|
Settings { target: shuffled, limit }.hash(&mut h2);
|
||||||
|
|
||||||
|
proptest::prop_assert_eq!(h1.finalize(), h2.finalize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn find_all_grants_empty_db() {
|
async fn find_all_grants_empty_db() {
|
||||||
@@ -347,7 +386,7 @@ async fn find_all_grants_excludes_revoked() {
|
|||||||
|
|
||||||
let all = EtherTransfer::find_all_grants(&mut *conn).await.unwrap();
|
let all = EtherTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||||
assert_eq!(all.len(), 1);
|
assert_eq!(all.len(), 1);
|
||||||
assert_eq!(all[0].settings.target, vec![ALLOWED]);
|
assert_eq!(all[0].settings.specific.target, vec![ALLOWED]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -363,8 +402,11 @@ async fn find_all_grants_multiple_targets() {
|
|||||||
|
|
||||||
let all = EtherTransfer::find_all_grants(&mut *conn).await.unwrap();
|
let all = EtherTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||||
assert_eq!(all.len(), 1);
|
assert_eq!(all.len(), 1);
|
||||||
assert_eq!(all[0].settings.target.len(), 2);
|
assert_eq!(all[0].settings.specific.target.len(), 2);
|
||||||
assert_eq!(all[0].settings.limit.max_volume, U256::from(1_000_000u64));
|
assert_eq!(
|
||||||
|
all[0].settings.specific.limit.max_volume,
|
||||||
|
U256::from(1_000_000u64)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use alloy::{
|
|
||||||
primitives::{Address, U256},
|
|
||||||
sol_types::SolCall,
|
|
||||||
};
|
|
||||||
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
|
||||||
use diesel::dsl::{auto_type, insert_into};
|
|
||||||
use diesel::sqlite::Sqlite;
|
|
||||||
use diesel::{ExpressionMethods, prelude::*};
|
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
|
||||||
|
|
||||||
use crate::db::models::{
|
|
||||||
EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit, NewEvmTokenTransferGrant,
|
|
||||||
NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit, SqliteTimestamp,
|
|
||||||
};
|
|
||||||
use crate::db::schema::{
|
use crate::db::schema::{
|
||||||
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
|
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
|
||||||
evm_token_transfer_volume_limit,
|
evm_token_transfer_volume_limit,
|
||||||
@@ -26,6 +11,25 @@ use crate::evm::{
|
|||||||
},
|
},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
use crate::{
|
||||||
|
crypto::integrity::Integrable,
|
||||||
|
db::models::{
|
||||||
|
EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
|
||||||
|
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
|
||||||
|
SqliteTimestamp,
|
||||||
|
},
|
||||||
|
evm::policies::CombinedSettings,
|
||||||
|
};
|
||||||
|
use alloy::{
|
||||||
|
primitives::{Address, U256},
|
||||||
|
sol_types::SolCall,
|
||||||
|
};
|
||||||
|
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use diesel::dsl::{auto_type, insert_into};
|
||||||
|
use diesel::sqlite::Sqlite;
|
||||||
|
use diesel::{ExpressionMethods, prelude::*};
|
||||||
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
|
||||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||||
|
|
||||||
@@ -38,9 +42,9 @@ fn grant_join() -> _ {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct Meaning {
|
pub struct Meaning {
|
||||||
token: &'static TokenInfo,
|
pub token: &'static TokenInfo,
|
||||||
to: Address,
|
pub to: Address,
|
||||||
value: U256,
|
pub value: U256,
|
||||||
}
|
}
|
||||||
impl std::fmt::Display for Meaning {
|
impl std::fmt::Display for Meaning {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
@@ -64,6 +68,20 @@ pub struct Settings {
|
|||||||
pub target: Option<Address>,
|
pub target: Option<Address>,
|
||||||
pub volume_limits: Vec<VolumeRateLimit>,
|
pub volume_limits: Vec<VolumeRateLimit>,
|
||||||
}
|
}
|
||||||
|
impl Integrable for Settings {
|
||||||
|
const KIND: &'static str = "TokenTransfer";
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
impl Hashable for Settings {
|
||||||
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
|
self.token_contract.hash(hasher);
|
||||||
|
self.target.hash(hasher);
|
||||||
|
self.volume_limits.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Settings> for SpecificGrant {
|
impl From<Settings> for SpecificGrant {
|
||||||
fn from(val: Settings) -> SpecificGrant {
|
fn from(val: Settings) -> SpecificGrant {
|
||||||
SpecificGrant::TokenTransfer(val)
|
SpecificGrant::TokenTransfer(val)
|
||||||
@@ -101,24 +119,32 @@ async fn query_relevant_past_transfers(
|
|||||||
|
|
||||||
async fn check_volume_rate_limits(
|
async fn check_volume_rate_limits(
|
||||||
grant: &Grant<Settings>,
|
grant: &Grant<Settings>,
|
||||||
|
current_transfer_value: U256,
|
||||||
db: &mut impl AsyncConnection<Backend = Sqlite>,
|
db: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
) -> QueryResult<Vec<EvalViolation>> {
|
) -> QueryResult<Vec<EvalViolation>> {
|
||||||
let mut violations = Vec::new();
|
let mut violations = Vec::new();
|
||||||
|
|
||||||
let Some(longest_window) = grant.settings.volume_limits.iter().map(|l| l.window).max() else {
|
let Some(longest_window) = grant
|
||||||
|
.settings
|
||||||
|
.specific
|
||||||
|
.volume_limits
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.window)
|
||||||
|
.max()
|
||||||
|
else {
|
||||||
return Ok(violations);
|
return Ok(violations);
|
||||||
};
|
};
|
||||||
|
|
||||||
let past_transfers = query_relevant_past_transfers(grant.id, longest_window, db).await?;
|
let past_transfers = query_relevant_past_transfers(grant.id, longest_window, db).await?;
|
||||||
|
|
||||||
for limit in &grant.settings.volume_limits {
|
for limit in &grant.settings.specific.volume_limits {
|
||||||
let window_start = chrono::Utc::now() - limit.window;
|
let window_start = chrono::Utc::now() - limit.window;
|
||||||
let cumulative_volume: U256 = past_transfers
|
let prospective_cumulative_volume: U256 = past_transfers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, timestamp)| timestamp >= &window_start)
|
.filter(|(_, timestamp)| timestamp >= &window_start)
|
||||||
.fold(U256::default(), |acc, (value, _)| acc + *value);
|
.fold(current_transfer_value, |acc, (value, _)| acc + *value);
|
||||||
|
|
||||||
if cumulative_volume > limit.max_volume {
|
if prospective_cumulative_volume > limit.max_volume {
|
||||||
violations.push(EvalViolation::VolumetricLimitExceeded);
|
violations.push(EvalViolation::VolumetricLimitExceeded);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -157,13 +183,13 @@ impl Policy for TokenTransfer {
|
|||||||
return Ok(violations);
|
return Ok(violations);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(allowed) = grant.settings.target
|
if let Some(allowed) = grant.settings.specific.target
|
||||||
&& allowed != meaning.to
|
&& allowed != meaning.to
|
||||||
{
|
{
|
||||||
violations.push(EvalViolation::InvalidTarget { target: meaning.to });
|
violations.push(EvalViolation::InvalidTarget { target: meaning.to });
|
||||||
}
|
}
|
||||||
|
|
||||||
let rate_violations = check_volume_rate_limits(grant, db).await?;
|
let rate_violations = check_volume_rate_limits(grant, meaning.value, db).await?;
|
||||||
violations.extend(rate_violations);
|
violations.extend(rate_violations);
|
||||||
|
|
||||||
Ok(violations)
|
Ok(violations)
|
||||||
@@ -260,17 +286,17 @@ impl Policy for TokenTransfer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let settings = Settings {
|
|
||||||
token_contract: Address::from(token_contract),
|
|
||||||
target,
|
|
||||||
volume_limits,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(Grant {
|
Ok(Some(Grant {
|
||||||
id: token_grant.id,
|
id: token_grant.id,
|
||||||
shared_grant_id: token_grant.basic_grant_id,
|
common_settings_id: token_grant.basic_grant_id,
|
||||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
settings: CombinedSettings {
|
||||||
settings,
|
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||||
|
specific: Settings {
|
||||||
|
token_contract: Address::from(token_contract),
|
||||||
|
target,
|
||||||
|
volume_limits,
|
||||||
|
},
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,12 +394,14 @@ impl Policy for TokenTransfer {
|
|||||||
|
|
||||||
Ok(Grant {
|
Ok(Grant {
|
||||||
id: specific.id,
|
id: specific.id,
|
||||||
shared_grant_id: specific.basic_grant_id,
|
common_settings_id: specific.basic_grant_id,
|
||||||
shared: SharedGrantSettings::try_from_model(basic)?,
|
settings: CombinedSettings {
|
||||||
settings: Settings {
|
shared: SharedGrantSettings::try_from_model(basic)?,
|
||||||
token_contract: Address::from(token_contract),
|
specific: Settings {
|
||||||
target,
|
token_contract: Address::from(token_contract),
|
||||||
volume_limits,
|
target,
|
||||||
|
volume_limits,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ use crate::db::{
|
|||||||
};
|
};
|
||||||
use crate::evm::{
|
use crate::evm::{
|
||||||
abi::IERC20::transferCall,
|
abi::IERC20::transferCall,
|
||||||
policies::{EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit},
|
policies::{
|
||||||
|
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
|
||||||
|
VolumeRateLimit,
|
||||||
|
},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,8 +101,6 @@ fn shared() -> SharedGrantSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── analyze ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn analyze_known_token_valid_calldata() {
|
fn analyze_known_token_valid_calldata() {
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
@@ -125,8 +126,6 @@ fn analyze_empty_calldata_returns_none() {
|
|||||||
assert!(TokenTransfer::analyze(&ctx(DAI, Bytes::new())).is_none());
|
assert!(TokenTransfer::analyze(&ctx(DAI, Bytes::new())).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── evaluate ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn evaluate_rejects_nonzero_eth_value() {
|
async fn evaluate_rejects_nonzero_eth_value() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
@@ -134,9 +133,11 @@ async fn evaluate_rejects_nonzero_eth_value() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: 999,
|
id: 999,
|
||||||
shared_grant_id: 999,
|
common_settings_id: 999,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings: make_settings(None, None),
|
shared: shared(),
|
||||||
|
specific: make_settings(None, None),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let mut context = ctx(DAI, calldata);
|
let mut context = ctx(DAI, calldata);
|
||||||
@@ -163,9 +164,11 @@ async fn evaluate_passes_any_recipient_when_no_restriction() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: 999,
|
id: 999,
|
||||||
shared_grant_id: 999,
|
common_settings_id: 999,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings: make_settings(None, None),
|
shared: shared(),
|
||||||
|
specific: make_settings(None, None),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
@@ -183,9 +186,11 @@ async fn evaluate_passes_matching_restricted_recipient() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: 999,
|
id: 999,
|
||||||
shared_grant_id: 999,
|
common_settings_id: 999,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings: make_settings(Some(RECIPIENT), None),
|
shared: shared(),
|
||||||
|
specific: make_settings(Some(RECIPIENT), None),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
@@ -203,9 +208,11 @@ async fn evaluate_rejects_wrong_restricted_recipient() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: 999,
|
id: 999,
|
||||||
shared_grant_id: 999,
|
common_settings_id: 999,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings: make_settings(Some(RECIPIENT), None),
|
shared: shared(),
|
||||||
|
specific: make_settings(Some(RECIPIENT), None),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let calldata = transfer_calldata(OTHER, U256::from(100u64));
|
let calldata = transfer_calldata(OTHER, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
@@ -220,7 +227,7 @@ async fn evaluate_rejects_wrong_restricted_recipient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn evaluate_passes_volume_within_limit() {
|
async fn evaluate_passes_volume_at_exact_limit() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
@@ -230,7 +237,7 @@ async fn evaluate_passes_volume_within_limit() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Record a past transfer of 500 (within 1000 limit)
|
// Record a past transfer of 900, with current transfer 100 => exactly 1000 limit
|
||||||
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
||||||
insert_into(evm_token_transfer_log::table)
|
insert_into(evm_token_transfer_log::table)
|
||||||
.values(NewEvmTokenTransferLog {
|
.values(NewEvmTokenTransferLog {
|
||||||
@@ -239,7 +246,7 @@ async fn evaluate_passes_volume_within_limit() {
|
|||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
token_contract: DAI.to_vec(),
|
token_contract: DAI.to_vec(),
|
||||||
recipient_address: RECIPIENT.to_vec(),
|
recipient_address: RECIPIENT.to_vec(),
|
||||||
value: utils::u256_to_bytes(U256::from(500u64)).to_vec(),
|
value: utils::u256_to_bytes(U256::from(900u64)).to_vec(),
|
||||||
})
|
})
|
||||||
.execute(&mut *conn)
|
.execute(&mut *conn)
|
||||||
.await
|
.await
|
||||||
@@ -247,9 +254,11 @@ async fn evaluate_passes_volume_within_limit() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: grant_id,
|
id: grant_id,
|
||||||
shared_grant_id: basic.id,
|
common_settings_id: basic.id,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings,
|
shared: shared(),
|
||||||
|
specific: settings,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
@@ -282,7 +291,7 @@ async fn evaluate_rejects_volume_over_limit() {
|
|||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
token_contract: DAI.to_vec(),
|
token_contract: DAI.to_vec(),
|
||||||
recipient_address: RECIPIENT.to_vec(),
|
recipient_address: RECIPIENT.to_vec(),
|
||||||
value: utils::u256_to_bytes(U256::from(1_001u64)).to_vec(),
|
value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
|
||||||
})
|
})
|
||||||
.execute(&mut *conn)
|
.execute(&mut *conn)
|
||||||
.await
|
.await
|
||||||
@@ -290,11 +299,13 @@ async fn evaluate_rejects_volume_over_limit() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: grant_id,
|
id: grant_id,
|
||||||
shared_grant_id: basic.id,
|
common_settings_id: basic.id,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings,
|
shared: shared(),
|
||||||
|
specific: settings,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
let m = TokenTransfer::analyze(&context).unwrap();
|
let m = TokenTransfer::analyze(&context).unwrap();
|
||||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||||
@@ -313,9 +324,11 @@ async fn evaluate_no_volume_limits_always_passes() {
|
|||||||
|
|
||||||
let grant = Grant {
|
let grant = Grant {
|
||||||
id: 999,
|
id: 999,
|
||||||
shared_grant_id: 999,
|
common_settings_id: 999,
|
||||||
shared: shared(),
|
settings: CombinedSettings {
|
||||||
settings: make_settings(None, None), // no volume limits
|
shared: shared(),
|
||||||
|
specific: make_settings(None, None), // no volume limits
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(u64::MAX));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(u64::MAX));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
@@ -349,10 +362,13 @@ async fn try_find_grant_roundtrip() {
|
|||||||
|
|
||||||
assert!(found.is_some());
|
assert!(found.is_some());
|
||||||
let g = found.unwrap();
|
let g = found.unwrap();
|
||||||
assert_eq!(g.settings.token_contract, DAI);
|
assert_eq!(g.settings.specific.token_contract, DAI);
|
||||||
assert_eq!(g.settings.target, Some(RECIPIENT));
|
assert_eq!(g.settings.specific.target, Some(RECIPIENT));
|
||||||
assert_eq!(g.settings.volume_limits.len(), 1);
|
assert_eq!(g.settings.specific.volume_limits.len(), 1);
|
||||||
assert_eq!(g.settings.volume_limits[0].max_volume, U256::from(5_000u64));
|
assert_eq!(
|
||||||
|
g.settings.specific.volume_limits[0].max_volume,
|
||||||
|
U256::from(5_000u64)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -392,7 +408,39 @@ async fn try_find_grant_unknown_token_returns_none() {
|
|||||||
assert!(found.is_none());
|
assert!(found.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── find_all_grants ──────────────────────────────────────────────────────
|
proptest::proptest! {
|
||||||
|
#[test]
|
||||||
|
fn volume_limits_order_does_not_affect_hash(
|
||||||
|
raw_limits in proptest::collection::vec(
|
||||||
|
(proptest::prelude::any::<u64>(), 1i64..=86400),
|
||||||
|
0..8,
|
||||||
|
),
|
||||||
|
seed in proptest::prelude::any::<u64>(),
|
||||||
|
) {
|
||||||
|
use rand::{SeedableRng, seq::SliceRandom};
|
||||||
|
use sha2::Digest;
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
let limits: Vec<VolumeRateLimit> = raw_limits
|
||||||
|
.iter()
|
||||||
|
.map(|(max_vol, window_secs)| VolumeRateLimit {
|
||||||
|
max_volume: U256::from(*max_vol),
|
||||||
|
window: Duration::seconds(*window_secs),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut shuffled = limits.clone();
|
||||||
|
shuffled.shuffle(&mut rand::rngs::StdRng::seed_from_u64(seed));
|
||||||
|
|
||||||
|
let mut h1 = sha2::Sha256::new();
|
||||||
|
Settings { token_contract: DAI, target: None, volume_limits: limits }.hash(&mut h1);
|
||||||
|
|
||||||
|
let mut h2 = sha2::Sha256::new();
|
||||||
|
Settings { token_contract: DAI, target: None, volume_limits: shuffled }.hash(&mut h2);
|
||||||
|
|
||||||
|
proptest::prop_assert_eq!(h1.finalize(), h2.finalize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn find_all_grants_empty_db() {
|
async fn find_all_grants_empty_db() {
|
||||||
@@ -434,9 +482,9 @@ async fn find_all_grants_loads_volume_limits() {
|
|||||||
|
|
||||||
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||||
assert_eq!(all.len(), 1);
|
assert_eq!(all.len(), 1);
|
||||||
assert_eq!(all[0].settings.volume_limits.len(), 1);
|
assert_eq!(all[0].settings.specific.volume_limits.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
all[0].settings.volume_limits[0].max_volume,
|
all[0].settings.specific.volume_limits[0].max_volume,
|
||||||
U256::from(9_999u64)
|
U256::from(9_999u64)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,24 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
proto::client::{
|
proto::client::{
|
||||||
ClientRequest, ClientResponse, VaultState as ProtoVaultState,
|
ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload,
|
||||||
client_request::Payload as ClientRequestPayload,
|
|
||||||
client_response::Payload as ClientResponsePayload,
|
client_response::Payload as ClientResponsePayload,
|
||||||
},
|
},
|
||||||
transport::{Receiver, Sender, grpc::GrpcBi},
|
transport::{Receiver, Sender, grpc::GrpcBi},
|
||||||
};
|
};
|
||||||
use kameo::{
|
use kameo::actor::{ActorRef, Spawn as _};
|
||||||
actor::{ActorRef, Spawn as _},
|
|
||||||
error::SendError,
|
|
||||||
};
|
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::client::{ClientConnection, session::ClientSession},
|
||||||
client::{
|
|
||||||
self, ClientConnection,
|
|
||||||
session::{ClientSession, Error, HandleQueryVaultState},
|
|
||||||
},
|
|
||||||
keyholder::KeyHolderState,
|
|
||||||
},
|
|
||||||
grpc::request_tracker::RequestTracker,
|
grpc::request_tracker::RequestTracker,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod evm;
|
||||||
mod inbound;
|
mod inbound;
|
||||||
mod outbound;
|
mod outbound;
|
||||||
|
mod vault;
|
||||||
|
|
||||||
async fn dispatch_loop(
|
async fn dispatch_loop(
|
||||||
mut bi: GrpcBi<ClientRequest, ClientResponse>,
|
mut bi: GrpcBi<ClientRequest, ClientResponse>,
|
||||||
@@ -34,7 +26,9 @@ async fn dispatch_loop(
|
|||||||
mut request_tracker: RequestTracker,
|
mut request_tracker: RequestTracker,
|
||||||
) {
|
) {
|
||||||
loop {
|
loop {
|
||||||
let Some(message) = bi.recv().await else { return };
|
let Some(message) = bi.recv().await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let conn = match message {
|
let conn = match message {
|
||||||
Ok(conn) => conn,
|
Ok(conn) => conn,
|
||||||
@@ -53,16 +47,24 @@ async fn dispatch_loop(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let Some(payload) = conn.payload else {
|
let Some(payload) = conn.payload else {
|
||||||
let _ = bi.send(Err(Status::invalid_argument("Missing client request payload"))).await;
|
let _ = bi
|
||||||
|
.send(Err(Status::invalid_argument(
|
||||||
|
"Missing client request payload",
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
match dispatch_inner(&actor, payload).await {
|
match dispatch_inner(&actor, payload).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if bi.send(Ok(ClientResponse {
|
if bi
|
||||||
request_id: Some(request_id),
|
.send(Ok(ClientResponse {
|
||||||
payload: Some(response),
|
request_id: Some(request_id),
|
||||||
})).await.is_err() {
|
payload: Some(response),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,21 +81,10 @@ async fn dispatch_inner(
|
|||||||
payload: ClientRequestPayload,
|
payload: ClientRequestPayload,
|
||||||
) -> Result<ClientResponsePayload, Status> {
|
) -> Result<ClientResponsePayload, Status> {
|
||||||
match payload {
|
match payload {
|
||||||
ClientRequestPayload::QueryVaultState(_) => {
|
ClientRequestPayload::Vault(req) => vault::dispatch(actor, req).await,
|
||||||
let state = match actor.ask(HandleQueryVaultState {}).await {
|
ClientRequestPayload::Evm(req) => evm::dispatch(actor, req).await,
|
||||||
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
ClientRequestPayload::Auth(..) => {
|
||||||
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
|
warn!("Unsupported post-auth client auth request");
|
||||||
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
|
|
||||||
Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error,
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to query vault state");
|
|
||||||
ProtoVaultState::Error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(ClientResponsePayload::VaultState(state.into()))
|
|
||||||
}
|
|
||||||
payload => {
|
|
||||||
warn!(?payload, "Unsupported post-auth client request");
|
|
||||||
Err(Status::invalid_argument("Unsupported client request"))
|
Err(Status::invalid_argument("Unsupported client request"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,14 +93,21 @@ async fn dispatch_inner(
|
|||||||
pub async fn start(mut conn: ClientConnection, mut bi: GrpcBi<ClientRequest, ClientResponse>) {
|
pub async fn start(mut conn: ClientConnection, mut bi: GrpcBi<ClientRequest, ClientResponse>) {
|
||||||
let mut request_tracker = RequestTracker::default();
|
let mut request_tracker = RequestTracker::default();
|
||||||
|
|
||||||
if let Err(e) = auth::start(&mut conn, &mut bi, &mut request_tracker).await {
|
let client_id = match auth::start(&mut conn, &mut bi, &mut request_tracker).await {
|
||||||
let mut transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker);
|
Ok(id) => id,
|
||||||
let _ = transport.send(Err(e.clone())).await;
|
Err(err) => {
|
||||||
warn!(error = ?e, "Client authentication failed");
|
let _ = bi
|
||||||
return;
|
.send(Err(Status::unauthenticated(format!(
|
||||||
|
"Authentication failed: {}",
|
||||||
|
err
|
||||||
|
))))
|
||||||
|
.await;
|
||||||
|
warn!(error = ?err, "Client authentication failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let actor = client::session::ClientSession::spawn(client::session::ClientSession::new(conn));
|
let actor = ClientSession::spawn(ClientSession::new(conn, client_id));
|
||||||
let actor_for_cleanup = actor.clone();
|
let actor_for_cleanup = actor.clone();
|
||||||
|
|
||||||
info!("Client authenticated successfully");
|
info!("Client authenticated successfully");
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
ClientMetadata, proto::client::{
|
ClientMetadata,
|
||||||
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
|
proto::{
|
||||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
client::{
|
||||||
ClientInfo as ProtoClientInfo, ClientRequest, ClientResponse,
|
ClientRequest, ClientResponse,
|
||||||
client_request::Payload as ClientRequestPayload,
|
auth::{
|
||||||
client_response::Payload as ClientResponsePayload,
|
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
|
||||||
}, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}
|
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||||
|
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
||||||
|
request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload,
|
||||||
|
},
|
||||||
|
client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
},
|
||||||
|
shared::ClientInfo as ProtoClientInfo,
|
||||||
|
},
|
||||||
|
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
@@ -32,22 +41,22 @@ impl<'a> AuthTransportAdapter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn response_to_proto(response: auth::Outbound) -> ClientResponsePayload {
|
fn response_to_proto(response: auth::Outbound) -> AuthResponsePayload {
|
||||||
match response {
|
match response {
|
||||||
auth::Outbound::AuthChallenge { pubkey, nonce } => {
|
auth::Outbound::AuthChallenge { pubkey, nonce } => {
|
||||||
ClientResponsePayload::AuthChallenge(ProtoAuthChallenge {
|
AuthResponsePayload::Challenge(ProtoAuthChallenge {
|
||||||
pubkey: pubkey.to_bytes().to_vec(),
|
pubkey: pubkey.to_bytes().to_vec(),
|
||||||
nonce,
|
nonce,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
auth::Outbound::AuthSuccess => {
|
auth::Outbound::AuthSuccess => {
|
||||||
ClientResponsePayload::AuthResult(ProtoAuthResult::Success.into())
|
AuthResponsePayload::Result(ProtoAuthResult::Success.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error_to_proto(error: auth::Error) -> ClientResponsePayload {
|
fn error_to_proto(error: auth::Error) -> AuthResponsePayload {
|
||||||
ClientResponsePayload::AuthResult(
|
AuthResponsePayload::Result(
|
||||||
match error {
|
match error {
|
||||||
auth::Error::InvalidChallengeSolution => ProtoAuthResult::InvalidSignature,
|
auth::Error::InvalidChallengeSolution => ProtoAuthResult::InvalidSignature,
|
||||||
auth::Error::ApproveError(auth::ApproveError::Denied) => {
|
auth::Error::ApproveError(auth::ApproveError::Denied) => {
|
||||||
@@ -59,6 +68,7 @@ impl<'a> AuthTransportAdapter<'a> {
|
|||||||
auth::Error::ApproveError(auth::ApproveError::Internal)
|
auth::Error::ApproveError(auth::ApproveError::Internal)
|
||||||
| auth::Error::DatabasePoolUnavailable
|
| auth::Error::DatabasePoolUnavailable
|
||||||
| auth::Error::DatabaseOperationFailed
|
| auth::Error::DatabaseOperationFailed
|
||||||
|
| auth::Error::IntegrityCheckFailed
|
||||||
| auth::Error::Transport => ProtoAuthResult::Internal,
|
| auth::Error::Transport => ProtoAuthResult::Internal,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
@@ -67,18 +77,20 @@ impl<'a> AuthTransportAdapter<'a> {
|
|||||||
|
|
||||||
async fn send_client_response(
|
async fn send_client_response(
|
||||||
&mut self,
|
&mut self,
|
||||||
payload: ClientResponsePayload,
|
payload: AuthResponsePayload,
|
||||||
) -> Result<(), TransportError> {
|
) -> Result<(), TransportError> {
|
||||||
self.bi
|
self.bi
|
||||||
.send(Ok(ClientResponse {
|
.send(Ok(ClientResponse {
|
||||||
request_id: Some(self.request_tracker.current_request_id()),
|
request_id: Some(self.request_tracker.current_request_id()),
|
||||||
payload: Some(payload),
|
payload: Some(ClientResponsePayload::Auth(proto_auth::Response {
|
||||||
|
payload: Some(payload),
|
||||||
|
})),
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_auth_result(&mut self, result: ProtoAuthResult) -> Result<(), TransportError> {
|
async fn send_auth_result(&mut self, result: ProtoAuthResult) -> Result<(), TransportError> {
|
||||||
self.send_client_response(ClientResponsePayload::AuthResult(result.into()))
|
self.send_client_response(AuthResponsePayload::Result(result.into()))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,9 +129,27 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let payload = request.payload?;
|
let payload = request.payload?;
|
||||||
|
let ClientRequestPayload::Auth(auth_request) = payload else {
|
||||||
|
let _ = self
|
||||||
|
.bi
|
||||||
|
.send(Err(Status::invalid_argument(
|
||||||
|
"Unsupported client auth request",
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(payload) = auth_request.payload else {
|
||||||
|
let _ = self
|
||||||
|
.bi
|
||||||
|
.send(Err(Status::invalid_argument(
|
||||||
|
"Missing client auth request payload",
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
match payload {
|
match payload {
|
||||||
ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
|
AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest {
|
||||||
pubkey,
|
pubkey,
|
||||||
client_info,
|
client_info,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -143,9 +173,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
metadata: client_metadata_from_proto(client_info),
|
metadata: client_metadata_from_proto(client_info),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
|
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
||||||
signature,
|
|
||||||
}) => {
|
|
||||||
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
|
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
|
||||||
let _ = self
|
let _ = self
|
||||||
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
||||||
@@ -154,15 +182,6 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
};
|
};
|
||||||
Some(auth::Inbound::AuthChallengeSolution { signature })
|
Some(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
let _ = self
|
|
||||||
.bi
|
|
||||||
.send(Err(Status::invalid_argument(
|
|
||||||
"Unsupported client auth request",
|
|
||||||
)))
|
|
||||||
.await;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,8 +200,7 @@ pub async fn start(
|
|||||||
conn: &mut ClientConnection,
|
conn: &mut ClientConnection,
|
||||||
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
|
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
|
||||||
request_tracker: &mut RequestTracker,
|
request_tracker: &mut RequestTracker,
|
||||||
) -> Result<(), auth::Error> {
|
) -> Result<i32, auth::Error> {
|
||||||
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
|
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
|
||||||
client::auth::authenticate(conn, &mut transport).await?;
|
client::auth::authenticate(conn, &mut transport).await
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
87
server/crates/arbiter-server/src/grpc/client/evm.rs
Normal file
87
server/crates/arbiter-server/src/grpc/client/evm.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use arbiter_proto::proto::{
|
||||||
|
client::{
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
evm::{
|
||||||
|
self as proto_evm, request::Payload as EvmRequestPayload,
|
||||||
|
response::Payload as EvmResponsePayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
evm::{
|
||||||
|
EvmError as ProtoEvmError, EvmSignTransactionResponse,
|
||||||
|
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use kameo::actor::ActorRef;
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError},
|
||||||
|
grpc::{
|
||||||
|
Convert, TryConvert,
|
||||||
|
common::inbound::{RawEvmAddress, RawEvmTransaction},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn wrap_response(payload: EvmResponsePayload) -> ClientResponsePayload {
|
||||||
|
ClientResponsePayload::Evm(proto_evm::Response {
|
||||||
|
payload: Some(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn dispatch(
|
||||||
|
actor: &ActorRef<ClientSession>,
|
||||||
|
req: proto_evm::Request,
|
||||||
|
) -> Result<ClientResponsePayload, Status> {
|
||||||
|
let Some(payload) = req.payload else {
|
||||||
|
return Err(Status::invalid_argument(
|
||||||
|
"Missing client EVM request payload",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
EvmRequestPayload::SignTransaction(request) => {
|
||||||
|
let address = RawEvmAddress(request.wallet_address).try_convert()?;
|
||||||
|
let transaction = RawEvmTransaction(request.rlp_transaction).try_convert()?;
|
||||||
|
|
||||||
|
let response = match actor
|
||||||
|
.ask(HandleSignTransaction {
|
||||||
|
wallet_address: address,
|
||||||
|
transaction,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(signature) => EvmSignTransactionResponse {
|
||||||
|
result: Some(EvmSignTransactionResult::Signature(
|
||||||
|
signature.as_bytes().to_vec(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Vet(
|
||||||
|
vet_error,
|
||||||
|
))) => EvmSignTransactionResponse {
|
||||||
|
result: Some(vet_error.convert()),
|
||||||
|
},
|
||||||
|
Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Internal)) => {
|
||||||
|
EvmSignTransactionResponse {
|
||||||
|
result: Some(EvmSignTransactionResult::Error(
|
||||||
|
ProtoEvmError::Internal.into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to sign EVM transaction");
|
||||||
|
EvmSignTransactionResponse {
|
||||||
|
result: Some(EvmSignTransactionResult::Error(
|
||||||
|
ProtoEvmError::Internal.into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(wrap_response(EvmResponsePayload::SignTransaction(response)))
|
||||||
|
}
|
||||||
|
EvmRequestPayload::AnalyzeTransaction(_) => Err(Status::unimplemented(
|
||||||
|
"EVM transaction analysis is not yet implemented",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
47
server/crates/arbiter-server/src/grpc/client/vault.rs
Normal file
47
server/crates/arbiter-server/src/grpc/client/vault.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use arbiter_proto::proto::{
|
||||||
|
client::{
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
vault::{
|
||||||
|
self as proto_vault, request::Payload as VaultRequestPayload,
|
||||||
|
response::Payload as VaultResponsePayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shared::VaultState as ProtoVaultState,
|
||||||
|
};
|
||||||
|
use kameo::{actor::ActorRef, error::SendError};
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::actors::{
|
||||||
|
client::session::{ClientSession, Error, HandleQueryVaultState},
|
||||||
|
keyholder::KeyHolderState,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) async fn dispatch(
|
||||||
|
actor: &ActorRef<ClientSession>,
|
||||||
|
req: proto_vault::Request,
|
||||||
|
) -> Result<ClientResponsePayload, Status> {
|
||||||
|
let Some(payload) = req.payload else {
|
||||||
|
return Err(Status::invalid_argument(
|
||||||
|
"Missing client vault request payload",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
VaultRequestPayload::QueryState(_) => {
|
||||||
|
let state = match actor.ask(HandleQueryVaultState {}).await {
|
||||||
|
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
||||||
|
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
|
||||||
|
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
|
||||||
|
Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to query vault state");
|
||||||
|
ProtoVaultState::Error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(ClientResponsePayload::Vault(proto_vault::Response {
|
||||||
|
payload: Some(VaultResponsePayload::State(state.into())),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
server/crates/arbiter-server/src/grpc/common.rs
Normal file
2
server/crates/arbiter-server/src/grpc/common.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod inbound;
|
||||||
|
pub mod outbound;
|
||||||
35
server/crates/arbiter-server/src/grpc/common/inbound.rs
Normal file
35
server/crates/arbiter-server/src/grpc/common/inbound.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use alloy::{consensus::TxEip1559, primitives::Address, rlp::Decodable as _};
|
||||||
|
|
||||||
|
use crate::grpc::TryConvert;
|
||||||
|
|
||||||
|
pub struct RawEvmAddress(pub Vec<u8>);
|
||||||
|
impl TryConvert for RawEvmAddress {
|
||||||
|
type Output = Address;
|
||||||
|
|
||||||
|
type Error = tonic::Status;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<Self::Output, Self::Error> {
|
||||||
|
let wallet_address = match <[u8; 20]>::try_from(self.0.as_slice()) {
|
||||||
|
Ok(address) => Address::from(address),
|
||||||
|
Err(_) => {
|
||||||
|
return Err(tonic::Status::invalid_argument(
|
||||||
|
"Invalid EVM wallet address",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(wallet_address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RawEvmTransaction(pub Vec<u8>);
|
||||||
|
impl TryConvert for RawEvmTransaction {
|
||||||
|
type Output = TxEip1559;
|
||||||
|
|
||||||
|
type Error = tonic::Status;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<Self::Output, Self::Error> {
|
||||||
|
let tx = TxEip1559::decode(&mut self.0.as_slice())
|
||||||
|
.map_err(|_| tonic::Status::invalid_argument("Invalid EVM transaction format"))?;
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
125
server/crates/arbiter-server/src/grpc/common/outbound.rs
Normal file
125
server/crates/arbiter-server/src/grpc/common/outbound.rs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
use alloy::primitives::U256;
|
||||||
|
use arbiter_proto::proto::{
|
||||||
|
evm::{
|
||||||
|
EvmError as ProtoEvmError,
|
||||||
|
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||||
|
},
|
||||||
|
shared::evm::{
|
||||||
|
EvalViolation as ProtoEvalViolation, GasLimitExceededViolation, NoMatchingGrantError,
|
||||||
|
PolicyViolationsError, SpecificMeaning as ProtoSpecificMeaning,
|
||||||
|
TokenInfo as ProtoTokenInfo, TransactionEvalError as ProtoTransactionEvalError,
|
||||||
|
eval_violation as proto_eval_violation, eval_violation::Kind as ProtoEvalViolationKind,
|
||||||
|
specific_meaning::Meaning as ProtoSpecificMeaningKind,
|
||||||
|
transaction_eval_error::Kind as ProtoTransactionEvalErrorKind,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
evm::{
|
||||||
|
PolicyError, VetError,
|
||||||
|
policies::{EvalViolation, SpecificMeaning},
|
||||||
|
},
|
||||||
|
grpc::Convert,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn u256_to_proto_bytes(value: U256) -> Vec<u8> {
|
||||||
|
value.to_be_bytes::<32>().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for SpecificMeaning {
|
||||||
|
type Output = ProtoSpecificMeaning;
|
||||||
|
|
||||||
|
fn convert(self) -> Self::Output {
|
||||||
|
let kind = match self {
|
||||||
|
SpecificMeaning::EtherTransfer(meaning) => ProtoSpecificMeaningKind::EtherTransfer(
|
||||||
|
arbiter_proto::proto::shared::evm::EtherTransferMeaning {
|
||||||
|
to: meaning.to.to_vec(),
|
||||||
|
value: u256_to_proto_bytes(meaning.value),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SpecificMeaning::TokenTransfer(meaning) => ProtoSpecificMeaningKind::TokenTransfer(
|
||||||
|
arbiter_proto::proto::shared::evm::TokenTransferMeaning {
|
||||||
|
token: Some(ProtoTokenInfo {
|
||||||
|
symbol: meaning.token.symbol.to_string(),
|
||||||
|
address: meaning.token.contract.to_vec(),
|
||||||
|
chain_id: meaning.token.chain,
|
||||||
|
}),
|
||||||
|
to: meaning.to.to_vec(),
|
||||||
|
value: u256_to_proto_bytes(meaning.value),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
ProtoSpecificMeaning {
|
||||||
|
meaning: Some(kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for EvalViolation {
|
||||||
|
type Output = ProtoEvalViolation;
|
||||||
|
|
||||||
|
fn convert(self) -> Self::Output {
|
||||||
|
let kind = match self {
|
||||||
|
EvalViolation::InvalidTarget { target } => {
|
||||||
|
ProtoEvalViolationKind::InvalidTarget(target.to_vec())
|
||||||
|
}
|
||||||
|
EvalViolation::GasLimitExceeded {
|
||||||
|
max_gas_fee_per_gas,
|
||||||
|
max_priority_fee_per_gas,
|
||||||
|
} => ProtoEvalViolationKind::GasLimitExceeded(GasLimitExceededViolation {
|
||||||
|
max_gas_fee_per_gas: max_gas_fee_per_gas.map(u256_to_proto_bytes),
|
||||||
|
max_priority_fee_per_gas: max_priority_fee_per_gas.map(u256_to_proto_bytes),
|
||||||
|
}),
|
||||||
|
EvalViolation::RateLimitExceeded => ProtoEvalViolationKind::RateLimitExceeded(()),
|
||||||
|
EvalViolation::VolumetricLimitExceeded => {
|
||||||
|
ProtoEvalViolationKind::VolumetricLimitExceeded(())
|
||||||
|
}
|
||||||
|
EvalViolation::InvalidTime => ProtoEvalViolationKind::InvalidTime(()),
|
||||||
|
EvalViolation::InvalidTransactionType => {
|
||||||
|
ProtoEvalViolationKind::InvalidTransactionType(())
|
||||||
|
}
|
||||||
|
EvalViolation::MismatchingChainId { expected, actual } => {
|
||||||
|
ProtoEvalViolationKind::ChainIdMismatch(proto_eval_violation::ChainIdMismatch {
|
||||||
|
expected,
|
||||||
|
actual,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ProtoEvalViolation { kind: Some(kind) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for VetError {
|
||||||
|
type Output = EvmSignTransactionResult;
|
||||||
|
|
||||||
|
fn convert(self) -> Self::Output {
|
||||||
|
let kind = match self {
|
||||||
|
VetError::ContractCreationNotSupported => {
|
||||||
|
ProtoTransactionEvalErrorKind::ContractCreationNotSupported(())
|
||||||
|
}
|
||||||
|
VetError::UnsupportedTransactionType => {
|
||||||
|
ProtoTransactionEvalErrorKind::UnsupportedTransactionType(())
|
||||||
|
}
|
||||||
|
VetError::Evaluated(meaning, policy_error) => match policy_error {
|
||||||
|
PolicyError::NoMatchingGrant => {
|
||||||
|
ProtoTransactionEvalErrorKind::NoMatchingGrant(NoMatchingGrantError {
|
||||||
|
meaning: Some(meaning.convert()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
PolicyError::Violations(violations) => {
|
||||||
|
ProtoTransactionEvalErrorKind::PolicyViolations(PolicyViolationsError {
|
||||||
|
meaning: Some(meaning.convert()),
|
||||||
|
violations: violations.into_iter().map(Convert::convert).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
PolicyError::Database(_) | PolicyError::Integrity(_) => {
|
||||||
|
return EvmSignTransactionResult::Error(ProtoEvmError::Internal.into());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
EvmSignTransactionResult::EvalError(ProtoTransactionEvalError { kind: Some(kind) })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,10 +14,13 @@ use crate::{
|
|||||||
grpc::user_agent::start,
|
grpc::user_agent::start,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod client;
|
|
||||||
mod request_tracker;
|
mod request_tracker;
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
pub mod user_agent;
|
pub mod user_agent;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
pub trait Convert {
|
pub trait Convert {
|
||||||
type Output;
|
type Output;
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +1,29 @@
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
proto::{
|
proto::user_agent::{
|
||||||
client::ClientInfo as ProtoClientMetadata,
|
UserAgentRequest, UserAgentResponse,
|
||||||
evm::{
|
user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse,
|
|
||||||
GrantEntry, WalletCreateResponse, WalletEntry, WalletList, WalletListResponse,
|
|
||||||
evm_grant_create_response::Result as EvmGrantCreateResult,
|
|
||||||
evm_grant_delete_response::Result as EvmGrantDeleteResult,
|
|
||||||
evm_grant_list_response::Result as EvmGrantListResult,
|
|
||||||
wallet_create_response::Result as WalletCreateResult,
|
|
||||||
wallet_list_response::Result as WalletListResult,
|
|
||||||
},
|
|
||||||
user_agent::{
|
|
||||||
BootstrapEncryptedKey as ProtoBootstrapEncryptedKey,
|
|
||||||
BootstrapResult as ProtoBootstrapResult, ListWalletAccessResponse,
|
|
||||||
SdkClientConnectionCancel as ProtoSdkClientConnectionCancel,
|
|
||||||
SdkClientConnectionRequest as ProtoSdkClientConnectionRequest,
|
|
||||||
SdkClientEntry as ProtoSdkClientEntry, SdkClientError as ProtoSdkClientError,
|
|
||||||
SdkClientGrantWalletAccess, SdkClientList as ProtoSdkClientList,
|
|
||||||
SdkClientListResponse as ProtoSdkClientListResponse, SdkClientRevokeWalletAccess,
|
|
||||||
SdkClientWalletAccess, UnsealEncryptedKey as ProtoUnsealEncryptedKey,
|
|
||||||
UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentRequest, UserAgentResponse,
|
|
||||||
VaultState as ProtoVaultState,
|
|
||||||
sdk_client_list_response::Result as ProtoSdkClientListResult,
|
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use kameo::{
|
use kameo::actor::{ActorRef, Spawn as _};
|
||||||
actor::{ActorRef, Spawn as _},
|
|
||||||
error::SendError,
|
|
||||||
};
|
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
|
||||||
keyholder::KeyHolderState,
|
grpc::request_tracker::RequestTracker,
|
||||||
user_agent::{
|
|
||||||
OutOfBand, UserAgentConnection, UserAgentSession,
|
|
||||||
session::connection::{
|
|
||||||
BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate,
|
|
||||||
HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete,
|
|
||||||
HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess,
|
|
||||||
HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess,
|
|
||||||
HandleSdkClientList, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
db::models::{CoreEvmWalletAccess, NewEvmWalletAccess},
|
|
||||||
grpc::{Convert, TryConvert, request_tracker::RequestTracker},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod evm;
|
||||||
mod inbound;
|
mod inbound;
|
||||||
mod outbound;
|
mod outbound;
|
||||||
|
mod sdk_client;
|
||||||
|
mod vault;
|
||||||
|
|
||||||
pub struct OutOfBandAdapter(mpsc::Sender<OutOfBand>);
|
pub struct OutOfBandAdapter(mpsc::Sender<OutOfBand>);
|
||||||
|
|
||||||
@@ -86,23 +51,7 @@ async fn dispatch_loop(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let payload = match oob {
|
let payload = sdk_client::out_of_band_payload(oob);
|
||||||
OutOfBand::ClientConnectionRequest { profile } => {
|
|
||||||
UserAgentResponsePayload::SdkClientConnectionRequest(ProtoSdkClientConnectionRequest {
|
|
||||||
pubkey: profile.pubkey.to_bytes().to_vec(),
|
|
||||||
info: Some(ProtoClientMetadata {
|
|
||||||
name: profile.metadata.name,
|
|
||||||
description: profile.metadata.description,
|
|
||||||
version: profile.metadata.version,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
OutOfBand::ClientConnectionCancel { pubkey } => {
|
|
||||||
UserAgentResponsePayload::SdkClientConnectionCancel(ProtoSdkClientConnectionCancel {
|
|
||||||
pubkey: pubkey.to_bytes().to_vec(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if bi.send(Ok(UserAgentResponse { id: None, payload: Some(payload) })).await.is_err() {
|
if bi.send(Ok(UserAgentResponse { id: None, payload: Some(payload) })).await.is_err() {
|
||||||
return;
|
return;
|
||||||
@@ -144,7 +93,7 @@ async fn dispatch_loop(
|
|||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(status) => {
|
Err(status) => {
|
||||||
error!(?status, "Failed to process user agent request");
|
error!(?status, "Failed to process user agent request");
|
||||||
let _ = bi.send(Err(status)).await;
|
let _ = bi.send(Err(status)).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -158,285 +107,15 @@ async fn dispatch_inner(
|
|||||||
actor: &ActorRef<UserAgentSession>,
|
actor: &ActorRef<UserAgentSession>,
|
||||||
payload: UserAgentRequestPayload,
|
payload: UserAgentRequestPayload,
|
||||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
let response = match payload {
|
match payload {
|
||||||
UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey }) => {
|
UserAgentRequestPayload::Vault(req) => vault::dispatch(actor, req).await,
|
||||||
let client_pubkey = <[u8; 32]>::try_from(client_pubkey)
|
UserAgentRequestPayload::Evm(req) => evm::dispatch(actor, req).await,
|
||||||
.map(x25519_dalek::PublicKey::from)
|
UserAgentRequestPayload::SdkClient(req) => sdk_client::dispatch(actor, req).await,
|
||||||
.map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?;
|
UserAgentRequestPayload::Auth(..) => {
|
||||||
|
warn!("Unsupported post-auth user agent auth request");
|
||||||
let response = actor
|
Err(Status::invalid_argument("Unsupported user-agent request"))
|
||||||
.ask(HandleUnsealRequest { client_pubkey })
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
warn!(error = ?err, "Failed to handle unseal start request");
|
|
||||||
Status::internal("Failed to start unseal flow")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
UserAgentResponsePayload::UnsealStartResponse(
|
|
||||||
arbiter_proto::proto::user_agent::UnsealStartResponse {
|
|
||||||
server_pubkey: response.server_pubkey.as_bytes().to_vec(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
}) => {
|
|
||||||
let result = match actor
|
|
||||||
.ask(HandleUnsealEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => ProtoUnsealResult::Success,
|
|
||||||
Err(SendError::HandlerError(UnsealError::InvalidKey)) => {
|
|
||||||
ProtoUnsealResult::InvalidKey
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to handle unseal request");
|
|
||||||
return Err(Status::internal("Failed to unseal vault"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::UnsealResult(result.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
}) => {
|
|
||||||
let result = match actor
|
|
||||||
.ask(HandleBootstrapEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => ProtoBootstrapResult::Success,
|
|
||||||
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => {
|
|
||||||
ProtoBootstrapResult::InvalidKey
|
|
||||||
}
|
|
||||||
Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => {
|
|
||||||
ProtoBootstrapResult::AlreadyBootstrapped
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to handle bootstrap request");
|
|
||||||
return Err(Status::internal("Failed to bootstrap vault"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::BootstrapResult(result.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::QueryVaultState(_) => {
|
|
||||||
let state = match actor.ask(HandleQueryVaultState {}).await {
|
|
||||||
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
|
||||||
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
|
|
||||||
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to query vault state");
|
|
||||||
ProtoVaultState::Error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::VaultState(state.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::EvmWalletCreate(_) => {
|
|
||||||
let result = match actor.ask(HandleEvmWalletCreate {}).await {
|
|
||||||
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
|
|
||||||
id: wallet_id,
|
|
||||||
address: address.to_vec(),
|
|
||||||
}),
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to create EVM wallet");
|
|
||||||
WalletCreateResult::Error(ProtoEvmError::Internal.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::EvmWalletCreate(WalletCreateResponse {
|
|
||||||
result: Some(result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::EvmWalletList(_) => {
|
|
||||||
let result = match actor.ask(HandleEvmWalletList {}).await {
|
|
||||||
Ok(wallets) => WalletListResult::Wallets(WalletList {
|
|
||||||
wallets: wallets
|
|
||||||
.into_iter()
|
|
||||||
.map(|(id, address)| WalletEntry {
|
|
||||||
address: address.to_vec(),
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}),
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to list EVM wallets");
|
|
||||||
WalletListResult::Error(ProtoEvmError::Internal.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::EvmWalletList(WalletListResponse {
|
|
||||||
result: Some(result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::EvmGrantList(_) => {
|
|
||||||
let result = match actor.ask(HandleGrantList {}).await {
|
|
||||||
Ok(grants) => EvmGrantListResult::Grants(EvmGrantList {
|
|
||||||
grants: grants
|
|
||||||
.into_iter()
|
|
||||||
.map(|grant| GrantEntry {
|
|
||||||
id: grant.id,
|
|
||||||
wallet_access_id: grant.shared.wallet_access_id,
|
|
||||||
shared: Some(grant.shared.convert()),
|
|
||||||
specific: Some(grant.settings.convert()),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}),
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to list EVM grants");
|
|
||||||
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::EvmGrantList(EvmGrantListResponse {
|
|
||||||
result: Some(result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { shared, specific }) => {
|
|
||||||
let basic = shared
|
|
||||||
.ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))?
|
|
||||||
.try_convert()?;
|
|
||||||
let grant = specific
|
|
||||||
.ok_or_else(|| Status::invalid_argument("Missing specific grant settings"))?
|
|
||||||
.try_convert()?;
|
|
||||||
|
|
||||||
let result = match actor.ask(HandleGrantCreate { basic, grant }).await {
|
|
||||||
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to create EVM grant");
|
|
||||||
EvmGrantCreateResult::Error(ProtoEvmError::Internal.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::EvmGrantCreate(EvmGrantCreateResponse {
|
|
||||||
result: Some(result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => {
|
|
||||||
let result = match actor.ask(HandleGrantDelete { grant_id }).await {
|
|
||||||
Ok(()) => EvmGrantDeleteResult::Ok(()),
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to delete EVM grant");
|
|
||||||
EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::EvmGrantDelete(EvmGrantDeleteResponse {
|
|
||||||
result: Some(result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::SdkClientConnectionResponse(resp) => {
|
|
||||||
let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey)
|
|
||||||
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?;
|
|
||||||
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes)
|
|
||||||
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?;
|
|
||||||
|
|
||||||
actor
|
|
||||||
.ask(HandleNewClientApprove {
|
|
||||||
approved: resp.approved,
|
|
||||||
pubkey,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
warn!(?err, "Failed to process client connection response");
|
|
||||||
Status::internal("Failed to process response")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::SdkClientRevoke(_) => todo!(),
|
|
||||||
|
|
||||||
UserAgentRequestPayload::SdkClientList(_) => {
|
|
||||||
let result = match actor.ask(HandleSdkClientList {}).await {
|
|
||||||
Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList {
|
|
||||||
clients: clients
|
|
||||||
.into_iter()
|
|
||||||
.map(|(client, metadata)| ProtoSdkClientEntry {
|
|
||||||
id: client.id,
|
|
||||||
pubkey: client.public_key,
|
|
||||||
info: Some(ProtoClientMetadata {
|
|
||||||
name: metadata.name,
|
|
||||||
description: metadata.description,
|
|
||||||
version: metadata.version,
|
|
||||||
}),
|
|
||||||
created_at: client.created_at.0.timestamp() as i32,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}),
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to list SDK clients");
|
|
||||||
ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::SdkClientListResponse(ProtoSdkClientListResponse {
|
|
||||||
result: Some(result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => {
|
|
||||||
let entries: Vec<NewEvmWalletAccess> =
|
|
||||||
accesses.into_iter().map(|a| a.convert()).collect();
|
|
||||||
|
|
||||||
match actor.ask(HandleGrantEvmWalletAccess { entries }).await {
|
|
||||||
Ok(()) => {
|
|
||||||
info!("Successfully granted wallet access");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to grant wallet access");
|
|
||||||
return Err(Status::internal("Failed to grant wallet access"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::RevokeWalletAccess(SdkClientRevokeWalletAccess { accesses }) => {
|
|
||||||
match actor.ask(HandleRevokeEvmWalletAccess { entries: accesses }).await {
|
|
||||||
Ok(()) => {
|
|
||||||
info!("Successfully revoked wallet access");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to revoke wallet access");
|
|
||||||
return Err(Status::internal("Failed to revoke wallet access"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::ListWalletAccess(_) => {
|
|
||||||
let result = match actor.ask(HandleListWalletAccess {}).await {
|
|
||||||
Ok(accesses) => ListWalletAccessResponse {
|
|
||||||
accesses: accesses.into_iter().map(|a| a.convert()).collect(),
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
warn!(error = ?err, "Failed to list wallet access");
|
|
||||||
return Err(Status::internal("Failed to list wallet access"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
UserAgentResponsePayload::ListWalletAccessResponse(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
UserAgentRequestPayload::AuthChallengeRequest(..)
|
|
||||||
| UserAgentRequestPayload::AuthChallengeSolution(..) => {
|
|
||||||
warn!(?payload, "Unsupported post-auth user agent request");
|
|
||||||
return Err(Status::invalid_argument("Unsupported user-agent request"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(response))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
proto::user_agent::{
|
proto::user_agent::{
|
||||||
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
|
UserAgentRequest, UserAgentResponse,
|
||||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
auth::{
|
||||||
KeyType as ProtoKeyType, UserAgentRequest, UserAgentResponse,
|
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
|
||||||
|
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||||
|
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
||||||
|
KeyType as ProtoKeyType, request::Payload as AuthRequestPayload,
|
||||||
|
response::Payload as AuthResponsePayload,
|
||||||
|
},
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
},
|
},
|
||||||
@@ -36,12 +41,14 @@ impl<'a> AuthTransportAdapter<'a> {
|
|||||||
|
|
||||||
async fn send_user_agent_response(
|
async fn send_user_agent_response(
|
||||||
&mut self,
|
&mut self,
|
||||||
payload: UserAgentResponsePayload,
|
payload: AuthResponsePayload,
|
||||||
) -> Result<(), TransportError> {
|
) -> Result<(), TransportError> {
|
||||||
self.bi
|
self.bi
|
||||||
.send(Ok(UserAgentResponse {
|
.send(Ok(UserAgentResponse {
|
||||||
id: Some(self.request_tracker.current_request_id()),
|
id: Some(self.request_tracker.current_request_id()),
|
||||||
payload: Some(payload),
|
payload: Some(UserAgentResponsePayload::Auth(proto_auth::Response {
|
||||||
|
payload: Some(payload),
|
||||||
|
})),
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -56,19 +63,19 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
|
|||||||
use auth::{Error, Outbound};
|
use auth::{Error, Outbound};
|
||||||
let payload = match item {
|
let payload = match item {
|
||||||
Ok(Outbound::AuthChallenge { nonce }) => {
|
Ok(Outbound::AuthChallenge { nonce }) => {
|
||||||
UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge { nonce })
|
AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce })
|
||||||
}
|
}
|
||||||
Ok(Outbound::AuthSuccess) => {
|
Ok(Outbound::AuthSuccess) => {
|
||||||
UserAgentResponsePayload::AuthResult(ProtoAuthResult::Success.into())
|
AuthResponsePayload::Result(ProtoAuthResult::Success.into())
|
||||||
}
|
}
|
||||||
Err(Error::UnregisteredPublicKey) => {
|
Err(Error::UnregisteredPublicKey) => {
|
||||||
UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidKey.into())
|
AuthResponsePayload::Result(ProtoAuthResult::InvalidKey.into())
|
||||||
}
|
}
|
||||||
Err(Error::InvalidChallengeSolution) => {
|
Err(Error::InvalidChallengeSolution) => {
|
||||||
UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidSignature.into())
|
AuthResponsePayload::Result(ProtoAuthResult::InvalidSignature.into())
|
||||||
}
|
}
|
||||||
Err(Error::InvalidBootstrapToken) => {
|
Err(Error::InvalidBootstrapToken) => {
|
||||||
UserAgentResponsePayload::AuthResult(ProtoAuthResult::TokenInvalid.into())
|
AuthResponsePayload::Result(ProtoAuthResult::TokenInvalid.into())
|
||||||
}
|
}
|
||||||
Err(Error::Internal { details }) => {
|
Err(Error::Internal { details }) => {
|
||||||
return self.bi.send(Err(Status::internal(details))).await;
|
return self.bi.send(Err(Status::internal(details))).await;
|
||||||
@@ -112,8 +119,26 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let UserAgentRequestPayload::Auth(auth_request) = payload else {
|
||||||
|
let _ = self
|
||||||
|
.bi
|
||||||
|
.send(Err(Status::invalid_argument(
|
||||||
|
"Unsupported user-agent auth request",
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(payload) = auth_request.payload else {
|
||||||
|
warn!(
|
||||||
|
event = "received auth request with empty payload",
|
||||||
|
"grpc.useragent.auth_adapter"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
match payload {
|
match payload {
|
||||||
UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
|
AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest {
|
||||||
pubkey,
|
pubkey,
|
||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
key_type,
|
key_type,
|
||||||
@@ -150,17 +175,8 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
|
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
||||||
signature,
|
Some(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
}) => Some(auth::Inbound::AuthChallengeSolution { signature }),
|
|
||||||
_ => {
|
|
||||||
let _ = self
|
|
||||||
.bi
|
|
||||||
.send(Err(Status::invalid_argument(
|
|
||||||
"Unsupported user-agent auth request",
|
|
||||||
)))
|
|
||||||
.await;
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
243
server/crates/arbiter-server/src/grpc/user_agent/evm.rs
Normal file
243
server/crates/arbiter-server/src/grpc/user_agent/evm.rs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
use arbiter_proto::proto::{
|
||||||
|
evm::{
|
||||||
|
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
|
||||||
|
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse,
|
||||||
|
EvmSignTransactionResponse, GrantEntry, WalletCreateResponse, WalletEntry, WalletList,
|
||||||
|
WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
|
||||||
|
evm_grant_delete_response::Result as EvmGrantDeleteResult,
|
||||||
|
evm_grant_list_response::Result as EvmGrantListResult,
|
||||||
|
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||||
|
wallet_create_response::Result as WalletCreateResult,
|
||||||
|
wallet_list_response::Result as WalletListResult,
|
||||||
|
},
|
||||||
|
user_agent::{
|
||||||
|
evm::{
|
||||||
|
self as proto_evm, SignTransactionRequest as ProtoSignTransactionRequest,
|
||||||
|
request::Payload as EvmRequestPayload, response::Payload as EvmResponsePayload,
|
||||||
|
},
|
||||||
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use kameo::actor::ActorRef;
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::user_agent::{
|
||||||
|
UserAgentSession,
|
||||||
|
session::connection::{
|
||||||
|
GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate,
|
||||||
|
HandleGrantDelete, HandleGrantList, HandleSignTransaction,
|
||||||
|
SignTransactionError as SessionSignTransactionError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grpc::{
|
||||||
|
Convert, TryConvert,
|
||||||
|
common::inbound::{RawEvmAddress, RawEvmTransaction},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn wrap_evm_response(payload: EvmResponsePayload) -> UserAgentResponsePayload {
|
||||||
|
UserAgentResponsePayload::Evm(proto_evm::Response {
|
||||||
|
payload: Some(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn dispatch(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: proto_evm::Request,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let Some(payload) = req.payload else {
|
||||||
|
return Err(Status::invalid_argument("Missing EVM request payload"));
|
||||||
|
};
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
EvmRequestPayload::WalletCreate(_) => handle_wallet_create(actor).await,
|
||||||
|
EvmRequestPayload::WalletList(_) => handle_wallet_list(actor).await,
|
||||||
|
EvmRequestPayload::GrantCreate(req) => handle_grant_create(actor, req).await,
|
||||||
|
EvmRequestPayload::GrantDelete(req) => handle_grant_delete(actor, req).await,
|
||||||
|
EvmRequestPayload::GrantList(_) => handle_grant_list(actor).await,
|
||||||
|
EvmRequestPayload::SignTransaction(req) => handle_sign_transaction(actor, req).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_wallet_create(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let result = match actor.ask(HandleEvmWalletCreate {}).await {
|
||||||
|
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
|
||||||
|
id: wallet_id,
|
||||||
|
address: address.to_vec(),
|
||||||
|
}),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to create EVM wallet");
|
||||||
|
WalletCreateResult::Error(ProtoEvmError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_evm_response(EvmResponsePayload::WalletCreate(
|
||||||
|
WalletCreateResponse {
|
||||||
|
result: Some(result),
|
||||||
|
},
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_wallet_list(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let result = match actor.ask(HandleEvmWalletList {}).await {
|
||||||
|
Ok(wallets) => WalletListResult::Wallets(WalletList {
|
||||||
|
wallets: wallets
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, address)| WalletEntry {
|
||||||
|
address: address.to_vec(),
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to list EVM wallets");
|
||||||
|
WalletListResult::Error(ProtoEvmError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_evm_response(EvmResponsePayload::WalletList(
|
||||||
|
WalletListResponse {
|
||||||
|
result: Some(result),
|
||||||
|
},
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_grant_list(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let result = match actor.ask(HandleGrantList {}).await {
|
||||||
|
Ok(grants) => EvmGrantListResult::Grants(EvmGrantList {
|
||||||
|
grants: grants
|
||||||
|
.into_iter()
|
||||||
|
.map(|grant| GrantEntry {
|
||||||
|
id: grant.common_settings_id,
|
||||||
|
wallet_access_id: grant.settings.shared.wallet_access_id,
|
||||||
|
shared: Some(grant.settings.shared.convert()),
|
||||||
|
specific: Some(grant.settings.specific.convert()),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||||
|
EvmGrantListResult::Error(ProtoEvmError::VaultSealed.into())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to list EVM grants");
|
||||||
|
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_evm_response(EvmResponsePayload::GrantList(
|
||||||
|
EvmGrantListResponse {
|
||||||
|
result: Some(result),
|
||||||
|
},
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_grant_create(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: EvmGrantCreateRequest,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let basic = req
|
||||||
|
.shared
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))?
|
||||||
|
.try_convert()?;
|
||||||
|
let grant = req
|
||||||
|
.specific
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing specific grant settings"))?
|
||||||
|
.try_convert()?;
|
||||||
|
|
||||||
|
let result = match actor.ask(HandleGrantCreate { basic, grant }).await {
|
||||||
|
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id.into_inner()),
|
||||||
|
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||||
|
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to create EVM grant");
|
||||||
|
EvmGrantCreateResult::Error(ProtoEvmError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_evm_response(EvmResponsePayload::GrantCreate(
|
||||||
|
EvmGrantCreateResponse {
|
||||||
|
result: Some(result),
|
||||||
|
},
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_grant_delete(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: EvmGrantDeleteRequest,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let result = match actor
|
||||||
|
.ask(HandleGrantDelete {
|
||||||
|
grant_id: req.grant_id,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => EvmGrantDeleteResult::Ok(()),
|
||||||
|
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||||
|
EvmGrantDeleteResult::Error(ProtoEvmError::VaultSealed.into())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to delete EVM grant");
|
||||||
|
EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_evm_response(EvmResponsePayload::GrantDelete(
|
||||||
|
EvmGrantDeleteResponse {
|
||||||
|
result: Some(result),
|
||||||
|
},
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_sign_transaction(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: ProtoSignTransactionRequest,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let request = req
|
||||||
|
.request
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing sign transaction request"))?;
|
||||||
|
let wallet_address = RawEvmAddress(request.wallet_address).try_convert()?;
|
||||||
|
let transaction = RawEvmTransaction(request.rlp_transaction).try_convert()?;
|
||||||
|
|
||||||
|
let response = match actor
|
||||||
|
.ask(HandleSignTransaction {
|
||||||
|
client_id: req.client_id,
|
||||||
|
wallet_address,
|
||||||
|
transaction,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(signature) => EvmSignTransactionResponse {
|
||||||
|
result: Some(EvmSignTransactionResult::Signature(
|
||||||
|
signature.as_bytes().to_vec(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
Err(kameo::error::SendError::HandlerError(SessionSignTransactionError::Vet(vet_error))) => {
|
||||||
|
EvmSignTransactionResponse {
|
||||||
|
result: Some(vet_error.convert()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(kameo::error::SendError::HandlerError(SessionSignTransactionError::Internal)) => {
|
||||||
|
EvmSignTransactionResponse {
|
||||||
|
result: Some(EvmSignTransactionResult::Error(
|
||||||
|
ProtoEvmError::Internal.into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to sign EVM transaction");
|
||||||
|
EvmSignTransactionResponse {
|
||||||
|
result: Some(EvmSignTransactionResult::Error(
|
||||||
|
ProtoEvmError::Internal.into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(wrap_evm_response(
|
||||||
|
EvmResponsePayload::SignTransaction(response),
|
||||||
|
)))
|
||||||
|
}
|
||||||
@@ -5,12 +5,14 @@ use arbiter_proto::proto::evm::{
|
|||||||
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
||||||
specific_grant::Grant as ProtoSpecificGrantType,
|
specific_grant::Grant as ProtoSpecificGrantType,
|
||||||
};
|
};
|
||||||
use arbiter_proto::proto::user_agent::{SdkClientWalletAccess, WalletAccess};
|
use arbiter_proto::proto::user_agent::sdk_client::{
|
||||||
|
WalletAccess, WalletAccessEntry as SdkClientWalletAccess,
|
||||||
|
};
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
use prost_types::Timestamp as ProtoTimestamp;
|
use prost_types::Timestamp as ProtoTimestamp;
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
|
|
||||||
use crate::db::models::{CoreEvmWalletAccess, NewEvmWallet, NewEvmWalletAccess};
|
use crate::db::models::{CoreEvmWalletAccess, NewEvmWalletAccess};
|
||||||
use crate::grpc::Convert;
|
use crate::grpc::Convert;
|
||||||
use crate::{
|
use crate::{
|
||||||
evm::policies::{
|
evm::policies::{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use arbiter_proto::proto::{
|
|||||||
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
||||||
specific_grant::Grant as ProtoSpecificGrantType,
|
specific_grant::Grant as ProtoSpecificGrantType,
|
||||||
},
|
},
|
||||||
user_agent::{SdkClientWalletAccess as ProtoSdkClientWalletAccess, WalletAccess},
|
user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess},
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use prost_types::Timestamp as ProtoTimestamp;
|
use prost_types::Timestamp as ProtoTimestamp;
|
||||||
|
|||||||
194
server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs
Normal file
194
server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
use arbiter_proto::proto::{
|
||||||
|
shared::ClientInfo as ProtoClientMetadata,
|
||||||
|
user_agent::{
|
||||||
|
sdk_client::{
|
||||||
|
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
|
||||||
|
ConnectionRequest as ProtoSdkClientConnectionRequest,
|
||||||
|
ConnectionResponse as ProtoSdkClientConnectionResponse, Entry as ProtoSdkClientEntry,
|
||||||
|
Error as ProtoSdkClientError, GrantWalletAccess as ProtoSdkClientGrantWalletAccess,
|
||||||
|
List as ProtoSdkClientList, ListResponse as ProtoSdkClientListResponse,
|
||||||
|
ListWalletAccessResponse, RevokeWalletAccess as ProtoSdkClientRevokeWalletAccess,
|
||||||
|
list_response::Result as ProtoSdkClientListResult,
|
||||||
|
request::Payload as SdkClientRequestPayload,
|
||||||
|
response::Payload as SdkClientResponsePayload,
|
||||||
|
},
|
||||||
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use kameo::actor::ActorRef;
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::user_agent::{
|
||||||
|
OutOfBand, UserAgentSession,
|
||||||
|
session::connection::{
|
||||||
|
HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove,
|
||||||
|
HandleRevokeEvmWalletAccess, HandleSdkClientList,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db::models::NewEvmWalletAccess,
|
||||||
|
grpc::Convert,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> UserAgentResponsePayload {
|
||||||
|
UserAgentResponsePayload::SdkClient(proto_sdk_client::Response {
|
||||||
|
payload: Some(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload {
|
||||||
|
match oob {
|
||||||
|
OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response(
|
||||||
|
SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest {
|
||||||
|
pubkey: profile.pubkey.to_bytes().to_vec(),
|
||||||
|
info: Some(ProtoClientMetadata {
|
||||||
|
name: profile.metadata.name,
|
||||||
|
description: profile.metadata.description,
|
||||||
|
version: profile.metadata.version,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
OutOfBand::ClientConnectionCancel { pubkey } => wrap_sdk_client_response(
|
||||||
|
SdkClientResponsePayload::ConnectionCancel(ProtoSdkClientConnectionCancel {
|
||||||
|
pubkey: pubkey.to_bytes().to_vec(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn dispatch(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: proto_sdk_client::Request,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let Some(payload) = req.payload else {
|
||||||
|
return Err(Status::invalid_argument(
|
||||||
|
"Missing SDK client request payload",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
SdkClientRequestPayload::ConnectionResponse(resp) => {
|
||||||
|
handle_connection_response(actor, resp).await
|
||||||
|
}
|
||||||
|
SdkClientRequestPayload::Revoke(_) => Err(Status::unimplemented(
|
||||||
|
"SdkClientRevoke is not yet implemented",
|
||||||
|
)),
|
||||||
|
SdkClientRequestPayload::List(_) => handle_list(actor).await,
|
||||||
|
SdkClientRequestPayload::GrantWalletAccess(req) => {
|
||||||
|
handle_grant_wallet_access(actor, req).await
|
||||||
|
}
|
||||||
|
SdkClientRequestPayload::RevokeWalletAccess(req) => {
|
||||||
|
handle_revoke_wallet_access(actor, req).await
|
||||||
|
}
|
||||||
|
SdkClientRequestPayload::ListWalletAccess(_) => handle_list_wallet_access(actor).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_connection_response(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
resp: ProtoSdkClientConnectionResponse,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey)
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?;
|
||||||
|
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes)
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?;
|
||||||
|
|
||||||
|
actor
|
||||||
|
.ask(HandleNewClientApprove {
|
||||||
|
approved: resp.approved,
|
||||||
|
pubkey,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!(?err, "Failed to process client connection response");
|
||||||
|
Status::internal("Failed to process response")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_list(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let result = match actor.ask(HandleSdkClientList {}).await {
|
||||||
|
Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList {
|
||||||
|
clients: clients
|
||||||
|
.into_iter()
|
||||||
|
.map(|(client, metadata)| ProtoSdkClientEntry {
|
||||||
|
id: client.id,
|
||||||
|
pubkey: client.public_key,
|
||||||
|
info: Some(ProtoClientMetadata {
|
||||||
|
name: metadata.name,
|
||||||
|
description: metadata.description,
|
||||||
|
version: metadata.version,
|
||||||
|
}),
|
||||||
|
created_at: client.created_at.0.timestamp() as i32,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to list SDK clients");
|
||||||
|
ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_sdk_client_response(
|
||||||
|
SdkClientResponsePayload::List(ProtoSdkClientListResponse {
|
||||||
|
result: Some(result),
|
||||||
|
}),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_grant_wallet_access(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: ProtoSdkClientGrantWalletAccess,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let entries: Vec<NewEvmWalletAccess> = req.accesses.into_iter().map(|a| a.convert()).collect();
|
||||||
|
match actor.ask(HandleGrantEvmWalletAccess { entries }).await {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Successfully granted wallet access");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to grant wallet access");
|
||||||
|
Err(Status::internal("Failed to grant wallet access"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_revoke_wallet_access(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: ProtoSdkClientRevokeWalletAccess,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
match actor
|
||||||
|
.ask(HandleRevokeEvmWalletAccess {
|
||||||
|
entries: req.accesses,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Successfully revoked wallet access");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to revoke wallet access");
|
||||||
|
Err(Status::internal("Failed to revoke wallet access"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_list_wallet_access(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
match actor.ask(HandleListWalletAccess {}).await {
|
||||||
|
Ok(accesses) => Ok(Some(wrap_sdk_client_response(
|
||||||
|
SdkClientResponsePayload::ListWalletAccess(ListWalletAccessResponse {
|
||||||
|
accesses: accesses.into_iter().map(|a| a.convert()).collect(),
|
||||||
|
}),
|
||||||
|
))),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to list wallet access");
|
||||||
|
Err(Status::internal("Failed to list wallet access"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
server/crates/arbiter-server/src/grpc/user_agent/vault.rs
Normal file
180
server/crates/arbiter-server/src/grpc/user_agent/vault.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
|
||||||
|
use arbiter_proto::proto::user_agent::{
|
||||||
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
|
vault::{
|
||||||
|
self as proto_vault,
|
||||||
|
bootstrap::{
|
||||||
|
self as proto_bootstrap, BootstrapEncryptedKey as ProtoBootstrapEncryptedKey,
|
||||||
|
BootstrapResult as ProtoBootstrapResult,
|
||||||
|
},
|
||||||
|
request::Payload as VaultRequestPayload,
|
||||||
|
response::Payload as VaultResponsePayload,
|
||||||
|
unseal::{
|
||||||
|
self as proto_unseal, UnsealEncryptedKey as ProtoUnsealEncryptedKey,
|
||||||
|
UnsealResult as ProtoUnsealResult, UnsealStart,
|
||||||
|
request::Payload as UnsealRequestPayload, response::Payload as UnsealResponsePayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use kameo::{actor::ActorRef, error::SendError};
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::actors::{
|
||||||
|
keyholder::KeyHolderState,
|
||||||
|
user_agent::{
|
||||||
|
UserAgentSession,
|
||||||
|
session::connection::{
|
||||||
|
BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState,
|
||||||
|
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload {
|
||||||
|
UserAgentResponsePayload::Vault(proto_vault::Response {
|
||||||
|
payload: Some(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_unseal_response(payload: UnsealResponsePayload) -> UserAgentResponsePayload {
|
||||||
|
wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response {
|
||||||
|
payload: Some(payload),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> UserAgentResponsePayload {
|
||||||
|
wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response {
|
||||||
|
result: result.into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn dispatch(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: proto_vault::Request,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let Some(payload) = req.payload else {
|
||||||
|
return Err(Status::invalid_argument("Missing vault request payload"));
|
||||||
|
};
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
VaultRequestPayload::QueryState(_) => handle_query_vault_state(actor).await,
|
||||||
|
VaultRequestPayload::Unseal(req) => dispatch_unseal_request(actor, req).await,
|
||||||
|
VaultRequestPayload::Bootstrap(req) => handle_bootstrap_request(actor, req).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dispatch_unseal_request(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: proto_unseal::Request,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let Some(payload) = req.payload else {
|
||||||
|
return Err(Status::invalid_argument("Missing unseal request payload"));
|
||||||
|
};
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
UnsealRequestPayload::Start(req) => handle_unseal_start(actor, req).await,
|
||||||
|
UnsealRequestPayload::EncryptedKey(req) => handle_unseal_encrypted_key(actor, req).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_unseal_start(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: UnsealStart,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let client_pubkey = <[u8; 32]>::try_from(req.client_pubkey)
|
||||||
|
.map(x25519_dalek::PublicKey::from)
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?;
|
||||||
|
|
||||||
|
let response = actor
|
||||||
|
.ask(HandleUnsealRequest { client_pubkey })
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!(error = ?err, "Failed to handle unseal start request");
|
||||||
|
Status::internal("Failed to start unseal flow")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Some(wrap_unseal_response(UnsealResponsePayload::Start(
|
||||||
|
proto_unseal::UnsealStartResponse {
|
||||||
|
server_pubkey: response.server_pubkey.as_bytes().to_vec(),
|
||||||
|
},
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_unseal_encrypted_key(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: ProtoUnsealEncryptedKey,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let result = match actor
|
||||||
|
.ask(HandleUnsealEncryptedKey {
|
||||||
|
nonce: req.nonce,
|
||||||
|
ciphertext: req.ciphertext,
|
||||||
|
associated_data: req.associated_data,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => ProtoUnsealResult::Success,
|
||||||
|
Err(SendError::HandlerError(UnsealError::InvalidKey)) => ProtoUnsealResult::InvalidKey,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to handle unseal request");
|
||||||
|
return Err(Status::internal("Failed to unseal vault"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_unseal_response(UnsealResponsePayload::Result(
|
||||||
|
result.into(),
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_bootstrap_request(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: proto_bootstrap::Request,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let encrypted_key = req
|
||||||
|
.encrypted_key
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing bootstrap encrypted key"))?;
|
||||||
|
handle_bootstrap_encrypted_key(actor, encrypted_key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_bootstrap_encrypted_key(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
req: ProtoBootstrapEncryptedKey,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let result = match actor
|
||||||
|
.ask(HandleBootstrapEncryptedKey {
|
||||||
|
nonce: req.nonce,
|
||||||
|
ciphertext: req.ciphertext,
|
||||||
|
associated_data: req.associated_data,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => ProtoBootstrapResult::Success,
|
||||||
|
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => {
|
||||||
|
ProtoBootstrapResult::InvalidKey
|
||||||
|
}
|
||||||
|
Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => {
|
||||||
|
ProtoBootstrapResult::AlreadyBootstrapped
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to handle bootstrap request");
|
||||||
|
return Err(Status::internal("Failed to bootstrap vault"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_bootstrap_response(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_query_vault_state(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let state = match actor.ask(HandleQueryVaultState {}).await {
|
||||||
|
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
||||||
|
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
|
||||||
|
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to query vault state");
|
||||||
|
ProtoVaultState::Error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(wrap_vault_response(VaultResponsePayload::State(
|
||||||
|
state.into(),
|
||||||
|
))))
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ use crate::context::ServerContext;
|
|||||||
|
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
pub mod crypto;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod evm;
|
pub mod evm;
|
||||||
pub mod grpc;
|
pub mod grpc;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
|
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
|
||||||
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
|
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
|
||||||
use miette::miette;
|
|
||||||
use rustls::crypto::aws_lc_rs;
|
use rustls::crypto::aws_lc_rs;
|
||||||
use tonic::transport::{Identity, ServerTlsConfig};
|
use tonic::transport::{Identity, ServerTlsConfig};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -10,7 +10,8 @@ use tracing::info;
|
|||||||
const PORT: u16 = 50051;
|
const PORT: u16 = 50051;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> miette::Result<()> {
|
#[mutants::skip]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
aws_lc_rs::default_provider().install_default().unwrap();
|
aws_lc_rs::default_provider().install_default().unwrap();
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
@@ -46,11 +47,11 @@ async fn main() -> miette::Result<()> {
|
|||||||
|
|
||||||
tonic::transport::Server::builder()
|
tonic::transport::Server::builder()
|
||||||
.tls_config(tls)
|
.tls_config(tls)
|
||||||
.map_err(|err| miette!("Faild to setup TLS: {err}"))?
|
.map_err(|err| anyhow!("Failed to setup TLS: {err}"))?
|
||||||
.add_service(ArbiterServiceServer::new(Server::new(context)))
|
.add_service(ArbiterServiceServer::new(Server::new(context)))
|
||||||
.serve(addr)
|
.serve(addr)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| miette::miette!("gRPC server error: {e}"))?;
|
.map_err(|e| anyhow!("gRPC server error: {e}"))?;
|
||||||
|
|
||||||
unreachable!("gRPC server should run indefinitely");
|
unreachable!("gRPC server should run indefinitely");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
use arbiter_proto::ClientMetadata;
|
use arbiter_proto::ClientMetadata;
|
||||||
use arbiter_proto::transport::{Receiver, Sender};
|
use arbiter_proto::transport::{Receiver, Sender};
|
||||||
use arbiter_server::actors::GlobalActors;
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::client::{ClientConnection, auth, connect_client},
|
actors::{
|
||||||
db,
|
GlobalActors,
|
||||||
|
client::{ClientConnection, ClientCredentials, auth, connect_client},
|
||||||
|
keyholder::Bootstrap,
|
||||||
|
},
|
||||||
|
crypto::integrity,
|
||||||
|
db::{self, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
|
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
@@ -21,7 +26,8 @@ fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> Cli
|
|||||||
|
|
||||||
async fn insert_registered_client(
|
async fn insert_registered_client(
|
||||||
db: &db::DatabasePool,
|
db: &db::DatabasePool,
|
||||||
pubkey: Vec<u8>,
|
actors: &GlobalActors,
|
||||||
|
pubkey: ed25519_dalek::VerifyingKey,
|
||||||
metadata: &ClientMetadata,
|
metadata: &ClientMetadata,
|
||||||
) {
|
) {
|
||||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||||
@@ -37,23 +43,64 @@ async fn insert_registered_client(
|
|||||||
.get_result(&mut conn)
|
.get_result(&mut conn)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
insert_into(program_client::table)
|
let client_id: i32 = insert_into(program_client::table)
|
||||||
.values((
|
.values((
|
||||||
program_client::public_key.eq(pubkey),
|
program_client::public_key.eq(pubkey.to_bytes().to_vec()),
|
||||||
program_client::metadata_id.eq(metadata_id),
|
program_client::metadata_id.eq(metadata_id),
|
||||||
))
|
))
|
||||||
|
.returning(program_client::id)
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
integrity::sign_entity(
|
||||||
|
&mut conn,
|
||||||
|
&actors.key_holder,
|
||||||
|
&ClientCredentials { pubkey, nonce: 1 },
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) {
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
let sentinel_key = ed25519_dalek::SigningKey::generate(&mut rand::rng())
|
||||||
|
.verifying_key()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
|
insert_into(schema::useragent_client::table)
|
||||||
|
.values((
|
||||||
|
schema::useragent_client::public_key.eq(sentinel_key),
|
||||||
|
schema::useragent_client::key_type.eq(1i32),
|
||||||
|
))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
|
||||||
|
insert_bootstrap_sentinel_useragent(db).await;
|
||||||
|
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
actors
|
||||||
|
.key_holder
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
actors
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_unregistered_pubkey_rejected() {
|
pub async fn test_unregistered_pubkey_rejected() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = spawn_test_actors(&db).await;
|
||||||
let props = ClientConnection::new(db.clone(), actors);
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
let mut server_transport = server_transport;
|
let mut server_transport = server_transport;
|
||||||
@@ -78,20 +125,19 @@ pub async fn test_unregistered_pubkey_rejected() {
|
|||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_challenge_auth() {
|
pub async fn test_challenge_auth() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
|
let actors = spawn_test_actors(&db).await;
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
|
||||||
|
|
||||||
insert_registered_client(
|
insert_registered_client(
|
||||||
&db,
|
&db,
|
||||||
pubkey_bytes.clone(),
|
&actors,
|
||||||
|
new_key.verifying_key(),
|
||||||
&metadata("client", Some("desc"), Some("1.0.0")),
|
&metadata("client", Some("desc"), Some("1.0.0")),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
|
||||||
|
|
||||||
let props = ClientConnection::new(db.clone(), actors);
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
let mut server_transport = server_transport;
|
let mut server_transport = server_transport;
|
||||||
@@ -147,34 +193,13 @@ pub async fn test_challenge_auth() {
|
|||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_metadata_unchanged_does_not_append_history() {
|
pub async fn test_metadata_unchanged_does_not_append_history() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = spawn_test_actors(&db).await;
|
||||||
let props = ClientConnection::new(db.clone(), actors);
|
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
||||||
|
|
||||||
{
|
insert_registered_client(&db, &actors, new_key.verifying_key(), &requested).await;
|
||||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
|
||||||
let mut conn = db.get().await.unwrap();
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
let metadata_id: i32 = insert_into(client_metadata::table)
|
|
||||||
.values((
|
|
||||||
client_metadata::name.eq(&requested.name),
|
|
||||||
client_metadata::description.eq(&requested.description),
|
|
||||||
client_metadata::version.eq(&requested.version),
|
|
||||||
))
|
|
||||||
.returning(client_metadata::id)
|
|
||||||
.get_result(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
insert_into(program_client::table)
|
|
||||||
.values((
|
|
||||||
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
|
||||||
program_client::metadata_id.eq(metadata_id),
|
|
||||||
))
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
@@ -225,33 +250,18 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
|
|||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = spawn_test_actors(&db).await;
|
||||||
let props = ClientConnection::new(db.clone(), actors);
|
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
|
||||||
{
|
insert_registered_client(
|
||||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
&db,
|
||||||
let mut conn = db.get().await.unwrap();
|
&actors,
|
||||||
let metadata_id: i32 = insert_into(client_metadata::table)
|
new_key.verifying_key(),
|
||||||
.values((
|
&metadata("client", Some("old"), Some("1.0.0")),
|
||||||
client_metadata::name.eq("client"),
|
)
|
||||||
client_metadata::description.eq(Some("old")),
|
.await;
|
||||||
client_metadata::version.eq(Some("1.0.0")),
|
|
||||||
))
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
.returning(client_metadata::id)
|
|
||||||
.get_result(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
insert_into(program_client::table)
|
|
||||||
.values((
|
|
||||||
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
|
||||||
program_client::metadata_id.eq(metadata_id),
|
|
||||||
))
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
@@ -322,3 +332,59 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let actors = spawn_test_actors(&db).await;
|
||||||
|
|
||||||
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
||||||
|
|
||||||
|
{
|
||||||
|
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
let metadata_id: i32 = insert_into(client_metadata::table)
|
||||||
|
.values((
|
||||||
|
client_metadata::name.eq(&requested.name),
|
||||||
|
client_metadata::description.eq(&requested.description),
|
||||||
|
client_metadata::version.eq(&requested.version),
|
||||||
|
))
|
||||||
|
.returning(client_metadata::id)
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
insert_into(program_client::table)
|
||||||
|
.values((
|
||||||
|
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
||||||
|
program_client::metadata_id.eq(metadata_id),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let mut server_transport = server_transport;
|
||||||
|
connect_client(props, &mut server_transport).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
|
pubkey: new_key.verifying_key(),
|
||||||
|
metadata: requested,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = test_transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.expect("should receive auth rejection");
|
||||||
|
assert!(matches!(response, Err(auth::Error::IntegrityCheckFailed)));
|
||||||
|
|
||||||
|
task.await.unwrap();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::{Error, KeyHolder},
|
actors::keyholder::{Error, KeyHolder},
|
||||||
|
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
|
||||||
db::{self, models, schema},
|
db::{self, models, schema},
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
@@ -25,16 +26,10 @@ async fn test_bootstrap() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(row.schema_version, 1);
|
assert_eq!(row.schema_version, 1);
|
||||||
assert_eq!(
|
assert_eq!(row.tag, ROOT_KEY_TAG);
|
||||||
row.tag,
|
|
||||||
arbiter_server::actors::keyholder::encryption::v1::ROOT_KEY_TAG
|
|
||||||
);
|
|
||||||
assert!(!row.ciphertext.is_empty());
|
assert!(!row.ciphertext.is_empty());
|
||||||
assert!(!row.salt.is_empty());
|
assert!(!row.salt.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(row.data_encryption_nonce, Nonce::default().to_vec());
|
||||||
row.data_encryption_nonce,
|
|
||||||
arbiter_server::actors::keyholder::encryption::v1::Nonce::default().to_vec()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::{Error, encryption::v1},
|
actors::keyholder::Error,
|
||||||
|
crypto::encryption::v1::Nonce,
|
||||||
db::{self, models, schema},
|
db::{self, models, schema},
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
@@ -102,7 +103,7 @@ async fn test_nonce_never_reused() {
|
|||||||
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
|
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
|
||||||
|
|
||||||
for (i, row) in rows.iter().enumerate() {
|
for (i, row) in rows.iter().enumerate() {
|
||||||
let mut expected = v1::Nonce::default();
|
let mut expected = Nonce::default();
|
||||||
for _ in 0..=i {
|
for _ in 0..=i {
|
||||||
expected.increment();
|
expected.increment();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ use arbiter_server::{
|
|||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
bootstrap::GetToken,
|
bootstrap::GetToken,
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
keyholder::Bootstrap,
|
||||||
|
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth},
|
||||||
},
|
},
|
||||||
|
crypto::integrity,
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
@@ -18,6 +21,13 @@ use super::common::ChannelTransport;
|
|||||||
pub async fn test_bootstrap_token_auth() {
|
pub async fn test_bootstrap_token_auth() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
actors
|
||||||
|
.key_holder
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap();
|
let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap();
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
@@ -83,7 +93,6 @@ pub async fn test_bootstrap_invalid_token_auth() {
|
|||||||
Err(auth::Error::InvalidBootstrapToken)
|
Err(auth::Error::InvalidBootstrapToken)
|
||||||
));
|
));
|
||||||
|
|
||||||
// Verify no key was registered
|
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
let count: i64 = schema::useragent_client::table
|
let count: i64 = schema::useragent_client::table
|
||||||
.count()
|
.count()
|
||||||
@@ -98,21 +107,39 @@ pub async fn test_bootstrap_invalid_token_auth() {
|
|||||||
pub async fn test_challenge_auth() {
|
pub async fn test_challenge_auth() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
actors
|
||||||
|
.key_holder
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
// Pre-register key with key_type
|
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
insert_into(schema::useragent_client::table)
|
let id: i32 = insert_into(schema::useragent_client::table)
|
||||||
.values((
|
.values((
|
||||||
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
||||||
schema::useragent_client::key_type.eq(1i32),
|
schema::useragent_client::key_type.eq(1i32),
|
||||||
))
|
))
|
||||||
.execute(&mut conn)
|
.returning(schema::useragent_client::id)
|
||||||
|
.get_result(&mut conn)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
integrity::sign_entity(
|
||||||
|
&mut conn,
|
||||||
|
&actors.key_holder,
|
||||||
|
&UserAgentCredentials {
|
||||||
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
|
nonce: 1,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
@@ -122,7 +149,6 @@ pub async fn test_challenge_auth() {
|
|||||||
auth::authenticate(&mut props, server_transport).await
|
auth::authenticate(&mut props, server_transport).await
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send challenge request
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
@@ -131,7 +157,6 @@ pub async fn test_challenge_auth() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Read the challenge response
|
|
||||||
let response = test_transport
|
let response = test_transport
|
||||||
.recv()
|
.recv()
|
||||||
.await
|
.await
|
||||||
@@ -165,3 +190,138 @@ pub async fn test_challenge_auth() {
|
|||||||
|
|
||||||
task.await.unwrap().unwrap();
|
task.await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
|
||||||
|
actors
|
||||||
|
.key_holder
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
insert_into(schema::useragent_client::table)
|
||||||
|
.values((
|
||||||
|
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
||||||
|
schema::useragent_client::key_type.eq(1i32),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
|
let db_for_task = db.clone();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||||
|
auth::authenticate(&mut props, server_transport).await
|
||||||
|
});
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
|
bootstrap_token: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
task.await.unwrap(),
|
||||||
|
Err(auth::Error::Internal { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
pub async fn test_challenge_auth_rejects_invalid_signature() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
actors
|
||||||
|
.key_holder
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
let id: i32 = insert_into(schema::useragent_client::table)
|
||||||
|
.values((
|
||||||
|
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
||||||
|
schema::useragent_client::key_type.eq(1i32),
|
||||||
|
))
|
||||||
|
.returning(schema::useragent_client::id)
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
integrity::sign_entity(
|
||||||
|
&mut conn,
|
||||||
|
&actors.key_holder,
|
||||||
|
&UserAgentCredentials {
|
||||||
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
|
nonce: 1,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
|
let db_for_task = db.clone();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||||
|
auth::authenticate(&mut props, server_transport).await
|
||||||
|
});
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
|
bootstrap_token: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = test_transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.expect("should receive challenge");
|
||||||
|
let challenge = match response {
|
||||||
|
Ok(resp) => match resp {
|
||||||
|
auth::Outbound::AuthChallenge { nonce } => nonce,
|
||||||
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
|
},
|
||||||
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes);
|
||||||
|
let signature = new_key.sign(&wrong_challenge);
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(auth::Inbound::AuthChallengeSolution {
|
||||||
|
signature: signature.to_bytes().to_vec(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected_err = task.await.unwrap();
|
||||||
|
println!("Received expected error: {expected_err:#?}");
|
||||||
|
assert!(matches!(
|
||||||
|
expected_err,
|
||||||
|
Err(auth::Error::InvalidChallengeSolution)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ use arbiter_server::{
|
|||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
keyholder::{Bootstrap, Seal},
|
keyholder::{Bootstrap, Seal},
|
||||||
user_agent::{UserAgentSession, session::connection::{
|
user_agent::{
|
||||||
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
UserAgentSession,
|
||||||
}},
|
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:arbiter/proto/client.pb.dart';
|
import 'package:arbiter/proto/shared/client.pb.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hooks_riverpod/experimental/mutation.dart';
|
|
||||||
|
|
||||||
part 'callout_event.freezed.dart';
|
part 'callout_event.freezed.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:arbiter/features/callouts/active_callout.dart';
|
|||||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||||
import 'package:arbiter/features/callouts/types/sdk_connect_approve.dart'
|
import 'package:arbiter/features/callouts/types/sdk_connect_approve.dart'
|
||||||
as connect_approve;
|
as connect_approve;
|
||||||
import 'package:arbiter/proto/client.pb.dart';
|
import 'package:arbiter/proto/shared/client.pb.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'callout_manager.g.dart';
|
part 'callout_manager.g.dart';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk;
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
import 'package:arbiter/providers/connection/connection_manager.dart';
|
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -14,20 +15,27 @@ Stream<CalloutEvent> connectApproveEvents(Ref ref) async* {
|
|||||||
|
|
||||||
await for (final message in connection.outOfBandMessages) {
|
await for (final message in connection.outOfBandMessages) {
|
||||||
switch (message.whichPayload()) {
|
switch (message.whichPayload()) {
|
||||||
case UserAgentResponse_Payload.sdkClientConnectionRequest:
|
case UserAgentResponse_Payload.sdkClient:
|
||||||
final body = message.sdkClientConnectionRequest;
|
final sdkClientMessage = message.sdkClient;
|
||||||
final id = base64Encode(body.pubkey);
|
switch (sdkClientMessage.whichPayload()) {
|
||||||
yield CalloutEvent.added(
|
case ua_sdk.Response_Payload.connectionRequest:
|
||||||
id: 'connect_approve:$id',
|
final body = sdkClientMessage.connectionRequest;
|
||||||
data: CalloutData.connectApproval(
|
final id = base64Encode(body.pubkey);
|
||||||
pubkey: id,
|
yield CalloutEvent.added(
|
||||||
clientInfo: body.info,
|
id: 'connect_approve:$id',
|
||||||
),
|
data: CalloutData.connectApproval(
|
||||||
);
|
pubkey: id,
|
||||||
|
clientInfo: body.info,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
case UserAgentResponse_Payload.sdkClientConnectionCancel:
|
case ua_sdk.Response_Payload.connectionCancel:
|
||||||
final id = base64Encode(message.sdkClientConnectionCancel.pubkey);
|
final id = base64Encode(sdkClientMessage.connectionCancel.pubkey);
|
||||||
yield CalloutEvent.cancelled(id: 'connect_approve:$id');
|
yield CalloutEvent.cancelled(id: 'connect_approve:$id');
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -41,11 +49,14 @@ Future<void> sendDecision(Ref ref, String pubkey, bool approved) async {
|
|||||||
|
|
||||||
final bytes = base64Decode(pubkey);
|
final bytes = base64Decode(pubkey);
|
||||||
|
|
||||||
final req = UserAgentRequest(sdkClientConnectionResponse: SdkClientConnectionResponse(
|
final req = UserAgentRequest(
|
||||||
approved: approved,
|
sdkClient: ua_sdk.Request(
|
||||||
pubkey: bytes
|
connectionResponse: ua_sdk.ConnectionResponse(
|
||||||
));
|
approved: approved,
|
||||||
|
pubkey: bytes,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await connection.tell(req);
|
await connection.tell(req);
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -47,4 +47,4 @@ final class ConnectApproveEventsProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$connectApproveEventsHash() =>
|
String _$connectApproveEventsHash() =>
|
||||||
r'6a0998288afc0836a7c1701a983f64c33d318fd6';
|
r'abab87cc875a9a4834f836c2c0eba4aa7671d82e';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:arbiter/features/connection/connection.dart';
|
|||||||
import 'package:arbiter/features/connection/server_info_storage.dart';
|
import 'package:arbiter/features/connection/server_info_storage.dart';
|
||||||
import 'package:arbiter/features/identity/pk_manager.dart';
|
import 'package:arbiter/features/identity/pk_manager.dart';
|
||||||
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
import 'package:grpc/grpc.dart';
|
import 'package:grpc/grpc.dart';
|
||||||
import 'package:mtcore/markettakers.dart';
|
import 'package:mtcore/markettakers.dart';
|
||||||
@@ -12,22 +13,22 @@ import 'package:mtcore/markettakers.dart';
|
|||||||
class AuthorizationException implements Exception {
|
class AuthorizationException implements Exception {
|
||||||
const AuthorizationException(this.result);
|
const AuthorizationException(this.result);
|
||||||
|
|
||||||
final AuthResult result;
|
final ua_auth.AuthResult result;
|
||||||
|
|
||||||
String get message => switch (result) {
|
String get message => switch (result) {
|
||||||
AuthResult.AUTH_RESULT_INVALID_KEY =>
|
ua_auth.AuthResult.AUTH_RESULT_INVALID_KEY =>
|
||||||
'Authentication failed: this device key is not registered on the server.',
|
'Authentication failed: this device key is not registered on the server.',
|
||||||
AuthResult.AUTH_RESULT_INVALID_SIGNATURE =>
|
ua_auth.AuthResult.AUTH_RESULT_INVALID_SIGNATURE =>
|
||||||
'Authentication failed: the server rejected the signature for this device key.',
|
'Authentication failed: the server rejected the signature for this device key.',
|
||||||
AuthResult.AUTH_RESULT_BOOTSTRAP_REQUIRED =>
|
ua_auth.AuthResult.AUTH_RESULT_BOOTSTRAP_REQUIRED =>
|
||||||
'Authentication failed: the server requires bootstrap before this device can connect.',
|
'Authentication failed: the server requires bootstrap before this device can connect.',
|
||||||
AuthResult.AUTH_RESULT_TOKEN_INVALID =>
|
ua_auth.AuthResult.AUTH_RESULT_TOKEN_INVALID =>
|
||||||
'Authentication failed: the bootstrap token is invalid.',
|
'Authentication failed: the bootstrap token is invalid.',
|
||||||
AuthResult.AUTH_RESULT_INTERNAL =>
|
ua_auth.AuthResult.AUTH_RESULT_INTERNAL =>
|
||||||
'Authentication failed: the server hit an internal error.',
|
'Authentication failed: the server hit an internal error.',
|
||||||
AuthResult.AUTH_RESULT_UNSPECIFIED =>
|
ua_auth.AuthResult.AUTH_RESULT_UNSPECIFIED =>
|
||||||
'Authentication failed: the server returned an unspecified auth error.',
|
'Authentication failed: the server returned an unspecified auth error.',
|
||||||
AuthResult.AUTH_RESULT_SUCCESS => 'Authentication succeeded.',
|
ua_auth.AuthResult.AUTH_RESULT_SUCCESS => 'Authentication succeeded.',
|
||||||
_ => 'Authentication failed: ${result.name}.',
|
_ => 'Authentication failed: ${result.name}.',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,56 +58,76 @@ Future<Connection> connectAndAuthorize(
|
|||||||
);
|
);
|
||||||
final pubkey = await key.getPublicKey();
|
final pubkey = await key.getPublicKey();
|
||||||
|
|
||||||
final req = AuthChallengeRequest(
|
final req = ua_auth.AuthChallengeRequest(
|
||||||
pubkey: pubkey,
|
pubkey: pubkey,
|
||||||
bootstrapToken: bootstrapToken,
|
bootstrapToken: bootstrapToken,
|
||||||
keyType: switch (key.alg) {
|
keyType: switch (key.alg) {
|
||||||
KeyAlgorithm.rsa => KeyType.KEY_TYPE_RSA,
|
KeyAlgorithm.rsa => ua_auth.KeyType.KEY_TYPE_RSA,
|
||||||
KeyAlgorithm.ecdsa => KeyType.KEY_TYPE_ECDSA_SECP256K1,
|
KeyAlgorithm.ecdsa => ua_auth.KeyType.KEY_TYPE_ECDSA_SECP256K1,
|
||||||
KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519,
|
KeyAlgorithm.ed25519 => ua_auth.KeyType.KEY_TYPE_ED25519,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(authChallengeRequest: req),
|
UserAgentRequest(auth: ua_auth.Request(challengeRequest: req)),
|
||||||
);
|
);
|
||||||
talker.info(
|
talker.info(
|
||||||
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
|
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
|
||||||
);
|
);
|
||||||
talker.info('Received response from server, checking auth flow...');
|
talker.info('Received response from server, checking auth flow...');
|
||||||
|
|
||||||
if (response.hasAuthResult()) {
|
if (!response.hasAuth()) {
|
||||||
if (response.authResult != AuthResult.AUTH_RESULT_SUCCESS) {
|
throw ConnectionException(
|
||||||
throw AuthorizationException(response.authResult);
|
'Expected auth response, got ${response.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final authResponse = response.auth;
|
||||||
|
|
||||||
|
if (authResponse.hasResult()) {
|
||||||
|
if (authResponse.result != ua_auth.AuthResult.AUTH_RESULT_SUCCESS) {
|
||||||
|
throw AuthorizationException(authResponse.result);
|
||||||
}
|
}
|
||||||
talker.info('Authentication successful, connection established');
|
talker.info('Authentication successful, connection established');
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.hasAuthChallenge()) {
|
if (!authResponse.hasChallenge()) {
|
||||||
throw ConnectionException(
|
throw ConnectionException(
|
||||||
'Expected AuthChallengeResponse, got ${response.whichPayload()}',
|
'Expected auth challenge response, got ${authResponse.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final challenge = _formatChallenge(response.authChallenge, pubkey);
|
final challenge = _formatChallenge(authResponse.challenge, pubkey);
|
||||||
talker.info(
|
talker.info(
|
||||||
'Received auth challenge, signing with key ${base64Encode(pubkey)}',
|
'Received auth challenge, signing with key ${base64Encode(pubkey)}',
|
||||||
);
|
);
|
||||||
|
|
||||||
final signature = await key.sign(challenge);
|
final signature = await key.sign(challenge);
|
||||||
final solutionResponse = await connection.ask(
|
final solutionResponse = await connection.ask(
|
||||||
UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)),
|
UserAgentRequest(
|
||||||
|
auth: ua_auth.Request(
|
||||||
|
challengeSolution: ua_auth.AuthChallengeSolution(signature: signature),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
talker.info('Sent auth challenge solution, waiting for server response...');
|
talker.info('Sent auth challenge solution, waiting for server response...');
|
||||||
|
|
||||||
if (!solutionResponse.hasAuthResult()) {
|
if (!solutionResponse.hasAuth()) {
|
||||||
throw ConnectionException(
|
throw ConnectionException(
|
||||||
'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}',
|
'Expected auth solution response, got ${solutionResponse.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (solutionResponse.authResult != AuthResult.AUTH_RESULT_SUCCESS) {
|
|
||||||
throw AuthorizationException(solutionResponse.authResult);
|
final authSolutionResponse = solutionResponse.auth;
|
||||||
|
|
||||||
|
if (!authSolutionResponse.hasResult()) {
|
||||||
|
throw ConnectionException(
|
||||||
|
'Expected auth solution result, got ${authSolutionResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (authSolutionResponse.result != ua_auth.AuthResult.AUTH_RESULT_SUCCESS) {
|
||||||
|
throw AuthorizationException(authSolutionResponse.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
talker.info('Authentication successful, connection established');
|
talker.info('Authentication successful, connection established');
|
||||||
@@ -147,7 +168,7 @@ Future<Connection> _connect(StoredServerInfo serverInfo) async {
|
|||||||
return Connection(channel: channel, tx: tx, rx: rx);
|
return Connection(channel: channel, tx: tx, rx: rx);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> _formatChallenge(AuthChallenge challenge, List<int> pubkey) {
|
List<int> _formatChallenge(ua_auth.AuthChallenge challenge, List<int> pubkey) {
|
||||||
final encodedPubkey = base64Encode(pubkey);
|
final encodedPubkey = base64Encode(pubkey);
|
||||||
final payload = "${challenge.nonce}:$encodedPubkey";
|
final payload = "${challenge.nonce}:$encodedPubkey";
|
||||||
return utf8.encode(payload);
|
return utf8.encode(payload);
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import 'package:arbiter/features/connection/connection.dart';
|
import 'package:arbiter/features/connection/connection.dart';
|
||||||
import 'package:arbiter/proto/evm.pb.dart';
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent/evm.pb.dart' as ua_evm;
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||||
|
|
||||||
Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
|
Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(evmWalletList: Empty()),
|
UserAgentRequest(evm: ua_evm.Request(walletList: Empty())),
|
||||||
);
|
);
|
||||||
if (!response.hasEvmWalletList()) {
|
if (!response.hasEvm()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM wallet list response, got ${response.whichPayload()}',
|
'Expected EVM response, got ${response.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = response.evmWalletList;
|
final evmResponse = response.evm;
|
||||||
|
if (!evmResponse.hasWalletList()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected EVM wallet list response, got ${evmResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = evmResponse.walletList;
|
||||||
switch (result.whichResult()) {
|
switch (result.whichResult()) {
|
||||||
case WalletListResponse_Result.wallets:
|
case WalletListResponse_Result.wallets:
|
||||||
return result.wallets.wallets.toList(growable: false);
|
return result.wallets.wallets.toList(growable: false);
|
||||||
@@ -26,15 +34,22 @@ Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
|
|||||||
|
|
||||||
Future<void> createEvmWallet(Connection connection) async {
|
Future<void> createEvmWallet(Connection connection) async {
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(evmWalletCreate: Empty()),
|
UserAgentRequest(evm: ua_evm.Request(walletCreate: Empty())),
|
||||||
);
|
);
|
||||||
if (!response.hasEvmWalletCreate()) {
|
if (!response.hasEvm()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM wallet create response, got ${response.whichPayload()}',
|
'Expected EVM response, got ${response.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = response.evmWalletCreate;
|
final evmResponse = response.evm;
|
||||||
|
if (!evmResponse.hasWalletCreate()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected EVM wallet create response, got ${evmResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = evmResponse.walletCreate;
|
||||||
switch (result.whichResult()) {
|
switch (result.whichResult()) {
|
||||||
case WalletCreateResponse_Result.wallet:
|
case WalletCreateResponse_Result.wallet:
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import 'package:arbiter/features/connection/connection.dart';
|
import 'package:arbiter/features/connection/connection.dart';
|
||||||
import 'package:arbiter/proto/evm.pb.dart';
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent/evm.pb.dart' as ua_evm;
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
|
||||||
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
|
|
||||||
|
|
||||||
Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
|
Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
|
||||||
final request = EvmGrantListRequest();
|
final request = EvmGrantListRequest();
|
||||||
|
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(evmGrantList: request),
|
UserAgentRequest(evm: ua_evm.Request(grantList: request)),
|
||||||
);
|
);
|
||||||
if (!response.hasEvmGrantList()) {
|
if (!response.hasEvm()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM grant list response, got ${response.whichPayload()}',
|
'Expected EVM response, got ${response.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = response.evmGrantList;
|
final evmResponse = response.evm;
|
||||||
|
if (!evmResponse.hasGrantList()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected EVM grant list response, got ${evmResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = evmResponse.grantList;
|
||||||
switch (result.whichResult()) {
|
switch (result.whichResult()) {
|
||||||
case EvmGrantListResponse_Result.grants:
|
case EvmGrantListResponse_Result.grants:
|
||||||
return result.grants.grants.toList(growable: false);
|
return result.grants.grants.toList(growable: false);
|
||||||
@@ -33,36 +39,56 @@ Future<int> createEvmGrant(
|
|||||||
required SpecificGrant specific,
|
required SpecificGrant specific,
|
||||||
}) async {
|
}) async {
|
||||||
final request = UserAgentRequest(
|
final request = UserAgentRequest(
|
||||||
evmGrantCreate: EvmGrantCreateRequest(
|
evm: ua_evm.Request(
|
||||||
shared: sharedSettings,
|
grantCreate: EvmGrantCreateRequest(
|
||||||
specific: specific,
|
shared: sharedSettings,
|
||||||
|
specific: specific,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final resp = await connection.ask(request);
|
final resp = await connection.ask(request);
|
||||||
|
|
||||||
if (!resp.hasEvmGrantCreate()) {
|
if (!resp.hasEvm()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM grant create response, got ${resp.whichPayload()}',
|
'Expected EVM response, got ${resp.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = resp.evmGrantCreate;
|
final evmResponse = resp.evm;
|
||||||
|
if (!evmResponse.hasGrantCreate()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected EVM grant create response, got ${evmResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = evmResponse.grantCreate;
|
||||||
|
|
||||||
return result.grantId;
|
return result.grantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
|
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)),
|
UserAgentRequest(
|
||||||
|
evm: ua_evm.Request(
|
||||||
|
grantDelete: EvmGrantDeleteRequest(grantId: grantId),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!response.hasEvmGrantDelete()) {
|
if (!response.hasEvm()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM grant delete response, got ${response.whichPayload()}',
|
'Expected EVM response, got ${response.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = response.evmGrantDelete;
|
final evmResponse = response.evm;
|
||||||
|
if (!evmResponse.hasGrantDelete()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected EVM grant delete response, got ${evmResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = evmResponse.grantDelete;
|
||||||
switch (result.whichResult()) {
|
switch (result.whichResult()) {
|
||||||
case EvmGrantDeleteResponse_Result.ok:
|
case EvmGrantDeleteResponse_Result.ok:
|
||||||
return;
|
return;
|
||||||
@@ -73,13 +99,6 @@ Future<void> deleteEvmGrant(Connection connection, int grantId) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timestamp _toTimestamp(DateTime value) {
|
|
||||||
final utc = value.toUtc();
|
|
||||||
return Timestamp()
|
|
||||||
..seconds = Int64(utc.millisecondsSinceEpoch ~/ 1000)
|
|
||||||
..nanos = (utc.microsecondsSinceEpoch % 1000000) * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _describeGrantError(EvmError error) {
|
String _describeGrantError(EvmError error) {
|
||||||
return switch (error) {
|
return switch (error) {
|
||||||
EvmError.EVM_ERROR_VAULT_SEALED =>
|
EvmError.EVM_ERROR_VAULT_SEALED =>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:arbiter/features/connection/connection.dart';
|
import 'package:arbiter/features/connection/connection.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk;
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||||
|
|
||||||
@@ -7,31 +8,47 @@ Future<Set<int>> readClientWalletAccess(
|
|||||||
required int clientId,
|
required int clientId,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(listWalletAccess: Empty()),
|
UserAgentRequest(
|
||||||
|
sdkClient: ua_sdk.Request(listWalletAccess: Empty()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!response.hasListWalletAccessResponse()) {
|
if (!response.hasSdkClient()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected list wallet access response, got ${response.whichPayload()}',
|
'Expected SDK client response, got ${response.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final sdkClientResponse = response.sdkClient;
|
||||||
|
if (!sdkClientResponse.hasListWalletAccess()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected list wallet access response, got ${sdkClientResponse.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
for (final entry in response.listWalletAccessResponse.accesses)
|
for (final entry in sdkClientResponse.listWalletAccess.accesses)
|
||||||
if (entry.access.sdkClientId == clientId) entry.access.walletId,
|
if (entry.access.sdkClientId == clientId) entry.access.walletId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SdkClientWalletAccess>> listAllWalletAccesses(
|
Future<List<ua_sdk.WalletAccessEntry>> listAllWalletAccesses(
|
||||||
Connection connection,
|
Connection connection,
|
||||||
) async {
|
) async {
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(listWalletAccess: Empty()),
|
UserAgentRequest(
|
||||||
|
sdkClient: ua_sdk.Request(listWalletAccess: Empty()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!response.hasListWalletAccessResponse()) {
|
if (!response.hasSdkClient()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected list wallet access response, got ${response.whichPayload()}',
|
'Expected SDK client response, got ${response.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return response.listWalletAccessResponse.accesses.toList(growable: false);
|
final sdkClientResponse = response.sdkClient;
|
||||||
|
if (!sdkClientResponse.hasListWalletAccess()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected list wallet access response, got ${sdkClientResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sdkClientResponse.listWalletAccess.accesses.toList(growable: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> writeClientWalletAccess(
|
Future<void> writeClientWalletAccess(
|
||||||
@@ -47,11 +64,13 @@ Future<void> writeClientWalletAccess(
|
|||||||
if (toGrant.isNotEmpty) {
|
if (toGrant.isNotEmpty) {
|
||||||
await connection.tell(
|
await connection.tell(
|
||||||
UserAgentRequest(
|
UserAgentRequest(
|
||||||
grantWalletAccess: SdkClientGrantWalletAccess(
|
sdkClient: ua_sdk.Request(
|
||||||
accesses: [
|
grantWalletAccess: ua_sdk.GrantWalletAccess(
|
||||||
for (final walletId in toGrant)
|
accesses: [
|
||||||
WalletAccess(sdkClientId: clientId, walletId: walletId),
|
for (final walletId in toGrant)
|
||||||
],
|
ua_sdk.WalletAccess(sdkClientId: clientId, walletId: walletId),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -60,11 +79,12 @@ Future<void> writeClientWalletAccess(
|
|||||||
if (toRevoke.isNotEmpty) {
|
if (toRevoke.isNotEmpty) {
|
||||||
await connection.tell(
|
await connection.tell(
|
||||||
UserAgentRequest(
|
UserAgentRequest(
|
||||||
revokeWalletAccess: SdkClientRevokeWalletAccess(
|
sdkClient: ua_sdk.Request(
|
||||||
accesses: [
|
revokeWalletAccess: ua_sdk.RevokeWalletAccess(
|
||||||
for (final walletId in toRevoke)
|
accesses: [
|
||||||
walletId
|
for (final walletId in toRevoke) walletId,
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import 'package:arbiter/features/connection/connection.dart';
|
import 'package:arbiter/features/connection/connection.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent/vault/bootstrap.pb.dart' as ua_bootstrap;
|
||||||
|
import 'package:arbiter/proto/user_agent/vault/unseal.pb.dart' as ua_unseal;
|
||||||
|
import 'package:arbiter/proto/user_agent/vault/vault.pb.dart' as ua_vault;
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
import 'package:cryptography/cryptography.dart';
|
import 'package:cryptography/cryptography.dart';
|
||||||
|
|
||||||
const _vaultKeyAssociatedData = 'arbiter.vault.password';
|
const _vaultKeyAssociatedData = 'arbiter.vault.password';
|
||||||
|
|
||||||
Future<BootstrapResult> bootstrapVault(
|
Future<ua_bootstrap.BootstrapResult> bootstrapVault(
|
||||||
Connection connection,
|
Connection connection,
|
||||||
String password,
|
String password,
|
||||||
) async {
|
) async {
|
||||||
@@ -12,39 +15,76 @@ Future<BootstrapResult> bootstrapVault(
|
|||||||
|
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(
|
UserAgentRequest(
|
||||||
bootstrapEncryptedKey: BootstrapEncryptedKey(
|
vault: ua_vault.Request(
|
||||||
nonce: encryptedKey.nonce,
|
bootstrap: ua_bootstrap.Request(
|
||||||
ciphertext: encryptedKey.ciphertext,
|
encryptedKey: ua_bootstrap.BootstrapEncryptedKey(
|
||||||
associatedData: encryptedKey.associatedData,
|
nonce: encryptedKey.nonce,
|
||||||
|
ciphertext: encryptedKey.ciphertext,
|
||||||
|
associatedData: encryptedKey.associatedData,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (!response.hasBootstrapResult()) {
|
if (!response.hasVault()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected bootstrap result, got ${response.whichPayload()}',
|
'Expected vault response, got ${response.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.bootstrapResult;
|
final vaultResponse = response.vault;
|
||||||
|
if (!vaultResponse.hasBootstrap()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected bootstrap result, got ${vaultResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bootstrapResponse = vaultResponse.bootstrap;
|
||||||
|
if (!bootstrapResponse.hasResult()) {
|
||||||
|
throw Exception('Expected bootstrap result payload.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bootstrapResponse.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UnsealResult> unsealVault(Connection connection, String password) async {
|
Future<ua_unseal.UnsealResult> unsealVault(
|
||||||
|
Connection connection,
|
||||||
|
String password,
|
||||||
|
) async {
|
||||||
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||||
|
|
||||||
final response = await connection.ask(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(
|
UserAgentRequest(
|
||||||
unsealEncryptedKey: UnsealEncryptedKey(
|
vault: ua_vault.Request(
|
||||||
nonce: encryptedKey.nonce,
|
unseal: ua_unseal.Request(
|
||||||
ciphertext: encryptedKey.ciphertext,
|
encryptedKey: ua_unseal.UnsealEncryptedKey(
|
||||||
associatedData: encryptedKey.associatedData,
|
nonce: encryptedKey.nonce,
|
||||||
|
ciphertext: encryptedKey.ciphertext,
|
||||||
|
associatedData: encryptedKey.associatedData,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (!response.hasUnsealResult()) {
|
if (!response.hasVault()) {
|
||||||
throw Exception('Expected unseal result, got ${response.whichPayload()}');
|
throw Exception('Expected vault response, got ${response.whichPayload()}');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.unsealResult;
|
final vaultResponse = response.vault;
|
||||||
|
if (!vaultResponse.hasUnseal()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected unseal result, got ${vaultResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final unsealResponse = vaultResponse.unseal;
|
||||||
|
if (!unsealResponse.hasResult()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected unseal result payload, got ${unsealResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unsealResponse.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
|
Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
|
||||||
@@ -57,16 +97,36 @@ Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
|
|||||||
final clientPublicKey = await clientKeyPair.extractPublicKey();
|
final clientPublicKey = await clientKeyPair.extractPublicKey();
|
||||||
|
|
||||||
final handshakeResponse = await connection.ask(
|
final handshakeResponse = await connection.ask(
|
||||||
UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)),
|
UserAgentRequest(
|
||||||
|
vault: ua_vault.Request(
|
||||||
|
unseal: ua_unseal.Request(
|
||||||
|
start: ua_unseal.UnsealStart(clientPubkey: clientPublicKey.bytes),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!handshakeResponse.hasUnsealStartResponse()) {
|
if (!handshakeResponse.hasVault()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}',
|
'Expected vault response, got ${handshakeResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final vaultResponse = handshakeResponse.vault;
|
||||||
|
if (!vaultResponse.hasUnseal()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected unseal handshake response, got ${vaultResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final unsealResponse = vaultResponse.unseal;
|
||||||
|
if (!unsealResponse.hasStart()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected unseal handshake payload, got ${unsealResponse.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final serverPublicKey = SimplePublicKey(
|
final serverPublicKey = SimplePublicKey(
|
||||||
handshakeResponse.unsealStartResponse.serverPubkey,
|
unsealResponse.start.serverPubkey,
|
||||||
type: KeyPairType.x25519,
|
type: KeyPairType.x25519,
|
||||||
);
|
);
|
||||||
final sharedSecret = await keyExchange.sharedSecretKey(
|
final sharedSecret = await keyExchange.sharedSecretKey(
|
||||||
|
|||||||
@@ -13,305 +13,26 @@
|
|||||||
import 'dart:core' as $core;
|
import 'dart:core' as $core;
|
||||||
|
|
||||||
import 'package:protobuf/protobuf.dart' as $pb;
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart' as $0;
|
|
||||||
|
|
||||||
import 'client.pbenum.dart';
|
import 'client/auth.pb.dart' as $0;
|
||||||
import 'evm.pb.dart' as $1;
|
import 'client/evm.pb.dart' as $2;
|
||||||
|
import 'client/vault.pb.dart' as $1;
|
||||||
|
|
||||||
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
|
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
|
||||||
|
|
||||||
export 'client.pbenum.dart';
|
enum ClientRequest_Payload { auth, vault, evm, notSet }
|
||||||
|
|
||||||
class ClientInfo extends $pb.GeneratedMessage {
|
|
||||||
factory ClientInfo({
|
|
||||||
$core.String? name,
|
|
||||||
$core.String? description,
|
|
||||||
$core.String? version,
|
|
||||||
}) {
|
|
||||||
final result = create();
|
|
||||||
if (name != null) result.name = name;
|
|
||||||
if (description != null) result.description = description;
|
|
||||||
if (version != null) result.version = version;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientInfo._();
|
|
||||||
|
|
||||||
factory ClientInfo.fromBuffer($core.List<$core.int> data,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromBuffer(data, registry);
|
|
||||||
factory ClientInfo.fromJson($core.String json,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromJson(json, registry);
|
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
|
||||||
_omitMessageNames ? '' : 'ClientInfo',
|
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
|
||||||
createEmptyInstance: create)
|
|
||||||
..aOS(1, _omitFieldNames ? '' : 'name')
|
|
||||||
..aOS(2, _omitFieldNames ? '' : 'description')
|
|
||||||
..aOS(3, _omitFieldNames ? '' : 'version')
|
|
||||||
..hasRequiredFields = false;
|
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
ClientInfo clone() => deepCopy();
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
ClientInfo copyWith(void Function(ClientInfo) updates) =>
|
|
||||||
super.copyWith((message) => updates(message as ClientInfo)) as ClientInfo;
|
|
||||||
|
|
||||||
@$core.override
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static ClientInfo create() => ClientInfo._();
|
|
||||||
@$core.override
|
|
||||||
ClientInfo createEmptyInstance() => create();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static ClientInfo getDefault() => _defaultInstance ??=
|
|
||||||
$pb.GeneratedMessage.$_defaultFor<ClientInfo>(create);
|
|
||||||
static ClientInfo? _defaultInstance;
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.String get name => $_getSZ(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set name($core.String value) => $_setString(0, value);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasName() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearName() => $_clearField(1);
|
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.String get description => $_getSZ(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
set description($core.String value) => $_setString(1, value);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.bool hasDescription() => $_has(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
void clearDescription() => $_clearField(2);
|
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
$core.String get version => $_getSZ(2);
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
set version($core.String value) => $_setString(2, value);
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
$core.bool hasVersion() => $_has(2);
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
void clearVersion() => $_clearField(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthChallengeRequest extends $pb.GeneratedMessage {
|
|
||||||
factory AuthChallengeRequest({
|
|
||||||
$core.List<$core.int>? pubkey,
|
|
||||||
ClientInfo? clientInfo,
|
|
||||||
}) {
|
|
||||||
final result = create();
|
|
||||||
if (pubkey != null) result.pubkey = pubkey;
|
|
||||||
if (clientInfo != null) result.clientInfo = clientInfo;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthChallengeRequest._();
|
|
||||||
|
|
||||||
factory AuthChallengeRequest.fromBuffer($core.List<$core.int> data,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromBuffer(data, registry);
|
|
||||||
factory AuthChallengeRequest.fromJson($core.String json,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromJson(json, registry);
|
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
|
||||||
_omitMessageNames ? '' : 'AuthChallengeRequest',
|
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
|
||||||
createEmptyInstance: create)
|
|
||||||
..a<$core.List<$core.int>>(
|
|
||||||
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
|
|
||||||
..aOM<ClientInfo>(2, _omitFieldNames ? '' : 'clientInfo',
|
|
||||||
subBuilder: ClientInfo.create)
|
|
||||||
..hasRequiredFields = false;
|
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
AuthChallengeRequest clone() => deepCopy();
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
AuthChallengeRequest copyWith(void Function(AuthChallengeRequest) updates) =>
|
|
||||||
super.copyWith((message) => updates(message as AuthChallengeRequest))
|
|
||||||
as AuthChallengeRequest;
|
|
||||||
|
|
||||||
@$core.override
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static AuthChallengeRequest create() => AuthChallengeRequest._();
|
|
||||||
@$core.override
|
|
||||||
AuthChallengeRequest createEmptyInstance() => create();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static AuthChallengeRequest getDefault() => _defaultInstance ??=
|
|
||||||
$pb.GeneratedMessage.$_defaultFor<AuthChallengeRequest>(create);
|
|
||||||
static AuthChallengeRequest? _defaultInstance;
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.List<$core.int> get pubkey => $_getN(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set pubkey($core.List<$core.int> value) => $_setBytes(0, value);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasPubkey() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearPubkey() => $_clearField(1);
|
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
ClientInfo get clientInfo => $_getN(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
set clientInfo(ClientInfo value) => $_setField(2, value);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.bool hasClientInfo() => $_has(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
void clearClientInfo() => $_clearField(2);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
ClientInfo ensureClientInfo() => $_ensure(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthChallenge extends $pb.GeneratedMessage {
|
|
||||||
factory AuthChallenge({
|
|
||||||
$core.List<$core.int>? pubkey,
|
|
||||||
$core.int? nonce,
|
|
||||||
}) {
|
|
||||||
final result = create();
|
|
||||||
if (pubkey != null) result.pubkey = pubkey;
|
|
||||||
if (nonce != null) result.nonce = nonce;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthChallenge._();
|
|
||||||
|
|
||||||
factory AuthChallenge.fromBuffer($core.List<$core.int> data,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromBuffer(data, registry);
|
|
||||||
factory AuthChallenge.fromJson($core.String json,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromJson(json, registry);
|
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
|
||||||
_omitMessageNames ? '' : 'AuthChallenge',
|
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
|
||||||
createEmptyInstance: create)
|
|
||||||
..a<$core.List<$core.int>>(
|
|
||||||
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
|
|
||||||
..aI(2, _omitFieldNames ? '' : 'nonce')
|
|
||||||
..hasRequiredFields = false;
|
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
AuthChallenge clone() => deepCopy();
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
AuthChallenge copyWith(void Function(AuthChallenge) updates) =>
|
|
||||||
super.copyWith((message) => updates(message as AuthChallenge))
|
|
||||||
as AuthChallenge;
|
|
||||||
|
|
||||||
@$core.override
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static AuthChallenge create() => AuthChallenge._();
|
|
||||||
@$core.override
|
|
||||||
AuthChallenge createEmptyInstance() => create();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static AuthChallenge getDefault() => _defaultInstance ??=
|
|
||||||
$pb.GeneratedMessage.$_defaultFor<AuthChallenge>(create);
|
|
||||||
static AuthChallenge? _defaultInstance;
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.List<$core.int> get pubkey => $_getN(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set pubkey($core.List<$core.int> value) => $_setBytes(0, value);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasPubkey() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearPubkey() => $_clearField(1);
|
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.int get nonce => $_getIZ(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
set nonce($core.int value) => $_setSignedInt32(1, value);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.bool hasNonce() => $_has(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
void clearNonce() => $_clearField(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthChallengeSolution extends $pb.GeneratedMessage {
|
|
||||||
factory AuthChallengeSolution({
|
|
||||||
$core.List<$core.int>? signature,
|
|
||||||
}) {
|
|
||||||
final result = create();
|
|
||||||
if (signature != null) result.signature = signature;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthChallengeSolution._();
|
|
||||||
|
|
||||||
factory AuthChallengeSolution.fromBuffer($core.List<$core.int> data,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromBuffer(data, registry);
|
|
||||||
factory AuthChallengeSolution.fromJson($core.String json,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromJson(json, registry);
|
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
|
||||||
_omitMessageNames ? '' : 'AuthChallengeSolution',
|
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
|
||||||
createEmptyInstance: create)
|
|
||||||
..a<$core.List<$core.int>>(
|
|
||||||
1, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY)
|
|
||||||
..hasRequiredFields = false;
|
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
AuthChallengeSolution clone() => deepCopy();
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
AuthChallengeSolution copyWith(
|
|
||||||
void Function(AuthChallengeSolution) updates) =>
|
|
||||||
super.copyWith((message) => updates(message as AuthChallengeSolution))
|
|
||||||
as AuthChallengeSolution;
|
|
||||||
|
|
||||||
@$core.override
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static AuthChallengeSolution create() => AuthChallengeSolution._();
|
|
||||||
@$core.override
|
|
||||||
AuthChallengeSolution createEmptyInstance() => create();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static AuthChallengeSolution getDefault() => _defaultInstance ??=
|
|
||||||
$pb.GeneratedMessage.$_defaultFor<AuthChallengeSolution>(create);
|
|
||||||
static AuthChallengeSolution? _defaultInstance;
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.List<$core.int> get signature => $_getN(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set signature($core.List<$core.int> value) => $_setBytes(0, value);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasSignature() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearSignature() => $_clearField(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ClientRequest_Payload {
|
|
||||||
authChallengeRequest,
|
|
||||||
authChallengeSolution,
|
|
||||||
queryVaultState,
|
|
||||||
notSet
|
|
||||||
}
|
|
||||||
|
|
||||||
class ClientRequest extends $pb.GeneratedMessage {
|
class ClientRequest extends $pb.GeneratedMessage {
|
||||||
factory ClientRequest({
|
factory ClientRequest({
|
||||||
AuthChallengeRequest? authChallengeRequest,
|
$0.Request? auth,
|
||||||
AuthChallengeSolution? authChallengeSolution,
|
$1.Request? vault,
|
||||||
$0.Empty? queryVaultState,
|
$2.Request? evm,
|
||||||
$core.int? requestId,
|
$core.int? requestId,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (authChallengeRequest != null)
|
if (auth != null) result.auth = auth;
|
||||||
result.authChallengeRequest = authChallengeRequest;
|
if (vault != null) result.vault = vault;
|
||||||
if (authChallengeSolution != null)
|
if (evm != null) result.evm = evm;
|
||||||
result.authChallengeSolution = authChallengeSolution;
|
|
||||||
if (queryVaultState != null) result.queryVaultState = queryVaultState;
|
|
||||||
if (requestId != null) result.requestId = requestId;
|
if (requestId != null) result.requestId = requestId;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -327,9 +48,9 @@ class ClientRequest extends $pb.GeneratedMessage {
|
|||||||
|
|
||||||
static const $core.Map<$core.int, ClientRequest_Payload>
|
static const $core.Map<$core.int, ClientRequest_Payload>
|
||||||
_ClientRequest_PayloadByTag = {
|
_ClientRequest_PayloadByTag = {
|
||||||
1: ClientRequest_Payload.authChallengeRequest,
|
1: ClientRequest_Payload.auth,
|
||||||
2: ClientRequest_Payload.authChallengeSolution,
|
2: ClientRequest_Payload.vault,
|
||||||
3: ClientRequest_Payload.queryVaultState,
|
3: ClientRequest_Payload.evm,
|
||||||
0: ClientRequest_Payload.notSet
|
0: ClientRequest_Payload.notSet
|
||||||
};
|
};
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
@@ -337,14 +58,12 @@ class ClientRequest extends $pb.GeneratedMessage {
|
|||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..oo(0, [1, 2, 3])
|
..oo(0, [1, 2, 3])
|
||||||
..aOM<AuthChallengeRequest>(
|
..aOM<$0.Request>(1, _omitFieldNames ? '' : 'auth',
|
||||||
1, _omitFieldNames ? '' : 'authChallengeRequest',
|
subBuilder: $0.Request.create)
|
||||||
subBuilder: AuthChallengeRequest.create)
|
..aOM<$1.Request>(2, _omitFieldNames ? '' : 'vault',
|
||||||
..aOM<AuthChallengeSolution>(
|
subBuilder: $1.Request.create)
|
||||||
2, _omitFieldNames ? '' : 'authChallengeSolution',
|
..aOM<$2.Request>(3, _omitFieldNames ? '' : 'evm',
|
||||||
subBuilder: AuthChallengeSolution.create)
|
subBuilder: $2.Request.create)
|
||||||
..aOM<$0.Empty>(3, _omitFieldNames ? '' : 'queryVaultState',
|
|
||||||
subBuilder: $0.Empty.create)
|
|
||||||
..aI(4, _omitFieldNames ? '' : 'requestId')
|
..aI(4, _omitFieldNames ? '' : 'requestId')
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@@ -378,38 +97,37 @@ class ClientRequest extends $pb.GeneratedMessage {
|
|||||||
void clearPayload() => $_clearField($_whichOneof(0));
|
void clearPayload() => $_clearField($_whichOneof(0));
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
AuthChallengeRequest get authChallengeRequest => $_getN(0);
|
$0.Request get auth => $_getN(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
set authChallengeRequest(AuthChallengeRequest value) => $_setField(1, value);
|
set auth($0.Request value) => $_setField(1, value);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.bool hasAuthChallengeRequest() => $_has(0);
|
$core.bool hasAuth() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearAuthChallengeRequest() => $_clearField(1);
|
void clearAuth() => $_clearField(1);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
AuthChallengeRequest ensureAuthChallengeRequest() => $_ensure(0);
|
$0.Request ensureAuth() => $_ensure(0);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
AuthChallengeSolution get authChallengeSolution => $_getN(1);
|
$1.Request get vault => $_getN(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
set authChallengeSolution(AuthChallengeSolution value) =>
|
set vault($1.Request value) => $_setField(2, value);
|
||||||
$_setField(2, value);
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.bool hasAuthChallengeSolution() => $_has(1);
|
$core.bool hasVault() => $_has(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearAuthChallengeSolution() => $_clearField(2);
|
void clearVault() => $_clearField(2);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
AuthChallengeSolution ensureAuthChallengeSolution() => $_ensure(1);
|
$1.Request ensureVault() => $_ensure(1);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$0.Empty get queryVaultState => $_getN(2);
|
$2.Request get evm => $_getN(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
set queryVaultState($0.Empty value) => $_setField(3, value);
|
set evm($2.Request value) => $_setField(3, value);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.bool hasQueryVaultState() => $_has(2);
|
$core.bool hasEvm() => $_has(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
void clearQueryVaultState() => $_clearField(3);
|
void clearEvm() => $_clearField(3);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$0.Empty ensureQueryVaultState() => $_ensure(2);
|
$2.Request ensureEvm() => $_ensure(2);
|
||||||
|
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
$core.int get requestId => $_getIZ(3);
|
$core.int get requestId => $_getIZ(3);
|
||||||
@@ -421,32 +139,19 @@ class ClientRequest extends $pb.GeneratedMessage {
|
|||||||
void clearRequestId() => $_clearField(4);
|
void clearRequestId() => $_clearField(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ClientResponse_Payload {
|
enum ClientResponse_Payload { auth, vault, evm, notSet }
|
||||||
authChallenge,
|
|
||||||
authResult,
|
|
||||||
evmSignTransaction,
|
|
||||||
evmAnalyzeTransaction,
|
|
||||||
vaultState,
|
|
||||||
notSet
|
|
||||||
}
|
|
||||||
|
|
||||||
class ClientResponse extends $pb.GeneratedMessage {
|
class ClientResponse extends $pb.GeneratedMessage {
|
||||||
factory ClientResponse({
|
factory ClientResponse({
|
||||||
AuthChallenge? authChallenge,
|
$0.Response? auth,
|
||||||
AuthResult? authResult,
|
$1.Response? vault,
|
||||||
$1.EvmSignTransactionResponse? evmSignTransaction,
|
$2.Response? evm,
|
||||||
$1.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction,
|
|
||||||
VaultState? vaultState,
|
|
||||||
$core.int? requestId,
|
$core.int? requestId,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (authChallenge != null) result.authChallenge = authChallenge;
|
if (auth != null) result.auth = auth;
|
||||||
if (authResult != null) result.authResult = authResult;
|
if (vault != null) result.vault = vault;
|
||||||
if (evmSignTransaction != null)
|
if (evm != null) result.evm = evm;
|
||||||
result.evmSignTransaction = evmSignTransaction;
|
|
||||||
if (evmAnalyzeTransaction != null)
|
|
||||||
result.evmAnalyzeTransaction = evmAnalyzeTransaction;
|
|
||||||
if (vaultState != null) result.vaultState = vaultState;
|
|
||||||
if (requestId != null) result.requestId = requestId;
|
if (requestId != null) result.requestId = requestId;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -462,30 +167,22 @@ class ClientResponse extends $pb.GeneratedMessage {
|
|||||||
|
|
||||||
static const $core.Map<$core.int, ClientResponse_Payload>
|
static const $core.Map<$core.int, ClientResponse_Payload>
|
||||||
_ClientResponse_PayloadByTag = {
|
_ClientResponse_PayloadByTag = {
|
||||||
1: ClientResponse_Payload.authChallenge,
|
1: ClientResponse_Payload.auth,
|
||||||
2: ClientResponse_Payload.authResult,
|
2: ClientResponse_Payload.vault,
|
||||||
3: ClientResponse_Payload.evmSignTransaction,
|
3: ClientResponse_Payload.evm,
|
||||||
4: ClientResponse_Payload.evmAnalyzeTransaction,
|
|
||||||
6: ClientResponse_Payload.vaultState,
|
|
||||||
0: ClientResponse_Payload.notSet
|
0: ClientResponse_Payload.notSet
|
||||||
};
|
};
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
_omitMessageNames ? '' : 'ClientResponse',
|
_omitMessageNames ? '' : 'ClientResponse',
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..oo(0, [1, 2, 3, 4, 6])
|
..oo(0, [1, 2, 3])
|
||||||
..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge',
|
..aOM<$0.Response>(1, _omitFieldNames ? '' : 'auth',
|
||||||
subBuilder: AuthChallenge.create)
|
subBuilder: $0.Response.create)
|
||||||
..aE<AuthResult>(2, _omitFieldNames ? '' : 'authResult',
|
..aOM<$1.Response>(2, _omitFieldNames ? '' : 'vault',
|
||||||
enumValues: AuthResult.values)
|
subBuilder: $1.Response.create)
|
||||||
..aOM<$1.EvmSignTransactionResponse>(
|
..aOM<$2.Response>(3, _omitFieldNames ? '' : 'evm',
|
||||||
3, _omitFieldNames ? '' : 'evmSignTransaction',
|
subBuilder: $2.Response.create)
|
||||||
subBuilder: $1.EvmSignTransactionResponse.create)
|
|
||||||
..aOM<$1.EvmAnalyzeTransactionResponse>(
|
|
||||||
4, _omitFieldNames ? '' : 'evmAnalyzeTransaction',
|
|
||||||
subBuilder: $1.EvmAnalyzeTransactionResponse.create)
|
|
||||||
..aE<VaultState>(6, _omitFieldNames ? '' : 'vaultState',
|
|
||||||
enumValues: VaultState.values)
|
|
||||||
..aI(7, _omitFieldNames ? '' : 'requestId')
|
..aI(7, _omitFieldNames ? '' : 'requestId')
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@@ -511,76 +208,52 @@ class ClientResponse extends $pb.GeneratedMessage {
|
|||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
@$pb.TagNumber(4)
|
|
||||||
@$pb.TagNumber(6)
|
|
||||||
ClientResponse_Payload whichPayload() =>
|
ClientResponse_Payload whichPayload() =>
|
||||||
_ClientResponse_PayloadByTag[$_whichOneof(0)]!;
|
_ClientResponse_PayloadByTag[$_whichOneof(0)]!;
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
@$pb.TagNumber(4)
|
|
||||||
@$pb.TagNumber(6)
|
|
||||||
void clearPayload() => $_clearField($_whichOneof(0));
|
void clearPayload() => $_clearField($_whichOneof(0));
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
AuthChallenge get authChallenge => $_getN(0);
|
$0.Response get auth => $_getN(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
set authChallenge(AuthChallenge value) => $_setField(1, value);
|
set auth($0.Response value) => $_setField(1, value);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.bool hasAuthChallenge() => $_has(0);
|
$core.bool hasAuth() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearAuthChallenge() => $_clearField(1);
|
void clearAuth() => $_clearField(1);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
AuthChallenge ensureAuthChallenge() => $_ensure(0);
|
$0.Response ensureAuth() => $_ensure(0);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
AuthResult get authResult => $_getN(1);
|
$1.Response get vault => $_getN(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
set authResult(AuthResult value) => $_setField(2, value);
|
set vault($1.Response value) => $_setField(2, value);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.bool hasAuthResult() => $_has(1);
|
$core.bool hasVault() => $_has(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearAuthResult() => $_clearField(2);
|
void clearVault() => $_clearField(2);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$1.Response ensureVault() => $_ensure(1);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$1.EvmSignTransactionResponse get evmSignTransaction => $_getN(2);
|
$2.Response get evm => $_getN(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
set evmSignTransaction($1.EvmSignTransactionResponse value) =>
|
set evm($2.Response value) => $_setField(3, value);
|
||||||
$_setField(3, value);
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.bool hasEvmSignTransaction() => $_has(2);
|
$core.bool hasEvm() => $_has(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
void clearEvmSignTransaction() => $_clearField(3);
|
void clearEvm() => $_clearField(3);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$1.EvmSignTransactionResponse ensureEvmSignTransaction() => $_ensure(2);
|
$2.Response ensureEvm() => $_ensure(2);
|
||||||
|
|
||||||
@$pb.TagNumber(4)
|
|
||||||
$1.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3);
|
|
||||||
@$pb.TagNumber(4)
|
|
||||||
set evmAnalyzeTransaction($1.EvmAnalyzeTransactionResponse value) =>
|
|
||||||
$_setField(4, value);
|
|
||||||
@$pb.TagNumber(4)
|
|
||||||
$core.bool hasEvmAnalyzeTransaction() => $_has(3);
|
|
||||||
@$pb.TagNumber(4)
|
|
||||||
void clearEvmAnalyzeTransaction() => $_clearField(4);
|
|
||||||
@$pb.TagNumber(4)
|
|
||||||
$1.EvmAnalyzeTransactionResponse ensureEvmAnalyzeTransaction() => $_ensure(3);
|
|
||||||
|
|
||||||
@$pb.TagNumber(6)
|
|
||||||
VaultState get vaultState => $_getN(4);
|
|
||||||
@$pb.TagNumber(6)
|
|
||||||
set vaultState(VaultState value) => $_setField(6, value);
|
|
||||||
@$pb.TagNumber(6)
|
|
||||||
$core.bool hasVaultState() => $_has(4);
|
|
||||||
@$pb.TagNumber(6)
|
|
||||||
void clearVaultState() => $_clearField(6);
|
|
||||||
|
|
||||||
@$pb.TagNumber(7)
|
@$pb.TagNumber(7)
|
||||||
$core.int get requestId => $_getIZ(5);
|
$core.int get requestId => $_getIZ(3);
|
||||||
@$pb.TagNumber(7)
|
@$pb.TagNumber(7)
|
||||||
set requestId($core.int value) => $_setSignedInt32(5, value);
|
set requestId($core.int value) => $_setSignedInt32(3, value);
|
||||||
@$pb.TagNumber(7)
|
@$pb.TagNumber(7)
|
||||||
$core.bool hasRequestId() => $_has(5);
|
$core.bool hasRequestId() => $_has(3);
|
||||||
@$pb.TagNumber(7)
|
@$pb.TagNumber(7)
|
||||||
void clearRequestId() => $_clearField(7);
|
void clearRequestId() => $_clearField(7);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user