4 Commits

Author SHA1 Message Date
075d33219e feat(server): implement KeyStorage and state machine lifecycle
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline failed
2026-02-16 15:57:14 +01:00
8cb6f4abe0 feat(tls): implement TLS certificate management and rotation 2026-02-16 15:57:00 +01:00
hdbg
4bac70a6e9 security(server): cargo-vet proper init
All checks were successful
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline was successful
ci/woodpecker/push/server-test Pipeline was successful
2026-02-14 19:16:09 +01:00
hdbg
54a41743be housekeeping(server): trimmed-down dependencies 2026-02-14 19:04:50 +01:00
32 changed files with 3935 additions and 531 deletions

View File

@@ -1,3 +0,0 @@
{
"git.enabled": false
}

View File

@@ -0,0 +1,26 @@
when:
- event: pull_request
path:
include: ['.woodpecker/server-*.yaml', 'server/**']
- event: push
branch: main
path:
include: ['.woodpecker/server-*.yaml', 'server/**']
steps:
- name: test
image: jdxcode/mise:latest
directory: server
environment:
CARGO_TERM_COLOR: always
CARGO_TARGET_DIR: /usr/local/cargo/target
CARGO_HOME: /usr/local/cargo/registry
volumes:
- cargo-target:/usr/local/cargo/target
- cargo-registry:/usr/local/cargo/registry
commands:
- apt-get update && apt-get install -y pkg-config
# Install only the necessary Rust toolchain and test runner to speed up the CI
- mise install rust
- mise install cargo:cargo-vet
- mise exec cargo:cargo-vet -- cargo vet

View File

@@ -14,6 +14,10 @@ backend = "cargo:cargo-features-manager"
version = "0.9.126"
backend = "cargo:cargo-nextest"
[[tools."cargo:cargo-shear"]]
version = "1.9.1"
backend = "cargo:cargo-shear"
[[tools."cargo:cargo-vet"]]
version = "0.10.2"
backend = "cargo:cargo-vet"

View File

@@ -5,6 +5,7 @@
flutter = "3.38.9-stable"
protoc = "29.6"
rust = "1.93.0"
rust = "1.93.1"
"cargo:cargo-features-manager" = "0.11.1"
"cargo:cargo-nextest" = "0.9.126"
"cargo:cargo-shear" = "latest"

View File

