From 4c6e860d6959686065d25933b72587c3df0a7d55 Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Wed, 14 Aug 2024 18:48:59 -0300 Subject: [PATCH] Add user registration (#252) * add user registration * addint support to coordinator and participant * Update participant/src/comms/http.rs Co-authored-by: Pili Guerra --------- Co-authored-by: Pili Guerra --- Cargo.lock | 788 +++++++++++++++++- coordinator/src/args.rs | 15 + coordinator/src/cli.rs | 2 +- coordinator/src/comms/http.rs | 63 +- coordinator/src/step_1.rs | 4 +- participant/src/args.rs | 9 + participant/src/cli.rs | 2 +- participant/src/comms/http.rs | 72 +- participant/src/tests/round1.rs | 2 + server/Cargo.toml | 6 +- server/build.rs | 5 + .../migrations/20240627222152_init.down.sql | 3 + server/migrations/20240627222152_init.up.sql | 16 + server/src/args.rs | 4 + server/src/functions.rs | 256 +++++- server/src/lib.rs | 16 +- server/src/state.rs | 26 +- server/src/types.rs | 28 + server/src/user.rs | 237 ++++++ server/tests/integration_tests.rs | 117 ++- 20 files changed, 1594 insertions(+), 77 deletions(-) create mode 100644 server/build.rs create mode 100644 server/migrations/20240627222152_init.down.sql create mode 100644 server/migrations/20240627222152_init.up.sql create mode 100644 server/src/user.rs diff --git a/Cargo.lock b/Cargo.lock index 21174db..0622d5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +39,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anstream" version = "0.6.15" @@ -81,6 +100,18 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.8" @@ -104,6 +135,15 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -187,13 +227,47 @@ dependencies = [ ] [[package]] -name = "axum-test" -version = "14.10.0" +name = "axum-extra" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167294800740b4b6bc7bfbccbf3a1d50a6c6e097342580ec4c11d1672e456292" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http 1.1.0", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "axum-test" +version = "15.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd2b6c11bc5e65ec121543c5049b7e07be9e7b5a515df79d01f74a72c6a15f0" dependencies = [ "anyhow", - "async-trait", "auto-future", "axum", "bytes", @@ -236,12 +310,24 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -253,6 +339,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -266,6 +355,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "blake2b_simd" version = "1.0.2" @@ -385,6 +483,12 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808ac43170e95b11dd23d78aa9eaac5bea45776a602955552c4e833f3f0f823d" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.3.0" @@ -449,6 +553,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "critical-section" version = "1.1.2" @@ -464,6 +583,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -519,6 +647,17 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f400d0750c0c069e8493f2256cb4da6f604b6d2eeb69a0ca8863acde352f8400" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -563,7 +702,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -593,11 +734,20 @@ dependencies = [ "litrs", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] [[package]] name = "embedded-io" @@ -630,6 +780,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "exitcode" version = "1.1.2" @@ -668,6 +835,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -761,6 +939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -769,6 +948,28 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -795,6 +996,7 @@ checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -819,8 +1021,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -873,6 +1077,43 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 1.1.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.1.0", +] [[package]] name = "heapless" @@ -893,6 +1134,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "heck" @@ -915,6 +1159,33 @@ dependencies = [ "serde", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1133,6 +1404,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1140,6 +1414,23 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1183,6 +1474,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1235,6 +1536,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -1297,6 +1604,16 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1307,12 +1624,59 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "object" version = "0.36.2" @@ -1396,7 +1760,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.3", "smallvec", "windows-targets 0.52.6", ] @@ -1422,6 +1786,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "password-auth" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524" +dependencies = [ + "argon2", + "getrandom", + "password-hash", + "rand_core", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pasta_curves" version = "0.5.1" @@ -1435,6 +1822,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1482,6 +1884,27 @@ dependencies = [ "crossbeam-channel", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1512,7 +1935,7 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" dependencies = [ - "zerocopy", + "zerocopy 0.6.6", ] [[package]] @@ -1615,6 +2038,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.3" @@ -1674,7 +2106,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -1736,6 +2168,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-multipart-rfc7578_2" version = "0.6.1" @@ -1799,7 +2251,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64", + "base64 0.22.1", "rustls-pki-types", ] @@ -1945,6 +2397,8 @@ name = "server" version = "0.1.0" dependencies = [ "axum", + "axum-extra", + "axum-macros", "axum-test", "clap", "derivative", @@ -1952,6 +2406,7 @@ dependencies = [ "frost-core", "frost-ed25519", "frost-rerandomized", + "password-auth", "rand", "reddsa 0.5.1 (git+https://github.com/ZcashFoundation/reddsa.git?rev=4d8c4bb337231e6e89117334d7c61dada589a953)", "regex", @@ -1959,6 +2414,7 @@ dependencies = [ "serde", "serde_json", "serdect", + "sqlx", "tokio", "tower-http", "tracing", @@ -2006,6 +2462,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "slab" version = "0.4.9" @@ -2040,6 +2506,228 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.6.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.6.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", + "uuid", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2052,6 +2740,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2323,6 +3022,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -2521,6 +3231,24 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "untrusted" version = "0.9.0" @@ -2538,6 +3266,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -2605,6 +3339,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -2681,6 +3421,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2874,7 +3624,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.6.6", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", ] [[package]] @@ -2888,6 +3647,17 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/coordinator/src/args.rs b/coordinator/src/args.rs index 4db3369..0a53446 100644 --- a/coordinator/src/args.rs +++ b/coordinator/src/args.rs @@ -17,6 +17,21 @@ pub struct Args { #[arg(long, default_value_t = false)] pub http: bool, + /// The username to use in HTTP mode. + #[arg(short = 'u', long, default_value = "")] + pub username: String, + + /// The password to use in HTTP mode. If specified, it will be read from the + /// environment variable with the given name. + #[arg(short = 'w', long, default_value = "")] + pub password: String, + + /// The comma-separated usernames of the signers to use in HTTP mode. + /// If HTTP mode is enabled and this is empty, then the session ID + /// will be printed and will have to be shared manually. + #[arg(short = 'S', long, value_delimiter = ',')] + pub signers: Vec, + /// The number of participants. If 0, will prompt for a value. #[arg(short = 'n', long, default_value_t = 0)] pub num_signers: u16, diff --git a/coordinator/src/cli.rs b/coordinator/src/cli.rs index 1f62102..1dc87f1 100644 --- a/coordinator/src/cli.rs +++ b/coordinator/src/cli.rs @@ -22,7 +22,7 @@ pub async fn cli( let mut comms: Box> = if args.cli { Box::new(CLIComms::new()) } else if args.http { - Box::new(HTTPComms::new(args)) + Box::new(HTTPComms::new(args)?) } else { Box::new(SocketComms::new(args)) }; diff --git a/coordinator/src/comms/http.rs b/coordinator/src/comms/http.rs index cde3dc5..da76f07 100644 --- a/coordinator/src/comms/http.rs +++ b/coordinator/src/comms/http.rs @@ -16,10 +16,12 @@ use frost::{ use std::{ collections::BTreeMap, + env, error::Error, io::{BufRead, Write}, marker::PhantomData, time::Duration, + vec, }; use super::Comms; @@ -29,18 +31,27 @@ pub struct HTTPComms { client: reqwest::Client, host_port: String, session_id: Option, + username: String, + password: String, + access_token: String, + signers: Vec, _phantom: PhantomData, } impl HTTPComms { - pub fn new(args: &Args) -> Self { + pub fn new(args: &Args) -> Result> { let client = reqwest::Client::new(); - Self { + let password = env::var(&args.password).map_err(|_| eyre!("The password argument must specify the name of a environment variable containing the password"))?; + Ok(Self { client, host_port: format!("http://{}:{}", args.ip, args.port), session_id: None, + username: args.username.clone(), + password, + access_token: String::new(), + signers: args.signers.clone(), _phantom: Default::default(), - } + }) } } @@ -53,10 +64,26 @@ impl Comms for HTTPComms { _pub_key_package: &PublicKeyPackage, num_signers: u16, ) -> Result, SigningCommitments>, Box> { + self.access_token = self + .client + .post(format!("{}/login", self.host_port)) + .json(&server::LoginArgs { + username: self.username.clone(), + password: self.password.clone(), + }) + .send() + .await? + .json::() + .await? + .access_token + .to_string(); + let r = self .client .post(format!("{}/create_new_session", self.host_port)) + .bearer_auth(&self.access_token) .json(&server::CreateNewSessionArgs { + usernames: self.signers.clone(), num_signers, message_count: 1, }) @@ -65,10 +92,12 @@ impl Comms for HTTPComms { .json::() .await?; - eprintln!( - "Send the following session ID to participants: {}", - r.session_id - ); + if self.signers.is_empty() { + eprintln!( + "Send the following session ID to participants: {}", + r.session_id + ); + } self.session_id = Some(r.session_id); eprint!("Waiting for participants to send their commitments..."); @@ -76,6 +105,7 @@ impl Comms for HTTPComms { let r = self .client .post(format!("{}/get_commitments", self.host_port)) + .bearer_auth(&self.access_token) .json(&server::GetCommitmentsArgs { session_id: r.session_id, }) @@ -114,6 +144,7 @@ impl Comms for HTTPComms { let _r = self .client .post(format!("{}/send_signing_package", self.host_port)) + .bearer_auth(&self.access_token) .json(&server::SendSigningPackageArgs { aux_msg: Default::default(), session_id: self.session_id.unwrap(), @@ -131,6 +162,7 @@ impl Comms for HTTPComms { let r = self .client .post(format!("{}/get_signature_shares", self.host_port)) + .bearer_auth(&self.access_token) .json(&server::GetSignatureSharesArgs { session_id: self.session_id.unwrap(), }) @@ -145,6 +177,23 @@ impl Comms for HTTPComms { }; eprintln!(); + let _r = self + .client + .post(format!("{}/close_session", self.host_port)) + .bearer_auth(&self.access_token) + .json(&server::CloseSessionArgs { + session_id: self.session_id.unwrap(), + }) + .send() + .await?; + + let _r = self + .client + .post(format!("{}/logout", self.host_port)) + .bearer_auth(&self.access_token) + .send() + .await?; + let signature_shares = r .signature_shares .first() diff --git a/coordinator/src/step_1.rs b/coordinator/src/step_1.rs index c4be946..47dadce 100755 --- a/coordinator/src/step_1.rs +++ b/coordinator/src/step_1.rs @@ -48,7 +48,9 @@ async fn read_commitments( let pub_key_package: PublicKeyPackage = serde_json::from_str(&out)?; - let num_of_participants = if args.num_signers == 0 { + let num_of_participants = if !args.signers.is_empty() { + args.signers.len() as u16 + } else if args.num_signers == 0 { writeln!(logger, "The number of participants: ")?; let mut participants = String::new(); diff --git a/participant/src/args.rs b/participant/src/args.rs index 63a0dc5..8bfc956 100644 --- a/participant/src/args.rs +++ b/participant/src/args.rs @@ -17,6 +17,15 @@ pub struct Args { #[arg(long, default_value_t = false)] pub http: bool, + /// The username to use in HTTP mode. + #[arg(short = 'u', long, default_value = "")] + pub username: String, + + /// The password to use in HTTP mode. If specified, it will be read from the + /// environment variable with the given name. + #[arg(short = 'w', long, default_value = "")] + pub password: String, + /// Public key package to use. Can be a file with a JSON-encoded /// package, or "". If the file does not exist or if "" is specified, /// then it will be read from standard input. diff --git a/participant/src/cli.rs b/participant/src/cli.rs index 1307c43..c6a9cf1 100644 --- a/participant/src/cli.rs +++ b/participant/src/cli.rs @@ -20,7 +20,7 @@ pub async fn cli( let mut comms: Box> = if args.cli { Box::new(CLIComms::new()) } else if args.http { - Box::new(HTTPComms::new(args)) + Box::new(HTTPComms::new(args)?) } else { Box::new(SocketComms::new(args)) }; diff --git a/participant/src/comms/http.rs b/participant/src/comms/http.rs index 78755dd..93dd8c0 100644 --- a/participant/src/comms/http.rs +++ b/participant/src/comms/http.rs @@ -10,6 +10,7 @@ use frost::{round1::SigningCommitments, round2::SignatureShare, Identifier}; use super::Comms; +use std::env; use std::io::{BufRead, Write}; use std::error::Error; @@ -22,7 +23,10 @@ use crate::args::Args; pub struct HTTPComms { client: reqwest::Client, host_port: String, - session_id: Uuid, + session_id: Option, + username: String, + password: String, + access_token: String, _phantom: PhantomData, } @@ -33,14 +37,18 @@ impl HTTPComms where C: Ciphersuite, { - pub fn new(args: &Args) -> Self { + pub fn new(args: &Args) -> Result> { let client = reqwest::Client::new(); - Self { + let password = env::var(&args.password).map_err(|_| eyre!("The password argument must specify the name of a environment variable containing the password"))?; + Ok(Self { client, host_port: format!("http://{}:{}", args.ip, args.port), - session_id: Uuid::parse_str(&args.session_id).expect("invalid session id"), + session_id: Uuid::parse_str(&args.session_id).ok(), + username: args.username.clone(), + password, + access_token: String::new(), _phantom: Default::default(), - } + }) } } @@ -63,11 +71,48 @@ where ), Box, > { + self.access_token = self + .client + .post(format!("{}/login", self.host_port)) + .json(&server::LoginArgs { + username: self.username.clone(), + password: self.password.clone(), + }) + .send() + .await? + .json::() + .await? + .access_token + .to_string(); + + let session_id = match self.session_id { + Some(s) => s, + None => { + // Get session ID from server + let r = self + .client + .post(format!("{}/list_sessions", self.host_port)) + .bearer_auth(&self.access_token) + .send() + .await? + .json::() + .await?; + if r.session_ids.len() > 1 { + return Err(eyre!("user has more than one FROST session active, which is still not supported by this tool").into()); + } else if r.session_ids.is_empty() { + return Err(eyre!("User has no current sessions active. The Coordinator should either specify your username, or manually share the session ID which you can specify with --session_id").into()); + } + r.session_ids[0] + } + }; + self.session_id = Some(session_id); + // Send Commitments to Server self.client .post(format!("{}/send_commitments", self.host_port)) + .bearer_auth(&self.access_token) .json(&server::SendCommitmentsArgs { - session_id: self.session_id, + session_id, identifier: identifier.into(), commitments: vec![(&commitments).try_into()?], }) @@ -82,9 +127,8 @@ where let r = self .client .post(format!("{}/get_signing_package", self.host_port)) - .json(&server::GetSigningPackageArgs { - session_id: self.session_id, - }) + .bearer_auth(&self.access_token) + .json(&server::GetSigningPackageArgs { session_id }) .send() .await?; if r.status() != 200 { @@ -126,14 +170,22 @@ where let _r = self .client .post(format!("{}/send_signature_share", self.host_port)) + .bearer_auth(&self.access_token) .json(&server::SendSignatureShareArgs { identifier: identifier.into(), - session_id: self.session_id, + session_id: self.session_id.unwrap(), signature_share: vec![signature_share.into()], }) .send() .await?; + let _r = self + .client + .post(format!("{}/logout", self.host_port)) + .bearer_auth(&self.access_token) + .send() + .await?; + Ok(()) } } diff --git a/participant/src/tests/round1.rs b/participant/src/tests/round1.rs index d4ed5e8..afc5292 100644 --- a/participant/src/tests/round1.rs +++ b/participant/src/tests/round1.rs @@ -46,6 +46,8 @@ async fn check_valid_round_1_inputs() { port: 80, session_id: "session-id".to_string(), http: false, + username: "".to_string(), + password: "".to_string(), }; let input = SECRET_SHARE_JSON; let mut valid_input = input.as_bytes(); diff --git a/server/Cargo.toml b/server/Cargo.toml index 48dc684..9d86f8d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,15 +7,19 @@ edition = "2021" [dependencies] axum = "0.7.5" +axum-extra = { version = "0.9.3", features = ["typed-header"] } +axum-macros = "0.4.1" clap = { version = "4.5.13", features = ["derive"] } derivative = "2.2.0" eyre = "0.6.11" frost-core = { version = "2.0.0-rc.0", features = ["serde"] } frost-rerandomized = { version = "2.0.0-rc.0", features = ["serde"] } +password-auth = "1.0.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } serdect = { version = "0.2.0" } serde_json = "1.0.122" +sqlx = { version = "0.7.3", features = ["sqlite", "time", "runtime-tokio", "uuid"] } tokio = { version = "1.38", features = ["full"] } tower-http = { version = "0.5.2", features = ["trace"] } tracing = "0.1" @@ -23,7 +27,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] } [dev-dependencies] -axum-test = "14.10.0" +axum-test = "15.2.0" frost-ed25519 = { version = "2.0.0-rc.0", features = ["serde"] } reddsa = { git = "https://github.com/ZcashFoundation/reddsa.git", rev = "4d8c4bb337231e6e89117334d7c61dada589a953", features = [ "frost", diff --git a/server/build.rs b/server/build.rs new file mode 100644 index 0000000..d506869 --- /dev/null +++ b/server/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/server/migrations/20240627222152_init.down.sql b/server/migrations/20240627222152_init.down.sql new file mode 100644 index 0000000..3dc5b14 --- /dev/null +++ b/server/migrations/20240627222152_init.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +drop table if exists users; +drop table if exists access_tokens; \ No newline at end of file diff --git a/server/migrations/20240627222152_init.up.sql b/server/migrations/20240627222152_init.up.sql new file mode 100644 index 0000000..650f0da --- /dev/null +++ b/server/migrations/20240627222152_init.up.sql @@ -0,0 +1,16 @@ +-- Create users table. +create table if not exists users +( + id integer primary key not null, + username text not null unique, + password text not null, + pubkey blob not null +); + +create table if not exists access_tokens +( + id integer primary key not null, + user_id integer not null, + access_token blob not null, + foreign key(user_id) references users(id) on delete cascade +); \ No newline at end of file diff --git a/server/src/args.rs b/server/src/args.rs index ce551c2..56de185 100644 --- a/server/src/args.rs +++ b/server/src/args.rs @@ -10,4 +10,8 @@ pub struct Args { /// Port to bind to #[arg(short, long, default_value_t = 2744)] pub port: u16, + + /// Database to use. + #[arg(short, long, default_value = "db.sqlite")] + pub database: String, } diff --git a/server/src/functions.rs b/server/src/functions.rs index f160665..f1b3073 100644 --- a/server/src/functions.rs +++ b/server/src/functions.rs @@ -1,32 +1,174 @@ use std::collections::HashSet; use axum::{extract::State, http::StatusCode, Json}; - use eyre::eyre; use uuid::Uuid; use crate::{ state::{Session, SessionState, SharedState}, types::*, + user::{ + add_access_token, authenticate_user, create_user, delete_user, get_user, + remove_access_token, User, + }, AppError, }; +/// Implement the register API. +#[tracing::instrument(ret, err(Debug), skip(state,args), fields(args.username = %args.username))] +pub(crate) async fn register( + State(state): State, + Json(args): Json, +) -> Result, AppError> { + let username = args.username.trim(); + let password = args.password.trim(); + + if username.is_empty() || password.is_empty() { + return Err(AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("empty args").into(), + )); + } + + let db = { + let state_lock = state.read().unwrap(); + state_lock.db.clone() + }; + + create_user(db, username, password, args.pubkey) + .await + .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; + + Ok(Json(())) +} + +/// Implement the login API. +#[tracing::instrument(ret, err(Debug), skip(state,args), fields(args.username = %args.username))] +pub(crate) async fn login( + State(state): State, + Json(args): Json, +) -> Result, AppError> { + // Check if the user sent the credentials + if args.username.is_empty() || args.password.is_empty() { + return Err(AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("empty args").into(), + )); + } + + let db = { + let state_lock = state.read().unwrap(); + state_lock.db.clone() + }; + + let user = authenticate_user(db.clone(), &args.username, &args.password) + .await + .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; + + let user = match user { + Some(user) => user, + None => { + return Err(AppError( + StatusCode::UNAUTHORIZED, + eyre!("invalid user or password").into(), + )) + } + }; + + let access_token = add_access_token(db.clone(), user.id) + .await + .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; + + let token = LoginOutput { access_token }; + + Ok(Json(token)) +} + +/// Implement the logout API. +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] +pub(crate) async fn logout( + State(state): State, + user: User, +) -> Result, AppError> { + let db = { + let state_lock = state.read().unwrap(); + state_lock.db.clone() + }; + + remove_access_token( + db.clone(), + user.current_token + .expect("user is logged in so they must have a token"), + ) + .await + .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; + + Ok(Json(())) +} + +/// Implement the unregister API. +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] +pub(crate) async fn unregister( + State(state): State, + user: User, +) -> Result, AppError> { + let db = { + let state_lock = state.read().unwrap(); + state_lock.db.clone() + }; + + delete_user(db, user.id) + .await + .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; + + Ok(Json(())) +} + /// Implement the create_new_session API. -#[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn create_new_session( State(state): State, + user: User, Json(args): Json, ) -> Result, AppError> { - tracing::info!("create_new_session"); if args.message_count == 0 { return Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("invalid message_count"), + eyre!("invalid message_count").into(), )); } + let db = { + let state_lock = state.read().unwrap(); + state_lock.db.clone() + }; + for username in &args.usernames { + if get_user(db.clone(), username) + .await + .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))? + .is_none() + { + return Err(AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("invalid user").into(), + )); + } + } // Create new session object. let id = Uuid::new_v4(); + + let mut state = state.write().unwrap(); + + // Save session ID in global state + for username in &args.usernames { + state + .sessions_by_username + .entry(username.to_string()) + .or_default() + .insert(id); + } + // Create Session object let session = Session { + usernames: args.usernames, num_signers: args.num_signers, message_count: args.message_count, state: SessionState::WaitingForCommitments { @@ -34,22 +176,41 @@ pub(crate) async fn create_new_session( }, }; // Save session into global state. - state.write().unwrap().sessions.insert(id, session); + state.sessions.insert(id, session); + let user = CreateNewSessionOutput { session_id: id }; Ok(Json(user)) } +/// Implement the create_new_session API. +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] +pub(crate) async fn list_sessions( + State(state): State, + user: User, +) -> Result, AppError> { + let state = state.read().unwrap(); + + let session_ids = state + .sessions_by_username + .get(&user.username) + .map(|s| s.iter().cloned().collect()) + .unwrap_or_default(); + + Ok(Json(ListSessionsOutput { session_ids })) +} + /// Implement the get_session_info API -#[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn get_session_info( State(state): State, + user: User, Json(args): Json, ) -> Result, AppError> { let state_lock = state.read().unwrap(); let session = state_lock.sessions.get(&args.session_id).ok_or(AppError( StatusCode::NOT_FOUND, - eyre!("session ID not found"), + eyre!("session ID not found").into(), ))?; Ok(Json(GetSessionInfoOutput { @@ -60,9 +221,10 @@ pub(crate) async fn get_session_info( /// Implement the send_commitments API // TODO: get identifier from channel rather from arguments -#[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn send_commitments( State(state): State, + user: User, Json(args): Json, ) -> Result<(), AppError> { // Get the mutex lock to read and write from the state @@ -73,7 +235,7 @@ pub(crate) async fn send_commitments( .get_mut(&args.session_id) .ok_or(AppError( StatusCode::NOT_FOUND, - eyre!("session ID not found"), + eyre!("session ID not found").into(), ))?; match &mut session.state { @@ -81,7 +243,7 @@ pub(crate) async fn send_commitments( if args.commitments.len() != session.message_count as usize { return Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("wrong number of commitments"), + eyre!("wrong number of commitments").into(), )); } // Add commitment to map. @@ -89,6 +251,11 @@ pub(crate) async fn send_commitments( // (it seems better to ignore overwrites, which could be caused by // poor networking connectivity leading to retries) commitments.insert(args.identifier, args.commitments); + tracing::debug!( + "added commitments, currently {}/{}", + commitments.len(), + session.num_signers + ); // If complete, advance to next state if commitments.len() == session.num_signers as usize { session.state = SessionState::CommitmentsReady { @@ -99,7 +266,7 @@ pub(crate) async fn send_commitments( _ => { return Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("incompatible session state"), + eyre!("incompatible session state").into(), )); } } @@ -107,16 +274,17 @@ pub(crate) async fn send_commitments( } /// Implement the get_commitments API -// #[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn get_commitments( State(state): State, + user: User, Json(args): Json, ) -> Result, AppError> { let state_lock = state.read().unwrap(); let session = state_lock.sessions.get(&args.session_id).ok_or(AppError( StatusCode::NOT_FOUND, - eyre!("session ID not found"), + eyre!("session ID not found").into(), ))?; match &session.state { @@ -135,15 +303,16 @@ pub(crate) async fn get_commitments( })), _ => Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("incompatible session state"), + eyre!("incompatible session state").into(), )), } } /// Implement the send_signing_package API -#[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn send_signing_package( State(state): State, + user: User, Json(args): Json, ) -> Result<(), AppError> { let mut state_lock = state.write().unwrap(); @@ -153,7 +322,7 @@ pub(crate) async fn send_signing_package( .get_mut(&args.session_id) .ok_or(AppError( StatusCode::NOT_FOUND, - eyre!("session ID not found"), + eyre!("session ID not found").into(), ))?; match &mut session.state { @@ -161,7 +330,7 @@ pub(crate) async fn send_signing_package( if args.signing_package.len() != session.message_count as usize { return Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("wrong number of inputs"), + eyre!("wrong number of inputs").into(), )); } if args.randomizer.len() != session.message_count as usize @@ -169,7 +338,7 @@ pub(crate) async fn send_signing_package( { return Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("wrong number of inputs"), + eyre!("wrong number of inputs").into(), )); } session.state = SessionState::WaitingForSignatureShares { @@ -183,7 +352,7 @@ pub(crate) async fn send_signing_package( _ => { return Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("incompatible session state"), + eyre!("incompatible session state").into(), )); } } @@ -191,16 +360,17 @@ pub(crate) async fn send_signing_package( } /// Implement the get_signing_package API -#[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn get_signing_package( State(state): State, + user: User, Json(args): Json, ) -> Result, AppError> { let state_lock = state.read().unwrap(); let session = state_lock.sessions.get(&args.session_id).ok_or(AppError( StatusCode::NOT_FOUND, - eyre!("session ID not found"), + eyre!("session ID not found").into(), ))?; match &session.state { @@ -217,16 +387,17 @@ pub(crate) async fn get_signing_package( })), _ => Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("incompatible session state"), + eyre!("incompatible session state").into(), )), } } /// Implement the send_signature_share API // TODO: get identifier from channel rather from arguments -#[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn send_signature_share( State(state): State, + user: User, Json(args): Json, ) -> Result<(), AppError> { let mut state_lock = state.write().unwrap(); @@ -236,7 +407,7 @@ pub(crate) async fn send_signature_share( .get_mut(&args.session_id) .ok_or(AppError( StatusCode::NOT_FOUND, - eyre!("session ID not found"), + eyre!("session ID not found").into(), ))?; match &mut session.state { @@ -248,12 +419,15 @@ pub(crate) async fn send_signature_share( aux_msg: _, } => { if !identifiers.contains(&args.identifier) { - return Err(AppError(StatusCode::NOT_FOUND, eyre!("invalid identifier"))); + return Err(AppError( + StatusCode::NOT_FOUND, + eyre!("invalid identifier").into(), + )); } if args.signature_share.len() != session.message_count as usize { return Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("wrong number of signature shares"), + eyre!("wrong number of signature shares").into(), )); } // Currently ignoring the possibility of overwriting previous values @@ -270,7 +444,7 @@ pub(crate) async fn send_signature_share( _ => { return Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("incompatible session state"), + eyre!("incompatible session state").into(), )); } } @@ -278,16 +452,17 @@ pub(crate) async fn send_signature_share( } /// Implement the get_signature_shares API -#[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn get_signature_shares( State(state): State, + user: User, Json(args): Json, ) -> Result, AppError> { let state_lock = state.read().unwrap(); let session = state_lock.sessions.get(&args.session_id).ok_or(AppError( StatusCode::NOT_FOUND, - eyre!("session ID not found"), + eyre!("session ID not found").into(), ))?; match &session.state { @@ -308,17 +483,34 @@ pub(crate) async fn get_signature_shares( } _ => Err(AppError( StatusCode::INTERNAL_SERVER_ERROR, - eyre!("incompatible session state"), + eyre!("incompatible session state").into(), )), } } /// Implement the close_session API. -#[tracing::instrument(ret, err(Debug))] +#[tracing::instrument(ret, err(Debug), skip(state,user), fields(user.username = %user.username))] pub(crate) async fn close_session( State(state): State, + user: User, Json(args): Json, ) -> Result, AppError> { - state.write().unwrap().sessions.remove(&args.session_id); + let mut state = state.write().unwrap(); + + for username in state + .sessions + .get(&args.session_id) + .ok_or(AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("invalid session ID").into(), + ))? + .usernames + .clone() + { + if let Some(v) = state.sessions_by_username.get_mut(&username) { + v.remove(&args.session_id); + } + } + state.sessions.remove(&args.session_id); Ok(Json(())) } diff --git a/server/src/lib.rs b/server/src/lib.rs index 79bf00b..46be2bf 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -2,6 +2,9 @@ pub mod args; mod functions; mod state; mod types; +mod user; + +pub use state::{AppState, SharedState}; use tower_http::trace::TraceLayer; pub use types::*; @@ -16,11 +19,15 @@ use axum::{ /// Create the axum Router for the server. /// Maps specific endpoints to handler functions. // TODO: use methods of a single object instead of separate functions? -pub fn router() -> Router { +pub fn router(shared_state: SharedState) -> Router { // Shared state that is passed to each handler by axum - let shared_state = state::SharedState::default(); Router::new() + .route("/register", post(functions::register)) + .route("/login", post(functions::login)) + .route("/logout", post(functions::logout)) + .route("/unregister", post(functions::unregister)) .route("/create_new_session", post(functions::create_new_session)) + .route("/list_sessions", post(functions::list_sessions)) .route("/get_session_info", post(functions::get_session_info)) .route("/send_commitments", post(functions::send_commitments)) .route("/get_commitments", post(functions::get_commitments)) @@ -44,7 +51,8 @@ pub fn router() -> Router { /// Run the server with the specified arguments. pub async fn run(args: &Args) -> Result<(), Box> { - let app = router(); + let shared_state = AppState::new(&args.database).await?; + let app = router(shared_state); let addr = format!("{}:{}", args.ip, args.port); let listener = tokio::net::TcpListener::bind(addr).await?; @@ -55,7 +63,7 @@ pub async fn run(args: &Args) -> Result<(), Box> { /// error happens during a API call, and a generic eyre::Report. // TODO: create an enum with specific errors #[derive(Debug)] -pub struct AppError(StatusCode, eyre::Report); +pub struct AppError(StatusCode, Box); impl IntoResponse for AppError { fn into_response(self) -> Response { diff --git a/server/src/state.rs b/server/src/state.rs index c7bd032..a465695 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,8 +1,13 @@ use std::{ collections::{HashMap, HashSet}, + str::FromStr, sync::{Arc, RwLock}, }; +use sqlx::{ + sqlite::{SqliteConnectOptions, SqlitePoolOptions}, + SqlitePool, +}; use uuid::Uuid; use crate::{ @@ -67,6 +72,8 @@ impl Default for SessionState { /// A particular signing session. #[derive(Debug)] pub struct Session { + /// The usernames of the participants + pub(crate) usernames: Vec, /// The number of signers in the session. pub(crate) num_signers: u16, /// The set of identifiers for the session. @@ -78,10 +85,27 @@ pub struct Session { } /// The global state of the server. -#[derive(Default, Debug)] +#[derive(Debug)] pub struct AppState { /// Mapping of signing sessions by UUID. pub(crate) sessions: HashMap, + pub(crate) sessions_by_username: HashMap>, + pub(crate) db: SqlitePool, +} + +impl AppState { + pub async fn new(database: &str) -> Result> { + tracing::event!(tracing::Level::INFO, "opening database {}", database); + let options = SqliteConnectOptions::from_str(database)?.create_if_missing(true); + let db = SqlitePoolOptions::new().connect_with(options).await?; + sqlx::migrate!().run(&db).await?; + let state = Self { + sessions: Default::default(), + sessions_by_username: Default::default(), + db, + }; + Ok(Arc::new(RwLock::new(state))) + } } /// Type alias for the global state under a reference-counted RW mutex, diff --git a/server/src/types.rs b/server/src/types.rs index b295bf5..9d9e262 100644 --- a/server/src/types.rs +++ b/server/src/types.rs @@ -147,8 +147,31 @@ impl TryFrom<&SerializedSignatureShare> } } +#[derive(Debug, Serialize, Deserialize)] +pub struct RegisterArgs { + pub username: String, + pub password: String, + #[serde( + serialize_with = "serdect::slice::serialize_hex_lower_or_bin", + deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec" + )] + pub pubkey: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginOutput { + pub access_token: Uuid, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginArgs { + pub username: String, + pub password: String, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CreateNewSessionArgs { + pub usernames: Vec, pub num_signers: u16, pub message_count: u8, } @@ -158,6 +181,11 @@ pub struct CreateNewSessionOutput { pub session_id: Uuid, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ListSessionsOutput { + pub session_ids: Vec, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct GetSessionInfoArgs { pub session_id: Uuid, diff --git a/server/src/user.rs b/server/src/user.rs new file mode 100644 index 0000000..8bf99ee --- /dev/null +++ b/server/src/user.rs @@ -0,0 +1,237 @@ +use std::str::FromStr; + +use axum::{ + async_trait, + extract::FromRequestParts, + http::{request::Parts, StatusCode}, + RequestPartsExt, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use eyre::eyre; +use sqlx::{FromRow, SqlitePool}; +use tokio::task; +use uuid::Uuid; + +use crate::{state::SharedState, AppError}; + +/// An User, as stored in the database. +#[derive(Debug, FromRow)] +#[allow(dead_code)] +pub struct User { + pub(crate) id: i64, + pub(crate) username: String, + pub(crate) password: String, + pub(crate) pubkey: Vec, + #[sqlx(skip)] + pub(crate) access_tokens: Vec, + #[sqlx(skip)] + pub(crate) current_token: Option, +} + +#[derive(Debug, FromRow)] +#[allow(dead_code)] +pub struct AccessToken { + pub(crate) id: i64, + pub(crate) user_id: i64, + pub(crate) access_token: Option, +} + +/// Create user in the database. +/// +/// The password is hashed and its hash is written in the DB. +pub(crate) async fn create_user( + db: SqlitePool, + username: &str, + password: &str, + pubkey: Vec, +) -> Result<(), Box> { + // TODO: enforce mininum password length + let password = password.to_owned(); + let pwhash = task::spawn_blocking(|| password_auth::generate_hash(password)).await?; + sqlx::query( + r#" + insert into users (username, password, pubkey) + values (?, ?, ?) + "#, + ) + .bind(username) + .bind(pwhash) + .bind(pubkey) + .execute(&db) + .await?; + Ok(()) +} + +/// Get user from database, or None if it's not registered. +pub(crate) async fn get_user( + db: SqlitePool, + username: &str, +) -> Result, Box> { + let user: Option = sqlx::query_as("select * from users where username = ? ") + .bind(username) + .fetch_optional(&db) + .await?; + if let Some(mut user) = user { + let access_tokens: Vec = + sqlx::query_as("select * from access_tokens where user_id = ?") + .bind(user.id) + .fetch_all(&db) + .await?; + user.access_tokens = access_tokens; + Ok(Some(user)) + } else { + Ok(None) + } +} + +/// Delete an User from the database, given its database ID. +pub(crate) async fn delete_user(db: SqlitePool, id: i64) -> Result<(), Box> { + sqlx::query( + r#" + delete from users where id = ? + "#, + ) + .bind(id) + .execute(&db) + .await?; + Ok(()) +} + +/// Authenticate user registered in the database. Returns the User if +/// authentication is successful, or None if the username or password is wrong. +/// +/// The given password is hashed and verified against the stored hash. +pub(crate) async fn authenticate_user( + db: SqlitePool, + username: &str, + password: &str, +) -> Result, Box> { + let user: Option = get_user(db, username).await?; + + // Verifying the password is blocking and potentially slow, so we'll do so + // via `spawn_blocking`. + let password = password.to_owned(); + let r: Result<_, password_auth::VerifyError> = task::spawn_blocking(|| { + // We're using password-based authentication--this works by comparing our form + // input with an argon2 password hash. + Ok(user.filter(|user| password_auth::verify_password(password, &user.password).is_ok())) + }) + .await?; + Ok(r?) +} + +/// Refreshes the user's access token, identified by its id in the database. +/// +/// Generates a new token and overwrites the old one in the database, if any. +pub(crate) async fn add_access_token( + db: SqlitePool, + id: i64, +) -> Result> { + let access_token = Uuid::new_v4(); + + sqlx::query( + r#" + insert into access_tokens (user_id, access_token) + values (?, ?) + "#, + ) + .bind(id) + .bind(access_token) + .execute(&db) + .await?; + + Ok(access_token) +} + +/// Remove a user's access token. +pub(crate) async fn remove_access_token( + db: SqlitePool, + access_token: Uuid, +) -> Result<(), Box> { + sqlx::query( + r#" + delete from access_tokens where access_token = ? + "#, + ) + .bind(access_token) + .execute(&db) + .await?; + + Ok(()) +} + +/// Return the User for a given access token, or None if there is no match. +pub(crate) async fn get_user_for_access_token( + db: SqlitePool, + access_token: Uuid, +) -> Result, Box> { + let user: Option = sqlx::query_as( + r#" + select * from users inner join access_tokens on users.id = access_tokens.user_id where access_tokens.access_token = ? + "#, + ) + .bind(access_token) + .fetch_optional(&db) + .await?; + Ok(user) +} + +/// Read a User from a request. This is used to authenticate users. If any axum +/// handler has an User argument, this will be called and the authentication +/// will be carried out. +#[async_trait] +impl FromRequestParts for User { + type Rejection = AppError; + + #[tracing::instrument(ret, err(Debug), skip(parts, state))] + // Can be removed after this fix is released + // https://github.com/rust-lang/rust-clippy/issues/12281 + #[allow(clippy::blocks_in_conditions)] + async fn from_request_parts( + parts: &mut Parts, + state: &SharedState, + ) -> Result { + // Extract the token from the authorization header + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|_| { + AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("Bearer token missing").into(), + ) + })?; + // Decode the user data + let access_token = Uuid::from_str(bearer.token()).map_err(|_| { + AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("invalid access token").into(), + ) + })?; + + let db = { + let state_lock = state.read().unwrap(); + state_lock.db.clone() + }; + + let user = get_user_for_access_token(db, access_token) + .await + .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e))?; + + match user { + Some(mut user) => { + user.current_token = Some(access_token); + Ok(user) + } + None => { + return Err(AppError( + StatusCode::INTERNAL_SERVER_ERROR, + eyre!("user not found").into(), + )) + } + } + } +} diff --git a/server/tests/integration_tests.rs b/server/tests/integration_tests.rs index c01de89..be8a59e 100644 --- a/server/tests/integration_tests.rs +++ b/server/tests/integration_tests.rs @@ -2,7 +2,7 @@ use std::{collections::BTreeMap, time::Duration}; use axum_test::TestServer; use rand::thread_rng; -use server::{args::Args, router, SerializedSignatureShare, SerializedSigningPackage}; +use server::{args::Args, router, AppState, SerializedSignatureShare, SerializedSigningPackage}; use frost_core as frost; @@ -47,14 +47,52 @@ async fn test_main_router< .collect(); // Instantiate test server using axum_test - let router = router(); + let shared_state = AppState::new(":memory:").await?; + let router = router(shared_state); let server = TestServer::new(router)?; + // Create a dummy user. We make all requests with the same user since + // it currently it doesn't really matter who the user is, users are only + // used to share session IDs. This will likely change soon. + + let res = server + .post("/register") + .json(&server::RegisterArgs { + username: "alice".to_string(), + password: "passw0rd".to_string(), + pubkey: vec![], + }) + .await; + res.assert_status_ok(); + + let res = server + .post("/register") + .json(&server::RegisterArgs { + username: "bob".to_string(), + password: "passw0rd".to_string(), + pubkey: vec![], + }) + .await; + res.assert_status_ok(); + + let res = server + .post("/login") + .json(&server::LoginArgs { + username: "alice".to_string(), + password: "passw0rd".to_string(), + }) + .await; + res.assert_status_ok(); + let r: server::LoginOutput = res.json(); + let token = r.access_token; + // As the coordinator, create a new signing session with all participants, // for 2 messages let res = server .post("/create_new_session") + .authorization_bearer(token) .json(&server::CreateNewSessionArgs { + usernames: vec!["alice".to_string(), "bob".to_string()], num_signers: 2, message_count: 2, }) @@ -75,6 +113,7 @@ async fn test_main_router< // asking the server). let res = server .post("/get_session_info") + .authorization_bearer(token) .json(&server::GetSessionInfoArgs { session_id }) .await; res.assert_status_ok(); @@ -96,6 +135,7 @@ async fn test_main_router< // Send commitments to server let res = server .post("/send_commitments") + .authorization_bearer(token) .json(&server::SendCommitmentsArgs { identifier: (*identifier).into(), session_id, @@ -110,6 +150,7 @@ async fn test_main_router< // As the coordinator, get the commitments let res = server .post("/get_commitments") + .authorization_bearer(token) .json(&server::GetCommitmentsArgs { session_id }) .await; res.assert_status_ok(); @@ -146,6 +187,7 @@ async fn test_main_router< // As the coordinator, send the SigningPackages to the server let res = server .post("/send_signing_package") + .authorization_bearer(token) .json(&server::SendSigningPackageArgs { session_id, signing_package: signing_packages @@ -173,6 +215,7 @@ async fn test_main_router< // Get SigningPackages let res = server .post("get_signing_package") + .authorization_bearer(token) .json(&server::GetSigningPackageArgs { session_id }) .await; res.assert_status_ok(); @@ -210,6 +253,7 @@ async fn test_main_router< // Send SignatureShares to the server let res = server .post("/send_signature_share") + .authorization_bearer(token) .json(&server::SendSignatureShareArgs { identifier: (*identifier).into(), session_id, @@ -225,6 +269,7 @@ async fn test_main_router< // As the coordinator, get SignatureShares let res = server .post("/get_signature_shares") + .authorization_bearer(token) .json(&server::GetSignatureSharesArgs { session_id }) .await; res.assert_status_ok(); @@ -265,6 +310,7 @@ async fn test_main_router< // Close the session let res = server .post("/close_session") + .authorization_bearer(token) .json(&server::CloseSessionArgs { session_id }) .await; res.assert_status_ok(); @@ -288,9 +334,12 @@ async fn test_main_router< /// A better example on how to write client code. #[tokio::test] async fn test_http() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + // Spawn server for testing tokio::spawn(async move { server::run(&Args { + database: ":memory:".to_string(), ip: "127.0.0.1".to_string(), port: 2744, }) @@ -302,19 +351,67 @@ async fn test_http() -> Result<(), Box> { // TODO: this could possibly be not enough, use some retry logic instead tokio::time::sleep(Duration::from_secs(2)).await; - // Call create_new_session + // Create a client to make requests let client = reqwest::Client::new(); + + // Call register to create users let r = client - .post("http://127.0.0.1:2744/create_new_session") - .json(&server::CreateNewSessionArgs { - num_signers: 2, - message_count: 1, + .post("http://127.0.0.1:2744/register") + .json(&server::RegisterArgs { + username: "alice".to_string(), + password: "passw0rd".to_string(), + pubkey: vec![], }) .send() - .await? - .json::() .await?; - println!("{}", r.session_id); + if r.status() != reqwest::StatusCode::OK { + panic!("{}", r.text().await?) + } + let r = client + .post("http://127.0.0.1:2744/register") + .json(&server::RegisterArgs { + username: "bob".to_string(), + password: "passw0rd".to_string(), + pubkey: vec![], + }) + .send() + .await?; + if r.status() != reqwest::StatusCode::OK { + panic!("{}", r.text().await?) + } + + // Call login to authenticate + let r = client + .post("http://127.0.0.1:2744/login") + .json(&server::LoginArgs { + username: "alice".to_string(), + password: "passw0rd".to_string(), + }) + .send() + .await?; + if r.status() != reqwest::StatusCode::OK { + panic!("{}", r.text().await?) + } + let r = r.json::().await?; + let access_token = r.access_token; + + // Call create_new_session + let r = client + .post("http://127.0.0.1:2744/create_new_session") + .bearer_auth(access_token) + .json(&server::CreateNewSessionArgs { + usernames: vec!["alice".to_string(), "bob".to_string()], + message_count: 1, + num_signers: 2, + }) + .send() + .await?; + if r.status() != reqwest::StatusCode::OK { + panic!("{}", r.text().await?) + } + let r = r.json::().await?; + let session_id = r.session_id; + println!("Session ID: {}", session_id); Ok(()) }