diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 205df23..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "git.enabled": false -} \ No newline at end of file diff --git a/mise.toml b/mise.toml index ae98d2d..7391189 100644 --- a/mise.toml +++ b/mise.toml @@ -5,7 +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" diff --git a/protobufs/arbiter.proto b/protobufs/arbiter.proto index 67bf6d0..ad58016 100644 --- a/protobufs/arbiter.proto +++ b/protobufs/arbiter.proto @@ -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); diff --git a/protobufs/google/protobuf/timestamp.proto b/protobufs/google/protobuf/timestamp.proto new file mode 100644 index 0000000..7ce2ae8 --- /dev/null +++ b/protobufs/google/protobuf/timestamp.proto @@ -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; +} diff --git a/server/Cargo.lock b/server/Cargo.lock index 76c1855..7d49aea 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -37,9 +47,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbiter-client" @@ -53,7 +63,9 @@ dependencies = [ "hex", "kameo", "prost", + "prost-build", "prost-types", + "serde_json", "tokio", "tonic", "tonic-prost", @@ -65,7 +77,9 @@ name = "arbiter-server" version = "0.1.0" dependencies = [ "arbiter-proto", + "argon2", "async-trait", + "chacha20poly1305", "chrono", "dashmap", "diesel", @@ -73,6 +87,7 @@ dependencies = [ "diesel_migrations", "ed25519-dalek", "futures", + "hex", "kameo", "memsafe", "miette", @@ -94,6 +109,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" @@ -118,7 +145,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -130,7 +157,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -141,7 +168,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -158,9 +185,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.4" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -169,9 +196,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -252,6 +279,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bb8" version = "0.9.1" @@ -266,24 +299,42 @@ 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.11.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ "hybrid-array", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" @@ -293,9 +344,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", @@ -309,6 +360,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.0" @@ -317,14 +379,27 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", ] [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -334,6 +409,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + [[package]] name = "cmake" version = "0.1.57" @@ -384,9 +470,20 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.2.0" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "hybrid-array", ] @@ -400,7 +497,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.11.1", "fiat-crypto", "rustc_version", "subtle", @@ -415,7 +512,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -439,7 +536,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -450,7 +547,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -489,9 +586,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -537,7 +634,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -557,17 +654,28 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" dependencies = [ - "syn 2.0.114", + "syn 2.0.117", ] [[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", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "285743a676ccb6b3e116bc14cc69319b957867930ae9c4822f8e0f54509d7243" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", ] [[package]] @@ -578,7 +686,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -598,7 +706,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -630,7 +738,7 @@ checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.10.0", "sha2", "subtle", "zeroize", @@ -718,9 +826,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", @@ -733,9 +841,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", @@ -743,15 +851,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", @@ -760,38 +868,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.117", ] [[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", @@ -801,10 +909,19 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -824,20 +941,20 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", - "rand_core", + "r-efi 6.0.0", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -950,9 +1067,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" dependencies = [ "typenum", ] @@ -1061,6 +1178,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -1094,9 +1220,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1126,7 +1252,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1143,9 +1269,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libsqlite3-sys" @@ -1159,9 +1285,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -1237,7 +1363,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1369,10 +1495,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "owo-colors" -version = "4.2.3" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "parking_lot" @@ -1397,6 +1529,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" @@ -1426,29 +1569,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1462,6 +1605,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1481,7 +1635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1520,7 +1674,7 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn 2.0.114", + "syn 2.0.117", "tempfile", ] @@ -1534,7 +1688,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1549,9 +1703,9 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" dependencies = [ "bitflags", "memchr", @@ -1569,9 +1723,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1582,15 +1736,30 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "chacha20", - "getrandom 0.4.1", - "rand_core", + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1648,9 +1817,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ring" @@ -1702,9 +1871,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -1715,9 +1884,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -1812,7 +1981,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1845,7 +2014,7 @@ checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.11.1", ] [[package]] @@ -1920,12 +2089,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1992,9 +2161,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2015,17 +2184,17 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2059,7 +2228,7 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2089,7 +2258,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2134,9 +2303,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -2152,13 +2321,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2198,9 +2367,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", @@ -2220,18 +2389,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] [[package]] name = "tonic" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", "axum", @@ -2261,21 +2430,21 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27aac809edf60b741e2d7db6367214d078856b8a5bff0087e94ff330fb97b6fc" +checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734" dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "tonic-prost" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f86539c0089bfd09b1f8c0ab0239d80392af74c21bc9e0f15e1b4aca4c1647f" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" dependencies = [ "bytes", "prost", @@ -2284,16 +2453,16 @@ dependencies = [ [[package]] name = "tonic-prost-build" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4556786613791cfef4ed134aa670b61a85cfcacf71543ef33e8d801abae988f" +checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "prost-types", "quote", - "syn 2.0.114", + "syn 2.0.117", "tempfile", "tonic-build", ] @@ -2348,7 +2517,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2395,9 +2564,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" @@ -2423,6 +2592,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -2437,9 +2616,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "uuid" -version = "1.20.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "js-sys", "wasm-bindgen", @@ -2451,6 +2630,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -2486,9 +2671,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2499,9 +2684,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2509,22 +2694,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -2606,7 +2791,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2617,7 +2802,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2802,9 +2987,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "wit-bindgen" @@ -2836,7 +3021,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.114", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2852,7 +3037,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2929,9 +3114,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" diff --git a/server/crates/arbiter-proto/Cargo.toml b/server/crates/arbiter-proto/Cargo.toml index dd2815b..473a22d 100644 --- a/server/crates/arbiter-proto/Cargo.toml +++ b/server/crates/arbiter-proto/Cargo.toml @@ -15,6 +15,8 @@ kameo.workspace = true prost-types.workspace = true [build-dependencies] +prost-build = "0.14.3" +serde_json = "1" tonic-prost-build = "0.14.3" diff --git a/server/crates/arbiter-proto/build.rs b/server/crates/arbiter-proto/build.rs index 77136cb..130845b 100644 --- a/server/crates/arbiter-proto/build.rs +++ b/server/crates/arbiter-proto/build.rs @@ -1,18 +1,15 @@ -use tonic_prost_build::configure; - static PROTOBUF_DIR: &str = "../../../protobufs"; fn main() -> Result<(), Box> { - configure() + 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( - &[ - format!("{}/arbiter.proto", PROTOBUF_DIR), - format!("{}/auth.proto", PROTOBUF_DIR), - ], - &[PROTOBUF_DIR.to_string()], - ) - - .unwrap(); + .compile_protos(&proto_files, &[PROTOBUF_DIR.to_string()])?; + Ok(()) } diff --git a/server/crates/arbiter-proto/src/lib.rs b/server/crates/arbiter-proto/src/lib.rs index bce8e36..a738e8f 100644 --- a/server/crates/arbiter-proto/src/lib.rs +++ b/server/crates/arbiter-proto/src/lib.rs @@ -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 { - 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", diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index 5cfe75f..fc950d5 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -43,7 +43,10 @@ rcgen = { version = "0.14.7", features = [ chrono.workspace = true memsafe = "0.4.0" zeroize = { version = "1.8.2", features = ["std", "simd"] } +argon2 = { version = "0.5", features = ["std"] } kameo.workspace = true +hex = "0.4.3" +chacha20poly1305 = "0.10.1" [dev-dependencies] test-log = { version = "0.2", default-features = false, features = ["trace"] } diff --git a/server/crates/arbiter-server/migrations/2026-02-13-155546-0000_add_tls_rotation/down.sql b/server/crates/arbiter-server/migrations/2026-02-13-155546-0000_add_tls_rotation/down.sql new file mode 100644 index 0000000..0c68996 --- /dev/null +++ b/server/crates/arbiter-server/migrations/2026-02-13-155546-0000_add_tls_rotation/down.sql @@ -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; diff --git a/server/crates/arbiter-server/migrations/2026-02-13-155546-0000_add_tls_rotation/up.sql b/server/crates/arbiter-server/migrations/2026-02-13-155546-0000_add_tls_rotation/up.sql new file mode 100644 index 0000000..5cc22e1 --- /dev/null +++ b/server/crates/arbiter-server/migrations/2026-02-13-155546-0000_add_tls_rotation/up.sql @@ -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; diff --git a/server/crates/arbiter-server/src/actors/user_agent.rs b/server/crates/arbiter-server/src/actors/user_agent.rs index 0b98e5b..78f7086 100644 --- a/server/crates/arbiter-server/src/actors/user_agent.rs +++ b/server/crates/arbiter-server/src/actors/user_agent.rs @@ -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) -> 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; @@ -348,7 +353,7 @@ mod tests { }) .await .expect("Shouldn't fail to send message"); - + // auth succeeded assert_eq!( result, diff --git a/server/crates/arbiter-server/src/context.rs b/server/crates/arbiter-server/src/context.rs index 6da6a80..7f7dfc3 100644 --- a/server/crates/arbiter-server/src/context.rs +++ b/server/crates/arbiter-server/src/context.rs @@ -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 _; @@ -8,13 +10,13 @@ use miette::Diagnostic; use rand::rngs::StdRng; use smlang::statemachine; use thiserror::Error; -use tokio::sync::RwLock; +use tokio::sync::{watch, RwLock}; use crate::{ context::{ bootstrap::{BootstrapActor, generate_token}, lease::LeaseHandler, - tls::{TlsDataRaw, TlsManager}, + tls::{RotationState, RotationTask, TlsDataRaw, TlsManager}, }, db::{ self, @@ -82,8 +84,12 @@ pub(crate) struct _ServerContextInner { pub db: db::DatabasePool, pub state: RwLock>, pub rng: StdRng, - pub tls: TlsManager, + pub tls: Arc, pub bootstrapper: ActorRef, + pub rotation_state: RwLock, + pub rotation_acks: Arc>>, + pub user_agent_leases: LeaseHandler, + pub client_leases: LeaseHandler, } #[derive(Clone)] pub(crate) struct ServerContext(Arc<_ServerContextInner>); @@ -97,34 +103,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 { - 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 +159,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 +180,24 @@ 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)) } } diff --git a/server/crates/arbiter-server/src/context/lease.rs b/server/crates/arbiter-server/src/context/lease.rs index 2b0f2bd..99f153e 100644 --- a/server/crates/arbiter-server/src/context/lease.rs +++ b/server/crates/arbiter-server/src/context/lease.rs @@ -38,4 +38,9 @@ impl LeaseHandler { Err(()) } } + + /// Get all currently leased items + pub fn get_all(&self) -> Vec { + self.storage.0.iter().map(|entry| entry.clone()).collect() + } } diff --git a/server/crates/arbiter-server/src/context/tls.rs b/server/crates/arbiter-server/src/context/tls.rs deleted file mode 100644 index ce9b1b4..0000000 --- a/server/crates/arbiter-server/src/context/tls.rs +++ /dev/null @@ -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, - pub key: Vec, -} -impl TlsDataRaw { - pub fn serialize(cert: &TlsData) -> Self { - Self { - cert: cert.cert.as_ref().to_vec(), - key: cert.keypair.serialize_pem().as_bytes().to_vec(), - } - } - - pub fn deserialize(&self) -> Result { - let cert = CertificateDer::from_slice(&self.cert).into_owned(); - - let key = - String::from_utf8(self.key.clone()).map_err(TlsInitError::KeyInvalidFormat)?; - - let keypair = KeyPair::from_pem(&key).map_err(TlsInitError::KeyDeserializationError)?; - - Ok(TlsData { cert, keypair }) - } -} - -fn generate_cert(key: &KeyPair) -> Result { - let params = rcgen::CertificateParams::new(vec![ - "arbiter.local".to_string(), - "localhost".to_string(), - ])?; - - params.self_signed(key) -} - -// TODO: Implement cert rotation -pub(crate) struct TlsManager { - data: TlsData, -} - -impl TlsManager { - pub async fn new(data: Option) -> Result { - match data { - Some(raw) => { - let tls_data = raw.deserialize()?; - Ok(Self { data: tls_data }) - } - None => { - let keypair = KeyPair::generate()?; - let cert = generate_cert(&keypair)?; - let tls_data = TlsData { - cert: cert.der().clone(), - keypair, - }; - Ok(Self { data: tls_data }) - } - } - } - - pub fn bytes(&self) -> TlsDataRaw { - TlsDataRaw::serialize(&self.data) - } -} diff --git a/server/crates/arbiter-server/src/context/tls/mod.rs b/server/crates/arbiter-server/src/context/tls/mod.rs new file mode 100644 index 0000000..609544b --- /dev/null +++ b/server/crates/arbiter-server/src/context/tls/mod.rs @@ -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, + pub key: Vec, +} +impl TlsDataRaw { + pub fn serialize(cert: &TlsData) -> Self { + Self { + cert: cert.cert.as_ref().to_vec(), + key: cert.keypair.serialize_pem().as_bytes().to_vec(), + } + } + + pub fn deserialize(&self) -> Result { + let cert = CertificateDer::from_slice(&self.cert).into_owned(); + + let key = + String::from_utf8(self.key.clone()).map_err(TlsInitError::KeyInvalidFormat)?; + + let keypair = KeyPair::from_pem(&key).map_err(TlsInitError::KeyDeserializationError)?; + + Ok(TlsData { cert, keypair }) + } +} + +/// Metadata about a certificate including validity period +pub struct CertificateMetadata { + pub cert_id: i32, + pub cert: CertificateDer<'static>, + pub keypair: Arc, + 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>, + + // Database pool for persistence + db: db::DatabasePool, +} + +impl TlsManager { + /// Create new TlsManager with a generated certificate + pub async fn new(db: db::DatabasePool) -> Result { + 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 { + // 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 { + 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) { + 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![], + } + } +} diff --git a/server/crates/arbiter-server/src/context/tls/rotation.rs b/server/crates/arbiter-server/src/context/tls/rotation.rs new file mode 100644 index 0000000..2dff081 --- /dev/null +++ b/server/crates/arbiter-server/src/context/tls/rotation.rs @@ -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 { + 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, Option, Option) = + 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, + check_interval: Duration, + rotation_threshold: Duration, + ack_timeout: Duration, + shutdown_rx: watch::Receiver, +} + +impl RotationTask { + /// Создать новый rotation task + pub fn new( + context: Arc, + check_interval: Duration, + rotation_threshold: Duration, + ack_timeout: Duration, + shutdown_rx: watch::Receiver, + ) -> 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 { + 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 { + 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::( + "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 { + 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, Vec, 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, 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, + ) -> 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(()) + } +} diff --git a/server/crates/arbiter-server/src/crypto/aead.rs b/server/crates/arbiter-server/src/crypto/aead.rs new file mode 100644 index 0000000..025c166 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/aead.rs @@ -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, 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, 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()); + } +} diff --git a/server/crates/arbiter-server/src/crypto/mod.rs b/server/crates/arbiter-server/src/crypto/mod.rs new file mode 100644 index 0000000..d33f0b1 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/mod.rs @@ -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), +} diff --git a/server/crates/arbiter-server/src/crypto/root_key.rs b/server/crates/arbiter-server/src/crypto/root_key.rs new file mode 100644 index 0000000..187bd91 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/root_key.rs @@ -0,0 +1,239 @@ +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 + }; + + 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); + } +} diff --git a/server/crates/arbiter-server/src/db.rs b/server/crates/arbiter-server/src/db.rs index c44d489..9df963c 100644 --- a/server/crates/arbiter-server/src/db.rs +++ b/server/crates/arbiter-server/src/db.rs @@ -22,7 +22,7 @@ pub type DatabasePool = diesel_async::pooled_connection::bb8::Pool, // references aead_encrypted.id pub cert_key: Vec, pub cert: Vec, + pub current_cert_id: Option, // references tls_certificates.id } #[derive(Queryable, Debug)] @@ -47,3 +48,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, + pub cert_key: Vec, + 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, + pub cert_key: Vec, + 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, + pub initiated_at: Option, + pub timeout_at: Option, +} + +#[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, +} + +#[derive(Insertable)] +#[diesel(table_name = schema::tls_rotation_history)] +pub struct NewTlsRotationHistory { + pub cert_id: i32, + pub event_type: String, + pub details: Option, +} diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 38f8afe..f560547 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -16,6 +16,7 @@ diesel::table! { root_key_id -> Nullable, cert_key -> Binary, cert -> Binary, + current_cert_id -> Nullable, } } @@ -39,11 +40,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, + initiated_at -> Nullable, + timeout_at -> Nullable, + } +} + +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, + } +} + 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, ); diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index 921bb8e..57f8a64 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -19,6 +19,7 @@ use crate::{ pub mod actors; mod context; +mod crypto; mod db; mod errors;