Compare commits

..

11 Commits

Author SHA1 Message Date
CleverWild
0098c3c08a refactor(server::crypto): use fixed-size [u8; 32] and KeyCell throughout seal key API
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-06-12 21:15:07 +02:00
CleverWild
a3b98ca024 fix(server::tests): tighten unseal test seal_key params to &[u8; 32] 2026-06-12 21:11:48 +02:00
CleverWild
0d364d1951 feat(server::grpc): wire Shamir committee bootstrap and unseal proto messages
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline failed
Adds DeclareCommittee and ContributePassphrase variants to bootstrap.proto,
ContributePassphrase to unseal.proto, and AwaitingContributions result codes
to both. Implements corresponding inbound converters and outbound reply
mappings. VaultGate handlers delegate to VaultCoordinator.
2026-06-12 19:43:17 +02:00
CleverWild
6f65c907a3 feat(server): introduce VaultCoordinator for multi-operator Shamir bootstrap/unseal
VaultCoordinator collects operator passphrases, splits the seal key into
Shamir shares on bootstrap (encrypting each share with the operator's
passphrase via Argon2 + XChaCha20-Poly1305), and reconstructs the seal
key from threshold shares on unseal. Adds vsss-rs 5.4.0 and rand_core 0.6
dependencies.
2026-06-12 19:43:09 +02:00
CleverWild
9764b0d5ce refactor(server::actors::vault): clean up Bootstrap/TryUnseal, remove Bootstrapping state
Bootstrap and TryUnseal now accept a SafeCell<Vec<u8>> seal key directly.
The Bootstrapping intermediate state is removed — multi-operator coordination
is the responsibility of VaultCoordinator, which calls Bootstrap atomically
once all shares are collected.
2026-06-12 19:43:02 +02:00
CleverWild
50fe18d6ce feat(server::crypto): add Shamir secret sharing utilities
Wraps vsss_rs Gf256::split_array / combine_array into thin split_key /
combine_shares helpers. Also widens derive_key salt parameter from &[u8;16]
to &[u8] to accommodate the 32-byte share salts.
2026-06-12 19:42:56 +02:00
CleverWild
3e5f0cb3df feat(server::db): add share_salt column to operator table
Each operator row now stores a 32-byte random salt used to derive the
per-operator share encryption key from their passphrase (Argon2 KDF).
2026-06-12 19:42:49 +02:00
CleverWild
34850137df feat(server::actors::evm): implement operator_delete_grant
Sets revoked_at on the evm_basic_grant row; returns NotFound if the grant
does not exist. Wires the handler in OperatorSession replacing the todo!().
2026-06-12 19:42:43 +02:00
CleverWild
d1b96c8409 fix(server::peers::operator::auth): make ChallengeContext pub for smlang state machine
smlang generates a public state enum whose variants contain ChallengeContext,
requiring the type itself to be fully public. Also tightens the wildcard arm
in client auth to an exhaustive match.
2026-06-12 19:42:37 +02:00
Skipper
9dbb18ae82 WIP: some things
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline failed
2026-05-20 21:04:16 +02:00
Skipper
a773255935 refactor(server::db): introduced newtype wrappers for entity id's in database 2026-05-04 19:35:27 +02:00
70 changed files with 1114 additions and 1003 deletions

View File

