From bbbb4feaa0cae8d64aaa40703fc250035f978f8b Mon Sep 17 00:00:00 2001 From: hdbg Date: Wed, 11 Feb 2026 13:31:49 +0100 Subject: [PATCH] feat(unseal): add unseal protocol and crypto infrastructure --- protobufs/unseal.proto | 2 +- server/Cargo.lock | 667 +++++++++++++++++- server/Cargo.toml | 5 +- server/crates/arbiter-proto/Cargo.toml | 3 + server/crates/arbiter-proto/src/lib.rs | 19 +- server/crates/arbiter-proto/src/transport.rs | 46 ++ server/crates/arbiter-server/Cargo.toml | 16 +- .../2026-02-09-143015-0000_init/up.sql | 40 +- server/crates/arbiter-server/src/context.rs | 157 +++++ .../arbiter-server/src/context/lease.rs | 41 ++ .../crates/arbiter-server/src/context/tls.rs | 89 +++ server/crates/arbiter-server/src/db.rs | 99 ++- server/crates/arbiter-server/src/db/models.rs | 57 ++ server/crates/arbiter-server/src/db/schema.rs | 24 +- server/crates/arbiter-server/src/handlers.rs | 2 + .../arbiter-server/src/handlers/client.rs | 12 + .../arbiter-server/src/handlers/user_agent.rs | 69 ++ server/crates/arbiter-server/src/lib.rs | 63 +- 18 files changed, 1323 insertions(+), 88 deletions(-) create mode 100644 server/crates/arbiter-proto/src/transport.rs create mode 100644 server/crates/arbiter-server/src/context.rs create mode 100644 server/crates/arbiter-server/src/context/lease.rs create mode 100644 server/crates/arbiter-server/src/context/tls.rs create mode 100644 server/crates/arbiter-server/src/handlers.rs create mode 100644 server/crates/arbiter-server/src/handlers/client.rs create mode 100644 server/crates/arbiter-server/src/handlers/user_agent.rs diff --git a/protobufs/unseal.proto b/protobufs/unseal.proto index b23d20a..9ba0837 100644 --- a/protobufs/unseal.proto +++ b/protobufs/unseal.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package arbiter.auth; +package arbiter.unseal; message UserAgentKeyRequest {} diff --git a/server/Cargo.lock b/server/Cargo.lock index 6723672..1a05936 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -50,12 +60,14 @@ name = "arbiter-proto" version = "0.1.0" dependencies = [ "bytes", + "futures", "prost", "prost-build", "prost-derive", "prost-types", "rkyv", "serde_json", + "tokio", "tonic", "tonic-prost", "tonic-prost-build", @@ -66,16 +78,30 @@ name = "arbiter-server" version = "0.1.0" dependencies = [ "arbiter-proto", + "async-trait", + "bytes", + "chacha20poly1305", + "chrono", + "dashmap", "diesel", "diesel-async", "diesel_migrations", "ed25519", "ed25519-dalek", + "futures", + "memsafe", "miette", + "rand", + "rcgen", + "restructed", + "rkyv", "rustls", + "secrecy", "smlang", + "statig", "thiserror", "tokio", + "tokio-stream", "tonic", "tracing", ] @@ -84,6 +110,45 @@ dependencies = [ name = "arbiter-useragent" version = "0.1.0" +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -114,6 +179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -208,6 +274,18 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bb8" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457d7ed3f888dfd2c7af56d4975cade43c622f74bdcddfed6d4352f57acc6310" +dependencies = [ + "futures-util", + "parking_lot", + "portable-atomic", + "tokio", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -276,6 +354,41 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.43" @@ -283,10 +396,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + [[package]] name = "cmake" version = "0.1.57" @@ -317,6 +444,41 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.0" @@ -333,7 +495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -389,22 +551,24 @@ dependencies = [ ] [[package]] -name = "deadpool" -version = "0.12.3" +name = "dashmap" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ - "deadpool-runtime", - "lazy_static", - "num_cpus", - "tokio", + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] -name = "deadpool-runtime" -version = "0.1.4" +name = "data-encoding" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "der" @@ -416,6 +580,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.5" @@ -447,7 +625,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13096fb8dae53f2d411c4b523bec85f45552ed3044a2ab4d85fb2092d9cb4f34" dependencies = [ - "deadpool", + "bb8", "diesel", "diesel_migrations", "futures-core", @@ -496,7 +674,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b42f1d9edf5207c137646b568a0168ca0ec25b7f9eaf7f9961da51a3d91cea" dependencies = [ "block-buffer", - "crypto-common", + "crypto-common 0.2.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] @@ -594,6 +783,16 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -618,6 +817,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -625,6 +839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -633,6 +848,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -651,11 +894,26 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", ] [[package]] @@ -690,6 +948,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -719,6 +978,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -743,12 +1008,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "http" version = "1.4.0" @@ -907,6 +1166,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -1009,6 +1277,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memsafe" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f3e199d5e8adf073900f95b635f1192c394a442ed406c16dc7991b74501645" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "miette" version = "7.6.0" @@ -1067,6 +1345,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1074,6 +1358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1113,12 +1398,41 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1128,16 +1442,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.37.3" @@ -1147,12 +1451,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "owo-colors" version = "4.2.3" @@ -1182,6 +1501,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1247,6 +1576,23 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1263,6 +1609,52 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1390,6 +1782,47 @@ dependencies = [ "ptr_meta", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.1", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "aws-lc-rs", + "pem", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", + "zeroize", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1437,6 +1870,18 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "restructed" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6f6e863d7d9d318699737c043d560dce1ea3cb6f5c78e0a3f0d1f257c73dfc" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "ring" version = "0.17.14" @@ -1447,7 +1892,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -1506,6 +1951,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.3" @@ -1552,7 +2006,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -1576,6 +2030,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "semver" version = "1.0.27" @@ -1641,7 +2104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -1667,6 +2130,12 @@ version = "3.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "simdutf8" version = "0.1.5" @@ -1738,6 +2207,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "statig" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c04b4a9f2d66294d63bdd8df834caad9f8e181997c3cf766b6b4f6d12d4fbc" +dependencies = [ + "statig_macro", +] + +[[package]] +name = "statig_macro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8090ca395ee30c4b38fee68cf4ddf0bcc5f01aa83364cd4c3ec737a1596dab4d" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "string_morph" version = "0.1.0" @@ -1805,6 +2295,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "tempfile" version = "3.25.0" @@ -1951,6 +2452,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -2007,6 +2509,7 @@ dependencies = [ "axum", "base64", "bytes", + "flate2", "h2", "http", "http-body", @@ -2025,6 +2528,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", + "zstd", ] [[package]] @@ -2176,6 +2680,22 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -2198,6 +2718,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -2310,6 +2836,28 @@ dependencies = [ "semver", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -2619,6 +3167,33 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "zeroize" version = "1.8.2" @@ -2630,3 +3205,31 @@ name = "zmij" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/server/Cargo.toml b/server/Cargo.toml index 20612b4..6e195b1 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ resolver = "3" [workspace.dependencies] prost = "0.14.3" -tonic = { version = "0.14.3", features = ["tls-connect-info"] } +tonic = { version = "0.14.3", features = ["deflate", "gzip", "tls-connect-info", "zstd"] } tracing = "0.1.44" tokio = { version = "1.49.0", features = ["full"] } ed25519 = "3.0.0-rc.4" @@ -22,3 +22,6 @@ rustls = "0.23.36" smlang = "0.8.0" miette = { version = "7.6.0", features = ["fancy", "serde"] } thiserror = "2.0.18" +async-trait = "0.1.89" +futures = "0.3.31" +tokio-stream = { version = "0.1.18", features = ["full"] } diff --git a/server/crates/arbiter-proto/Cargo.toml b/server/crates/arbiter-proto/Cargo.toml index 2d3ca52..792c24e 100644 --- a/server/crates/arbiter-proto/Cargo.toml +++ b/server/crates/arbiter-proto/Cargo.toml @@ -12,6 +12,9 @@ prost-derive = "0.14.3" prost-types = { version = "0.14.3", features = ["chrono"] } tonic-prost = "0.14.3" rkyv = "0.8.15" +tokio.workspace = true +futures.workspace = true + [build-dependencies] diff --git a/server/crates/arbiter-proto/src/lib.rs b/server/crates/arbiter-proto/src/lib.rs index 4d1ff6d..db493d3 100644 --- a/server/crates/arbiter-proto/src/lib.rs +++ b/server/crates/arbiter-proto/src/lib.rs @@ -4,4 +4,21 @@ pub mod proto { pub mod auth { tonic::include_proto!("arbiter.auth"); } -} \ No newline at end of file +} + +pub mod transport; + +pub static BOOTSTRAP_TOKEN_PATH: &'static str = "bootstrap_token"; + +pub fn home_path() -> Result { + static ARBITER_HOME: &'static str = ".arbiter"; + let home_dir = std::env::home_dir().ok_or(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "can not get home directory", + ))?; + + let arbiter_home = home_dir.join(ARBITER_HOME); + std::fs::create_dir_all(&arbiter_home)?; + + Ok(arbiter_home) +} diff --git a/server/crates/arbiter-proto/src/transport.rs b/server/crates/arbiter-proto/src/transport.rs new file mode 100644 index 0000000..691ef9a --- /dev/null +++ b/server/crates/arbiter-proto/src/transport.rs @@ -0,0 +1,46 @@ +use futures::{Stream, StreamExt}; +use tokio::sync::mpsc::{self, error::SendError}; +use tonic::{Status, Streaming}; + + +// Abstraction for stream for sans-io capabilities +pub trait Bi: Stream> + Send + Sync + 'static { + type Error; + fn send( + &mut self, + item: Result, + ) -> impl std::future::Future> + Send; +} + +// Bi-directional stream abstraction for handling gRPC streaming requests and responses +pub struct BiStream { + pub request_stream: Streaming, + pub response_sender: mpsc::Sender>, +} + +impl Stream for BiStream +where + T: Send + 'static, + U: Send + 'static, +{ + type Item = Result; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.request_stream.poll_next_unpin(cx) + } +} + +impl Bi for BiStream +where + T: Send + 'static, + U: Send + 'static, +{ + type Error = SendError>; + + async fn send(&mut self, item: Result) -> Result<(), Self::Error> { + self.response_sender.send(item).await + } +} diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index 360975d..9f8bcda 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://git.markettakers.org/MarketTakers/arbiter" [dependencies] diesel = { version = "2.3.6", features = ["sqlite", "uuid", "time", "chrono", "serde_json"] } -diesel-async = { version = "0.7.4", features = ["sqlite", "tokio", "migrations", "pool", "deadpool"] } +diesel-async = { version = "0.7.4", features = ["bb8", "migrations", "sqlite", "tokio"] } ed25519.workspace = true ed25519-dalek.workspace = true arbiter-proto.path = "../arbiter-proto" @@ -18,3 +18,17 @@ smlang.workspace = true miette.workspace = true thiserror.workspace = true diesel_migrations = { version = "2.3.1", features = ["sqlite"] } +async-trait.workspace = true +statig = { version = "0.4.1", features = ["async"] } +secrecy = "0.10.3" +futures.workspace = true +tokio-stream.workspace = true +dashmap = "6.1.0" +rand.workspace = true +rcgen = { version = "0.14.7", features = ["aws_lc_rs", "pem", "x509-parser", "zeroize"], default-features = false } +rkyv = { version = "0.8.15", features = ["aligned", "little_endian", "pointer_width_64"] } +restructed = "0.2.2" +chrono.workspace = true +bytes = "1.11.1" +memsafe = "0.4.0" +chacha20poly1305 = { version = "0.10.1", features = ["std"] } diff --git a/server/crates/arbiter-server/migrations/2026-02-09-143015-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-09-143015-0000_init/up.sql index 65cac74..366cf76 100644 --- a/server/crates/arbiter-server/migrations/2026-02-09-143015-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-09-143015-0000_init/up.sql @@ -1,29 +1,37 @@ --- This is a singleton +create table if not exists aead_encrypted ( + id INTEGER not null PRIMARY KEY, + current_nonce integer not null default(1), -- if re-encrypted, this should be incremented + ciphertext blob not null, + tag blob not null, + schema_version integer not null default(1) -- server would need to reencrypt, because this means that we have changed algorithm +) STRICT; + +-- This is a singleton create table if not exists arbiter_settings ( - root_key_enc blob, -- if null, means wasn't bootstrapped yet + id INTEGER not null PRIMARY KEY CHECK (id = 1), -- singleton row, id must be 1 + root_key_id integer references aead_encrypted (id) on delete RESTRICT, -- if null, means wasn't bootstrapped yet cert_key blob not null, cert blob not null ) STRICT; -create table if not exists key_identity( - id integer primary key, +create table if not exists key_identity ( + id integer not null primary key, name text not null, public_key text not null, - created_at integer not null default (unixepoch('now')), - updated_at integer not null default (unixepoch('now')) + created_at integer not null default(unixepoch ('now')), + updated_at integer not null default(unixepoch ('now')) ) STRICT; create table if not exists useragent_client ( - id integer primary key, - key_identity_id integer not null references key_identity(id) on delete cascade, - created_at integer not null default (unixepoch('now')), - updated_at integer not null default (unixepoch('now')) + id integer not null primary key, + key_identity_id integer not null references key_identity (id) on delete cascade, + created_at integer not null default(unixepoch ('now')), + updated_at integer not null default(unixepoch ('now')) ) STRICT; - -create table if not exists program_client( - id integer primary key, - key_identity_id integer not null references key_identity(id) on delete cascade, - created_at integer not null default (unixepoch('now')), - updated_at integer not null default (unixepoch('now')) +create table if not exists program_client ( + id integer not null primary key, + key_identity_id integer not null references key_identity (id) on delete cascade, + created_at integer not null default(unixepoch ('now')), + updated_at integer not null default(unixepoch ('now')) ) STRICT; \ No newline at end of file diff --git a/server/crates/arbiter-server/src/context.rs b/server/crates/arbiter-server/src/context.rs new file mode 100644 index 0000000..bd6ebd6 --- /dev/null +++ b/server/crates/arbiter-server/src/context.rs @@ -0,0 +1,157 @@ +use std::sync::Arc; + +use diesel::OptionalExtension as _; +use diesel_async::RunQueryDsl as _; +use ed25519_dalek::VerifyingKey; +use miette::Diagnostic; +use rand::rngs::StdRng; +use smlang::statemachine; +use thiserror::Error; +use tokio::sync::RwLock; + +use crate::{ + context::{ + lease::LeaseHandler, + tls::{TlsDataRaw, TlsManager}, + }, + db::{ + self, + models::ArbiterSetting, + schema::{self, arbiter_settings}, + }, +}; + +pub(crate) mod lease; +pub(crate) mod tls; +pub(crate) mod bootstrap { + +} + +#[derive(Error, Debug, Diagnostic)] +pub enum InitError { + #[error("Database setup failed: {0}")] + #[diagnostic(code(arbiter_server::init::database_setup))] + DatabaseSetup(#[from] db::DatabaseSetupError), + + #[error("Connection acquire failed: {0}")] + #[diagnostic(code(arbiter_server::init::database_pool))] + DatabasePool(#[from] db::PoolError), + + #[error("Database query error: {0}")] + #[diagnostic(code(arbiter_server::init::database_query))] + DatabaseQuery(#[from] diesel::result::Error), + + #[error("TLS initialization failed: {0}")] + #[diagnostic(code(arbiter_server::init::tls_init))] + Tls(#[from] tls::TlsInitError), +} + +// TODO: Placeholder for secure root key cell implementation +pub struct KeyStorage; + +statemachine! { + name: Server, + transitions: { + *NotBootstrapped + Bootstrapped = Sealed, + Sealed + Unsealed(KeyStorage) / move_key = Ready(KeyStorage), + Ready(KeyStorage) + Sealed / dispose_key = Sealed, + } +} +pub struct _Context; +impl ServerStateMachineContext for _Context { + fn move_key(&mut self, _event_data: KeyStorage) -> Result { + todo!() + } + + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + fn dispose_key(&mut self, _state_data: &KeyStorage) -> Result<(), ()> { + todo!() + } +} + +pub(crate) struct _ServerContextInner { + pub db: db::DatabasePool, + pub state: RwLock>, + pub rng: StdRng, + pub tls: TlsManager, + pub user_agent_leases: LeaseHandler, + pub client_leases: LeaseHandler, +} +#[derive(Clone)] +pub(crate) struct ServerContext(Arc<_ServerContextInner>); + +impl std::ops::Deref for ServerContext { + type Target = _ServerContextInner; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ServerContext { + async fn load_tls( + db: &mut db::DatabaseConnection, + settings: Option<&ArbiterSetting>, + ) -> Result { + match &settings { + Some(settings) => { + let tls_data_raw = TlsDataRaw { + cert: settings.cert.clone(), + key: settings.cert_key.clone(), + }; + + Ok(TlsManager::new(Some(tls_data_raw)).await?) + } + None => { + let tls = TlsManager::new(None).await?; + let tls_data_raw = tls.bytes(); + + diesel::insert_into(arbiter_settings::table) + .values(&ArbiterSetting { + id: 1, + root_key_id: None, + cert_key: tls_data_raw.key, + cert: tls_data_raw.cert, + }) + .execute(db) + .await?; + + Ok(tls) + } + } + } + + pub async fn new(db: db::DatabasePool) -> Result { + let mut conn = db.get().await?; + let rng = rand::make_rng(); + + let settings = arbiter_settings::table + .first::(&mut conn) + .await + .optional()?; + + let tls = Self::load_tls(&mut conn, settings.as_ref()).await?; + + drop(conn); + + let mut state = ServerStateMachine::new(_Context); + + if let Some(settings) = &settings + && settings.root_key_id.is_some() + { + // TODO: pass the encrypted root key to the state machine and let it handle decryption and transition to Sealed + let _ = state.process_event(ServerEvents::Bootstrapped); + } + + + Ok(Self(Arc::new(_ServerContextInner { + db, + rng, + tls, + state: RwLock::new(state), + user_agent_leases: Default::default(), + client_leases: Default::default(), + }))) + } +} diff --git a/server/crates/arbiter-server/src/context/lease.rs b/server/crates/arbiter-server/src/context/lease.rs new file mode 100644 index 0000000..2b0f2bd --- /dev/null +++ b/server/crates/arbiter-server/src/context/lease.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use dashmap::DashSet; + +#[derive(Clone, Default)] +struct LeaseStorage(Arc>); + +// A lease that automatically releases the item when dropped +pub struct Lease { + item: T, + storage: LeaseStorage, +} +impl Drop for Lease { + fn drop(&mut self) { + self.storage.0.remove(&self.item); + } +} + +#[derive(Clone, Default)] +pub struct LeaseHandler { + storage: LeaseStorage, +} + +impl LeaseHandler { + pub fn new() -> Self { + Self { + storage: LeaseStorage(Arc::new(DashSet::new())), + } + } + + pub fn acquire(&self, item: T) -> Result, ()> { + if self.storage.0.insert(item.clone()) { + Ok(Lease { + item, + storage: self.storage.clone(), + }) + } else { + Err(()) + } + } +} diff --git a/server/crates/arbiter-server/src/context/tls.rs b/server/crates/arbiter-server/src/context/tls.rs new file mode 100644 index 0000000..ce9b1b4 --- /dev/null +++ b/server/crates/arbiter-server/src/context/tls.rs @@ -0,0 +1,89 @@ +use std::string::FromUtf8Error; + +use miette::Diagnostic; +use rcgen::{Certificate, KeyPair}; +use rustls::pki_types::CertificateDer; +use thiserror::Error; + + +#[derive(Error, Debug, Diagnostic)] +pub enum TlsInitError { + #[error("Key generation error during TLS initialization: {0}")] + #[diagnostic(code(arbiter_server::tls_init::key_generation))] + KeyGeneration(#[from] rcgen::Error), + + #[error("Key invalid format: {0}")] + #[diagnostic(code(arbiter_server::tls_init::key_invalid_format))] + KeyInvalidFormat(#[from] FromUtf8Error), + + #[error("Key deserialization error: {0}")] + #[diagnostic(code(arbiter_server::tls_init::key_deserialization))] + KeyDeserializationError(rcgen::Error), +} + +pub struct TlsData { + pub cert: CertificateDer<'static>, + pub keypair: KeyPair, +} + +pub struct TlsDataRaw { + pub cert: Vec, + pub key: Vec, +} +impl TlsDataRaw { + pub fn serialize(cert: &TlsData) -> Self { + Self { + cert: cert.cert.as_ref().to_vec(), + key: cert.keypair.serialize_pem().as_bytes().to_vec(), + } + } + + pub fn deserialize(&self) -> Result { + let cert = CertificateDer::from_slice(&self.cert).into_owned(); + + let key = + String::from_utf8(self.key.clone()).map_err(TlsInitError::KeyInvalidFormat)?; + + let keypair = KeyPair::from_pem(&key).map_err(TlsInitError::KeyDeserializationError)?; + + Ok(TlsData { cert, keypair }) + } +} + +fn generate_cert(key: &KeyPair) -> Result { + let params = rcgen::CertificateParams::new(vec![ + "arbiter.local".to_string(), + "localhost".to_string(), + ])?; + + params.self_signed(key) +} + +// TODO: Implement cert rotation +pub(crate) struct TlsManager { + data: TlsData, +} + +impl TlsManager { + pub async fn new(data: Option) -> Result { + match data { + Some(raw) => { + let tls_data = raw.deserialize()?; + Ok(Self { data: tls_data }) + } + None => { + let keypair = KeyPair::generate()?; + let cert = generate_cert(&keypair)?; + let tls_data = TlsData { + cert: cert.der().clone(), + keypair, + }; + Ok(Self { data: tls_data }) + } + } + } + + pub fn bytes(&self) -> TlsDataRaw { + TlsDataRaw::serialize(&self.data) + } +} diff --git a/server/crates/arbiter-server/src/db.rs b/server/crates/arbiter-server/src/db.rs index 1b965ee..80fa260 100644 --- a/server/crates/arbiter-server/src/db.rs +++ b/server/crates/arbiter-server/src/db.rs @@ -1,5 +1,11 @@ +use std::sync::Arc; + use diesel::{Connection as _, SqliteConnection, connection::SimpleConnection as _}; -use diesel_async::sync_connection_wrapper::SyncConnectionWrapper; +use diesel_async::{ + AsyncConnection, SimpleAsyncConnection as _, + pooled_connection::{AsyncDieselConnectionManager, ManagerConfig, RecyclingMethod}, + sync_connection_wrapper::SyncConnectionWrapper, +}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use miette::Diagnostic; use thiserror::Error; @@ -7,10 +13,12 @@ use thiserror::Error; pub mod models; pub mod schema; -pub type Database = SyncConnectionWrapper; +pub type DatabaseConnection = SyncConnectionWrapper; +pub type DatabasePool = diesel_async::pooled_connection::bb8::Pool; +pub type PoolInitError = diesel_async::pooled_connection::PoolError; +pub type PoolError = diesel_async::pooled_connection::bb8::RunError; -static ARBITER_HOME: &'static str = ".arbiter"; -static DB_FILE: &'static str = "db.sqlite"; +static DB_FILE: &'static str = "arbiter.sqlite"; const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); @@ -18,7 +26,7 @@ const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); pub enum DatabaseSetupError { #[error("Failed to determine home directory")] #[diagnostic(code(arbiter::db::home_dir_error))] - HomeDir(Option), + HomeDir(std::io::Error), #[error(transparent)] #[diagnostic(code(arbiter::db::connection_error))] @@ -31,27 +39,22 @@ pub enum DatabaseSetupError { #[error(transparent)] #[diagnostic(code(arbiter::db::migration_error))] Migration(Box), + + #[error(transparent)] + #[diagnostic(code(arbiter::db::pool_error))] + Pool(#[from] PoolInitError), } fn database_path() -> Result { - let home_dir = std::env::home_dir().ok_or_else(|| DatabaseSetupError::HomeDir(None))?; - - let arbiter_home = home_dir.join(ARBITER_HOME); + let arbiter_home = arbiter_proto::home_path().map_err(DatabaseSetupError::HomeDir)?; let db_path = arbiter_home.join(DB_FILE); - std::fs::create_dir_all(arbiter_home) - .map_err(|err| DatabaseSetupError::HomeDir(Some(err)))?; - Ok(db_path) } -fn setup_concurrency(conn: &mut SqliteConnection) -> Result<(), diesel::result::Error> { - // see https://fractaledmind.github.io/2023/09/07/enhancing-rails-sqlite-fine-tuning/ - // sleep if the database is busy, this corresponds to up to 2 seconds sleeping time. - conn.batch_execute("PRAGMA busy_timeout = 2000;")?; - // better write-concurrency - conn.batch_execute("PRAGMA journal_mode = WAL;")?; + +fn db_config(conn: &mut SqliteConnection) -> Result<(), diesel::result::Error> { // fsync only in critical moments conn.batch_execute("PRAGMA synchronous = NORMAL;")?; // write WAL changes back every 1000 pages, for an in average 1MB WAL file. @@ -60,24 +63,62 @@ fn setup_concurrency(conn: &mut SqliteConnection) -> Result<(), diesel::result:: // free some space by truncating possibly massive WAL files from the last run conn.batch_execute("PRAGMA wal_checkpoint(TRUNCATE);")?; + // sqlite foreign keys are disabled by default, enable them for safety + conn.batch_execute("PRAGMA foreign_keys = ON;")?; + + // better space reclamation + conn.batch_execute("PRAGMA auto_vacuum = FULL;")?; + + // secure delete, overwrite deleted content with zeros to prevent recovery + conn.batch_execute("PRAGMA secure_delete = ON;")?; + Ok(()) } -#[tracing::instrument] -pub fn connect() -> Result { - let database_url = format!( - "{}?mode=rwc", - database_path()? - .to_str() - .ok_or_else(|| DatabaseSetupError::HomeDir(None))? - ); - let mut conn = - SqliteConnection::establish(&database_url).map_err(DatabaseSetupError::Connection)?; +fn initialize_database(url: &str) -> Result<(), DatabaseSetupError> { + let mut conn = SqliteConnection::establish(url).map_err(DatabaseSetupError::Connection)?; - setup_concurrency(&mut conn).map_err(DatabaseSetupError::ConcurrencySetup)?; + db_config(&mut conn).map_err(DatabaseSetupError::ConcurrencySetup)?; conn.run_pending_migrations(MIGRATIONS) .map_err(DatabaseSetupError::Migration)?; - Ok(SyncConnectionWrapper::new(conn)) + Ok(()) +} + +pub async fn create_pool() -> Result { + let database_url = format!( + "{}?mode=rwc", + database_path()? + .to_str() + .expect("database path is not valid UTF-8") + ); + + initialize_database(&database_url)?; + + let mut config = ManagerConfig::default(); + config.custom_setup = Box::new(|url| { + Box::pin(async move { + let mut conn = DatabaseConnection::establish(url).await?; + + // see https://fractaledmind.github.io/2023/09/07/enhancing-rails-sqlite-fine-tuning/ + // sleep if the database is busy, this corresponds to up to 9 seconds sleeping time. + conn.batch_execute("PRAGMA busy_timeout = 9000;") + .await + .map_err(diesel::ConnectionError::CouldntSetupConfiguration)?; + // better write-concurrency + conn.batch_execute("PRAGMA journal_mode = WAL;") + .await + .map_err(diesel::ConnectionError::CouldntSetupConfiguration)?; + + Ok(conn) + }) + }); + + let pool = DatabasePool::builder().build(AsyncDieselConnectionManager::new_with_config( + database_url, + config, + )).await?; + + Ok(pool) } diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index e69de29..63810ec 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -0,0 +1,57 @@ +#![allow(unused)] +#![allow(clippy::all)] + +use crate::db::schema::{self, aead_encrypted, arbiter_settings}; +use diesel::{prelude::*, sqlite::Sqlite}; + +pub mod types { + use chrono::{DateTime, Utc}; + pub struct SqliteTimestamp(DateTime); +} + +#[derive(Queryable, Debug, Insertable)] +#[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))] +pub struct AeadEncrypted { + pub id: i32, + pub ciphertext: Vec, + pub tag: Vec, + pub current_nonce: i32, + pub schema_version: i32, +} + +#[derive(Queryable, Debug, Insertable)] +#[diesel(table_name = arbiter_settings, check_for_backend(Sqlite))] +pub struct ArbiterSetting { + pub id: i32, + pub root_key_id: Option, // references aead_encrypted.id + pub cert_key: Vec, + pub cert: Vec, +} + +#[derive(Queryable, Debug)] +#[diesel(table_name = schema::key_identity, check_for_backend(Sqlite))] +pub struct KeyIdentity { + pub id: i32, + pub name: String, + pub public_key: String, + pub created_at: i32, + pub updated_at: i32, +} + +#[derive(Queryable, Debug)] +#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))] +pub struct ProgramClient { + pub id: i32, + pub key_identity_id: i32, + pub created_at: i32, + pub updated_at: i32, +} + +#[derive(Queryable, Debug)] +#[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))] +pub struct UseragentClient { + pub id: i32, + pub key_identity_id: i32, + pub created_at: i32, + pub updated_at: i32, +} diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 346f0eb..f662849 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -1,9 +1,19 @@ // @generated automatically by Diesel CLI. diesel::table! { - arbiter_settings (rowid) { - rowid -> Integer, - root_key_enc -> Nullable, + aead_encrypted (id) { + id -> Integer, + current_nonce -> Integer, + ciphertext -> Binary, + tag -> Binary, + schema_version -> Integer, + } +} + +diesel::table! { + arbiter_settings (id) { + id -> Integer, + root_key_id -> Nullable, cert_key -> Binary, cert -> Binary, } @@ -11,7 +21,7 @@ diesel::table! { diesel::table! { key_identity (id) { - id -> Nullable, + id -> Integer, name -> Text, public_key -> Text, created_at -> Integer, @@ -21,7 +31,7 @@ diesel::table! { diesel::table! { program_client (id) { - id -> Nullable, + id -> Integer, key_identity_id -> Integer, created_at -> Integer, updated_at -> Integer, @@ -30,17 +40,19 @@ diesel::table! { diesel::table! { useragent_client (id) { - id -> Nullable, + id -> Integer, key_identity_id -> Integer, created_at -> Integer, updated_at -> Integer, } } +diesel::joinable!(arbiter_settings -> aead_encrypted (root_key_id)); diesel::joinable!(program_client -> key_identity (key_identity_id)); diesel::joinable!(useragent_client -> key_identity (key_identity_id)); diesel::allow_tables_to_appear_in_same_query!( + aead_encrypted, arbiter_settings, key_identity, program_client, diff --git a/server/crates/arbiter-server/src/handlers.rs b/server/crates/arbiter-server/src/handlers.rs new file mode 100644 index 0000000..691101e --- /dev/null +++ b/server/crates/arbiter-server/src/handlers.rs @@ -0,0 +1,2 @@ +pub mod user_agent; +pub mod client; diff --git a/server/crates/arbiter-server/src/handlers/client.rs b/server/crates/arbiter-server/src/handlers/client.rs new file mode 100644 index 0000000..3828821 --- /dev/null +++ b/server/crates/arbiter-server/src/handlers/client.rs @@ -0,0 +1,12 @@ +use arbiter_proto::{ + proto::{ClientRequest, ClientResponse}, + transport::Bi, +}; + +use crate::ServerContext; + +pub(crate) async fn handle_client( + _context: ServerContext, + _bistream: impl Bi, +) { +} diff --git a/server/crates/arbiter-server/src/handlers/user_agent.rs b/server/crates/arbiter-server/src/handlers/user_agent.rs new file mode 100644 index 0000000..3c2cdb3 --- /dev/null +++ b/server/crates/arbiter-server/src/handlers/user_agent.rs @@ -0,0 +1,69 @@ +use arbiter_proto::{ + proto::{ + UserAgentRequest, UserAgentResponse, + auth::{ + self, AuthChallengeRequest, ClientMessage, client_message::Payload as ClientAuthPayload + }, + user_agent_request::Payload as UserAgentRequestPayload, + }, + transport::Bi, +}; +use futures::StreamExt; +use tracing::error; + +use crate::ServerContext; + +smlang::statemachine!( + name: UserAgentAuth, + derive_states: [Debug], + derive_events: [Clone, Debug], + transitions: { + *Init + ReceivedRequest(ed25519_dalek::VerifyingKey) / provide_challenge = WaitingForChallengeSolution(auth::AuthChallenge), + WaitingForChallengeSolution(auth::AuthChallenge) + ReceivedGoodSolution = Authenticated, + WaitingForChallengeSolution(auth::AuthChallenge) + ReceivedBadSolution = Error, + } +); + + + +impl UserAgentAuthStateMachineContext for ServerContext { + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + fn provide_challenge< >(&mut self,_event_data:ed25519_dalek::VerifyingKey) -> Result { + todo!() + } +} + +pub(crate) async fn handle_user_agent( + context: ServerContext, + mut bistream: impl Bi + Unpin, +) { + let auth_sm = UserAgentAuthStateMachine::new(context); + + while let Some(Ok(msg)) = bistream.next().await + && auth_sm.state() != &UserAgentAuthStates::Authenticated + { + let Some(msg) = msg.payload else { + error!(handler = "useragent", "Received message with no payload"); + return; + }; + + let UserAgentRequestPayload::AuthMessage(ClientMessage { + payload: Some(client_message), + }) = msg + else { + error!( + handler = "useragent", + "Received unexpected message type during authentication" + ); + return; + }; + + match client_message { + ClientAuthPayload::AuthChallengeRequest(auth_challenge_request) => { + let AuthChallengeRequest { pubkey } = auth_challenge_request; + }, + ClientAuthPayload::AuthChallengeSolution(_auth_challenge_solution) => todo!(), + } + } +} diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index 027d337..b2ad941 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -1,5 +1,66 @@ +#![allow(unused)] + +use arbiter_proto::{ + proto::{ClientRequest, ClientResponse, UserAgentRequest, UserAgentResponse}, + transport::BiStream, +}; +use async_trait::async_trait; +use tokio_stream::wrappers::ReceiverStream; + +use tokio::sync::mpsc; +use tonic::{Request, Response, Status}; + +use crate::{ + handlers::{client::handle_client, user_agent::handle_user_agent}, + context::ServerContext, +}; + mod db; +pub mod handlers; +mod context; + +const DEFAULT_CHANNEL_SIZE: usize = 1000; pub struct Server { - pub db: db::Database, + context: ServerContext, +} + +#[async_trait] +impl arbiter_proto::proto::arbiter_service_server::ArbiterService for Server { + type UserAgentStream = ReceiverStream>; + type ClientStream = ReceiverStream>; + + async fn client( + &self, + request: Request>, + ) -> Result, Status> { + let req_stream = request.into_inner(); + let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE); + tokio::spawn(handle_client( + self.context.clone(), + BiStream { + request_stream: req_stream, + response_sender: tx, + }, + )); + + Ok(Response::new(ReceiverStream::new(rx))) + } + + async fn user_agent( + &self, + request: Request>, + ) -> Result, Status> { + let req_stream = request.into_inner(); + let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE); + + tokio::spawn(handle_user_agent( + self.context.clone(), + BiStream { + request_stream: req_stream, + response_sender: tx, + }, + )); + Ok(Response::new(ReceiverStream::new(rx))) + } }