Compare commits
6 Commits
0bb6e596ac
...
win-servic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a07e0ed6 | ||
|
|
f245a6575d | ||
|
|
e3050bc5ff | ||
|
|
d593eedf01 | ||
|
|
2fb5bb3d84 | ||
|
|
86052c9350 |
62
mise.lock
62
mise.lock
@@ -8,10 +8,18 @@ backend = "aqua:ast-grep/ast-grep"
|
||||
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
||||
|
||||
[tools.ast-grep."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
||||
|
||||
[tools.ast-grep."platforms.linux-x64"]
|
||||
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
||||
|
||||
[tools.ast-grep."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
||||
|
||||
[tools.ast-grep."platforms.macos-arm64"]
|
||||
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620"
|
||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"
|
||||
@@ -32,10 +40,6 @@ backend = "cargo:cargo-audit"
|
||||
version = "0.13.9"
|
||||
backend = "cargo:cargo-edit"
|
||||
|
||||
[[tools."cargo:cargo-features"]]
|
||||
version = "1.0.0"
|
||||
backend = "cargo:cargo-features"
|
||||
|
||||
[[tools."cargo:cargo-features-manager"]]
|
||||
version = "0.11.1"
|
||||
backend = "cargo:cargo-features-manager"
|
||||
@@ -49,21 +53,13 @@ version = "0.9.126"
|
||||
backend = "cargo:cargo-nextest"
|
||||
|
||||
[[tools."cargo:cargo-shear"]]
|
||||
version = "1.9.1"
|
||||
version = "1.11.2"
|
||||
backend = "cargo:cargo-shear"
|
||||
|
||||
[[tools."cargo:cargo-vet"]]
|
||||
version = "0.10.2"
|
||||
backend = "cargo:cargo-vet"
|
||||
|
||||
[[tools."cargo:diesel-cli"]]
|
||||
version = "2.3.6"
|
||||
backend = "cargo:diesel-cli"
|
||||
|
||||
[tools."cargo:diesel-cli".options]
|
||||
default-features = "false"
|
||||
features = "sqlite,sqlite-bundled"
|
||||
|
||||
[[tools."cargo:diesel_cli"]]
|
||||
version = "2.3.6"
|
||||
backend = "cargo:diesel_cli"
|
||||
@@ -72,10 +68,6 @@ backend = "cargo:diesel_cli"
|
||||
default-features = "false"
|
||||
features = "sqlite,sqlite-bundled"
|
||||
|
||||
[[tools."cargo:rinf_cli"]]
|
||||
version = "8.9.1"
|
||||
backend = "cargo:rinf_cli"
|
||||
|
||||
[[tools.flutter]]
|
||||
version = "3.38.9-stable"
|
||||
backend = "asdf:flutter"
|
||||
@@ -88,10 +80,18 @@ backend = "aqua:protocolbuffers/protobuf/protoc"
|
||||
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||
|
||||
[tools.protoc."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||
|
||||
[tools.protoc."platforms.linux-x64"]
|
||||
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||
|
||||
[tools.protoc."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||
|
||||
[tools.protoc."platforms.macos-arm64"]
|
||||
checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed"
|
||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"
|
||||
@@ -109,24 +109,32 @@ version = "3.14.3"
|
||||
backend = "core:python"
|
||||
|
||||
[tools.python."platforms.linux-arm64"]
|
||||
checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
|
||||
[tools.python."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
|
||||
[tools.python."platforms.linux-x64"]
|
||||
checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
|
||||
[tools.python."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
|
||||
[tools.python."platforms.macos-arm64"]
|
||||
checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:c43aecde4a663aebff99b9b83da0efec506479f1c3f98331442f33d2c43501f9"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||
|
||||
[tools.python."platforms.macos-x64"]
|
||||
checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:9ab41dbc2f100a2a45d1833b9c11165f51051c558b5213eda9a9731d5948a0c0"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||
|
||||
[tools.python."platforms.windows-x64"]
|
||||
checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||
checksum = "sha256:bbe19034b35b0267176a7442575ae7dc6343480fd4d35598cb7700173d431e09"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||
|
||||
[[tools.rust]]
|
||||
version = "1.93.0"
|
||||
|
||||
144
server/Cargo.lock
generated
144
server/Cargo.lock
generated
@@ -669,6 +669,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
@@ -730,6 +780,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dashmap",
|
||||
"diesel",
|
||||
"diesel-async",
|
||||
@@ -737,7 +788,6 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"fatality",
|
||||
"futures",
|
||||
"hmac",
|
||||
"insta",
|
||||
"k256",
|
||||
"kameo",
|
||||
@@ -762,6 +812,7 @@ dependencies = [
|
||||
"tonic",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"windows-service",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -1434,6 +1485,46 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
@@ -1443,6 +1534,12 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.11"
|
||||
@@ -2052,7 +2149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2854,6 +2951,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
@@ -3187,7 +3290,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3321,6 +3424,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
@@ -4286,7 +4395,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4702,7 +4811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4896,7 +5005,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5470,6 +5579,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
@@ -5676,6 +5791,12 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -5748,6 +5869,17 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-service"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"widestring",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
|
||||
@@ -2,6 +2,10 @@ pub mod transport;
|
||||
pub mod url;
|
||||
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{LazyLock, RwLock},
|
||||
};
|
||||
|
||||
pub mod proto {
|
||||
tonic::include_proto!("arbiter");
|
||||
@@ -27,8 +31,27 @@ pub struct ClientMetadata {
|
||||
}
|
||||
|
||||
pub static BOOTSTRAP_PATH: &str = "bootstrap_token";
|
||||
pub const DEFAULT_SERVER_PORT: u16 = 50051;
|
||||
static HOME_OVERRIDE: LazyLock<RwLock<Option<PathBuf>>> = LazyLock::new(|| RwLock::new(None));
|
||||
|
||||
pub fn set_home_path_override(path: Option<PathBuf>) -> Result<(), std::io::Error> {
|
||||
let mut lock = HOME_OVERRIDE
|
||||
.write()
|
||||
.map_err(|_| std::io::Error::other("home path override lock poisoned"))?;
|
||||
*lock = path;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn home_path() -> Result<std::path::PathBuf, std::io::Error> {
|
||||
if let Some(path) = HOME_OVERRIDE
|
||||
.read()
|
||||
.map_err(|_| std::io::Error::other("home path override lock poisoned"))?
|
||||
.clone()
|
||||
{
|
||||
std::fs::create_dir_all(&path)?;
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
static ARBITER_HOME: &str = ".arbiter";
|
||||
let home_dir = std::env::home_dir().ok_or(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
|
||||
@@ -49,12 +49,15 @@ pem = "3.0.6"
|
||||
k256.workspace = true
|
||||
rsa.workspace = true
|
||||
sha2.workspace = true
|
||||
hmac = "0.12"
|
||||
spki.workspace = true
|
||||
alloy.workspace = true
|
||||
prost-types.workspace = true
|
||||
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||
clap = { version = "4.6", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.46.3"
|
||||
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-service = "0.8"
|
||||
|
||||
@@ -47,7 +47,6 @@ create table if not exists useragent_client (
|
||||
id integer not null primary key,
|
||||
nonce integer not null default(1), -- used for auth challenge
|
||||
public_key blob not null,
|
||||
pubkey_integrity_tag blob,
|
||||
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
|
||||
created_at integer not null default(unixepoch ('now')),
|
||||
updated_at integer not null default(unixepoch ('now'))
|
||||
|
||||
@@ -5,7 +5,6 @@ use chacha20poly1305::{
|
||||
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
|
||||
aead::{AeadMut, Error, Payload},
|
||||
};
|
||||
use hmac::Mac as _;
|
||||
use rand::{
|
||||
Rng as _, SeedableRng,
|
||||
rngs::{StdRng, SysRng},
|
||||
@@ -15,8 +14,6 @@ use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||
|
||||
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
|
||||
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
|
||||
pub const USERAGENT_INTEGRITY_DERIVE_TAG: &[u8] = "arbiter/useragent/integrity-key/v1".as_bytes();
|
||||
pub const USERAGENT_INTEGRITY_TAG: &[u8] = "arbiter/useragent/pubkey-entry/v1".as_bytes();
|
||||
|
||||
pub const NONCE_LENGTH: usize = 24;
|
||||
|
||||
@@ -172,50 +169,6 @@ pub fn derive_seal_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell
|
||||
key.into()
|
||||
}
|
||||
|
||||
/// Derives a dedicated key used for integrity tags within a specific domain.
|
||||
pub fn derive_integrity_key(seal_key: &mut KeyCell, derive_tag: &[u8]) -> KeyCell {
|
||||
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
||||
|
||||
let mut derived = SafeCell::new(Key::default());
|
||||
seal_key.0.read_inline(|seal_key_bytes| {
|
||||
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(seal_key_bytes.as_ref())
|
||||
.expect("HMAC key initialization must not fail for 32-byte key");
|
||||
mac.update(derive_tag);
|
||||
let output = mac.finalize().into_bytes();
|
||||
|
||||
let mut writer = derived.write();
|
||||
let writer: &mut [u8] = writer.as_mut();
|
||||
writer.copy_from_slice(&output);
|
||||
});
|
||||
|
||||
derived.into()
|
||||
}
|
||||
|
||||
/// Computes an integrity tag for a specific domain and payload shape.
|
||||
pub fn compute_integrity_tag<'a, I>(
|
||||
integrity_key: &mut KeyCell,
|
||||
purpose_tag: &[u8],
|
||||
data_parts: I,
|
||||
) -> [u8; 32]
|
||||
where
|
||||
I: IntoIterator<Item = &'a [u8]>,
|
||||
{
|
||||
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
||||
|
||||
let mut output_tag = [0u8; 32];
|
||||
integrity_key.0.read_inline(|integrity_key_bytes| {
|
||||
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(integrity_key_bytes.as_ref())
|
||||
.expect("HMAC key initialization must not fail for 32-byte key");
|
||||
mac.update(purpose_tag);
|
||||
for data_part in data_parts {
|
||||
mac.update(data_part);
|
||||
}
|
||||
output_tag.copy_from_slice(&mac.finalize().into_bytes());
|
||||
});
|
||||
|
||||
output_tag
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -287,43 +240,4 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn integrity_tag_deterministic() {
|
||||
let salt = generate_salt();
|
||||
let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt);
|
||||
let mut integrity_key = derive_integrity_key(&mut seal_key, USERAGENT_INTEGRITY_DERIVE_TAG);
|
||||
let key_type = 1i32.to_be_bytes();
|
||||
let t1 = compute_integrity_tag(
|
||||
&mut integrity_key,
|
||||
USERAGENT_INTEGRITY_TAG,
|
||||
[key_type.as_slice(), b"pubkey".as_ref()],
|
||||
);
|
||||
let t2 = compute_integrity_tag(
|
||||
&mut integrity_key,
|
||||
USERAGENT_INTEGRITY_TAG,
|
||||
[key_type.as_slice(), b"pubkey".as_ref()],
|
||||
);
|
||||
assert_eq!(t1, t2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn integrity_tag_changes_with_payload() {
|
||||
let salt = generate_salt();
|
||||
let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt);
|
||||
let mut integrity_key = derive_integrity_key(&mut seal_key, USERAGENT_INTEGRITY_DERIVE_TAG);
|
||||
let key_type_1 = 1i32.to_be_bytes();
|
||||
let key_type_2 = 2i32.to_be_bytes();
|
||||
let t1 = compute_integrity_tag(
|
||||
&mut integrity_key,
|
||||
USERAGENT_INTEGRITY_TAG,
|
||||
[key_type_1.as_slice(), b"pubkey".as_ref()],
|
||||
);
|
||||
let t2 = compute_integrity_tag(
|
||||
&mut integrity_key,
|
||||
USERAGENT_INTEGRITY_TAG,
|
||||
[key_type_2.as_slice(), b"pubkey".as_ref()],
|
||||
);
|
||||
assert_ne!(t1, t2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ enum State {
|
||||
Unsealed {
|
||||
root_key_history_id: i32,
|
||||
root_key: KeyCell,
|
||||
integrity_key: KeyCell,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -146,8 +145,6 @@ impl KeyHolder {
|
||||
}
|
||||
let salt = v1::generate_salt();
|
||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
||||
let integrity_key =
|
||||
v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG);
|
||||
let mut root_key = KeyCell::new_secure_random();
|
||||
|
||||
// Zero nonces are fine because they are one-time
|
||||
@@ -196,7 +193,6 @@ impl KeyHolder {
|
||||
self.state = State::Unsealed {
|
||||
root_key,
|
||||
root_key_history_id,
|
||||
integrity_key,
|
||||
};
|
||||
|
||||
info!("Keyholder bootstrapped successfully");
|
||||
@@ -230,8 +226,6 @@ impl KeyHolder {
|
||||
Error::BrokenDatabase
|
||||
})?;
|
||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
||||
let integrity_key =
|
||||
v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG);
|
||||
|
||||
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
||||
|
||||
@@ -255,7 +249,6 @@ impl KeyHolder {
|
||||
error!(?err, "Broken database: invalid encryption key size");
|
||||
Error::BrokenDatabase
|
||||
})?,
|
||||
integrity_key,
|
||||
};
|
||||
|
||||
info!("Keyholder unsealed successfully");
|
||||
@@ -263,25 +256,7 @@ impl KeyHolder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Signs a generic integrity payload using the vault-derived integrity key
|
||||
#[message]
|
||||
pub fn sign_integrity_tag(
|
||||
&mut self,
|
||||
purpose_tag: Vec<u8>,
|
||||
data_parts: Vec<Vec<u8>>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let State::Unsealed { integrity_key, .. } = &mut self.state else {
|
||||
return Err(Error::NotBootstrapped);
|
||||
};
|
||||
|
||||
let tag = v1::compute_integrity_tag(
|
||||
integrity_key,
|
||||
&purpose_tag,
|
||||
data_parts.iter().map(Vec::as_slice),
|
||||
);
|
||||
Ok(tag.to_vec())
|
||||
}
|
||||
|
||||
// Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext
|
||||
#[message]
|
||||
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||
@@ -317,7 +292,6 @@ impl KeyHolder {
|
||||
let State::Unsealed {
|
||||
root_key,
|
||||
root_key_history_id,
|
||||
..
|
||||
} = &mut self.state
|
||||
else {
|
||||
return Err(Error::NotBootstrapped);
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
use arbiter_proto::transport::Bi;
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::error::SendError;
|
||||
use tracing::error;
|
||||
|
||||
use super::Error;
|
||||
use crate::{
|
||||
actors::{
|
||||
bootstrap::ConsumeToken,
|
||||
keyholder::{self, SignIntegrityTag},
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
||||
},
|
||||
db::schema,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AttestationStatus {
|
||||
Attested,
|
||||
NotAttested,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
pub struct ChallengeRequest {
|
||||
pub pubkey: AuthPublicKey,
|
||||
}
|
||||
@@ -49,11 +40,7 @@ smlang::statemachine!(
|
||||
}
|
||||
);
|
||||
|
||||
async fn create_nonce(
|
||||
db: &crate::db::DatabasePool,
|
||||
pubkey_bytes: &[u8],
|
||||
key_type: crate::db::models::KeyType,
|
||||
) -> Result<i32, Error> {
|
||||
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
@@ -63,14 +50,12 @@ async fn create_nonce(
|
||||
Box::pin(async move {
|
||||
let current_nonce = schema::useragent_client::table
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
||||
.filter(schema::useragent_client::key_type.eq(key_type))
|
||||
.select(schema::useragent_client::nonce)
|
||||
.first::<i32>(conn)
|
||||
.await?;
|
||||
|
||||
update(schema::useragent_client::table)
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
||||
.filter(schema::useragent_client::key_type.eq(key_type))
|
||||
.set(schema::useragent_client::nonce.eq(current_nonce + 1))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
@@ -90,11 +75,7 @@ async fn create_nonce(
|
||||
})
|
||||
}
|
||||
|
||||
async fn register_key(
|
||||
db: &crate::db::DatabasePool,
|
||||
pubkey: &AuthPublicKey,
|
||||
integrity_tag: Option<Vec<u8>>,
|
||||
) -> Result<(), Error> {
|
||||
async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> {
|
||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||
let key_type = pubkey.key_type();
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
@@ -107,7 +88,6 @@ async fn register_key(
|
||||
schema::useragent_client::public_key.eq(pubkey_bytes),
|
||||
schema::useragent_client::nonce.eq(1),
|
||||
schema::useragent_client::key_type.eq(key_type),
|
||||
schema::useragent_client::pubkey_integrity_tag.eq(integrity_tag),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
@@ -140,15 +120,8 @@ where
|
||||
&mut self,
|
||||
ChallengeRequest { pubkey }: ChallengeRequest,
|
||||
) -> Result<ChallengeContext, Self::Error> {
|
||||
match self.verify_pubkey_attestation_status(&pubkey).await? {
|
||||
AttestationStatus::Attested | AttestationStatus::Unavailable => {}
|
||||
AttestationStatus::NotAttested => {
|
||||
return Err(Error::InvalidChallengeSolution);
|
||||
}
|
||||
}
|
||||
|
||||
let stored_bytes = pubkey.to_stored_bytes();
|
||||
let nonce = create_nonce(&self.conn.db, &stored_bytes, pubkey.key_type()).await?;
|
||||
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
|
||||
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthChallenge { nonce }))
|
||||
@@ -188,15 +161,7 @@ where
|
||||
return Err(Error::InvalidBootstrapToken);
|
||||
}
|
||||
|
||||
let integrity_tag = self
|
||||
.try_sign_pubkey_integrity_tag(&pubkey)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(?err, "Failed to sign user-agent pubkey integrity tag");
|
||||
Error::internal("Failed to sign user-agent pubkey integrity tag")
|
||||
})?;
|
||||
|
||||
register_key(&self.conn.db, &pubkey, integrity_tag).await?;
|
||||
register_key(&self.conn.db, &pubkey).await?;
|
||||
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
@@ -255,94 +220,3 @@ where
|
||||
Ok(key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AuthContext<'_, T>
|
||||
where
|
||||
T: Bi<super::Inbound, Result<super::Outbound, Error>> + Send,
|
||||
{
|
||||
async fn try_sign_pubkey_integrity_tag(
|
||||
&self,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<Option<Vec<u8>>, Error> {
|
||||
let signed = self
|
||||
.conn
|
||||
.actors
|
||||
.key_holder
|
||||
.ask(SignIntegrityTag {
|
||||
purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(),
|
||||
data_parts: vec![
|
||||
(pubkey.key_type() as i32).to_be_bytes().to_vec(),
|
||||
pubkey.to_stored_bytes(),
|
||||
],
|
||||
})
|
||||
.await;
|
||||
|
||||
match signed {
|
||||
Ok(tag) => Ok(Some(tag)),
|
||||
Err(SendError::HandlerError(keyholder::Error::NotBootstrapped)) => Ok(None),
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(
|
||||
?err,
|
||||
"Keyholder failed to sign user-agent pubkey integrity tag"
|
||||
);
|
||||
Err(Error::internal(
|
||||
"Keyholder failed to sign user-agent pubkey integrity tag",
|
||||
))
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
?err,
|
||||
"Failed to contact keyholder for user-agent pubkey integrity tag"
|
||||
);
|
||||
Err(Error::internal(
|
||||
"Failed to contact keyholder for user-agent pubkey integrity tag",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_pubkey_attestation_status(
|
||||
&self,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<AttestationStatus, Error> {
|
||||
let stored_tag: Option<Option<Vec<u8>>> = {
|
||||
let mut conn = self.conn.db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
|
||||
schema::useragent_client::table
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey.to_stored_bytes()))
|
||||
.filter(schema::useragent_client::key_type.eq(pubkey.key_type()))
|
||||
.select(schema::useragent_client::pubkey_integrity_tag)
|
||||
.first::<Option<Vec<u8>>>(&mut conn)
|
||||
.await
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?
|
||||
};
|
||||
|
||||
let Some(stored_tag) = stored_tag else {
|
||||
return Err(Error::UnregisteredPublicKey);
|
||||
};
|
||||
|
||||
let Some(expected_tag) = self.try_sign_pubkey_integrity_tag(pubkey).await? else {
|
||||
// Vault sealed/unbootstrapped: cannot verify integrity yet.
|
||||
return Ok(AttestationStatus::Unavailable);
|
||||
};
|
||||
|
||||
match stored_tag {
|
||||
Some(stored_tag) if stored_tag == expected_tag => Ok(AttestationStatus::Attested),
|
||||
Some(_) => {
|
||||
error!("User-agent pubkey integrity tag mismatch");
|
||||
Ok(AttestationStatus::NotAttested)
|
||||
}
|
||||
None => {
|
||||
error!("Missing pubkey integrity tag for registered key while vault is unsealed");
|
||||
Ok(AttestationStatus::NotAttested)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ use std::sync::Mutex;
|
||||
|
||||
use alloy::primitives::Address;
|
||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper, dsl::update};
|
||||
use diesel::sql_types::ops::Add;
|
||||
use diesel::{BoolExpressionMethods as _, ExpressionMethods as _, QueryDsl as _, SelectableHelper};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use kameo::error::SendError;
|
||||
use kameo::messages;
|
||||
use kameo::prelude::Context;
|
||||
use kameo::{message, messages};
|
||||
use tracing::{error, info};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
@@ -14,8 +15,9 @@ use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnsw
|
||||
use crate::actors::keyholder::KeyHolderState;
|
||||
use crate::actors::user_agent::session::Error;
|
||||
use crate::db::models::{
|
||||
EvmWalletAccess, KeyType, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
||||
CoreEvmWalletAccess, EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
||||
};
|
||||
use crate::db::schema::evm_wallet_access;
|
||||
use crate::evm::policies::{Grant, SpecificGrant};
|
||||
use crate::safe_cell::SafeCell;
|
||||
use crate::{
|
||||
@@ -23,7 +25,7 @@ use crate::{
|
||||
evm::{
|
||||
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
||||
},
|
||||
keyholder::{self, Bootstrap, SignIntegrityTag, TryUnseal},
|
||||
keyholder::{self, Bootstrap, TryUnseal},
|
||||
user_agent::session::{
|
||||
UserAgentSession,
|
||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||
@@ -85,56 +87,6 @@ impl UserAgentSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn backfill_missing_useragent_pubkey_integrity_tags(&mut self) -> Result<(), Error> {
|
||||
use crate::db::schema::useragent_client;
|
||||
|
||||
let mut conn = self.props.db.get().await?;
|
||||
let missing_rows: Vec<(i32, Vec<u8>, KeyType)> = useragent_client::table
|
||||
.filter(useragent_client::pubkey_integrity_tag.is_null())
|
||||
.select((
|
||||
useragent_client::id,
|
||||
useragent_client::public_key,
|
||||
useragent_client::key_type,
|
||||
))
|
||||
.load(&mut conn)
|
||||
.await?;
|
||||
drop(conn);
|
||||
|
||||
if missing_rows.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut updates = Vec::with_capacity(missing_rows.len());
|
||||
for (id, public_key, key_type) in missing_rows {
|
||||
let tag = self
|
||||
.props
|
||||
.actors
|
||||
.key_holder
|
||||
.ask(SignIntegrityTag {
|
||||
purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(),
|
||||
data_parts: vec![(key_type as i32).to_be_bytes().to_vec(), public_key],
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(?err, "Failed to sign integrity tag");
|
||||
Error::internal("Failed to sign integrity tag")
|
||||
})?;
|
||||
updates.push((id, tag));
|
||||
}
|
||||
|
||||
let mut conn = self.props.db.get().await?;
|
||||
for (id, tag) in updates {
|
||||
update(useragent_client::table)
|
||||
.filter(useragent_client::id.eq(id))
|
||||
.set(useragent_client::pubkey_integrity_tag.eq(Some(tag)))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
info!("Backfilled missing user-agent pubkey integrity tags");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnsealStartResponse {
|
||||
@@ -222,8 +174,6 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.backfill_missing_useragent_pubkey_integrity_tags()
|
||||
.await?;
|
||||
info!("Successfully unsealed key with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(())
|
||||
@@ -285,8 +235,6 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.backfill_missing_useragent_pubkey_integrity_tags()
|
||||
.await?;
|
||||
info!("Successfully bootstrapped vault with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(())
|
||||
|
||||
72
server/crates/arbiter-server/src/cli.rs
Normal file
72
server/crates/arbiter-server/src/cli.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::{
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
const DEFAULT_LISTEN_ADDR: SocketAddr = SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::LOCALHOST,
|
||||
arbiter_proto::DEFAULT_SERVER_PORT,
|
||||
));
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "arbiter-server")]
|
||||
#[command(about = "Arbiter gRPC server")]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Run server in foreground mode.
|
||||
Run(RunArgs),
|
||||
/// Manage service lifecycle.
|
||||
Service {
|
||||
#[command(subcommand)]
|
||||
command: ServiceCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct RunArgs {
|
||||
#[arg(long, default_value_t = DEFAULT_LISTEN_ADDR)]
|
||||
pub listen_addr: SocketAddr,
|
||||
#[arg(long)]
|
||||
pub data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for RunArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
listen_addr: DEFAULT_LISTEN_ADDR,
|
||||
data_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ServiceCommand {
|
||||
/// Install Windows service in Service Control Manager.
|
||||
Install(ServiceInstallArgs),
|
||||
/// Internal service entrypoint. SCM only.
|
||||
#[command(hide = true)]
|
||||
Run(ServiceRunArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct ServiceInstallArgs {
|
||||
#[arg(long)]
|
||||
pub start: bool,
|
||||
#[arg(long)]
|
||||
pub data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct ServiceRunArgs {
|
||||
#[arg(long, default_value_t = DEFAULT_LISTEN_ADDR)]
|
||||
pub listen_addr: SocketAddr,
|
||||
#[arg(long)]
|
||||
pub data_dir: Option<PathBuf>,
|
||||
}
|
||||
@@ -242,7 +242,6 @@ pub struct UseragentClient {
|
||||
pub id: i32,
|
||||
pub nonce: i32,
|
||||
pub public_key: Vec<u8>,
|
||||
pub pubkey_integrity_tag: Option<Vec<u8>>,
|
||||
pub created_at: SqliteTimestamp,
|
||||
pub updated_at: SqliteTimestamp,
|
||||
pub key_type: KeyType,
|
||||
|
||||
@@ -178,7 +178,6 @@ diesel::table! {
|
||||
id -> Integer,
|
||||
nonce -> Integer,
|
||||
public_key -> Binary,
|
||||
pubkey_integrity_tag -> Nullable<Binary>,
|
||||
key_type -> Integer,
|
||||
created_at -> Integer,
|
||||
updated_at -> Integer,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::context::ServerContext;
|
||||
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
||||
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
|
||||
use miette::miette;
|
||||
use tonic::transport::{Identity, ServerTlsConfig};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{actors::bootstrap::GetToken, context::ServerContext};
|
||||
|
||||
pub mod actors;
|
||||
pub mod context;
|
||||
@@ -18,3 +26,64 @@ impl Server {
|
||||
Self { context }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RunConfig {
|
||||
pub addr: SocketAddr,
|
||||
pub data_dir: Option<PathBuf>,
|
||||
pub log_arbiter_url: bool,
|
||||
}
|
||||
|
||||
impl RunConfig {
|
||||
pub fn new(addr: SocketAddr, data_dir: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
addr,
|
||||
data_dir,
|
||||
log_arbiter_url: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_server_until_shutdown<F>(config: RunConfig, shutdown: F) -> miette::Result<()>
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
arbiter_proto::set_home_path_override(config.data_dir.clone())
|
||||
.map_err(|err| miette!("failed to set home path override: {err}"))?;
|
||||
|
||||
let db = db::create_pool(None).await?;
|
||||
info!(addr = %config.addr, "Database ready");
|
||||
|
||||
let context = ServerContext::new(db).await?;
|
||||
info!(addr = %config.addr, "Server context ready");
|
||||
|
||||
if config.log_arbiter_url {
|
||||
let url = ArbiterUrl {
|
||||
host: config.addr.ip().to_string(),
|
||||
port: config.addr.port(),
|
||||
ca_cert: context.tls.ca_cert().clone().into_owned(),
|
||||
bootstrap_token: context
|
||||
.actors
|
||||
.bootstrapper
|
||||
.ask(GetToken)
|
||||
.await
|
||||
.map_err(|err| miette!("failed to get bootstrap token from actor: {err}"))?,
|
||||
};
|
||||
info!(%url, "Server URL");
|
||||
}
|
||||
|
||||
let tls = ServerTlsConfig::new().identity(Identity::from_pem(
|
||||
context.tls.cert_pem(),
|
||||
context.tls.key_pem(),
|
||||
));
|
||||
|
||||
tonic::transport::Server::builder()
|
||||
.tls_config(tls)
|
||||
.map_err(|err| miette!("Failed to setup TLS: {err}"))?
|
||||
.add_service(ArbiterServiceServer::new(Server::new(context)))
|
||||
.serve_with_shutdown(config.addr, shutdown)
|
||||
.await
|
||||
.map_err(|e| miette!("gRPC server error: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,56 +1,42 @@
|
||||
use std::net::SocketAddr;
|
||||
mod cli;
|
||||
mod service;
|
||||
|
||||
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
|
||||
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
|
||||
use miette::miette;
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Command, RunArgs, ServiceCommand};
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
use tonic::transport::{Identity, ServerTlsConfig};
|
||||
use tracing::info;
|
||||
|
||||
const PORT: u16 = 50051;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> miette::Result<()> {
|
||||
aws_lc_rs::default_provider().install_default().unwrap();
|
||||
init_logging();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
None => run_foreground(RunArgs::default()).await,
|
||||
Some(Command::Run(args)) => run_foreground(args).await,
|
||||
Some(Command::Service { command }) => match command {
|
||||
ServiceCommand::Install(args) => service::install_service(args),
|
||||
ServiceCommand::Run(args) => service::run_service_dispatcher(args),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_foreground(args: RunArgs) -> miette::Result<()> {
|
||||
info!(addr = %args.listen_addr, "Starting arbiter server");
|
||||
arbiter_server::run_server_until_shutdown(
|
||||
arbiter_server::RunConfig::new(args.listen_addr, args.data_dir),
|
||||
std::future::pending::<()>(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn init_logging() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
info!("Starting arbiter server");
|
||||
|
||||
let db = db::create_pool(None).await?;
|
||||
info!("Database ready");
|
||||
|
||||
let context = ServerContext::new(db).await?;
|
||||
|
||||
let addr: SocketAddr = format!("127.0.0.1:{PORT}").parse().expect("valid address");
|
||||
info!(%addr, "Starting gRPC server");
|
||||
|
||||
let url = ArbiterUrl {
|
||||
host: addr.ip().to_string(),
|
||||
port: addr.port(),
|
||||
ca_cert: context.tls.ca_cert().clone().into_owned(),
|
||||
bootstrap_token: context.actors.bootstrapper.ask(GetToken).await.unwrap(),
|
||||
};
|
||||
|
||||
info!(%url, "Server URL");
|
||||
|
||||
let tls = ServerTlsConfig::new().identity(Identity::from_pem(
|
||||
context.tls.cert_pem(),
|
||||
context.tls.key_pem(),
|
||||
));
|
||||
|
||||
tonic::transport::Server::builder()
|
||||
.tls_config(tls)
|
||||
.map_err(|err| miette!("Faild to setup TLS: {err}"))?
|
||||
.add_service(ArbiterServiceServer::new(Server::new(context)))
|
||||
.serve(addr)
|
||||
.await
|
||||
.map_err(|e| miette::miette!("gRPC server error: {e}"))?;
|
||||
|
||||
unreachable!("gRPC server should run indefinitely");
|
||||
.try_init();
|
||||
}
|
||||
|
||||
19
server/crates/arbiter-server/src/service/mod.rs
Normal file
19
server/crates/arbiter-server/src/service/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub use windows::{install_service, run_service_dispatcher};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn install_service(_: crate::cli::ServiceInstallArgs) -> miette::Result<()> {
|
||||
Err(miette::miette!(
|
||||
"service install is currently supported only on Windows"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn run_service_dispatcher(_: crate::cli::ServiceRunArgs) -> miette::Result<()> {
|
||||
Err(miette::miette!(
|
||||
"service run entrypoint is currently supported only on Windows"
|
||||
))
|
||||
}
|
||||
230
server/crates/arbiter-server/src/service/windows.rs
Normal file
230
server/crates/arbiter-server/src/service/windows.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use miette::{Context as _, IntoDiagnostic as _, miette};
|
||||
use windows_service::{
|
||||
define_windows_service,
|
||||
service::{
|
||||
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode,
|
||||
ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType,
|
||||
},
|
||||
service_control_handler::{self, ServiceControlHandlerResult},
|
||||
service_dispatcher,
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
use crate::cli::{ServiceInstallArgs, ServiceRunArgs};
|
||||
use arbiter_server::{RunConfig, run_server_until_shutdown};
|
||||
|
||||
const SERVICE_NAME: &str = "ArbiterServer";
|
||||
const SERVICE_DISPLAY_NAME: &str = "Arbiter Server";
|
||||
|
||||
pub fn default_service_data_dir() -> PathBuf {
|
||||
let base = std::env::var_os("PROGRAMDATA")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"));
|
||||
base.join("Arbiter")
|
||||
}
|
||||
|
||||
pub fn install_service(args: ServiceInstallArgs) -> miette::Result<()> {
|
||||
ensure_admin_rights()?;
|
||||
|
||||
let executable = std::env::current_exe().into_diagnostic()?;
|
||||
let data_dir = args.data_dir.unwrap_or_else(default_service_data_dir);
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("failed to create service data dir: {}", data_dir.display()))?;
|
||||
ensure_token_acl_contract(&data_dir)?;
|
||||
|
||||
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
|
||||
let manager = ServiceManager::local_computer(None::<&str>, manager_access)
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to open Service Control Manager")?;
|
||||
|
||||
let launch_arguments = vec![
|
||||
OsString::from("service"),
|
||||
OsString::from("run"),
|
||||
OsString::from("--data-dir"),
|
||||
data_dir.as_os_str().to_os_string(),
|
||||
];
|
||||
|
||||
let service_info = ServiceInfo {
|
||||
name: OsString::from(SERVICE_NAME),
|
||||
display_name: OsString::from(SERVICE_DISPLAY_NAME),
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
start_type: ServiceStartType::AutoStart,
|
||||
error_control: ServiceErrorControl::Normal,
|
||||
executable_path: executable,
|
||||
launch_arguments,
|
||||
dependencies: vec![],
|
||||
account_name: Some(OsString::from(r"NT AUTHORITY\LocalService")),
|
||||
account_password: None,
|
||||
};
|
||||
|
||||
let service = manager
|
||||
.create_service(
|
||||
&service_info,
|
||||
ServiceAccess::QUERY_STATUS | ServiceAccess::START,
|
||||
)
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to create Windows service in SCM")?;
|
||||
|
||||
if args.start {
|
||||
service
|
||||
.start::<&str>(&[])
|
||||
.into_diagnostic()
|
||||
.wrap_err("service created but failed to start")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_service_dispatcher(args: ServiceRunArgs) -> miette::Result<()> {
|
||||
SERVICE_RUN_ARGS
|
||||
.set(args)
|
||||
.map_err(|_| miette!("service runtime args are already initialized"))?;
|
||||
|
||||
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to start service dispatcher")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
define_windows_service!(ffi_service_main, service_main);
|
||||
|
||||
static SERVICE_RUN_ARGS: std::sync::OnceLock<ServiceRunArgs> = std::sync::OnceLock::new();
|
||||
|
||||
fn service_main(_arguments: Vec<OsString>) {
|
||||
if let Err(error) = run_service_main() {
|
||||
tracing::error!(error = ?error, "Windows service main failed");
|
||||
}
|
||||
}
|
||||
|
||||
fn run_service_main() -> miette::Result<()> {
|
||||
let args = SERVICE_RUN_ARGS
|
||||
.get()
|
||||
.cloned()
|
||||
.ok_or_else(|| miette!("service run args are missing"))?;
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
|
||||
|
||||
let status_handle =
|
||||
service_control_handler::register(SERVICE_NAME, move |control| match control {
|
||||
ServiceControl::Stop => {
|
||||
let _ = shutdown_tx.send(());
|
||||
ServiceControlHandlerResult::NoError
|
||||
}
|
||||
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||
_ => ServiceControlHandlerResult::NotImplemented,
|
||||
})
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to register service control handler")?;
|
||||
|
||||
set_status(
|
||||
&status_handle,
|
||||
ServiceState::StartPending,
|
||||
ServiceControlAccept::empty(),
|
||||
)?;
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to build tokio runtime for service")?;
|
||||
|
||||
set_status(
|
||||
&status_handle,
|
||||
ServiceState::Running,
|
||||
ServiceControlAccept::STOP,
|
||||
)?;
|
||||
|
||||
let data_dir = args.data_dir.unwrap_or_else(default_service_data_dir);
|
||||
let config = RunConfig {
|
||||
addr: args.listen_addr,
|
||||
data_dir: Some(data_dir),
|
||||
log_arbiter_url: true,
|
||||
};
|
||||
|
||||
let result = runtime.block_on(run_server_until_shutdown(config, async move {
|
||||
let _ = tokio::task::spawn_blocking(move || shutdown_rx.recv()).await;
|
||||
}));
|
||||
|
||||
set_status(
|
||||
&status_handle,
|
||||
ServiceState::Stopped,
|
||||
ServiceControlAccept::empty(),
|
||||
)?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn set_status(
|
||||
status_handle: &service_control_handler::ServiceStatusHandle,
|
||||
current_state: ServiceState,
|
||||
controls_accepted: ServiceControlAccept,
|
||||
) -> miette::Result<()> {
|
||||
status_handle
|
||||
.set_service_status(ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state,
|
||||
controls_accepted,
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::from_secs(10),
|
||||
process_id: None,
|
||||
})
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to update service state")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_admin_rights() -> miette::Result<()> {
|
||||
let status = Command::new("net")
|
||||
.arg("session")
|
||||
.status()
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to check administrator rights")?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(miette!(
|
||||
"administrator privileges are required to install Windows service"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_token_acl_contract(data_dir: &Path) -> miette::Result<()> {
|
||||
// IMPORTANT: Keep this ACL setup explicit.
|
||||
// The service account needs write access, while the interactive user only needs read access
|
||||
// to the bootstrap token and service data directory.
|
||||
let target = data_dir.as_os_str();
|
||||
|
||||
let status = Command::new("icacls")
|
||||
.arg(target)
|
||||
.arg("/grant")
|
||||
.arg("*S-1-5-19:(OI)(CI)M")
|
||||
.arg("/grant")
|
||||
.arg("*S-1-5-32-545:(OI)(CI)RX")
|
||||
.arg("/T")
|
||||
.arg("/C")
|
||||
.status()
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to apply ACLs for service data directory")?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(miette!(
|
||||
"failed to ensure ACL contract for service data directory"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,9 @@ use arbiter_server::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
bootstrap::GetToken,
|
||||
keyholder::Bootstrap,
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
||||
},
|
||||
db::{self, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
@@ -167,120 +165,3 @@ pub async fn test_challenge_auth() {
|
||||
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
|
||||
let db = db::create_test_pool().await;
|
||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||
|
||||
actors
|
||||
.key_holder
|
||||
.ask(Bootstrap {
|
||||
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(schema::useragent_client::table)
|
||||
.values((
|
||||
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
||||
schema::useragent_client::key_type.eq(1i32),
|
||||
schema::useragent_client::pubkey_integrity_tag.eq(Some(vec![0u8; 32])),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let db_for_task = db.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||
auth::authenticate(&mut props, server_transport).await
|
||||
});
|
||||
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeRequest {
|
||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||
bootstrap_token: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
task.await.unwrap(),
|
||||
Err(auth::Error::InvalidChallengeSolution)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_challenge_auth_rejects_invalid_signature() {
|
||||
let db = db::create_test_pool().await;
|
||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||
|
||||
// Pre-register key with key_type
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(schema::useragent_client::table)
|
||||
.values((
|
||||
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
||||
schema::useragent_client::key_type.eq(1i32),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let db_for_task = db.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||
auth::authenticate(&mut props, server_transport).await
|
||||
});
|
||||
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeRequest {
|
||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||
bootstrap_token: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response = test_transport
|
||||
.recv()
|
||||
.await
|
||||
.expect("should receive challenge");
|
||||
let challenge = match response {
|
||||
Ok(resp) => match resp {
|
||||
auth::Outbound::AuthChallenge { nonce } => nonce,
|
||||
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||
},
|
||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||
};
|
||||
|
||||
// Sign a different challenge value so signature format is valid but verification must fail.
|
||||
let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes);
|
||||
let signature = new_key.sign(&wrong_challenge);
|
||||
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeSolution {
|
||||
signature: signature.to_bytes().to_vec(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
task.await.unwrap(),
|
||||
Err(auth::Error::InvalidChallengeSolution)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -2,17 +2,14 @@ use arbiter_server::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
keyholder::{Bootstrap, Seal},
|
||||
user_agent::{
|
||||
UserAgentSession,
|
||||
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
|
||||
},
|
||||
user_agent::{UserAgentSession, session::connection::{
|
||||
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||
}},
|
||||
},
|
||||
db,
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::actor::Spawn as _;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
@@ -152,42 +149,3 @@ pub async fn test_unseal_retry_after_invalid_key() {
|
||||
assert!(matches!(response, Ok(())));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_unseal_backfills_missing_pubkey_integrity_tags() {
|
||||
let seal_key = b"test-seal-key";
|
||||
let (db, user_agent) = setup_sealed_user_agent(seal_key).await;
|
||||
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(arbiter_server::db::schema::useragent_client::table)
|
||||
.values((
|
||||
arbiter_server::db::schema::useragent_client::public_key
|
||||
.eq(vec![1u8, 2u8, 3u8, 4u8]),
|
||||
arbiter_server::db::schema::useragent_client::key_type.eq(1i32),
|
||||
arbiter_server::db::schema::useragent_client::pubkey_integrity_tag
|
||||
.eq(Option::<Vec<u8>>::None),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
|
||||
let response = user_agent.ask(encrypted_key).await;
|
||||
assert!(matches!(response, Ok(())));
|
||||
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
let tags: Vec<Option<Vec<u8>>> = arbiter_server::db::schema::useragent_client::table
|
||||
.select(arbiter_server::db::schema::useragent_client::pubkey_integrity_tag)
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
tags.iter()
|
||||
.all(|tag| matches!(tag, Some(v) if v.len() == 32))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user