@@ -22,3 +22,5 @@ run = '''
dart pub global activate protoc_plugin && \ dart pub global activate protoc_plugin && \
protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ $(find protobufs -name '*.proto' | sort) protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ $(find protobufs -name '*.proto' | sort)
''' '''
[tasks.generate_schema]

View File

@@ -8,15 +8,28 @@ message BootstrapEncryptedKey {
bytes associated_data = 3; bytes associated_data = 3;
} }
message DeclareCommittee {
uint32 count = 1;
}
message ContributePassphrase {
bytes passphrase = 1;
}
enum BootstrapResult { enum BootstrapResult {
BOOTSTRAP_RESULT_UNSPECIFIED = 0; BOOTSTRAP_RESULT_UNSPECIFIED = 0;
BOOTSTRAP_RESULT_SUCCESS = 1; BOOTSTRAP_RESULT_SUCCESS = 1;
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2; BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2;
BOOTSTRAP_RESULT_INVALID_KEY = 3; BOOTSTRAP_RESULT_INVALID_KEY = 3;
BOOTSTRAP_RESULT_AWAITING_CONTRIBUTIONS = 4;
} }
message Request { message Request {
BootstrapEncryptedKey encrypted_key = 2; oneof payload {
BootstrapEncryptedKey encrypted_key = 2;
DeclareCommittee declare_committee = 3;
ContributePassphrase contribute_passphrase = 4;
}
} }
message Response { message Response {

View File

@@ -15,17 +15,23 @@ message UnsealEncryptedKey {
bytes associated_data = 3; bytes associated_data = 3;
} }
message ContributePassphrase {
bytes passphrase = 1;
}
enum UnsealResult { enum UnsealResult {
UNSEAL_RESULT_UNSPECIFIED = 0; UNSEAL_RESULT_UNSPECIFIED = 0;
UNSEAL_RESULT_SUCCESS = 1; UNSEAL_RESULT_SUCCESS = 1;
UNSEAL_RESULT_INVALID_KEY = 2; UNSEAL_RESULT_INVALID_KEY = 2;
UNSEAL_RESULT_UNBOOTSTRAPPED = 3; UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
UNSEAL_RESULT_AWAITING_CONTRIBUTIONS = 4;
} }
message Request { message Request {
oneof payload { oneof payload {
UnsealStart start = 1; UnsealStart start = 1;
UnsealEncryptedKey encrypted_key = 2; UnsealEncryptedKey encrypted_key = 2;
ContributePassphrase contribute_passphrase = 3;
} }
} }

View File

@@ -5,7 +5,8 @@ package arbiter.shared;
enum VaultState { enum VaultState {
VAULT_STATE_UNSPECIFIED = 0; VAULT_STATE_UNSPECIFIED = 0;
VAULT_STATE_UNBOOTSTRAPPED = 1; VAULT_STATE_UNBOOTSTRAPPED = 1;
VAULT_STATE_SEALED = 2; VAULT_STATE_BOOSTRAPPING = 2;
VAULT_STATE_UNSEALED = 3; VAULT_STATE_SEALED = 3;
VAULT_STATE_ERROR = 4; VAULT_STATE_UNSEALED = 4;
VAULT_STATE_ERROR = 5;
} }

279
server/Cargo.lock generated
View File

@@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [ dependencies = [
"crypto-common 0.1.7", "crypto-common 0.1.7",
"generic-array", "generic-array 0.14.7",
] ]
[[package]] [[package]]
@@ -719,7 +719,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn 2.0.117",
"trybuild",
] ]
[[package]] [[package]]
@@ -772,6 +771,7 @@ dependencies = [
"proptest", "proptest",
"prost-types", "prost-types",
"rand 0.10.1", "rand 0.10.1",
"rand_core 0.6.4",
"rcgen", "rcgen",
"restructed", "restructed",
"rstest", "rstest",
@@ -787,6 +787,7 @@ dependencies = [
"tonic", "tonic",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"vsss-rs",
"x25519-dalek 2.0.1", "x25519-dalek 2.0.1",
] ]
@@ -1284,7 +1285,7 @@ version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.7",
] ]
[[package]] [[package]]
@@ -1613,8 +1614,22 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.7",
"rand_core 0.6.4", "rand_core 0.6.4",
"serdect 0.2.0",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-bigint"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96272c2ff28b807e09250b180ad1fb7889a3258f7455759b5c3c58b719467130"
dependencies = [
"num-traits",
"rand_core 0.6.4",
"serdect 0.3.0",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -1625,7 +1640,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.7",
"rand_core 0.6.4", "rand_core 0.6.4",
"typenum", "typenum",
] ]
@@ -1928,7 +1943,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.7",
] ]
[[package]] [[package]]
@@ -1966,12 +1981,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "dissimilar"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e"
[[package]] [[package]]
name = "downcast-rs" name = "downcast-rs"
version = "2.0.2" version = "2.0.2"
@@ -2014,7 +2023,7 @@ dependencies = [
"digest 0.10.7", "digest 0.10.7",
"elliptic-curve", "elliptic-curve",
"rfc6979", "rfc6979",
"serdect", "serdect 0.2.0",
"signature 2.2.0", "signature 2.2.0",
"spki 0.7.3", "spki 0.7.3",
] ]
@@ -2047,16 +2056,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [ dependencies = [
"base16ct", "base16ct",
"crypto-bigint", "crypto-bigint 0.5.5",
"digest 0.10.7", "digest 0.10.7",
"ff", "ff",
"generic-array", "generic-array 0.14.7",
"group", "group",
"hkdf",
"pkcs8 0.10.2", "pkcs8 0.10.2",
"rand_core 0.6.4", "rand_core 0.6.4",
"sec1", "sec1",
"serdect", "serdect 0.2.0",
"subtle", "subtle",
"tap",
"zeroize",
]
[[package]]
name = "elliptic-curve-tools"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de2b6fae800f08032a6ea32995b52925b1d451bff9d445c8ab2932323277faf"
dependencies = [
"elliptic-curve",
"heapless",
"hex",
"multiexp",
"serde",
"zeroize", "zeroize",
] ]
@@ -2130,6 +2155,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [ dependencies = [
"bitvec",
"rand_core 0.6.4", "rand_core 0.6.4",
"subtle", "subtle",
] ]
@@ -2330,6 +2356,17 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "generic-array"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649"
dependencies = [
"rustversion",
"serde_core",
"typenum",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.17" version = "0.2.17"
@@ -2413,6 +2450,15 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "hash32"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@@ -2453,6 +2499,16 @@ version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heapless"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [
"hash32",
"stable_deref_trait",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -2480,6 +2536,15 @@ dependencies = [
"arrayvec", "arrayvec",
] ]
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac 0.12.1",
]
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.12.1" version = "0.12.1"
@@ -2550,6 +2615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5"
dependencies = [ dependencies = [
"ctutils", "ctutils",
"serde",
"typenum", "typenum",
"zeroize", "zeroize",
] ]
@@ -2815,7 +2881,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.7",
] ]
[[package]] [[package]]
@@ -2954,7 +3020,7 @@ dependencies = [
"ecdsa", "ecdsa",
"elliptic-curve", "elliptic-curve",
"once_cell", "once_cell",
"serdect", "serdect 0.2.0",
"sha2 0.10.9", "sha2 0.10.9",
"signature 2.2.0", "signature 2.2.0",
] ]
@@ -2962,7 +3028,7 @@ dependencies = [
[[package]] [[package]]
name = "kameo" name = "kameo"
version = "0.20.0" version = "0.20.0"
source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a" source = "git+https://github.com/hdbg/kameo.git?rev=3e18ba2#3e18ba24023d0422034e60ff2ea1ecd49e8c3c93"
dependencies = [ dependencies = [
"downcast-rs", "downcast-rs",
"dyn-clone", "dyn-clone",
@@ -2976,7 +3042,7 @@ dependencies = [
[[package]] [[package]]
name = "kameo_actors" name = "kameo_actors"
version = "0.5.0" version = "0.5.0"
source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a" source = "git+https://github.com/hdbg/kameo.git?rev=3e18ba2#3e18ba24023d0422034e60ff2ea1ecd49e8c3c93"
dependencies = [ dependencies = [
"futures", "futures",
"glob", "glob",
@@ -2988,9 +3054,8 @@ dependencies = [
[[package]] [[package]]
name = "kameo_macros" name = "kameo_macros"
version = "0.20.0" version = "0.20.0"
source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a" source = "git+https://github.com/hdbg/kameo.git?rev=3e18ba2#3e18ba24023d0422034e60ff2ea1ecd49e8c3c93"
dependencies = [ dependencies = [
"darling 0.23.0",
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3207,7 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d"
dependencies = [ dependencies = [
"serde", "serde",
"toml 0.9.12+spec-1.1.0", "toml",
] ]
[[package]] [[package]]
@@ -3297,6 +3362,20 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "multiexp"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb"
dependencies = [
"ff",
"group",
"rand_core 0.6.4",
"rustversion",
"std-shims",
"zeroize",
]
[[package]] [[package]]
name = "multimap" name = "multimap"
version = "0.10.1" version = "0.10.1"
@@ -3328,6 +3407,20 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.6" version = "0.4.6"
@@ -3336,6 +3429,19 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [ dependencies = [
"num-integer", "num-integer",
"num-traits", "num-traits",
"rand 0.8.6",
"serde",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
"rand 0.8.6",
"serde",
] ]
[[package]] [[package]]
@@ -3353,6 +3459,29 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
"serde",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -4461,9 +4590,9 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [ dependencies = [
"base16ct", "base16ct",
"der 0.7.10", "der 0.7.10",
"generic-array", "generic-array 0.14.7",
"pkcs8 0.10.2", "pkcs8 0.10.2",
"serdect", "serdect 0.2.0",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -4629,6 +4758,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serdect"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53"
dependencies = [
"base16ct",
"serde",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@@ -4794,6 +4933,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[package]] [[package]]
name = "spki" name = "spki"
version = "0.7.3" version = "0.7.3"
@@ -4838,6 +4983,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "std-shims"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "227c4f8561598188d0df96dbe749824576174bba278b5b6bb2eacff1066067d0"
dependencies = [
"hashbrown 0.16.1",
"rustversion",
"spin",
]
[[package]] [[package]]
name = "string_morph" name = "string_morph"
version = "0.1.0" version = "0.1.0"
@@ -4979,12 +5135,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-triple"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.27.0" version = "3.27.0"
@@ -4998,15 +5148,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "terminal_size" name = "terminal_size"
version = "0.4.4" version = "0.4.4"
@@ -5229,21 +5370,6 @@ dependencies = [
"winnow 0.7.15", "winnow 0.7.15",
] ]
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [
"indexmap 2.14.0",
"serde_core",
"serde_spanned",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow 1.0.2",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.5+spec-1.1.0" version = "0.7.5+spec-1.1.0"
@@ -5283,12 +5409,6 @@ dependencies = [
"winnow 1.0.2", "winnow 1.0.2",
] ]
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]] [[package]]
name = "tonic" name = "tonic"
version = "0.14.5" version = "0.14.5"
@@ -5476,22 +5596,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "trybuild"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0710d4dfbeae4f9c390baa784c49858a7468fa433f3fe5d0ec5ebef651cf59f9"
dependencies = [
"dissimilar",
"glob",
"serde",
"serde_derive",
"serde_json",
"target-triple",
"termcolor",
"toml 1.1.2+spec-1.1.0",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.20.0" version = "1.20.0"
@@ -5633,6 +5737,27 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vsss-rs"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ec751bdcc8bda099e269b24cc6b4ad14f9ce8b0490c1599174070e792ecd70c"
dependencies = [
"crypto-bigint 0.5.5",
"crypto-bigint 0.6.1",
"elliptic-curve",
"elliptic-curve-tools",
"generic-array 1.4.1",
"hex",
"hybrid-array",
"num",
"rand_core 0.6.4",
"serde",
"sha3 0.10.9",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "wait-timeout" name = "wait-timeout"
version = "0.2.1" version = "0.2.1"

View File

@@ -12,8 +12,8 @@ base64 = "0.22.1"
chrono = { version = "0.4.44", features = ["serde"] } chrono = { version = "0.4.44", features = ["serde"] }
futures = "0.3.32" futures = "0.3.32"
k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] } k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
kameo = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"} kameo = {git = "https://github.com/hdbg/kameo.git", rev = "3e18ba2"}
kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"} kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "3e18ba2"}
hmac = "0.13.0" hmac = "0.13.0"
miette = { version = "7.6.0", features = ["fancy", "serde"] } miette = { version = "7.6.0", features = ["fancy", "serde"] }
ml-dsa = { version = "0.1.0-rc.9", features = ["zeroize"] } ml-dsa = { version = "0.1.0-rc.9", features = ["zeroize"] }
@@ -76,7 +76,6 @@ needless_pass_by_ref_mut = "allow"
pub_underscore_fields = "allow" pub_underscore_fields = "allow"
redundant_pub_crate = "allow" redundant_pub_crate = "allow"
uninhabited_references = "allow" # safe with unsafe_code = "forbid" and standard uninhabited pattern (match *self {}) uninhabited_references = "allow" # safe with unsafe_code = "forbid" and standard uninhabited pattern (match *self {})
too-many-lines = "allow" # this is a very common pattern in server code, and it's not always possible to break it down into smaller modules without hurting readability
# restriction lints # restriction lints
alloc_instead_of_core = "warn" alloc_instead_of_core = "warn"
@@ -107,7 +106,6 @@ indexing_slicing = "warn"
infinite_loop = "warn" infinite_loop = "warn"
inline_asm_x86_att_syntax = "warn" inline_asm_x86_att_syntax = "warn"
inline_asm_x86_intel_syntax = "warn" inline_asm_x86_intel_syntax = "warn"
integer_division = "warn"
large_include_file = "warn" large_include_file = "warn"
lossy_float_literal = "warn" lossy_float_literal = "warn"
map_with_unused_argument_over_ranges = "warn" map_with_unused_argument_over_ranges = "warn"
@@ -129,6 +127,7 @@ rc_buffer = "warn"
rc_mutex = "warn" rc_mutex = "warn"
redundant_test_prefix = "warn" redundant_test_prefix = "warn"
redundant_type_annotations = "warn" redundant_type_annotations = "warn"
ref_patterns = "warn"
renamed_function_params = "warn" renamed_function_params = "warn"
rest_pat_in_fully_bound_structs = "warn" rest_pat_in_fully_bound_structs = "warn"
return_and_then = "warn" return_and_then = "warn"

View File

@@ -100,7 +100,7 @@ async fn send_auth_challenge_solution(
key: &SigningKey, key: &SigningKey,
challenge: AuthChallenge, challenge: AuthChallenge,
) -> Result<(), AuthError> { ) -> Result<(), AuthError> {
let timestamp = DateTime::from_timestamp_nanos(challenge.timestamp_nanos.cast_signed()); let timestamp = DateTime::from_timestamp_nanos(challenge.timestamp_nanos as i64);
let challenge = authn::AuthChallenge { let challenge = authn::AuthChallenge {
nonce: *challenge nonce: *challenge
.random .random

View File

@@ -1,49 +0,0 @@
use crate::hashing::Hashable;
/// Marks a struct as a participant in the database integrity system.
///
/// Implementors are protected by an HMAC-SHA256 MAC stored in the
/// `integrity_envelope` table. The MAC is computed over:
///
/// ```text
/// HMAC-SHA256(key, len(KIND) || KIND || len(entity_id) || entity_id || VERSION || SHA256(Hashable))
/// ```
///
/// Both `KIND` and `VERSION` act as domain separators — they prevent a valid
/// MAC for one entity type or schema version from being accepted for another.
///
/// # Deriving
///
/// Use `#[derive(Integrable)]` with the `#[integrable(kind = "...")]` attribute.
/// `VERSION` is computed automatically as an FNV-1a hash of the struct's field
/// names and types, so it changes whenever the schema changes without any manual
/// bookkeeping.
///
/// ```rust,ignore
/// #[derive(Hashable, Integrable)]
/// #[integrable(kind = "operator_credentials")]
/// pub struct OperatorCredentials {
/// pub pubkey: PublicKey,
/// }
/// ```
///
/// # Upgrading schema
///
/// When fields are added, removed, or reordered, `VERSION` changes automatically.
/// Existing MAC records in the database will return [`PayloadVersionMismatch`] on
/// verification — this is the signal to re-sign all rows for this `KIND` as part
/// of a migration.
///
/// [`PayloadVersionMismatch`]: crate::integrity::Integrable
pub trait Integrable: Hashable {
/// Stable name of this entity type as stored in `integrity_envelope.entity_kind`.
///
/// Must be a valid schema name: starts with a letter, contains only `[a-zA-Z0-9_]`,
/// and must be globally unique across all `Integrable` types in the system.
const KIND: &'static str;
/// FNV-1a hash of the struct's field names and types at the time the derive
/// macro ran. Changes automatically when the schema changes, invalidating
/// existing MACs and signalling that a migration is required.
const VERSION: i32;
}

View File

@@ -1,7 +1,6 @@
#[cfg(feature = "authn")] #[cfg(feature = "authn")]
pub mod authn; pub mod authn;
pub mod hashing; pub mod hashing;
pub mod integrity;
#[cfg(feature = "safecell")] #[cfg(feature = "safecell")]
pub mod safecell; pub mod safecell;

View File

@@ -22,7 +22,7 @@ pub trait SafeCellHandle<T> {
fn read(&mut self) -> Self::CellRead<'_>; fn read(&mut self) -> Self::CellRead<'_>;
fn write(&mut self) -> Self::CellWrite<'_>; fn write(&mut self) -> Self::CellWrite<'_>;
fn new_inline<F>(f: F) -> Self fn new_inline_default<F>(f: F) -> Self
where where
Self: Sized, Self: Sized,
T: Default, T: Default,
@@ -36,6 +36,14 @@ pub trait SafeCellHandle<T> {
cell cell
} }
fn new_inline<F>(f: Box<F>) -> Self
where
Self: Sized,
F: for<'a> FnOnce() -> T,
{
Self::new(f())
}
#[inline(always)] #[inline(always)]
fn read_inline<F, R>(&mut self, f: F) -> R fn read_inline<F, R>(&mut self, f: F) -> R
where where

View File

@@ -14,7 +14,6 @@ syn = { version = "2.0", features = ["derive", "fold", "full", "visit-mut"] }
[dev-dependencies] [dev-dependencies]
arbiter-crypto = { path = "../arbiter-crypto" } arbiter-crypto = { path = "../arbiter-crypto" }
trybuild = { version = "1.0", features = ["diff"] }
[lints] [lints]
workspace = true workspace = true

View File

@@ -53,16 +53,32 @@ struct FieldAccess {
fn collect_field_accesses(struct_data: &DataStruct) -> Vec<FieldAccess> { fn collect_field_accesses(struct_data: &DataStruct) -> Vec<FieldAccess> {
match &struct_data.fields { match &struct_data.fields {
Fields::Named(fields) => crate::utils::sorted_named_fields(fields) Fields::Named(fields) => {
.into_iter() // Keep deterministic alphabetical order for named fields.
.map(|field| { // Do not remove this sort, because it keeps hash output stable regardless of source order.
let name = field.ident.as_ref().unwrap(); let mut named_fields = fields
FieldAccess { .named
.iter()
.map(|field| {
let name = field
.ident
.as_ref()
.expect("Fields::Named(fields) must have names")
.clone();
(name.to_string(), name)
})
.collect::<Vec<_>>();
named_fields.sort_by(|a, b| a.0.cmp(&b.0));
named_fields
.into_iter()
.map(|(_, name)| FieldAccess {
access: quote! { #name }, access: quote! { #name },
span: name.span(), span: name.span(),
} })
}) .collect()
.collect(), }
Fields::Unnamed(fields) => fields Fields::Unnamed(fields) => fields
.unnamed .unnamed
.iter() .iter()

View File

@@ -1,134 +0,0 @@
use crate::utils::INTEGRABLE_TRAIT_PATH;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, LitStr, spanned::Spanned as _};
struct IntegrableAttr {
kind: String,
}
impl IntegrableAttr {
fn from_attrs(
attrs: &[syn::Attribute],
ident_span: proc_macro2::Span,
) -> Result<Self, syn::Error> {
let mut kind: Option<String> = None;
let mut found = false;
for attr in attrs {
if !attr.path().is_ident("integrable") {
continue;
}
if found {
return Err(syn::Error::new(attr.span(), "duplicate #[integrable] attribute"));
}
found = true;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("kind") {
let lit: LitStr = meta.value()?.parse()?;
let v = lit.value();
if v.is_empty() {
return Err(syn::Error::new(lit.span(), "kind must not be empty"));
}
if !is_valid_kind(&v) {
return Err(syn::Error::new(
lit.span(),
"kind must be a valid schema name: start with a letter, contain only [a-zA-Z0-9_]",
));
}
kind = Some(v);
} else {
return Err(meta.error("unknown key; expected `kind`"));
}
Ok(())
})?;
}
let kind = kind.ok_or_else(|| {
syn::Error::new(ident_span, "#[integrable(kind = \"...\")] is required")
})?;
Ok(Self { kind })
}
}
fn is_valid_kind(s: &str) -> bool {
let mut chars = s.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic())
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn fnv1a(data: &[u8], mut hash: u32) -> u32 {
const FNV_PRIME: u32 = 16_777_619;
for &b in data {
hash ^= u32::from(b);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
// Hashes field names and types using the same alphabetical sort order as Hashable,
// so that source-level field reordering never changes VERSION.
fn compute_version(fields: &syn::Fields) -> i32 {
const FNV_OFFSET: u32 = 2_166_136_261;
let mut hash = FNV_OFFSET;
match fields {
syn::Fields::Named(named) => {
for field in crate::utils::sorted_named_fields(named) {
let name = field.ident.as_ref().unwrap().to_string();
let ty = &field.ty;
hash = fnv1a(name.as_bytes(), hash);
hash = fnv1a(quote!(#ty).to_string().as_bytes(), hash);
}
}
syn::Fields::Unnamed(unnamed) => {
for (i, field) in unnamed.unnamed.iter().enumerate() {
let ty = &field.ty;
hash = fnv1a(i.to_string().as_bytes(), hash);
hash = fnv1a(quote!(#ty).to_string().as_bytes(), hash);
}
}
syn::Fields::Unit => {}
}
// Clear sign bit to guarantee a positive i32; substitute 0 → 1.
let v = (hash >> 1).cast_signed();
if v == 0 { 1 } else { v }
}
pub(crate) fn derive(input: &DeriveInput) -> TokenStream {
let syn::Data::Struct(ref data) = input.data else {
return syn::Error::new(
input.ident.span(),
"#[derive(Integrable)] is only supported on structs",
)
.to_compile_error();
};
let integrable_trait = INTEGRABLE_TRAIT_PATH.to_path();
let hashable_trait = crate::utils::HASHABLE_TRAIT_PATH.to_path();
let ident = &input.ident;
let mut generics = input.generics.clone();
for type_param in generics.type_params_mut() {
type_param.bounds.push(syn::parse_quote!(#hashable_trait));
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let attr = match IntegrableAttr::from_attrs(&input.attrs, input.ident.span()) {
Ok(a) => a,
Err(e) => return e.to_compile_error(),
};
let kind = attr.kind;
let version = compute_version(&data.fields);
quote! {
#[automatically_derived]
impl #impl_generics #integrable_trait for #ident #ty_generics #where_clause {
const KIND: &'static str = #kind;
const VERSION: i32 = #version;
}
}
}

View File

@@ -1,7 +1,6 @@
use syn::{DeriveInput, parse_macro_input}; use syn::{DeriveInput, parse_macro_input};
mod hashable; mod hashable;
mod integrable;
mod utils; mod utils;
#[proc_macro_derive(Hashable)] #[proc_macro_derive(Hashable)]
@@ -9,9 +8,3 @@ pub fn derive_hashable(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
let input = parse_macro_input!(input as DeriveInput); let input = parse_macro_input!(input as DeriveInput);
hashable::derive(&input).into() hashable::derive(&input).into()
} }
#[proc_macro_derive(Integrable, attributes(integrable))]
pub fn derive_integrable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
integrable::derive(&input).into()
}

View File

@@ -22,14 +22,3 @@ macro_rules! ensure_path {
ensure_path!(::arbiter_crypto::hashing::Hashable as HASHABLE_TRAIT_PATH); ensure_path!(::arbiter_crypto::hashing::Hashable as HASHABLE_TRAIT_PATH);
ensure_path!(::arbiter_crypto::hashing::Digest as HMAC_DIGEST_PATH); ensure_path!(::arbiter_crypto::hashing::Digest as HMAC_DIGEST_PATH);
ensure_path!(::arbiter_crypto::integrity::Integrable as INTEGRABLE_TRAIT_PATH);
/// Returns named struct fields sorted alphabetically by name.
/// Both `Hashable` and `Integrable` derive macros must iterate fields in the
/// same deterministic order so that source-level reordering never changes
/// either the runtime hash or the compile-time VERSION.
pub(crate) fn sorted_named_fields(fields: &syn::FieldsNamed) -> Vec<&syn::Field> {
let mut v: Vec<&syn::Field> = fields.named.iter().collect();
v.sort_by_key(|f| f.ident.as_ref().unwrap().to_string());
v
}

View File

@@ -1,53 +0,0 @@
use arbiter_crypto::integrity::Integrable;
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "test_entity")]
struct TestEntity {
value: i32,
}
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "other_entity")]
struct OtherEntity {
label: String,
count: u64,
}
#[test]
fn kind_is_set_correctly() {
assert_eq!(<TestEntity as Integrable>::KIND, "test_entity");
assert_eq!(<OtherEntity as Integrable>::KIND, "other_entity");
}
#[test]
fn version_is_positive() {
const {
assert!(<TestEntity as Integrable>::VERSION > 0);
assert!(<OtherEntity as Integrable>::VERSION > 0);
}
}
#[test]
fn different_field_layouts_produce_different_versions() {
assert_ne!(
<TestEntity as Integrable>::VERSION,
<OtherEntity as Integrable>::VERSION,
);
}
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "generic_entity")]
struct GenericEntity<T> {
inner: T,
}
#[test]
fn generic_struct_derives_integrable() {
assert_eq!(
<GenericEntity<TestEntity> as Integrable>::KIND,
"generic_entity"
);
const {
assert!(<GenericEntity<TestEntity> as Integrable>::VERSION > 0);
}
}

View File

@@ -1,5 +0,0 @@
#[test]
fn integrable_compile_fail() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/integrable/*.rs");
}

View File

@@ -1,8 +0,0 @@
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "entity_a")]
#[integrable(kind = "entity_b")]
struct DuplicateAttr {
value: i32,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: duplicate #[integrable] attribute
--> tests/ui/integrable/duplicate_attr.rs:3:1
|
3 | #[integrable(kind = "entity_b")]
| ^

View File

@@ -1,7 +0,0 @@
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "")]
struct EmptyKind {
value: i32,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: kind must not be empty
--> tests/ui/integrable/empty_kind.rs:2:21
|
2 | #[integrable(kind = "")]
| ^^

View File

@@ -1,8 +0,0 @@
#[derive(arbiter_macros::Integrable)]
#[integrable(kind = "my_enum")]
enum MyEnum {
A,
B,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: #[derive(Integrable)] is only supported on structs
--> tests/ui/integrable/enum_not_supported.rs:3:6
|
3 | enum MyEnum {
| ^^^^^^

View File

@@ -1,7 +0,0 @@
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "bad kind!")]
struct InvalidKind {
value: i32,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: kind must be a valid schema name: start with a letter, contain only [a-zA-Z0-9_]
--> tests/ui/integrable/invalid_kind.rs:2:21
|
2 | #[integrable(kind = "bad kind!")]
| ^^^^^^^^^^^

View File

@@ -1,6 +0,0 @@
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
struct MissingAttr {
value: i32,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: #[integrable(kind = "...")] is required
--> tests/ui/integrable/missing_attr.rs:2:8
|
2 | struct MissingAttr {
| ^^^^^^^^^^^

View File

@@ -50,6 +50,8 @@ subtle = "2.6.1"
x25519-dalek.workspace = true x25519-dalek.workspace = true
k256.workspace = true k256.workspace = true
kameo_actors.workspace = true kameo_actors.workspace = true
vsss-rs = "5.4.0"
rand_core = "0.6"
[dev-dependencies] [dev-dependencies]
proptest = "1.11.0" proptest = "1.11.0"

View File

@@ -43,13 +43,25 @@ create table if not exists arbiter_settings (
insert into arbiter_settings (id) values (1) on conflict do nothing; insert into arbiter_settings (id) values (1) on conflict do nothing;
-- ensure singleton row exists -- ensure singleton row exists
create table if not exists operator_client ( create table if not exists operator_identity (
id integer not null primary key, id integer not null primary key,
public_key blob not null, public_key blob not null,
created_at integer not null default(unixepoch ('now')), created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now')) updated_at integer not null default(unixepoch ('now'))
) STRICT; ) STRICT;
create unique index if not exists uniq_operator_client_public_key on operator_client (public_key); create unique index if not exists uniq_operator_identity_public_key on operator_identity (public_key);
create table if not exists operator (
id integer primary key references operator_identity(id) on delete restrict, -- same id as operator_identity
share blob not null,
share_nonce blob not null,
share_salt blob not null default (randomblob(32)),
created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now'))
) STRICT;
create table if not exists client_metadata ( create table if not exists client_metadata (
id integer not null primary key, id integer not null primary key,

View File

@@ -48,7 +48,7 @@ impl Bootstrapper {
let row_count: i64 = { let row_count: i64 = {
let mut conn = db.get().await?; let mut conn = db.get().await?;
schema::operator_client::table schema::operator::table
.count() .count()
.get_result(&mut conn) .get_result(&mut conn)
.await? .await?

View File

@@ -3,7 +3,7 @@ use crate::{
crypto::integrity, crypto::integrity,
db::{ db::{
DatabaseError, DatabasePool, DatabaseError, DatabasePool,
models::{self}, models::{self, EvmWalletId},
schema, schema,
}, },
evm::{ evm::{
@@ -116,7 +116,7 @@ impl EvmActor {
} }
#[message] #[message]
pub async fn list_wallets(&self) -> Result<Vec<(i32, Address)>, Error> { pub async fn list_wallets(&self) -> Result<Vec<(EvmWalletId, Address)>, Error> {
let mut conn = self.db.get().await.map_err(DatabaseError::from)?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let rows: Vec<models::EvmWallet> = schema::evm_wallet::table let rows: Vec<models::EvmWallet> = schema::evm_wallet::table
.select(models::EvmWallet::as_select()) .select(models::EvmWallet::as_select())
@@ -160,14 +160,23 @@ impl EvmActor {
} }
#[message] #[message]
pub async fn useragent_delete_grant( pub async fn operator_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
&mut self, let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
grant_id: i32,
) -> Result<(), Error> { let affected = diesel::update(schema::evm_basic_grant::table)
self.engine .filter(schema::evm_basic_grant::id.eq(grant_id))
.revoke_grant(grant_id) .set(schema::evm_basic_grant::revoked_at.eq(models::SqliteTimestamp::now()))
.execute(&mut conn)
.await .await
.map_err(Error::from) .map_err(DatabaseError::from)?;
if affected == 0 {
return Err(Error::Database(DatabaseError::from(
diesel::result::Error::NotFound,
)));
}
Ok(())
} }
#[message] #[message]

View File

@@ -11,9 +11,7 @@ use kameo::{
prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef}, prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef},
reply::ReplySender, reply::ReplySender,
}; };
use std::{ops::ControlFlow, time::Duration}; use std::ops::ControlFlow;
const APPROVAL_TIMEOUT: Duration = Duration::from_secs(30);
pub struct Args { pub struct Args {
pub client: ClientProfile, pub client: ClientProfile,
@@ -66,14 +64,6 @@ impl Actor for ClientApprovalController {
.await; .await;
} }
let weak = actor_ref.downgrade();
tokio::spawn(async move {
tokio::time::sleep(APPROVAL_TIMEOUT).await;
if let Some(r) = weak.upgrade() {
let _ = r.tell(OnApprovalTimeout {}).await;
}
});
Ok(this) Ok(this)
} }
@@ -114,14 +104,4 @@ impl ClientApprovalController {
ctx.stop(); ctx.stop();
} }
} }
/// Fired after `APPROVAL_TIMEOUT` elapses. Any operator that hasn't responded
/// by then is treated as a denial to prevent zombie sessions from blocking the flow.
#[message(ctx)]
pub fn on_approval_timeout(&mut self, ctx: &mut Context<Self, ()>) {
if self.pending > 0 {
self.send_reply(Ok(false));
ctx.stop();
}
}
} }

View File

@@ -2,6 +2,7 @@ use crate::{
actors::{ actors::{
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator, bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
operator_registry::OperatorRegistry, vault::Vault, operator_registry::OperatorRegistry, vault::Vault,
vault_coordinator::VaultCoordinator,
}, },
db, db,
}; };
@@ -15,6 +16,7 @@ pub mod evm;
pub mod flow_coordinator; pub mod flow_coordinator;
pub mod operator_registry; pub mod operator_registry;
pub mod vault; pub mod vault;
pub mod vault_coordinator;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum SpawnError { pub enum SpawnError {
@@ -30,6 +32,7 @@ pub enum SpawnError {
pub struct GlobalActors { pub struct GlobalActors {
pub vault: ActorRef<Vault>, pub vault: ActorRef<Vault>,
pub bootstrapper: ActorRef<Bootstrapper>, pub bootstrapper: ActorRef<Bootstrapper>,
pub vault_coordinator: ActorRef<VaultCoordinator>,
pub flow_coordinator: ActorRef<FlowCoordinator>, pub flow_coordinator: ActorRef<FlowCoordinator>,
pub operator_registry: ActorRef<OperatorRegistry>, pub operator_registry: ActorRef<OperatorRegistry>,
pub evm: ActorRef<EvmActor>, pub evm: ActorRef<EvmActor>,
@@ -47,7 +50,11 @@ impl GlobalActors {
let operator_registry = OperatorRegistry::spawn(OperatorRegistry::default()); let operator_registry = OperatorRegistry::spawn(OperatorRegistry::default());
Ok(Self { Ok(Self {
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?), bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)), evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db.clone())),
vault_coordinator: VaultCoordinator::spawn(VaultCoordinator::new(
db,
key_holder.clone(),
)),
vault: key_holder, vault: key_holder,
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new( flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new(
operator_registry.clone(), operator_registry.clone(),

View File

@@ -1,13 +1,13 @@
use crate::{ use crate::{
crypto::{ crypto::{
KeyCell, derive_key, KeyCell,
encryption::v1::{self, Nonce}, encryption::v1::{self, Nonce},
integrity::v1::HmacSha256, integrity::v1::HmacSha256,
}, },
db::{ db::{
self, self,
models::{self, RootKeyHistory}, models::{self, RootKeyHistory, RootKeyHistoryId},
schema::{self}, schema,
}, },
}; };
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
@@ -25,7 +25,6 @@ use strum::{EnumDiscriminants, IntoDiscriminant};
use tracing::{error, info}; use tracing::{error, info};
pub mod events { pub mod events {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct Bootstrapped; pub struct Bootstrapped;
@@ -64,7 +63,7 @@ pub enum Error {
} }
struct Unsealed { struct Unsealed {
root_key_history_id: i32, root_key_history_id: RootKeyHistoryId,
root_key: KeyCell, root_key: KeyCell,
} }
@@ -73,8 +72,9 @@ struct Unsealed {
enum State { enum State {
#[default] #[default]
Unbootstrapped, Unbootstrapped,
Sealed { Sealed {
root_key_history_id: i32, root_key_history_id: RootKeyHistoryId,
}, },
Unsealed(Unsealed), Unsealed(Unsealed),
} }
@@ -90,7 +90,6 @@ pub struct Vault {
events: ActorRef<MessageBus>, events: ActorRef<MessageBus>,
} }
#[messages]
impl Vault { impl Vault {
pub async fn new(db: db::DatabasePool, events: ActorRef<MessageBus>) -> Result<Self, Error> { pub async fn new(db: db::DatabasePool, events: ActorRef<MessageBus>) -> Result<Self, Error> {
let state = { let state = {
@@ -113,9 +112,12 @@ impl Vault {
Ok(Self { db, state, events }) Ok(Self { db, state, events })
} }
// Exclusive transaction to avoid race condtions if multiple vaults write // Exclusive transaction to avoid race conditions if multiple vaults write
// additional layer of protection against nonce-reuse // additional layer of protection against nonce-reuse
async fn get_new_nonce(pool: &db::DatabasePool, root_key_id: i32) -> Result<Nonce, Error> { async fn get_new_nonce(
pool: &db::DatabasePool,
root_key_id: RootKeyHistoryId,
) -> Result<Nonce, Error> {
let mut conn = pool.get().await?; let mut conn = pool.get().await?;
let nonce = conn let nonce = conn
@@ -128,7 +130,7 @@ impl Vault {
let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|()| { let mut nonce = 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
@@ -155,43 +157,47 @@ impl Vault {
State::Sealed { .. } => Err(Error::Sealed), State::Sealed { .. } => Err(Error::Sealed),
} }
} }
}
#[messages]
impl Vault {
#[message] #[message]
pub async fn bootstrap(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> { pub async fn bootstrap(&mut self, mut seal_key: KeyCell) -> Result<(), Error> {
if !matches!(self.state, State::Unbootstrapped) { if !matches!(&self.state, State::Unbootstrapped) {
return Err(Error::AlreadyBootstrapped); return Err(Error::AlreadyBootstrapped);
} }
let salt = v1::generate_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 = Nonce::default(); let root_key_nonce = Nonce::default();
let data_encryption_nonce = Nonce::default(); let data_encryption_nonce = Nonce::default();
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| { // Generate salt (kept for schema compat)
let root_key_reader = reader.as_slice(); let root_key_salt = v1::generate_salt();
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|rk| {
seal_key seal_key
.encrypt(&root_key_nonce, v1::ROOT_KEY_TAG, root_key_reader) .encrypt(&root_key_nonce, v1::ROOT_KEY_TAG, rk.as_slice())
.map_err(|err| { .map_err(|err| {
error!(?err, "Fatal bootstrap error"); error!(?err, "Fatal bootstrap error");
Error::Encryption(err) Error::Encryption(err)
}) })
})?; })?;
let data_encryption_nonce_bytes = data_encryption_nonce.to_vec();
let mut conn = self.db.get().await?; let mut conn = self.db.get().await?;
let data_encryption_nonce_bytes = data_encryption_nonce.to_vec();
let root_key_history_id = conn let root_key_history_id = conn
.transaction(async |conn| { .transaction(async |conn| {
let root_key_history_id: i32 = insert_into(schema::root_key_history::table) let root_key_history_id = insert_into(schema::root_key_history::table)
.values(&models::NewRootKeyHistory { .values(&models::NewRootKeyHistory {
ciphertext: root_key_ciphertext.clone(), ciphertext: root_key_ciphertext.clone(),
tag: v1::ROOT_KEY_TAG.to_vec(), tag: v1::ROOT_KEY_TAG.to_vec(),
root_key_encryption_nonce: root_key_nonce.to_vec(), root_key_encryption_nonce: root_key_nonce.to_vec(),
data_encryption_nonce: data_encryption_nonce_bytes.clone(), data_encryption_nonce: data_encryption_nonce_bytes.clone(),
schema_version: 1, schema_version: 1,
salt: salt.to_vec(), salt: root_key_salt.to_vec(),
}) })
.returning(schema::root_key_history::id) .returning(schema::root_key_history::id)
.get_result(&mut *conn) .get_result(&mut *conn)
@@ -202,7 +208,9 @@ impl Vault {
.execute(&mut *conn) .execute(&mut *conn)
.await?; .await?;
Result::<_, diesel::result::Error>::Ok(root_key_history_id) Result::<_, diesel::result::Error>::Ok(RootKeyHistoryId::from_raw(
root_key_history_id,
))
}) })
.await?; .await?;
@@ -218,52 +226,47 @@ impl Vault {
} }
#[message] #[message]
pub async fn try_unseal(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> { pub async fn try_unseal(&mut self, mut seal_key: KeyCell) -> Result<(), Error> {
let State::Sealed { let State::Sealed {
root_key_history_id, root_key_history_id,
} = &self.state } = &self.state
else { else {
return Err(Error::NotBootstrapped); return Err(Error::NotBootstrapped);
}; };
let root_key_history_id = *root_key_history_id;
// We don't want to hold connection while doing expensive KDF work // We don't want to hold connection while doing expensive work
let current_key = { let current_key = {
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(RootKeyHistory::as_select()) .select(RootKeyHistory::as_select())
.first(&mut conn) .first(&mut conn)
.await? .await?
}; };
let salt = &current_key.salt;
let salt = v1::Salt::try_from(salt.as_slice()).map_err(|_| {
error!("Broken database: invalid salt for root key");
Error::BrokenDatabase
})?;
let mut seal_key = derive_key(seal_key_raw, &salt);
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
let nonce = let nonce =
Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err(|()| { Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err(|()| {
error!("Broken database: invalid nonce for root key"); error!("Broken database: invalid nonce for root key");
Error::BrokenDatabase Error::BrokenDatabase
})?; })?;
let mut root_key_bytes = SafeCell::new(current_key.ciphertext.clone());
seal_key seal_key
.decrypt_in_place(&nonce, v1::ROOT_KEY_TAG, &mut root_key) .decrypt_in_place(&nonce, v1::ROOT_KEY_TAG, &mut root_key_bytes)
.map_err(|err| { .map_err(|err| {
error!(?err, "Failed to unseal root key: invalid seal key"); error!(?err, "Failed to unseal root key: invalid seal key");
Error::InvalidKey Error::InvalidKey
})?; })?;
let root_key = KeyCell::try_from(root_key_bytes).map_err(|()| {
error!("Broken database: invalid encryption key size");
Error::BrokenDatabase
})?;
self.state = State::Unsealed(Unsealed { self.state = State::Unsealed(Unsealed {
root_key_history_id: current_key.id, root_key_history_id: current_key.id,
root_key: KeyCell::try_from(root_key).map_err(|err| { root_key,
error!(?err, "Broken database: invalid encryption key size");
Error::BrokenDatabase
})?,
}); });
info!("Vault unsealed successfully"); info!("Vault unsealed successfully");
@@ -272,6 +275,24 @@ impl Vault {
Ok(()) Ok(())
} }
#[message]
pub async fn seal(&mut self) -> Result<(), Error> {
let Unsealed {
root_key_history_id,
..
} = Self::expect_unsealed(&mut self.state)?;
self.state = State::Sealed {
root_key_history_id: *root_key_history_id,
};
let _ = self.events.tell(Publish(events::VaultResealed)).await;
Ok(())
}
}
// Server-side cryptographic operations
#[messages]
impl Vault {
#[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 Unsealed { root_key, .. } = Self::expect_unsealed(&mut self.state)?; let Unsealed { root_key, .. } = Self::expect_unsealed(&mut self.state)?;
@@ -340,7 +361,10 @@ impl Vault {
} }
#[message] #[message]
pub fn sign_integrity(&mut self, mac_input: Vec<u8>) -> Result<(i32, Vec<u8>), Error> { pub fn sign_integrity(
&mut self,
mac_input: Vec<u8>,
) -> Result<(RootKeyHistoryId, Vec<u8>), Error> {
let Unsealed { let Unsealed {
root_key, root_key,
root_key_history_id, root_key_history_id,
@@ -350,7 +374,7 @@ impl Vault {
HmacSha256::new_from_slice(k) HmacSha256::new_from_slice(k)
.unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size")) .unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size"))
}); });
hmac.update(&root_key_history_id.to_be_bytes()); hmac.update(&root_key_history_id.to_raw().to_be_bytes());
hmac.update(&mac_input); hmac.update(&mac_input);
let mac = hmac.finalize().into_bytes().to_vec(); let mac = hmac.finalize().into_bytes().to_vec();
@@ -362,7 +386,7 @@ impl Vault {
&mut self, &mut self,
mac_input: Vec<u8>, mac_input: Vec<u8>,
expected_mac: Vec<u8>, expected_mac: Vec<u8>,
key_version: i32, key_version: RootKeyHistoryId,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
let Unsealed { let Unsealed {
root_key, root_key,
@@ -377,25 +401,11 @@ impl Vault {
HmacSha256::new_from_slice(k) HmacSha256::new_from_slice(k)
.unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size")) .unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size"))
}); });
hmac.update(&key_version.to_be_bytes()); hmac.update(&key_version.to_raw().to_be_bytes());
hmac.update(&mac_input); hmac.update(&mac_input);
Ok(hmac.verify_slice(&expected_mac).is_ok()) Ok(hmac.verify_slice(&expected_mac).is_ok())
} }
#[message]
pub async fn seal(&mut self) -> Result<(), Error> {
let Unsealed {
root_key_history_id,
..
} = Self::expect_unsealed(&mut self.state)?;
self.state = State::Sealed {
root_key_history_id: *root_key_history_id,
};
let _ = self.events.tell(Publish(events::VaultResealed)).await;
Ok(())
}
} }
#[cfg(test)] #[cfg(test)]
@@ -409,8 +419,7 @@ mod tests {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await .await
.unwrap(); .unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec()); actor.bootstrap(KeyCell::from([0u8; 32])).await.unwrap();
actor.bootstrap(seal_key).await.unwrap();
actor actor
} }
@@ -419,13 +428,12 @@ mod tests {
async fn nonce_monotonic_even_when_nonce_allocation_interleaves() { async fn nonce_monotonic_even_when_nonce_allocation_interleaves() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = bootstrapped_actor(&db).await; let mut actor = bootstrapped_actor(&db).await;
let State::Unsealed(Unsealed { let State::Unsealed(Unsealed {
root_key_history_id, root_key_history_id,
.. ..
}) = actor.state }) = actor.state
else { else {
panic!("expected unsealed state") panic!("expected unsealed state");
}; };
let n1 = Vault::get_new_nonce(&db, root_key_history_id) let n1 = Vault::get_new_nonce(&db, root_key_history_id)

View File

@@ -0,0 +1,312 @@
use std::collections::HashMap;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use diesel::{ExpressionMethods as _, QueryDsl};
use diesel_async::RunQueryDsl;
use kameo::{Actor, actor::ActorRef, messages};
use rand_core::{OsRng, RngCore as _};
use tracing::error;
use crate::{
actors::vault::{Bootstrap, TryUnseal, Vault},
crypto::{KeyCell, derive_key, encryption::v1::Nonce, shamir},
db::{self, models, schema},
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Already coordinating a bootstrap")]
AlreadyBootstrapping,
#[error("Already coordinating an unseal")]
AlreadyUnsealing,
#[error("Bootstrap not in progress")]
NotBootstrapping,
#[error("Unseal not in progress")]
NotUnsealing,
#[error("Operator already contributed")]
DuplicateContribution,
#[error("Operator not found in database")]
OperatorNotFound,
#[error("Invalid passphrase (decryption failed)")]
InvalidPassphrase,
#[error("Shamir error: {0}")]
Shamir(String),
#[error("Database connection error: {0}")]
DatabaseConnection(#[from] db::PoolError),
#[error("Database query error: {0}")]
DatabaseQuery(#[from] diesel::result::Error),
#[error("Encryption error")]
Encryption,
#[error("Vault error")]
VaultError,
#[error("Broken database")]
BrokenDatabase,
}
// Passphrases stored as plain Vec<u8> (not SafeCell) so CoordinatorState is Sync.
// They are ephemeral and dropped immediately after use.
enum CoordinatorState {
Idle,
Bootstrapping {
declared_count: usize,
passphrases: HashMap<i32, Vec<u8>>,
},
Unsealing {
threshold: usize,
passphrases: HashMap<i32, Vec<u8>>,
},
}
#[derive(Actor)]
pub struct VaultCoordinator {
db: db::DatabasePool,
vault: ActorRef<Vault>,
state: CoordinatorState,
}
impl VaultCoordinator {
pub const fn new(db: db::DatabasePool, vault: ActorRef<Vault>) -> Self {
Self {
db,
vault,
state: CoordinatorState::Idle,
}
}
}
const SHARE_AAD: &[u8] = b"arbiter/shamir-share/v1";
const fn shamir_threshold(n: usize) -> usize {
match n {
0 => panic!("No operators"),
1 => 1,
2 => 2,
n => n / 2 + 1,
}
}
async fn finalize_bootstrap(
db: db::DatabasePool,
vault: ActorRef<Vault>,
passphrases: HashMap<i32, Vec<u8>>,
) -> Result<(), Error> {
let total = passphrases.len();
let threshold = shamir_threshold(total);
// Generate random 32-byte seal key
let mut seal_key_bytes = [0u8; 32];
OsRng.fill_bytes(&mut seal_key_bytes);
// Split seal key into shares using Shamir (OsRng from rand_core 0.6, compatible with vsss-rs)
let shares = shamir::split_key(threshold, total, &seal_key_bytes, OsRng)
.map_err(|e| Error::Shamir(e.to_string()))?;
let seal_key = KeyCell::from(seal_key_bytes);
let mut conn = db.get().await?;
for ((operator_id_raw, passphrase_bytes), share) in passphrases.into_iter().zip(shares) {
// Generate a fresh share_salt for this operator
let mut share_salt = vec![0u8; 32];
OsRng.fill_bytes(&mut share_salt);
// Derive share encryption key from passphrase + salt
let mut passphrase_cell = SafeCell::new(passphrase_bytes);
let mut share_seal_key = derive_key(&mut passphrase_cell, &share_salt);
// Encrypt this operator's share
let nonce = Nonce::default();
let encrypted_share = share_seal_key
.encrypt(&nonce, SHARE_AAD, &share)
.map_err(|_| Error::Encryption)?;
let nonce_bytes = nonce.to_vec();
diesel::replace_into(schema::operator::table)
.values((
schema::operator::id.eq(Some(operator_id_raw)),
schema::operator::share.eq(&encrypted_share),
schema::operator::share_nonce.eq(&nonce_bytes),
schema::operator::share_salt.eq(&share_salt),
schema::operator::created_at.eq(models::SqliteTimestamp::now()),
schema::operator::updated_at.eq(models::SqliteTimestamp::now()),
))
.execute(&mut conn)
.await?;
}
vault
.ask(Bootstrap { seal_key })
.await
.map_err(|err| {
error!(?err, "Vault bootstrap failed");
Error::VaultError
})?;
Ok(())
}
async fn finalize_unseal(
db: db::DatabasePool,
vault: ActorRef<Vault>,
passphrases: HashMap<i32, Vec<u8>>,
) -> Result<(), Error> {
let mut conn = db.get().await?;
let mut shares: Vec<Vec<u8>> = Vec::new();
for (operator_id_raw, passphrase_bytes) in passphrases {
let (encrypted_share, share_nonce_bytes, share_salt): (Vec<u8>, Vec<u8>, Vec<u8>) =
schema::operator::table
.filter(schema::operator::id.eq(Some(operator_id_raw)))
.select((
schema::operator::share,
schema::operator::share_nonce,
schema::operator::share_salt,
))
.first(&mut conn)
.await
.map_err(|_| Error::OperatorNotFound)?;
let nonce = Nonce::try_from(share_nonce_bytes.as_slice()).map_err(|()| {
error!(operator_id = operator_id_raw, "Invalid nonce in DB");
Error::BrokenDatabase
})?;
let mut passphrase_cell = SafeCell::new(passphrase_bytes);
let mut share_seal_key = derive_key(&mut passphrase_cell, &share_salt);
let mut share_buffer = SafeCell::new(encrypted_share);
share_seal_key
.decrypt_in_place(&nonce, SHARE_AAD, &mut share_buffer)
.map_err(|_| Error::InvalidPassphrase)?;
let decrypted_share = share_buffer.read().clone();
shares.push(decrypted_share);
}
let seal_key_bytes =
shamir::combine_shares(&shares).map_err(|e| Error::Shamir(e.to_string()))?;
let seal_key = KeyCell::from(seal_key_bytes);
vault
.ask(TryUnseal { seal_key })
.await
.map_err(|err| {
error!(?err, "Vault unseal failed");
Error::VaultError
})?;
Ok(())
}
#[messages]
impl VaultCoordinator {
/// Phase 1 of multi-operator bootstrap: declare the committee size.
#[message]
#[expect(clippy::unused_async, reason = "kameo requires messages to be async")]
pub async fn start_bootstrap(
&mut self,
operator_id: i32,
declared_count: usize,
) -> Result<(), Error> {
let _ = operator_id;
if !matches!(self.state, CoordinatorState::Idle) {
return Err(Error::AlreadyBootstrapping);
}
self.state = CoordinatorState::Bootstrapping {
declared_count,
passphrases: HashMap::new(),
};
Ok(())
}
/// Phase 2 of multi-operator bootstrap: contribute a passphrase.
/// Returns Ok(true) when all operators contributed and bootstrap finalized.
#[message]
pub async fn contribute_bootstrap(
&mut self,
operator_id: i32,
mut passphrase: SafeCell<Vec<u8>>,
) -> Result<bool, Error> {
let CoordinatorState::Bootstrapping {
declared_count,
passphrases,
} = &mut self.state
else {
return Err(Error::NotBootstrapping);
};
if passphrases.contains_key(&operator_id) {
return Err(Error::DuplicateContribution);
}
// Extract bytes immediately so state stays Sync
let passphrase_bytes = passphrase.read().to_vec();
passphrases.insert(operator_id, passphrase_bytes);
if passphrases.len() < *declared_count {
return Ok(false);
}
let CoordinatorState::Bootstrapping { passphrases, .. } =
std::mem::replace(&mut self.state, CoordinatorState::Idle)
else {
unreachable!()
};
finalize_bootstrap(self.db.clone(), self.vault.clone(), passphrases).await?;
Ok(true)
}
/// Contribute a passphrase for vault unseal.
/// Returns Ok(true) when threshold reached and vault is unsealed.
#[message]
pub async fn contribute_unseal(
&mut self,
operator_id: i32,
mut passphrase: SafeCell<Vec<u8>>,
) -> Result<bool, Error> {
if matches!(self.state, CoordinatorState::Idle) {
let mut conn = self.db.get().await?;
let count: i64 = schema::operator::table
.count()
.get_result(&mut conn)
.await?;
let threshold = shamir_threshold(usize::try_from(count).unwrap_or_default());
self.state = CoordinatorState::Unsealing {
threshold,
passphrases: HashMap::new(),
};
}
let CoordinatorState::Unsealing {
threshold,
passphrases,
} = &mut self.state
else {
return Err(Error::NotUnsealing);
};
if passphrases.contains_key(&operator_id) {
return Err(Error::DuplicateContribution);
}
let passphrase_bytes = passphrase.read().to_vec();
passphrases.insert(operator_id, passphrase_bytes);
if passphrases.len() < *threshold {
return Ok(false);
}
let CoordinatorState::Unsealing { passphrases, .. } =
std::mem::replace(&mut self.state, CoordinatorState::Idle)
else {
unreachable!()
};
finalize_unseal(self.db.clone(), self.vault.clone(), passphrases).await?;
Ok(true)
}
}

View File

@@ -61,12 +61,12 @@ mod tests {
#[test] #[test]
fn derive_seal_key_deterministic() { fn derive_seal_key_deterministic() {
static PASSWORD: &[u8] = b"password"; static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec()); let mut password = SafeCell::new(PASSWORD.to_vec());
let password2 = SafeCell::new(PASSWORD.to_vec()); let mut password2 = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt(); let salt = generate_salt();
let mut key1 = derive_key(password, &salt); let mut key1 = derive_key(&mut password, &salt);
let mut key2 = derive_key(password2, &salt); let mut key2 = derive_key(&mut password2, &salt);
let key1_reader = key1.0.read(); let key1_reader = key1.0.read();
let key2_reader = key2.0.read(); let key2_reader = key2.0.read();
@@ -77,10 +77,10 @@ mod tests {
#[test] #[test]
fn successful_derive() { fn successful_derive() {
static PASSWORD: &[u8] = b"password"; static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec()); let mut password = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt(); let salt = generate_salt();
let mut key = derive_key(password, &salt); let mut key = derive_key(&mut password, &salt);
let key_reader = key.0.read(); let key_reader = key.0.read();
assert_ne!(key_reader.as_slice(), &[0u8; 32][..]); assert_ne!(key_reader.as_slice(), &[0u8; 32][..]);

View File

@@ -52,7 +52,10 @@ pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
pub type HmacSha256 = Hmac<Sha256>; pub type HmacSha256 = Hmac<Sha256>;
pub use arbiter_crypto::integrity::Integrable; pub trait Integrable: Hashable {
const KIND: &'static str;
const VERSION: i32 = 1;
}
fn payload_hash(payload: &impl Hashable) -> [u8; 32] { fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
@@ -212,15 +215,15 @@ mod tests {
}, },
db::{self, schema}, db::{self, schema},
}; };
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use super::{Error, Integrable, sign_entity, verify_entity};
#[derive(Clone, arbiter_macros::Hashable)]
use super::{Error, sign_entity, verify_entity};
#[derive(Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "dummy_entity")]
struct DummyEntity { struct DummyEntity {
payload_version: i32, payload_version: i32,
payload: Vec<u8>, payload: Vec<u8>,
} }
impl Integrable for DummyEntity {
const KIND: &'static str = "dummy_entity";
}
async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> { async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> {
let actor = Vault::spawn( let actor = Vault::spawn(
@@ -230,7 +233,7 @@ mod tests {
); );
actor actor
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()), seal_key: crate::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();

View File

@@ -1,5 +1,5 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use encryption::v1::{Nonce, Salt}; use encryption::v1::Nonce;
use argon2::{Algorithm, Argon2}; use argon2::{Algorithm, Argon2};
use chacha20poly1305::{ use chacha20poly1305::{
@@ -13,6 +13,7 @@ use rand::{
pub mod encryption; pub mod encryption;
pub mod integrity; pub mod integrity;
pub mod shamir;
pub struct KeyCell(pub SafeCell<Key>); pub struct KeyCell(pub SafeCell<Key>);
impl From<SafeCell<Key>> for KeyCell { impl From<SafeCell<Key>> for KeyCell {
@@ -20,6 +21,15 @@ impl From<SafeCell<Key>> for KeyCell {
Self(value) Self(value)
} }
} }
impl From<[u8; 32]> for KeyCell {
fn from(bytes: [u8; 32]) -> Self {
let cell = SafeCell::new_inline_default(|key: &mut Key| {
key.copy_from_slice(&bytes);
});
Self(cell)
}
}
impl TryFrom<SafeCell<Vec<u8>>> for KeyCell { impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
type Error = (); type Error = ();
@@ -28,7 +38,7 @@ impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
if value.len() != size_of::<Key>() { if value.len() != size_of::<Key>() {
return Err(()); return Err(());
} }
let cell = SafeCell::new_inline(|cell_write: &mut Key| { let cell = SafeCell::new_inline_default(|cell_write: &mut Key| {
cell_write.copy_from_slice(&value); cell_write.copy_from_slice(&value);
}); });
Ok(Self(cell)) Ok(Self(cell))
@@ -37,7 +47,7 @@ impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
impl KeyCell { impl KeyCell {
pub fn new_secure_random() -> Self { pub fn new_secure_random() -> Self {
let key = SafeCell::new_inline(|key_buffer: &mut Key| { let key = SafeCell::new_inline_default(|key_buffer: &mut Key| {
let mut rng = StdRng::try_from_rng(&mut SysRng) let mut rng = StdRng::try_from_rng(&mut SysRng)
.expect("Rng failure is unrecoverable and should panic"); .expect("Rng failure is unrecoverable and should panic");
rng.fill_bytes(key_buffer); rng.fill_bytes(key_buffer);
@@ -94,7 +104,7 @@ impl KeyCell {
} }
/// 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_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell { pub fn derive_key(password: &mut SafeCell<Vec<u8>>, salt: &[u8]) -> KeyCell {
let params = { let params = {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
@@ -132,10 +142,10 @@ mod tests {
#[test] #[test]
fn encrypt_decrypt() { fn encrypt_decrypt() {
static PASSWORD: &[u8] = b"password"; static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec()); let mut password = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt(); let salt = generate_salt();
let mut key = derive_key(password, &salt); let mut key = derive_key(&mut 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();

View File

@@ -0,0 +1,29 @@
use vsss_rs::Gf256;
#[derive(Debug, thiserror::Error)]
pub enum ShamirError {
#[error("Failed to split key: {0}")]
Split(String),
#[error("Failed to combine shares: {0}")]
Combine(String),
}
/// Split `key` into `total` shares where any `threshold` shares can reconstruct it.
/// Each returned Vec<u8> is a share with format [`identifier_byte`, `value_bytes`...].
pub fn split_key(
threshold: usize,
total: usize,
key: &[u8; 32],
rng: impl rand_core::RngCore + rand_core::CryptoRng,
) -> Result<Vec<Vec<u8>>, ShamirError> {
Gf256::split_array(threshold, total, key.as_slice(), rng)
.map_err(|e| ShamirError::Split(format!("{e:?}")))
}
/// Reconstruct the secret from `threshold` or more shares.
pub fn combine_shares(shares: &[Vec<u8>]) -> Result<[u8; 32], ShamirError> {
let bytes = Gf256::combine_array(shares)
.map_err(|e| ShamirError::Combine(format!("{e:?}")))?;
<[u8; 32]>::try_from(bytes.as_slice())
.map_err(|_| ShamirError::Combine("unexpected reconstructed key length".to_owned()))
}

View File

@@ -79,10 +79,41 @@ pub mod types {
} }
} }
#[derive(Debug, FromSqlRow, AsExpression, Clone)] macro_rules! declare_id {
#[diesel(sql_type = Integer)] ($name:ident) => {
#[repr(transparent)] // hint compiler to optimize the wrapper struct away #[derive(Debug, FromSqlRow, AsExpression, Clone, Hash, Copy, PartialEq, Eq)]
pub struct ChainId(pub i32); #[diesel(sql_type = Integer)]
#[repr(transparent)] // hint compiler to optimize the wrapper struct away
pub struct $name(i32);
impl $name {
pub const fn to_raw(self) -> i32 {
self.0
}
pub const fn from_raw(raw: i32) -> Self {
Self(raw)
}
}
impl FromSql<Integer, Sqlite> for $name {
fn from_sql(
bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>,
) -> diesel::deserialize::Result<Self> {
FromSql::<Integer, Sqlite>::from_sql(bytes).map(Self)
}
}
impl ToSql<Integer, Sqlite> for $name {
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
) -> diesel::serialize::Result {
ToSql::<Integer, Sqlite>::to_sql(&self.0, out)
}
}
};
}
declare_id!(ChainId);
#[expect( #[expect(
clippy::cast_sign_loss, clippy::cast_sign_loss,
@@ -103,21 +134,13 @@ pub mod types {
} }
}; };
impl FromSql<Integer, Sqlite> for ChainId { declare_id!(OperatorId);
fn from_sql( declare_id!(OperatorIdentityId);
bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>, declare_id!(AeadEncryptedId);
) -> diesel::deserialize::Result<Self> { declare_id!(RootKeyHistoryId);
FromSql::<Integer, Sqlite>::from_sql(bytes).map(Self) declare_id!(TlsHistoryId);
} declare_id!(EvmWalletId);
} declare_id!(ClientId);
impl ToSql<Integer, Sqlite> for ChainId {
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
) -> diesel::serialize::Result {
ToSql::<Integer, Sqlite>::to_sql(&self.0, out)
}
}
} }
pub use types::*; pub use types::*;
@@ -130,12 +153,12 @@ pub use types::*;
)] )]
#[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))] #[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))]
pub struct AeadEncrypted { pub struct AeadEncrypted {
pub id: i32, pub id: AeadEncryptedId,
pub ciphertext: Vec<u8>, pub ciphertext: Vec<u8>,
pub tag: Vec<u8>, pub tag: Vec<u8>,
pub current_nonce: Vec<u8>, pub current_nonce: Vec<u8>,
pub schema_version: i32, pub schema_version: i32,
pub associated_root_key_id: i32, // references root_key_history.id pub associated_root_key_id: RootKeyHistoryId,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
} }
@@ -148,7 +171,7 @@ pub struct AeadEncrypted {
attributes_with = "deriveless" attributes_with = "deriveless"
)] )]
pub struct RootKeyHistory { pub struct RootKeyHistory {
pub id: i32, pub id: RootKeyHistoryId,
pub ciphertext: Vec<u8>, pub ciphertext: Vec<u8>,
pub tag: Vec<u8>, pub tag: Vec<u8>,
pub root_key_encryption_nonce: Vec<u8>, pub root_key_encryption_nonce: Vec<u8>,
@@ -166,7 +189,7 @@ pub struct RootKeyHistory {
attributes_with = "deriveless" attributes_with = "deriveless"
)] )]
pub struct TlsHistory { pub struct TlsHistory {
pub id: i32, pub id: TlsHistoryId,
pub cert: String, pub cert: String,
pub cert_key: String, // PEM Encoded private key pub cert_key: String, // PEM Encoded private key
pub ca_cert: String, // PEM Encoded certificate for cert signing pub ca_cert: String, // PEM Encoded certificate for cert signing
@@ -191,7 +214,7 @@ pub struct ArbiterSettings {
attributes_with = "deriveless" attributes_with = "deriveless"
)] )]
pub struct EvmWallet { pub struct EvmWallet {
pub id: i32, pub id: EvmWalletId,
pub address: Vec<u8>, pub address: Vec<u8>,
pub aead_encrypted_id: i32, pub aead_encrypted_id: i32,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
@@ -213,7 +236,7 @@ pub struct EvmWallet {
)] )]
pub struct EvmWalletAccess { pub struct EvmWalletAccess {
pub id: i32, pub id: i32,
pub wallet_id: i32, pub wallet_id: EvmWalletId,
pub client_id: i32, pub client_id: i32,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
} }
@@ -240,7 +263,7 @@ pub struct ProgramClientMetadataHistory {
#[derive(Models, Queryable, Debug, Insertable, Selectable)] #[derive(Models, Queryable, Debug, Insertable, Selectable)]
#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::program_client, check_for_backend(Sqlite))]
pub struct ProgramClient { pub struct ProgramClient {
pub id: i32, pub id: ClientId,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub metadata_id: i32, pub metadata_id: i32,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
@@ -250,12 +273,23 @@ pub struct ProgramClient {
#[derive(Queryable, Debug)] #[derive(Queryable, Debug)]
#[diesel(table_name = schema::operator_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::operator_client, check_for_backend(Sqlite))]
pub struct OperatorClient { pub struct OperatorClient {
pub id: i32, pub id: OperatorIdentityId,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp, pub updated_at: SqliteTimestamp,
} }
#[derive(Queryable, Debug)]
#[diesel(table_name = schema::operator, check_for_backend(Sqlite))]
pub struct Operator {
pub id: OperatorId,
pub share: Vec<u8>,
pub share_nonce: Vec<u8>,
pub share_salt: Vec<u8>,
pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp,
}
#[derive(Models, Queryable, Debug, Insertable, Selectable)] #[derive(Models, Queryable, Debug, Insertable, Selectable)]
#[diesel(table_name = evm_ether_transfer_limit, check_for_backend(Sqlite))] #[diesel(table_name = evm_ether_transfer_limit, check_for_backend(Sqlite))]
#[view( #[view(
@@ -399,7 +433,7 @@ pub struct IntegrityEnvelope {
pub entity_kind: String, pub entity_kind: String,
pub entity_id: Vec<u8>, pub entity_id: Vec<u8>,
pub payload_version: i32, pub payload_version: i32,
pub key_version: i32, pub key_version: RootKeyHistoryId,
pub mac: Vec<u8>, pub mac: Vec<u8>,
pub signed_at: SqliteTimestamp, pub signed_at: SqliteTimestamp,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,

View File

@@ -152,6 +152,26 @@ diesel::table! {
} }
} }
diesel::table! {
operator (id) {
id -> Nullable<Integer>,
share -> Binary,
share_nonce -> Binary,
share_salt -> Binary,
created_at -> Integer,
updated_at -> Integer,
}
}
diesel::table! {
operator_identity (id) {
id -> Integer,
public_key -> Binary,
created_at -> Integer,
updated_at -> Integer,
}
}
diesel::table! { diesel::table! {
program_client (id) { program_client (id) {
id -> Integer, id -> Integer,
@@ -185,15 +205,6 @@ diesel::table! {
} }
} }
diesel::table! {
operator_client (id) {
id -> Integer,
public_key -> Binary,
created_at -> Integer,
updated_at -> Integer,
}
}
diesel::joinable!(aead_encrypted -> root_key_history (associated_root_key_id)); diesel::joinable!(aead_encrypted -> root_key_history (associated_root_key_id));
diesel::joinable!(arbiter_settings -> root_key_history (root_key_id)); diesel::joinable!(arbiter_settings -> root_key_history (root_key_id));
diesel::joinable!(arbiter_settings -> tls_history (tls_id)); diesel::joinable!(arbiter_settings -> tls_history (tls_id));
@@ -212,6 +223,7 @@ diesel::joinable!(evm_transaction_log -> evm_wallet_access (wallet_access_id));
diesel::joinable!(evm_wallet -> aead_encrypted (aead_encrypted_id)); diesel::joinable!(evm_wallet -> aead_encrypted (aead_encrypted_id));
diesel::joinable!(evm_wallet_access -> evm_wallet (wallet_id)); diesel::joinable!(evm_wallet_access -> evm_wallet (wallet_id));
diesel::joinable!(evm_wallet_access -> program_client (client_id)); diesel::joinable!(evm_wallet_access -> program_client (client_id));
diesel::joinable!(operator -> operator_identity (id));
diesel::joinable!(program_client -> client_metadata (metadata_id)); diesel::joinable!(program_client -> client_metadata (metadata_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
@@ -230,8 +242,9 @@ diesel::allow_tables_to_appear_in_same_query!(
evm_wallet, evm_wallet,
evm_wallet_access, evm_wallet_access,
integrity_envelope, integrity_envelope,
operator,
operator_identity,
program_client, program_client,
root_key_history, root_key_history,
tls_history, tls_history,
operator_client,
); );

View File

@@ -1,34 +1,28 @@
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::actor::ActorRef;
use crate::{ use crate::{
actors::vault::Vault, actors::vault::Vault,
crypto::integrity, crypto::integrity,
db::{ db::{
self, DatabaseError, self, DatabaseError,
models::{ models::{
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
EvmEtherTransferLimit, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
}, },
schema::{self, evm_transaction_log}, schema::{self, evm_transaction_log},
}, },
evm::policies::{ evm::policies::{
CombinedSettings, DatabaseID, EvalContext, EvalViolation, Grant, Policy, CombinedSettings, DatabaseID, EvalContext, EvalViolation, Grant, Policy,
SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit, SharedGrantSettings, SpecificGrant, SpecificMeaning, ether_transfer::EtherTransfer,
ether_transfer::EtherTransfer, token_transfers::TokenTransfer, token_transfers::TokenTransfer,
}, },
}; };
use alloy::{ use alloy::{
consensus::TxEip1559, consensus::TxEip1559,
primitives::{Address, TxKind, U256}, primitives::{TxKind, U256},
}; };
use chrono::Utc; use chrono::Utc;
use diesel::{ use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite};
ExpressionMethods as _, OptionalExtension, QueryDsl as _, QueryResult, SelectableHelper, use diesel_async::{AsyncConnection, RunQueryDsl};
insert_into, sqlite::Sqlite, update, use kameo::actor::ActorRef;
};
pub mod abi; pub mod abi;
pub mod safe_signer; pub mod safe_signer;
@@ -278,151 +272,6 @@ impl Engine {
Ok(id) Ok(id)
} }
pub async fn revoke_grant(
&self,
basic_grant_id: i32,
) -> Result<(), DatabaseError> {
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let vault = self.vault.clone();
conn.transaction(async move |conn| {
use crate::db::schema::{
evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target,
evm_ether_transfer_limit, evm_token_transfer_grant,
evm_token_transfer_volume_limit,
};
update(evm_basic_grant::table)
.filter(evm_basic_grant::id.eq(basic_grant_id))
.set(evm_basic_grant::revoked_at.eq(SqliteTimestamp(Utc::now())))
.execute(&mut *conn)
.await?;
let basic_grant: EvmBasicGrant = evm_basic_grant::table
.filter(evm_basic_grant::id.eq(basic_grant_id))
.select(EvmBasicGrant::as_select())
.first(&mut *conn)
.await?;
let shared = SharedGrantSettings::try_from_model(basic_grant)?;
if let Some(ether_grant) = evm_ether_transfer_grant::table
.filter(evm_ether_transfer_grant::basic_grant_id.eq(basic_grant_id))
.select(EvmEtherTransferGrant::as_select())
.first(&mut *conn)
.await
.optional()?
{
let target_rows: Vec<EvmEtherTransferGrantTarget> =
evm_ether_transfer_grant_target::table
.filter(evm_ether_transfer_grant_target::grant_id.eq(ether_grant.id))
.select(EvmEtherTransferGrantTarget::as_select())
.load(&mut *conn)
.await?;
let targets: Vec<Address> = target_rows
.into_iter()
.filter_map(|target| {
let arr: [u8; 20] = target.address.try_into().ok()?;
Some(Address::from(arr))
})
.collect();
let limit: EvmEtherTransferLimit = evm_ether_transfer_limit::table
.filter(evm_ether_transfer_limit::id.eq(ether_grant.limit_id))
.select(EvmEtherTransferLimit::as_select())
.first(&mut *conn)
.await?;
let settings = CombinedSettings {
shared: shared.clone(),
specific: policies::ether_transfer::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.into()),
},
},
};
integrity::sign_entity(&mut *conn, &vault, &settings, basic_grant_id)
.await
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
return QueryResult::Ok(());
}
if let Some(token_grant) = evm_token_transfer_grant::table
.filter(evm_token_transfer_grant::basic_grant_id.eq(basic_grant_id))
.select(EvmTokenTransferGrant::as_select())
.first(&mut *conn)
.await
.optional()?
{
let volume_limit_rows: Vec<EvmTokenTransferVolumeLimit> =
evm_token_transfer_volume_limit::table
.filter(evm_token_transfer_volume_limit::grant_id.eq(token_grant.id))
.select(EvmTokenTransferVolumeLimit::as_select())
.load(&mut *conn)
.await?;
let volume_limits: Vec<VolumeRateLimit> = volume_limit_rows
.into_iter()
.map(|row| {
Ok(VolumeRateLimit {
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(
|err| {
diesel::result::Error::DeserializationError(Box::new(err))
},
)?,
window: chrono::Duration::seconds(row.window_secs.into()),
})
})
.collect::<QueryResult<Vec<_>>>()?;
let target: Option<Address> = match token_grant.receiver {
None => None,
Some(bytes) => {
let arr: [u8; 20] = bytes.try_into().map_err(|_| {
diesel::result::Error::DeserializationError(
"Invalid receiver address length".into(),
)
})?;
Some(Address::from(arr))
}
};
let token_contract: [u8; 20] =
token_grant.token_contract.clone().try_into().map_err(|_| {
diesel::result::Error::DeserializationError(
"Invalid token contract address length".into(),
)
})?;
let settings = CombinedSettings {
shared,
specific: policies::token_transfers::Settings {
token_contract: Address::from(token_contract),
target,
volume_limits,
},
};
integrity::sign_entity(&mut *conn, &vault, &settings, basic_grant_id)
.await
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
return QueryResult::Ok(());
}
Err(diesel::result::Error::NotFound)
})
.await
.map_err(DatabaseError::from)
}
async fn list_one_kind<Kind: Policy, Y>( async fn list_one_kind<Kind: Policy, Y>(
&self, &self,
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
@@ -502,26 +351,21 @@ impl Engine {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use alloy::primitives::{Address, Bytes, U256, address}; use alloy::primitives::{Address, Bytes, U256, address};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use diesel::{SelectableHelper, insert_into}; use diesel::{SelectableHelper, insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use kameo::{actor::ActorRef, prelude::Spawn};
use rstest::rstest; use rstest::rstest;
use crate::actors::{GlobalActors, vault::{Bootstrap, Vault}};
use crate::crypto::integrity;
use crate::db::{ use crate::db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{ models::{
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, NewEvmTransactionLog,
SqliteTimestamp,
}, },
schema::{evm_basic_grant, evm_transaction_log}, schema::{evm_basic_grant, evm_transaction_log},
}; };
use crate::evm::policies::ether_transfer::EtherTransfer;
use crate::evm::policies::{ use crate::evm::policies::{
CombinedSettings, EvalContext, EvalViolation, Policy, SharedGrantSettings, EvalContext, EvalViolation, SharedGrantSettings, TransactionRateLimit,
TransactionRateLimit, VolumeRateLimit,
}; };
use super::check_shared_constraints; use super::check_shared_constraints;
@@ -534,7 +378,7 @@ mod tests {
EvalContext { EvalContext {
target: EvmWalletAccess { target: EvmWalletAccess {
id: WALLET_ACCESS_ID, id: WALLET_ACCESS_ID,
wallet_id: 10, wallet_id: EvmWalletId::from_raw(5),
client_id: 20, client_id: 20,
created_at: SqliteTimestamp(Utc::now()), created_at: SqliteTimestamp(Utc::now()),
}, },
@@ -553,7 +397,6 @@ mod tests {
chain: CHAIN_ID, chain: CHAIN_ID,
valid_from: None, valid_from: None,
valid_until: None, valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None, max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None, max_priority_fee_per_gas: None,
rate_limit: None, rate_limit: None,
@@ -762,115 +605,4 @@ mod tests {
assert!(violations.is_empty()); assert!(violations.is_empty());
} }
} }
async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> {
let actor = Vault::spawn(
Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap(),
);
actor
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
})
.await
.unwrap();
actor
}
#[tokio::test]
async fn revoke_grant_preserves_revoked_integrity() {
use crate::db::schema::evm_basic_grant;
use diesel::ExpressionMethods as _;
let db = db::create_test_pool().await;
let vault = bootstrapped_vault(&db).await;
let engine = super::Engine::new(db.clone(), vault.clone());
let full_grant = CombinedSettings {
shared: SharedGrantSettings {
wallet_access_id: WALLET_ACCESS_ID,
chain: CHAIN_ID,
valid_from: None,
valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,
},
specific: super::policies::ether_transfer::Settings {
target: vec![RECIPIENT],
limit: VolumeRateLimit {
max_volume: U256::from(100u64),
window: Duration::hours(1),
},
},
};
let grant_id = engine
.create_grant::<EtherTransfer>(full_grant)
.await
.unwrap();
engine.revoke_grant(grant_id).await.unwrap();
let mut conn = db.get().await.unwrap();
diesel::update(evm_basic_grant::table)
.filter(evm_basic_grant::id.eq(grant_id))
.set(evm_basic_grant::revoked_at.eq::<Option<SqliteTimestamp>>(None))
.execute(&mut conn)
.await
.unwrap();
let wallet_access = EvmWalletAccess {
id: WALLET_ACCESS_ID,
wallet_id: 10,
client_id: 20,
created_at: SqliteTimestamp(Utc::now()),
};
let context = EvalContext {
target: wallet_access,
chain: CHAIN_ID,
to: RECIPIENT,
value: U256::ONE,
calldata: Bytes::new(),
max_fee_per_gas: 1,
max_priority_fee_per_gas: 1,
};
let grant = EtherTransfer::try_find_grant(
&context, &mut conn,
)
.await
.unwrap()
.unwrap();
let result =
integrity::verify_entity(&mut conn, &vault, &grant.settings, grant.id).await;
assert!(matches!(
result,
Err(integrity::Error::MacMismatch { .. })
));
}
#[test]
fn shared_settings_hash_changes_when_revoked_at_changes() {
use arbiter_crypto::hashing::Hashable;
use sha2::Digest;
let active = shared_settings();
let revoked = SharedGrantSettings {
revoked_at: Some(Utc::now()),
..shared_settings()
};
let mut active_hash = sha2::Sha256::new();
active.hash(&mut active_hash);
let mut revoked_hash = sha2::Sha256::new();
revoked.hash(&mut revoked_hash);
assert_ne!(active_hash.finalize(), revoked_hash.finalize());
}
} }

View File

@@ -144,7 +144,6 @@ pub struct SharedGrantSettings {
pub valid_from: Option<DateTime<Utc>>, pub valid_from: Option<DateTime<Utc>>,
pub valid_until: Option<DateTime<Utc>>, pub valid_until: Option<DateTime<Utc>>,
pub revoked_at: Option<DateTime<Utc>>,
pub max_gas_fee_per_gas: Option<U256>, pub max_gas_fee_per_gas: Option<U256>,
pub max_priority_fee_per_gas: Option<U256>, pub max_priority_fee_per_gas: Option<U256>,
@@ -159,7 +158,6 @@ impl SharedGrantSettings {
chain: model.chain_id.into(), chain: model.chain_id.into(),
valid_from: model.valid_from.map(Into::into), valid_from: model.valid_from.map(Into::into),
valid_until: model.valid_until.map(Into::into), valid_until: model.valid_until.map(Into::into),
revoked_at: model.revoked_at.map(Into::into),
max_gas_fee_per_gas: model max_gas_fee_per_gas: model
.max_gas_fee_per_gas .max_gas_fee_per_gas
.map(|b| utils::try_bytes_to_u256(&b)) .map(|b| utils::try_bytes_to_u256(&b))

View File

@@ -1,5 +1,6 @@
use super::{DatabaseID, EvalContext, EvalViolation}; use super::{DatabaseID, EvalContext, EvalViolation};
use crate::{ use crate::{
crypto::integrity::v1::Integrable,
db::models::{ db::models::{
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit, EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
NewEvmEtherTransferLimit, SqliteTimestamp, NewEvmEtherTransferLimit, SqliteTimestamp,
@@ -51,12 +52,14 @@ impl From<Meaning> for SpecificMeaning {
} }
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits // A grant for ether transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)] #[derive(Debug, Clone, arbiter_macros::Hashable)]
#[integrable(kind = "EtherTransfer")]
pub struct Settings { 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";
}
impl From<Settings> for SpecificGrant { impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> Self { fn from(val: Settings) -> Self {

View File

@@ -3,7 +3,8 @@ use crate::{
db::{ db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{ models::{
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, NewEvmTransactionLog,
SqliteTimestamp,
}, },
schema::{evm_basic_grant, evm_transaction_log}, schema::{evm_basic_grant, evm_transaction_log},
}, },
@@ -31,7 +32,7 @@ fn ctx(to: Address, value: U256) -> EvalContext {
EvalContext { EvalContext {
target: EvmWalletAccess { target: EvmWalletAccess {
id: WALLET_ACCESS_ID, id: WALLET_ACCESS_ID,
wallet_id: 10, wallet_id: EvmWalletId::from_raw(10),
client_id: 20, client_id: 20,
created_at: SqliteTimestamp(Utc::now()), created_at: SqliteTimestamp(Utc::now()),
}, },
@@ -79,7 +80,6 @@ fn shared() -> SharedGrantSettings {
chain: CHAIN_ID, chain: CHAIN_ID,
valid_from: None, valid_from: None,
valid_until: None, valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None, max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None, max_priority_fee_per_gas: None,
rate_limit: None, rate_limit: None,

View File

@@ -1,5 +1,6 @@
use super::{DatabaseID, EvalContext, EvalViolation}; use super::{DatabaseID, EvalContext, EvalViolation};
use crate::{ use crate::{
crypto::integrity::Integrable,
db::models::{ db::models::{
EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit, EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit, NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
@@ -62,13 +63,15 @@ impl From<Meaning> for SpecificMeaning {
} }
// A grant for token transfers, which can be scoped to specific target addresses and volume limits // A grant for token transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)] #[derive(Debug, Clone, arbiter_macros::Hashable)]
#[integrable(kind = "TokenTransfer")]
pub struct Settings { pub struct Settings {
pub token_contract: Address, pub token_contract: Address,
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";
}
impl From<Settings> for SpecificGrant { impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> Self { fn from(val: Settings) -> Self {

View File

@@ -2,7 +2,7 @@ use super::{Settings, TokenTransfer};
use crate::{ use crate::{
db::{ db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp}, models::{EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, SqliteTimestamp},
schema::evm_basic_grant, schema::evm_basic_grant,
}, },
evm::{ evm::{
@@ -45,7 +45,7 @@ fn ctx(to: Address, calldata: Bytes) -> EvalContext {
EvalContext { EvalContext {
target: EvmWalletAccess { target: EvmWalletAccess {
id: WALLET_ACCESS_ID, id: WALLET_ACCESS_ID,
wallet_id: 10, wallet_id: EvmWalletId::from_raw(10),
client_id: 20, client_id: 20,
created_at: SqliteTimestamp(Utc::now()), created_at: SqliteTimestamp(Utc::now()),
}, },
@@ -98,7 +98,6 @@ fn shared() -> SharedGrantSettings {
chain: CHAIN_ID, chain: CHAIN_ID,
valid_from: None, valid_from: None,
valid_until: None, valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None, max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None, max_priority_fee_per_gas: None,
rate_limit: None, rate_limit: None,

View File

@@ -44,7 +44,7 @@ impl std::fmt::Debug for SafeSigner {
/// Returns the protected key bytes and the derived Ethereum address. /// Returns the protected key bytes and the derived Ethereum address.
pub fn generate(rng: &mut impl rand::Rng) -> (SafeCell<[u8; 32]>, Address) { pub fn generate(rng: &mut impl rand::Rng) -> (SafeCell<[u8; 32]>, Address) {
loop { loop {
let mut cell = SafeCell::new_inline(|w: &mut [u8; 32]| { let mut cell = SafeCell::new_inline_default(|w: &mut [u8; 32]| {
rng.fill_bytes(w); rng.fill_bytes(w);
}); });

View File

@@ -200,7 +200,7 @@ impl Convert for auth::Outbound {
.timestamp .timestamp
.timestamp_nanos_opt() .timestamp_nanos_opt()
.expect("timestamp within range") .expect("timestamp within range")
.cast_unsigned(), as u64,
random: challenge.nonce.to_vec(), random: challenge.nonce.to_vec(),
}) })
} }

View File

@@ -80,7 +80,7 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
.timestamp .timestamp
.timestamp_nanos_opt() .timestamp_nanos_opt()
.expect("timestamp within range") .expect("timestamp within range")
.cast_unsigned(), as u64,
random: challenge.nonce.to_vec(), random: challenge.nonce.to_vec(),
}) })
} }

View File

@@ -90,7 +90,7 @@ async fn handle_wallet_list(
.into_iter() .into_iter()
.map(|(id, address)| WalletEntry { .map(|(id, address)| WalletEntry {
address: address.to_vec(), address: address.to_vec(),
id, id: id.to_raw(),
}) })
.collect(), .collect(),
}), }),

View File

@@ -1,11 +1,10 @@
use crate::{ use crate::{
db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}, db::models::{CoreEvmWalletAccess, EvmWalletId, NewEvmWalletAccess},
evm::policies::{ evm::policies::{
SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer,
token_transfers, token_transfers,
}, },
grpc::Convert, grpc::{Convert, TryConvert},
grpc::TryConvert,
}; };
use arbiter_proto::{ use arbiter_proto::{
proto::evm::{ proto::evm::{
@@ -87,7 +86,6 @@ impl TryConvert for ProtoSharedSettings {
.valid_until .valid_until
.map(ProtoTimestamp::try_convert) .map(ProtoTimestamp::try_convert)
.transpose()?, .transpose()?,
revoked_at: None,
max_gas_fee_per_gas: self max_gas_fee_per_gas: self
.max_gas_fee_per_gas .max_gas_fee_per_gas
.as_deref() .as_deref()
@@ -151,7 +149,7 @@ impl Convert for WalletAccess {
fn convert(self) -> Self::Output { fn convert(self) -> Self::Output {
NewEvmWalletAccess { NewEvmWalletAccess {
wallet_id: self.wallet_id, wallet_id: EvmWalletId::from_raw(self.wallet_id),
client_id: self.sdk_client_id, client_id: self.sdk_client_id,
} }
} }
@@ -166,7 +164,7 @@ impl TryConvert for SdkClientWalletAccess {
return Err(Status::invalid_argument("Missing wallet access entry")); return Err(Status::invalid_argument("Missing wallet access entry"));
}; };
Ok(CoreEvmWalletAccess { Ok(CoreEvmWalletAccess {
wallet_id: access.wallet_id, wallet_id: EvmWalletId::from_raw(access.wallet_id),
client_id: access.sdk_client_id, client_id: access.sdk_client_id,
id: self.id, id: self.id,
}) })

View File

@@ -103,7 +103,7 @@ impl Convert for EvmWalletAccess {
Self::Output { Self::Output {
id: self.id, id: self.id,
access: Some(WalletAccess { access: Some(WalletAccess {
wallet_id: self.wallet_id, wallet_id: self.wallet_id.to_raw(),
sdk_client_id: self.client_id, sdk_client_id: self.client_id,
}), }),
} }

View File

@@ -2,7 +2,7 @@ use crate::{
db::models::NewEvmWalletAccess, db::models::NewEvmWalletAccess,
grpc::Convert, grpc::Convert,
peers::operator::{ peers::operator::{
OutOfBand, OperatorSession, OperatorSession, OutOfBand,
session::handlers::{ session::handlers::{
HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove, HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove,
HandleRevokeEvmWalletAccess, HandleSdkClientList, HandleRevokeEvmWalletAccess, HandleSdkClientList,
@@ -11,8 +11,8 @@ use crate::{
}; };
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::proto::{ use arbiter_proto::proto::{
shared::ClientInfo as ProtoClientMetadata,
operator::{ operator::{
operator_response::Payload as OperatorResponsePayload,
sdk_client::{ sdk_client::{
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel, self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
ConnectionRequest as ProtoSdkClientConnectionRequest, ConnectionRequest as ProtoSdkClientConnectionRequest,
@@ -24,8 +24,8 @@ use arbiter_proto::proto::{
request::Payload as SdkClientRequestPayload, request::Payload as SdkClientRequestPayload,
response::Payload as SdkClientResponsePayload, response::Payload as SdkClientResponsePayload,
}, },
operator_response::Payload as OperatorResponsePayload,
}, },
shared::ClientInfo as ProtoClientMetadata,
}; };
use kameo::actor::ActorRef; use kameo::actor::ActorRef;
@@ -115,7 +115,7 @@ async fn handle_list(
clients: clients clients: clients
.into_iter() .into_iter()
.map(|(client, metadata)| ProtoSdkClientEntry { .map(|(client, metadata)| ProtoSdkClientEntry {
id: client.id, id: client.id.to_raw(),
pubkey: client.public_key.clone(), pubkey: client.public_key.clone(),
info: Some(ProtoClientMetadata { info: Some(ProtoClientMetadata {
name: metadata.name, name: metadata.name,

View File

@@ -3,7 +3,6 @@ use crate::{
peers::operator::{OperatorSession, session::handlers::HandleQueryVaultState}, peers::operator::{OperatorSession, session::handlers::HandleQueryVaultState},
}; };
use arbiter_proto::{ use arbiter_proto::{
proto::shared::VaultState as ProtoVaultState,
proto::operator::{ proto::operator::{
operator_response::Payload as OperatorResponsePayload, operator_response::Payload as OperatorResponsePayload,
vault::{ vault::{
@@ -11,6 +10,7 @@ use arbiter_proto::{
response::Payload as VaultResponsePayload, response::Payload as VaultResponsePayload,
}, },
}, },
proto::shared::VaultState as ProtoVaultState,
}; };
use kameo::actor::ActorRef; use kameo::actor::ActorRef;

View File

@@ -1,14 +1,16 @@
use crate::{ use crate::{
grpc::{Convert, TryConvert}, grpc::{Convert, TryConvert},
peers::operator::vault_gate::{ peers::operator::vault_gate::{
self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey, self as vault_gate, HandleBootstrapEncryptedKey, HandleContributeBootstrapPassphrase,
HandleContributeUnsealPassphrase, HandleDeclareCommittee, HandleHandshake,
HandleUnsealEncryptedKey,
}, },
}; };
use arbiter_proto::proto::operator::{ use arbiter_proto::proto::operator::{
operator_request::Payload as OperatorRequestPayload, operator_request::Payload as OperatorRequestPayload,
vault::{ vault::{
self as proto_vault, self as proto_vault,
bootstrap::{self as proto_bootstrap}, bootstrap::{self as proto_bootstrap, request::Payload as BootstrapRequestPayload},
request::Payload as VaultRequestPayload, request::Payload as VaultRequestPayload,
unseal::{self as proto_unseal, request::Payload as UnsealRequestPayload}, unseal::{self as proto_unseal, request::Payload as UnsealRequestPayload},
}, },
@@ -73,6 +75,13 @@ impl TryConvert for UnsealRequestPayload {
match self { match self {
Self::Start(start) => start.try_convert(), Self::Start(start) => start.try_convert(),
Self::EncryptedKey(key) => Ok(key.convert()), Self::EncryptedKey(key) => Ok(key.convert()),
Self::ContributePassphrase(cp) => Ok(
vault_gate::Inbound::HandleContributeUnsealPassphrase(
HandleContributeUnsealPassphrase {
passphrase: cp.passphrase,
},
),
),
} }
} }
} }
@@ -107,12 +116,35 @@ impl TryConvert for proto_bootstrap::Request {
type Error = Status; type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> { fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
self.encrypted_key self.payload
.ok_or_else(|| Status::invalid_argument("Missing bootstrap encrypted key"))? .ok_or_else(|| Status::invalid_argument("Missing bootstrap payload"))?
.try_convert() .try_convert()
} }
} }
impl TryConvert for BootstrapRequestPayload {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
match self {
Self::EncryptedKey(key) => key.try_convert(),
Self::DeclareCommittee(dc) => Ok(
vault_gate::Inbound::HandleDeclareCommittee(HandleDeclareCommittee {
count: dc.count as usize,
}),
),
Self::ContributePassphrase(cp) => Ok(
vault_gate::Inbound::HandleContributeBootstrapPassphrase(
HandleContributeBootstrapPassphrase {
passphrase: cp.passphrase,
},
),
),
}
}
}
impl TryConvert for proto_bootstrap::BootstrapEncryptedKey { impl TryConvert for proto_bootstrap::BootstrapEncryptedKey {
type Output = vault_gate::Inbound; type Output = vault_gate::Inbound;
type Error = Status; type Error = Status;

View File

@@ -4,7 +4,6 @@ use crate::{
peers::operator::vault_gate::{self as vault_gate}, peers::operator::vault_gate::{self as vault_gate},
}; };
use arbiter_proto::proto::{ use arbiter_proto::proto::{
shared::VaultState as ProtoVaultState,
operator::{ operator::{
operator_response::Payload as OperatorResponsePayload, operator_response::Payload as OperatorResponsePayload,
vault::{ vault::{
@@ -17,6 +16,7 @@ use arbiter_proto::proto::{
}, },
}, },
}, },
shared::VaultState as ProtoVaultState,
}; };
use tonic::Status; use tonic::Status;
@@ -110,6 +110,40 @@ impl TryConvert for vault_gate::Outbound {
}; };
Ok(wrap_bootstrap_response(proto_result)) Ok(wrap_bootstrap_response(proto_result))
} }
Self::HandleDeclareCommittee(result) => {
let proto_result = match result {
Ok(()) => ProtoBootstrapResult::Success,
Err(err) => {
warn!(?err, "declare committee failed");
return Err(Status::internal("Failed to declare committee"));
}
};
Ok(wrap_bootstrap_response(proto_result))
}
Self::HandleContributeBootstrapPassphrase(result) => {
let proto_result = match result {
Ok(true) => ProtoBootstrapResult::Success,
Ok(false) => ProtoBootstrapResult::AwaitingContributions,
Err(err) => {
warn!(?err, "contribute bootstrap passphrase failed");
return Err(Status::internal("Failed to contribute bootstrap passphrase"));
}
};
Ok(wrap_bootstrap_response(proto_result))
}
Self::HandleContributeUnsealPassphrase(result) => {
let proto_result = match result {
Ok(true) => ProtoUnsealResult::Success,
Ok(false) => ProtoUnsealResult::AwaitingContributions,
Err(err) => {
warn!(?err, "contribute unseal passphrase failed");
return Err(Status::internal("Failed to contribute unseal passphrase"));
}
};
Ok(wrap_unseal_response(UnsealResponsePayload::Result(
proto_result.into(),
)))
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
actors::GlobalActors, db, peers::client::session::ClientSession, actors::GlobalActors, crypto::integrity::Integrable, db, peers::client::session::ClientSession,
}; };
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_macros::Hashable; use arbiter_macros::Hashable;
@@ -14,12 +14,15 @@ pub struct ClientProfile {
pub metadata: ClientMetadata, pub metadata: ClientMetadata,
} }
#[derive(Hashable, arbiter_macros::Integrable)] #[derive(Hashable)]
#[integrable(kind = "client_credentials")]
pub struct ClientCredentials { pub struct ClientCredentials {
pub pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
} }
impl Integrable for ClientCredentials {
const KIND: &'static str = "client_credentials";
}
pub struct ClientConnection { pub struct ClientConnection {
pub(crate) db: db::DatabasePool, pub(crate) db: db::DatabasePool,
pub(crate) actors: GlobalActors, pub(crate) actors: GlobalActors,

View File

@@ -4,7 +4,7 @@ use super::{
}; };
use crate::{ use crate::{
actors::bootstrap::ConsumeToken, actors::bootstrap::ConsumeToken,
db::{DatabasePool, schema::operator_client}, db::{DatabasePool, schema::operator_identity},
peers::operator::auth::Outbound, peers::operator::auth::Outbound,
}; };
use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT}; use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT};
@@ -14,19 +14,19 @@ use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use tracing::error; use tracing::error;
pub(super) struct ChallengeRequest { pub(crate) struct ChallengeRequest {
pub(super) pubkey: authn::PublicKey, pub(crate) pubkey: authn::PublicKey,
pub(super) bootstrap_token: Option<String>, pub(crate) bootstrap_token: Option<String>,
} }
pub struct ChallengeContext { pub struct ChallengeContext {
pub(super) challenge: AuthChallenge, pub challenge: AuthChallenge,
pub(super) pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
pub(super) bootstrap_token: Option<String>, pub bootstrap_token: Option<String>,
} }
pub(super) struct ChallengeSolution { pub(crate) struct ChallengeSolution {
pub(super) solution: Vec<u8>, pub(crate) solution: Vec<u8>,
} }
smlang::statemachine!( smlang::statemachine!(
@@ -44,9 +44,9 @@ async fn get_client_id(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<O
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
operator_client::table operator_identity::table
.filter(operator_client::public_key.eq(pubkey.to_bytes())) .filter(operator_identity::public_key.eq(pubkey.to_bytes()))
.select(operator_client::id) .select(operator_identity::id)
.first::<i32>(&mut conn) .first::<i32>(&mut conn)
.await .await
.optional() .optional()
@@ -63,9 +63,9 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
let id: i32 = diesel::insert_into(operator_client::table) let id: i32 = diesel::insert_into(operator_identity::table)
.values((operator_client::public_key.eq(pubkey_bytes),)) .values((operator_identity::public_key.eq(pubkey_bytes),))
.returning(operator_client::id) .returning(operator_identity::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.map_err(|e| { .map_err(|e| {

View File

@@ -3,7 +3,7 @@ use crate::{
GlobalActors, GlobalActors,
vault::{GetState, Vault}, vault::{GetState, Vault},
}, },
crypto::integrity::{self, AttestationStatus}, crypto::integrity::{self, AttestationStatus, Integrable},
db::{DatabaseError, DatabasePool}, db::{DatabaseError, DatabasePool},
peers::client::ClientProfile, peers::client::ClientProfile,
}; };
@@ -23,13 +23,16 @@ pub mod auth;
pub mod session; pub mod session;
pub mod vault_gate; pub mod vault_gate;
#[derive(Debug, Clone, Hashable, arbiter_macros::Integrable)] #[derive(Debug, Clone, Hashable)]
#[integrable(kind = "operator_credentials")]
pub struct Credentials { pub struct Credentials {
pub id: i32, pub id: i32,
pub pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
} }
impl Integrable for Credentials {
const KIND: &'static str = "operator_credentials";
}
// Messages, sent by operator to connection client without having a request // Messages, sent by operator to connection client without having a request
#[derive(Debug)] #[derive(Debug)]
pub enum OutOfBand { pub enum OutOfBand {

View File

@@ -1,12 +1,16 @@
use super::{Error, OperatorSession}; use super::{Error, OperatorSession};
use crate::{ use crate::{
actors::evm::{ actors::{
ClientSignTransaction, Generate, ListWallets, OperatorCreateGrant, OperatorListGrants, evm::{
SignTransactionError as EvmSignError, ClientSignTransaction, Generate, ListWallets, OperatorCreateGrant, OperatorDeleteGrant,
OperatorListGrants, SignTransactionError as EvmSignError,
},
flow_coordinator::client_connect_approval::ClientApprovalAnswer,
vault::VaultState,
},
db::models::{
EvmWalletAccess, EvmWalletId, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
}, },
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
actors::vault::VaultState,
db::models::{EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata},
evm::policies::{Grant, SpecificGrant}, evm::policies::{Grant, SpecificGrant},
}; };
use arbiter_crypto::authn; use arbiter_crypto::authn;
@@ -70,7 +74,9 @@ impl OperatorSession {
} }
#[message] #[message]
pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result<Vec<(i32, Address)>, Error> { pub(crate) async fn handle_evm_wallet_list(
&mut self,
) -> Result<Vec<(EvmWalletId, Address)>, Error> {
match self.props.actors.evm.ask(ListWallets {}).await { match self.props.actors.evm.ask(ListWallets {}).await {
Ok(wallets) => Ok(wallets), Ok(wallets) => Ok(wallets),
Err(err) => { Err(err) => {
@@ -116,22 +122,23 @@ impl OperatorSession {
} }
#[message] #[message]
pub(crate) fn handle_grant_delete(&mut self, grant_id: i32) -> Result<(), GrantMutationError> { pub(crate) async fn handle_grant_delete(
// match self &mut self,
// .props grant_id: i32,
// .actors ) -> Result<(), GrantMutationError> {
// .evm match self
// .ask(OperatorDeleteGrant { grant_id }) .props
// .await .actors
// { .evm
// Ok(()) => Ok(()), .ask(OperatorDeleteGrant { grant_id })
// Err(err) => { .await
// error!(?err, "EVM grant delete failed"); {
// Err(GrantMutationError::Internal) Ok(()) => Ok(()),
// } Err(err) => {
// } error!(?err, "EVM grant delete failed");
let _ = grant_id; Err(GrantMutationError::Internal)
todo!() }
}
} }
#[message] #[message]
@@ -211,8 +218,9 @@ impl OperatorSession {
pub(crate) async fn handle_list_wallet_access( pub(crate) async fn handle_list_wallet_access(
&mut self, &mut self,
) -> Result<Vec<EvmWalletAccess>, Error> { ) -> Result<Vec<EvmWalletAccess>, Error> {
use crate::db::schema::evm_wallet_access;
let mut conn = self.props.db.get().await?; let mut conn = self.props.db.get().await?;
let access_entries = crate::db::schema::evm_wallet_access::table let access_entries = evm_wallet_access::table
.select(EvmWalletAccess::as_select()) .select(EvmWalletAccess::as_select())
.load::<_>(&mut conn) .load::<_>(&mut conn)
.await?; .await?;

View File

@@ -1,7 +1,7 @@
use super::{OutOfBand, OperatorConnection}; use super::{OutOfBand, OperatorConnection};
use crate::{ use crate::{
actors::{ actors::{
flow_coordinator::client_connect_approval::{ClientApprovalAnswer, ClientApprovalController}, flow_coordinator::client_connect_approval::ClientApprovalController,
operator_registry::ConnectOperator, operator_registry::ConnectOperator,
}, },
peers::client::ClientProfile, peers::client::ClientProfile,
@@ -88,7 +88,6 @@ impl OperatorSession {
actor = "operator", actor = "operator",
event = "failed to announce new client connection" event = "failed to announce new client connection"
); );
let _ = controller.tell(ClientApprovalAnswer { approved: false }).await;
return; return;
} }

View File

@@ -3,8 +3,9 @@ use crate::{
actors::{ actors::{
GlobalActors, GlobalActors,
vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events}, vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events},
vault_coordinator::{ContributeBootstrap, ContributeUnseal, StartBootstrap},
}, },
crypto::integrity::{self}, crypto::{KeyCell, integrity::{self}},
db::DatabasePool, db::DatabasePool,
}; };
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
@@ -17,6 +18,9 @@ use tokio::sync::oneshot;
use tracing::{error, info}; use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
pub use VaultGateMessage as Inbound;
pub use VaultGateMessageReply as Outbound;
pub mod state; pub mod state;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@@ -98,11 +102,9 @@ impl VaultGate {
nonce: &[u8], nonce: &[u8],
ciphertext: &[u8], ciphertext: &[u8],
associated_data: &[u8], associated_data: &[u8],
) -> Result<SafeCell<Vec<u8>>, ()> { ) -> Result<KeyCell, ()> {
let nonce = XNonce::from_slice(nonce); let nonce = XNonce::from_slice(nonce);
let cipher = XChaCha20Poly1305::new(secret.as_bytes().into()); let cipher = XChaCha20Poly1305::new(secret.as_bytes().into());
let mut key_buffer = SafeCell::new(ciphertext.to_vec()); let mut key_buffer = SafeCell::new(ciphertext.to_vec());
let decryption_result = key_buffer.write_inline(|write_handle| { let decryption_result = key_buffer.write_inline(|write_handle| {
@@ -110,7 +112,9 @@ impl VaultGate {
}); });
match decryption_result { match decryption_result {
Ok(()) => Ok(key_buffer), Ok(()) => KeyCell::try_from(key_buffer).map_err(|()| {
error!("Decrypted key material has unexpected length");
}),
Err(err) => { Err(err) => {
error!(?err, "Failed to decrypt encrypted key material"); error!(?err, "Failed to decrypt encrypted key material");
Err(()) Err(())
@@ -119,7 +123,7 @@ impl VaultGate {
} }
} }
#[messages(messages = Inbound, replies = Outbound)] #[messages(enum)]
impl VaultGate { impl VaultGate {
#[message] #[message]
pub fn handle_handshake( pub fn handle_handshake(
@@ -152,17 +156,14 @@ impl VaultGate {
return Err(Error::State); return Err(Error::State);
}; };
let Ok(seal_key_buffer) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) let Ok(seal_key) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) else {
else {
return Err(Error::InvalidKey); return Err(Error::InvalidKey);
}; };
match self match self
.actors .actors
.vault .vault
.ask(TryUnseal { .ask(TryUnseal { seal_key })
seal_key_raw: seal_key_buffer,
})
.await .await
{ {
Ok(()) => { Ok(()) => {
@@ -192,17 +193,14 @@ impl VaultGate {
return Err(Error::State); return Err(Error::State);
}; };
let Ok(seal_key_buffer) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) let Ok(seal_key) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) else {
else {
return Err(Error::InvalidKey); return Err(Error::InvalidKey);
}; };
match self match self
.actors .actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap { seal_key })
seal_key_raw: seal_key_buffer,
})
.await .await
{ {
Ok(()) => { Ok(()) => {
@@ -234,6 +232,50 @@ impl VaultGate {
Ok(answer) Ok(answer)
} }
#[message]
pub async fn handle_declare_committee(&mut self, count: usize) -> Result<(), Error> {
self.actors
.vault_coordinator
.ask(StartBootstrap {
operator_id: self.auth_creds.id,
declared_count: count,
})
.await
.map_err(|_| Error::internal("VaultCoordinator unavailable"))
}
#[message]
pub async fn handle_contribute_bootstrap_passphrase(
&mut self,
passphrase: Vec<u8>,
) -> Result<bool, Error> {
let passphrase_cell = SafeCell::new(passphrase);
self.actors
.vault_coordinator
.ask(ContributeBootstrap {
operator_id: self.auth_creds.id,
passphrase: passphrase_cell,
})
.await
.map_err(|_| Error::internal("VaultCoordinator unavailable"))
}
#[message]
pub async fn handle_contribute_unseal_passphrase(
&mut self,
passphrase: Vec<u8>,
) -> Result<bool, Error> {
let passphrase_cell = SafeCell::new(passphrase);
self.actors
.vault_coordinator
.ask(ContributeUnseal {
operator_id: self.auth_creds.id,
passphrase: passphrase_cell,
})
.await
.map_err(|_| Error::internal("VaultCoordinator unavailable"))
}
} }
impl Message<events::Bootstrapped> for VaultGate { impl Message<events::Bootstrapped> for VaultGate {

View File

@@ -1,8 +1,5 @@
use super::common::ChannelTransport; use super::common::ChannelTransport;
use arbiter_crypto::{ use arbiter_crypto::authn::{self, AuthChallenge, CLIENT_CONTEXT};
authn::{self, AuthChallenge, CLIENT_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, ClientMetadata,
transport::{Receiver, Sender}, transport::{Receiver, Sender},
@@ -86,8 +83,8 @@ async fn insert_bootstrap_sentinel_operator(db: &db::DatabasePool) {
.0 .0
.to_vec(); .to_vec();
insert_into(schema::operator_client::table) insert_into(schema::operator_identity::table)
.values((schema::operator_client::public_key.eq(sentinel_key),)) .values((schema::operator_identity::public_key.eq(sentinel_key),))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -100,7 +97,7 @@ async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();

View File

@@ -2,7 +2,6 @@
dead_code, dead_code,
reason = "Common test utilities that may not be used in every test" reason = "Common test utilities that may not be used in every test"
)] )]
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_proto::transport::{Bi, Error, Receiver, Sender}; use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
use arbiter_server::{ use arbiter_server::{
actors::{GlobalActors, vault::Vault}, actors::{GlobalActors, vault::Vault},
@@ -19,7 +18,7 @@ pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
.await .await
.unwrap(); .unwrap();
actor actor
.bootstrap(SafeCell::new(b"test-seal-key".to_vec())) .bootstrap(arbiter_server::crypto::KeyCell::from([0u8; 32]))
.await .await
.unwrap(); .unwrap();
actor actor

View File

@@ -1,8 +1,5 @@
use super::common::ChannelTransport; use super::common::ChannelTransport;
use arbiter_crypto::{ use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT};
authn::{self, AuthChallenge, OPERATOR_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::transport::{Error as TransportError, Receiver, Sender}; use arbiter_proto::transport::{Error as TransportError, Receiver, Sender};
use arbiter_server::{ use arbiter_server::{
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap}, actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
@@ -157,7 +154,7 @@ pub async fn bootstrap_token_auth() {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();
@@ -206,8 +203,8 @@ pub async fn bootstrap_token_auth() {
task.await.unwrap().unwrap(); task.await.unwrap().unwrap();
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let stored_pubkey: Vec<u8> = schema::operator_client::table let stored_pubkey: Vec<u8> = schema::operator_identity::table
.select(schema::operator_client::public_key) .select(schema::operator_identity::public_key)
.first::<Vec<u8>>(&mut conn) .first::<Vec<u8>>(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -259,7 +256,7 @@ pub async fn bootstrap_invalid_token_auth() {
)); ));
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let count: i64 = schema::operator_client::table let count: i64 = schema::operator_identity::table
.count() .count()
.get_result::<i64>(&mut conn) .get_result::<i64>(&mut conn)
.await .await
@@ -275,7 +272,7 @@ pub async fn challenge_auth() {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();
@@ -285,9 +282,9 @@ pub async fn challenge_auth() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::operator_client::table) let id: i32 = insert_into(schema::operator_identity::table)
.values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_identity::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::operator_client::id) .returning(schema::operator_identity::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -361,7 +358,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();
@@ -371,8 +368,8 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
insert_into(schema::operator_client::table) insert_into(schema::operator_identity::table)
.values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_identity::public_key.eq(pubkey_bytes.clone()),))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -400,7 +397,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
let challenge = match response { let challenge = match response {
Ok(resp) => match resp { Ok(resp) => match resp {
auth::Outbound::AuthChallenge { challenge } => challenge, auth::Outbound::AuthChallenge { challenge } => challenge,
other @ auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got {other:?}"), other => panic!("Expected AuthChallenge, got {other:?}"),
}, },
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
@@ -434,7 +431,7 @@ pub async fn challenge_auth_rejects_invalid_signature() {
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), seal_key: arbiter_server::crypto::KeyCell::from([0u8; 32]),
}) })
.await .await
.unwrap(); .unwrap();
@@ -444,9 +441,9 @@ pub async fn challenge_auth_rejects_invalid_signature() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::operator_client::table) let id: i32 = insert_into(schema::operator_identity::table)
.values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_identity::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::operator_client::id) .returning(schema::operator_identity::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.unwrap(); .unwrap();

View File

@@ -1,7 +1,4 @@
use arbiter_crypto::{ use arbiter_crypto::authn;
authn,
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_server::{ use arbiter_server::{
actors::{ actors::{
GlobalActors, GlobalActors,
@@ -22,7 +19,7 @@ use tokio::sync::oneshot;
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
async fn setup_sealed_gate( async fn setup_sealed_gate(
seal_key: &[u8], seal_key: &[u8; 32],
) -> ( ) -> (
db::DatabasePool, db::DatabasePool,
kameo::actor::ActorRef<VaultGate>, kameo::actor::ActorRef<VaultGate>,
@@ -34,7 +31,7 @@ async fn setup_sealed_gate(
actors actors
.vault .vault
.ask(Bootstrap { .ask(Bootstrap {
seal_key_raw: SafeCell::new(seal_key.to_vec()), seal_key: arbiter_server::crypto::KeyCell::from(*seal_key),
}) })
.await .await
.unwrap(); .unwrap();
@@ -50,7 +47,7 @@ async fn setup_sealed_gate(
async fn client_dh_encrypt( async fn client_dh_encrypt(
gate: &kameo::actor::ActorRef<VaultGate>, gate: &kameo::actor::ActorRef<VaultGate>,
key_to_send: &[u8], key_to_send: &[u8; 32],
) -> HandleUnsealEncryptedKey { ) -> HandleUnsealEncryptedKey {
let client_secret = EphemeralSecret::random(); let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret); let client_public = PublicKey::from(&client_secret);
@@ -83,7 +80,7 @@ async fn client_dh_encrypt(
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_success() { pub async fn unseal_success() {
let seal_key = b"test-seal-key"; let seal_key = b"test-seal-key-padded-to-32bytes!";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await; let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, seal_key).await; let encrypted_key = client_dh_encrypt(&gate, seal_key).await;
@@ -95,10 +92,10 @@ pub async fn unseal_success() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_wrong_seal_key() { pub async fn unseal_wrong_seal_key() {
let seal_key = b"test-seal-key"; let seal_key = b"test-seal-key-padded-to-32bytes!";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await; let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await; let encrypted_key = client_dh_encrypt(&gate, b"wrong-key-padded-to-32-bytes!!!!").await;
let response = gate.ask(encrypted_key).await; let response = gate.ask(encrypted_key).await;
assert!(matches!( assert!(matches!(
@@ -112,7 +109,7 @@ pub async fn unseal_wrong_seal_key() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_corrupted_ciphertext() { pub async fn unseal_corrupted_ciphertext() {
let seal_key = b"test-seal-key"; let seal_key = b"test-seal-key-padded-to-32bytes!";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await; let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let client_secret = EphemeralSecret::random(); let client_secret = EphemeralSecret::random();
@@ -143,11 +140,11 @@ pub async fn unseal_corrupted_ciphertext() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_retry_after_invalid_key() { pub async fn unseal_retry_after_invalid_key() {
let seal_key = b"real-seal-key"; let seal_key = b"real-seal-key-padded-to-32bytes!";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await; let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
{ {
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await; let encrypted_key = client_dh_encrypt(&gate, b"wrong-key-padded-to-32-bytes!!!!").await;
let response = gate.ask(encrypted_key).await; let response = gate.ask(encrypted_key).await;
assert!(matches!( assert!(matches!(

View File

@@ -166,7 +166,7 @@ async fn decrypt_roundtrip_after_high_concurrency() {
.await .await
.unwrap(); .unwrap();
decryptor decryptor
.try_unseal(SafeCell::new(b"test-seal-key".to_vec())) .try_unseal(arbiter_server::crypto::KeyCell::from([0u8; 32]))
.await .await
.unwrap(); .unwrap();

View File

@@ -5,7 +5,7 @@ use arbiter_server::{
GlobalActors, GlobalActors,
vault::{Error, Vault}, vault::{Error, Vault},
}, },
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG}, crypto::{KeyCell, encryption::v1::{Nonce, ROOT_KEY_TAG}},
db::{self, models, schema}, db::{self, models, schema},
}; };
@@ -14,13 +14,13 @@ use diesel_async::RunQueryDsl;
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn bootstrap() { async fn test_bootstrap() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await .await
.unwrap(); .unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec()); let seal_key = KeyCell::from([0u8; 32]);
actor.bootstrap(seal_key).await.unwrap(); actor.bootstrap(seal_key).await.unwrap();
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
@@ -39,18 +39,18 @@ async fn bootstrap() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn bootstrap_rejects_double() { async fn test_bootstrap_rejects_double() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
let seal_key2 = SafeCell::new(b"test-seal-key".to_vec()); let seal_key2 = KeyCell::from([0u8; 32]);
let err = actor.bootstrap(seal_key2).await.unwrap_err(); let err = actor.bootstrap(seal_key2).await.unwrap_err();
assert!(matches!(err, Error::AlreadyBootstrapped)); assert!(matches!(err, Error::AlreadyBootstrapped));
} }
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn create_new_before_bootstrap_fails() { async fn test_create_new_before_bootstrap_fails() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await .await
@@ -65,7 +65,7 @@ async fn create_new_before_bootstrap_fails() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn decrypt_before_bootstrap_fails() { async fn test_decrypt_before_bootstrap_fails() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await .await
@@ -77,7 +77,7 @@ async fn decrypt_before_bootstrap_fails() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn new_restores_sealed_state() { async fn test_new_restores_sealed_state() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actor = common::bootstrapped_vault(&db).await; let actor = common::bootstrapped_vault(&db).await;
drop(actor); drop(actor);
@@ -91,7 +91,7 @@ async fn new_restores_sealed_state() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn unseal_correct_password() { async fn test_unseal_correct_password() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -105,7 +105,7 @@ async fn unseal_correct_password() {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await .await
.unwrap(); .unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec()); let seal_key = KeyCell::from([0u8; 32]);
actor.try_unseal(seal_key).await.unwrap(); actor.try_unseal(seal_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap(); let mut decrypted = actor.decrypt(aead_id).await.unwrap();
@@ -114,7 +114,7 @@ async fn unseal_correct_password() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn unseal_wrong_then_correct_password() { async fn test_unseal_wrong_then_correct_password() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -129,11 +129,11 @@ async fn unseal_wrong_then_correct_password() {
.await .await
.unwrap(); .unwrap();
let bad_key = SafeCell::new(b"wrong-password".to_vec()); let bad_key = KeyCell::from([1u8; 32]);
let err = actor.try_unseal(bad_key).await.unwrap_err(); let err = actor.try_unseal(bad_key).await.unwrap_err();
assert!(matches!(err, Error::InvalidKey)); assert!(matches!(err, Error::InvalidKey));
let good_key = SafeCell::new(b"test-seal-key".to_vec()); let good_key = KeyCell::from([0u8; 32]);
actor.try_unseal(good_key).await.unwrap(); actor.try_unseal(good_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap(); let mut decrypted = actor.decrypt(aead_id).await.unwrap();

View File

@@ -12,7 +12,7 @@ use std::collections::HashSet;
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn create_decrypt_roundtrip() { async fn test_create_decrypt_roundtrip() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -28,7 +28,7 @@ async fn create_decrypt_roundtrip() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn decrypt_nonexistent_returns_not_found() { async fn test_decrypt_nonexistent_returns_not_found() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -38,7 +38,7 @@ async fn decrypt_nonexistent_returns_not_found() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn ciphertext_differs_across_entries() { async fn test_ciphertext_differs_across_entries() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -76,7 +76,7 @@ async fn ciphertext_differs_across_entries() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn nonce_never_reused() { async fn test_nonce_never_reused() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;