commit 5095bcdcdd09798a0474612f6ca8160e0d64e27a Author: eternal-flame-AD Date: Sat Nov 2 19:50:14 2024 -0500 init Signed-off-by: eternal-flame-AD diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fcb2ad2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2683 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[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 = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-compression" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" +dependencies = [ + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "aws-lc-rs" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[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 = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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 = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "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 = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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 = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[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 = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64", + "serde", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.0.0", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash 2.0.0", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rcgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "replikey" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "argon2", + "async-compression", + "clap", + "env_logger", + "log", + "openssl", + "pem-rfc7468", + "rand_core", + "rcgen", + "reqwest", + "rpassword", + "rustls", + "serde", + "sha2", + "sqlx", + "tempfile", + "thiserror", + "time", + "tokio", + "tokio-rustls", + "toml", + "x509-parser", +] + +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-postgres", +] + +[[package]] +name = "sqlx-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +dependencies = [ + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-postgres", + "syn", + "tempfile", + "url", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +dependencies = [ + "atoi", + "base64", + "bitflags", + "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", + "tracing", + "whoami", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +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.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +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-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[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", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9bd9755 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "replikey" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.20", features = ["derive"] } +env_logger = "0.11.5" +log = "0.4" +rand_core = { version = "0.6.4", features = ["getrandom"] } +x509-parser = "0.16.0" +thiserror = "1.0.66" + +time = { version = "0.3.36", optional = true } +aes-gcm = { version = "0.10.3", optional = true } +sha2 = { version = "0.10.8", optional = true } +argon2 = { version = "0.5.3", optional = true } +rpassword = { version = "7.3.1", optional = true } +rcgen = { version = "0.13.1", optional = true, features = ["crypto", "pem", "x509-parser"] } +pem-rfc7468 = { version = "0.7.0", features = ["alloc"], optional = true } +toml = { version = "0.8.19", optional = true } +reqwest = { version = "0.12.9", optional = true, default-features = false, features = ["rustls-tls"] } +openssl = { version = "0.10.68", optional = true } +tokio-rustls = { version = "0.26.0", optional = true } +serde = { version = "1.0.214", features = ["derive"], optional = true } +sqlx = { version = "0.8.2", optional = true, default-features = false, features = ["tls-none", "postgres"] } +tokio = { version = "1.41.0", features = ["rt", "rt-multi-thread", "macros", "net", "io-util", "sync"], optional = true } +rustls = { version = "0.23.16", optional = true } +async-compression = { version = "0.4.17", optional = true, features = ["tokio", "zstd"] } + +[features] +default = ["keygen", "networking", "service", "remote-crl", "setup-postgres"] +asyncio = ["dep:tokio"] +keygen = ["dep:rcgen", "dep:pem-rfc7468", "dep:rpassword", "dep:argon2", "dep:sha2", "dep:aes-gcm", "dep:time"] +networking = ["asyncio", "dep:tokio-rustls", "dep:rustls", "dep:async-compression"] +test-crosscheck-openssl = ["dep:openssl"] +serde = ["dep:serde"] +service = ["serde", "networking", "dep:toml"] +remote-crl = ["dep:reqwest"] +setup-postgres = ["dep:sqlx"] +stat-service = ["networking", "serde"] +rustls = ["dep:rustls"] +async-compression = ["dep:async-compression"] + +[[bin]] +name = "replikey" +path = "src/bin/replikey.rs" +required-features = ["keygen"] + +[dev-dependencies] +tempfile = "3.13.0" + +[profile.release] +lto = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64bc58f --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] [Yumechi yume@yumechi.jp] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..81d658a --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# replikey + +Misskey logical replication tool for replication over insecure connections. + +Current development status: + - Automated tested: Step 1,2,3,4,5 + - Works on my machine: Step 6,7,8 + - Docs: Later :) + +## Architecture + +This is essentially a DB configuration tool and a Web PKI CA workflow and an mTLS proxy combined into one with feature flags to enable or disable each part. + +Network architecture is as follows: + +![Network Architecture](./doc/architecture.svg) + +For postgres the tool has automation for setting up official logical replication, for redis it was just one command so didn't bother automating it. + +## Setup Workflow + +Overview, we want to do these: + +1. Create a root CA for authenticating the server and client +2. Create a server certificate for the Misskey instance +3. Create a client certificate for the replication client +4. Sign the server and client certificates +5. Set up Postgres for replication +6. Test the connection +7. Integrate the program into docker-compose +8. Start the replication + +### 1. Create a root CA (you will be prompted for a password to encrypt the key) + +```sh +replikey cert create-ca --valid-days 1825 --dn-common-name "MyInstance Replication Root Certificate Authority" -o ca-certs +``` + +### 2. Create a server CSR, SAN can be any number of combinations of DNS and IP addresses + + +If you use DNS name SAN, all SNIs you later use must match one of the DNS name or wildcard in the SAN +If you use IP address SAN, all connections (supposedly) to your IP address will be considered from your server + +```sh +replikey cert create-server --valid-days 365 --dn-common-name "MyInstance Production Server" -d '*.replication.myinstance.com' --ip-address 123.123.123.123 -o server-certs +``` + +### 3. Sign the server CSR + +```sh +replikey cert sign-server-csr --valid-days 365 --ca-dir ca-certs --input-csr server-certs/server.csr --output server-certs-signed.pem + +Enter password: +CSR Params: +Serial number: 7b6a82c3d9171f7ba8fbd8973aac0146dac611dd +SAN: DNS=*.replication.myinstance.com +SAN: IP=123.123.123.123 +Not before: 2024-11-02 22:43:56.751788095 +00:00:00 +Not after: 2025-11-02 22:43:56.751783366 +00:00:00 +Distinguished name: DistinguishedName { entries: {CommonName: Utf8String("MyInstance Production Server")}, order: [CommonName] } +Key usages: [DigitalSignature, DataEncipherment] +Extended key usages: [ServerAuth] +CRL distribution points: [] +Do you want to sign this CSR? (YES/NO) +IMPORTANT: Keep this certificate or its serial number for revocation +``` + +### 4. Create a client CSR (for each client) + +Ideally the workflow is the client should generate their own CSR and send it to you, you sign the certificate and send it back to them. + +```sh +replikey cert create-client --valid-days 365 \ + --dn-common-name "MyInstance Replication Client" \ + -o client-certs +``` + +### 5. Sign the client CSR (for each client) + +```sh +replikey cert sign-client-csr --valid-days 365 \ + --ca-dir ca-certs \ + --input-csr client-certs/client.csr \ + --output client-certs-signed.pem +``` + +### BTW.0 Later if you want to revoke a certificate, generate a CRL with the following command, then pass a URL or path to the CRL(s) to any networking command via the --crl option +replikey cert generate-crl --ca-dir ca-certs --serial abcdef --serial 123456 --output revoked.crl + +### 6. Check your certificates can communicate, this is just a zstd wrapper around rustls, so you should be able to use any TLS client or server + +```sh +replikey network reverse-proxy --listen 0.0.0.0:8443 \ + --redis-sni localhost --redis-target 127.0.0.1:22 \ + --postgres-sni postgres --postgres-target 127.0.0.1:8441 \ + --cert server-signed.pem --key test-server/server.key \ + --ca test-ca/ca.pem & + +# this SNI MUST match one of the dns name in the server certificate or the IP address is signed (not recommended) +replikey network forward-proxy --listen 0.0.0.0:8444 \ + --sni localhost --target localhost:8443 \ + --cert client-signed.pem --key test-client/client.key \ + --ca test-ca/ca.pem & + +ssh -p8444 localhost # this should work +``` + +### 7. Prepare the replication server for connection + +Login to your master Misskey instance postgres and create a user for connection. You do not have to and should not grant any permissions to the replication user + +```sql +CREATE ROLE replication WITH REPLICATION LOGIN ENCRYPTED PASSWORD 'password'; +``` + +### BTW.1 Table names for checking replication status + +``` +pg_catalog.pg_publication +pg_catalog.pg_subscription +pg_catalog.pg_stat_subscription +``` + +### 8. Create postgres publication on the master side + +```sh +# DATABASE_URL should be _the_ connection string Misskey uses to connect to the database +replikey setup-postgres-master setup --must-not-exist --publication "my_name" +replikey setup-postgres-master drop-table --publication "my_name" -t auth_session -t password_reset_request -t access_token +``` + + +### 9. Prepare postgres slave on the slave side + +```sh +# DATABASE_URL should be any valid connection string to the master database, probably the user you created in step 7 +replikey setup-postgres-slave setup --must-not-exist --subscription "my_subscription_name" --publication "my_name" +``` + +### 10. Set redis slave to replicate from the master + +```sh +# replace REDIS_PROXY with the address of the redis TLS proxy listener +# replace PORT with the port of the redis TLS proxy listener +redis-cli 'REPLICAOF REDIS_PROXY PORT' +``` + +### Integration into docker-compose: + +WIP, but I have `replikey service` subcommand for running the proxies with environment variables or config files and optionally set up the replication on startup. \ No newline at end of file diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..0ab4ef5 --- /dev/null +++ b/deny.toml @@ -0,0 +1,250 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# Root options + +# The graph table configures how the dependency graph is constructed and thus +# which crates the checks are performed against +[graph] +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] +# When creating the dependency graph used as the source of truth when checks are +# executed, this field can be used to prune crates from the graph, removing them +# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate +# is pruned from the graph, all of its dependencies will also be pruned unless +# they are connected to another crate in the graph that hasn't been pruned, +# so it should be used with care. The identifiers are [Package ID Specifications] +# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) +#exclude = [] +# If true, metadata will be collected with `--all-features`. Note that this can't +# be toggled off if true, if you want to conditionally enable `--all-features` it +# is recommended to pass `--all-features` on the cmd line instead +all-features = true +# If true, metadata will be collected with `--no-default-features`. The same +# caveat with `all-features` applies +no-default-features = false +# If set, these feature will be enabled when collecting metadata. If `--features` +# is specified on the cmd line they will take precedence over this option. +#features = [] + +# The output table provides options for how/if diagnostics are outputted +[output] +# When outputting inclusion graphs in diagnostics that include features, this +# option can be used to specify the depth at which feature edges will be added. +# This option is included since the graphs can be quite large and the addition +# of features from the crate(s) to all of the graph roots can be far too verbose. +# This option can be overridden via `--feature-depth` on the cmd line +feature-depth = 1 + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory databases are cloned/fetched into +#db-path = "$CARGO_HOME/advisory-dbs" +# The url(s) of the advisory databases to use +#db-urls = ["https://github.com/rustsec/advisory-db"] +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", + #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, + #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish + #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, +] +# If this is true, then cargo deny will use the git executable to fetch advisory database. +# If this is false, then it uses a built-in git library. +# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. +# See Git Authentication for more information about setting up git authentication. +#git-fetch-with-cli = true + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "CC0-1.0", + "MIT", + "MPL-2.0", + "Apache-2.0", + "Unicode-DFS-2016", + "ISC", + "BSD-3-Clause", + "OpenSSL", + "Zlib", +] +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], crate = "adler32" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The package spec the clarification applies to +#crate = "ring" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ +# Each entry is a crate relative path, and the (opaque) hash of its contents +#{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[[licenses.clarify]] +crate = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 } +] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# The default lint level for `default` features for crates that are members of +# the workspace that is being checked. This can be overridden by allowing/denying +# `default` on a crate-by-crate basis if desired. +workspace-default-features = "allow" +# The default lint level for `default` features for external crates that are not +# members of the workspace. This can be overridden by allowing/denying `default` +# on a crate-by-crate basis if desired. +external-default-features = "allow" +# List of crates that are allowed. Use with care! +allow = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, +] +# List of crates to deny +deny = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, +] + +# List of features to allow/deny +# Each entry the name of a crate and a version range. If version is +# not specified, all versions will be matched. +#[[bans.features]] +#crate = "reqwest" +# Features to not allow +#deny = ["json"] +# Features to allow +#allow = [ +# "rustls", +# "__rustls", +# "__tls", +# "hyper-rustls", +# "rustls", +# "rustls-pemfile", +# "rustls-tls-webpki-roots", +# "tokio-rustls", +# "webpki-roots", +#] +# If true, the allowed features must exactly match the enabled feature set. If +# this is set there is no point setting `deny` +#exact = true + +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + "hashbrown", + "sync_wrapper", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite. +skip-tree = [ + #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies + #{ crate = "ansi_term@0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +github = [] +# 1 or more gitlab.com organizations to allow git sources for +gitlab = [] +# 1 or more bitbucket.org organizations to allow git sources for +bitbucket = [] diff --git a/doc/architecture.gv b/doc/architecture.gv new file mode 100644 index 0000000..43bbfb9 --- /dev/null +++ b/doc/architecture.gv @@ -0,0 +1,60 @@ +digraph { + subgraph cluster_pki { + label="PKI" + ca [label="CA Key", shape=note] + + subgraph cluster_pki_crl { + label="CRL Infrastructure(Optional)" + crl_listener [label="http://my.crl", shape=triangle,rank=0] + crl -> crl_listener [label="Static file"] + } + } + + subgraph cluster_0 { + label="Master docker compose" + web_app [label="Web app", shape=box] + db [label="Postgres", shape=box] + redis [label="Redis", shape=box] + replikey [label="Replikey", shape=box] + replikey -> db [label="SNI Routing",color=orange] + replikey -> redis [label="SNI Routing",color=orange] + server_cert [label="Server cert", shape=note] + server_key [label="Server key", shape=note] + server_key -> server_cert [label="Private key"] + web_app -> db + web_app -> redis + + ca_cert [label="CA cert", shape=note] + + server_cert -> replikey [label="Authenticate"] + ca_cert -> replikey [label="Trust"] + + listen_master_web [label=":80", shape=triangle,rank=0] + listen_master_replikey [label=":6443", shape=triangle,rank=0] + replikey -> listen_master_replikey [label="Listen",dir=back] + web_app -> listen_master_web [label="Listen"] + } + + subgraph cluster_1 { + label="Slave docker compose" + db_slave [label="Postgres", shape=box] + redis_slave [label="Redis", shape=box] + replikey_slave_db [label="Replikey DB Client", shape=box] + replikey_slave_redis [label="Replikey Redis Client", shape=box] + db_slave -> replikey_slave_db [label="Plain TCP",color=orange] + redis_slave -> replikey_slave_redis [label="Plain TCP",color=orange] + client_cert [label="Client cert", shape=note] + client_key [label="Client key", shape=note] + client_key -> client_cert [label="Private key"] + ca_cert_slave [label="CA cert", shape=note] + + client_cert -> replikey_slave_db [label="Authenticate"] + ca_cert_slave -> replikey_slave_db [label="Trust"] + client_cert -> replikey_slave_redis [label="Authenticate"] + ca_cert_slave -> replikey_slave_redis [label="Trust"] + + } + + replikey_slave_db -> listen_master_replikey [label="TLS with SNI",constraint=false,color=green] + replikey_slave_redis -> listen_master_replikey [label="TLS with SNI",constraint=false,color=green] +} \ No newline at end of file diff --git a/doc/architecture.svg b/doc/architecture.svg new file mode 100644 index 0000000..a6dc8c2 --- /dev/null +++ b/doc/architecture.svg @@ -0,0 +1,385 @@ + + + + + + + + + cluster_pki + + PKI + + + cluster_pki_crl + + CRL Infrastructure(Optional) + + + cluster_0 + + Master docker compose + + + cluster_1 + + Slave docker compose + + + + ca + + + + CA Key + + + + crl_listener + + http://my.crl + + + + crl + + crl + + + + crl->crl_listener + + + Static file + + + + web_app + + Web app + + + + db + + Postgres + + + + web_app->db + + + + + + redis + + Redis + + + + web_app->redis + + + + + + listen_master_web + + :80 + + + + web_app->listen_master_web + + + Listen + + + + replikey + + Replikey + + + + replikey->db + + + SNI Routing + + + + replikey->redis + + + SNI Routing + + + + listen_master_replikey + + :6443 + + + + replikey->listen_master_replikey + + + Listen + + + + server_cert + + + + Server cert + + + + server_cert->replikey + + + Authenticate + + + + server_key + + + + Server key + + + + server_key->server_cert + + + Private key + + + + ca_cert + + + + CA cert + + + + ca_cert->replikey + + + Trust + + + + db_slave + + Postgres + + + + replikey_slave_db + + Replikey DB Client + + + + db_slave->replikey_slave_db + + + Plain TCP + + + + redis_slave + + Redis + + + + replikey_slave_redis + + Replikey Redis Client + + + + redis_slave->replikey_slave_redis + + + Plain TCP + + + + replikey_slave_db->listen_master_replikey + + + TLS with SNI + + + + replikey_slave_redis->listen_master_replikey + + + TLS with SNI + + + + client_cert + + + + Client cert + + + + client_cert->replikey_slave_db + + + Authenticate + + + + client_cert->replikey_slave_redis + + + Authenticate + + + + client_key + + + + Client key + + + + client_key->client_cert + + + Private key + + + + ca_cert_slave + + + + CA cert + + + + ca_cert_slave->replikey_slave_db + + + Trust + + + + ca_cert_slave->replikey_slave_redis + + + Trust + + + \ No newline at end of file diff --git a/src/bin/replikey.rs b/src/bin/replikey.rs new file mode 100644 index 0000000..93ed220 --- /dev/null +++ b/src/bin/replikey.rs @@ -0,0 +1,200 @@ +use clap::Parser; + +#[cfg(feature = "setup-postgres")] +use replikey::ops::postgres::{ + add_table_to_postgres_pub, drop_postgres_pub, drop_postgres_sub, drop_table_from_postgres_pub, + setup_postgres_pub, setup_postgres_sub, SetupPostgresMasterCommand, + SetupPostgresMasterSubCommand, SetupPostgresSlaveCommand, SetupPostgresSlaveSubCommand, +}; + +#[cfg(feature = "keygen")] +use replikey::{ + cert::UsageType, + ops::service::{ + service_replicate_master, service_replicate_slave, ServiceCommand, ServiceSubCommand, + }, +}; + +#[cfg(feature = "keygen")] +use replikey::ops::cert::*; + +#[cfg(feature = "networking")] +use replikey::ops::network::*; +use rustls::crypto::{aws_lc_rs, CryptoProvider}; + +#[derive(Debug, Parser)] +#[clap(name = "replikey")] +#[clap(version = env!("CARGO_PKG_VERSION"))] +struct Opts { + #[clap(subcommand)] + subcmd: SubCommand, +} + +#[derive(Debug, Parser)] +#[clap(name = "not-available")] +struct NotAvailable; + +#[derive(Debug, Parser)] +enum SubCommand { + #[cfg(feature = "keygen")] + #[clap(name = "cert")] + Cert(CertCommand), + + #[cfg(feature = "networking")] + #[clap(name = "network")] + Network(NetworkCommand), + + #[clap(name = "service")] + #[cfg(feature = "service")] + Service(ServiceCommand), + + #[cfg(feature = "setup-postgres")] + #[clap(name = "setup-postgres-master")] + SetupPostgresMaster(SetupPostgresMasterCommand), + + #[cfg(feature = "setup-postgres")] + #[clap(name = "setup-postgres-slave")] + SetupPostgresSlave(SetupPostgresSlaveCommand), + + Info, +} +#[cfg(feature = "asyncio")] +fn start_runtime() -> tokio::runtime::Runtime { + use tokio::runtime::Builder; + Builder::new_current_thread().enable_all().build().unwrap() +} + +fn main() { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "info"); + } + env_logger::init(); + + let opts: Opts = Opts::parse(); + + CryptoProvider::install_default(aws_lc_rs::default_provider()) + .expect("Failed to install crypto provider"); + + match opts.subcmd { + SubCommand::Info => { + println!("replikey v{}", env!("CARGO_PKG_VERSION")); + println!("Feature flags:"); + macro_rules! print_feature { + ($feature:literal) => { + println!( + " {}: {}", + $feature, + if cfg!(feature = $feature) { + "YES" + } else { + "NO" + } + ); + }; + } + print_feature!("serde"); + print_feature!("keygen"); + print_feature!("asyncio"); + print_feature!("networking"); + print_feature!("service"); + print_feature!("remote-crl"); + print_feature!("setup-postgres"); + } + #[cfg(feature = "keygen")] + SubCommand::Cert(cert) => match cert.subcmd { + CertSubCommand::CreateCa(ca) => { + create_ca(ca, true); + } + CertSubCommand::CreateServer(server) => { + create_server(server); + } + CertSubCommand::CreateClient(client) => { + create_client(client); + } + CertSubCommand::SignServerCSR(opts) => { + sign_csr(opts, UsageType::Server, true); + } + CertSubCommand::SignClientCSR(opts) => { + sign_csr(opts, UsageType::Client, true); + } + CertSubCommand::GenerateCrl(opts) => { + revoke_cert(opts); + } + }, + #[cfg(feature = "service")] + SubCommand::Network(network) => match network.subcmd { + NetworkSubCommand::ReverseProxy(opts) => { + println!("Reverse proxy: {:?}", opts); + let rt = start_runtime(); + + rt.block_on(reverse_proxy(opts)) + .expect("Failed to run reverse proxy"); + } + NetworkSubCommand::ForwardProxy(opts) => { + println!("Forward proxy: {:?}", opts); + + let rt = start_runtime(); + + rt.block_on(forward_proxy(opts)) + .expect("Failed to run forward proxy"); + } + }, + #[cfg(feature = "service")] + SubCommand::Service(service) => match service.subcmd { + ServiceSubCommand::ReplicateMaster { config } => { + service_replicate_master(config); + } + ServiceSubCommand::ReplicateSlave { config } => { + service_replicate_slave(config); + } + }, + #[cfg(feature = "setup-postgres")] + SubCommand::SetupPostgresMaster(opts) => { + let conn = opts.connection_string.as_deref(); + let rt = start_runtime(); + rt.block_on(async { + match opts.subcmd { + SetupPostgresMasterSubCommand::Setup(opts) => { + setup_postgres_pub(conn, opts) + .await + .expect("Failed to setup publication"); + } + SetupPostgresMasterSubCommand::Drop(opts) => { + drop_postgres_pub(conn, opts) + .await + .expect("Failed to drop publication"); + } + SetupPostgresMasterSubCommand::AddTable(opts) => { + add_table_to_postgres_pub(conn, opts) + .await + .expect("Failed to add table to publication"); + } + SetupPostgresMasterSubCommand::DropTable(opts) => { + drop_table_from_postgres_pub(conn, opts) + .await + .expect("Failed to drop table from publication"); + } + } + }); + } + #[cfg(feature = "setup-postgres")] + SubCommand::SetupPostgresSlave(opts) => { + let conn = opts.connection_string.as_deref(); + let rt = start_runtime(); + rt.block_on(async { + match opts.subcmd { + SetupPostgresSlaveSubCommand::Setup(opts) => { + setup_postgres_sub(conn, opts) + .await + .expect("Failed to setup subscription"); + } + SetupPostgresSlaveSubCommand::Drop(opts) => { + drop_postgres_sub(conn, opts) + .await + .expect("Failed to drop subscription"); + } + } + }); + } + } +} diff --git a/src/cert.rs b/src/cert.rs new file mode 100644 index 0000000..0081ed6 --- /dev/null +++ b/src/cert.rs @@ -0,0 +1,161 @@ +use rand_core::{OsRng, RngCore}; +use rcgen::{ + BasicConstraints, CertificateParams, CrlDistributionPoint, DistinguishedName, + ExtendedKeyUsagePurpose, Ia5String, IsCa, KeyUsagePurpose, SanType, SerialNumber, +}; +use time::OffsetDateTime; + +pub fn default_ca_options(not_after: OffsetDateTime, dn: DistinguishedName) -> CertificateParams { + let mut start = CertificateParams::default(); + + let mut serial = [0u8; 20]; + OsRng.fill_bytes(&mut serial); + + start.serial_number = Some(SerialNumber::from_slice(&serial)); + start.not_before = OffsetDateTime::now_utc(); + start.not_after = not_after; + start.distinguished_name = dn; + start.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::CrlSign, + KeyUsagePurpose::KeyCertSign, + ]; + start.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ]; + + start.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + + start +} + +pub fn default_server_cert_options( + not_after: OffsetDateTime, + dns_names: &[&str], + ip_addrs: &[&str], + dn: DistinguishedName, + csr: bool, // remove fields that are not needed for csr +) -> CertificateParams { + let mut start = CertificateParams::default(); + + let mut serial = [0u8; 20]; + OsRng.fill_bytes(&mut serial); + start.distinguished_name = dn; + start.not_before = OffsetDateTime::now_utc(); + start.not_after = not_after; + start.subject_alt_names = dns_names + .iter() + .map(|n| SanType::DnsName(Ia5String::try_from(*n).expect("Invalid DNS name"))) + .chain( + ip_addrs + .iter() + .map(|n| SanType::IpAddress(n.parse().unwrap())), + ) + .collect(); + + if !csr { + start.serial_number = Some(SerialNumber::from_slice(&serial)); + start.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::DataEncipherment, + ]; + start.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; + start.is_ca = IsCa::ExplicitNoCa; + } + + start +} + +#[derive(Debug, Clone, Copy)] +pub enum UsageType { + Server, + Client, +} + +pub fn csr_apply_server( + csr: &mut CertificateParams, + not_after: OffsetDateTime, + dns_names: &[&str], + ip_addrs: &[&str], +) { + let mut serial = [0u8; 20]; + OsRng.fill_bytes(&mut serial); + csr.serial_number = Some(SerialNumber::from_slice(&serial)); + csr.not_before = OffsetDateTime::now_utc(); + csr.not_after = not_after.min(csr.not_after); + csr.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::DataEncipherment, + ]; + csr.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; + csr.is_ca = IsCa::ExplicitNoCa; + + csr.subject_alt_names = dns_names + .iter() + .map(|n| SanType::DnsName(Ia5String::try_from(*n).expect("Invalid DNS name"))) + .chain( + ip_addrs + .iter() + .map(|n| SanType::IpAddress(n.parse().unwrap())), + ) + .collect(); +} + +pub fn csr_apply_client( + csr: &mut CertificateParams, + not_after: OffsetDateTime, + dns_names: &[&str], + ip_addrs: &[&str], +) { + let mut serial = [0u8; 20]; + OsRng.fill_bytes(&mut serial); + csr.serial_number = Some(SerialNumber::from_slice(&serial)); + csr.not_before = OffsetDateTime::now_utc(); + csr.not_after = not_after.min(csr.not_after); + csr.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::DataEncipherment, + ]; + csr.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; + csr.is_ca = IsCa::ExplicitNoCa; + + csr.subject_alt_names = dns_names + .iter() + .map(|n| SanType::DnsName(Ia5String::try_from(*n).expect("Invalid DNS name"))) + .chain( + ip_addrs + .iter() + .map(|n| SanType::IpAddress(n.parse().unwrap())), + ) + .collect(); +} + +pub fn default_client_cert_options( + not_after: OffsetDateTime, + dn: DistinguishedName, + crls: Vec, + csr: bool, +) -> CertificateParams { + let mut start = CertificateParams::default(); + start.distinguished_name = dn; + start.not_before = OffsetDateTime::now_utc(); + start.not_after = not_after; + + let mut serial = [0u8; 20]; + OsRng.fill_bytes(&mut serial); + + if !csr { + start.serial_number = Some(SerialNumber::from_slice(&serial)); + start.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::DataEncipherment, + ]; + start.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; + start.is_ca = IsCa::ExplicitNoCa; + + start.crl_distribution_points = crls; + } + + start +} diff --git a/src/fs_crypt.rs b/src/fs_crypt.rs new file mode 100644 index 0000000..3eed987 --- /dev/null +++ b/src/fs_crypt.rs @@ -0,0 +1,221 @@ +use std::{ + fs::OpenOptions, + io::{Read, Seek, Write}, + os::unix::fs::OpenOptionsExt, + path::Path, +}; + +use aes_gcm::{ + aead::{Aead, AeadMutInPlace}, + AeadCore, Aes256Gcm, KeyInit, +}; +use argon2::Argon2; +use rand_core::{OsRng, RngCore}; +use rpassword::prompt_password; + +const KEY_SIZE: usize = 32; +const SALT_SIZE: usize = 16; +const NONCE_SIZE: usize = 12; + +#[derive(Debug, Clone, Copy)] +pub enum Argon2Hardness { + Test, + Default, + Hard, +} + +pub fn private_fs_write, C: AsRef<[u8]>>(path: P, data: C) -> std::io::Result<()> { + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path)?; + + file.write_all(data.as_ref()) +} + +pub fn derive_password(password: &str, salt: &[u8], hardness: Argon2Hardness) -> [u8; KEY_SIZE] { + let mut key = [0u8; KEY_SIZE]; + Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + match hardness { + Argon2Hardness::Test => argon2::ParamsBuilder::default() + .t_cost(1) + .m_cost(16) + .build() + .unwrap(), + Argon2Hardness::Default => argon2::ParamsBuilder::default().build().unwrap(), + Argon2Hardness::Hard => argon2::ParamsBuilder::default() + .t_cost(30) + .m_cost(128 << 20) + .build() + .unwrap(), + }, + ) + .hash_password_into(password.as_bytes(), salt, &mut key) + .unwrap(); + key +} + +fn pkcs7_pad(output: &mut Vec, block_size: usize) { + let pad = block_size - output.len() % block_size; + output.extend(std::iter::repeat(pad as u8).take(pad)); +} + +pub fn encrypt(data: &mut Vec, password: &str, hardness: Argon2Hardness) { + let mut salt = [0u8; SALT_SIZE]; + OsRng + .try_fill_bytes(&mut salt) + .expect("Failed to generate salt"); + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let key = derive_password(password, &salt, hardness); + + let mut header = vec![1]; // in case we need to add a version byte + header.extend_from_slice(&salt); + header.extend_from_slice(&nonce); + + let mut cipher = Aes256Gcm::new_from_slice(&key).unwrap(); + + pkcs7_pad(data, 16); + + cipher + .encrypt_in_place(&nonce, &[], data) + .expect("Encryption failed"); + + data.splice(0..0, header); +} + +pub fn decrypt(data: &[u8], password: &str, hardness: Argon2Hardness) -> Vec { + if data[0] != 1 { + panic!("Unsupported version"); + } + let salt = &data[1..SALT_SIZE + 1]; + let nonce = &data[SALT_SIZE + 1..SALT_SIZE + NONCE_SIZE + 1]; + let key = derive_password(password, salt, hardness); + + let cipher = Aes256Gcm::new_from_slice(&key).unwrap(); + if (data.len() - 1 - SALT_SIZE - NONCE_SIZE) % 16 != 0 { + panic!("Invalid data length"); + } + + let mut output = cipher + .decrypt(nonce.into(), &data[SALT_SIZE + NONCE_SIZE + 1..]) + .expect("Decryption failed"); + + let pad = output[output.len() - 1] as usize; + output.truncate(output.len() - pad); + output +} + +pub fn encrypt_to_pem(data: &[u8], password: &str, hardness: Argon2Hardness) -> String { + let mut encrypted = data.to_vec(); + encrypt(&mut encrypted, password, hardness); + + pem_rfc7468::encode_string("ENCRYPTED DATA", pem_rfc7468::LineEnding::LF, &encrypted) + .expect("Failed to encode PEM") +} + +pub fn write_encrypted_pem( + mut writer: W, + data: &[u8], + password: &str, + hardness: Argon2Hardness, +) -> std::io::Result<()> { + let mut encrypted = data.to_vec(); + encrypt(&mut encrypted, password, hardness); + + let pem = pem_rfc7468::encode_string("ENCRYPTED DATA", pem_rfc7468::LineEnding::LF, &encrypted) + .expect("Failed to encode PEM"); + + writer.write_all(pem.as_bytes()) +} + +pub fn read_maybe_encrypted_pem( + mut reader: R, + password: Option<&str>, + hardness: Argon2Hardness, +) -> std::io::Result>> { + const SIGNATURE: &[u8] = b"-----BEGIN ENCRYPTED DATA-----"; + let mut signature = [0u8; SIGNATURE.len()]; + match reader.read_exact(&mut signature) { + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + reader.seek(std::io::SeekFrom::Start(0))?; + return Ok(None); + } + Err(e) => { + return Err(e); + } + Ok(_) => { + if signature != SIGNATURE { + reader.seek(std::io::SeekFrom::Start(0))?; + return Ok(None); + } + } + } + reader.seek(std::io::SeekFrom::Start(0))?; + let mut pem = String::new(); + reader.read_to_string(&mut pem)?; + + let (_, encrypted) = pem_rfc7468::decode_vec(pem.as_bytes()).expect("Failed to decode PEM"); + + match password { + None => { + let password = prompt_password("Enter password: ").expect("Failed to read password"); + + Ok(Some(decrypt(&encrypted, &password, hardness))) + } + Some(password) => Ok(Some(decrypt(&encrypted, password, hardness))), + } +} + +#[cfg(test)] +mod tests { + + use std::io::Cursor; + + use super::*; + + #[test] + fn test_kdf() { + let password = "password"; + let salt = [0u8; SALT_SIZE]; + let key = derive_password(password, &salt, Argon2Hardness::Test); + assert_eq!(key.len(), KEY_SIZE); + let key2 = derive_password(password, &salt, Argon2Hardness::Test); + assert_eq!(key, key2); + } + + #[test] + fn test_encrypt_decrypt() { + let data = (0..64).map(|_| "Hello, world!").collect::(); + + for len in 0..data.len() { + let password = "password"; + let mut encrypted = data.as_bytes()[..len].to_vec(); + encrypt(&mut encrypted, password, Argon2Hardness::Test); + let decrypted = decrypt(&encrypted, password, Argon2Hardness::Test); + assert_eq!(&data.as_bytes()[..len], &decrypted[..]); + } + } + + #[test] + fn test_pem() { + let data = (0..64).map(|_| "Hello, world!").collect::(); + + for len in 0..data.len() { + let password = "password"; + let pt = data.as_bytes()[..len].to_vec(); + let mut pem = Vec::new(); + write_encrypted_pem(&mut pem, &pt, password, Argon2Hardness::Test) + .expect("Failed to write PEM"); + let mut pem = Cursor::new(pem); + let decrypted = + read_maybe_encrypted_pem(&mut pem, Some(password), Argon2Hardness::Test) + .expect("Failed to read PEM") + .expect("Failed to decrypt"); + assert_eq!(&data.as_bytes()[..len], &decrypted[..]); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4722e34 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "keygen")] +pub mod cert; +pub mod ops; + +pub mod fs_crypt; diff --git a/src/ops/cert.rs b/src/ops/cert.rs new file mode 100644 index 0000000..7539c5e --- /dev/null +++ b/src/ops/cert.rs @@ -0,0 +1,1020 @@ +use std::io::Read; +use std::{fs, path::PathBuf}; + +use clap::Parser; +use rcgen::{ + CertificateParams, CertificateSigningRequestParams, DistinguishedName, DnType, KeyPair, + RemoteKeyPair, RevokedCertParams, SanType, SerialNumber, PKCS_ECDSA_P256_SHA256, +}; +use rcgen::{CertificateRevocationListParams, KeyIdMethod, PKCS_ECDSA_P384_SHA384}; +use rpassword::prompt_password; +use sha2::digest::generic_array::GenericArray; +use sha2::digest::OutputSizeUser; +use sha2::Digest; +use sha2::Sha512; +use time::{Duration, OffsetDateTime}; + +type CrlSerial = [u8; 20]; + +fn crl_serial_by_time() -> CrlSerial { + let now = OffsetDateTime::now_utc(); + let mut serial = [0u8; 20]; + let res: u128 = now.unix_timestamp_nanos().try_into().unwrap(); + + serial[20 - 16..].copy_from_slice(&res.to_be_bytes()); + serial +} + +use crate::cert::UsageType; +use crate::fs_crypt::{self, private_fs_write, Argon2Hardness}; + +#[derive(Debug, Parser)] +pub struct CertCommand { + #[clap(subcommand)] + pub subcmd: CertSubCommand, +} + +#[derive(Debug, Parser)] +pub enum CertSubCommand { + #[clap(name = "create-ca")] + CreateCa(CreateCaCommand), + #[clap(name = "create-server")] + CreateServer(CreateServerCommand), + #[clap(name = "create-client")] + CreateClient(CreateClientCommand), + + #[clap(name = "generate-crl")] + GenerateCrl(GenerateCrlCommand), + + #[clap(name = "sign-server-csr")] + SignServerCSR(SignCSRCommand), + + #[clap(name = "sign-client-csr")] + SignClientCSR(SignCSRCommand), +} + +#[derive(Debug, Parser)] +pub struct DnOptSet { + #[clap(long)] + dn_country: Option, + #[clap(long)] + dn_state: Option, + #[clap(long)] + dn_locality: Option, + #[clap(long)] + dn_organization: Option, + #[clap(long)] + dn_organizational_unit: Option, +} + +#[derive(Debug, Parser)] +pub struct SignCSRCommand { + #[clap(short, long, help = "The max validity of the signed certificate")] + valid_days: u64, + + #[clap(short, long)] + ca_dir: String, + + #[clap(short, long)] + input_csr: String, + + #[clap(short, long)] + dns_name: Vec, + + #[clap(long)] + ip_address: Vec, + + #[clap(short, long)] + output: String, +} + +#[derive(Debug, Parser)] +pub struct CreateCaCommand { + #[clap(short, long)] + valid_days: u64, + + #[clap(flatten)] + dn_opts: DnOptSet, + + #[clap(short, long)] + dn_common_name: String, + + #[clap(short, long)] + output: Option, +} + +#[derive(Debug, Parser)] +pub struct CreateServerCommand { + #[clap(long)] + valid_days: u64, + + #[clap(flatten)] + dn_opts: DnOptSet, + + #[clap(long)] + dn_common_name: String, + + #[clap(short, long)] + dns_name: Vec, + + #[clap(long)] + ip_address: Vec, + + #[clap(short, long)] + output: Option, +} + +#[derive(Debug, Parser)] +pub struct CreateClientCommand { + #[clap(long)] + valid_days: u64, + + #[clap(flatten)] + dn_opts: DnOptSet, + + #[clap(short, long)] + dn_common_name: String, + + #[clap(short, long)] + output: Option, +} + +#[derive(Debug, Parser)] +pub struct GenerateCrlCommand { + #[clap(long)] + ca_dir: String, + + #[clap(long)] + certs: Vec, + + #[clap(short, long)] + serial: Vec, + + #[clap(short, long)] + output: String, +} + +fn fs_safe_string(s: &str) -> String { + s.replace("/", "_") + .replace("\\", "_") + .replace(":", "_") + .replace("*", "_") +} + +fn populate_optional_dns(dn: &mut DistinguishedName, opts: &DnOptSet) { + if let Some(country) = &opts.dn_country { + dn.push(DnType::CountryName, country); + } + if let Some(state) = &opts.dn_state { + dn.push(DnType::StateOrProvinceName, state); + } + if let Some(locality) = &opts.dn_locality { + dn.push(DnType::LocalityName, locality); + } + if let Some(organization) = &opts.dn_organization { + dn.push(DnType::OrganizationName, organization); + } + if let Some(organizational_unit) = &opts.dn_organizational_unit { + dn.push(DnType::OrganizationalUnitName, organizational_unit); + } +} + +pub fn dump_certificate_params(params: &CertificateParams) { + if let Some(serial_number) = ¶ms.serial_number { + print!("Serial number: "); + for b in serial_number.as_ref() { + print!("{:02x}", b); + } + println!(""); + } + for san in ¶ms.subject_alt_names { + match san { + SanType::DnsName(dns) => println!("SAN: DNS={}", dns), + SanType::IpAddress(ip) => println!("SAN: IP={}", ip), + _ => panic!("Unsupported SAN type"), + } + } + println!("Not before: {:?}", params.not_before); + println!("Not after: {:?}", params.not_after); + println!("Distinguished name: {:?}", params.distinguished_name); + println!("Key usages: {:?}", params.key_usages); + println!("Extended key usages: {:?}", params.extended_key_usages); + println!( + "CRL distribution points: {:?}", + params.crl_distribution_points + ); +} + +pub fn key_signature(key: &KeyPair) -> GenericArray::OutputSize> { + println!("Key algorithm: {:?}", key.algorithm()); + let mut sha512 = Sha512::new(); + + sha512.update(key.public_key_raw()); + + sha512.finalize() +} + +pub fn remote_key_signature( + key: &R, +) -> GenericArray::OutputSize> { + println!("Key algorithm: {:?}", key.algorithm()); + let mut sha512 = Sha512::new(); + + sha512.update(key.public_key()); + + sha512.finalize() +} + +pub fn create_ca(opt: CreateCaCommand, ask_password: bool) { + println!("Creating CA with options: {:?}", opt); + let output = PathBuf::from( + opt.output + .unwrap_or_else(|| fs_safe_string(&opt.dn_common_name)), + ); + if fs::exists(&output).expect("Failed to check if output file exists") + && fs::read_dir(&output) + .expect("Failed to read output directory") + .count() + > 0 + { + eprintln!("Output file already exists"); + std::process::exit(1); + } else { + fs::create_dir_all(&output).expect("Failed to create output directory"); + } + let expire = OffsetDateTime::now_utc() + Duration::days(opt.valid_days as i64); + + let mut dn = DistinguishedName::new(); + populate_optional_dns(&mut dn, &opt.dn_opts); + dn.push(DnType::CommonName, opt.dn_common_name); + + let kp = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("Failed to generate key pair"); + + let cert = crate::cert::default_ca_options(expire, dn) + .self_signed(&kp) + .expect("Failed to create CA"); + + let cert_pem = cert.pem(); + let key_pem = kp.serialize_pem(); + + fs::write(output.join("ca.pem"), cert_pem).expect("Failed to write CA cert"); + + let crl_serial = crl_serial_by_time(); + + let crl_params = CertificateRevocationListParams { + this_update: OffsetDateTime::now_utc(), + next_update: OffsetDateTime::now_utc() + Duration::days(7), // not used, + crl_number: SerialNumber::from_slice(&crl_serial), + issuing_distribution_point: None, + revoked_certs: vec![], + key_identifier_method: KeyIdMethod::Sha256, + }; + + let crl = crl_params + .signed_by(&cert, &kp) + .expect("Failed to create CRL"); + + fs::write(output.join("ca.crl"), crl.der()).expect("Failed to write CRL"); + + if !ask_password { + private_fs_write(output.join("ca.key"), key_pem).expect("Failed to write CA key"); + return; + } + let password = + prompt_password("Provide password for CA key: ").expect("Failed to read password"); + if password != password.trim() { + eprintln!("Password contains leading or trailing whitespace"); + std::process::exit(1); + } + let password_repeated = prompt_password("Repeat password: ").expect("Failed to read password"); + if password != password_repeated { + eprintln!("Passwords do not match"); + std::process::exit(1); + } + if password != password_repeated { + eprintln!("Passwords do not match"); + std::process::exit(1); + } + let key_pem_encrypted = + fs_crypt::encrypt_to_pem(key_pem.as_bytes(), &password, Argon2Hardness::Default); + private_fs_write(output.join("ca.key"), key_pem_encrypted).expect("Failed to write CA key"); +} + +pub fn create_server(server: CreateServerCommand) { + println!("Creating server cert with options: {:?}", server); + + let output = PathBuf::from( + server + .output + .unwrap_or_else(|| fs_safe_string(&server.dn_common_name)), + ); + + if fs::exists(&output).expect("Failed to check if output file exists") + && fs::read_dir(&output) + .expect("Failed to read output directory") + .count() + > 0 + { + eprintln!("Output file already exists"); + std::process::exit(1); + } else { + fs::create_dir_all(&output).expect("Failed to create output directory"); + } + + let expire = OffsetDateTime::now_utc() + Duration::days(server.valid_days as i64); + + let mut dn = DistinguishedName::new(); + populate_optional_dns(&mut dn, &server.dn_opts); + dn.push(DnType::CommonName, server.dn_common_name); + + let kp = KeyPair::generate_for(&PKCS_ECDSA_P384_SHA384).expect("Failed to generate key pair"); + + let cert_opts = crate::cert::default_server_cert_options( + expire, + &server + .dns_name + .iter() + .map(|s| s.as_str()) + .collect::>(), + &server + .ip_address + .iter() + .map(|s| s.as_str()) + .collect::>(), + dn.clone(), + false, + ); + let cert_opts_csr = crate::cert::default_server_cert_options( + expire, + &server + .dns_name + .iter() + .map(|s| s.as_str()) + .collect::>(), + &server + .ip_address + .iter() + .map(|s| s.as_str()) + .collect::>(), + dn, + true, + ); + + let csr = cert_opts_csr + .serialize_request(&kp) + .expect("Failed to serialize CSR"); + + let self_signed_cert = cert_opts + .clone() + .self_signed(&kp) + .expect("Failed to create server self-signed cert"); + + let cert_pem = self_signed_cert.pem(); + + fs::write(output.join("server.pem"), cert_pem).expect("Failed to write server cert"); + private_fs_write(output.join("server.key"), kp.serialize_pem()) + .expect("Failed to write server key"); + fs::write( + output.join("server.csr"), + csr.pem().expect("Failed to serialize CSR"), + ) + .expect("Failed to write server CSR"); + + println!("Summary:"); + dump_certificate_params(&cert_opts); + println!("Key signature: {:02x?}", key_signature(&kp)); + println!( + "Server cert created, sign on the CA side to get the usable cert with CSR {}", + output.join("server.csr").to_str().unwrap() + ); +} + +pub fn create_client(client: CreateClientCommand) { + println!("Creating client cert with options: {:?}", client); + + let output = PathBuf::from( + client + .output + .unwrap_or_else(|| fs_safe_string(&client.dn_common_name)), + ); + + if fs::exists(&output).expect("Failed to check if output file exists") + && fs::read_dir(&output) + .expect("Failed to read output directory") + .count() + > 0 + { + eprintln!("Output file already exists and is not empty"); + std::process::exit(1); + } else { + fs::create_dir_all(&output).expect("Failed to create output directory"); + } + + let expire = OffsetDateTime::now_utc() + Duration::days(client.valid_days as i64); + + let mut dn = DistinguishedName::new(); + populate_optional_dns(&mut dn, &client.dn_opts); + dn.push(DnType::CommonName, client.dn_common_name); + + let kp = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("Failed to generate key pair"); + + let cert_opts_csr = crate::cert::default_client_cert_options(expire, dn.clone(), vec![], true); + let cert_opts = crate::cert::default_client_cert_options(expire, dn, vec![], false); + + let csr = cert_opts_csr + .serialize_request(&kp) + .expect("Failed to serialize CSR"); + + let self_signed_cert = cert_opts + .clone() + .self_signed(&kp) + .expect("Failed to create client self-signed cert"); + + let cert_pem = self_signed_cert.pem(); + + fs::write(output.join("client.pem"), cert_pem).expect("Failed to write client cert"); + fs::write(output.join("client.key"), kp.serialize_pem()).expect("Failed to write client key"); + fs::write( + output.join("client.csr"), + csr.pem().expect("Failed to serialize CSR"), + ) + .expect("Failed to write client CSR"); + + println!("Summary:"); + dump_certificate_params(&cert_opts); + println!("Key signature: {:02x?}", key_signature(&kp)); + println!( + "Client cert created, sign on the CA side to get the usable cert with CSR {}", + output.join("client.csr").to_str().unwrap() + ); +} + +pub fn sign_csr(opts: SignCSRCommand, usage: UsageType, confirm: bool) { + println!("Signing CSR with options: {:?}", opts); + + let ca_dir = PathBuf::from(&opts.ca_dir); + let input_csr = PathBuf::from(&opts.input_csr); + let output = PathBuf::from(&opts.output); + + let ca_cert = fs::read_to_string(ca_dir.join("ca.pem")).expect("Failed to read CA cert"); + + let mut ca_key_file = fs::File::open(ca_dir.join("ca.key")).expect("Failed to open CA key"); + let ca_key = + fs_crypt::read_maybe_encrypted_pem(&mut ca_key_file, None, Argon2Hardness::Default) + .expect("Failed to read CA key") + .map(|b| String::from_utf8(b).expect("Failed to read CA key")) + .unwrap_or_else(|| { + let mut buf = String::new(); + ca_key_file + .read_to_string(&mut buf) + .expect("Failed to read CA key"); + buf + }); + + let ca_key = KeyPair::from_pem(&ca_key).expect("Failed to parse CA key"); + + let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert) + .expect("Failed to parse CA cert") + .self_signed(&ca_key) + .expect("Failed to create CA cert"); + + let csr = fs::read_to_string(&input_csr).expect("Failed to read CSR"); + + let mut csr = CertificateSigningRequestParams::from_pem(&csr).expect("Failed to parse CSR"); + + match usage { + UsageType::Server => crate::cert::csr_apply_server( + &mut csr.params, + OffsetDateTime::now_utc() + Duration::days(opts.valid_days as i64), + &opts + .dns_name + .iter() + .map(|s| s.as_str()) + .collect::>(), + &opts + .ip_address + .iter() + .map(|s| s.as_str()) + .collect::>(), + ), + UsageType::Client => crate::cert::csr_apply_client( + &mut csr.params, + OffsetDateTime::now_utc() + Duration::days(opts.valid_days as i64), + &opts + .dns_name + .iter() + .map(|s| s.as_str()) + .collect::>(), + &opts + .ip_address + .iter() + .map(|s| s.as_str()) + .collect::>(), + ), + } + + eprintln!("CSR Params:"); + dump_certificate_params(&csr.params); + + if confirm { + let mut confirm = String::new(); + println!("Do you want to sign this CSR? (YES/NO)"); + std::io::stdin() + .read_line(&mut confirm) + .expect("Failed to read confirmation"); + if confirm.trim().to_lowercase() != "yes" { + eprintln!("Aborting"); + std::process::exit(1); + } + } + + let signed = csr + .signed_by(&ca_cert, &ca_key) + .expect("Failed to sign CSR"); + + println!("Signed Params:"); + dump_certificate_params(signed.params()); + println!("IMPORTANT: Keep this certificate or its serial number for revocation"); + + fs::write(&output, signed.pem()).expect("Failed to write signed cert"); +} + +fn hex_parse(s: &str) -> Vec { + let mut res = Vec::new(); + for i in 0..s.len() / 2 { + res.push(u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("Failed to parse hex")); + } + res +} + +pub fn revoke_cert(opts: GenerateCrlCommand) { + println!("Revoking cert with options: {:?}", opts); + + let ca_dir = PathBuf::from(&opts.ca_dir); + let ca_cert = fs::read_to_string(ca_dir.join("ca.pem")).expect("Failed to read CA cert"); + + let mut ca_key_file = fs::File::open(ca_dir.join("ca.key")).expect("Failed to open CA key"); + let ca_key = + fs_crypt::read_maybe_encrypted_pem(&mut ca_key_file, None, Argon2Hardness::Default) + .expect("Failed to read CA key") + .map(|b| String::from_utf8(b).expect("Failed to read CA key")) + .unwrap_or_else(|| { + let mut buf = String::new(); + ca_key_file + .read_to_string(&mut buf) + .expect("Failed to read CA key"); + buf + }); + + let ca_key = KeyPair::from_pem(&ca_key).expect("Failed to parse CA key"); + + let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert) + .expect("Failed to parse CA cert") + .self_signed(&ca_key) + .expect("Failed to create CA cert"); + + let crl_serial = crl_serial_by_time(); + + let mut crl = CertificateRevocationListParams { + this_update: OffsetDateTime::now_utc(), + next_update: OffsetDateTime::now_utc() + Duration::days(7), + crl_number: SerialNumber::from_slice(&crl_serial), + issuing_distribution_point: None, + revoked_certs: vec![], + key_identifier_method: KeyIdMethod::Sha256, + }; + + for cert in opts.certs { + let cert = fs::read_to_string(cert).expect("Failed to read cert"); + let cert = CertificateParams::from_ca_cert_pem(&cert) + .expect("Failed to parse cert") + .self_signed(&ca_key) + .expect("Failed to create cert"); + crl.revoked_certs.push(RevokedCertParams { + serial_number: cert + .params() + .serial_number + .as_ref() + .expect("Cert has no serial number") + .clone(), + revocation_time: OffsetDateTime::now_utc(), // not needed + reason_code: None, + invalidity_date: None, + }); + } + + for serial in opts.serial { + crl.revoked_certs.push(RevokedCertParams { + serial_number: SerialNumber::from_slice(&hex_parse(&serial)), + revocation_time: OffsetDateTime::now_utc(), // not needed + reason_code: None, + invalidity_date: None, + }); + } + + let crl = crl + .signed_by(&ca_cert, &ca_key) + .expect("Failed to create CRL"); + + fs::write(&opts.output, crl.der()).expect("Failed to write CRL"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hex_parse() { + assert_eq!(hex_parse("00"), vec![0]); + assert_eq!(hex_parse("ff"), vec![255]); + assert_eq!(hex_parse("ff00"), vec![255, 0]); + assert_eq!(hex_parse("ff000f"), vec![255, 0, 15]); + } + + #[test] + fn test_fs_safe_string() { + assert_eq!(fs_safe_string("foo"), "foo"); + assert_eq!(fs_safe_string("foo/bar"), "foo_bar"); + assert_eq!(fs_safe_string("foo\\bar"), "foo_bar"); + assert_eq!(fs_safe_string("foo:bar"), "foo_bar"); + assert_eq!(fs_safe_string("foo*bar"), "foo_bar"); + } + + #[test] + fn test_can_create_ca() { + let temp = tempfile::TempDir::new().unwrap(); + let opt = CreateCaCommand { + valid_days: 365, + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "Example CA".to_string(), + output: Some(temp.path().to_str().unwrap().to_string()), + }; + + create_ca(opt, false); + + assert!(temp.path().join("ca.pem").exists()); + assert!(temp.path().join("ca.key").exists()); + + #[cfg(feature = "test-crosscheck-openssl")] + { + let ca_cert = + openssl::x509::X509::from_pem(&fs::read(temp.path().join("ca.pem")).unwrap()) + .expect("Failed to parse CA cert"); + let ca_key = openssl::pkey::PKey::private_key_from_pem( + &fs::read(temp.path().join("ca.key")).unwrap(), + ) + .expect("Failed to parse CA key"); + + assert!(ca_cert.verify(&ca_key).unwrap()); + } + } + + #[test] + fn test_can_create_server() { + let temp = tempfile::TempDir::new().unwrap(); + let ca_temp = tempfile::TempDir::new().unwrap(); + let ca_opt = CreateCaCommand { + valid_days: 365, + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "Example CA".to_string(), + output: Some(ca_temp.path().to_str().unwrap().to_string()), + }; + + create_ca(ca_opt, false); + + let opt = CreateServerCommand { + valid_days: 365, + dns_name: vec!["example.com".to_string()], + ip_address: vec![], + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "example.com".to_string(), + output: Some(temp.path().to_str().unwrap().to_string()), + }; + + create_server(opt); + + assert!(temp.path().join("server.pem").exists()); + assert!(temp.path().join("server.key").exists()); + assert!(temp.path().join("server.csr").exists()); + + #[cfg(feature = "test-crosscheck-openssl")] + { + let server_cert = + openssl::x509::X509::from_pem(&fs::read(temp.path().join("server.pem")).unwrap()) + .expect("Failed to parse server cert"); + + let server_key = openssl::pkey::PKey::private_key_from_pem( + &fs::read(temp.path().join("server.key")).unwrap(), + ) + .expect("Failed to parse server key"); + + assert!(server_cert.verify(&server_key).unwrap()); + } + } + + #[test] + fn test_can_create_client() { + let temp = tempfile::TempDir::new().unwrap(); + let ca_temp = tempfile::TempDir::new().unwrap(); + let ca_opt = CreateCaCommand { + valid_days: 365, + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "Example CA".to_string(), + output: Some(ca_temp.path().to_str().unwrap().to_string()), + }; + + create_ca(ca_opt, false); + + let opt = CreateClientCommand { + valid_days: 365, + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "example.com".to_string(), + output: Some(temp.path().to_str().unwrap().to_string()), + }; + + create_client(opt); + + assert!(temp.path().join("client.pem").exists()); + assert!(temp.path().join("client.key").exists()); + assert!(temp.path().join("client.csr").exists()); + + #[cfg(feature = "test-crosscheck-openssl")] + { + let client_cert = + openssl::x509::X509::from_pem(&fs::read(temp.path().join("client.pem")).unwrap()) + .expect("Failed to parse client cert"); + + let client_key = openssl::pkey::PKey::private_key_from_pem( + &fs::read(temp.path().join("client.key")).unwrap(), + ) + .expect("Failed to parse client key"); + + assert!(client_cert.verify(&client_key).unwrap()); + } + } + + #[test] + fn test_can_revocate_cert() { + let ca_temp = tempfile::TempDir::new().unwrap(); + let ca_opt = CreateCaCommand { + valid_days: 365, + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "Example CA".to_string(), + output: Some(ca_temp.path().to_str().unwrap().to_string()), + }; + + create_ca(ca_opt, false); + + let client_temp = tempfile::TempDir::new().unwrap(); + let client_opt = CreateClientCommand { + valid_days: 365, + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "example.com".to_string(), + output: Some(client_temp.path().to_str().unwrap().to_string()), + }; + + create_client(client_opt); + + let sign_temp = tempfile::TempDir::new().unwrap(); + + let sign_opt = SignCSRCommand { + valid_days: 365, + ca_dir: ca_temp.path().to_str().unwrap().to_string(), + dns_name: vec![], + ip_address: vec![], + input_csr: client_temp + .path() + .join("client.csr") + .to_str() + .unwrap() + .to_string(), + output: sign_temp + .path() + .join("client.pem") + .to_str() + .unwrap() + .to_string(), + }; + + sign_csr(sign_opt, UsageType::Client, false); + + assert!(sign_temp.path().join("client.pem").exists()); + + let crl_temp = tempfile::TempDir::new().unwrap(); + + let crl_opt = GenerateCrlCommand { + ca_dir: ca_temp.path().to_str().unwrap().to_string(), + certs: vec![sign_temp + .path() + .join("client.pem") + .to_str() + .unwrap() + .to_string()], + serial: vec![], + output: crl_temp.path().join("ca.crl").to_str().unwrap().to_string(), + }; + + revoke_cert(crl_opt); + + assert!(crl_temp.path().join("ca.crl").exists()); + } + + #[test] + fn integration_test_sign_csr() { + let ca_temp = tempfile::TempDir::new().unwrap(); + let ca_opt = CreateCaCommand { + valid_days: 365, + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "Example CA".to_string(), + output: Some(ca_temp.path().to_str().unwrap().to_string()), + }; + + create_ca(ca_opt, false); + + let server_temp = tempfile::TempDir::new().unwrap(); + let server_opt = CreateServerCommand { + valid_days: 365, + dns_name: vec!["example.com".to_string()], + ip_address: vec![], + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "example.com".to_string(), + output: Some(server_temp.path().to_str().unwrap().to_string()), + }; + + create_server(server_opt); + + let sign_temp = tempfile::TempDir::new().unwrap(); + let sign_opt = SignCSRCommand { + valid_days: 365, + ca_dir: ca_temp.path().to_str().unwrap().to_string(), + dns_name: vec![], + ip_address: vec![], + input_csr: server_temp + .path() + .join("server.csr") + .to_str() + .unwrap() + .to_string(), + output: sign_temp + .path() + .join("server.pem") + .to_str() + .unwrap() + .to_string(), + }; + + sign_csr(sign_opt, UsageType::Server, false); + + assert!(sign_temp.path().join("server.pem").exists()); + + #[cfg(feature = "test-crosscheck-openssl")] + { + let ca_cert = + openssl::x509::X509::from_pem(&fs::read(ca_temp.path().join("ca.pem")).unwrap()) + .expect("Failed to parse CA cert"); + + let server_cert = openssl::x509::X509::from_pem( + &fs::read(sign_temp.path().join("server.pem")).unwrap(), + ) + .expect("Failed to parse server cert"); + + assert!(server_cert + .verify(&ca_cert.public_key().expect("Failed to get CA public key")) + .unwrap()); + + let ca_cert = + openssl::x509::X509::from_pem(&fs::read(ca_temp.path().join("ca.pem")).unwrap()) + .expect("Failed to parse CA cert"); + + assert!(server_cert + .verify(&ca_cert.public_key().expect("Failed to get CA public key")) + .unwrap()); + + let ca_key = openssl::pkey::PKey::private_key_from_pem( + &fs::read(ca_temp.path().join("ca.key")).unwrap(), + ) + .expect("Failed to parse CA key"); + + assert!(server_cert.verify(&ca_key).unwrap()); + } + + let client_temp = tempfile::TempDir::new().unwrap(); + + let client_opt = CreateClientCommand { + valid_days: 365, + dn_opts: DnOptSet { + dn_country: Some("US".to_string()), + dn_state: Some("CA".to_string()), + dn_locality: Some("San Francisco".to_string()), + dn_organization: Some("Example Inc.".to_string()), + dn_organizational_unit: Some("IT".to_string()), + }, + dn_common_name: "example.com".to_string(), + output: Some(client_temp.path().to_str().unwrap().to_string()), + }; + + create_client(client_opt); + + let sign_temp = tempfile::TempDir::new().unwrap(); + + let sign_opt = SignCSRCommand { + valid_days: 365, + ca_dir: ca_temp.path().to_str().unwrap().to_string(), + dns_name: vec![], + ip_address: vec![], + input_csr: client_temp + .path() + .join("client.csr") + .to_str() + .unwrap() + .to_string(), + output: sign_temp + .path() + .join("client.pem") + .to_str() + .unwrap() + .to_string(), + }; + + sign_csr(sign_opt, UsageType::Client, false); + + assert!(sign_temp.path().join("client.pem").exists()); + + #[cfg(feature = "test-crosscheck-openssl")] + { + let ca_cert = + openssl::x509::X509::from_pem(&fs::read(ca_temp.path().join("ca.pem")).unwrap()) + .expect("Failed to parse CA cert"); + + let client_cert = openssl::x509::X509::from_pem( + &fs::read(sign_temp.path().join("client.pem")).unwrap(), + ) + .expect("Failed to parse client cert"); + + assert!(client_cert + .verify(&ca_cert.public_key().expect("Failed to get CA public key")) + .unwrap()); + + let ca_key = openssl::pkey::PKey::private_key_from_pem( + &fs::read(ca_temp.path().join("ca.key")).unwrap(), + ) + .expect("Failed to parse CA key"); + + assert!(client_cert.verify(&ca_key).unwrap()); + } + } +} diff --git a/src/ops/mod.rs b/src/ops/mod.rs new file mode 100644 index 0000000..66ec2d2 --- /dev/null +++ b/src/ops/mod.rs @@ -0,0 +1,9 @@ +#[cfg(feature = "keygen")] +pub mod cert; +#[cfg(feature = "networking")] +pub mod network; +#[cfg(feature = "service")] +pub mod service; + +#[cfg(feature = "setup-postgres")] +pub mod postgres; diff --git a/src/ops/network.rs b/src/ops/network.rs new file mode 100644 index 0000000..ec55308 --- /dev/null +++ b/src/ops/network.rs @@ -0,0 +1,362 @@ +use std::{io::Cursor, net::ToSocketAddrs, sync::Arc}; + +use clap::Parser; +use tokio::{ + io::{AsyncBufRead, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}, + net::TcpStream, +}; +use tokio_rustls::{ + rustls::{ + client::WebPkiServerVerifier, + pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer, ServerName}, + server::WebPkiClientVerifier, + ClientConfig, RootCertStore, ServerConfig, + }, + TlsAcceptor, TlsConnector, +}; + +#[derive(Debug, Parser)] +pub struct NetworkCommand { + #[clap(subcommand)] + pub subcmd: NetworkSubCommand, +} + +#[derive(Debug, Parser)] +pub enum NetworkSubCommand { + #[clap(name = "reverse-proxy")] + ReverseProxy(ReverseProxyCommand), + + #[clap(name = "forward-proxy")] + ForwardProxy(ForwardProxyCommand), +} + +#[derive(Debug, Parser)] +pub struct ReverseProxyCommand { + #[clap(short, long)] + pub listen: String, + + #[clap(long)] + pub redis_sni: String, + + #[clap(long)] + pub redis_target: String, + + #[clap(long)] + pub postgres_sni: String, + + #[clap(long)] + pub postgres_target: String, + + #[clap(long, help = "Certificate")] + pub cert: String, + + #[clap(long, help = "Private key")] + pub key: String, + + #[clap(long, help = "CA to trust")] + pub ca: String, + + #[clap(long, help = "CRLs to use")] + pub crl: Vec, +} + +#[derive(Debug, Parser)] +pub struct ForwardProxyCommand { + #[clap(short, long)] + pub listen: String, + + #[clap(short, long)] + pub sni: String, + + #[clap(short, long)] + pub target: String, + + #[clap(long)] + pub cert: String, + + #[clap(long)] + pub key: String, + + #[clap(long)] + pub ca: String, + + #[clap(long)] + pub crl: Vec, +} + +fn compressor_to(w: impl AsyncWrite + Unpin) -> impl AsyncWrite + Unpin { + async_compression::tokio::write::ZstdEncoder::new(w) +} + +fn decompressor_from(r: impl AsyncBufRead + Unpin) -> impl AsyncRead + Unpin { + async_compression::tokio::bufread::ZstdDecoder::new(r) +} + +async fn send_static_string(w: &mut (impl AsyncWrite + Unpin), s: &str) -> tokio::io::Result<()> { + let mut cursor = Cursor::new(s); + + tokio::io::copy(&mut cursor, &mut compressor_to(w)).await?; + + Ok(()) +} + +async fn copy_bidirectional_compressed( + local: impl AsyncRead + AsyncWrite + Unpin, + remote: impl AsyncRead + AsyncWrite + Unpin, +) -> tokio::io::Result<(u64, u64)> { + let (mut local_rx, mut local_tx) = tokio::io::split(local); + let (remote_rx, remote_tx) = tokio::io::split(remote); + + let remote_rx_buf = BufReader::new(remote_rx); + + let mut remote_tx_comp = compressor_to(remote_tx); + let mut remote_rx_decomp = decompressor_from(remote_rx_buf); + + log::info!("Starting transfer"); + + let uplink = async move { + let res = tokio::io::copy(&mut local_rx, &mut remote_tx_comp).await; + let shutdown = remote_tx_comp.shutdown().await; + let res = res?; + shutdown?; + tokio::io::Result::Ok(res) + }; + let downlink = async move { + let res = tokio::io::copy(&mut remote_rx_decomp, &mut local_tx).await; + let shutdown = local_tx.shutdown().await; + let res = res?; + shutdown?; + tokio::io::Result::Ok(res) + }; + let res = tokio::try_join!(uplink, downlink)?; + log::info!( + "Finished transferring {} bytes from local to remote and {} bytes from remote to local (compressed)", + res.0, + res.1 + ); + + Ok(res) +} + +pub async fn reverse_proxy(opts: ReverseProxyCommand) -> Result<(), Box> { + let (_, ca_pem) = x509_parser::pem::parse_x509_pem(&std::fs::read(&opts.ca)?)?; + let (_, ca_cert) = x509_parser::parse_x509_certificate(&ca_pem.contents)?; + let mut cert_store = RootCertStore::empty(); + cert_store.add(CertificateDer::from_pem_file(&opts.ca)?)?; + let cert_store = Arc::new(cert_store); + + let mut crls = Vec::new(); + + for crl_def in &opts.crl { + #[cfg(feature = "remote-crl")] + { + // crls are signed so we can trust them + if crl_def.starts_with("http://") || crl_def.starts_with("https://") { + log::info!("Downloading CRL: {}", crl_def); + let crl = reqwest::get(crl_def).await?.bytes().await?; + let (_, parsed) = x509_parser::parse_x509_crl(&crl)?; + if let Err(e) = parsed.verify_signature(ca_cert.public_key()) { + log::error!("Failed to verify CRL signature: {}", e); + if !crl_def.starts_with("https://") { + return Err(e.into()); + } + continue; + } + + crls.push(crl.to_vec().into()); + continue; + } + } + crls.push(std::fs::read(crl_def).expect("Failed to read CRL").into()); + } + + let cv = WebPkiClientVerifier::builder(cert_store.clone()) + .with_crls(crls) + .build() + .expect("Failed to build client verifier"); + + let config = ServerConfig::builder() + .with_client_cert_verifier(cv) + .with_single_cert( + vec![CertificateDer::from_pem_file(&opts.cert)?], + PrivateKeyDer::from_pem_file(&opts.key)?, + )?; + + let acceptor = TlsAcceptor::from(Arc::new(config)); + + let listener = tokio::net::TcpListener::bind(&opts.listen).await?; + log::info!("Listening on: {}", opts.listen); + + let (redis_sni, postgres_sni, redis_target, postgres_target) = ( + Arc::new(opts.redis_sni.clone()), + Arc::new(opts.postgres_sni.clone()), + opts.redis_target.clone(), + opts.postgres_target.clone(), + ); + if let Err(e) = opts.redis_target.to_socket_addrs() { + eprintln!("Failed to resolve redis target: {}", e); + return Ok(()); + } + if let Err(e) = opts.postgres_target.to_socket_addrs() { + eprintln!("Failed to resolve postgres target: {}", e); + return Ok(()); + } + loop { + let (pt_stream, _) = match listener.accept().await { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to accept connection: {}", e); + continue; + } + }; + let acceptor = acceptor.clone(); + let (redis_sni, postgres_sni, redis_target, postgres_target) = ( + redis_sni.clone(), + postgres_sni.clone(), + redis_target.clone(), + postgres_target.clone(), + ); + tokio::spawn(async move { + match acceptor.accept(pt_stream).await { + Ok(mut tls) => match tls.get_ref().1.server_name().map(|s| s.to_string()) { + Some(sni) if sni == *redis_sni => { + log::info!( + "Accepted Redis connection for {:?}", + tls.get_ref().1.server_name() + ); + match tokio::net::TcpStream::connect(&redis_target).await { + Ok(redis) => { + if let Err(e) = copy_bidirectional_compressed(redis, tls).await { + eprintln!("Failed to copy data: {}", e); + } + } + Err(e) => { + eprintln!("Failed to connect to redis: {}", e); + tls.shutdown().await.expect("Failed to shutdown TLS stream"); + } + } + } + Some(sni) if sni == *postgres_sni => { + log::info!( + "Accepted Postgres connection for {:?}", + tls.get_ref().1.server_name() + ); + match tokio::net::TcpStream::connect(&postgres_target).await { + Ok(postgres) => { + if let Err(e) = copy_bidirectional_compressed(postgres, tls).await { + eprintln!("Failed to copy data: {}", e); + } + } + Err(e) => { + eprintln!("Failed to connect to postgres: {}", e); + tls.shutdown().await.expect("Failed to shutdown TLS stream"); + } + } + } + Some(sni) => { + log::warn!("Accepted connection for {:?}, but SNI {} does not match any configured SNI", tls.get_ref().1.server_name(), sni); + send_static_string( + &mut tls, + format!("SNI {} does not match any configured SNI", sni).as_str(), + ) + .await + .expect("Failed to send static string"); + tls.shutdown().await.expect("Failed to shutdown TLS stream"); + } + _ => { + send_static_string(&mut tls, "No SNI provided") + .await + .expect("Failed to send static string"); + eprintln!("No SNI provided"); + tls.shutdown().await.expect("Failed to shutdown TLS stream"); + } + }, + Err(e) => { + eprintln!("Failed to accept connection: {}", e); + } + } + }); + } +} + +pub async fn forward_proxy(opts: ForwardProxyCommand) -> Result<(), Box> { + let (_, ca_pem) = x509_parser::pem::parse_x509_pem(&std::fs::read(&opts.ca)?)?; + let (_, ca_cert) = x509_parser::parse_x509_certificate(&ca_pem.contents)?; + let mut cert_store = RootCertStore::empty(); + cert_store.add(CertificateDer::from_pem_file(&opts.ca)?)?; + let cert_store = Arc::new(cert_store); + + let mut crls = Vec::new(); + + for crl_def in &opts.crl { + #[cfg(feature = "remote-crl")] + { + // crls are signed so we can trust them + if crl_def.starts_with("http://") || crl_def.starts_with("https://") { + log::info!("Downloading CRL: {}", crl_def); + let crl = reqwest::get(crl_def).await?.bytes().await?; + let (_, parsed) = x509_parser::parse_x509_crl(&crl)?; + if let Err(e) = parsed.verify_signature(ca_cert.public_key()) { + log::error!("Failed to verify CRL signature: {}", e); + if !crl_def.starts_with("https://") { + return Err(e.into()); + } + continue; + } + + crls.push(crl.to_vec().into()); + continue; + } + } + crls.push(std::fs::read(crl_def).expect("Failed to read CRL").into()); + } + + let cv = WebPkiServerVerifier::builder(cert_store.clone()) + .with_crls(crls) + .build() + .expect("Failed to build server verifier"); + + let config = ClientConfig::builder() + .with_webpki_verifier(cv) + .with_client_auth_cert( + vec![CertificateDer::from_pem_file(&opts.cert)?], + PrivateKeyDer::from_pem_file(&opts.key)?, + )?; + + let connector = TlsConnector::from(Arc::new(config)); + + let listener = tokio::net::TcpListener::bind(&opts.listen).await?; + log::info!("Listening on: {}", opts.listen); + + let sni = ServerName::try_from(opts.sni.as_str()).expect("Failed to parse SNI"); + loop { + let (pt_stream, _) = match listener.accept().await { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to accept connection: {}", e); + continue; + } + }; + let connector = connector.clone(); + let tls_stream = match TcpStream::connect(&opts.target).await { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to connect to target: {}", e); + continue; + } + }; + let sni = sni.to_owned(); + tokio::spawn(async move { + match connector.connect(sni, tls_stream).await { + Ok(tls) => { + if let Err(e) = copy_bidirectional_compressed(pt_stream, tls).await { + eprintln!("Failed to copy data: {}", e); + } + } + Err(e) => { + eprintln!("Failed to connect to target: {}", e); + } + } + }); + } +} diff --git a/src/ops/postgres.rs b/src/ops/postgres.rs new file mode 100644 index 0000000..cf530e1 --- /dev/null +++ b/src/ops/postgres.rs @@ -0,0 +1,344 @@ +use clap::Parser; +use sqlx::{Connection, PgConnection}; + +const DEFAULT_URL_ENV: &str = "DATABASE_URL"; + +#[derive(Debug, Parser)] +pub struct SetupPostgresMasterCommand { + #[clap(long, help = "Postgres Connection String, defaults to DATABASE_URL")] + pub connection_string: Option, + + #[clap(subcommand)] + pub subcmd: SetupPostgresMasterSubCommand, +} + +#[derive(Debug, Parser)] +pub enum SetupPostgresMasterSubCommand { + #[clap(name = "setup")] + Setup(SetupPublicationCommand), + + #[clap(name = "teardown")] + Drop(DropPublicationCommand), + + #[clap(name = "add-table")] + AddTable(AddTableCommand), + + #[clap(name = "drop-table")] + DropTable(DropTableCommand), +} + +#[derive(Debug, Parser)] +pub struct SetupPublicationCommand { + #[clap(long, help = "Publication Name")] + pub publication: String, + + #[clap(long, help = "Whitelist mode, each table must be added manually")] + pub whitelist: bool, + + #[clap(long, help = "Publish Delete", default_value = "true")] + pub publish_delete: bool, + + #[clap(long, help = "Publish Truncate")] + pub publish_truncate: bool, + + #[clap(long, help = "Fail if publication already exists")] + pub must_not_exist: bool, +} + +#[derive(Debug, Parser)] +pub struct DropPublicationCommand { + #[clap(long, help = "Publication Name")] + pub publication: String, +} + +#[derive(Debug, Parser)] +pub struct DropTableCommand { + #[clap(long, help = "Publication Name")] + pub publication: String, + + #[clap(short, long, help = "Table Name")] + pub table: Vec, +} + +#[derive(Debug, Parser)] +pub struct AddTableCommand { + #[clap(long, help = "Publication Name")] + pub publication: String, + + #[clap(short, long, help = "Table Name")] + pub table: Vec, +} + +#[derive(Debug, Parser)] +pub struct SetupPostgresSlaveCommand { + #[clap(long, help = "Postgres Connection String, defaults to DATABASE_URL")] + pub connection_string: Option, + + #[clap(subcommand)] + pub subcmd: SetupPostgresSlaveSubCommand, +} + +#[derive(Debug, Parser)] +pub enum SetupPostgresSlaveSubCommand { + #[clap(name = "setup")] + Setup(SetupSubscriptionCommand), + + #[clap(name = "teardown")] + Drop(DropSubscriptionCommand), +} + +#[derive(Debug, Parser)] +pub struct SetupSubscriptionCommand { + #[clap(long, help = "Publication Name")] + pub publication: String, + + #[clap(long, help = "Subscription Name")] + pub subscription: String, + + #[clap(long, help = "Two phase transaction")] + pub two_phase: bool, +} + +#[derive(Debug, Parser)] +pub struct DropSubscriptionCommand { + #[clap(long, help = "Subscription Name")] + pub subscription: String, +} + +pub fn postgres_connection_string_from_env() -> Option { + std::env::var("DATABASE_URL").ok() +} + +fn safe_ident(name: &str) -> Option<&str> { + if name + .as_bytes() + .iter() + .enumerate() + .all(|(i, &c)| i > 0 && c.is_ascii_digit() || c.is_ascii_lowercase() || c == b'_') + { + Some(name) + } else { + None + } +} +#[derive(Debug, thiserror::Error)] +pub enum PostgresSetupError { + #[error("Missing Connection String, set via DATABASE_URL")] + MissingConnection, + + #[error("Entity already exists: {0}")] + AlreadyExists(String), + + #[error("Invalid Identifier: {0}")] + InvalidIdentifier(String), + + #[error("Postgres Error: {0}")] + PostgresError(#[from] sqlx::Error), +} + +pub async fn setup_postgres_pub( + connection_string: Option<&str>, + opts: SetupPublicationCommand, +) -> Result<(), PostgresSetupError> { + let mut postgres = PgConnection::connect( + &&connection_string + .map(|s| s.to_string()) + .or_else(postgres_connection_string_from_env) + .ok_or(PostgresSetupError::MissingConnection)?, + ) + .await?; + + let existing_pub = + sqlx::query_scalar::<_, i32>("SELECT 1 FROM pg_publication WHERE pubname = $1") + .bind(&opts.publication) + .fetch_optional(&mut postgres) + .await?; + + if existing_pub.is_some() { + return if opts.must_not_exist { + Err(PostgresSetupError::AlreadyExists( + "Publication already exists".to_string(), + )) + } else { + Ok(()) + }; + } + + let query = format!( + "CREATE PUBLICATION {}{}{}", + safe_ident(&opts.publication) + .ok_or_else(|| PostgresSetupError::InvalidIdentifier(opts.publication.clone()))?, + if opts.whitelist { + "" + } else { + " FOR TABLES IN SCHEMA public" + }, + match (opts.publish_delete, opts.publish_truncate) { + (true, true) => " WITH (publish = 'insert, update, delete, truncate')", + (true, false) => " WITH (publish = 'insert, update, delete')", + (false, true) => { + log::warn!("Publishing truncate without delete does not make sense!"); + " WITH (publish = 'insert, update, truncate')" + } + (false, false) => " WITH (publish = 'insert, update')", + } + ); + + log::info!("Executing: {}", query); + + sqlx::query(&query).execute(&mut postgres).await?; + + Ok(()) +} + +pub async fn drop_postgres_pub( + connection_string: Option<&str>, + opts: DropPublicationCommand, +) -> Result<(), PostgresSetupError> { + let mut postgres = PgConnection::connect( + &&connection_string + .map(|s| s.to_string()) + .or_else(postgres_connection_string_from_env) + .ok_or(PostgresSetupError::MissingConnection)?, + ) + .await?; + + let query = format!( + "DROP PUBLICATION {}", + safe_ident(&opts.publication).unwrap() + ); + + log::info!("Executing: {}", query); + + sqlx::query(&query).execute(&mut postgres).await?; + + Ok(()) +} + +pub async fn add_table_to_postgres_pub( + connection_string: Option<&str>, + opts: AddTableCommand, +) -> Result<(), PostgresSetupError> { + let mut postgres = PgConnection::connect( + &&connection_string + .map(|s| s.to_string()) + .or_else(postgres_connection_string_from_env) + .ok_or(PostgresSetupError::MissingConnection)?, + ) + .await?; + + for table in opts.table { + let query = format!( + "ALTER PUBLICATION {} ADD TABLE {}", + safe_ident(&opts.publication).unwrap(), + safe_ident(&table).unwrap() + ); + + log::info!("Executing: {}", query); + + sqlx::query(&query).execute(&mut postgres).await?; + } + + Ok(()) +} + +pub async fn drop_table_from_postgres_pub( + connection_string: Option<&str>, + opts: DropTableCommand, +) -> Result<(), PostgresSetupError> { + let mut postgres = PgConnection::connect( + &&connection_string + .map(|s| s.to_string()) + .or_else(postgres_connection_string_from_env) + .ok_or(PostgresSetupError::MissingConnection)?, + ) + .await?; + + for table in opts.table { + let query = format!( + "ALTER PUBLICATION {} DROP TABLE {}", + safe_ident(&opts.publication).unwrap(), + safe_ident(&table).unwrap() + ); + + log::info!("Executing: {}", query); + + sqlx::query(&query).execute(&mut postgres).await?; + } + + Ok(()) +} + +pub async fn setup_postgres_sub( + connection_string: Option<&str>, + opts: SetupSubscriptionCommand, +) -> Result<(), PostgresSetupError> { + let mut postgres = PgConnection::connect( + &&connection_string + .map(|s| s.to_string()) + .or_else(postgres_connection_string_from_env) + .ok_or(PostgresSetupError::MissingConnection)?, + ) + .await?; + + let existing_sub = + sqlx::query_scalar::<_, i32>("SELECT 1 FROM pg_subscription WHERE subname = $1") + .bind(&opts.subscription) + .fetch_optional(&mut postgres) + .await?; + + if existing_sub.is_some() { + return Err(PostgresSetupError::AlreadyExists( + "Subscription already exists".to_string(), + )); + } + + let query = format!( + "CREATE SUBSCRIPTION {} CONNECTION $1 PUBLICATION {}{}", + safe_ident(&opts.subscription) + .ok_or_else(|| PostgresSetupError::InvalidIdentifier(opts.subscription.clone()))?, + safe_ident(&opts.publication).unwrap(), + if opts.two_phase { + " WITH (two_phase = on)" + } else { + "" + } + ); + + log::info!( + "Executing: {} with $1 = {}", + query, + connection_string.unwrap_or(DEFAULT_URL_ENV) + ); + + sqlx::query(&query) + .bind(connection_string.unwrap_or(DEFAULT_URL_ENV)) + .execute(&mut postgres) + .await?; + + Ok(()) +} + +pub async fn drop_postgres_sub( + connection_string: Option<&str>, + opts: DropSubscriptionCommand, +) -> Result<(), PostgresSetupError> { + let mut postgres = PgConnection::connect( + &&connection_string + .map(|s| s.to_string()) + .or_else(postgres_connection_string_from_env) + .ok_or(PostgresSetupError::MissingConnection)?, + ) + .await?; + + let query = format!( + "DROP SUBSCRIPTION {}", + safe_ident(&opts.subscription).unwrap() + ); + + log::info!("Executing: {}", query); + + sqlx::query(&query).execute(&mut postgres).await?; + + Ok(()) +} diff --git a/src/ops/service.rs b/src/ops/service.rs new file mode 100644 index 0000000..eae09c1 --- /dev/null +++ b/src/ops/service.rs @@ -0,0 +1,135 @@ +use std::path::Path; + +use clap::Parser; +use serde::Deserialize; + +use super::network::ReverseProxyCommand; + +const DEF_CONFIG_FILE: &str = "/etc/replikey.toml"; +const CA_CERT: &str = "ca.pem"; +const SERVER_CERT: &str = "server.pem"; +const SERVER_KEY: &str = "server.key"; +const CLIENT_CERT: &str = "client.pem"; +const CLIENT_KEY: &str = "client.key"; + +#[derive(Debug, Parser)] +pub struct ServiceCommand { + #[clap(subcommand)] + pub subcmd: ServiceSubCommand, +} + +#[derive(Debug, Parser)] +pub enum ServiceSubCommand { + #[clap(name = "replicate-master")] + ReplicateMaster { + #[clap(short, long, default_value = DEF_CONFIG_FILE)] + config: String, + }, + + #[clap(name = "replicate-slave")] + ReplicateSlave { + #[clap(short, long, default_value = DEF_CONFIG_FILE)] + config: String, + }, +} + +#[derive(Debug, Deserialize)] +pub struct Config { + connection: ConnectionConfig, +} + +#[derive(Debug, Deserialize)] +pub struct ConnectionConfig { + master: Option, + slave: Option, +} + +#[derive(Debug, Deserialize)] +pub struct MasterConfig { + listen: String, + redis_sni: String, + redis_target: String, + postgres_sni: String, + postgres_target: String, + workdir: Option, + crl: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct SlaveConfig { + listen: String, + redis_sni: String, + postgres_sni: String, + workdir: Option, + crl: Vec, +} + +pub fn service_replicate_master(config: String) { + let config = std::fs::read_to_string(config).unwrap(); + let config: Config = toml::from_str(&config).expect("Failed to parse config"); + + if let Some(wd) = config + .connection + .master + .as_ref() + .and_then(|m| m.workdir.as_ref()) + { + std::env::set_current_dir(wd).expect("Failed to change directory"); + } + + let master_conf = config.connection.master.as_ref().unwrap(); + let cmd = ReverseProxyCommand { + listen: master_conf.listen.clone(), + redis_sni: master_conf.redis_sni.clone(), + redis_target: master_conf.redis_target.clone(), + postgres_sni: master_conf.postgres_sni.clone(), + postgres_target: master_conf.postgres_target.clone(), + + cert: Path::new(SERVER_CERT).to_string_lossy().to_string(), + key: Path::new(SERVER_KEY).to_string_lossy().to_string(), + ca: Path::new(CA_CERT).to_string_lossy().to_string(), + crl: config.connection.master.as_ref().unwrap().crl.clone(), + }; + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(crate::ops::network::reverse_proxy(cmd)) + .unwrap(); + + println!("Replication master started"); +} + +pub fn service_replicate_slave(config: String) { + let config = std::fs::read_to_string(config).unwrap(); + let config: Config = toml::from_str(&config).expect("Failed to parse config"); + + if let Some(wd) = config + .connection + .slave + .as_ref() + .and_then(|m| m.workdir.as_ref()) + { + std::env::set_current_dir(wd).expect("Failed to change directory"); + } + + let slave_conf = config.connection.slave.as_ref().unwrap(); + let cmd = ReverseProxyCommand { + listen: slave_conf.listen.clone(), + redis_sni: slave_conf.redis_sni.clone(), + redis_target: slave_conf.redis_sni.clone(), + postgres_sni: slave_conf.postgres_sni.clone(), + postgres_target: slave_conf.postgres_sni.clone(), + + cert: Path::new(CLIENT_CERT).to_string_lossy().to_string(), + key: Path::new(CLIENT_KEY).to_string_lossy().to_string(), + ca: Path::new(CA_CERT).to_string_lossy().to_string(), + crl: config.connection.slave.as_ref().unwrap().crl.clone(), + }; + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(crate::ops::network::reverse_proxy(cmd)) + .unwrap(); + + println!("Replication slave started"); +}