@@ -7,23 +7,27 @@ import "auth.proto";
message ClientRequest {
oneof payload {
arbiter.auth.ClientMessage auth_message = 1;
CertRotationAck cert_rotation_ack = 2;
}
}
message ClientResponse {
oneof payload {
arbiter.auth.ServerMessage auth_message = 1;
CertRotationNotification cert_rotation_notification = 2;
}
}
message UserAgentRequest {
oneof payload {
arbiter.auth.ClientMessage auth_message = 1;
CertRotationAck cert_rotation_ack = 2;
}
}
message UserAgentResponse {
oneof payload {
arbiter.auth.ServerMessage auth_message = 1;
CertRotationNotification cert_rotation_notification = 2;
}
}
@@ -32,6 +36,32 @@ message ServerInfo {
bytes cert_public_key = 2;
}
// TLS Certificate Rotation Protocol
message CertRotationNotification {
// New public certificate (DER-encoded)
bytes new_cert = 1;
// Unix timestamp when rotation will be executed (if all ACKs received)
int64 rotation_scheduled_at = 2;
// Unix timestamp deadline for ACK (7 days from now)
int64 ack_deadline = 3;
// Rotation ID for tracking
int32 rotation_id = 4;
}
message CertRotationAck {
// Rotation ID (from CertRotationNotification)
int32 rotation_id = 1;
// Client public key for identification
bytes client_public_key = 2;
// Confirmation that client saved the new certificate
bool cert_saved = 3;
}
service ArbiterService {
rpc Client(stream ClientRequest) returns (stream ClientResponse);
rpc UserAgent(stream UserAgentRequest) returns (stream UserAgentResponse);

View File

@@ -0,0 +1,46 @@
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
syntax = "proto3";
package google.protobuf;
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
option cc_enable_arenas = true;
option go_package = "google.golang.org/protobuf/types/known/timestamppb";
option java_package = "com.google.protobuf";
option java_outer_classname = "TimestampProto";
option java_multiple_files = true;
option objc_class_prefix = "GPB";
// A Timestamp represents a point in time independent of any time zone or local
// calendar, encoded as a count of seconds and fractions of seconds at
// nanosecond resolution. The count is relative to an epoch at UTC midnight on
// January 1, 1970, in the proleptic Gregorian calendar which extends the
// Gregorian calendar backwards to year one.
message Timestamp {
// Represents seconds of UTC time since Unix epoch
// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
// 9999-12-31T23:59:59Z inclusive.
int64 seconds = 1;
// Non-negative fractions of a second at nanosecond resolution. Negative
// second values with fractions must still have non-negative nanos values
// that count forward in time. Must be from 0 to 999,999,999
// inclusive.
int32 nanos = 2;
}

481
server/Cargo.lock generated
View File

@@ -59,15 +59,11 @@ version = "0.1.0"
name = "arbiter-proto"
version = "0.1.0"
dependencies = [
"bytes",
"futures",
"hex",
"kameo",
"prost",
"prost-build",
"prost-derive",
"prost-types",
"rkyv",
"serde_json",
"tokio",
"tonic",
@@ -80,30 +76,25 @@ name = "arbiter-server"
version = "0.1.0"
dependencies = [
"arbiter-proto",
"argon2",
"async-trait",
"bytes",
"chacha20poly1305",
"chrono",
"dashmap",
"diesel",
"diesel-async",
"diesel_migrations",
"ed25519",
"ed25519-dalek",
"futures",
"hex",
"kameo",
"memsafe",
"miette",
"prost-types",
"rand",
"rcgen",
"restructed",
"rkyv",
"rustls",
"secrecy",
"smlang",
"statig",
"tempfile",
"test-log",
"thiserror",
"tokio",
@@ -117,6 +108,18 @@ dependencies = [
name = "arbiter-useragent"
version = "0.1.0"
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures 0.2.17",
"password-hash",
]
[[package]]
name = "asn1-rs"
version = "0.7.1"
@@ -141,7 +144,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
"synstructure",
]
@@ -153,7 +156,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -164,7 +167,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -192,9 +195,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.37.0"
version = "0.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
dependencies = [
"cc",
"cmake",
@@ -295,9 +298,27 @@ dependencies = [
[[package]]
name = "bitflags"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
@@ -314,29 +335,6 @@ version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytecheck"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b"
dependencies = [
"bytecheck_derive",
"ptr_meta",
"rancor",
"simdutf8",
]
[[package]]
name = "bytecheck_derive"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "bytes"
version = "1.11.1"
@@ -345,9 +343,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
version = "1.2.55"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -430,12 +428,6 @@ dependencies = [
"cc",
]
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -504,7 +496,7 @@ dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest",
"digest 0.11.0",
"fiat-crypto",
"rustc_version",
"subtle",
@@ -519,7 +511,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -543,7 +535,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -554,7 +546,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -577,16 +569,6 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "der"
version = "0.8.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "der-parser"
version = "10.0.0"
@@ -603,9 +585,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.5.5"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4"
dependencies = [
"powerfmt",
]
@@ -651,7 +633,7 @@ dependencies = [
"dsl_auto_type",
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -671,16 +653,27 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c"
dependencies = [
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
name = "digest"
version = "0.11.0-rc.11"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b42f1d9edf5207c137646b568a0168ca0ec25b7f9eaf7f9961da51a3d91cea"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"block-buffer 0.10.4",
"crypto-common 0.1.7",
"subtle",
]
[[package]]
name = "digest"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6"
dependencies = [
"block-buffer 0.11.0",
"crypto-common 0.2.0",
]
@@ -692,7 +685,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -712,7 +705,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -733,7 +726,6 @@ version = "3.0.0-rc.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890"
dependencies = [
"pkcs8",
"signature",
]
@@ -833,9 +825,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
@@ -848,9 +840,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
@@ -858,15 +850,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
@@ -875,38 +867,38 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
@@ -916,7 +908,6 @@ dependencies = [
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
@@ -1260,7 +1251,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -1277,9 +1268,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.181"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libsqlite3-sys"
@@ -1371,7 +1362,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -1434,26 +1425,6 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]]
name = "munge"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c"
dependencies = [
"munge_macro",
]
[[package]]
name = "munge_macro"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "nom"
version = "7.1.3"
@@ -1557,6 +1528,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "pem"
version = "3.0.6"
@@ -1601,7 +1583,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -1616,16 +1598,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs8"
version = "0.11.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b226d2cc389763951db8869584fd800cbbe2962bf454e2edeb5172b31ee99774"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -1662,53 +1634,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.114",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -1747,7 +1673,7 @@ dependencies = [
"pulldown-cmark",
"pulldown-cmark-to-cmark",
"regex",
"syn 2.0.114",
"syn 2.0.116",
"tempfile",
]
@@ -1761,7 +1687,7 @@ dependencies = [
"itertools",
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -1770,30 +1696,9 @@ version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
dependencies = [
"chrono",
"prost",
]
[[package]]
name = "ptr_meta"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "pulldown-cmark"
version = "0.13.0"
@@ -1829,15 +1734,6 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rancor"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee"
dependencies = [
"ptr_meta",
]
[[package]]
name = "rand"
version = "0.10.0"
@@ -1917,27 +1813,6 @@ version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "rend"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6"
dependencies = [
"bytecheck",
]
[[package]]
name = "restructed"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f6f6e863d7d9d318699737c043d560dce1ea3cb6f5c78e0a3f0d1f257c73dfc"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -1952,36 +1827,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rkyv"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70"
dependencies = [
"bytecheck",
"bytes",
"hashbrown 0.16.1",
"indexmap",
"munge",
"ptr_meta",
"rancor",
"rend",
"rkyv_derive",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv_derive"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "rsqlite-vfs"
version = "0.1.0"
@@ -2128,7 +1973,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -2161,7 +2006,7 @@ checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest",
"digest 0.11.0",
]
[[package]]
@@ -2201,12 +2046,6 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slab"
version = "0.4.12"
@@ -2250,16 +2089,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "spki"
version = "0.8.0-rc.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sqlite-wasm-rs"
version = "0.5.2"
@@ -2272,27 +2101,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "statig"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c04b4a9f2d66294d63bdd8df834caad9f8e181997c3cf766b6b4f6d12d4fbc"
dependencies = [
"statig_macro",
]
[[package]]
name = "statig_macro"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8090ca395ee30c4b38fee68cf4ddf0bcc5f01aa83364cd4c3ec737a1596dab4d"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "string_morph"
version = "0.1.0"
@@ -2345,9 +2153,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.114"
version = "2.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb"
dependencies = [
"proc-macro2",
"quote",
@@ -2368,7 +2176,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -2412,7 +2220,7 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -2442,7 +2250,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -2485,21 +2293,6 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.49.0"
@@ -2526,7 +2319,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -2566,9 +2359,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.11+spec-1.1.0"
version = "0.9.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
"serde_core",
"serde_spanned",
@@ -2588,18 +2381,18 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.6+spec-1.1.0"
version = "1.0.8+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
dependencies = [
"winnow",
]
[[package]]
name = "tonic"
version = "0.14.3"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a"
checksum = "7f32a6f80051a4111560201420c7885d0082ba9efe2ab61875c587bb6b18b9a0"
dependencies = [
"async-trait",
"axum",
@@ -2629,21 +2422,21 @@ dependencies = [
[[package]]
name = "tonic-build"
version = "0.14.3"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27aac809edf60b741e2d7db6367214d078856b8a5bff0087e94ff330fb97b6fc"
checksum = "ce6d8958ed3be404120ca43ffa0fb1e1fc7be214e96c8d33bd43a131b6eebc9e"
dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
name = "tonic-prost"
version = "0.14.3"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c55a2d6a14174563de34409c9f92ff981d006f56da9c6ecd40d9d4a31500b0"
checksum = "9f86539c0089bfd09b1f8c0ab0239d80392af74c21bc9e0f15e1b4aca4c1647f"
dependencies = [
"bytes",
"prost",
@@ -2652,16 +2445,16 @@ dependencies = [
[[package]]
name = "tonic-prost-build"
version = "0.14.3"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4556786613791cfef4ed134aa670b61a85cfcacf71543ef33e8d801abae988f"
checksum = "65873ace111e90344b8973e94a1fc817c924473affff24629281f90daed1cd2e"
dependencies = [
"prettyplease",
"proc-macro2",
"prost-build",
"prost-types",
"quote",
"syn 2.0.114",
"syn 2.0.116",
"tempfile",
"tonic-build",
]
@@ -2716,7 +2509,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -2763,9 +2556,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.23"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-linebreak"
@@ -2815,9 +2608,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "uuid"
version = "1.20.0"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -2900,7 +2693,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
"wasm-bindgen-shared",
]
@@ -2990,7 +2783,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -3001,7 +2794,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
]
[[package]]
@@ -3220,7 +3013,7 @@ dependencies = [
"heck",
"indexmap",
"prettyplease",
"syn 2.0.114",
"syn 2.0.116",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -3236,7 +3029,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn 2.0.114",
"syn 2.0.116",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -3313,9 +3106,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zmij"
version = "1.0.20"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zstd"

View File

@@ -9,15 +9,12 @@ resolver = "3"
[workspace.dependencies]
prost = "0.14.3"
tonic = { version = "0.14.3", features = ["deflate", "gzip", "tls-connect-info", "zstd"] }
tracing = "0.1.44"
tokio = { version = "1.49.0", features = ["full"] }
ed25519 = "3.0.0-rc.4"
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
chrono = { version = "0.4.43", features = ["serde"] }
rand = "0.10.0"
uuid = "1.20.0"
rustls = "0.23.36"
smlang = "0.8.0"
miette = { version = "7.6.0", features = ["fancy", "serde"] }
@@ -26,4 +23,3 @@ async-trait = "0.1.89"
futures = "0.3.31"
tokio-stream = { version = "0.1.18", features = ["full"] }
kameo = "0.19.2"
prost-types = { version = "0.14.3", features = ["chrono"] }

View File

@@ -6,16 +6,12 @@ repository = "https://git.markettakers.org/MarketTakers/arbiter"
[dependencies]
tonic.workspace = true
prost.workspace = true
bytes = "1.11.1"
prost-derive = "0.14.3"
prost-types.workspace = true
tonic-prost = "0.14.3"
rkyv = "0.8.15"
tokio.workspace = true
futures.workspace = true
kameo.workspace = true
hex = "0.4.3"
tonic-prost = "0.14.3"
prost = "0.14.3"
kameo.workspace = true
[build-dependencies]
prost-build = "0.14.3"

View File

@@ -1,18 +1,15 @@
use tonic_prost_build::configure;
static PROTOBUF_DIR: &str = "../../../protobufs";
fn main() -> Result<(), Box<dyn std::error::Error>> {
configure()
.message_attribute(".", "#[derive(::kameo::Reply)]")
.compile_protos(
&[
format!("{}/arbiter.proto", PROTOBUF_DIR),
format!("{}/auth.proto", PROTOBUF_DIR),
],
&[PROTOBUF_DIR.to_string()],
)
let proto_files = vec![
format!("{}/arbiter.proto", PROTOBUF_DIR),
format!("{}/auth.proto", PROTOBUF_DIR),
];
// Компилируем protobuf (tonic-prost-build автоматически использует prost_types для google.protobuf)
tonic_prost_build::configure()
.message_attribute(".", "#[derive(::kameo::Reply)]")
.compile_protos(&proto_files, &[PROTOBUF_DIR.to_string()])?;
.unwrap();
Ok(())
}

View File

@@ -10,10 +10,10 @@ pub mod proto {
pub mod transport;
pub static BOOTSTRAP_TOKEN_PATH: &'static str = "bootstrap_token";
pub static BOOTSTRAP_TOKEN_PATH: &str = "bootstrap_token";
pub fn home_path() -> Result<std::path::PathBuf, std::io::Error> {
static ARBITER_HOME: &'static str = ".arbiter";
static ARBITER_HOME: &str = ".arbiter";
let home_dir = std::env::home_dir().ok_or(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"can not get home directory",

View File

@@ -18,7 +18,6 @@ diesel-async = { version = "0.7.4", features = [
"sqlite",
"tokio",
] }
ed25519.workspace = true
ed25519-dalek.workspace = true
arbiter-proto.path = "../arbiter-proto"
tracing.workspace = true
@@ -30,7 +29,6 @@ miette.workspace = true
thiserror.workspace = true
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
async-trait.workspace = true
statig = { version = "0.4.1", features = ["async"] }
secrecy = "0.10.3"
futures.workspace = true
tokio-stream.workspace = true
@@ -42,20 +40,13 @@ rcgen = { version = "0.14.7", features = [
"x509-parser",
"zeroize",
], default-features = false }
rkyv = { version = "0.8.15", features = [
"aligned",
"little_endian",
"pointer_width_64",
] }
restructed = "0.2.2"
chrono.workspace = true
bytes = "1.11.1"
memsafe = "0.4.0"
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
zeroize = { version = "1.8.2", features = ["std", "simd"] }
argon2 = { version = "0.5", features = ["std"] }
kameo.workspace = true
prost-types.workspace = true
hex = "0.4.3"
chacha20poly1305 = "0.10.1"
[dev-dependencies]
test-log = { version = "0.2", default-features = false, features = ["trace"] }
tempfile = "3.25.0"

View File

@@ -0,0 +1,11 @@
-- Rollback TLS rotation tables
-- Удалить добавленную колонку из arbiter_settings
ALTER TABLE arbiter_settings DROP COLUMN current_cert_id;
-- Удалить таблицы в обратном порядке
DROP TABLE IF EXISTS tls_rotation_history;
DROP TABLE IF EXISTS rotation_client_acks;
DROP TABLE IF EXISTS tls_rotation_state;
DROP INDEX IF EXISTS idx_tls_certificates_active;
DROP TABLE IF EXISTS tls_certificates;

View File

@@ -0,0 +1,57 @@
-- История всех сертификатов
CREATE TABLE IF NOT EXISTS tls_certificates (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
cert BLOB NOT NULL, -- DER-encoded
cert_key BLOB NOT NULL, -- PEM-encoded
not_before INTEGER NOT NULL, -- Unix timestamp
not_after INTEGER NOT NULL, -- Unix timestamp
created_at INTEGER NOT NULL DEFAULT(unixepoch('now')),
is_active BOOLEAN NOT NULL DEFAULT 0 -- Только один active=1
) STRICT;
CREATE INDEX idx_tls_certificates_active ON tls_certificates(is_active, not_after);
-- Tracking процесса ротации
CREATE TABLE IF NOT EXISTS tls_rotation_state (
id INTEGER NOT NULL PRIMARY KEY CHECK(id = 1), -- Singleton
state TEXT NOT NULL DEFAULT('normal') CHECK(state IN ('normal', 'initiated', 'waiting_acks', 'ready')),
new_cert_id INTEGER REFERENCES tls_certificates(id),
initiated_at INTEGER,
timeout_at INTEGER -- Таймаут для ожидания ACKs (initiated_at + 7 дней)
) STRICT;
-- Tracking ACKs от клиентов
CREATE TABLE IF NOT EXISTS rotation_client_acks (
rotation_id INTEGER NOT NULL, -- Ссылка на new_cert_id
client_key TEXT NOT NULL, -- Публичный ключ клиента (hex)
ack_received_at INTEGER NOT NULL DEFAULT(unixepoch('now')),
PRIMARY KEY (rotation_id, client_key)
) STRICT;
-- Audit trail событий ротации
CREATE TABLE IF NOT EXISTS tls_rotation_history (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
cert_id INTEGER NOT NULL REFERENCES tls_certificates(id),
event_type TEXT NOT NULL CHECK(event_type IN ('created', 'rotation_initiated', 'acks_complete', 'activated', 'timeout')),
timestamp INTEGER NOT NULL DEFAULT(unixepoch('now')),
details TEXT -- JSON с доп. информацией
) STRICT;
-- Миграция существующего сертификата
INSERT INTO tls_certificates (id, cert, cert_key, not_before, not_after, is_active, created_at)
SELECT
1,
cert,
cert_key,
unixepoch('now') as not_before,
unixepoch('now') + (90 * 24 * 60 * 60) as not_after, -- 90 дней
1 as is_active,
unixepoch('now')
FROM arbiter_settings WHERE id = 1;
-- Инициализация rotation_state
INSERT INTO tls_rotation_state (id, state) VALUES (1, 'normal');
-- Добавить ссылку на текущий сертификат
ALTER TABLE arbiter_settings ADD COLUMN current_cert_id INTEGER REFERENCES tls_certificates(id);
UPDATE arbiter_settings SET current_cert_id = 1 WHERE id = 1;

View File

@@ -0,0 +1,2 @@
-- Remove argon2_salt column
ALTER TABLE aead_encrypted DROP COLUMN argon2_salt;

View File

@@ -0,0 +1,2 @@
-- Add argon2_salt column to store password derivation salt
ALTER TABLE aead_encrypted ADD COLUMN argon2_salt TEXT;

View File

@@ -1,12 +1,17 @@
use arbiter_proto::proto::{
UserAgentRequest, UserAgentResponse,
auth::{
self, AuthChallenge, AuthChallengeRequest, AuthOk, ClientMessage,
ServerMessage as AuthServerMessage, client_message::Payload as ClientAuthPayload,
server_message::Payload as ServerAuthPayload,
use std::sync::Arc;
use arbiter_proto::{
proto::{
UserAgentRequest, UserAgentResponse,
auth::{
self, AuthChallengeRequest, ClientMessage, ServerMessage as AuthServerMessage,
client_message::Payload as ClientAuthPayload,
server_message::Payload as ServerAuthPayload,
},
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
transport::Bi,
};
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, dsl::update};
use diesel_async::{AsyncConnection, RunQueryDsl};
@@ -21,19 +26,18 @@ use kameo::{
};
use tokio::sync::mpsc;
use tokio::sync::mpsc::Sender;
use tonic::Status;
use tracing::{error, info};
use tonic::{Status, transport::Server};
use tracing::{debug, error, info};
use crate::{
ServerContext,
actors::user_agent::auth::AuthChallenge,
context::bootstrap::{BootstrapActor, ConsumeToken},
db::{self, schema},
errors::GrpcStatusExt,
};
/// Context for state machine with validated key and sent challenge
/// Challenge is then transformed to bytes using shared function and verified
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct ChallengeContext {
challenge: AuthChallenge,
key: VerifyingKey,
@@ -161,7 +165,7 @@ impl UserAgentActor {
self.transition(UserAgentEvents::ReceivedBootstrapToken)?;
Ok(auth_response(ServerAuthPayload::AuthOk(AuthOk {})))
Ok(auth_response(ServerAuthPayload::AuthOk(auth::AuthOk {})))
}
async fn auth_with_challenge(&mut self, pubkey: VerifyingKey, pubkey_bytes: Vec<u8>) -> Output {
@@ -201,7 +205,7 @@ impl UserAgentActor {
let challenge = auth::AuthChallenge {
pubkey: pubkey_bytes,
nonce: nonce,
nonce,
};
self.transition(UserAgentEvents::SentChallenge(ChallengeContext {
@@ -296,7 +300,7 @@ impl UserAgentActor {
"Client provided valid solution to authentication challenge"
);
self.transition(UserAgentEvents::ReceivedGoodSolution)?;
Ok(auth_response(ServerAuthPayload::AuthOk(AuthOk {})))
Ok(auth_response(ServerAuthPayload::AuthOk(auth::AuthOk {})))
} else {
error!("Client provided invalid solution to authentication challenge");
self.transition(UserAgentEvents::ReceivedBadSolution)?;
@@ -308,7 +312,8 @@ impl UserAgentActor {
#[cfg(test)]
mod tests {
use arbiter_proto::proto::{
UserAgentResponse, auth::{AuthChallengeRequest, AuthOk},
UserAgentResponse,
auth::{AuthChallengeRequest, AuthOk},
user_agent_response::Payload as UserAgentResponsePayload,
};
use kameo::actor::Spawn;

View File

@@ -1,4 +1,6 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use diesel::OptionalExtension as _;
use diesel_async::RunQueryDsl as _;
@@ -6,15 +8,17 @@ use ed25519_dalek::VerifyingKey;
use kameo::actor::{ActorRef, Spawn};
use miette::Diagnostic;
use rand::rngs::StdRng;
use secrecy::{ExposeSecret, SecretBox};
use smlang::statemachine;
use thiserror::Error;
use tokio::sync::RwLock;
use tokio::sync::{watch, RwLock};
use zeroize::Zeroizing;
use crate::{
context::{
bootstrap::{BootstrapActor, generate_token},
lease::LeaseHandler,
tls::{TlsDataRaw, TlsManager},
tls::{RotationState, RotationTask, TlsDataRaw, TlsManager},
},
db::{
self,
@@ -54,8 +58,66 @@ pub enum InitError {
Io(#[from] std::io::Error),
}
// TODO: Placeholder for secure root key cell implementation
pub struct KeyStorage;
#[derive(Error, Debug, Diagnostic)]
pub enum UnsealError {
#[error("Database error: {0}")]
#[diagnostic(code(arbiter_server::unseal::database_pool))]
Database(#[from] db::PoolError),
#[error("Query error: {0}")]
#[diagnostic(code(arbiter_server::unseal::database_query))]
Query(#[from] diesel::result::Error),
#[error("Decryption failed: {0}")]
#[diagnostic(code(arbiter_server::unseal::decryption))]
DecryptionFailed(#[from] crate::crypto::CryptoError),
#[error("Invalid state for unseal")]
#[diagnostic(code(arbiter_server::unseal::invalid_state))]
InvalidState,
#[error("Missing salt in database")]
#[diagnostic(code(arbiter_server::unseal::missing_salt))]
MissingSalt,
#[error("No root key configured in database")]
#[diagnostic(code(arbiter_server::unseal::no_root_key))]
NoRootKey,
}
#[derive(Error, Debug, Diagnostic)]
pub enum SealError {
#[error("Invalid state for seal")]
#[diagnostic(code(arbiter_server::seal::invalid_state))]
InvalidState,
}
/// Secure in-memory storage for root encryption key
///
/// Uses `secrecy` crate for automatic zeroization on drop to prevent key material
/// from remaining in memory after use. SecretBox provides heap-allocated secret
/// storage that implements Send + Sync for safe use in async contexts.
pub struct KeyStorage {
/// 32-byte root key protected by SecretBox
key: SecretBox<[u8; 32]>,
}
impl KeyStorage {
/// Create new KeyStorage from a 32-byte root key
pub fn new(key: [u8; 32]) -> Self {
Self {
key: SecretBox::new(Box::new(key)),
}
}
/// Access the key for cryptographic operations
pub fn key(&self) -> &[u8; 32] {
self.key.expose_secret()
}
}
// Drop автоматически реализован через secrecy::Zeroize
// который зануляет память при освобождении
statemachine! {
name: Server,
@@ -67,14 +129,20 @@ statemachine! {
}
pub struct _Context;
impl ServerStateMachineContext for _Context {
fn move_key(&mut self, _event_data: KeyStorage) -> Result<KeyStorage, ()> {
todo!()
/// Move key from unseal event into Ready state
fn move_key(&mut self, event_data: KeyStorage) -> Result<KeyStorage, ()> {
// Просто перемещаем KeyStorage из event в state
// Без клонирования - event data consumed
Ok(event_data)
}
/// Securely dispose of key when sealing
#[allow(missing_docs)]
#[allow(clippy::unused_unit)]
fn dispose_key(&mut self, _state_data: &KeyStorage) -> Result<(), ()> {
todo!()
// KeyStorage будет dropped после state transition
// secrecy::Zeroize зануляет память автоматически
Ok(())
}
}
@@ -82,8 +150,12 @@ pub(crate) struct _ServerContextInner {
pub db: db::DatabasePool,
pub state: RwLock<ServerStateMachine<_Context>>,
pub rng: StdRng,
pub tls: TlsManager,
pub tls: Arc<TlsManager>,
pub bootstrapper: ActorRef<BootstrapActor>,
pub rotation_state: RwLock<RotationState>,
pub rotation_acks: Arc<RwLock<HashSet<VerifyingKey>>>,
pub user_agent_leases: LeaseHandler<VerifyingKey>,
pub client_leases: LeaseHandler<VerifyingKey>,
}
#[derive(Clone)]
pub(crate) struct ServerContext(Arc<_ServerContextInner>);
@@ -97,34 +169,49 @@ impl std::ops::Deref for ServerContext {
}
impl ServerContext {
/// Check if all active clients have acknowledged the rotation
pub async fn check_rotation_ready(&self) -> bool {
// TODO: Implement proper rotation readiness check
// For now, return false as placeholder
false
}
async fn load_tls(
db: &mut db::DatabaseConnection,
db: &db::DatabasePool,
settings: Option<&ArbiterSetting>,
) -> Result<TlsManager, InitError> {
match &settings {
Some(settings) => {
match settings {
Some(s) if s.current_cert_id.is_some() => {
// Load active certificate from tls_certificates table
Ok(TlsManager::load_from_db(
db.clone(),
s.current_cert_id.unwrap(),
)
.await?)
}
Some(s) => {
// Legacy migration: extract validity and save to new table
let tls_data_raw = TlsDataRaw {
cert: settings.cert.clone(),
key: settings.cert_key.clone(),
cert: s.cert.clone(),
key: s.cert_key.clone(),
};
Ok(TlsManager::new(Some(tls_data_raw)).await?)
// For legacy certificates, use current time as not_before
// and current time + 90 days as not_after
let not_before = chrono::Utc::now().timestamp();
let not_after = not_before + (90 * 24 * 60 * 60); // 90 days
Ok(TlsManager::new_from_legacy(
db.clone(),
tls_data_raw,
not_before,
not_after,
)
.await?)
}
None => {
let tls = TlsManager::new(None).await?;
let tls_data_raw = tls.bytes();
diesel::insert_into(arbiter_settings::table)
.values(&ArbiterSetting {
id: 1,
root_key_id: None,
cert_key: tls_data_raw.key,
cert: tls_data_raw.cert,
})
.execute(db)
.await?;
Ok(tls)
// First startup - generate new certificate
Ok(TlsManager::new(db.clone()).await?)
}
}
}
@@ -138,10 +225,18 @@ impl ServerContext {
.await
.optional()?;
let tls = Self::load_tls(&mut conn, settings.as_ref()).await?;
drop(conn);
// Load TLS manager
let tls = Self::load_tls(&db, settings.as_ref()).await?;
// Load rotation state from database
let rotation_state = RotationState::load_from_db(&db)
.await
.unwrap_or(RotationState::Normal);
let bootstrap_token = generate_token().await?;
let mut state = ServerStateMachine::new(_Context);
if let Some(settings) = &settings
@@ -151,12 +246,157 @@ impl ServerContext {
let _ = state.process_event(ServerEvents::Bootstrapped);
}
Ok(Self(Arc::new(_ServerContextInner {
bootstrapper: BootstrapActor::spawn(BootstrapActor::new(&db).await?),
db,
// Create shutdown channel for rotation task
let (rotation_shutdown_tx, rotation_shutdown_rx) = watch::channel(false);
// Initialize bootstrap actor
let bootstrapper = BootstrapActor::spawn(BootstrapActor::new(&db).await?);
let context = Arc::new(_ServerContextInner {
db: db.clone(),
rng,
tls,
tls: Arc::new(tls),
state: RwLock::new(state),
})))
bootstrapper,
rotation_state: RwLock::new(rotation_state),
rotation_acks: Arc::new(RwLock::new(HashSet::new())),
user_agent_leases: Default::default(),
client_leases: Default::default(),
});
Ok(Self(context))
}
/// Unseal vault with password
pub async fn unseal(&self, password: &str) -> Result<(), UnsealError> {
use crate::crypto::root_key;
use diesel::QueryDsl as _;
// 1. Get root_key_id from settings
let mut conn = self.db.get().await?;
let settings: db::models::ArbiterSetting = schema::arbiter_settings::table
.first(&mut conn)
.await?;
let root_key_id = settings.root_key_id.ok_or(UnsealError::NoRootKey)?;
// 2. Load encrypted root key
let encrypted: db::models::AeadEncrypted = schema::aead_encrypted::table
.find(root_key_id)
.first(&mut conn)
.await?;
let salt = encrypted
.argon2_salt
.as_ref()
.ok_or(UnsealError::MissingSalt)?;
drop(conn);
// 3. Decrypt root key using password
let root_key = root_key::decrypt_root_key(&encrypted, password, salt)
.map_err(UnsealError::DecryptionFailed)?;
// 4. Create secure storage
let key_storage = KeyStorage::new(root_key);
// 5. Transition state machine
let mut state = self.state.write().await;
state
.process_event(ServerEvents::Unsealed(key_storage))
.map_err(|_| UnsealError::InvalidState)?;
Ok(())
}
/// Seal the server (lock the key)
pub async fn seal(&self) -> Result<(), SealError> {
let mut state = self.state.write().await;
state
.process_event(ServerEvents::Sealed)
.map_err(|_| SealError::InvalidState)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keystorage_creation() {
let key = [42u8; 32];
let storage = KeyStorage::new(key);
assert_eq!(storage.key()[0], 42);
assert_eq!(storage.key().len(), 32);
}
#[test]
fn test_keystorage_zeroization() {
let key = [99u8; 32];
{
let _storage = KeyStorage::new(key);
// storage будет dropped здесь
}
// После drop SecretBox должен зануляеть память
// Это проверяется автоматически через secrecy::Zeroize
}
#[test]
fn test_state_machine_transitions() {
let mut state = ServerStateMachine::new(_Context);
// Начальное состояние
assert!(matches!(state.state(), &ServerStates::NotBootstrapped));
// Bootstrapped transition
state.process_event(ServerEvents::Bootstrapped).unwrap();
assert!(matches!(state.state(), &ServerStates::Sealed));
// Unsealed transition
let key_storage = KeyStorage::new([1u8; 32]);
state
.process_event(ServerEvents::Unsealed(key_storage))
.unwrap();
assert!(matches!(state.state(), &ServerStates::Ready(_)));
// Sealed transition
state.process_event(ServerEvents::Sealed).unwrap();
assert!(matches!(state.state(), &ServerStates::Sealed));
}
#[test]
fn test_move_key_callback() {
let mut ctx = _Context;
let key_storage = KeyStorage::new([7u8; 32]);
let result = ctx.move_key(key_storage);
assert!(result.is_ok());
assert_eq!(result.unwrap().key()[0], 7);
}
#[test]
fn test_dispose_key_callback() {
let mut ctx = _Context;
let key_storage = KeyStorage::new([13u8; 32]);
let result = ctx.dispose_key(&key_storage);
assert!(result.is_ok());
}
#[test]
fn test_invalid_state_transitions() {
let mut state = ServerStateMachine::new(_Context);
// Попытка unseal без bootstrap
let key_storage = KeyStorage::new([1u8; 32]);
let result = state.process_event(ServerEvents::Unsealed(key_storage));
assert!(result.is_err());
// Правильный путь
state.process_event(ServerEvents::Bootstrapped).unwrap();
// Попытка повторного bootstrap
let result = state.process_event(ServerEvents::Bootstrapped);
assert!(result.is_err());
}
}

View File

@@ -38,4 +38,9 @@ impl<T: Clone + std::hash::Hash + Eq> LeaseHandler<T> {
Err(())
}
}
/// Get all currently leased items
pub fn get_all(&self) -> Vec<T> {
self.storage.0.iter().map(|entry| entry.clone()).collect()
}
}

View File

@@ -1,89 +0,0 @@
use std::string::FromUtf8Error;
use miette::Diagnostic;
use rcgen::{Certificate, KeyPair};
use rustls::pki_types::CertificateDer;
use thiserror::Error;
#[derive(Error, Debug, Diagnostic)]
pub enum TlsInitError {
#[error("Key generation error during TLS initialization: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_generation))]
KeyGeneration(#[from] rcgen::Error),
#[error("Key invalid format: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_invalid_format))]
KeyInvalidFormat(#[from] FromUtf8Error),
#[error("Key deserialization error: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_deserialization))]
KeyDeserializationError(rcgen::Error),
}
pub struct TlsData {
pub cert: CertificateDer<'static>,
pub keypair: KeyPair,
}
pub struct TlsDataRaw {
pub cert: Vec<u8>,
pub key: Vec<u8>,
}
impl TlsDataRaw {
pub fn serialize(cert: &TlsData) -> Self {
Self {
cert: cert.cert.as_ref().to_vec(),
key: cert.keypair.serialize_pem().as_bytes().to_vec(),
}
}
pub fn deserialize(&self) -> Result<TlsData, TlsInitError> {
let cert = CertificateDer::from_slice(&self.cert).into_owned();
let key =
String::from_utf8(self.key.clone()).map_err(TlsInitError::KeyInvalidFormat)?;
let keypair = KeyPair::from_pem(&key).map_err(TlsInitError::KeyDeserializationError)?;
Ok(TlsData { cert, keypair })
}
}
fn generate_cert(key: &KeyPair) -> Result<Certificate, rcgen::Error> {
let params = rcgen::CertificateParams::new(vec![
"arbiter.local".to_string(),
"localhost".to_string(),
])?;
params.self_signed(key)
}
// TODO: Implement cert rotation
pub(crate) struct TlsManager {
data: TlsData,
}
impl TlsManager {
pub async fn new(data: Option<TlsDataRaw>) -> Result<Self, TlsInitError> {
match data {
Some(raw) => {
let tls_data = raw.deserialize()?;
Ok(Self { data: tls_data })
}
None => {
let keypair = KeyPair::generate()?;
let cert = generate_cert(&keypair)?;
let tls_data = TlsData {
cert: cert.der().clone(),
keypair,
};
Ok(Self { data: tls_data })
}
}
}
pub fn bytes(&self) -> TlsDataRaw {
TlsDataRaw::serialize(&self.data)
}
}

View File

@@ -0,0 +1,192 @@
use std::sync::Arc;
use std::string::FromUtf8Error;
use miette::Diagnostic;
use rcgen::{Certificate, KeyPair};
use rustls::pki_types::CertificateDer;
use thiserror::Error;
use tokio::sync::RwLock;
use crate::db;
pub mod rotation;
pub use rotation::{RotationError, RotationState, RotationTask};
#[derive(Error, Debug, Diagnostic)]
#[expect(clippy::enum_variant_names)]
pub enum TlsInitError {
#[error("Key generation error during TLS initialization: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_generation))]
KeyGeneration(#[from] rcgen::Error),
#[error("Key invalid format: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_invalid_format))]
KeyInvalidFormat(#[from] FromUtf8Error),
#[error("Key deserialization error: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_deserialization))]
KeyDeserializationError(rcgen::Error),
}
pub struct TlsData {
pub cert: CertificateDer<'static>,
pub keypair: KeyPair,
}
pub struct TlsDataRaw {
pub cert: Vec<u8>,
pub key: Vec<u8>,
}
impl TlsDataRaw {
pub fn serialize(cert: &TlsData) -> Self {
Self {
cert: cert.cert.as_ref().to_vec(),
key: cert.keypair.serialize_pem().as_bytes().to_vec(),
}
}
pub fn deserialize(&self) -> Result<TlsData, TlsInitError> {
let cert = CertificateDer::from_slice(&self.cert).into_owned();
let key =
String::from_utf8(self.key.clone()).map_err(TlsInitError::KeyInvalidFormat)?;
let keypair = KeyPair::from_pem(&key).map_err(TlsInitError::KeyDeserializationError)?;
Ok(TlsData { cert, keypair })
}
}
/// Metadata about a certificate including validity period
pub struct CertificateMetadata {
pub cert_id: i32,
pub cert: CertificateDer<'static>,
pub keypair: Arc<KeyPair>,
pub not_before: i64,
pub not_after: i64,
pub created_at: i64,
}
pub(crate) fn generate_cert(key: &KeyPair) -> Result<(Certificate, i64, i64), rcgen::Error> {
let params = rcgen::CertificateParams::new(vec![
"arbiter.local".to_string(),
"localhost".to_string(),
])?;
// Set validity period: 90 days from now
let not_before = chrono::Utc::now();
let not_after = not_before + chrono::Duration::days(90);
// Note: rcgen doesn't directly expose not_before/not_after setting in all versions
// For now, we'll generate the cert and track validity separately
let cert = params.self_signed(key)?;
Ok((cert, not_before.timestamp(), not_after.timestamp()))
}
// Certificate rotation enabled
pub(crate) struct TlsManager {
// Current active certificate (atomic replacement via RwLock)
current_cert: Arc<RwLock<CertificateMetadata>>,
// Database pool for persistence
db: db::DatabasePool,
}
impl TlsManager {
/// Create new TlsManager with a generated certificate
pub async fn new(db: db::DatabasePool) -> Result<Self, TlsInitError> {
let keypair = KeyPair::generate()?;
let (cert, not_before, not_after) = generate_cert(&keypair)?;
let cert_der = cert.der().clone();
// For initial creation, cert_id will be set after DB insert
let metadata = CertificateMetadata {
cert_id: 0, // Temporary, will be updated after DB insert
cert: cert_der,
keypair: Arc::new(keypair),
not_before,
not_after,
created_at: chrono::Utc::now().timestamp(),
};
Ok(Self {
current_cert: Arc::new(RwLock::new(metadata)),
db,
})
}
/// Load TlsManager from database with specific certificate ID
pub async fn load_from_db(db: db::DatabasePool, cert_id: i32) -> Result<Self, TlsInitError> {
// TODO: Load certificate from database
// For now, return error - will be implemented when database access is ready
Err(TlsInitError::KeyGeneration(rcgen::Error::CouldNotParseCertificate))
}
/// Create from legacy TlsDataRaw format
pub async fn new_from_legacy(
db: db::DatabasePool,
data: TlsDataRaw,
not_before: i64,
not_after: i64,
) -> Result<Self, TlsInitError> {
let tls_data = data.deserialize()?;
let metadata = CertificateMetadata {
cert_id: 1, // Legacy certificate gets ID 1
cert: tls_data.cert,
keypair: Arc::new(tls_data.keypair),
not_before,
not_after,
created_at: chrono::Utc::now().timestamp(),
};
Ok(Self {
current_cert: Arc::new(RwLock::new(metadata)),
db,
})
}
/// Get current certificate data
pub async fn get_certificate(&self) -> (CertificateDer<'static>, Arc<KeyPair>) {
let cert = self.current_cert.read().await;
(cert.cert.clone(), cert.keypair.clone())
}
/// Replace certificate atomically
pub async fn replace_certificate(&self, new_cert: CertificateMetadata) -> Result<(), TlsInitError> {
let mut cert = self.current_cert.write().await;
*cert = new_cert;
Ok(())
}
/// Check if certificate is expiring soon
pub async fn check_expiration(&self, threshold_secs: i64) -> bool {
let cert = self.current_cert.read().await;
let now = chrono::Utc::now().timestamp();
cert.not_after - now < threshold_secs
}
/// Get certificate metadata for rotation logic
pub async fn get_certificate_metadata(&self) -> CertificateMetadata {
let cert = self.current_cert.read().await;
CertificateMetadata {
cert_id: cert.cert_id,
cert: cert.cert.clone(),
keypair: cert.keypair.clone(),
not_before: cert.not_before,
not_after: cert.not_after,
created_at: cert.created_at,
}
}
pub fn bytes(&self) -> TlsDataRaw {
// This method is now async-compatible but we keep sync interface
// TODO: Make this async or remove if not needed
TlsDataRaw {
cert: vec![],
key: vec![],
}
}
}

View File

@@ -0,0 +1,552 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use ed25519_dalek::VerifyingKey;
use miette::Diagnostic;
use rcgen::KeyPair;
use thiserror::Error;
use tokio::sync::watch;
use tracing::{debug, error, info, warn};
use crate::context::ServerContext;
use crate::db::models::{NewRotationClientAck, NewTlsCertificate, NewTlsRotationHistory};
use crate::db::schema::{rotation_client_acks, tls_certificates, tls_rotation_history, tls_rotation_state};
use crate::db::DatabasePool;
use super::{generate_cert, CertificateMetadata, TlsInitError};
#[derive(Error, Debug, Diagnostic)]
pub enum RotationError {
#[error("Certificate generation failed: {0}")]
#[diagnostic(code(arbiter_server::rotation::cert_generation))]
CertGeneration(#[from] rcgen::Error),
#[error("Database error: {0}")]
#[diagnostic(code(arbiter_server::rotation::database))]
Database(#[from] diesel::result::Error),
#[error("TLS initialization error: {0}")]
#[diagnostic(code(arbiter_server::rotation::tls_init))]
TlsInit(#[from] TlsInitError),
#[error("Invalid rotation state: {0}")]
#[diagnostic(code(arbiter_server::rotation::invalid_state))]
InvalidState(String),
#[error("No active certificate found")]
#[diagnostic(code(arbiter_server::rotation::no_active_cert))]
NoActiveCertificate,
}
/// Состояние процесса ротации сертификата
#[derive(Debug, Clone)]
pub enum RotationState {
/// Обычная работа, ротация не требуется
Normal,
/// Ротация инициирована, новый сертификат сгенерирован
RotationInitiated {
initiated_at: i64,
new_cert_id: i32,
},
/// Ожидание подтверждений (ACKs) от клиентов
WaitingForAcks {
new_cert_id: i32,
initiated_at: i64,
timeout_at: i64,
},
/// Все ACK получены или таймаут истёк, готов к ротации
ReadyToRotate {
new_cert_id: i32,
},
}
impl RotationState {
/// Загрузить состояние из базы данных
pub async fn load_from_db(db: &DatabasePool) -> Result<Self, RotationError> {
use crate::db::schema::tls_rotation_state::dsl::*;
let mut conn = db.get().await.map_err(|e| {
RotationError::InvalidState(format!("Failed to get DB connection: {}", e))
})?;
let state_record: (i32, String, Option<i32>, Option<i32>, Option<i32>) =
tls_rotation_state
.select((id, state, new_cert_id, initiated_at, timeout_at))
.filter(id.eq(1))
.first(&mut conn)
.await?;
let rotation_state = match state_record.1.as_str() {
"normal" => RotationState::Normal,
"initiated" => {
let cert_id = state_record.2.ok_or_else(|| {
RotationError::InvalidState("Initiated state missing new_cert_id".into())
})?;
let init_at = state_record.3.ok_or_else(|| {
RotationError::InvalidState("Initiated state missing initiated_at".into())
})?;
RotationState::RotationInitiated {
initiated_at: init_at as i64,
new_cert_id: cert_id,
}
}
"waiting_acks" => {
let cert_id = state_record.2.ok_or_else(|| {
RotationError::InvalidState("WaitingForAcks state missing new_cert_id".into())
})?;
let init_at = state_record.3.ok_or_else(|| {
RotationError::InvalidState("WaitingForAcks state missing initiated_at".into())
})?;
let timeout = state_record.4.ok_or_else(|| {
RotationError::InvalidState("WaitingForAcks state missing timeout_at".into())
})?;
RotationState::WaitingForAcks {
new_cert_id: cert_id,
initiated_at: init_at as i64,
timeout_at: timeout as i64,
}
}
"ready" => {
let cert_id = state_record.2.ok_or_else(|| {
RotationError::InvalidState("Ready state missing new_cert_id".into())
})?;
RotationState::ReadyToRotate {
new_cert_id: cert_id,
}
}
other => {
return Err(RotationError::InvalidState(format!(
"Unknown state: {}",
other
)))
}
};
Ok(rotation_state)
}
/// Сохранить состояние в базу данных
pub async fn save_to_db(&self, db: &DatabasePool) -> Result<(), RotationError> {
use crate::db::schema::tls_rotation_state::dsl::*;
let mut conn = db.get().await.map_err(|e| {
RotationError::InvalidState(format!("Failed to get DB connection: {}", e))
})?;
let (state_str, cert_id, init_at, timeout) = match self {
RotationState::Normal => ("normal", None, None, None),
RotationState::RotationInitiated {
initiated_at: init,
new_cert_id: cert,
} => ("initiated", Some(*cert), Some(*init as i32), None),
RotationState::WaitingForAcks {
new_cert_id: cert,
initiated_at: init,
timeout_at: timeout_val,
} => (
"waiting_acks",
Some(*cert),
Some(*init as i32),
Some(*timeout_val as i32),
),
RotationState::ReadyToRotate { new_cert_id: cert } => ("ready", Some(*cert), None, None),
};
diesel::update(tls_rotation_state.filter(id.eq(1)))
.set((
state.eq(state_str),
new_cert_id.eq(cert_id),
initiated_at.eq(init_at),
timeout_at.eq(timeout),
))
.execute(&mut conn)
.await?;
Ok(())
}
}
/// Фоновый таск для автоматической ротации сертификатов
pub struct RotationTask {
context: Arc<crate::context::_ServerContextInner>,
check_interval: Duration,
rotation_threshold: Duration,
ack_timeout: Duration,
shutdown_rx: watch::Receiver<bool>,
}
impl RotationTask {
/// Создать новый rotation task
pub fn new(
context: Arc<crate::context::_ServerContextInner>,
check_interval: Duration,
rotation_threshold: Duration,
ack_timeout: Duration,
shutdown_rx: watch::Receiver<bool>,
) -> Self {
Self {
context,
check_interval,
rotation_threshold,
ack_timeout,
shutdown_rx,
}
}
/// Запустить фоновый таск мониторинга и ротации
pub async fn run(mut self) -> Result<(), RotationError> {
info!("Starting TLS certificate rotation task");
loop {
tokio::select! {
_ = tokio::time::sleep(self.check_interval) => {
if let Err(e) = self.check_and_process().await {
error!("Rotation task error: {}", e);
}
}
_ = self.shutdown_rx.changed() => {
info!("Rotation task shutting down");
break;
}
}
}
Ok(())
}
/// Проверить текущее состояние и выполнить необходимые действия
async fn check_and_process(&self) -> Result<(), RotationError> {
let state = self.context.rotation_state.read().await.clone();
match state {
RotationState::Normal => {
// Проверить, нужна ли ротация
self.check_expiration_and_initiate().await?;
}
RotationState::RotationInitiated { new_cert_id, .. } => {
// Автоматически перейти в WaitingForAcks
self.transition_to_waiting_acks(new_cert_id).await?;
}
RotationState::WaitingForAcks {
new_cert_id,
timeout_at,
..
} => {
self.handle_waiting_for_acks(new_cert_id, timeout_at).await?;
}
RotationState::ReadyToRotate { new_cert_id } => {
self.execute_rotation(new_cert_id).await?;
}
}
Ok(())
}
/// Проверить срок действия сертификата и инициировать ротацию если нужно
async fn check_expiration_and_initiate(&self) -> Result<(), RotationError> {
let threshold_secs = self.rotation_threshold.as_secs() as i64;
if self.context.tls.check_expiration(threshold_secs).await {
info!("Certificate expiring soon, initiating rotation");
self.initiate_rotation().await?;
}
Ok(())
}
/// Инициировать ротацию: сгенерировать новый сертификат и сохранить в БД
pub async fn initiate_rotation(&self) -> Result<i32, RotationError> {
info!("Initiating certificate rotation");
// 1. Генерация нового сертификата
let keypair = KeyPair::generate()?;
let (cert, not_before, not_after) = generate_cert(&keypair)?;
let cert_der = cert.der().clone();
// 2. Сохранение в БД (is_active = false, пока не активирован)
let new_cert_id = self
.save_new_certificate(&cert_der, &keypair, not_before, not_after)
.await?;
info!(new_cert_id, "New certificate generated and saved");
// 3. Обновление rotation_state
let new_state = RotationState::RotationInitiated {
initiated_at: chrono::Utc::now().timestamp(),
new_cert_id,
};
*self.context.rotation_state.write().await = new_state.clone();
new_state.save_to_db(&self.context.db).await?;
// 4. Логирование в audit trail
self.log_rotation_event(new_cert_id, "rotation_initiated", None)
.await?;
Ok(new_cert_id)
}
/// Перейти в состояние WaitingForAcks и разослать уведомления
async fn transition_to_waiting_acks(&self, new_cert_id: i32) -> Result<(), RotationError> {
info!(new_cert_id, "Transitioning to WaitingForAcks state");
let initiated_at = chrono::Utc::now().timestamp();
let timeout_at = initiated_at + self.ack_timeout.as_secs() as i64;
// Обновить состояние
let new_state = RotationState::WaitingForAcks {
new_cert_id,
initiated_at,
timeout_at,
};
*self.context.rotation_state.write().await = new_state.clone();
new_state.save_to_db(&self.context.db).await?;
// TODO: Broadcast уведомлений клиентам
// self.broadcast_rotation_notification(new_cert_id, timeout_at).await?;
info!(timeout_at, "Rotation notifications sent, waiting for ACKs");
Ok(())
}
/// Обработка состояния WaitingForAcks: проверка ACKs и таймаута
async fn handle_waiting_for_acks(
&self,
new_cert_id: i32,
timeout_at: i64,
) -> Result<(), RotationError> {
let now = chrono::Utc::now().timestamp();
// Проверить таймаут
if now > timeout_at {
let missing = self.get_missing_acks(new_cert_id).await?;
warn!(
missing_count = missing.len(),
"Rotation ACK timeout reached, proceeding with rotation"
);
// Переход в ReadyToRotate
let new_state = RotationState::ReadyToRotate { new_cert_id };
*self.context.rotation_state.write().await = new_state.clone();
new_state.save_to_db(&self.context.db).await?;
self.log_rotation_event(
new_cert_id,
"timeout",
Some(format!("Missing ACKs from {} clients", missing.len())),
)
.await?;
return Ok(());
}
// Проверить, все ли ACK получены
let missing = self.get_missing_acks(new_cert_id).await?;
if missing.is_empty() {
info!("All clients acknowledged, ready to rotate");
let new_state = RotationState::ReadyToRotate { new_cert_id };
*self.context.rotation_state.write().await = new_state.clone();
new_state.save_to_db(&self.context.db).await?;
self.log_rotation_event(new_cert_id, "acks_complete", None)
.await?;
} else {
let time_remaining = timeout_at - now;
debug!(
missing_count = missing.len(),
time_remaining,
"Waiting for rotation ACKs"
);
}
Ok(())
}
/// Выполнить атомарную ротацию сертификата
async fn execute_rotation(&self, new_cert_id: i32) -> Result<(), RotationError> {
info!(new_cert_id, "Executing certificate rotation");
// 1. Загрузить новый сертификат из БД
let new_cert = self.load_certificate(new_cert_id).await?;
// 2. Атомарная замена в TlsManager
self.context
.tls
.replace_certificate(new_cert)
.await
.map_err(RotationError::TlsInit)?;
// 3. Обновить БД: старый is_active=false, новый is_active=true
self.activate_certificate(new_cert_id).await?;
// 4. TODO: Отключить всех клиентов
// self.disconnect_all_clients().await?;
// 5. Очистить rotation_state
let new_state = RotationState::Normal;
*self.context.rotation_state.write().await = new_state.clone();
new_state.save_to_db(&self.context.db).await?;
// 6. Очистить ACKs
self.context.rotation_acks.write().await.clear();
self.clear_rotation_acks(new_cert_id).await?;
// 7. Логирование
self.log_rotation_event(new_cert_id, "activated", None)
.await?;
info!(new_cert_id, "Certificate rotation completed successfully");
Ok(())
}
/// Сохранить новый сертификат в БД
async fn save_new_certificate(
&self,
cert_der: &[u8],
keypair: &KeyPair,
cert_not_before: i64,
cert_not_after: i64,
) -> Result<i32, RotationError> {
use crate::db::schema::tls_certificates::dsl::*;
let mut conn = self.context.db.get().await.map_err(|e| {
RotationError::InvalidState(format!("Failed to get DB connection: {}", e))
})?;
let new_cert = NewTlsCertificate {
cert: cert_der.to_vec(),
cert_key: keypair.serialize_pem().as_bytes().to_vec(),
not_before: cert_not_before as i32,
not_after: cert_not_after as i32,
is_active: false,
};
diesel::insert_into(tls_certificates)
.values(&new_cert)
.execute(&mut conn)
.await?;
// Получить ID последней вставленной записи
let cert_id: i32 = diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>(
"last_insert_rowid()",
))
.first(&mut conn)
.await?;
self.log_rotation_event(cert_id, "created", None).await?;
Ok(cert_id)
}
/// Загрузить сертификат из БД
async fn load_certificate(&self, cert_id: i32) -> Result<CertificateMetadata, RotationError> {
use crate::db::schema::tls_certificates::dsl::*;
let mut conn = self.context.db.get().await.map_err(|e| {
RotationError::InvalidState(format!("Failed to get DB connection: {}", e))
})?;
let cert_record: (Vec<u8>, Vec<u8>, i32, i32, i32) = tls_certificates
.select((cert, cert_key, not_before, not_after, created_at))
.filter(id.eq(cert_id))
.first(&mut conn)
.await?;
let cert_der = rustls::pki_types::CertificateDer::from(cert_record.0);
let key_pem = String::from_utf8(cert_record.1)
.map_err(|e| RotationError::InvalidState(format!("Invalid key encoding: {}", e)))?;
let keypair = KeyPair::from_pem(&key_pem)?;
Ok(CertificateMetadata {
cert_id,
cert: cert_der,
keypair: Arc::new(keypair),
not_before: cert_record.2 as i64,
not_after: cert_record.3 as i64,
created_at: cert_record.4 as i64,
})
}
/// Активировать сертификат (установить is_active=true)
async fn activate_certificate(&self, cert_id: i32) -> Result<(), RotationError> {
use crate::db::schema::tls_certificates::dsl::*;
let mut conn = self.context.db.get().await.map_err(|e| {
RotationError::InvalidState(format!("Failed to get DB connection: {}", e))
})?;
// Деактивировать все сертификаты
diesel::update(tls_certificates)
.set(is_active.eq(false))
.execute(&mut conn)
.await?;
// Активировать новый
diesel::update(tls_certificates.filter(id.eq(cert_id)))
.set(is_active.eq(true))
.execute(&mut conn)
.await?;
Ok(())
}
/// Получить список клиентов, которые ещё не отправили ACK
async fn get_missing_acks(&self, rotation_id: i32) -> Result<Vec<VerifyingKey>, RotationError> {
// TODO: Реализовать получение списка всех активных клиентов
// и вычитание тех, кто уже отправил ACK
// Пока возвращаем пустой список
Ok(Vec::new())
}
/// Очистить ACKs для данной ротации из БД
async fn clear_rotation_acks(&self, rotation_id: i32) -> Result<(), RotationError> {
use crate::db::schema::rotation_client_acks::dsl::*;
let mut conn = self.context.db.get().await.map_err(|e| {
RotationError::InvalidState(format!("Failed to get DB connection: {}", e))
})?;
diesel::delete(rotation_client_acks.filter(rotation_id.eq(rotation_id)))
.execute(&mut conn)
.await?;
Ok(())
}
/// Записать событие в audit trail
async fn log_rotation_event(
&self,
history_cert_id: i32,
history_event_type: &str,
history_details: Option<String>,
) -> Result<(), RotationError> {
use crate::db::schema::tls_rotation_history::dsl::*;
let mut conn = self.context.db.get().await.map_err(|e| {
RotationError::InvalidState(format!("Failed to get DB connection: {}", e))
})?;
let new_history = NewTlsRotationHistory {
cert_id: history_cert_id,
event_type: history_event_type.to_string(),
details: history_details,
};
diesel::insert_into(tls_rotation_history)
.values(&new_history)
.execute(&mut conn)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,139 @@
use chacha20poly1305::{
aead::{Aead, KeyInit},
ChaCha20Poly1305, Key, Nonce,
};
use super::CryptoError;
/// Encrypt plaintext with AEAD (ChaCha20Poly1305)
///
/// Returns (ciphertext, tag) on success
pub fn encrypt(
plaintext: &[u8],
key: &[u8; 32],
nonce: &[u8; 12],
) -> Result<Vec<u8>, CryptoError> {
let cipher_key = Key::from_slice(key);
let cipher = ChaCha20Poly1305::new(cipher_key);
let nonce_array = Nonce::from_slice(nonce);
cipher
.encrypt(nonce_array, plaintext)
.map_err(|e| CryptoError::AeadEncryption(e.to_string()))
}
/// Decrypt ciphertext with AEAD (ChaCha20Poly1305)
///
/// The ciphertext должен содержать tag (последние 16 bytes)
pub fn decrypt(
ciphertext_with_tag: &[u8],
key: &[u8; 32],
nonce: &[u8; 12],
) -> Result<Vec<u8>, CryptoError> {
let cipher_key = Key::from_slice(key);
let cipher = ChaCha20Poly1305::new(cipher_key);
let nonce_array = Nonce::from_slice(nonce);
cipher
.decrypt(nonce_array, ciphertext_with_tag)
.map_err(|e| CryptoError::AeadDecryption(e.to_string()))
}
/// Generate nonce from counter
///
/// Converts i32 counter to 12-byte nonce (big-endian encoding)
pub fn nonce_from_counter(counter: i32) -> [u8; 12] {
let mut nonce = [0u8; 12];
nonce[8..12].copy_from_slice(&counter.to_be_bytes());
nonce
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aead_encrypt_decrypt_round_trip() {
let plaintext = b"Hello, World! This is a secret message.";
let key = [42u8; 32];
let nonce = nonce_from_counter(1);
// Encrypt
let ciphertext = encrypt(plaintext, &key, &nonce).expect("Encryption failed");
// Verify ciphertext is different from plaintext
assert_ne!(ciphertext.as_slice(), plaintext);
// Decrypt
let decrypted = decrypt(&ciphertext, &key, &nonce).expect("Decryption failed");
// Verify round-trip
assert_eq!(decrypted.as_slice(), plaintext);
}
#[test]
fn test_aead_decrypt_with_wrong_key() {
let plaintext = b"Secret data";
let key = [1u8; 32];
let wrong_key = [2u8; 32];
let nonce = nonce_from_counter(1);
let ciphertext = encrypt(plaintext, &key, &nonce).expect("Encryption failed");
// Attempt decrypt with wrong key
let result = decrypt(&ciphertext, &wrong_key, &nonce);
// Should fail
assert!(result.is_err());
}
#[test]
fn test_aead_decrypt_with_wrong_nonce() {
let plaintext = b"Secret data";
let key = [1u8; 32];
let nonce = nonce_from_counter(1);
let wrong_nonce = nonce_from_counter(2);
let ciphertext = encrypt(plaintext, &key, &nonce).expect("Encryption failed");
// Attempt decrypt with wrong nonce
let result = decrypt(&ciphertext, &key, &wrong_nonce);
// Should fail
assert!(result.is_err());
}
#[test]
fn test_nonce_generation_from_counter() {
let nonce1 = nonce_from_counter(1);
let nonce2 = nonce_from_counter(2);
let nonce_max = nonce_from_counter(i32::MAX);
// Verify nonces are different
assert_ne!(nonce1, nonce2);
// Verify nonce format (first 8 bytes should be zero, last 4 contain counter)
assert_eq!(&nonce1[0..8], &[0u8; 8]);
assert_eq!(&nonce1[8..12], &1i32.to_be_bytes());
assert_eq!(&nonce_max[8..12], &i32::MAX.to_be_bytes());
}
#[test]
fn test_aead_tampered_ciphertext() {
let plaintext = b"Important message";
let key = [7u8; 32];
let nonce = nonce_from_counter(5);
let mut ciphertext = encrypt(plaintext, &key, &nonce).expect("Encryption failed");
// Tamper with ciphertext (flip a bit)
if let Some(byte) = ciphertext.get_mut(5) {
*byte ^= 0x01;
}
// Attempt decrypt - should fail due to authentication tag mismatch
let result = decrypt(&ciphertext, &key, &nonce);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,28 @@
pub mod aead;
pub mod root_key;
use miette::Diagnostic;
use thiserror::Error;
#[derive(Error, Debug, Diagnostic)]
pub enum CryptoError {
#[error("AEAD encryption failed: {0}")]
#[diagnostic(code(arbiter_server::crypto::aead_encryption))]
AeadEncryption(String),
#[error("AEAD decryption failed: {0}")]
#[diagnostic(code(arbiter_server::crypto::aead_decryption))]
AeadDecryption(String),
#[error("Key derivation failed: {0}")]
#[diagnostic(code(arbiter_server::crypto::key_derivation))]
KeyDerivation(String),
#[error("Invalid nonce: {0}")]
#[diagnostic(code(arbiter_server::crypto::invalid_nonce))]
InvalidNonce(String),
#[error("Invalid key format: {0}")]
#[diagnostic(code(arbiter_server::crypto::invalid_key))]
InvalidKey(String),
}

View File

@@ -0,0 +1,240 @@
use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2, PasswordHash, PasswordVerifier,
};
use crate::db::models::AeadEncrypted;
use super::{aead, CryptoError};
/// Encrypt root key with user password
///
/// Uses Argon2id for password derivation and ChaCha20Poly1305 for encryption
pub fn encrypt_root_key(
root_key: &[u8; 32],
password: &str,
nonce_counter: i32,
) -> Result<(AeadEncrypted, String), CryptoError> {
// Derive key from password using Argon2
let (derived_key, salt) = derive_key_from_password(password)?;
// Generate nonce from counter
let nonce = aead::nonce_from_counter(nonce_counter);
// Encrypt root key
let ciphertext_with_tag = aead::encrypt(root_key, &derived_key, &nonce)?;
// Extract tag (last 16 bytes)
let tag_start = ciphertext_with_tag
.len()
.checked_sub(16)
.ok_or_else(|| CryptoError::AeadEncryption("Ciphertext too short".into()))?;
let ciphertext = ciphertext_with_tag[..tag_start].to_vec();
let tag = ciphertext_with_tag[tag_start..].to_vec();
let aead_encrypted = AeadEncrypted {
id: 1, // Will be set by database
current_nonce: nonce_counter,
ciphertext,
tag,
schema_version: 1, // Current version
argon2_salt: Some(salt.clone()),
};
Ok((aead_encrypted, salt))
}
/// Decrypt root key with user password
///
/// Verifies password hash and decrypts using ChaCha20Poly1305
pub fn decrypt_root_key(
encrypted: &AeadEncrypted,
password: &str,
salt: &str,
) -> Result<[u8; 32], CryptoError> {
// Derive key from password using stored salt
let derived_key = derive_key_with_salt(password, salt)?;
// Generate nonce from counter
let nonce = aead::nonce_from_counter(encrypted.current_nonce);
// Reconstruct ciphertext with tag
let mut ciphertext_with_tag = encrypted.ciphertext.clone();
ciphertext_with_tag.extend_from_slice(&encrypted.tag);
// Decrypt
let plaintext = aead::decrypt(&ciphertext_with_tag, &derived_key, &nonce)?;
// Verify length
if plaintext.len() != 32 {
return Err(CryptoError::InvalidKey(format!(
"Expected 32 bytes, got {}",
plaintext.len()
)));
}
// Convert to fixed-size array
let mut root_key = [0u8; 32];
root_key.copy_from_slice(&plaintext);
Ok(root_key)
}
/// Derive 32-byte key from password using Argon2id
///
/// Generates new random salt and returns (derived_key, salt_string)
fn derive_key_from_password(password: &str) -> Result<([u8; 32], String), CryptoError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| CryptoError::KeyDerivation(e.to_string()))?;
// Extract hash output (32 bytes)
let hash_output = password_hash
.hash
.ok_or_else(|| CryptoError::KeyDerivation("No hash output".into()))?;
let hash_bytes = hash_output.as_bytes();
if hash_bytes.len() != 32 {
return Err(CryptoError::KeyDerivation(format!(
"Expected 32 bytes, got {}",
hash_bytes.len()
)));
}
let mut key = [0u8; 32];
key.copy_from_slice(hash_bytes);
Ok((key, salt.to_string()))
}
/// Derive 32-byte key from password using existing salt
fn derive_key_with_salt(password: &str, salt_str: &str) -> Result<[u8; 32], CryptoError> {
let argon2 = Argon2::default();
// Parse salt
let salt =
SaltString::from_b64(salt_str).map_err(|e| CryptoError::InvalidKey(e.to_string()))?;
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| CryptoError::KeyDerivation(e.to_string()))?;
// Extract hash output
let hash_output = password_hash
.hash
.ok_or_else(|| CryptoError::KeyDerivation("No hash output".into()))?;
let hash_bytes = hash_output.as_bytes();
if hash_bytes.len() != 32 {
return Err(CryptoError::KeyDerivation(format!(
"Expected 32 bytes, got {}",
hash_bytes.len()
)));
}
let mut key = [0u8; 32];
key.copy_from_slice(hash_bytes);
Ok(key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_root_key_encrypt_decrypt_round_trip() {
let root_key = [42u8; 32];
let password = "super_secret_password_123";
let nonce_counter = 1;
// Encrypt
let (encrypted, salt) =
encrypt_root_key(&root_key, password, nonce_counter).expect("Encryption failed");
// Verify structure
assert_eq!(encrypted.current_nonce, nonce_counter);
assert_eq!(encrypted.schema_version, 1);
assert_eq!(encrypted.tag.len(), 16); // AEAD tag size
// Decrypt
let decrypted =
decrypt_root_key(&encrypted, password, &salt).expect("Decryption failed");
// Verify round-trip
assert_eq!(decrypted, root_key);
}
#[test]
fn test_decrypt_with_wrong_password() {
let root_key = [99u8; 32];
let correct_password = "correct_password";
let wrong_password = "wrong_password";
let nonce_counter = 1;
// Encrypt with correct password
let (encrypted, salt) =
encrypt_root_key(&root_key, correct_password, nonce_counter).expect("Encryption failed");
// Attempt decrypt with wrong password
let result = decrypt_root_key(&encrypted, wrong_password, &salt);
// Should fail due to authentication tag mismatch
assert!(result.is_err());
}
#[test]
fn test_password_derivation_different_salts() {
let password = "same_password";
// Derive key twice - should produce different salts
let (key1, salt1) = derive_key_from_password(password).expect("Derivation 1 failed");
let (key2, salt2) = derive_key_from_password(password).expect("Derivation 2 failed");
// Salts should be different (randomly generated)
assert_ne!(salt1, salt2);
// Keys should be different (due to different salts)
assert_ne!(key1, key2);
}
#[test]
fn test_password_derivation_with_same_salt() {
let password = "test_password";
// Generate key and salt
let (key1, salt) = derive_key_from_password(password).expect("Derivation failed");
// Derive key again with same salt
let key2 = derive_key_with_salt(password, &salt).expect("Re-derivation failed");
// Keys should be identical
assert_eq!(key1, key2);
}
#[test]
fn test_different_nonce_produces_different_ciphertext() {
let root_key = [77u8; 32];
let password = "password123";
let (encrypted1, salt1) = encrypt_root_key(&root_key, password, 1).expect("Encryption 1 failed");
let (encrypted2, salt2) = encrypt_root_key(&root_key, password, 2).expect("Encryption 2 failed");
// Different nonces should produce different ciphertexts
assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);
// But both should decrypt correctly
let decrypted1 = decrypt_root_key(&encrypted1, password, &salt1).expect("Decryption 1 failed");
let decrypted2 = decrypt_root_key(&encrypted2, password, &salt2).expect("Decryption 2 failed");
assert_eq!(decrypted1, root_key);
assert_eq!(decrypted2, root_key);
}
}

View File

@@ -22,7 +22,7 @@ pub type DatabasePool = diesel_async::pooled_connection::bb8::Pool<DatabaseConne
pub type PoolInitError = diesel_async::pooled_connection::PoolError;
pub type PoolError = diesel_async::pooled_connection::bb8::RunError;
static DB_FILE: &'static str = "arbiter.sqlite";
static DB_FILE: &str = "arbiter.sqlite";
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");

View File

@@ -9,14 +9,15 @@ pub mod types {
pub struct SqliteTimestamp(DateTime<Utc>);
}
#[derive(Queryable, Debug, Insertable)]
#[derive(Queryable, Selectable, Debug, Insertable)]
#[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))]
pub struct AeadEncrypted {
pub id: i32,
pub current_nonce: i32,
pub ciphertext: Vec<u8>,
pub tag: Vec<u8>,
pub current_nonce: i32,
pub schema_version: i32,
pub argon2_salt: Option<String>,
}
#[derive(Queryable, Debug, Insertable)]
@@ -26,6 +27,7 @@ pub struct ArbiterSetting {
pub root_key_id: Option<i32>, // references aead_encrypted.id
pub cert_key: Vec<u8>,
pub cert: Vec<u8>,
pub current_cert_id: Option<i32>, // references tls_certificates.id
}
#[derive(Queryable, Debug)]
@@ -47,3 +49,70 @@ pub struct UseragentClient {
pub created_at: i32,
pub updated_at: i32,
}
// TLS Certificate Rotation Models
#[derive(Queryable, Debug, Insertable)]
#[diesel(table_name = schema::tls_certificates, check_for_backend(Sqlite))]
pub struct TlsCertificate {
pub id: i32,
pub cert: Vec<u8>,
pub cert_key: Vec<u8>,
pub not_before: i32,
pub not_after: i32,
pub created_at: i32,
pub is_active: bool,
}
#[derive(Insertable)]
#[diesel(table_name = schema::tls_certificates)]
pub struct NewTlsCertificate {
pub cert: Vec<u8>,
pub cert_key: Vec<u8>,
pub not_before: i32,
pub not_after: i32,
pub is_active: bool,
}
#[derive(Queryable, Debug, Insertable)]
#[diesel(table_name = schema::tls_rotation_state, check_for_backend(Sqlite))]
pub struct TlsRotationState {
pub id: i32,
pub state: String,
pub new_cert_id: Option<i32>,
pub initiated_at: Option<i32>,
pub timeout_at: Option<i32>,
}
#[derive(Queryable, Debug, Insertable)]
#[diesel(table_name = schema::rotation_client_acks, check_for_backend(Sqlite))]
pub struct RotationClientAck {
pub rotation_id: i32,
pub client_key: String,
pub ack_received_at: i32,
}
#[derive(Insertable)]
#[diesel(table_name = schema::rotation_client_acks)]
pub struct NewRotationClientAck {
pub rotation_id: i32,
pub client_key: String,
}
#[derive(Queryable, Debug, Insertable)]
#[diesel(table_name = schema::tls_rotation_history, check_for_backend(Sqlite))]
pub struct TlsRotationHistory {
pub id: i32,
pub cert_id: i32,
pub event_type: String,
pub timestamp: i32,
pub details: Option<String>,
}
#[derive(Insertable)]
#[diesel(table_name = schema::tls_rotation_history)]
pub struct NewTlsRotationHistory {
pub cert_id: i32,
pub event_type: String,
pub details: Option<String>,
}

View File

@@ -7,6 +7,7 @@ diesel::table! {
ciphertext -> Binary,
tag -> Binary,
schema_version -> Integer,
argon2_salt -> Nullable<Text>,
}
}
@@ -16,6 +17,7 @@ diesel::table! {
root_key_id -> Nullable<Integer>,
cert_key -> Binary,
cert -> Binary,
current_cert_id -> Nullable<Integer>,
}
}
@@ -39,11 +41,59 @@ diesel::table! {
}
}
diesel::table! {
tls_certificates (id) {
id -> Integer,
cert -> Binary,
cert_key -> Binary,
not_before -> Integer,
not_after -> Integer,
created_at -> Integer,
is_active -> Bool,
}
}
diesel::table! {
tls_rotation_state (id) {
id -> Integer,
state -> Text,
new_cert_id -> Nullable<Integer>,
initiated_at -> Nullable<Integer>,
timeout_at -> Nullable<Integer>,
}
}
diesel::table! {
rotation_client_acks (rotation_id, client_key) {
rotation_id -> Integer,
client_key -> Text,
ack_received_at -> Integer,
}
}
diesel::table! {
tls_rotation_history (id) {
id -> Integer,
cert_id -> Integer,
event_type -> Text,
timestamp -> Integer,
details -> Nullable<Text>,
}
}
diesel::joinable!(arbiter_settings -> aead_encrypted (root_key_id));
diesel::joinable!(arbiter_settings -> tls_certificates (current_cert_id));
diesel::joinable!(tls_rotation_state -> tls_certificates (new_cert_id));
diesel::joinable!(rotation_client_acks -> tls_certificates (rotation_id));
diesel::joinable!(tls_rotation_history -> tls_certificates (cert_id));
diesel::allow_tables_to_appear_in_same_query!(
aead_encrypted,
arbiter_settings,
program_client,
useragent_client,
tls_certificates,
tls_rotation_state,
rotation_client_acks,
tls_rotation_history,
);

View File

@@ -19,6 +19,7 @@ use crate::{
pub mod actors;
mod context;
mod crypto;
mod db;
mod errors;

View File

@@ -1,4 +1,48 @@
# cargo-vet audits file
[audits]
[[audits.test-log]]
who = "hdbg <httpdebugger@protonmail.com>"
criteria = "safe-to-deploy"
delta = "0.2.18 -> 0.2.19"
[[audits.test-log-macros]]
who = "hdbg <httpdebugger@protonmail.com>"
criteria = "safe-to-deploy"
delta = "0.2.18 -> 0.2.19"
[[trusted.h2]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-03-13"
end = "2027-02-14"
[[trusted.hashbrown]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2025-04-30"
end = "2027-02-14"
[[trusted.hyper-util]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2022-01-15"
end = "2027-02-14"
[[trusted.rustix]]
criteria = "safe-to-deploy"
user-id = 6825 # Dan Gohman (sunfishcode)
start = "2021-10-29"
end = "2027-02-14"
[[trusted.serde_json]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-02-28"
end = "2027-02-14"
[[trusted.syn]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-03-01"
end = "2027-02-14"

View File

@@ -3,3 +3,944 @@
[cargo-vet]
version = "0.10"
[imports.bytecode-alliance]
url = "https://raw.githubusercontent.com/bytecodealliance/wasmtime/main/supply-chain/audits.toml"
[imports.google]
url = "https://raw.githubusercontent.com/google/supply-chain/main/audits.toml"
[imports.mozilla]
url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml"
[[exemptions.addr2line]]
version = "0.25.1"
criteria = "safe-to-deploy"
[[exemptions.aho-corasick]]
version = "1.1.4"
criteria = "safe-to-deploy"
[[exemptions.anyhow]]
version = "1.0.101"
criteria = "safe-to-deploy"
[[exemptions.asn1-rs]]
version = "0.7.1"
criteria = "safe-to-deploy"
[[exemptions.asn1-rs-derive]]
version = "0.6.0"
criteria = "safe-to-deploy"
[[exemptions.asn1-rs-impl]]
version = "0.2.0"
criteria = "safe-to-deploy"
[[exemptions.async-trait]]
version = "0.1.89"
criteria = "safe-to-deploy"
[[exemptions.autocfg]]
version = "1.5.0"
criteria = "safe-to-deploy"
[[exemptions.aws-lc-rs]]
version = "1.15.4"
criteria = "safe-to-deploy"
[[exemptions.aws-lc-sys]]
version = "0.37.0"
criteria = "safe-to-deploy"
[[exemptions.axum]]
version = "0.8.8"
criteria = "safe-to-deploy"
[[exemptions.axum-core]]
version = "0.5.6"
criteria = "safe-to-deploy"
[[exemptions.backtrace]]
version = "0.3.76"
criteria = "safe-to-deploy"
[[exemptions.backtrace-ext]]
version = "0.2.1"
criteria = "safe-to-deploy"
[[exemptions.bb8]]
version = "0.9.1"
criteria = "safe-to-deploy"
[[exemptions.bitflags]]
version = "2.10.0"
criteria = "safe-to-deploy"
[[exemptions.block-buffer]]
version = "0.11.0"
criteria = "safe-to-deploy"
[[exemptions.bytes]]
version = "1.11.1"
criteria = "safe-to-deploy"
[[exemptions.cc]]
version = "1.2.55"
criteria = "safe-to-deploy"
[[exemptions.cfg-if]]
version = "1.0.4"
criteria = "safe-to-deploy"
[[exemptions.chacha20]]
version = "0.10.0"
criteria = "safe-to-deploy"
[[exemptions.chrono]]
version = "0.4.43"
criteria = "safe-to-deploy"
[[exemptions.cmake]]
version = "0.1.57"
criteria = "safe-to-deploy"
[[exemptions.cpufeatures]]
version = "0.2.17"
criteria = "safe-to-deploy"
[[exemptions.cpufeatures]]
version = "0.3.0"
criteria = "safe-to-deploy"
[[exemptions.crc32fast]]
version = "1.5.0"
criteria = "safe-to-deploy"
[[exemptions.crossbeam-utils]]
version = "0.8.21"
criteria = "safe-to-deploy"
[[exemptions.crypto-common]]
version = "0.2.0"
criteria = "safe-to-deploy"
[[exemptions.curve25519-dalek]]
version = "5.0.0-pre.6"
criteria = "safe-to-deploy"
[[exemptions.curve25519-dalek-derive]]
version = "0.1.1"
criteria = "safe-to-deploy"
[[exemptions.darling]]
version = "0.21.3"
criteria = "safe-to-deploy"
[[exemptions.darling_core]]
version = "0.21.3"
criteria = "safe-to-deploy"
[[exemptions.darling_macro]]
version = "0.21.3"
criteria = "safe-to-deploy"
[[exemptions.dashmap]]
version = "6.1.0"
criteria = "safe-to-deploy"
[[exemptions.data-encoding]]
version = "2.10.0"
criteria = "safe-to-deploy"
[[exemptions.der-parser]]
version = "10.0.0"
criteria = "safe-to-deploy"
[[exemptions.deranged]]
version = "0.5.5"
criteria = "safe-to-deploy"
[[exemptions.diesel]]
version = "2.3.6"
criteria = "safe-to-deploy"
[[exemptions.diesel-async]]
version = "0.7.4"
criteria = "safe-to-deploy"
[[exemptions.diesel_derives]]
version = "2.3.7"
criteria = "safe-to-deploy"
[[exemptions.diesel_migrations]]
version = "2.3.1"
criteria = "safe-to-deploy"
[[exemptions.diesel_table_macro_syntax]]
version = "0.3.0"
criteria = "safe-to-deploy"
[[exemptions.digest]]
version = "0.11.0-rc.11"
criteria = "safe-to-deploy"
[[exemptions.downcast-rs]]
version = "2.0.2"
criteria = "safe-to-deploy"
[[exemptions.dsl_auto_type]]
version = "0.2.0"
criteria = "safe-to-deploy"
[[exemptions.dunce]]
version = "1.0.5"
criteria = "safe-to-deploy"
[[exemptions.dyn-clone]]
version = "1.0.20"
criteria = "safe-to-deploy"
[[exemptions.ed25519]]
version = "3.0.0-rc.4"
criteria = "safe-to-deploy"
[[exemptions.ed25519-dalek]]
version = "3.0.0-pre.6"
criteria = "safe-to-deploy"
[[exemptions.errno]]
version = "0.3.14"
criteria = "safe-to-deploy"
[[exemptions.fiat-crypto]]
version = "0.3.0"
criteria = "safe-to-deploy"
[[exemptions.find-msvc-tools]]
version = "0.1.9"
criteria = "safe-to-deploy"
[[exemptions.fixedbitset]]
version = "0.5.7"
criteria = "safe-to-deploy"
[[exemptions.flate2]]
version = "1.1.9"
criteria = "safe-to-deploy"
[[exemptions.fs_extra]]
version = "1.3.0"
criteria = "safe-to-deploy"
[[exemptions.futures-task]]
version = "0.3.31"
criteria = "safe-to-deploy"
[[exemptions.futures-util]]
version = "0.3.31"
criteria = "safe-to-deploy"
[[exemptions.getrandom]]
version = "0.2.17"
criteria = "safe-to-deploy"
[[exemptions.getrandom]]
version = "0.3.4"
criteria = "safe-to-deploy"
[[exemptions.getrandom]]
version = "0.4.1"
criteria = "safe-to-deploy"
[[exemptions.hashbrown]]
version = "0.14.5"
criteria = "safe-to-deploy"
[[exemptions.http]]
version = "1.4.0"
criteria = "safe-to-deploy"
[[exemptions.http-body]]
version = "1.0.1"
criteria = "safe-to-deploy"
[[exemptions.http-body-util]]
version = "0.1.3"
criteria = "safe-to-deploy"
[[exemptions.httparse]]
version = "1.10.1"
criteria = "safe-to-deploy"
[[exemptions.hybrid-array]]
version = "0.4.7"
criteria = "safe-to-deploy"
[[exemptions.hyper]]
version = "1.8.1"
criteria = "safe-to-deploy"
[[exemptions.hyper-timeout]]
version = "0.5.2"
criteria = "safe-to-deploy"
[[exemptions.iana-time-zone]]
version = "0.1.65"
criteria = "safe-to-deploy"
[[exemptions.id-arena]]
version = "2.3.0"
criteria = "safe-to-deploy"
[[exemptions.ident_case]]
version = "1.0.1"
criteria = "safe-to-deploy"
[[exemptions.indexmap]]
version = "2.13.0"
criteria = "safe-to-deploy"
[[exemptions.is_ci]]
version = "1.2.0"
criteria = "safe-to-deploy"
[[exemptions.itertools]]
version = "0.14.0"
criteria = "safe-to-deploy"
[[exemptions.itoa]]
version = "1.0.17"
criteria = "safe-to-deploy"
[[exemptions.jobserver]]
version = "0.1.34"
criteria = "safe-to-deploy"
[[exemptions.js-sys]]
version = "0.3.85"
criteria = "safe-to-deploy"
[[exemptions.kameo]]
version = "0.19.2"
criteria = "safe-to-deploy"
[[exemptions.kameo_macros]]
version = "0.19.0"
criteria = "safe-to-deploy"
[[exemptions.libc]]
version = "0.2.181"
criteria = "safe-to-deploy"
[[exemptions.libsqlite3-sys]]
version = "0.35.0"
criteria = "safe-to-deploy"
[[exemptions.linux-raw-sys]]
version = "0.11.0"
criteria = "safe-to-deploy"
[[exemptions.lock_api]]
version = "0.4.14"
criteria = "safe-to-deploy"
[[exemptions.log]]
version = "0.4.29"
criteria = "safe-to-deploy"
[[exemptions.matchit]]
version = "0.8.4"
criteria = "safe-to-deploy"
[[exemptions.memchr]]
version = "2.8.0"
criteria = "safe-to-deploy"
[[exemptions.memsafe]]
version = "0.4.0"
criteria = "safe-to-deploy"
[[exemptions.miette]]
version = "7.6.0"
criteria = "safe-to-deploy"
[[exemptions.miette-derive]]
version = "7.6.0"
criteria = "safe-to-deploy"
[[exemptions.migrations_internals]]
version = "2.3.0"
criteria = "safe-to-deploy"
[[exemptions.migrations_macros]]
version = "2.3.0"
criteria = "safe-to-deploy"
[[exemptions.mime]]
version = "0.3.17"
criteria = "safe-to-deploy"
[[exemptions.minimal-lexical]]
version = "0.2.1"
criteria = "safe-to-deploy"
[[exemptions.mio]]
version = "1.1.1"
criteria = "safe-to-deploy"
[[exemptions.multimap]]
version = "0.10.1"
criteria = "safe-to-deploy"
[[exemptions.num-bigint]]
version = "0.4.6"
criteria = "safe-to-deploy"
[[exemptions.num-conv]]
version = "0.2.0"
criteria = "safe-to-deploy"
[[exemptions.object]]
version = "0.37.3"
criteria = "safe-to-deploy"
[[exemptions.oid-registry]]
version = "0.8.1"
criteria = "safe-to-deploy"
[[exemptions.once_cell]]
version = "1.21.3"
criteria = "safe-to-deploy"
[[exemptions.owo-colors]]
version = "4.2.3"
criteria = "safe-to-deploy"
[[exemptions.parking_lot]]
version = "0.12.5"
criteria = "safe-to-deploy"
[[exemptions.parking_lot_core]]
version = "0.9.12"
criteria = "safe-to-deploy"
[[exemptions.pem]]
version = "3.0.6"
criteria = "safe-to-deploy"
[[exemptions.petgraph]]
version = "0.8.3"
criteria = "safe-to-deploy"
[[exemptions.pin-project]]
version = "1.1.10"
criteria = "safe-to-deploy"
[[exemptions.pin-project-internal]]
version = "1.1.10"
criteria = "safe-to-deploy"
[[exemptions.portable-atomic]]
version = "1.13.1"
criteria = "safe-to-deploy"
[[exemptions.prettyplease]]
version = "0.2.37"
criteria = "safe-to-deploy"
[[exemptions.proc-macro2]]
version = "1.0.106"
criteria = "safe-to-deploy"
[[exemptions.prost]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.prost-build]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.prost-derive]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.prost-types]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.pulldown-cmark]]
version = "0.13.0"
criteria = "safe-to-deploy"
[[exemptions.pulldown-cmark-to-cmark]]
version = "22.0.0"
criteria = "safe-to-deploy"
[[exemptions.quote]]
version = "1.0.44"
criteria = "safe-to-deploy"
[[exemptions.r-efi]]
version = "5.3.0"
criteria = "safe-to-deploy"
[[exemptions.rand]]
version = "0.10.0"
criteria = "safe-to-deploy"
[[exemptions.rand_core]]
version = "0.10.0"
criteria = "safe-to-deploy"
[[exemptions.rcgen]]
version = "0.14.7"
criteria = "safe-to-deploy"
[[exemptions.redox_syscall]]
version = "0.5.18"
criteria = "safe-to-deploy"
[[exemptions.regex]]
version = "1.12.3"
criteria = "safe-to-deploy"
[[exemptions.regex-automata]]
version = "0.4.14"
criteria = "safe-to-deploy"
[[exemptions.regex-syntax]]
version = "0.8.9"
criteria = "safe-to-deploy"
[[exemptions.ring]]
version = "0.17.14"
criteria = "safe-to-deploy"
[[exemptions.rsqlite-vfs]]
version = "0.1.0"
criteria = "safe-to-deploy"
[[exemptions.rustc-demangle]]
version = "0.1.27"
criteria = "safe-to-deploy"
[[exemptions.rustc_version]]
version = "0.4.1"
criteria = "safe-to-deploy"
[[exemptions.rusticata-macros]]
version = "4.1.0"
criteria = "safe-to-deploy"
[[exemptions.rustls]]
version = "0.23.36"
criteria = "safe-to-deploy"
[[exemptions.rustls-pki-types]]
version = "1.14.0"
criteria = "safe-to-deploy"
[[exemptions.rustls-webpki]]
version = "0.103.9"
criteria = "safe-to-deploy"
[[exemptions.rustversion]]
version = "1.0.22"
criteria = "safe-to-deploy"
[[exemptions.scoped-futures]]
version = "0.1.4"
criteria = "safe-to-deploy"
[[exemptions.scopeguard]]
version = "1.2.0"
criteria = "safe-to-deploy"
[[exemptions.secrecy]]
version = "0.10.3"
criteria = "safe-to-deploy"
[[exemptions.semver]]
version = "1.0.27"
criteria = "safe-to-deploy"
[[exemptions.serde]]
version = "1.0.228"
criteria = "safe-to-deploy"
[[exemptions.serde_core]]
version = "1.0.228"
criteria = "safe-to-deploy"
[[exemptions.serde_derive]]
version = "1.0.228"
criteria = "safe-to-deploy"
[[exemptions.sha2]]
version = "0.11.0-rc.5"
criteria = "safe-to-deploy"
[[exemptions.signal-hook-registry]]
version = "1.4.8"
criteria = "safe-to-deploy"
[[exemptions.signature]]
version = "3.0.0-rc.10"
criteria = "safe-to-deploy"
[[exemptions.simd-adler32]]
version = "0.3.8"
criteria = "safe-to-deploy"
[[exemptions.slab]]
version = "0.4.12"
criteria = "safe-to-deploy"
[[exemptions.smlang]]
version = "0.8.0"
criteria = "safe-to-deploy"
[[exemptions.smlang-macros]]
version = "0.8.0"
criteria = "safe-to-deploy"
[[exemptions.socket2]]
version = "0.6.2"
criteria = "safe-to-deploy"
[[exemptions.sqlite-wasm-rs]]
version = "0.5.2"
criteria = "safe-to-deploy"
[[exemptions.string_morph]]
version = "0.1.0"
criteria = "safe-to-deploy"
[[exemptions.subtle]]
version = "2.6.1"
criteria = "safe-to-deploy"
[[exemptions.supports-color]]
version = "3.0.2"
criteria = "safe-to-deploy"
[[exemptions.supports-hyperlinks]]
version = "3.2.0"
criteria = "safe-to-deploy"
[[exemptions.supports-unicode]]
version = "3.0.0"
criteria = "safe-to-deploy"
[[exemptions.sync_wrapper]]
version = "1.0.2"
criteria = "safe-to-deploy"
[[exemptions.tempfile]]
version = "3.25.0"
criteria = "safe-to-deploy"
[[exemptions.terminal_size]]
version = "0.4.3"
criteria = "safe-to-deploy"
[[exemptions.thiserror]]
version = "2.0.18"
criteria = "safe-to-deploy"
[[exemptions.thiserror-impl]]
version = "2.0.18"
criteria = "safe-to-deploy"
[[exemptions.thread_local]]
version = "1.1.9"
criteria = "safe-to-run"
[[exemptions.time]]
version = "0.3.47"
criteria = "safe-to-deploy"
[[exemptions.time-core]]
version = "0.1.8"
criteria = "safe-to-deploy"
[[exemptions.time-macros]]
version = "0.2.27"
criteria = "safe-to-deploy"
[[exemptions.tokio]]
version = "1.49.0"
criteria = "safe-to-deploy"
[[exemptions.tokio-macros]]
version = "2.6.0"
criteria = "safe-to-deploy"
[[exemptions.tokio-rustls]]
version = "0.26.4"
criteria = "safe-to-deploy"
[[exemptions.tokio-stream]]
version = "0.1.18"
criteria = "safe-to-deploy"
[[exemptions.tokio-util]]
version = "0.7.18"
criteria = "safe-to-deploy"
[[exemptions.toml]]
version = "0.9.11+spec-1.1.0"
criteria = "safe-to-deploy"
[[exemptions.toml_parser]]
version = "1.0.6+spec-1.1.0"
criteria = "safe-to-deploy"
[[exemptions.tonic]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.tonic-build]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.tonic-prost]]
version = "0.14.4"
criteria = "safe-to-deploy"
[[exemptions.tonic-prost-build]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.tower]]
version = "0.5.3"
criteria = "safe-to-deploy"
[[exemptions.tower-layer]]
version = "0.3.3"
criteria = "safe-to-deploy"
[[exemptions.tower-service]]
version = "0.3.3"
criteria = "safe-to-deploy"
[[exemptions.tracing]]
version = "0.1.44"
criteria = "safe-to-deploy"
[[exemptions.tracing-attributes]]
version = "0.1.31"
criteria = "safe-to-deploy"
[[exemptions.tracing-core]]
version = "0.1.36"
criteria = "safe-to-deploy"
[[exemptions.tracing-subscriber]]
version = "0.3.22"
criteria = "safe-to-run"
[[exemptions.try-lock]]
version = "0.2.5"
criteria = "safe-to-deploy"
[[exemptions.typenum]]
version = "1.19.0"
criteria = "safe-to-deploy"
[[exemptions.unicase]]
version = "2.9.0"
criteria = "safe-to-deploy"
[[exemptions.unicode-ident]]
version = "1.0.23"
criteria = "safe-to-deploy"
[[exemptions.untrusted]]
version = "0.7.1"
criteria = "safe-to-deploy"
[[exemptions.untrusted]]
version = "0.9.0"
criteria = "safe-to-deploy"
[[exemptions.uuid]]
version = "1.20.0"
criteria = "safe-to-deploy"
[[exemptions.want]]
version = "0.3.1"
criteria = "safe-to-deploy"
[[exemptions.wasi]]
version = "0.11.1+wasi-snapshot-preview1"
criteria = "safe-to-deploy"
[[exemptions.wasm-bindgen]]
version = "0.2.108"
criteria = "safe-to-deploy"
[[exemptions.wasm-bindgen-macro]]
version = "0.2.108"
criteria = "safe-to-deploy"
[[exemptions.wasm-bindgen-macro-support]]
version = "0.2.108"
criteria = "safe-to-deploy"
[[exemptions.wasm-bindgen-shared]]
version = "0.2.108"
criteria = "safe-to-deploy"
[[exemptions.winapi]]
version = "0.3.9"
criteria = "safe-to-deploy"
[[exemptions.winapi-i686-pc-windows-gnu]]
version = "0.4.0"
criteria = "safe-to-deploy"
[[exemptions.winapi-x86_64-pc-windows-gnu]]
version = "0.4.0"
criteria = "safe-to-deploy"
[[exemptions.windows-core]]
version = "0.62.2"
criteria = "safe-to-deploy"
[[exemptions.windows-implement]]
version = "0.60.2"
criteria = "safe-to-deploy"
[[exemptions.windows-interface]]
version = "0.59.3"
criteria = "safe-to-deploy"
[[exemptions.windows-link]]
version = "0.2.1"
criteria = "safe-to-deploy"
[[exemptions.windows-result]]
version = "0.4.1"
criteria = "safe-to-deploy"
[[exemptions.windows-strings]]
version = "0.5.1"
criteria = "safe-to-deploy"
[[exemptions.windows-sys]]
version = "0.52.0"
criteria = "safe-to-deploy"
[[exemptions.windows-sys]]
version = "0.60.2"
criteria = "safe-to-deploy"
[[exemptions.windows-sys]]
version = "0.61.2"
criteria = "safe-to-deploy"
[[exemptions.windows-targets]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows-targets]]
version = "0.53.5"
criteria = "safe-to-deploy"
[[exemptions.windows_aarch64_gnullvm]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_aarch64_gnullvm]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_aarch64_msvc]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_aarch64_msvc]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_gnu]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_gnu]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_gnullvm]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_gnullvm]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_msvc]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_msvc]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_gnu]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_gnu]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_gnullvm]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_gnullvm]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_msvc]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_msvc]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.winnow]]
version = "0.7.14"
criteria = "safe-to-deploy"
[[exemptions.x509-parser]]
version = "0.18.1"
criteria = "safe-to-deploy"
[[exemptions.yasna]]
version = "0.5.2"
criteria = "safe-to-deploy"
[[exemptions.zeroize]]
version = "1.8.2"
criteria = "safe-to-deploy"
[[exemptions.zmij]]
version = "1.0.20"
criteria = "safe-to-deploy"
[[exemptions.zstd]]
version = "0.13.3"
criteria = "safe-to-deploy"
[[exemptions.zstd-safe]]
version = "7.2.4"
criteria = "safe-to-deploy"
[[exemptions.zstd-sys]]
version = "2.0.16+zstd.1.5.7"
criteria = "safe-to-deploy"

File diff suppressed because it is too large Load Diff