Compare commits
6 Commits
352ee3ee63
...
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"
|
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
||||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
[tools.ast-grep."platforms.linux-x64"]
|
[tools.ast-grep."platforms.linux-x64"]
|
||||||
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
||||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
[tools.ast-grep."platforms.macos-arm64"]
|
[tools.ast-grep."platforms.macos-arm64"]
|
||||||
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620"
|
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620"
|
||||||
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"
|
||||||
@@ -32,10 +40,6 @@ backend = "cargo:cargo-audit"
|
|||||||
version = "0.13.9"
|
version = "0.13.9"
|
||||||
backend = "cargo:cargo-edit"
|
backend = "cargo:cargo-edit"
|
||||||
|
|
||||||
[[tools."cargo:cargo-features"]]
|
|
||||||
version = "1.0.0"
|
|
||||||
backend = "cargo:cargo-features"
|
|
||||||
|
|
||||||
[[tools."cargo:cargo-features-manager"]]
|
[[tools."cargo:cargo-features-manager"]]
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
backend = "cargo:cargo-features-manager"
|
backend = "cargo:cargo-features-manager"
|
||||||
@@ -49,21 +53,13 @@ version = "0.9.126"
|
|||||||
backend = "cargo:cargo-nextest"
|
backend = "cargo:cargo-nextest"
|
||||||
|
|
||||||
[[tools."cargo:cargo-shear"]]
|
[[tools."cargo:cargo-shear"]]
|
||||||
version = "1.9.1"
|
version = "1.11.2"
|
||||||
backend = "cargo:cargo-shear"
|
backend = "cargo:cargo-shear"
|
||||||
|
|
||||||
[[tools."cargo:cargo-vet"]]
|
[[tools."cargo:cargo-vet"]]
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
backend = "cargo:cargo-vet"
|
backend = "cargo:cargo-vet"
|
||||||
|
|
||||||
[[tools."cargo:diesel-cli"]]
|
|
||||||
version = "2.3.6"
|
|
||||||
backend = "cargo:diesel-cli"
|
|
||||||
|
|
||||||
[tools."cargo:diesel-cli".options]
|
|
||||||
default-features = "false"
|
|
||||||
features = "sqlite,sqlite-bundled"
|
|
||||||
|
|
||||||
[[tools."cargo:diesel_cli"]]
|
[[tools."cargo:diesel_cli"]]
|
||||||
version = "2.3.6"
|
version = "2.3.6"
|
||||||
backend = "cargo:diesel_cli"
|
backend = "cargo:diesel_cli"
|
||||||
@@ -72,10 +68,6 @@ backend = "cargo:diesel_cli"
|
|||||||
default-features = "false"
|
default-features = "false"
|
||||||
features = "sqlite,sqlite-bundled"
|
features = "sqlite,sqlite-bundled"
|
||||||
|
|
||||||
[[tools."cargo:rinf_cli"]]
|
|
||||||
version = "8.9.1"
|
|
||||||
backend = "cargo:rinf_cli"
|
|
||||||
|
|
||||||
[[tools.flutter]]
|
[[tools.flutter]]
|
||||||
version = "3.38.9-stable"
|
version = "3.38.9-stable"
|
||||||
backend = "asdf:flutter"
|
backend = "asdf:flutter"
|
||||||
@@ -88,10 +80,18 @@ backend = "aqua:protocolbuffers/protobuf/protoc"
|
|||||||
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||||
|
|
||||||
[tools.protoc."platforms.linux-x64"]
|
[tools.protoc."platforms.linux-x64"]
|
||||||
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||||
|
|
||||||
[tools.protoc."platforms.macos-arm64"]
|
[tools.protoc."platforms.macos-arm64"]
|
||||||
checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed"
|
checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed"
|
||||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"
|
||||||
@@ -109,24 +109,32 @@ version = "3.14.3"
|
|||||||
backend = "core:python"
|
backend = "core:python"
|
||||||
|
|
||||||
[tools.python."platforms.linux-arm64"]
|
[tools.python."platforms.linux-arm64"]
|
||||||
checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625"
|
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
|
[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"]
|
[tools.python."platforms.linux-x64"]
|
||||||
checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0"
|
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
|
[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"]
|
[tools.python."platforms.macos-arm64"]
|
||||||
checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb"
|
checksum = "sha256:c43aecde4a663aebff99b9b83da0efec506479f1c3f98331442f33d2c43501f9"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
[tools.python."platforms.macos-x64"]
|
[tools.python."platforms.macos-x64"]
|
||||||
checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7"
|
checksum = "sha256:9ab41dbc2f100a2a45d1833b9c11165f51051c558b5213eda9a9731d5948a0c0"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
[tools.python."platforms.windows-x64"]
|
[tools.python."platforms.windows-x64"]
|
||||||
checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0"
|
checksum = "sha256:bbe19034b35b0267176a7442575ae7dc6343480fd4d35598cb7700173d431e09"
|
||||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
[[tools.rust]]
|
[[tools.rust]]
|
||||||
version = "1.93.0"
|
version = "1.93.0"
|
||||||
|
|||||||
144
server/Cargo.lock
generated
144
server/Cargo.lock
generated
@@ -669,6 +669,56 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.102"
|
version = "1.0.102"
|
||||||
@@ -730,6 +780,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel-async",
|
"diesel-async",
|
||||||
@@ -737,7 +788,6 @@ dependencies = [
|
|||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"fatality",
|
"fatality",
|
||||||
"futures",
|
"futures",
|
||||||
"hmac",
|
|
||||||
"insta",
|
"insta",
|
||||||
"k256",
|
"k256",
|
||||||
"kameo",
|
"kameo",
|
||||||
@@ -762,6 +812,7 @@ dependencies = [
|
|||||||
"tonic",
|
"tonic",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"windows-service",
|
||||||
"x25519-dalek",
|
"x25519-dalek",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -1434,6 +1485,46 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.57"
|
version = "0.1.57"
|
||||||
@@ -1443,6 +1534,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.11"
|
version = "0.15.11"
|
||||||
@@ -2052,7 +2149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2854,6 +2951,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
@@ -3187,7 +3290,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3321,6 +3424,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -4286,7 +4395,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4702,7 +4811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4896,7 +5005,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5470,6 +5579,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
@@ -5676,6 +5791,12 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -5748,6 +5869,17 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ pub mod transport;
|
|||||||
pub mod url;
|
pub mod url;
|
||||||
|
|
||||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||||
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{LazyLock, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
pub mod proto {
|
pub mod proto {
|
||||||
tonic::include_proto!("arbiter");
|
tonic::include_proto!("arbiter");
|
||||||
@@ -27,8 +31,27 @@ pub struct ClientMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub static BOOTSTRAP_PATH: &str = "bootstrap_token";
|
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> {
|
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";
|
static ARBITER_HOME: &str = ".arbiter";
|
||||||
let home_dir = std::env::home_dir().ok_or(std::io::Error::new(
|
let home_dir = std::env::home_dir().ok_or(std::io::Error::new(
|
||||||
std::io::ErrorKind::PermissionDenied,
|
std::io::ErrorKind::PermissionDenied,
|
||||||
|
|||||||
@@ -49,12 +49,15 @@ pem = "3.0.6"
|
|||||||
k256.workspace = true
|
k256.workspace = true
|
||||||
rsa.workspace = true
|
rsa.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
hmac = "0.12"
|
|
||||||
spki.workspace = true
|
spki.workspace = true
|
||||||
alloy.workspace = true
|
alloy.workspace = true
|
||||||
prost-types.workspace = true
|
prost-types.workspace = true
|
||||||
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||||
|
clap = { version = "4.6", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.46.3"
|
insta = "1.46.3"
|
||||||
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
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,
|
id integer not null primary key,
|
||||||
nonce integer not null default(1), -- used for auth challenge
|
nonce integer not null default(1), -- used for auth challenge
|
||||||
public_key blob not null,
|
public_key blob not null,
|
||||||
pubkey_integrity_tag blob,
|
|
||||||
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
|
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
|
||||||
created_at integer not null default(unixepoch ('now')),
|
created_at integer not null default(unixepoch ('now')),
|
||||||
updated_at integer not null default(unixepoch ('now'))
|
updated_at integer not null default(unixepoch ('now'))
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod v1;
|
||||||
@@ -1,18 +1,52 @@
|
|||||||
use std::ops::Deref as _;
|
use std::ops::Deref as _;
|
||||||
|
|
||||||
use argon2::{Algorithm, Argon2};
|
use argon2::{Algorithm, Argon2, password_hash::Salt as ArgonSalt};
|
||||||
use chacha20poly1305::{
|
use chacha20poly1305::{
|
||||||
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
|
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
|
||||||
aead::{AeadMut, Error, Payload},
|
aead::{AeadMut, Error, Payload},
|
||||||
};
|
};
|
||||||
use rand::{Rng as _, SeedableRng as _, rngs::{StdRng, SysRng}};
|
use rand::{
|
||||||
|
Rng as _, SeedableRng,
|
||||||
|
rngs::{StdRng, SysRng},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{safe_cell::{SafeCell, SafeCellHandle as _}};
|
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||||
|
|
||||||
pub mod encryption;
|
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
|
||||||
pub mod integrity;
|
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
|
||||||
|
|
||||||
use encryption::v1::{Nonce, Salt};
|
pub const NONCE_LENGTH: usize = 24;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Nonce([u8; NONCE_LENGTH]);
|
||||||
|
impl Nonce {
|
||||||
|
pub fn increment(&mut self) {
|
||||||
|
for i in (0..self.0.len()).rev() {
|
||||||
|
if self.0[i] == 0xFF {
|
||||||
|
self.0[i] = 0;
|
||||||
|
} else {
|
||||||
|
self.0[i] += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vec(&self) -> Vec<u8> {
|
||||||
|
self.0.to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> TryFrom<&'a [u8]> for Nonce {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != NONCE_LENGTH {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let mut nonce = [0u8; NONCE_LENGTH];
|
||||||
|
nonce.copy_from_slice(value);
|
||||||
|
Ok(Self(nonce))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct KeyCell(pub SafeCell<Key>);
|
pub struct KeyCell(pub SafeCell<Key>);
|
||||||
impl From<SafeCell<Key>> for KeyCell {
|
impl From<SafeCell<Key>> for KeyCell {
|
||||||
@@ -99,9 +133,22 @@ impl KeyCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH];
|
||||||
|
|
||||||
|
pub fn generate_salt() -> Salt {
|
||||||
|
let mut salt = Salt::default();
|
||||||
|
#[allow(
|
||||||
|
clippy::unwrap_used,
|
||||||
|
reason = "Rng failure is unrecoverable and should panic"
|
||||||
|
)]
|
||||||
|
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
|
||||||
|
rng.fill_bytes(&mut salt);
|
||||||
|
salt
|
||||||
|
}
|
||||||
|
|
||||||
/// User password might be of different length, have not enough entropy, etc...
|
/// User password might be of different length, have not enough entropy, etc...
|
||||||
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
|
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
|
||||||
pub fn derive_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
pub fn derive_seal_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
let params = argon2::Params::new(262_144, 3, 4, None).unwrap();
|
let params = argon2::Params::new(262_144, 3, 4, None).unwrap();
|
||||||
let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params);
|
let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params);
|
||||||
@@ -124,8 +171,37 @@ pub fn derive_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{safe_cell::{SafeCell, SafeCellHandle as _}};
|
use super::*;
|
||||||
use super::{derive_key, encryption::v1::{Nonce, generate_salt}};
|
use crate::safe_cell::SafeCell;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn derive_seal_key_deterministic() {
|
||||||
|
static PASSWORD: &[u8] = b"password";
|
||||||
|
let password = SafeCell::new(PASSWORD.to_vec());
|
||||||
|
let password2 = SafeCell::new(PASSWORD.to_vec());
|
||||||
|
let salt = generate_salt();
|
||||||
|
|
||||||
|
let mut key1 = derive_seal_key(password, &salt);
|
||||||
|
let mut key2 = derive_seal_key(password2, &salt);
|
||||||
|
|
||||||
|
let key1_reader = key1.0.read();
|
||||||
|
let key2_reader = key2.0.read();
|
||||||
|
|
||||||
|
assert_eq!(key1_reader.deref(), key2_reader.deref());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn successful_derive() {
|
||||||
|
static PASSWORD: &[u8] = b"password";
|
||||||
|
let password = SafeCell::new(PASSWORD.to_vec());
|
||||||
|
let salt = generate_salt();
|
||||||
|
|
||||||
|
let mut key = derive_seal_key(password, &salt);
|
||||||
|
let key_reader = key.0.read();
|
||||||
|
let key_ref = key_reader.deref();
|
||||||
|
|
||||||
|
assert_ne!(key_ref.as_slice(), &[0u8; 32][..]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn encrypt_decrypt() {
|
pub fn encrypt_decrypt() {
|
||||||
@@ -133,7 +209,7 @@ mod tests {
|
|||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
let password = SafeCell::new(PASSWORD.to_vec());
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
|
|
||||||
let mut key = derive_key(password, &salt);
|
let mut key = derive_seal_key(password, &salt);
|
||||||
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
||||||
let associated_data = b"associated data";
|
let associated_data = b"associated data";
|
||||||
let mut buffer = b"secret data".to_vec();
|
let mut buffer = b"secret data".to_vec();
|
||||||
@@ -150,4 +226,18 @@ mod tests {
|
|||||||
let buffer = buffer.read();
|
let buffer = buffer.read();
|
||||||
assert_eq!(*buffer, b"secret data");
|
assert_eq!(*buffer, b"secret data");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// We should fuzz this
|
||||||
|
pub fn test_nonce_increment() {
|
||||||
|
let mut nonce = Nonce([0u8; NONCE_LENGTH]);
|
||||||
|
nonce.increment();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
nonce.0,
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ use kameo::{Actor, Reply, messages};
|
|||||||
use strum::{EnumDiscriminants, IntoDiscriminant};
|
use strum::{EnumDiscriminants, IntoDiscriminant};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{crypto::{KeyCell, derive_key, encryption::v1::{self, Nonce}, integrity::v1::compute_integrity_tag}, safe_cell::SafeCell};
|
use crate::safe_cell::SafeCell;
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
@@ -17,7 +17,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
safe_cell::SafeCellHandle as _,
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
|
use encryption::v1::{self, KeyCell, Nonce};
|
||||||
|
|
||||||
|
pub mod encryption;
|
||||||
|
|
||||||
#[derive(Default, EnumDiscriminants)]
|
#[derive(Default, EnumDiscriminants)]
|
||||||
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
||||||
@@ -113,7 +115,7 @@ impl KeyHolder {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut nonce =
|
let mut nonce =
|
||||||
Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
v1::Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
||||||
error!(
|
error!(
|
||||||
"Broken database: invalid nonce for root key history id={}",
|
"Broken database: invalid nonce for root key history id={}",
|
||||||
root_key_id
|
root_key_id
|
||||||
@@ -142,12 +144,12 @@ impl KeyHolder {
|
|||||||
return Err(Error::AlreadyBootstrapped);
|
return Err(Error::AlreadyBootstrapped);
|
||||||
}
|
}
|
||||||
let salt = v1::generate_salt();
|
let salt = v1::generate_salt();
|
||||||
let mut seal_key = derive_key(seal_key_raw, &salt);
|
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
||||||
let mut root_key = KeyCell::new_secure_random();
|
let mut root_key = KeyCell::new_secure_random();
|
||||||
|
|
||||||
// Zero nonces are fine because they are one-time
|
// Zero nonces are fine because they are one-time
|
||||||
let root_key_nonce = Nonce::default();
|
let root_key_nonce = v1::Nonce::default();
|
||||||
let data_encryption_nonce = Nonce::default();
|
let data_encryption_nonce = v1::Nonce::default();
|
||||||
|
|
||||||
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
||||||
let root_key_reader = reader.as_slice();
|
let root_key_reader = reader.as_slice();
|
||||||
@@ -223,7 +225,7 @@ impl KeyHolder {
|
|||||||
error!("Broken database: invalid salt for root key");
|
error!("Broken database: invalid salt for root key");
|
||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?;
|
})?;
|
||||||
let mut seal_key = derive_key(seal_key_raw, &salt);
|
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
||||||
|
|
||||||
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
||||||
|
|
||||||
@@ -243,7 +245,7 @@ impl KeyHolder {
|
|||||||
|
|
||||||
self.state = State::Unsealed {
|
self.state = State::Unsealed {
|
||||||
root_key_history_id: current_key.id,
|
root_key_history_id: current_key.id,
|
||||||
root_key: KeyCell::try_from(root_key).map_err(|err| {
|
root_key: v1::KeyCell::try_from(root_key).map_err(|err| {
|
||||||
error!(?err, "Broken database: invalid encryption key size");
|
error!(?err, "Broken database: invalid encryption key size");
|
||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?,
|
})?,
|
||||||
@@ -254,25 +256,7 @@ impl KeyHolder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signs a generic integrity payload using the vault-derived integrity key
|
// Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext
|
||||||
#[message]
|
|
||||||
pub fn sign_integrity_tag(
|
|
||||||
&mut self,
|
|
||||||
purpose_tag: Vec<u8>,
|
|
||||||
data_parts: Vec<Vec<u8>>,
|
|
||||||
) -> Result<Vec<u8>, Error> {
|
|
||||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
|
||||||
return Err(Error::NotBootstrapped);
|
|
||||||
};
|
|
||||||
|
|
||||||
let tag = compute_integrity_tag(
|
|
||||||
root_key,
|
|
||||||
&purpose_tag,
|
|
||||||
data_parts.iter().map(Vec::as_slice),
|
|
||||||
);
|
|
||||||
Ok(tag.to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
||||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||||
@@ -308,7 +292,6 @@ impl KeyHolder {
|
|||||||
let State::Unsealed {
|
let State::Unsealed {
|
||||||
root_key,
|
root_key,
|
||||||
root_key_history_id,
|
root_key_history_id,
|
||||||
..
|
|
||||||
} = &mut self.state
|
} = &mut self.state
|
||||||
else {
|
else {
|
||||||
return Err(Error::NotBootstrapped);
|
return Err(Error::NotBootstrapped);
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::Bi;
|
||||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::error::SendError;
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
bootstrap::ConsumeToken,
|
bootstrap::ConsumeToken,
|
||||||
keyholder::{self, SignIntegrityTag},
|
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
||||||
},
|
},
|
||||||
crypto::integrity::v1::USERAGENT_INTEGRITY_TAG,
|
|
||||||
db::schema,
|
db::schema,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum AttestationStatus {
|
|
||||||
Attested,
|
|
||||||
NotAttested,
|
|
||||||
Unavailable,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ChallengeRequest {
|
pub struct ChallengeRequest {
|
||||||
pub pubkey: AuthPublicKey,
|
pub pubkey: AuthPublicKey,
|
||||||
}
|
}
|
||||||
@@ -50,11 +40,7 @@ smlang::statemachine!(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
async fn create_nonce(
|
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
|
||||||
db: &crate::db::DatabasePool,
|
|
||||||
pubkey_bytes: &[u8],
|
|
||||||
key_type: crate::db::models::KeyType,
|
|
||||||
) -> Result<i32, Error> {
|
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
Error::internal("Database unavailable")
|
Error::internal("Database unavailable")
|
||||||
@@ -64,14 +50,12 @@ async fn create_nonce(
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let current_nonce = schema::useragent_client::table
|
let current_nonce = schema::useragent_client::table
|
||||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
.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)
|
.select(schema::useragent_client::nonce)
|
||||||
.first::<i32>(conn)
|
.first::<i32>(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
update(schema::useragent_client::table)
|
update(schema::useragent_client::table)
|
||||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
.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))
|
.set(schema::useragent_client::nonce.eq(current_nonce + 1))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -91,11 +75,7 @@ async fn create_nonce(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn register_key(
|
async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> {
|
||||||
db: &crate::db::DatabasePool,
|
|
||||||
pubkey: &AuthPublicKey,
|
|
||||||
integrity_tag: Option<Vec<u8>>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||||
let key_type = pubkey.key_type();
|
let key_type = pubkey.key_type();
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
@@ -108,7 +88,6 @@ async fn register_key(
|
|||||||
schema::useragent_client::public_key.eq(pubkey_bytes),
|
schema::useragent_client::public_key.eq(pubkey_bytes),
|
||||||
schema::useragent_client::nonce.eq(1),
|
schema::useragent_client::nonce.eq(1),
|
||||||
schema::useragent_client::key_type.eq(key_type),
|
schema::useragent_client::key_type.eq(key_type),
|
||||||
schema::useragent_client::pubkey_integrity_tag.eq(integrity_tag),
|
|
||||||
))
|
))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.await
|
.await
|
||||||
@@ -141,15 +120,8 @@ where
|
|||||||
&mut self,
|
&mut self,
|
||||||
ChallengeRequest { pubkey }: ChallengeRequest,
|
ChallengeRequest { pubkey }: ChallengeRequest,
|
||||||
) -> Result<ChallengeContext, Self::Error> {
|
) -> 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 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
|
self.transport
|
||||||
.send(Ok(Outbound::AuthChallenge { nonce }))
|
.send(Ok(Outbound::AuthChallenge { nonce }))
|
||||||
@@ -189,15 +161,7 @@ where
|
|||||||
return Err(Error::InvalidBootstrapToken);
|
return Err(Error::InvalidBootstrapToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
let integrity_tag = self
|
register_key(&self.conn.db, &pubkey).await?;
|
||||||
.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?;
|
|
||||||
|
|
||||||
self.transport
|
self.transport
|
||||||
.send(Ok(Outbound::AuthSuccess))
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
@@ -246,112 +210,13 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match valid {
|
if valid {
|
||||||
true => {
|
|
||||||
self.transport
|
self.transport
|
||||||
.send(Ok(Outbound::AuthSuccess))
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::Transport)?;
|
.map_err(|_| Error::Transport)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(key.clone())
|
Ok(key.clone())
|
||||||
}
|
}
|
||||||
false => {
|
|
||||||
self.transport
|
|
||||||
.send(Err(Error::InvalidChallengeSolution))
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::Transport)?;
|
|
||||||
Err(Error::InvalidChallengeSolution)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 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,21 +2,22 @@ use std::sync::Mutex;
|
|||||||
|
|
||||||
use alloy::primitives::Address;
|
use alloy::primitives::Address;
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
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 diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use kameo::error::SendError;
|
use kameo::error::SendError;
|
||||||
use kameo::messages;
|
|
||||||
use kameo::prelude::Context;
|
use kameo::prelude::Context;
|
||||||
|
use kameo::{message, messages};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
||||||
use crate::actors::keyholder::KeyHolderState;
|
use crate::actors::keyholder::KeyHolderState;
|
||||||
use crate::actors::user_agent::session::Error;
|
use crate::actors::user_agent::session::Error;
|
||||||
use crate::crypto::integrity::v1::USERAGENT_INTEGRITY_TAG;
|
|
||||||
use crate::db::models::{
|
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::evm::policies::{Grant, SpecificGrant};
|
||||||
use crate::safe_cell::SafeCell;
|
use crate::safe_cell::SafeCell;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -24,7 +25,7 @@ use crate::{
|
|||||||
evm::{
|
evm::{
|
||||||
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
||||||
},
|
},
|
||||||
keyholder::{self, Bootstrap, SignIntegrityTag, TryUnseal},
|
keyholder::{self, Bootstrap, TryUnseal},
|
||||||
user_agent::session::{
|
user_agent::session::{
|
||||||
UserAgentSession,
|
UserAgentSession,
|
||||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||||
@@ -86,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: 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 {
|
pub struct UnsealStartResponse {
|
||||||
@@ -223,8 +174,6 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.backfill_missing_useragent_pubkey_integrity_tags()
|
|
||||||
.await?;
|
|
||||||
info!("Successfully unsealed key with client-provided key");
|
info!("Successfully unsealed key with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -286,8 +235,6 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.backfill_missing_useragent_pubkey_integrity_tags()
|
|
||||||
.await?;
|
|
||||||
info!("Successfully bootstrapped vault with client-provided key");
|
info!("Successfully bootstrapped vault with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
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>,
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod v1;
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
use argon2::password_hash::Salt as ArgonSalt;
|
|
||||||
|
|
||||||
use rand::{
|
|
||||||
Rng as _, SeedableRng,
|
|
||||||
rngs::{StdRng, SysRng},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
|
|
||||||
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
|
|
||||||
|
|
||||||
pub const NONCE_LENGTH: usize = 24;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Nonce(pub [u8; NONCE_LENGTH]);
|
|
||||||
impl Nonce {
|
|
||||||
pub fn increment(&mut self) {
|
|
||||||
for i in (0..self.0.len()).rev() {
|
|
||||||
if self.0[i] == 0xFF {
|
|
||||||
self.0[i] = 0;
|
|
||||||
} else {
|
|
||||||
self.0[i] += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_vec(&self) -> Vec<u8> {
|
|
||||||
self.0.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> TryFrom<&'a [u8]> for Nonce {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
|
||||||
if value.len() != NONCE_LENGTH {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
let mut nonce = [0u8; NONCE_LENGTH];
|
|
||||||
nonce.copy_from_slice(value);
|
|
||||||
Ok(Self(nonce))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH];
|
|
||||||
|
|
||||||
pub fn generate_salt() -> Salt {
|
|
||||||
let mut salt = Salt::default();
|
|
||||||
#[allow(
|
|
||||||
clippy::unwrap_used,
|
|
||||||
reason = "Rng failure is unrecoverable and should panic"
|
|
||||||
)]
|
|
||||||
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
|
|
||||||
rng.fill_bytes(&mut salt);
|
|
||||||
salt
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::ops::Deref as _;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::{crypto::derive_key, safe_cell::{SafeCell, SafeCellHandle as _}};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn derive_seal_key_deterministic() {
|
|
||||||
static PASSWORD: &[u8] = b"password";
|
|
||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
|
||||||
let password2 = SafeCell::new(PASSWORD.to_vec());
|
|
||||||
let salt = generate_salt();
|
|
||||||
|
|
||||||
let mut key1 = derive_key(password, &salt);
|
|
||||||
let mut key2 = derive_key(password2, &salt);
|
|
||||||
|
|
||||||
let key1_reader = key1.0.read();
|
|
||||||
let key2_reader = key2.0.read();
|
|
||||||
|
|
||||||
assert_eq!(key1_reader.deref(), key2_reader.deref());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn successful_derive() {
|
|
||||||
static PASSWORD: &[u8] = b"password";
|
|
||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
|
||||||
let salt = generate_salt();
|
|
||||||
|
|
||||||
let mut key = derive_key(password, &salt);
|
|
||||||
let key_reader = key.0.read();
|
|
||||||
let key_ref = key_reader.deref();
|
|
||||||
|
|
||||||
assert_ne!(key_ref.as_slice(), &[0u8; 32][..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// We should fuzz this
|
|
||||||
pub fn test_nonce_increment() {
|
|
||||||
let mut nonce = Nonce([0u8; NONCE_LENGTH]);
|
|
||||||
nonce.increment();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
nonce.0,
|
|
||||||
[
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod v1;
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
use crate::{crypto::KeyCell, safe_cell::SafeCellHandle as _};
|
|
||||||
use chacha20poly1305::Key;
|
|
||||||
use hmac::Mac as _;
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
|
|
||||||
/// 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: &Key| {
|
|
||||||
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 crate::{crypto::{derive_key, encryption::v1::generate_salt}, safe_cell::{SafeCell, SafeCellHandle as _}};
|
|
||||||
|
|
||||||
use super::{compute_integrity_tag, USERAGENT_INTEGRITY_TAG};
|
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn integrity_tag_deterministic() {
|
|
||||||
let salt = generate_salt();
|
|
||||||
let mut integrity_key = derive_key(SafeCell::new(b"password".to_vec()), &salt);
|
|
||||||
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 integrity_key = derive_key(SafeCell::new(b"password".to_vec()), &salt);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -242,7 +242,6 @@ pub struct UseragentClient {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub nonce: i32,
|
pub nonce: i32,
|
||||||
pub public_key: Vec<u8>,
|
pub public_key: Vec<u8>,
|
||||||
pub pubkey_integrity_tag: Option<Vec<u8>>,
|
|
||||||
pub created_at: SqliteTimestamp,
|
pub created_at: SqliteTimestamp,
|
||||||
pub updated_at: SqliteTimestamp,
|
pub updated_at: SqliteTimestamp,
|
||||||
pub key_type: KeyType,
|
pub key_type: KeyType,
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ diesel::table! {
|
|||||||
id -> Integer,
|
id -> Integer,
|
||||||
nonce -> Integer,
|
nonce -> Integer,
|
||||||
public_key -> Binary,
|
public_key -> Binary,
|
||||||
pubkey_integrity_tag -> Nullable<Binary>,
|
|
||||||
key_type -> Integer,
|
key_type -> Integer,
|
||||||
created_at -> Integer,
|
created_at -> Integer,
|
||||||
updated_at -> Integer,
|
updated_at -> Integer,
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
use crate::context::ServerContext;
|
|
||||||
|
|
||||||
pub mod crypto;
|
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 actors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
@@ -19,3 +26,64 @@ impl Server {
|
|||||||
Self { context }
|
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 clap::Parser;
|
||||||
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
|
use cli::{Cli, Command, RunArgs, ServiceCommand};
|
||||||
use miette::miette;
|
|
||||||
use rustls::crypto::aws_lc_rs;
|
use rustls::crypto::aws_lc_rs;
|
||||||
use tonic::transport::{Identity, ServerTlsConfig};
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
const PORT: u16 = 50051;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> miette::Result<()> {
|
async fn main() -> miette::Result<()> {
|
||||||
aws_lc_rs::default_provider().install_default().unwrap();
|
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(
|
.with_env_filter(
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||||
)
|
)
|
||||||
.init();
|
.try_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");
|
|
||||||
}
|
}
|
||||||
|
|||||||
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"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::{Error, KeyHolder}, crypto::encryption::v1::{Nonce, ROOT_KEY_TAG}, db::{self, models, schema}, safe_cell::{SafeCell, SafeCellHandle as _}
|
actors::keyholder::{Error, KeyHolder},
|
||||||
|
db::{self, models, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use diesel::{QueryDsl, SelectableHelper};
|
use diesel::{QueryDsl, SelectableHelper};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
@@ -25,13 +27,13 @@ async fn test_bootstrap() {
|
|||||||
assert_eq!(row.schema_version, 1);
|
assert_eq!(row.schema_version, 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
row.tag,
|
row.tag,
|
||||||
ROOT_KEY_TAG
|
arbiter_server::actors::keyholder::encryption::v1::ROOT_KEY_TAG
|
||||||
);
|
);
|
||||||
assert!(!row.ciphertext.is_empty());
|
assert!(!row.ciphertext.is_empty());
|
||||||
assert!(!row.salt.is_empty());
|
assert!(!row.salt.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
row.data_encryption_nonce,
|
row.data_encryption_nonce,
|
||||||
Nonce::default().to_vec()
|
arbiter_server::actors::keyholder::encryption::v1::Nonce::default().to_vec()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::Error, crypto::encryption::v1::Nonce, db::{self, models, schema}, safe_cell::{SafeCell, SafeCellHandle as _}
|
actors::keyholder::{Error, encryption::v1},
|
||||||
|
db::{self, models, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
|
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
@@ -100,7 +102,7 @@ async fn test_nonce_never_reused() {
|
|||||||
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
|
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
|
||||||
|
|
||||||
for (i, row) in rows.iter().enumerate() {
|
for (i, row) in rows.iter().enumerate() {
|
||||||
let mut expected = Nonce::default();
|
let mut expected = v1::Nonce::default();
|
||||||
for _ in 0..=i {
|
for _ in 0..=i {
|
||||||
expected.increment();
|
expected.increment();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ use arbiter_server::{
|
|||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
bootstrap::GetToken,
|
bootstrap::GetToken,
|
||||||
keyholder::Bootstrap,
|
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
||||||
},
|
},
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
@@ -167,124 +165,3 @@ pub async fn test_challenge_auth() {
|
|||||||
|
|
||||||
task.await.unwrap().unwrap();
|
task.await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[test_log::test]
|
|
||||||
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
|
||||||
|
|
||||||
actors
|
|
||||||
.key_holder
|
|
||||||
.ask(Bootstrap {
|
|
||||||
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
insert_into(schema::useragent_client::table)
|
|
||||||
.values((
|
|
||||||
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
|
||||||
schema::useragent_client::key_type.eq(1i32),
|
|
||||||
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();
|
|
||||||
|
|
||||||
let expected_err = task.await.unwrap();
|
|
||||||
|
|
||||||
println!("Received expected error: {expected_err:#?}");
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
expected_err,
|
|
||||||
Err(auth::Error::InvalidChallengeSolution)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ use arbiter_server::{
|
|||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
keyholder::{Bootstrap, Seal},
|
keyholder::{Bootstrap, Seal},
|
||||||
user_agent::{
|
user_agent::{UserAgentSession, session::connection::{
|
||||||
UserAgentSession,
|
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||||
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
|
}},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
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 kameo::actor::Spawn as _;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
@@ -152,42 +149,3 @@ pub async fn test_unseal_retry_after_invalid_key() {
|
|||||||
assert!(matches!(response, Ok(())));
|
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