diff --git a/.woodpecker/server-lint.yaml b/.woodpecker/server-lint.yaml new file mode 100644 index 0000000..f17bead --- /dev/null +++ b/.woodpecker/server-lint.yaml @@ -0,0 +1,24 @@ +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 + - mise install rust + - mise exec rust -- cargo clippy --all-targets --all-features -- -D warnings \ No newline at end of file diff --git a/mise.lock b/mise.lock index b54380c..847643a 100644 --- a/mise.lock +++ b/mise.lock @@ -10,6 +10,10 @@ backend = "cargo:cargo-features" version = "0.11.1" backend = "cargo:cargo-features-manager" +[[tools."cargo:cargo-insta"]] +version = "1.46.3" +backend = "cargo:cargo-insta" + [[tools."cargo:cargo-nextest"]] version = "0.9.126" backend = "cargo:cargo-nextest" diff --git a/mise.toml b/mise.toml index ae98d2d..22adad3 100644 --- a/mise.toml +++ b/mise.toml @@ -9,3 +9,4 @@ rust = "1.93.0" "cargo:cargo-features-manager" = "0.11.1" "cargo:cargo-nextest" = "0.9.126" "cargo:cargo-shear" = "latest" +"cargo:cargo-insta" = "1.46.3" diff --git a/protobufs/arbiter.proto b/protobufs/arbiter.proto index 67bf6d0..9d991c5 100644 --- a/protobufs/arbiter.proto +++ b/protobufs/arbiter.proto @@ -3,29 +3,8 @@ syntax = "proto3"; package arbiter; import "auth.proto"; - -message ClientRequest { - oneof payload { - arbiter.auth.ClientMessage auth_message = 1; - } -} - -message ClientResponse { - oneof payload { - arbiter.auth.ServerMessage auth_message = 1; - } -} - -message UserAgentRequest { - oneof payload { - arbiter.auth.ClientMessage auth_message = 1; - } -} -message UserAgentResponse { - oneof payload { - arbiter.auth.ServerMessage auth_message = 1; - } -} +import "client.proto"; +import "user_agent.proto"; message ServerInfo { string version = 1; diff --git a/protobufs/client.proto b/protobufs/client.proto new file mode 100644 index 0000000..ebe0422 --- /dev/null +++ b/protobufs/client.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package arbiter; + +import "auth.proto"; + +message ClientRequest { + oneof payload { + arbiter.auth.ClientMessage auth_message = 1; + } +} + +message ClientResponse { + oneof payload { + arbiter.auth.ServerMessage auth_message = 1; + } +} diff --git a/protobufs/unseal.proto b/protobufs/unseal.proto index 9ba0837..8c006a0 100644 --- a/protobufs/unseal.proto +++ b/protobufs/unseal.proto @@ -2,13 +2,24 @@ syntax = "proto3"; package arbiter.unseal; -message UserAgentKeyRequest {} +import "google/protobuf/empty.proto"; -message ServerKeyResponse { - bytes pubkey = 1; +message UnsealStart { + bytes client_pubkey = 1; } -message UserAgentSealedKey { - bytes sealed_key = 1; - bytes pubkey = 2; - bytes nonce = 3; + +message UnsealStartResponse { + bytes server_pubkey = 1; +} +message UnsealEncryptedKey { + bytes nonce = 1; + bytes ciphertext = 2; + bytes associated_data = 3; +} + +enum UnsealResult { + UNSEAL_RESULT_UNSPECIFIED = 0; + UNSEAL_RESULT_SUCCESS = 1; + UNSEAL_RESULT_INVALID_KEY = 2; + UNSEAL_RESULT_UNBOOTSTRAPPED = 3; } diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto new file mode 100644 index 0000000..a3d7fd5 --- /dev/null +++ b/protobufs/user_agent.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package arbiter; + +import "auth.proto"; +import "unseal.proto"; + +message UserAgentRequest { + oneof payload { + arbiter.auth.ClientMessage auth_message = 1; + arbiter.unseal.UnsealStart unseal_start = 2; + arbiter.unseal.UnsealEncryptedKey unseal_encrypted_key = 3; + } +} +message UserAgentResponse { + oneof payload { + arbiter.auth.ServerMessage auth_message = 1; + arbiter.unseal.UnsealStartResponse unseal_start_response = 2; + arbiter.unseal.UnsealResult unseal_result = 3; + } +} diff --git a/server/Cargo.lock b/server/Cargo.lock index 5c52185..2e10b79 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" @@ -64,7 +74,9 @@ name = "arbiter-server" version = "0.1.0" dependencies = [ "arbiter-proto", + "argon2", "async-trait", + "chacha20poly1305", "chrono", "dashmap", "diesel", @@ -72,20 +84,25 @@ dependencies = [ "diesel_migrations", "ed25519-dalek", "futures", + "insta", "kameo", "memsafe", "miette", "rand", "rcgen", + "restructed", "rustls", "secrecy", "smlang", + "strum", "test-log", "thiserror", "tokio", "tokio-stream", "tonic", "tracing", + "tracing-subscriber", + "x25519-dalek", "zeroize", ] @@ -93,6 +110,19 @@ 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", + "zeroize", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -117,7 +147,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "synstructure", ] @@ -129,7 +159,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -140,7 +170,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -168,9 +198,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", @@ -251,6 +281,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" @@ -265,9 +301,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" @@ -292,9 +346,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", @@ -308,6 +362,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" @@ -316,7 +381,20 @@ 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]] @@ -333,6 +411,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" @@ -342,6 +431,18 @@ dependencies = [ "cc", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -381,6 +482,17 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.0" @@ -390,6 +502,21 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "5.0.0-pre.6" @@ -399,8 +526,8 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", - "fiat-crypto", + "digest 0.11.0", + "fiat-crypto 0.3.0", "rustc_version", "subtle", "zeroize", @@ -414,7 +541,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -438,7 +565,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -449,7 +576,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -488,9 +615,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", ] @@ -536,7 +663,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -556,17 +683,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.115", ] [[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6" +dependencies = [ + "block-buffer 0.11.0", + "crypto-common 0.2.0", ] [[package]] @@ -577,7 +715,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -597,7 +735,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -627,9 +765,9 @@ version = "3.0.0-pre.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 5.0.0-pre.6", "ed25519", - "rand_core", + "rand_core 0.10.0", "sha2", "subtle", "zeroize", @@ -641,6 +779,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.2" @@ -663,6 +807,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -717,9 +867,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", @@ -732,9 +882,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", @@ -742,15 +892,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", @@ -759,38 +909,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.115", ] [[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", @@ -800,10 +950,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" @@ -836,7 +995,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "rand_core", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -1060,6 +1219,27 @@ 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 = "insta" +version = "1.46.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -1125,7 +1305,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1142,9 +1322,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" @@ -1236,7 +1416,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1309,6 +1489,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1367,6 +1556,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "owo-colors" version = "4.2.3" @@ -1396,6 +1591,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" @@ -1440,7 +1646,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1461,6 +1667,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" @@ -1480,7 +1697,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.115", +] + +[[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]] @@ -1519,7 +1760,7 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn 2.0.114", + "syn 2.0.115", "tempfile", ] @@ -1533,7 +1774,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1586,9 +1827,18 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "chacha20", + "chacha20 0.10.0", "getrandom 0.4.1", - "rand_core", + "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]] @@ -1650,6 +1900,18 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[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.115", +] + [[package]] name = "ring" version = "0.17.14" @@ -1810,7 +2072,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1843,7 +2105,7 @@ checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.11.0", ] [[package]] @@ -1883,6 +2145,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" version = "0.4.12" @@ -1950,6 +2218,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1990,9 +2279,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -2013,7 +2302,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2057,7 +2346,7 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2087,7 +2376,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2156,7 +2445,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2196,9 +2485,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", @@ -2218,18 +2507,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", @@ -2259,14 +2548,14 @@ 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.115", ] [[package]] @@ -2282,16 +2571,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.115", "tempfile", "tonic-build", ] @@ -2346,7 +2635,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2356,6 +2645,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", ] [[package]] @@ -2365,12 +2666,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", + "nu-ansi-term", "once_cell", "regex-automata", "sharded-slab", + "smallvec", "thread_local", "tracing", "tracing-core", + "tracing-log", ] [[package]] @@ -2421,6 +2725,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" @@ -2435,20 +2749,32 @@ 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", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" 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" @@ -2514,7 +2840,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "wasm-bindgen-shared", ] @@ -2604,7 +2930,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2615,7 +2941,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2651,6 +2977,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -2834,7 +3169,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.114", + "syn 2.0.115", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2850,7 +3185,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2892,6 +3227,18 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "x509-parser" version = "0.18.1" @@ -2924,12 +3271,26 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] [[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/.DS_Store b/server/crates/.DS_Store new file mode 100644 index 0000000..2db1fe5 Binary files /dev/null and b/server/crates/.DS_Store differ diff --git a/server/crates/arbiter-proto/build.rs b/server/crates/arbiter-proto/build.rs index 77136cb..1294e9e 100644 --- a/server/crates/arbiter-proto/build.rs +++ b/server/crates/arbiter-proto/build.rs @@ -3,6 +3,9 @@ use tonic_prost_build::configure; static PROTOBUF_DIR: &str = "../../../protobufs"; fn main() -> Result<(), Box> { + + println!("cargo::rerun-if-changed={PROTOBUF_DIR}"); + configure() .message_attribute(".", "#[derive(::kameo::Reply)]") .compile_protos( diff --git a/server/crates/arbiter-proto/src/lib.rs b/server/crates/arbiter-proto/src/lib.rs index bce8e36..2a40151 100644 --- a/server/crates/arbiter-proto/src/lib.rs +++ b/server/crates/arbiter-proto/src/lib.rs @@ -6,6 +6,9 @@ pub mod proto { pub mod auth { tonic::include_proto!("arbiter.auth"); } + pub mod unseal { + tonic::include_proto!("arbiter.unseal"); + } } pub mod transport; diff --git a/server/crates/arbiter-server/.DS_Store b/server/crates/arbiter-server/.DS_Store new file mode 100644 index 0000000..d8eda6f Binary files /dev/null and b/server/crates/arbiter-server/.DS_Store differ diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index 5cfe75f..538c33a 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -5,13 +5,7 @@ edition = "2024" repository = "https://git.markettakers.org/MarketTakers/arbiter" [dependencies] -diesel = { version = "2.3.6", features = [ - "sqlite", - "uuid", - "time", - "chrono", - "serde_json", -] } +diesel = { version = "2.3.6", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] } diesel-async = { version = "0.7.4", features = [ "bb8", "migrations", @@ -21,6 +15,7 @@ diesel-async = { version = "0.7.4", features = [ ed25519-dalek.workspace = true arbiter-proto.path = "../arbiter-proto" tracing.workspace = true +tracing-subscriber = { version = "0.3", features = ["env-filter"] } tonic.workspace = true tokio.workspace = true rustls.workspace = true @@ -44,6 +39,12 @@ chrono.workspace = true memsafe = "0.4.0" zeroize = { version = "1.8.2", features = ["std", "simd"] } kameo.workspace = true +x25519-dalek = { version = "2.0.1", features = ["getrandom"] } +chacha20poly1305 = { version = "0.10.1", features = ["std"] } +argon2 = { version = "0.5.3", features = ["zeroize"] } +restructed = "0.2.2" +strum = { version = "0.27.2", features = ["derive"] } [dev-dependencies] +insta = "1.46.3" test-log = { version = "0.2", default-features = false, features = ["trace"] } diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 1ca612d..f673de7 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -1,22 +1,40 @@ -create table if not exists aead_encrypted ( +create table if not exists root_key_history ( id INTEGER not null PRIMARY KEY, - current_nonce integer not null default(1), -- if re-encrypted, this should be incremented + -- root key stored as aead encrypted artifact, with only difference that it's decrypted by unseal key (derived from user password) + root_key_encryption_nonce blob not null default(1), -- if re-encrypted, this should be incremented. Used for encrypting root key + data_encryption_nonce blob not null default(1), -- nonce used for encrypting with key itself ciphertext blob not null, tag blob not null, - schema_version integer not null default(1) -- server would need to reencrypt, because this means that we have changed algorithm + schema_version integer not null default(1), -- server would need to reencrypt, because this means that we have changed algorithm + salt blob not null -- for key deriviation ) STRICT; +create table if not exists aead_encrypted ( + id INTEGER not null PRIMARY KEY, + current_nonce blob not null default(1), -- if re-encrypted, this should be incremented + ciphertext blob not null, + tag blob not null, + schema_version integer not null default(1), -- server would need to reencrypt, because this means that we have changed algorithm + associated_root_key_id integer not null references root_key_history (id) on delete RESTRICT, + created_at integer not null default(unixepoch ('now')) +) STRICT; + +create unique index if not exists uniq_nonce_per_root_key on aead_encrypted ( + current_nonce, + associated_root_key_id +); + -- This is a singleton create table if not exists arbiter_settings ( id INTEGER not null PRIMARY KEY CHECK (id = 1), -- singleton row, id must be 1 - root_key_id integer references aead_encrypted (id) on delete RESTRICT, -- if null, means wasn't bootstrapped yet + root_key_id integer references root_key_history (id) on delete RESTRICT, -- if null, means wasn't bootstrapped yet cert_key blob not null, cert blob not null ) STRICT; create table if not exists useragent_client ( id integer not null primary key, - nonce integer not null default (1), -- used for auth challenge + nonce integer not null default(1), -- used for auth challenge public_key blob not null, created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) @@ -24,7 +42,7 @@ create table if not exists useragent_client ( create table if not exists program_client ( id integer not null primary key, - nonce integer not null default (1), -- used for auth challenge + nonce integer not null default(1), -- used for auth challenge public_key blob not null, created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) diff --git a/server/crates/arbiter-server/src/.DS_Store b/server/crates/arbiter-server/src/.DS_Store new file mode 100644 index 0000000..4f32ec0 Binary files /dev/null and b/server/crates/arbiter-server/src/.DS_Store differ diff --git a/server/crates/arbiter-server/src/actors.rs b/server/crates/arbiter-server/src/actors.rs index 691101e..a987633 100644 --- a/server/crates/arbiter-server/src/actors.rs +++ b/server/crates/arbiter-server/src/actors.rs @@ -1,2 +1,4 @@ pub mod user_agent; pub mod client; +pub(crate) mod bootstrap; +pub(crate) mod keyholder; \ No newline at end of file diff --git a/server/crates/arbiter-server/src/context/bootstrap.rs b/server/crates/arbiter-server/src/actors/bootstrap.rs similarity index 91% rename from server/crates/arbiter-server/src/context/bootstrap.rs rename to server/crates/arbiter-server/src/actors/bootstrap.rs index 211344c..b25788d 100644 --- a/server/crates/arbiter-server/src/context/bootstrap.rs +++ b/server/crates/arbiter-server/src/actors/bootstrap.rs @@ -1,19 +1,13 @@ use arbiter_proto::{BOOTSTRAP_TOKEN_PATH, home_path}; -use diesel::{ExpressionMethods, QueryDsl}; +use diesel::QueryDsl; use diesel_async::RunQueryDsl; use kameo::{Actor, messages}; -use memsafe::MemSafe; use miette::Diagnostic; use rand::{RngExt, distr::StandardUniform, make_rng, rngs::StdRng}; -use secrecy::SecretString; use thiserror::Error; use tracing::info; -use zeroize::{Zeroize, Zeroizing}; -use crate::{ - context::{self, ServerContext}, - db::{self, DatabasePool, schema}, -}; +use crate::db::{self, DatabasePool, schema}; const TOKEN_LENGTH: usize = 64; diff --git a/server/crates/arbiter-server/src/actors/keyholder.rs b/server/crates/arbiter-server/src/actors/keyholder.rs new file mode 100644 index 0000000..7ab4571 --- /dev/null +++ b/server/crates/arbiter-server/src/actors/keyholder.rs @@ -0,0 +1,939 @@ +use diesel::{ + ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper, + dsl::{insert_into, update}, +}; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use kameo::{Actor, Reply, messages}; +use memsafe::MemSafe; +use strum::{EnumDiscriminants, IntoDiscriminant}; +use tracing::{error, info}; + +use crate::{ + actors::keyholder::v1::{KeyCell, Nonce}, + db::{ + self, + models::{self, RootKeyHistory}, + schema::{self}, + }, +}; + +pub mod v1; + +#[derive(Default, EnumDiscriminants)] +#[strum_discriminants(derive(Reply), vis(pub))] +enum State { + #[default] + Unbootstrapped, + Sealed { + root_key_history_id: i32, + }, + Unsealed { + root_key_history_id: i32, + root_key: KeyCell, + }, +} + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum Error { + #[error("Keyholder is already bootstrapped")] + #[diagnostic(code(arbiter::keyholder::already_bootstrapped))] + AlreadyBootstrapped, + #[error("Keyholder is not bootstrapped")] + #[diagnostic(code(arbiter::keyholder::not_bootstrapped))] + NotBootstrapped, + #[error("Invalid key provided")] + #[diagnostic(code(arbiter::keyholder::invalid_key))] + InvalidKey, + + #[error("Requested aead entry not found")] + #[diagnostic(code(arbiter::keyholder::aead_not_found))] + NotFound, + + #[error("Encryption error: {0}")] + #[diagnostic(code(arbiter::keyholder::encryption_error))] + Encryption(#[from] chacha20poly1305::aead::Error), + + #[error("Database error: {0}")] + #[diagnostic(code(arbiter::keyholder::database_error))] + DatabaseConnection(#[from] db::PoolError), + + #[error("Database transaction error: {0}")] + #[diagnostic(code(arbiter::keyholder::database_transaction_error))] + DatabaseTransaction(#[from] diesel::result::Error), + + #[error("Broken database")] + #[diagnostic(code(arbiter::keyholder::broken_database))] + BrokenDatabase, +} + +/// Manages vault root key and tracks current state of the vault (bootstrapped/unbootstrapped, sealed/unsealed). +/// Provides API for encrypting and decrypting data using the vault root key. +/// Abstraction over database to make sure nonces are never reused and encryption keys are never exposed in plaintext outside of this actor. +#[derive(Actor)] +pub struct KeyHolder { + db: db::DatabasePool, + state: State, +} + +#[messages] +impl KeyHolder { + pub async fn new(db: db::DatabasePool) -> Result { + let state = { + let mut conn = db.get().await?; + + let (root_key_history,) = schema::arbiter_settings::table + .left_join(schema::root_key_history::table) + .select((Option::::as_select(),)) + .get_result::<(Option,)>(&mut conn) + .await?; + + match root_key_history { + Some(root_key_history) => State::Sealed { + root_key_history_id: root_key_history.id, + }, + None => State::Unbootstrapped, + } + }; + + Ok(Self { db, state }) + } + + // Exclusive transaction to avoid race condtions if multiple keyholders write + // additional layer of protection against nonce-reuse + async fn get_new_nonce(pool: &db::DatabasePool, root_key_id: i32) -> Result { + let mut conn = pool.get().await?; + + let nonce = conn + .exclusive_transaction(|conn| { + Box::pin(async move { + let current_nonce: Vec = schema::root_key_history::table + .filter(schema::root_key_history::id.eq(root_key_id)) + .select(schema::root_key_history::data_encryption_nonce) + .first(conn) + .await?; + + let mut nonce = + v1::Nonce::try_from(current_nonce.as_slice()).map_err(|_| { + error!( + "Broken database: invalid nonce for root key history id={}", + root_key_id + ); + Error::BrokenDatabase + })?; + nonce.increment(); + + update(schema::root_key_history::table) + .filter(schema::root_key_history::id.eq(root_key_id)) + .set(schema::root_key_history::data_encryption_nonce.eq(nonce.to_vec())) + .execute(conn) + .await?; + + Result::<_, Error>::Ok(nonce) + }) + }) + .await?; + + Ok(nonce) + } + + #[message] + pub async fn bootstrap(&mut self, seal_key_raw: MemSafe>) -> Result<(), Error> { + if !matches!(self.state, State::Unbootstrapped) { + return Err(Error::AlreadyBootstrapped); + } + let salt = v1::generate_salt(); + let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); + let mut root_key = KeyCell::new_secure_random(); + + // Zero nonces are fine because they are one-time + let root_key_nonce = v1::Nonce::default(); + let data_encryption_nonce = v1::Nonce::default(); + + let root_key_ciphertext: Vec = { + let root_key_reader = root_key.0.read().unwrap(); + let root_key_reader = root_key_reader.as_slice(); + seal_key + .encrypt(&root_key_nonce, v1::ROOT_KEY_TAG, root_key_reader) + .map_err(|err| { + error!(?err, "Fatal bootstrap error"); + Error::Encryption(err) + })? + }; + + let mut conn = self.db.get().await?; + + let data_encryption_nonce_bytes = data_encryption_nonce.to_vec(); + let root_key_history_id = conn + .transaction(|conn| { + Box::pin(async move { + let root_key_history_id: i32 = insert_into(schema::root_key_history::table) + .values(&models::NewRootKeyHistory { + ciphertext: root_key_ciphertext, + tag: v1::ROOT_KEY_TAG.to_vec(), + root_key_encryption_nonce: root_key_nonce.to_vec(), + data_encryption_nonce: data_encryption_nonce_bytes, + schema_version: 1, + salt: salt.to_vec(), + }) + .returning(schema::root_key_history::id) + .get_result(conn) + .await?; + + update(schema::arbiter_settings::table) + .set(schema::arbiter_settings::root_key_id.eq(root_key_history_id)) + .execute(conn) + .await?; + + Result::<_, diesel::result::Error>::Ok(root_key_history_id) + }) + }) + .await?; + + self.state = State::Unsealed { + root_key, + root_key_history_id, + }; + + info!("Keyholder bootstrapped successfully"); + + Ok(()) + } + + #[message] + pub async fn try_unseal(&mut self, seal_key_raw: MemSafe>) -> Result<(), Error> { + let State::Sealed { + root_key_history_id, + } = &self.state + else { + return Err(Error::NotBootstrapped); + }; + + // We don't want to hold connection while doing expensive KDF work + let current_key = { + let mut conn = self.db.get().await?; + schema::root_key_history::table + .filter(schema::root_key_history::id.eq(*root_key_history_id)) + .select(schema::root_key_history::data_encryption_nonce ) + .select(RootKeyHistory::as_select() ) + .first(&mut conn) + .await? + }; + + let salt = ¤t_key.salt; + let salt = v1::Salt::try_from(salt.as_slice()).map_err(|_| { + error!("Broken database: invalid salt for root key"); + Error::BrokenDatabase + })?; + let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); + + let mut root_key = MemSafe::new(current_key.ciphertext.clone()).unwrap(); + + let nonce = v1::Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err( + |_| { + error!("Broken database: invalid nonce for root key"); + Error::BrokenDatabase + }, + )?; + + seal_key + .decrypt_in_place(&nonce, v1::ROOT_KEY_TAG, &mut root_key) + .map_err(|err| { + error!(?err, "Failed to unseal root key: invalid seal key"); + Error::InvalidKey + })?; + + self.state = State::Unsealed { + root_key_history_id: current_key.id, + root_key: v1::KeyCell::try_from(root_key).map_err(|err| { + error!(?err, "Broken database: invalid encryption key size"); + Error::BrokenDatabase + })?, + }; + + info!("Keyholder unsealed successfully"); + + Ok(()) + } + + // Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext + #[message] + pub async fn decrypt(&mut self, aead_id: i32) -> Result>, Error> { + let State::Unsealed { root_key, .. } = &mut self.state else { + return Err(Error::NotBootstrapped); + }; + + let row: models::AeadEncrypted = { + let mut conn = self.db.get().await?; + schema::aead_encrypted::table + .select(models::AeadEncrypted::as_select()) + .filter(schema::aead_encrypted::id.eq(aead_id)) + .first(&mut conn) + .await + .optional()? + .ok_or(Error::NotFound)? + }; + + let nonce = v1::Nonce::try_from(row.current_nonce.as_slice()).map_err(|_| { + error!( + "Broken database: invalid nonce for aead_encrypted id={}", + aead_id + ); + Error::BrokenDatabase + })?; + let mut output = MemSafe::new(row.ciphertext).unwrap(); + root_key.decrypt_in_place(&nonce, v1::TAG, &mut output)?; + Ok(output) + } + + // Creates new `aead_encrypted` entry in the database and returns it's ID + #[message] + pub async fn create_new(&mut self, mut plaintext: MemSafe>) -> Result { + let State::Unsealed { + root_key, + root_key_history_id, + } = &mut self.state + else { + return Err(Error::NotBootstrapped); + }; + + // Order matters here - `get_new_nonce` acquires connection, so we need to call it before next acquire + // Borrow checker note: &mut borrow a few lines above is disjoint from this field + let nonce = Self::get_new_nonce(&self.db, *root_key_history_id).await?; + + let mut ciphertext_buffer = plaintext.write().unwrap(); + let ciphertext_buffer: &mut Vec = ciphertext_buffer.as_mut(); + root_key.encrypt_in_place(&nonce, v1::TAG, &mut *ciphertext_buffer)?; + + let ciphertext = std::mem::take(ciphertext_buffer); + + let mut conn = self.db.get().await?; + let aead_id: i32 = insert_into(schema::aead_encrypted::table) + .values(&models::NewAeadEncrypted { + ciphertext, + tag: v1::TAG.to_vec(), + current_nonce: nonce.to_vec(), + schema_version: 1, + associated_root_key_id: *root_key_history_id, + created_at: chrono::Utc::now().timestamp() as i32, + }) + .returning(schema::aead_encrypted::id) + .get_result(&mut conn) + .await?; + + Ok(aead_id) + } + + #[message] + pub fn get_state(&self) -> StateDiscriminants { + self.state.discriminant() + } +} + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + use std::sync::Arc; + + use diesel::dsl::{insert_into, sql_query, update}; + use diesel_async::RunQueryDsl; + use futures::stream::TryUnfold; + use kameo::actor::{ActorRef, Spawn as _}; + use memsafe::MemSafe; + use tokio::sync::Mutex; + use tokio::task::JoinSet; + + use crate::db::{self, models::ArbiterSetting}; + + use super::*; + + async fn seed_settings(pool: &db::DatabasePool) { + let mut conn = pool.get().await.unwrap(); + insert_into(schema::arbiter_settings::table) + .values(&ArbiterSetting { + id: 1, + root_key_id: None, + cert_key: vec![], + cert: vec![], + }) + .execute(&mut conn) + .await + .unwrap(); + } + + async fn bootstrapped_actor(db: &db::DatabasePool) -> KeyHolder { + seed_settings(db).await; + let mut actor = KeyHolder::new(db.clone()).await.unwrap(); + let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + actor.bootstrap(seal_key).await.unwrap(); + actor + } + + async fn write_concurrently( + actor: ActorRef, + prefix: &'static str, + count: usize, + ) -> Vec<(i32, Vec)> { + let mut set = JoinSet::new(); + for i in 0..count { + let actor = actor.clone(); + set.spawn(async move { + let plaintext = format!("{prefix}-{i}").into_bytes(); + let id = { + actor + .ask(CreateNew { + plaintext: MemSafe::new(plaintext.clone()).unwrap(), + }) + .await + .unwrap() + }; + (id, plaintext) + }); + } + + let mut out = Vec::with_capacity(count); + while let Some(res) = set.join_next().await { + out.push(res.unwrap()); + } + out + } + + #[tokio::test] + #[test_log::test] + async fn test_bootstrap() { + let db = db::create_test_pool().await; + seed_settings(&db).await; + let mut actor = KeyHolder::new(db.clone()).await.unwrap(); + + assert!(matches!(actor.state, State::Unbootstrapped)); + + let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + actor.bootstrap(seal_key).await.unwrap(); + + assert!(matches!(actor.state, State::Unsealed { .. })); + + let mut conn = db.get().await.unwrap(); + let row: models::RootKeyHistory = schema::root_key_history::table + .select(models::RootKeyHistory::as_select()) + .first(&mut conn) + .await + .unwrap(); + + assert_eq!(row.schema_version, 1); + assert_eq!(row.tag, v1::ROOT_KEY_TAG); + assert!(!row.ciphertext.is_empty()); + assert!(!row.salt.is_empty()); + assert_eq!(row.data_encryption_nonce, v1::Nonce::default().to_vec()); + } + + #[tokio::test] + #[test_log::test] + async fn test_bootstrap_rejects_double() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let seal_key2 = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + let err = actor.bootstrap(seal_key2).await.unwrap_err(); + assert!(matches!(err, Error::AlreadyBootstrapped)); + } + + #[tokio::test] + #[test_log::test] + async fn test_create_decrypt_roundtrip() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let plaintext = b"hello arbiter"; + let aead_id = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + + let mut decrypted = actor.decrypt(aead_id).await.unwrap(); + let decrypted = decrypted.read().unwrap(); + assert_eq!(*decrypted, plaintext); + } + + #[tokio::test] + #[test_log::test] + async fn test_create_new_before_bootstrap_fails() { + let db = db::create_test_pool().await; + seed_settings(&db).await; + let mut actor = KeyHolder::new(db).await.unwrap(); + + let err = actor + .create_new(MemSafe::new(b"data".to_vec()).unwrap()) + .await + .unwrap_err(); + assert!(matches!(err, Error::NotBootstrapped)); + } + + #[tokio::test] + #[test_log::test] + async fn test_decrypt_before_bootstrap_fails() { + let db = db::create_test_pool().await; + seed_settings(&db).await; + let mut actor = KeyHolder::new(db).await.unwrap(); + + let err = actor.decrypt(1).await.unwrap_err(); + assert!(matches!(err, Error::NotBootstrapped)); + } + + #[tokio::test] + #[test_log::test] + async fn test_decrypt_nonexistent_returns_not_found() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let err = actor.decrypt(9999).await.unwrap_err(); + assert!(matches!(err, Error::NotFound)); + } + + #[tokio::test] + #[test_log::test] + async fn test_new_restores_sealed_state() { + let db = db::create_test_pool().await; + let actor = bootstrapped_actor(&db).await; + drop(actor); + + let actor2 = KeyHolder::new(db).await.unwrap(); + assert!(matches!(actor2.state, State::Sealed { .. })); + } + + #[tokio::test] + #[test_log::test] + async fn test_nonce_never_reused() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let n = 5; + let mut ids = Vec::with_capacity(n); + for i in 0..n { + let id = actor + .create_new(MemSafe::new(format!("secret {i}").into_bytes()).unwrap()) + .await + .unwrap(); + ids.push(id); + } + + // read all stored nonces from DB + let mut conn = db.get().await.unwrap(); + let rows: Vec = schema::aead_encrypted::table + .select(models::AeadEncrypted::as_select()) + .load(&mut conn) + .await + .unwrap(); + + assert_eq!(rows.len(), n); + + let nonces: Vec<&Vec> = rows.iter().map(|r| &r.current_nonce).collect(); + let unique: HashSet<&Vec> = nonces.iter().copied().collect(); + assert_eq!(nonces.len(), unique.len(), "all nonces must be unique"); + + // verify nonces are sequential increments from 1 + for (i, row) in rows.iter().enumerate() { + let mut expected = v1::Nonce::default(); + for _ in 0..=i { + expected.increment(); + } + assert_eq!(row.current_nonce, expected.to_vec(), "nonce {i} mismatch"); + } + + // verify data_encryption_nonce on root_key_history tracks the latest nonce + let root_row: models::RootKeyHistory = schema::root_key_history::table + .select(models::RootKeyHistory::as_select()) + .first(&mut conn) + .await + .unwrap(); + let last_nonce = &rows.last().unwrap().current_nonce; + assert_eq!( + &root_row.data_encryption_nonce, last_nonce, + "root_key_history must track the latest nonce" + ); + } + + #[tokio::test] + #[test_log::test] + async fn test_unseal_correct_password() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let plaintext = b"survive a restart"; + let aead_id = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + drop(actor); + + let mut actor = KeyHolder::new(db.clone()).await.unwrap(); + assert!(matches!(actor.state, State::Sealed { .. })); + + let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + actor.try_unseal(seal_key).await.unwrap(); + assert!(matches!(actor.state, State::Unsealed { .. })); + + // previously encrypted data is still decryptable + let mut decrypted = actor.decrypt(aead_id).await.unwrap(); + assert_eq!(*decrypted.read().unwrap(), plaintext); + } + + #[tokio::test] + #[test_log::test] + async fn test_unseal_wrong_then_correct_password() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let plaintext = b"important data"; + let aead_id = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + drop(actor); + + let mut actor = KeyHolder::new(db.clone()).await.unwrap(); + assert!(matches!(actor.state, State::Sealed { .. })); + + // wrong password + let bad_key = MemSafe::new(b"wrong-password".to_vec()).unwrap(); + let err = actor.try_unseal(bad_key).await.unwrap_err(); + assert!(matches!(err, Error::InvalidKey)); + assert!( + matches!(actor.state, State::Sealed { .. }), + "state must remain Sealed after failed attempt" + ); + + // correct password + let good_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + actor.try_unseal(good_key).await.unwrap(); + assert!(matches!(actor.state, State::Unsealed { .. })); + + let mut decrypted = actor.decrypt(aead_id).await.unwrap(); + assert_eq!(*decrypted.read().unwrap(), plaintext); + } + + #[tokio::test] + #[test_log::test] + async fn test_ciphertext_differs_across_entries() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let plaintext = b"same content"; + let id1 = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + let id2 = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + + // different nonces => different ciphertext, even for identical plaintext + let mut conn = db.get().await.unwrap(); + let row1: models::AeadEncrypted = schema::aead_encrypted::table + .filter(schema::aead_encrypted::id.eq(id1)) + .select(models::AeadEncrypted::as_select()) + .first(&mut conn) + .await + .unwrap(); + let row2: models::AeadEncrypted = schema::aead_encrypted::table + .filter(schema::aead_encrypted::id.eq(id2)) + .select(models::AeadEncrypted::as_select()) + .first(&mut conn) + .await + .unwrap(); + + assert_ne!(row1.ciphertext, row2.ciphertext); + + // but both decrypt to the same plaintext + let mut d1 = actor.decrypt(id1).await.unwrap(); + let mut d2 = actor.decrypt(id2).await.unwrap(); + assert_eq!(*d1.read().unwrap(), plaintext); + assert_eq!(*d2.read().unwrap(), plaintext); + } + + #[tokio::test] + #[test_log::test] + async fn concurrent_create_new_no_duplicate_nonces_() { + let db = db::create_test_pool().await; + let actor = KeyHolder::spawn(bootstrapped_actor(&db).await); + + let writes = write_concurrently(actor, "nonce-unique", 32).await; + assert_eq!(writes.len(), 32); + + let mut conn = db.get().await.unwrap(); + let rows: Vec = schema::aead_encrypted::table + .select(models::AeadEncrypted::as_select()) + .load(&mut conn) + .await + .unwrap(); + assert_eq!(rows.len(), 32); + + let nonces: Vec<&Vec> = rows.iter().map(|r| &r.current_nonce).collect(); + let unique: HashSet<&Vec> = nonces.iter().copied().collect(); + assert_eq!(nonces.len(), unique.len(), "all nonces must be unique"); + } + + #[tokio::test] + #[test_log::test] + async fn concurrent_create_new_root_nonce_never_moves_backward() { + let db = db::create_test_pool().await; + let actor = KeyHolder::spawn(bootstrapped_actor(&db).await); + + write_concurrently(actor, "root-max", 24).await; + + let mut conn = db.get().await.unwrap(); + let rows: Vec = schema::aead_encrypted::table + .select(models::AeadEncrypted::as_select()) + .load(&mut conn) + .await + .unwrap(); + let max_nonce = rows + .iter() + .map(|r| r.current_nonce.clone()) + .max() + .expect("at least one row"); + + let root_row: models::RootKeyHistory = schema::root_key_history::table + .select(models::RootKeyHistory::as_select()) + .first(&mut conn) + .await + .unwrap(); + assert_eq!(root_row.data_encryption_nonce, max_nonce); + } + + #[tokio::test] + #[test_log::test] + async fn nonce_monotonic_even_when_nonce_allocation_interleaves() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + let root_key_history_id = match actor.state { + State::Unsealed { + root_key_history_id, + .. + } => root_key_history_id, + _ => panic!("expected unsealed state"), + }; + + let n1 = KeyHolder::get_new_nonce(&db, root_key_history_id) + .await + .unwrap(); + let n2 = KeyHolder::get_new_nonce(&db, root_key_history_id) + .await + .unwrap(); + assert!(n2.to_vec() > n1.to_vec(), "nonce must increase"); + + let mut conn = db.get().await.unwrap(); + let root_row: models::RootKeyHistory = schema::root_key_history::table + .select(models::RootKeyHistory::as_select()) + .first(&mut conn) + .await + .unwrap(); + assert_eq!(root_row.data_encryption_nonce, n2.to_vec()); + + let id = actor + .create_new(MemSafe::new(b"post-interleave".to_vec()).unwrap()) + .await + .unwrap(); + let row: models::AeadEncrypted = schema::aead_encrypted::table + .filter(schema::aead_encrypted::id.eq(id)) + .select(models::AeadEncrypted::as_select()) + .first(&mut conn) + .await + .unwrap(); + assert!( + row.current_nonce > n2.to_vec(), + "next write must advance nonce" + ); + } + + #[tokio::test] + #[test_log::test] + async fn insert_failure_does_not_create_partial_row() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + let root_key_history_id = match actor.state { + State::Unsealed { + root_key_history_id, + .. + } => root_key_history_id, + _ => panic!("expected unsealed state"), + }; + + let mut conn = db.get().await.unwrap(); + let before_count: i64 = schema::aead_encrypted::table + .count() + .get_result(&mut conn) + .await + .unwrap(); + let before_root_nonce: Vec = schema::root_key_history::table + .filter(schema::root_key_history::id.eq(root_key_history_id)) + .select(schema::root_key_history::data_encryption_nonce) + .first(&mut conn) + .await + .unwrap(); + + sql_query( + "CREATE TRIGGER fail_aead_insert BEFORE INSERT ON aead_encrypted BEGIN SELECT RAISE(ABORT, 'forced test failure'); END;", + ) + .execute(&mut conn) + .await + .unwrap(); + drop(conn); + + let err = actor + .create_new(MemSafe::new(b"should fail".to_vec()).unwrap()) + .await + .unwrap_err(); + assert!(matches!(err, Error::DatabaseTransaction(_))); + + let mut conn = db.get().await.unwrap(); + sql_query("DROP TRIGGER fail_aead_insert;") + .execute(&mut conn) + .await + .unwrap(); + + let after_count: i64 = schema::aead_encrypted::table + .count() + .get_result(&mut conn) + .await + .unwrap(); + assert_eq!( + before_count, after_count, + "failed insert must not create row" + ); + + let after_root_nonce: Vec = schema::root_key_history::table + .filter(schema::root_key_history::id.eq(root_key_history_id)) + .select(schema::root_key_history::data_encryption_nonce) + .first(&mut conn) + .await + .unwrap(); + assert!( + after_root_nonce > before_root_nonce, + "current behavior allows nonce gap on failed insert" + ); + } + + #[tokio::test] + #[test_log::test] + async fn decrypt_roundtrip_after_high_concurrency() { + let db = db::create_test_pool().await; + let actor = KeyHolder::spawn(bootstrapped_actor(&db).await); + + let writes = write_concurrently(actor, "roundtrip", 40).await; + let expected: HashMap> = writes.into_iter().collect(); + + let mut decryptor = KeyHolder::new(db.clone()).await.unwrap(); + decryptor + .try_unseal(MemSafe::new(b"test-seal-key".to_vec()).unwrap()) + .await + .unwrap(); + + for (id, plaintext) in expected { + let mut decrypted = decryptor.decrypt(id).await.unwrap(); + assert_eq!(*decrypted.read().unwrap(), plaintext); + } + } + + // #[tokio::test] + // #[test_log::test] + // async fn swapping_ciphertext_and_nonce_between_rows_changes_logical_binding() { + // let db = db::create_test_pool().await; + // let mut actor = bootstrapped_actor(&db).await; + + // let plaintext1 = b"entry-one"; + // let plaintext2 = b"entry-two"; + // let id1 = actor + // .create_new(MemSafe::new(plaintext1.to_vec()).unwrap()) + // .await + // .unwrap(); + // let id2 = actor + // .create_new(MemSafe::new(plaintext2.to_vec()).unwrap()) + // .await + // .unwrap(); + + // let mut conn = db.get().await.unwrap(); + // let row1: models::AeadEncrypted = schema::aead_encrypted::table + // .filter(schema::aead_encrypted::id.eq(id1)) + // .select(models::AeadEncrypted::as_select()) + // .first(&mut conn) + // .await + // .unwrap(); + // let row2: models::AeadEncrypted = schema::aead_encrypted::table + // .filter(schema::aead_encrypted::id.eq(id2)) + // .select(models::AeadEncrypted::as_select()) + // .first(&mut conn) + // .await + // .unwrap(); + + // update(schema::aead_encrypted::table.filter(schema::aead_encrypted::id.eq(id1))) + // .set(( + // schema::aead_encrypted::ciphertext.eq(row2.ciphertext.clone()), + // schema::aead_encrypted::current_nonce.eq(row2.current_nonce.clone()), + // )) + // .execute(&mut conn) + // .await + // .unwrap(); + // update(schema::aead_encrypted::table.filter(schema::aead_encrypted::id.eq(id2))) + // .set(( + // schema::aead_encrypted::ciphertext.eq(row1.ciphertext.clone()), + // schema::aead_encrypted::current_nonce.eq(row1.current_nonce.clone()), + // )) + // .execute(&mut conn) + // .await + // .unwrap(); + + // let mut d1 = actor.decrypt(id1).await.unwrap(); + // let mut d2 = actor.decrypt(id2).await.unwrap(); + // assert_eq!(*d1.read().unwrap(), plaintext2); + // assert_eq!(*d2.read().unwrap(), plaintext1); + // } + #[tokio::test] + #[test_log::test] + async fn broken_db_nonce_format_fails_closed() { + // malformed root_key_history nonce must fail create_new + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + let root_key_history_id = match actor.state { + State::Unsealed { + root_key_history_id, + .. + } => root_key_history_id, + _ => panic!("expected unsealed state"), + }; + + let mut conn = db.get().await.unwrap(); + update( + schema::root_key_history::table + .filter(schema::root_key_history::id.eq(root_key_history_id)), + ) + .set(schema::root_key_history::data_encryption_nonce.eq(vec![1, 2, 3])) + .execute(&mut conn) + .await + .unwrap(); + drop(conn); + + let err = actor + .create_new(MemSafe::new(b"must fail".to_vec()).unwrap()) + .await + .unwrap_err(); + assert!(matches!(err, Error::BrokenDatabase)); + + // malformed per-row nonce must fail decrypt + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + let id = actor + .create_new(MemSafe::new(b"decrypt target".to_vec()).unwrap()) + .await + .unwrap(); + let mut conn = db.get().await.unwrap(); + update(schema::aead_encrypted::table.filter(schema::aead_encrypted::id.eq(id))) + .set(schema::aead_encrypted::current_nonce.eq(vec![7, 8])) + .execute(&mut conn) + .await + .unwrap(); + drop(conn); + + let err = actor.decrypt(id).await.unwrap_err(); + assert!(matches!(err, Error::BrokenDatabase)); + } +} diff --git a/server/crates/arbiter-server/src/actors/keyholder/v1.rs b/server/crates/arbiter-server/src/actors/keyholder/v1.rs new file mode 100644 index 0000000..86878dc --- /dev/null +++ b/server/crates/arbiter-server/src/actors/keyholder/v1.rs @@ -0,0 +1,242 @@ +use std::ops::Deref as _; + +use argon2::{Algorithm, Argon2, password_hash::Salt as ArgonSalt}; +use chacha20poly1305::{ + AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce, + aead::{AeadMut, Error, Payload}, +}; +use memsafe::MemSafe; +use rand::{ + Rng as _, SeedableRng, + rngs::{StdRng, SysRng}, +}; + +pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes(); +pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes(); + +pub const NONCE_LENGTH: usize = 24; + +#[derive(Default)] +pub struct Nonce([u8; NONCE_LENGTH]); +impl Nonce { + pub fn increment(&mut self) { + for i in (0..self.0.len()).rev() { + if self.0[i] == 0xFF { + self.0[i] = 0; + } else { + self.0[i] += 1; + break; + } + } + } + + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} +impl<'a> TryFrom<&'a [u8]> for Nonce { + type Error = (); + + fn try_from(value: &'a [u8]) -> Result { + if value.len() != NONCE_LENGTH { + return Err(()); + } + let mut nonce = [0u8; NONCE_LENGTH]; + nonce.copy_from_slice(&value); + Ok(Self(nonce)) + } +} + +pub struct KeyCell(pub(super) MemSafe); +impl From> for KeyCell { + fn from(value: MemSafe) -> Self { + Self(value) + } +} +impl TryFrom>> for KeyCell { + type Error = (); + + fn try_from(mut value: MemSafe>) -> Result { + let value = value.read().unwrap(); + if value.len() != size_of::() { + return Err(()); + } + let mut cell = MemSafe::new(Key::default()).unwrap(); + { + let mut cell_write = cell.write().unwrap(); + let cell_slice: &mut [u8] = cell_write.as_mut(); + cell_slice.copy_from_slice(&value); + } + Ok(Self(cell)) + } +} + +impl KeyCell { + pub fn new_secure_random() -> Self { + let mut key = MemSafe::new(Key::default()).unwrap(); + { + let mut key_buffer = key.write().unwrap(); + let key_buffer: &mut [u8] = key_buffer.as_mut(); + + let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap(); + rng.fill_bytes(key_buffer); + } + + key.into() + } + + pub fn into_inner(self) -> MemSafe { + self.0 + } + + pub fn encrypt_in_place( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + mut buffer: impl AsMut>, + ) -> Result<(), Error> { + let key_reader = self.0.read().unwrap(); + let key_ref = key_reader.deref(); + let cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + let buffer = buffer.as_mut(); + cipher.encrypt_in_place(nonce, associated_data, buffer) + } + pub fn decrypt_in_place( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + buffer: &mut MemSafe>, + ) -> Result<(), Error> { + let key_reader = self.0.read().unwrap(); + let key_ref = key_reader.deref(); + let cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + let mut buffer = buffer.write().unwrap(); + let buffer: &mut Vec = buffer.as_mut(); + cipher.decrypt_in_place(nonce, associated_data, buffer) + } + + pub fn encrypt( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + plaintext: impl AsRef<[u8]>, + ) -> Result, Error> { + let key_reader = self.0.read().unwrap(); + let key_ref = key_reader.deref(); + let mut cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + + + let ciphertext = cipher.encrypt( + &nonce, + Payload { + msg: plaintext.as_ref(), + aad: associated_data, + }, + )?; + Ok(ciphertext) + } +} + +pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH]; + +pub(super) fn generate_salt() -> Salt { + let mut salt = Salt::default(); + let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap(); + rng.fill_bytes(&mut salt); + salt +} + +/// User password might be of different length, have not enough entropy, etc... +/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation. +pub(super) fn derive_seal_key(mut password: MemSafe>, salt: &Salt) -> KeyCell { + let params = argon2::Params::new(262_144, 3, 4, None).unwrap(); + let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params); + let mut key = MemSafe::new(Key::default()).unwrap(); + { + let password_source = password.read().unwrap(); + let mut key_buffer = key.write().unwrap(); + let key_buffer: &mut [u8] = key_buffer.as_mut(); + + hasher + .hash_password_into(password_source.deref(), salt, key_buffer) + .unwrap(); + } + + key.into() +} + +#[cfg(test)] +mod tests { + use super::*; + use memsafe::MemSafe; + + #[test] + pub fn derive_seal_key_deterministic() { + static PASSWORD: &[u8] = b"password"; + let password = MemSafe::new(PASSWORD.to_vec()).unwrap(); + let password2 = MemSafe::new(PASSWORD.to_vec()).unwrap(); + let salt = generate_salt(); + + let mut key1 = derive_seal_key(password, &salt); + let mut key2 = derive_seal_key(password2, &salt); + + let key1_reader = key1.0.read().unwrap(); + let key2_reader = key2.0.read().unwrap(); + + assert_eq!(key1_reader.deref(), key2_reader.deref()); + } + + #[test] + pub fn successful_derive() { + static PASSWORD: &[u8] = b"password"; + let password = MemSafe::new(PASSWORD.to_vec()).unwrap(); + let salt = generate_salt(); + + let mut key = derive_seal_key(password, &salt); + let key_reader = key.0.read().unwrap(); + let key_ref = key_reader.deref(); + + assert_ne!(key_ref.as_slice(), &[0u8; 32][..]); + } + + #[test] + pub fn encrypt_decrypt() { + static PASSWORD: &[u8] = b"password"; + let password = MemSafe::new(PASSWORD.to_vec()).unwrap(); + let salt = generate_salt(); + + let mut key = derive_seal_key(password, &salt); + let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305 + let associated_data = b"associated data"; + let mut buffer = b"secret data".to_vec(); + + key.encrypt_in_place(&nonce, associated_data, &mut buffer) + .unwrap(); + assert_ne!(buffer, b"secret data"); + + let mut buffer = MemSafe::new(buffer).unwrap(); + + key.decrypt_in_place(&nonce, associated_data, &mut buffer) + .unwrap(); + + let buffer = buffer.read().unwrap(); + assert_eq!(*buffer, b"secret data"); + } + + #[test] + // We should fuzz this + pub fn test_nonce_increment() { + let mut nonce = Nonce([0u8; NONCE_LENGTH]); + nonce.increment(); + + assert_eq!( + nonce.0, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + ] + ); + } +} diff --git a/server/crates/arbiter-server/src/actors/user_agent.rs b/server/crates/arbiter-server/src/actors/user_agent.rs index 0b98e5b..0542082 100644 --- a/server/crates/arbiter-server/src/actors/user_agent.rs +++ b/server/crates/arbiter-server/src/actors/user_agent.rs @@ -1,96 +1,58 @@ +use std::{ + ops::DerefMut, + sync::Mutex, +}; + use arbiter_proto::proto::{ - UserAgentRequest, UserAgentResponse, + UserAgentResponse, auth::{ - self, AuthChallenge, AuthChallengeRequest, AuthOk, ClientMessage, - ServerMessage as AuthServerMessage, client_message::Payload as ClientAuthPayload, + self, AuthChallengeRequest, AuthOk, ServerMessage as AuthServerMessage, server_message::Payload as ServerAuthPayload, }, - user_agent_request::Payload as UserAgentRequestPayload, + unseal::{UnsealEncryptedKey, UnsealResult, UnsealStart, UnsealStartResponse}, user_agent_response::Payload as UserAgentResponsePayload, }; +use chacha20poly1305::{ + AeadInPlace, XChaCha20Poly1305, XNonce, + aead::KeyInit, +}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, dsl::update}; use diesel_async::{AsyncConnection, RunQueryDsl}; use ed25519_dalek::VerifyingKey; -use futures::StreamExt; -use kameo::{ - Actor, - actor::{ActorRef, Spawn}, - error::SendError, - messages, - prelude::Context, -}; -use tokio::sync::mpsc; +use kameo::{Actor, actor::ActorRef, messages}; +use memsafe::MemSafe; use tokio::sync::mpsc::Sender; use tonic::Status; use tracing::{error, info}; +use x25519_dalek::{EphemeralSecret, PublicKey}; use crate::{ ServerContext, - context::bootstrap::{BootstrapActor, ConsumeToken}, + actors::{ + bootstrap::{BootstrapActor, ConsumeToken}, + user_agent::state::{ + AuthRequestContext, ChallengeContext, DummyContext, UnsealContext, UserAgentEvents, + UserAgentStateMachine, UserAgentStates, + }, + }, 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)] -pub struct ChallengeContext { - challenge: AuthChallenge, - key: VerifyingKey, -} +mod state; +#[cfg(test)] +mod tests; -// Request context with deserialized public key for state machine. -// This intermediate struct is needed because the state machine branches depending on presence of bootstrap token, -// but we want to have the deserialized key in both branches. -#[derive(Clone, Debug)] -pub struct AuthRequestContext { - pubkey: VerifyingKey, - bootstrap_token: Option, -} - -smlang::statemachine!( - name: UserAgent, - derive_states: [Debug], - custom_error: false, - transitions: { - *Init + AuthRequest(AuthRequestContext) / auth_request_context = ReceivedAuthRequest(AuthRequestContext), - ReceivedAuthRequest(AuthRequestContext) + ReceivedBootstrapToken = Authenticated, - - ReceivedAuthRequest(AuthRequestContext) + SentChallenge(ChallengeContext) / move_challenge = WaitingForChallengeSolution(ChallengeContext), - - WaitingForChallengeSolution(ChallengeContext) + ReceivedGoodSolution = Authenticated, - WaitingForChallengeSolution(ChallengeContext) + ReceivedBadSolution = AuthError, // block further transitions, but connection should close anyway - } -); - -pub struct DummyContext; -impl UserAgentStateMachineContext for DummyContext { - #[allow(missing_docs)] - #[allow(clippy::unused_unit)] - fn move_challenge( - &mut self, - state_data: &AuthRequestContext, - event_data: ChallengeContext, - ) -> Result { - Ok(event_data) - } - - #[allow(missing_docs)] - #[allow(clippy::unused_unit)] - fn auth_request_context( - &mut self, - event_data: AuthRequestContext, - ) -> Result { - Ok(event_data) - } -} +mod transport; +pub(crate) use transport::handle_user_agent; #[derive(Actor)] pub struct UserAgentActor { db: db::DatabasePool, bootstapper: ActorRef, state: UserAgentStateMachine, - tx: Sender>, + // will be used in future + _tx: Sender>, } impl UserAgentActor { @@ -102,10 +64,11 @@ impl UserAgentActor { db: context.db.clone(), bootstapper: context.bootstrapper.clone(), state: UserAgentStateMachine::new(DummyContext), - tx, + _tx: tx, } } + #[cfg(test)] pub(crate) fn new_manual( db: db::DatabasePool, bootstapper: ActorRef, @@ -115,7 +78,7 @@ impl UserAgentActor { db, bootstapper, state: UserAgentStateMachine::new(DummyContext), - tx, + _tx: tx, } } @@ -255,18 +218,93 @@ fn auth_response(payload: ServerAuthPayload) -> UserAgentResponse { } } +fn unseal_response(payload: UserAgentResponsePayload) -> UserAgentResponse { + UserAgentResponse { + payload: Some(payload), + } +} + #[messages] impl UserAgentActor { - #[message(ctx)] - pub async fn handle_auth_challenge_request( - &mut self, - req: AuthChallengeRequest, - ctx: &mut Context, - ) -> Output { + #[message] + pub async fn handle_unseal_request(&mut self, req: UnsealStart) -> Output { + let secret = EphemeralSecret::random(); + let public_key = PublicKey::from(&secret); + + let client_pubkey_bytes: [u8; 32] = req + .client_pubkey + .try_into() + .map_err(|_| Status::invalid_argument("client_pubkey must be 32 bytes"))?; + + let client_public_key = PublicKey::from(client_pubkey_bytes); + + self.transition(UserAgentEvents::UnsealRequest(UnsealContext { + server_public_key: public_key, + secret: Mutex::new(Some(secret)), + client_public_key, + }))?; + + Ok(unseal_response( + UserAgentResponsePayload::UnsealStartResponse(UnsealStartResponse { + server_pubkey: public_key.as_bytes().to_vec(), + }), + )) + } + + #[message] + pub async fn handle_unseal_encrypted_key(&mut self, req: UnsealEncryptedKey) -> Output { + let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else { + error!("Received unseal encrypted key in invalid state"); + return Err(Status::failed_precondition( + "Invalid state for unseal encrypted key", + )); + }; + let ephemeral_secret = { + let mut secret_lock = unseal_context.secret.lock().unwrap(); + let secret = secret_lock.take(); + match secret { + Some(secret) => secret, + None => { + drop(secret_lock); + error!("Ephemeral secret already taken"); + self.transition(UserAgentEvents::ReceivedInvalidKey)?; + return Ok(unseal_response(UserAgentResponsePayload::UnsealResult( + UnsealResult::InvalidKey.into(), + ))); + } + } + }; + + let nonce = XNonce::from_slice(&req.nonce); + + let shared_secret = ephemeral_secret.diffie_hellman(&unseal_context.client_public_key); + let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into()); + + let mut root_key_buffer = MemSafe::new(req.ciphertext.clone()).unwrap(); + let mut write_handle = root_key_buffer.write().unwrap(); + let write_handle = write_handle.deref_mut(); + + let decryption_result = cipher + .decrypt_in_place(nonce, &req.associated_data, write_handle); + + match decryption_result { + Ok(_) => todo!("Send key to the keyguarding"), + Err(err) => { + error!(?err, "Failed to decrypt unseal key"); + self.transition(UserAgentEvents::ReceivedInvalidKey)?; + return Ok(unseal_response(UserAgentResponsePayload::UnsealResult( + UnsealResult::InvalidKey.into(), + ))); + }, + } + } + + #[message] + pub async fn handle_auth_challenge_request(&mut self, req: AuthChallengeRequest) -> Output { let pubkey = req.pubkey.as_array().ok_or(Status::invalid_argument( "Expected pubkey to have specific length", ))?; - let pubkey = VerifyingKey::from_bytes(pubkey).map_err(|err| { + let pubkey = VerifyingKey::from_bytes(pubkey).map_err(|_err| { error!(?pubkey, "Failed to convert to VerifyingKey"); Status::invalid_argument("Failed to convert pubkey to VerifyingKey") })?; @@ -282,11 +320,10 @@ impl UserAgentActor { } } - #[message(ctx)] + #[message] pub async fn handle_auth_challenge_solution( &mut self, solution: auth::AuthChallengeSolution, - ctx: &mut Context, ) -> Output { let (valid, challenge_context) = self.verify_challenge_solution(&solution)?; @@ -304,66 +341,3 @@ impl UserAgentActor { } } } - -#[cfg(test)] -mod tests { - use arbiter_proto::proto::{ - UserAgentResponse, auth::{AuthChallengeRequest, AuthOk}, - user_agent_response::Payload as UserAgentResponsePayload, - }; - use kameo::actor::Spawn; - - use crate::{ - actors::user_agent::HandleAuthChallengeRequest, context::bootstrap::BootstrapActor, db, - }; - - use super::UserAgentActor; - - #[tokio::test] - #[test_log::test] - pub async fn test_bootstrap_token_auth() { - let db = db::create_test_pool().await; - // explicitly not installing any user_agent pubkeys - let bootstrapper = BootstrapActor::new(&db).await.unwrap(); // this will create bootstrap token - let token = bootstrapper.get_token().unwrap(); - - let bootstrapper_ref = BootstrapActor::spawn(bootstrapper); - let user_agent = UserAgentActor::new_manual( - db.clone(), - bootstrapper_ref, - tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test - ); - let user_agent_ref = UserAgentActor::spawn(user_agent); - - // simulate client sending auth request with bootstrap token - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); - - let result = user_agent_ref - .ask(HandleAuthChallengeRequest { - req: AuthChallengeRequest { - pubkey: pubkey_bytes, - bootstrap_token: Some(token), - }, - }) - .await - .expect("Shouldn't fail to send message"); - - // auth succeeded - assert_eq!( - result, - UserAgentResponse { - payload: Some(UserAgentResponsePayload::AuthMessage( - arbiter_proto::proto::auth::ServerMessage { - payload: Some(arbiter_proto::proto::auth::server_message::Payload::AuthOk( - AuthOk {}, - )), - }, - )), - } - ); - } -} - -mod transport; -pub(crate) use transport::handle_user_agent; diff --git a/server/crates/arbiter-server/src/actors/user_agent/state.rs b/server/crates/arbiter-server/src/actors/user_agent/state.rs new file mode 100644 index 0000000..866fa6e --- /dev/null +++ b/server/crates/arbiter-server/src/actors/user_agent/state.rs @@ -0,0 +1,76 @@ +use std::sync::Mutex; + +use arbiter_proto::proto::auth::AuthChallenge; +use ed25519_dalek::VerifyingKey; +use x25519_dalek::{EphemeralSecret, PublicKey}; + +/// Context for state machine with validated key and sent challenge +/// Challenge is then transformed to bytes using shared function and verified +#[derive(Clone, Debug)] +pub struct ChallengeContext { + pub challenge: AuthChallenge, + pub key: VerifyingKey, +} + +// Request context with deserialized public key for state machine. +// This intermediate struct is needed because the state machine branches depending on presence of bootstrap token, +// but we want to have the deserialized key in both branches. +#[derive(Clone, Debug)] +pub struct AuthRequestContext { + pub pubkey: VerifyingKey, + pub bootstrap_token: Option, +} + +pub struct UnsealContext { + pub server_public_key: PublicKey, + pub client_public_key: PublicKey, + pub secret: Mutex>, +} + + + +smlang::statemachine!( + name: UserAgent, + custom_error: false, + transitions: { + *Init + AuthRequest(AuthRequestContext) / auth_request_context = ReceivedAuthRequest(AuthRequestContext), + ReceivedAuthRequest(AuthRequestContext) + ReceivedBootstrapToken = Idle, + + ReceivedAuthRequest(AuthRequestContext) + SentChallenge(ChallengeContext) / move_challenge = WaitingForChallengeSolution(ChallengeContext), + + WaitingForChallengeSolution(ChallengeContext) + ReceivedGoodSolution = Idle, + WaitingForChallengeSolution(ChallengeContext) + ReceivedBadSolution = AuthError, // block further transitions, but connection should close anyway + + Idle + UnsealRequest(UnsealContext) / generate_temp_keypair = WaitingForUnsealKey(UnsealContext), + WaitingForUnsealKey(UnsealContext) + ReceivedValidKey = Unsealed, + WaitingForUnsealKey(UnsealContext) + ReceivedInvalidKey = Idle, + } +); + +pub struct DummyContext; +impl UserAgentStateMachineContext for DummyContext { + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + fn move_challenge( + &mut self, + _state_data: &AuthRequestContext, + event_data: ChallengeContext, + ) -> Result { + Ok(event_data) + } + + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + fn auth_request_context( + &mut self, + event_data: AuthRequestContext, + ) -> Result { + Ok(event_data) + } + + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + fn generate_temp_keypair(&mut self, event_data: UnsealContext) -> Result { + Ok(event_data) + } +} diff --git a/server/crates/arbiter-server/src/actors/user_agent/tests.rs b/server/crates/arbiter-server/src/actors/user_agent/tests.rs new file mode 100644 index 0000000..5b4adca --- /dev/null +++ b/server/crates/arbiter-server/src/actors/user_agent/tests.rs @@ -0,0 +1,199 @@ +use arbiter_proto::proto::{ + UserAgentResponse, + auth::{self, AuthChallengeRequest, AuthOk}, + user_agent_response::Payload as UserAgentResponsePayload, +}; +use chrono::format; +use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; +use diesel_async::RunQueryDsl; +use ed25519_dalek::Signer as _; +use kameo::actor::Spawn; + +use crate::{ + actors::{ + bootstrap::BootstrapActor, + user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution}, + }, + db::{self, schema}, +}; + +use super::UserAgentActor; + +#[tokio::test] +#[test_log::test] +pub async fn test_bootstrap_token_auth() { + let db = db::create_test_pool().await; + // explicitly not installing any user_agent pubkeys + let bootstrapper = BootstrapActor::new(&db).await.unwrap(); // this will create bootstrap token + let token = bootstrapper.get_token().unwrap(); + + let bootstrapper_ref = BootstrapActor::spawn(bootstrapper); + let user_agent = UserAgentActor::new_manual( + db.clone(), + bootstrapper_ref, + tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test + ); + let user_agent_ref = UserAgentActor::spawn(user_agent); + + // simulate client sending auth request with bootstrap token + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + let result = user_agent_ref + .ask(HandleAuthChallengeRequest { + req: AuthChallengeRequest { + pubkey: pubkey_bytes, + bootstrap_token: Some(token), + }, + }) + .await + .expect("Shouldn't fail to send message"); + + // auth succeeded + assert_eq!( + result, + UserAgentResponse { + payload: Some(UserAgentResponsePayload::AuthMessage( + arbiter_proto::proto::auth::ServerMessage { + payload: Some(arbiter_proto::proto::auth::server_message::Payload::AuthOk( + AuthOk {}, + )), + }, + )), + } + ); + + // key is succesfully recorded in database + let mut conn = db.get().await.unwrap(); + let stored_pubkey: Vec = schema::useragent_client::table + .select(schema::useragent_client::public_key) + .first::>(&mut conn) + .await + .unwrap(); + assert_eq!(stored_pubkey, new_key.verifying_key().to_bytes().to_vec()); +} + +#[tokio::test] +#[test_log::test] +pub async fn test_bootstrap_invalid_token_auth() { + let db = db::create_test_pool().await; + // explicitly not installing any user_agent pubkeys + let bootstrapper = BootstrapActor::new(&db).await.unwrap(); // this will create bootstrap token + + let bootstrapper_ref = BootstrapActor::spawn(bootstrapper); + let user_agent = UserAgentActor::new_manual( + db.clone(), + bootstrapper_ref, + tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test + ); + let user_agent_ref = UserAgentActor::spawn(user_agent); + + // simulate client sending auth request with bootstrap token + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + let result = user_agent_ref + .ask(HandleAuthChallengeRequest { + req: AuthChallengeRequest { + pubkey: pubkey_bytes, + bootstrap_token: Some("invalid_token".to_string()), + }, + }) + .await; + + match result { + Err(kameo::error::SendError::HandlerError(status)) => { + assert_eq!(status.code(), tonic::Code::InvalidArgument); + insta::assert_debug_snapshot!(status, @r#" + Status { + code: InvalidArgument, + message: "Invalid bootstrap token", + source: None, + } + "#); + } + Err(other) => { + panic!("Expected SendError::HandlerError, got {other:?}"); + } + Ok(_) => { + panic!("Expected error due to invalid bootstrap token, but got success"); + } + } +} + +#[tokio::test] +#[test_log::test] +pub async fn test_challenge_auth() { + let db = db::create_test_pool().await; + + let bootstrapper_ref = BootstrapActor::spawn(BootstrapActor::new(&db).await.unwrap()); + let user_agent = UserAgentActor::new_manual( + db.clone(), + bootstrapper_ref, + tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test + ); + let user_agent_ref = UserAgentActor::spawn(user_agent); + + // simulate client sending auth request with bootstrap token + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + // insert pubkey into database to trigger challenge-response auth flow + { + let mut conn = db.get().await.unwrap(); + insert_into(schema::useragent_client::table) + .values(schema::useragent_client::public_key.eq(pubkey_bytes.clone())) + .execute(&mut conn) + .await + .unwrap(); + } + + let result = user_agent_ref + .ask(HandleAuthChallengeRequest { + req: AuthChallengeRequest { + pubkey: pubkey_bytes, + bootstrap_token: None, + }, + }) + .await + .expect("Shouldn't fail to send message"); + + // auth challenge succeeded + let UserAgentResponse { + payload: + Some(UserAgentResponsePayload::AuthMessage(arbiter_proto::proto::auth::ServerMessage { + payload: + Some(arbiter_proto::proto::auth::server_message::Payload::AuthChallenge(challenge)), + })), + } = result + else { + panic!("Expected auth challenge response, got {result:?}"); + }; + + let formatted_challenge = arbiter_proto::format_challenge(&challenge); + let signature = new_key.sign(&formatted_challenge); + let serialized_signature = signature.to_bytes().to_vec(); + + let result = user_agent_ref + .ask(HandleAuthChallengeSolution { + solution: auth::AuthChallengeSolution { + signature: serialized_signature, + }, + }) + .await + .expect("Shouldn't fail to send message"); + + // auth succeeded + assert_eq!( + result, + UserAgentResponse { + payload: Some(UserAgentResponsePayload::AuthMessage( + arbiter_proto::proto::auth::ServerMessage { + payload: Some(arbiter_proto::proto::auth::server_message::Payload::AuthOk( + AuthOk {}, + )), + }, + )), + } + ); +} diff --git a/server/crates/arbiter-server/src/actors/user_agent/transport.rs b/server/crates/arbiter-server/src/actors/user_agent/transport.rs index 2114d56..bf54094 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/transport.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/transport.rs @@ -2,12 +2,9 @@ use super::UserAgentActor; 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, + ClientMessage as ClientAuthMessage, client_message::Payload as ClientAuthPayload, }, user_agent_request::Payload as UserAgentRequestPayload, - user_agent_response::Payload as UserAgentResponsePayload, }; use futures::StreamExt; use kameo::{ @@ -19,7 +16,10 @@ use tonic::Status; use tracing::error; use crate::{ - actors::user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution}, + actors::user_agent::{ + HandleAuthChallengeRequest, HandleAuthChallengeSolution, HandleUnsealEncryptedKey, + HandleUnsealRequest, + }, context::ServerContext, }; @@ -59,28 +59,30 @@ async fn process_message( Status::invalid_argument("Expected message with payload") })?; - let UserAgentRequestPayload::AuthMessage(ClientMessage { - payload: Some(client_message), - }) = msg - else { - error!( - actor = "useragent", - "Received unexpected message type during authentication" - ); - return Err(Status::invalid_argument( - "Expected AuthMessage with ClientMessage payload", - )); - }; - - match client_message { - ClientAuthPayload::AuthChallengeRequest(req) => actor + match msg { + UserAgentRequestPayload::AuthMessage(ClientAuthMessage { + payload: Some(ClientAuthPayload::AuthChallengeRequest(req)), + }) => actor .ask(HandleAuthChallengeRequest { req }) .await .map_err(into_status), - ClientAuthPayload::AuthChallengeSolution(solution) => actor + UserAgentRequestPayload::AuthMessage(ClientAuthMessage { + payload: Some(ClientAuthPayload::AuthChallengeSolution(solution)), + }) => actor .ask(HandleAuthChallengeSolution { solution }) .await .map_err(into_status), + UserAgentRequestPayload::UnsealStart(unseal_start) => actor + .ask(HandleUnsealRequest { req: unseal_start }) + .await + .map_err(into_status), + UserAgentRequestPayload::UnsealEncryptedKey(unseal_encrypted_key) => actor + .ask(HandleUnsealEncryptedKey { + req: unseal_encrypted_key, + }) + .await + .map_err(into_status), + _ => Err(Status::invalid_argument("Expected message with payload")), } } diff --git a/server/crates/arbiter-server/src/context.rs b/server/crates/arbiter-server/src/context.rs index 6da6a80..0afffd2 100644 --- a/server/crates/arbiter-server/src/context.rs +++ b/server/crates/arbiter-server/src/context.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use diesel::OptionalExtension as _; use diesel_async::RunQueryDsl as _; -use ed25519_dalek::VerifyingKey; use kameo::actor::{ActorRef, Spawn}; use miette::Diagnostic; use rand::rngs::StdRng; @@ -11,21 +10,15 @@ use thiserror::Error; use tokio::sync::RwLock; use crate::{ - context::{ - bootstrap::{BootstrapActor, generate_token}, - lease::LeaseHandler, - tls::{TlsDataRaw, TlsManager}, - }, - db::{ + actors::bootstrap::{self, BootstrapActor}, context::tls::{TlsDataRaw, TlsManager}, db::{ self, models::ArbiterSetting, - schema::{self, arbiter_settings}, - }, + schema::arbiter_settings, + } }; -pub(crate) mod bootstrap; -pub(crate) mod lease; -pub(crate) mod tls; + +pub mod tls; #[derive(Error, Debug, Diagnostic)] pub enum InitError { @@ -78,7 +71,7 @@ impl ServerStateMachineContext for _Context { } } -pub(crate) struct _ServerContextInner { +pub struct _ServerContextInner { pub db: db::DatabasePool, pub state: RwLock>, pub rng: StdRng, @@ -86,7 +79,7 @@ pub(crate) struct _ServerContextInner { pub bootstrapper: ActorRef, } #[derive(Clone)] -pub(crate) struct ServerContext(Arc<_ServerContextInner>); +pub struct ServerContext(Arc<_ServerContextInner>); impl std::ops::Deref for ServerContext { type Target = _ServerContextInner; diff --git a/server/crates/arbiter-server/src/context/lease.rs b/server/crates/arbiter-server/src/context/lease.rs deleted file mode 100644 index 2b0f2bd..0000000 --- a/server/crates/arbiter-server/src/context/lease.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::sync::Arc; - -use dashmap::DashSet; - -#[derive(Clone, Default)] -struct LeaseStorage(Arc>); - -// A lease that automatically releases the item when dropped -pub struct Lease { - item: T, - storage: LeaseStorage, -} -impl Drop for Lease { - fn drop(&mut self) { - self.storage.0.remove(&self.item); - } -} - -#[derive(Clone, Default)] -pub struct LeaseHandler { - storage: LeaseStorage, -} - -impl LeaseHandler { - pub fn new() -> Self { - Self { - storage: LeaseStorage(Arc::new(DashSet::new())), - } - } - - pub fn acquire(&self, item: T) -> Result, ()> { - if self.storage.0.insert(item.clone()) { - Ok(Lease { - item, - storage: self.storage.clone(), - }) - } else { - Err(()) - } - } -} diff --git a/server/crates/arbiter-server/src/context/tls.rs b/server/crates/arbiter-server/src/context/tls.rs index ce9b1b4..267c8b2 100644 --- a/server/crates/arbiter-server/src/context/tls.rs +++ b/server/crates/arbiter-server/src/context/tls.rs @@ -60,7 +60,7 @@ fn generate_cert(key: &KeyPair) -> Result { } // TODO: Implement cert rotation -pub(crate) struct TlsManager { +pub struct TlsManager { data: TlsData, } diff --git a/server/crates/arbiter-server/src/db.rs b/server/crates/arbiter-server/src/db.rs index c44d489..16567c6 100644 --- a/server/crates/arbiter-server/src/db.rs +++ b/server/crates/arbiter-server/src/db.rs @@ -1,12 +1,11 @@ -use std::sync::Arc; use diesel::{ Connection as _, SqliteConnection, - connection::{SimpleConnection as _, TransactionManager}, + connection::SimpleConnection as _, }; use diesel_async::{ AsyncConnection, SimpleAsyncConnection, - pooled_connection::{AsyncDieselConnectionManager, ManagerConfig, RecyclingMethod}, + pooled_connection::{AsyncDieselConnectionManager, ManagerConfig}, sync_connection_wrapper::SyncConnectionWrapper, }; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 7417875..a3a35dd 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -1,29 +1,56 @@ #![allow(unused)] #![allow(clippy::all)] -use crate::db::schema::{self, aead_encrypted, arbiter_settings}; +use crate::db::schema::{self, aead_encrypted, arbiter_settings, root_key_history}; use diesel::{prelude::*, sqlite::Sqlite}; +use restructed::Models; pub mod types { use chrono::{DateTime, Utc}; pub struct SqliteTimestamp(DateTime); } -#[derive(Queryable, Debug, Insertable)] +#[derive(Models, Queryable, Debug, Insertable, Selectable)] +#[view( + NewAeadEncrypted, + derive(Insertable), + omit(id), + attributes_with = "deriveless" +)] #[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))] pub struct AeadEncrypted { pub id: i32, pub ciphertext: Vec, pub tag: Vec, - pub current_nonce: i32, + pub current_nonce: Vec, pub schema_version: i32, + pub associated_root_key_id: i32, // references root_key_history.id + pub created_at: i32, +} + +#[derive(Models, Queryable, Debug, Insertable, Selectable)] +#[diesel(table_name = root_key_history, check_for_backend(Sqlite))] +#[view( + NewRootKeyHistory, + derive(Insertable), + omit(id), + attributes_with = "deriveless" +)] +pub struct RootKeyHistory { + pub id: i32, + pub ciphertext: Vec, + pub tag: Vec, + pub root_key_encryption_nonce: Vec, + pub data_encryption_nonce: Vec, + pub schema_version: i32, + pub salt: Vec, } #[derive(Queryable, Debug, Insertable)] #[diesel(table_name = arbiter_settings, check_for_backend(Sqlite))] pub struct ArbiterSetting { pub id: i32, - pub root_key_id: Option, // references aead_encrypted.id + pub root_key_id: Option, // references root_key_history.id pub cert_key: Vec, pub cert: Vec, } diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 38f8afe..7ba8193 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -3,10 +3,12 @@ diesel::table! { aead_encrypted (id) { id -> Integer, - current_nonce -> Integer, + current_nonce -> Binary, ciphertext -> Binary, tag -> Binary, schema_version -> Integer, + associated_root_key_id -> Integer, + created_at -> Integer, } } @@ -29,6 +31,18 @@ diesel::table! { } } +diesel::table! { + root_key_history (id) { + id -> Integer, + root_key_encryption_nonce -> Binary, + data_encryption_nonce -> Binary, + ciphertext -> Binary, + tag -> Binary, + schema_version -> Integer, + salt -> Binary, + } +} + diesel::table! { useragent_client (id) { id -> Integer, @@ -39,11 +53,13 @@ diesel::table! { } } -diesel::joinable!(arbiter_settings -> aead_encrypted (root_key_id)); +diesel::joinable!(aead_encrypted -> root_key_history (associated_root_key_id)); +diesel::joinable!(arbiter_settings -> root_key_history (root_key_id)); diesel::allow_tables_to_appear_in_same_query!( aead_encrypted, arbiter_settings, program_client, + root_key_history, useragent_client, ); diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index 921bb8e..9d86e27 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -1,7 +1,4 @@ -#![allow(unused)] - -use std::sync::Arc; - +#![forbid(unsafe_code)] use arbiter_proto::{ proto::{ClientRequest, ClientResponse, UserAgentRequest, UserAgentResponse}, transport::BiStream, @@ -18,8 +15,8 @@ use crate::{ }; pub mod actors; -mod context; -mod db; +pub mod context; +pub mod db; mod errors; const DEFAULT_CHANNEL_SIZE: usize = 1000; @@ -28,6 +25,12 @@ pub struct Server { context: ServerContext, } +impl Server { + pub fn new(context: ServerContext) -> Self { + Self { context } + } +} + #[async_trait] impl arbiter_proto::proto::arbiter_service_server::ArbiterService for Server { type UserAgentStream = ReceiverStream>; diff --git a/server/crates/arbiter-server/src/main.rs b/server/crates/arbiter-server/src/main.rs new file mode 100644 index 0000000..5e3a3b9 --- /dev/null +++ b/server/crates/arbiter-server/src/main.rs @@ -0,0 +1,34 @@ +use arbiter_proto::proto::arbiter_service_server::ArbiterServiceServer; +use arbiter_server::{Server, context::ServerContext, db}; +use tracing::info; + +#[tokio::main] +async fn main() -> miette::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + info!("Starting arbiter server"); + + info!("Initializing database"); + let db = db::create_pool(None).await?; + info!("Database ready"); + + info!("Initializing server context"); + let context = ServerContext::new(db).await?; + info!("Server context ready"); + + let addr = "[::1]:50051".parse().expect("valid address"); + info!(%addr, "Starting gRPC server"); + + tonic::transport::Server::builder() + .add_service(ArbiterServiceServer::new(Server::new(context))) + .serve(addr) + .await + .map_err(|e| miette::miette!("gRPC server error: {e}"))?; + + unreachable!("gRPC server should run indefinitely"); +} diff --git a/server/supply-chain/audits.toml b/server/supply-chain/audits.toml index f7e6d2e..677bc7d 100644 --- a/server/supply-chain/audits.toml +++ b/server/supply-chain/audits.toml @@ -1,6 +1,11 @@ # cargo-vet audits file +[[audits.similar]] +who = "hdbg " +criteria = "safe-to-deploy" +version = "2.2.1" + [[audits.test-log]] who = "hdbg " criteria = "safe-to-deploy" @@ -11,6 +16,12 @@ who = "hdbg " criteria = "safe-to-deploy" delta = "0.2.18 -> 0.2.19" +[[trusted.cc]] +criteria = "safe-to-deploy" +user-id = 55123 # rust-lang-owner +start = "2022-10-29" +end = "2027-02-16" + [[trusted.h2]] criteria = "safe-to-deploy" user-id = 359 # Sean McArthur (seanmonstar) @@ -29,6 +40,12 @@ user-id = 359 # Sean McArthur (seanmonstar) start = "2022-01-15" end = "2027-02-14" +[[trusted.libc]] +criteria = "safe-to-deploy" +user-id = 55123 # rust-lang-owner +start = "2024-08-15" +end = "2027-02-16" + [[trusted.rustix]] criteria = "safe-to-deploy" user-id = 6825 # Dan Gohman (sunfishcode) @@ -46,3 +63,33 @@ criteria = "safe-to-deploy" user-id = 3618 # David Tolnay (dtolnay) start = "2019-03-01" end = "2027-02-14" + +[[trusted.thread_local]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-09-07" +end = "2027-02-16" + +[[trusted.toml]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2022-12-14" +end = "2027-02-16" + +[[trusted.toml_parser]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2025-07-08" +end = "2027-02-16" + +[[trusted.tonic-build]] +criteria = "safe-to-deploy" +user-id = 10 +start = "2019-09-10" +end = "2027-02-16" + +[[trusted.windows-sys]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-15" +end = "2027-02-16" diff --git a/server/supply-chain/config.toml b/server/supply-chain/config.toml index e4404b4..f60f5f6 100644 --- a/server/supply-chain/config.toml +++ b/server/supply-chain/config.toml @@ -13,6 +13,9 @@ url = "https://raw.githubusercontent.com/google/supply-chain/main/audits.toml" [imports.mozilla] url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml" +[imports.zcash] +url = "https://raw.githubusercontent.com/zcash/rust-ecosystem/main/supply-chain/audits.toml" + [[exemptions.addr2line]] version = "0.25.1" criteria = "safe-to-deploy" @@ -41,10 +44,6 @@ criteria = "safe-to-deploy" 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" @@ -193,10 +192,6 @@ criteria = "safe-to-deploy" 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" @@ -209,10 +204,6 @@ criteria = "safe-to-deploy" 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" @@ -261,10 +252,6 @@ criteria = "safe-to-deploy" 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" @@ -329,10 +316,6 @@ criteria = "safe-to-deploy" 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" @@ -525,10 +508,6 @@ criteria = "safe-to-deploy" 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" @@ -545,10 +524,6 @@ criteria = "safe-to-deploy" 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" @@ -653,10 +628,6 @@ criteria = "safe-to-deploy" 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" @@ -689,14 +660,6 @@ criteria = "safe-to-deploy" 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" @@ -741,10 +704,6 @@ criteria = "safe-to-deploy" 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" @@ -769,10 +728,6 @@ criteria = "safe-to-deploy" 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" @@ -817,10 +772,6 @@ criteria = "safe-to-deploy" 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" @@ -829,18 +780,6 @@ criteria = "safe-to-deploy" 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" @@ -925,10 +864,6 @@ criteria = "safe-to-deploy" 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" diff --git a/server/supply-chain/imports.lock b/server/supply-chain/imports.lock index a35eb4b..0692e20 100644 --- a/server/supply-chain/imports.lock +++ b/server/supply-chain/imports.lock @@ -41,6 +41,12 @@ user-id = 359 user-login = "seanmonstar" user-name = "Sean McArthur" +[[publisher.libc]] +version = "0.2.182" +when = "2026-02-13" +user-id = 55123 +user-login = "rust-lang-owner" + [[publisher.rustix]] version = "1.1.3" when = "2025-12-23" @@ -63,12 +69,33 @@ user-login = "dtolnay" user-name = "David Tolnay" [[publisher.syn]] -version = "2.0.114" -when = "2026-01-07" +version = "2.0.115" +when = "2026-02-12" user-id = 3618 user-login = "dtolnay" user-name = "David Tolnay" +[[publisher.thread_local]] +version = "1.1.9" +when = "2025-06-12" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.toml]] +version = "0.9.12+spec-1.1.0" +when = "2026-02-10" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.toml_parser]] +version = "1.0.8+spec-1.1.0" +when = "2026-02-12" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + [[publisher.unicode-width]] version = "0.1.14" when = "2024-09-19" @@ -120,6 +147,34 @@ version = "0.244.0" when = "2026-01-06" trusted-publisher = "github:bytecodealliance/wasm-tools" +[[publisher.windows-sys]] +version = "0.52.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.59.0" +when = "2024-07-30" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.60.2" +when = "2025-06-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.61.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + [[publisher.wit-bindgen]] version = "0.51.0" when = "2026-01-12" @@ -265,6 +320,12 @@ criteria = "safe-to-deploy" version = "1.1.2" notes = "Contains `unsafe` code but it's well-documented and scoped to what it's intended to be doing. Otherwise a well-focused and straightforward crate." +[[audits.bytecode-alliance.audits.cipher]] +who = "Andrew Brown " +criteria = "safe-to-deploy" +version = "0.4.4" +notes = "Most unsafe is hidden by `inout` dependency; only remaining unsafe is raw-splitting a slice and an unreachable hint. Older versions of this regularly reach ~150k daily downloads." + [[audits.bytecode-alliance.audits.core-foundation-sys]] who = "Dan Gohman " criteria = "safe-to-deploy" @@ -279,6 +340,23 @@ who = "Nick Fitzgerald " criteria = "safe-to-deploy" delta = "0.2.4 -> 0.2.5" +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +version = "0.3.0" +notes = "This crate uses libc and windows-sys APIs to get and set the raw OS error value." + +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +notes = "Just a dependency version bump and a bug fix for redox" + +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.3.9 -> 0.3.10" + [[audits.bytecode-alliance.audits.fastrand]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -385,11 +463,28 @@ criteria = "safe-to-deploy" delta = "0.4.1 -> 0.5.0" notes = "Minor changes for a `no_std` upgrade but otherwise everything looks as expected." +[[audits.bytecode-alliance.audits.http-body]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "1.0.0-rc.2" + +[[audits.bytecode-alliance.audits.http-body]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "1.0.0-rc.2 -> 1.0.0" +notes = "Only minor changes made for a stable release." + [[audits.bytecode-alliance.audits.iana-time-zone-haiku]] who = "Dan Gohman " criteria = "safe-to-deploy" version = "0.1.2" +[[audits.bytecode-alliance.audits.inout]] +who = "Andrew Brown " +criteria = "safe-to-deploy" +version = "0.1.3" +notes = "A part of RustCrypto/utils, this crate is designed to handle unsafe buffers and carefully documents the safety concerns throughout. Older versions of this tally up to ~130k daily downloads." + [[audits.bytecode-alliance.audits.leb128fmt]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -443,6 +538,24 @@ criteria = "safe-to-deploy" delta = "0.8.5 -> 0.8.9" notes = "No new unsafe code, just refactorings." +[[audits.bytecode-alliance.audits.nu-ansi-term]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.46.0" +notes = "one use of unsafe to call windows specific api to get console handle." + +[[audits.bytecode-alliance.audits.nu-ansi-term]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.46.0 -> 0.50.1" +notes = "Lots of stylistic/rust-related chanegs, plus new features, but nothing out of the ordrinary." + +[[audits.bytecode-alliance.audits.nu-ansi-term]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.50.1 -> 0.50.3" +notes = "CI changes, Rust changes, nothing out of the ordinary." + [[audits.bytecode-alliance.audits.num-traits]] who = "Andrew Brown " criteria = "safe-to-deploy" @@ -537,12 +650,38 @@ criteria = "safe-to-run" delta = "0.2.16 -> 0.2.18" notes = "Standard macro changes, nothing out of place" +[[audits.bytecode-alliance.audits.tracing-log]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.1.3" +notes = """ +This is a standard adapter between the `log` ecosystem and the `tracing` +ecosystem. There's one `unsafe` block in this crate and it's well-scoped. +""" + +[[audits.bytecode-alliance.audits.tracing-log]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.1.3 -> 0.2.0" +notes = "Nothing out of the ordinary, a typical major version update and nothing awry." + +[[audits.bytecode-alliance.audits.try-lock]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.2.4" +notes = "Implements a concurrency primitive with atomics, and is not obviously incorrect" + [[audits.bytecode-alliance.audits.vcpkg]] who = "Pat Hickey " criteria = "safe-to-deploy" version = "0.2.15" notes = "no build.rs, no macros, no unsafe. It reads the filesystem and makes copies of DLLs into OUT_DIR." +[[audits.bytecode-alliance.audits.want]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.0" + [[audits.bytecode-alliance.audits.wasm-metadata]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -591,6 +730,13 @@ criteria = "safe-to-deploy" delta = "0.243.0 -> 0.244.0" notes = "The Bytecode Alliance is the author of this crate" +[[audits.google.audits.autocfg]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "1.4.0" +notes = "Contains no unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.base64]] who = "amarjotgill " criteria = "safe-to-deploy" @@ -719,6 +865,89 @@ delta = "0.2.9 -> 0.2.13" notes = "Audited at https://fxrev.dev/946396" aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.proc-macro-error-attr]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.rand_core]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "0.6.4" +notes = """ +For more detailed unsafe review notes please see https://crrev.com/c/6362797 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.14" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` +and there were no hits except for: + +* Using trivially-safe `unsafe` in test code: + + ``` + tests/test_const.rs:unsafe fn _unsafe() {} + tests/test_const.rs:const _UNSAFE: () = unsafe { _unsafe() }; + ``` + +* Using `unsafe` in a string: + + ``` + src/constfn.rs: "unsafe" => Qualifiers::Unsafe, + ``` + +* Using `std::fs` in `build/build.rs` to write `${OUT_DIR}/version.expr` + which is later read back via `include!` used in `src/lib.rs`. + +Version `1.0.6` of this crate has been added to Chromium in +https://source.chromium.org/chromium/chromium/src/+/28841c33c77833cc30b286f9ae24c97e7a8f4057 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.14 -> 1.0.15" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.15 -> 1.0.16" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.16 -> 1.0.17" +notes = "Just updates windows compat" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Liza Burakova " +criteria = "safe-to-deploy" +delta = "1.0.17 -> 1.0.18" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.18 -> 1.0.19" +notes = "No unsafe, just doc changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rustversion]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "1.0.19 -> 1.0.20" +notes = "Only minor updates to documentation and the mock today used for testing." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.smallvec]] who = "Manish Goregaokar " criteria = "safe-to-deploy" @@ -736,6 +965,28 @@ Previously reviewed during security review and the audit is grandparented in. """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.strum]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.25.0" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.strum_macros]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.25.3" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.mozilla.wildcard-audits.core-foundation-sys]] who = "Bobby Holley " criteria = "safe-to-deploy" @@ -812,6 +1063,12 @@ criteria = "safe-to-deploy" delta = "0.2.3 -> 0.2.4" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.errno]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.1 -> 0.3.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.fastrand]] who = "Mike Hommey " criteria = "safe-to-deploy" @@ -929,6 +1186,16 @@ yet, but it's all valid. Otherwise it's a pretty simple crate. """ aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.rustc_version]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = """ +Use of powerful capabilities is limited to invoking `rustc -vV` to get version +information for parsing version information. +""" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + [[audits.mozilla.audits.serde_spanned]] who = "Ben Dean-Kawamura " criteria = "safe-to-deploy" @@ -955,6 +1222,12 @@ criteria = "safe-to-deploy" delta = "1.1.0 -> 1.3.0" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.similar]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +delta = "2.2.1 -> 2.7.0" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + [[audits.mozilla.audits.smallvec]] who = "Erich Gubler " criteria = "safe-to-deploy" @@ -967,6 +1240,30 @@ criteria = "safe-to-deploy" delta = "0.10.0 -> 0.11.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.strum]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.25.0 -> 0.26.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.26.3 -> 0.27.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum_macros]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.25.3 -> 0.26.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum_macros]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.26.4 -> 0.27.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.synstructure]] who = "Nika Layzell " criteria = "safe-to-deploy" @@ -1038,3 +1335,153 @@ who = "Jan-Erik Rediger " criteria = "safe-to-deploy" version = "0.1.5" aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.windows-link]] +who = "Mark Hammond " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "A microsoft crate allowing unsafe calls to windows apis." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.windows-link]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.2.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.zeroize]] +who = "Benjamin Beurdouche " +criteria = "safe-to-deploy" +version = "1.8.1" +notes = """ +This code DOES contain unsafe code required to internally call volatiles +for deleting data. This is expected and documented behavior. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.zcash.audits.autocfg]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +notes = "Filesystem change is to remove the generated LLVM IR output file after probing." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.dunce]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "1.0.5" +notes = """ +Does what it says on the tin. No `unsafe`, and the only IO is `std::fs::canonicalize`. +Path and string handling looks plausibly correct. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.3 -> 0.3.8" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.8 -> 0.3.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.10 -> 0.3.11" +notes = "The `__errno` location for vxworks and cygwin looks correct from a quick search." +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.11 -> 0.3.13" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.13 -> 0.3.14" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.http-body]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.0 -> 1.0.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.inout]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.3 -> 0.1.4" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.rustc_version]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.4.1" +notes = "Changes to `Command` usage are to add support for `RUSTC_WRAPPER`." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustversion]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.20 -> 1.0.21" +notes = "Build script change is to fix building with `-Zfmt-debug=none`." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustversion]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.21 -> 1.0.22" +notes = "Changes to generated code are to prepend a clippy annotation." +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.strum]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.27.1 -> 0.27.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.strum_macros]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.27.1 -> 0.27.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.try-lock]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.5" +notes = "Bumps MSRV to remove unsafe code block." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.want]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +notes = """ +Migrates to `try-lock 0.2.4` to replace some unsafe APIs that were not marked +`unsafe` (but that were being used safely). +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.windows-link]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.2.1" +notes = "No code changes at all." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.zeroize]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.8.1 -> 1.8.2" +notes = """ +Changes to `unsafe` code are to alter how `core::mem::size_of` is named; no actual changes +to the `unsafe` logic. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml"