commit 4e72c52ca0887e98ddc5c57dcf8384462f8fdcee Author: Karolin Varner Date: Thu Feb 23 20:12:52 2023 +0100 add Rosenpass, the tool Initial implementation of the Rosenpass tool, implemented by @koraa. Includes contributions and some lints from @wucke13. Co-authored-by: wucke13 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ee0272 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Rust +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..27949c5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1449 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +dependencies = [ + "backtrace", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bindgen" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "clap 3.2.23", + "env_logger 0.9.3", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "build-deps" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f14468960818ce4f3e3553c32d524446687884f8e7af5d3e252331d8a87e43" +dependencies = [ + "glob", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[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 = "clang-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "bitflags", + "textwrap 0.11.0", + "unicode-width", +] + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "atty", + "bitflags", + "clap_lex", + "indexmap", + "strsim", + "termcolor", + "textwrap 0.16.0", + "yaml-rust", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "cmake" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +dependencies = [ + "cc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" +dependencies = [ + "atty", + "cast", + "clap 2.34.0", + "criterion-plot", + "csv", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.7.1", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa 0.4.8", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "filetime" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.42.0", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "gimli" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[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.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libflate" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05605ab2bce11bcfc0e9c635ff29ef8b2ea83f29be257ee7d730cac3ee373093" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a734c0493409afcd49deee13c006a04e3586b9761a03543c6272c9c51f2f5a" +dependencies = [ + "rle-decode-fast", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libsodium-sys-stable" +version = "1.19.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2e36a6759ec7f4d772d2e01af0bf5ba63eb114bbab488cbcf53884c6408bb9" +dependencies = [ + "cc", + "libc", + "libflate", + "minisign-verify", + "pkg-config", + "tar", + "ureq", + "vcpkg", + "zip", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "minisign-verify" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "nom" +version = "7.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "object" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d864c91689fdc196779b98dba0aceac6118594c2df6ee5d943eb6a8df4d107a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "oqs-sys" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3929aaf333acdb26bfade2acc25bdfc927a013c87616509be67e0462a3c165b" +dependencies = [ + "bindgen", + "build-deps", + "cmake", + "libc", +] + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "paste" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "plotters" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" + +[[package]] +name = "plotters-svg" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rosenpass" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "clap 3.2.23", + "criterion", + "env_logger 0.10.0", + "lazy_static", + "libsodium-sys-stable", + "log", + "memoffset 0.6.5", + "oqs-sys", + "paste", + "static_assertions", + "test_bin", + "thiserror", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa 1.0.5", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test_bin" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7a7de15468c6e65dd7db81cf3822c1ec94c71b2a3c1a976ea8e4696c91115c" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "unicode-bidi" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +dependencies = [ + "base64", + "log", + "once_cell", + "rustls", + "url", + "webpki", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "which" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zip" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2b5e5e9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "rosenpass" +version = "0.1.0" +authors = ["Karolin Varner "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[[bench]] +name = "handshake" +harness = false + +[dependencies] +anyhow = { version = "1.0.52", features = ["backtrace"] } +base64 = "0.13.0" +clap = { version = "3.0.0", features = ["yaml"] } +static_assertions = "1.1.0" +memoffset = "0.6.5" +libsodium-sys-stable = { version = "1.19.26", features = ["use-pkg-config"] } +oqs-sys = { version = "0.7.1", default-features = false, features = ['classic_mceliece', 'kyber'] } +lazy_static = "1.4.0" +thiserror = "1.0.38" +paste = "1.0.11" +log = { version = "0.4.17", optional = true } +env_logger = { version = "0.10.0", optional = true } + +[dev-dependencies] +criterion = "0.3.5" +test_bin = "0.4.0" + +[features] +default = ["log", "env_logger"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + 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 diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/benches/handshake.rs b/benches/handshake.rs new file mode 100644 index 0000000..72d44d1 --- /dev/null +++ b/benches/handshake.rs @@ -0,0 +1,82 @@ +use anyhow::Result; +use rosenpass::{ + pqkem::{CCAKEM, KEM}, + protocol::{CcaPk, CcaSk, HandleMsgResult, MsgBuf, PeerPtr, Server, SymKey}, + sodium::sodium_init, +}; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +fn handle( + tx: &mut Server, + msgb: &mut MsgBuf, + msgl: usize, + rx: &mut Server, + resb: &mut MsgBuf, +) -> Result<(Option, Option)> { + let HandleMsgResult { + exchanged_with: xch, + resp, + } = rx.handle_msg(&msgb[..msgl], &mut **resb)?; + assert!(matches!(xch, None | Some(PeerPtr(0)))); + + let xch = xch.map(|p| rx.osk(p).unwrap()); + let (rxk, txk) = resp + .map(|resl| handle(rx, resb, resl, tx, msgb)) + .transpose()? + .unwrap_or((None, None)); + + assert!(rxk.is_none() || xch.is_none()); + Ok((txk, rxk.or(xch))) +} + +fn hs(ini: &mut Server, res: &mut Server) -> Result<()> { + let (mut inib, mut resb) = (MsgBuf::zero(), MsgBuf::zero()); + let sz = ini.initiate_handshake(PeerPtr(0), &mut *inib)?; + let (kini, kres) = handle(ini, &mut inib, sz, res, &mut resb)?; + assert!(kini.unwrap().secret() == kres.unwrap().secret()); + Ok(()) +} + +fn keygen() -> Result<(CcaSk, CcaPk)> { + let (mut sk, mut pk) = (CcaSk::zero(), CcaPk::zero()); + CCAKEM::keygen(sk.secret_mut(), pk.secret_mut())?; + Ok((sk, pk)) +} + +fn make_server_pair() -> Result<(Server, Server)> { + let psk = SymKey::random(); + let ((ska, pka), (skb, pkb)) = (keygen()?, keygen()?); + let (mut a, mut b) = (Server::new(ska, pka.clone()), Server::new(skb, pkb.clone())); + a.add_peer(Some(psk.clone()), pkb)?; + b.add_peer(Some(psk), pka)?; + Ok((a, b)) +} + +fn criterion_benchmark(c: &mut Criterion) { + sodium_init().unwrap(); + let (mut a, mut b) = make_server_pair().unwrap(); + c.bench_function("cca_secret_alloc", |bench| { + bench.iter(|| { + CcaSk::zero(); + }) + }); + c.bench_function("cca_public_alloc", |bench| { + bench.iter(|| { + CcaPk::zero(); + }) + }); + c.bench_function("keygen", |bench| { + bench.iter(|| { + keygen().unwrap(); + }) + }); + c.bench_function("handshake", |bench| { + bench.iter(|| { + hs(black_box(&mut a), black_box(&mut b)).unwrap(); + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rp b/rp new file mode 100755 index 0000000..5c34ecf --- /dev/null +++ b/rp @@ -0,0 +1,333 @@ +#!/usr/bin/env bash + +set -e + +# String formatting subsystem + +formatting_init() { + endl=$'\n' +} + +enquote() { + while (( $# > 1 )); do + printf "%q " "${1}"; shift + done + if (( $# == 1 )); then + printf "%q" "${1}"; shift + fi +} + +multiline() { + # shellcheck disable=SC1004 + echo "${1} " | awk ' + function pm(a, b, l) { + return length(a) > l \ + && length(b) > l \ + && substr(a, 1, l+1) == substr(b, 1, l+1) \ + ? pm(a, b, l+1) : l; + } + + !started && $0 !~ /^[ \t]*$/ { + started=1 + match($0, /^[ \t]*/) + prefix=substr($0, 1, RLENGTH) + } + + started { + print(substr($0, 1 + pm($0, prefix))); + } + ' +} + +dbg() { + echo >&2 "$@" +} + +# Cleanup subsystem (sigterm) + +cleanup_init() { + cleanup_actions=() + trap cleanup_apply exit +} + +cleanup_apply() { + local f + for f in "${cleanup_actions[@]}"; do + eval "${f}" + done +} + +cleanup() { + cleanup_actions+=("$(multiline "${1}")") +} + +# Transactional execution subsystem + +frag_init() { + explain=0 + frag_transaction=() + frag " + #! /bin/bash + set -e" +} + +frag_apply() { + local f + for f in "${frag_transaction[@]}"; do + if (( explain == 1 )); then + dbg "${f}" + fi + eval "${f}" + done +} + +frag() { + frag_transaction+=("$(multiline "${1}")") +} + +frag_append() { + local len; len="${#frag_transaction[@]}" + frag_transaction=("${frag_transaction[@]:0:len-1}" "${frag_transaction[len-1]}${1}") +} + +frag_append_esc() { + frag_append " \\${endl}${1}" +} + +# Usage documentation subsystem +usage_init() { + usagestack=("${script}") +} + +usage_snap() { + echo "${#usagestack}" +} + +usage_restore() { + local n; n="${1}" + dbg REST "${1}" + usagestack=("${usagestack[@]:0:n-2}") +} + + +usage() { + dbg "Usage: ${usagestack[*]}" +} + +fatal() { + dbg "FATAL: $*" + usage + exit 1 +} + +genkey() { + usagestack+=("PRIVATE_KEYS_DIR") + local skdir + skdir="${1/\//}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR" + + while (( $# > 0 )); do + local arg; arg="$1"; shift + case "${arg}" in + -h | -help | --help | help) usage; return 0 ;; + *) fatal "Unknown option ${arg}";; + esac + done + + if test -e "${skdir}"; then + fatal "PRIVATE_KEYS_DIR \"${skdir}\" already exists" + fi + + frag " + umask 077 + mkdir -p $(enquote "${skdir}") + wg genkey > $(enquote "${skdir}"/wgsk) + $(enquote "${binary}") keygen \\ + private-key $(enquote "${skdir}"/pqsk) \\ + public-key $(enquote "${skdir}"/pqpk)" +} + +pubkey() { + usagestack+=("PRIVATE_KEYS_DIR" "PUBLIC_KEYS_DIR") + local skdir pkdir + skdir="${1/\//}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR" + pkdir="${1/\//}"; shift || fatal "Required positional argument: PUBLIC_KEYS_DIR" + + while (( $# > 0 )); do + local arg; arg="$1"; shift + case "${arg}" in + -h | -help | --help | help) usage; exit 0;; + *) fatal "Unknown option ${arg}";; + esac + done + + if test -e "${pkdir}"; then + fatal "PUBLIC_KEYS_DIR \"${pkdir}\" already exists" + fi + + frag " + mkdir -p $(enquote "${pkdir}") + wg pubkey < $(enquote "${skdir}"/wgsk) > $(enquote "${pkdir}/wgpk") + cp $(enquote "${skdir}"/pqpk) $(enquote "${pkdir}/pqpk")" +} + +exchange() { + usagestack+=("PRIVATE_KEYS_DIR" "[dev ]" "[listen :]" "[peer PUBLIC_KEYS_DIR [endpoint :] [persistent-keepalive ] [allowed-ips /[,/]...]]...") + local skdir dev lport + dev="${project_name}0" + skdir="${1/\//}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR" + + while (( $# > 0 )); do + local arg; arg="$1"; shift + case "${arg}" in + dev) dev="${1}"; shift || fatal "dev option requires parameter";; + peer) set -- "peer" "$@"; break;; # Parsed down below + listen) + local listen; listen="${1}"; + lip="${listen%:*}"; + lport="${listen/*:/}"; + if [[ "$lip" = "$lport" ]]; then + lip="[0::0]" + fi + shift;; + -h | -help | --help | help) usage; return 0;; + *) fatal "Unknown option ${arg}";; + esac + done + + if (( $# == 0 )); then + fatal "Needs at least one peer specified" + fi + + frag " + # Create the Wireguard interface + ip link add dev $(enquote "${dev}") type wireguard || true" + + cleanup " + ip link del dev $(enquote "${dev}") || true" + + frag " + ip link set dev $(enquote "${dev}") up" + + frag " + # Deploy the classic wireguard private key + wg set $(enquote "${dev}") private-key $(enquote "${skdir}/wgsk")" + + + if test -n "${lport}"; then + frag_append "listen-port $(enquote "$(( lport + 1 ))")" + fi + + frag " + # Launch the post quantum wireguard exchange daemon + $(enquote "${binary}") exchange" + + if (( verbose == 1 )); then + frag_append "verbose" + fi + + frag_append_esc " private-key $(enquote "${skdir}/pqsk")" + frag_append_esc " public-key $(enquote "${skdir}/pqpk")" + + if test -n "${lport}"; then + frag_append_esc " listen $(enquote "${lip}:${lport}")" + fi + + usagestack+=("peer" "PUBLIC_KEYS_DIR endpoint IP:PORT") + + while (( $# > 0 )); do + shift; # Skip "peer" argument + + local peerdir ip port keepalive allowedips + peerdir="${1/\//}"; shift || fatal "Required peer argument: PUBLIC_KEYS_DIR" + + while (( $# > 0 )); do + local arg; arg="$1"; shift + case "${arg}" in + peer) set -- "peer" "$@"; break;; # Next peer + endpoint) ip="${1%:*}"; port="${1/*:/}"; shift;; + persistent-keepalive) keepalive="${1}"; shift;; + allowed-ips) allowedips="${1}"; shift;; + -h | -help | --help | help) usage; return 0;; + *) fatal "Unknown option ${arg}";; + esac + done + + # Public key + frag_append_esc " peer public-key $(enquote "${peerdir}/pqpk")" + + # PSK + local pskfile; pskfile="${peerdir}/psk" + if test -f "${pskfile}"; then + frag_append_esc " preshared-key $(enquote "${pskfile}")" + fi + + + if test -n "${ip}"; then + frag_append_esc " endpoint $(enquote "${ip}:${port}")" + fi + + frag_append_esc " wireguard $(enquote "${dev}") $(enquote "$(cat "${peerdir}/wgpk")")" + + if test -n "${ip}"; then + frag_append_esc " endpoint $(enquote "${ip}:$(( port + 1 ))")" + fi + + if test -n "${keepalive}"; then + frag_append_esc " persistent-keepalive $(enquote "${keepalive}")" + fi + + if test -n "${allowedips}"; then + frag_append_esc " allowed-ips $(enquote "${allowedips}")" + fi + done +} + +main() { + formatting_init + cleanup_init + usage_init + frag_init + + project_name="rosenpass" + scriptdir="$(dirname "${script}")" + verbose=0 + binary="$( + find "${scriptdir}"/target/{release,debug}/"${project_name}" -printf "%T@ %p\n" 2>/dev/null \ + | sort -nr \ + | awk -v fallback="${project_name}" ' + NR == 1 { print($2) } + END { if (NR == 0) print(fallback) }' + )" + + # Parse command + + usagestack+=("[explain]" "[verbose]" "genkey|pubkey|exchange" "[ARGS]...") + + local cmd + while (( $# > 0 )); do + local arg; arg="$1"; shift + case "${arg}" in + genkey|pubkey|exchange) cmd="${arg}"; break;; + explain) explain=1;; + verbose) verbose=1;; + -h | -help | --help | help) usage; return 0 ;; + *) fatal "Unknown command ${arg}";; + esac + done + + test -n "${cmd}" || fatal "No command supplied" + usagestack=("${script}") + + # Execute command + + usagestack+=("${cmd}") + "${cmd}" "$@" + usagestack=("${script}") + + # Apply transaction + + frag_apply +} + +script="$0" +main "$@" diff --git a/src/coloring.rs b/src/coloring.rs new file mode 100644 index 0000000..9f591c5 --- /dev/null +++ b/src/coloring.rs @@ -0,0 +1,358 @@ +//! This module contains various types for dealing with secrets +//! +//! These types use type level coloring to make accidential leackage of secrets extra hard. +//! + +use crate::{ + sodium::{rng, zeroize}, + util::{cpy, mutating}, +}; +use lazy_static::lazy_static; +use libsodium_sys as libsodium; +use std::{ + collections::HashMap, + convert::TryInto, + fmt, + ops::{Deref, DerefMut}, + os::raw::c_void, + ptr::null_mut, + sync::Mutex, +}; + +// This might become a problem in library usage; it's effectively a memory +// leak which probably isn't a problem right now because most memory will +// be reused… +lazy_static! { + static ref SECRET_CACHE: Mutex = Mutex::new(SecretMemoryPool::new()); +} + +/// Pool that stores secret memory allocations +/// +/// Allocation of secret memory is expensive. Thus, this struct provides a +/// pool of secret memory, readily available to yield protected, slices of +/// memory. +/// +/// Further information about the protection in place can be found in in the +/// [libsodium documentation](https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations) +#[derive(Debug)] // TODO check on Debug derive, is that clever +pub struct SecretMemoryPool { + pool: HashMap>, +} + +impl SecretMemoryPool { + /// Create a new [SecretMemoryPool] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let pool = HashMap::new(); + + Self { pool } + } + + /// Return secrete back to the pool for future re-use + /// + /// This consumes the [Secret], but its memory is re-used. + pub fn release(&mut self, mut s: Secret) { + unsafe { + self.release_by_ref(&mut s); + } + std::mem::forget(s); + } + + /// Return secret back to the pool for future re-use, by slice + /// + /// # Safety + /// + /// After calling this function on a [Secret], the secret must never be + /// used again for anything. + unsafe fn release_by_ref(&mut self, s: &mut Secret) { + s.zeroize(); + let Secret { ptr: secret } = s; + // don't call Secret::drop, that could cause a double free + self.pool.entry(N).or_default().push(*secret); + } + + /// Take protected memory from the pool, allocating new one if no suitable + /// chunk is found in the inventory. + /// + /// The secret is guaranteed to be full of nullbytes + /// + /// # Safety + /// + /// This function contains an unsafe call to [libsodium::sodium_malloc]. + /// This call has no known safety invariants, thus nothing can go wrong™. + /// However, just like normal `malloc()` this can return a null ptr. Thus + /// the returned pointer is checked for null; causing the program to panic + /// if it is null. + pub fn take(&mut self) -> Secret { + let entry = self.pool.entry(N).or_default(); + let secret = entry.pop().unwrap_or_else(|| { + let ptr = unsafe { libsodium::sodium_malloc(N) }; + assert!( + !ptr.is_null(), + "libsodium::sodium_mallloc() returned a null ptr" + ); + ptr + }); + + let mut s = Secret { ptr: secret }; + s.zeroize(); + s + } +} + +impl Drop for SecretMemoryPool { + /// # Safety + /// + /// The drop implementation frees the contained elements using + /// [libsodium::sodium_free]. This is safe as long as every `*mut c_void` + /// contained was initialized with a call to [libsodium::sodium_malloc] + fn drop(&mut self) { + for ptr in self.pool.drain().flat_map(|(_, x)| x.into_iter()) { + unsafe { + libsodium::sodium_free(ptr); + } + } + } +} + +/// # Safety +/// +/// No safety implications are known, since the `*mut c_void` in +/// is essentially used like a `&mut u8` [SecretMemoryPool]. +unsafe impl Send for SecretMemoryPool {} + +/// Store for a secret +/// +/// Uses memory allocated with [libsodium::sodium_malloc], +/// esentially can do the same things as `[u8; N].as_mut_ptr()`. +pub struct Secret { + ptr: *mut c_void, +} + +impl Clone for Secret { + fn clone(&self) -> Self { + let mut new = Self::zero(); + new.secret_mut().clone_from_slice(self.secret()); + new + } +} + +impl Drop for Secret { + fn drop(&mut self) { + self.zeroize(); + // the invariant that the [Secret] is not used after the + // `release_by_ref` call is guaranteed, since this is a drop implementation + unsafe { SECRET_CACHE.lock().unwrap().release_by_ref(self) }; + self.ptr = null_mut(); + } +} + +impl Secret { + pub fn from_slice(slice: &[u8]) -> Self { + let mut new_self = Self::zero(); + new_self.secret_mut().copy_from_slice(slice); + new_self + } + + /// Returns a new [Secret] that is zero initialized + pub fn zero() -> Self { + // Using [SecretMemoryPool] here because this operation is expensive, + // yet it is used in hot loops + let s = SECRET_CACHE.lock().unwrap().take(); + assert_eq!(s.secret(), &[0u8; N]); + s + } + + /// Returns a new [Secret] that is randomized + pub fn random() -> Self { + mutating(Self::zero(), |r| r.randomize()) + } + + /// Sets all data of an existing secret to null bytes + pub fn zeroize(&mut self) { + zeroize(self.secret_mut()); + } + + /// Sets all data an existing secret to random bytes + pub fn randomize(&mut self) { + rng(self.secret_mut()); + } + + /// Borrows the data + pub fn secret(&self) -> &[u8; N] { + // - calling `from_raw_parts` is safe, because `ptr` is initalized with + // as `N` byte allocation from the creation of `Secret` onwards. `ptr` + // stays valid over the full lifetime of `Secret` + // + // - calling uwnrap is safe, because we can guarantee that the slice has + // exactly the required size `N` to create an array of `N` elements. + let ptr = self.ptr as *const u8; + let slice = unsafe { std::slice::from_raw_parts(ptr, N) }; + slice.try_into().unwrap() + } + + /// Borrows the data mutably + pub fn secret_mut(&mut self) -> &mut [u8; N] { + // the same safety argument as for `secret()` holds + let ptr = self.ptr as *mut u8; + let slice = unsafe { std::slice::from_raw_parts_mut(ptr, N) }; + slice.try_into().unwrap() + } +} + +/// The Debug implementation of [Secret] does not reveal the secret data, +/// instead a placeholder `` is used +impl fmt::Debug for Secret { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str("") + } +} + +/// Contains information in the form of a byte array that may be known to the +/// public +// TODO: We should get rid of the Public type; just use a normal value +#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct Public { + pub value: [u8; N], +} + +impl Public { + /// Create a new [Public] from a byte slice + pub fn from_slice(value: &[u8]) -> Self { + mutating(Self::zero(), |r| cpy(value, &mut r.value)) + } + + /// Create a new [Public] from a byte array + pub fn new(value: [u8; N]) -> Self { + Self { value } + } + + /// Create a zero initialized [Public] + pub fn zero() -> Self { + Self { value: [0u8; N] } + } + + /// Create a random initialized [Public] + pub fn random() -> Self { + mutating(Self::zero(), |r| r.randomize()) + } + + /// Randomize all bytes in an existing [Public] + pub fn randomize(&mut self) { + rng(&mut self.value); + } +} + +/// Writes the contents of an `&[u8]` as hexadecimal symbols to a [std::fmt::Formatter] +pub fn debug_crypto_array(v: &[u8], fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str("[{}]=")?; + if v.len() > 64 { + for byte in &v[..32] { + std::fmt::LowerHex::fmt(byte, fmt)?; + } + fmt.write_str("…")?; + for byte in &v[v.len() - 32..] { + std::fmt::LowerHex::fmt(byte, fmt)?; + } + } else { + for byte in v { + std::fmt::LowerHex::fmt(byte, fmt)?; + } + } + Ok(()) +} + +impl fmt::Debug for Public { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + debug_crypto_array(&self.value, fmt) + } +} + +impl Deref for Public { + type Target = [u8; N]; + + fn deref(&self) -> &[u8; N] { + &self.value + } +} + +impl DerefMut for Public { + fn deref_mut(&mut self) -> &mut [u8; N] { + &mut self.value + } +} + +#[cfg(test)] +mod test { + use super::*; + + /// https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations + /// promises us that allocated memory is initialized with this magic byte + const SODIUM_MAGIC_BYTE: u8 = 0xdb; + + /// must be called before any interaction with libsodium + fn init() { + unsafe { libsodium_sys::sodium_init() }; + } + + /// checks that whe can malloc with libsodium + #[test] + fn sodium_malloc() { + init(); + const N: usize = 8; + let ptr = unsafe { libsodium_sys::sodium_malloc(N) }; + let mem = unsafe { std::slice::from_raw_parts(ptr as *mut u8, N) }; + assert_eq!(mem, &[SODIUM_MAGIC_BYTE; N]) + } + + /// checks that whe can free with libsodium + #[test] + fn sodium_free() { + init(); + const N: usize = 8; + let ptr = unsafe { libsodium_sys::sodium_malloc(N) }; + unsafe { libsodium_sys::sodium_free(ptr) } + } + + /// check that we can alloc using the magic pool + #[test] + fn secret_memory_pool_take() { + init(); + const N: usize = 0x100; + let mut pool = SecretMemoryPool::new(); + let secret: Secret = pool.take(); + assert_eq!(secret.secret(), &[0; N]); + } + + /// check that a secrete lives, even if its [SecretMemoryPool] is deleted + #[test] + fn secret_memory_pool_drop() { + init(); + const N: usize = 0x100; + let mut pool = SecretMemoryPool::new(); + let secret: Secret = pool.take(); + std::mem::drop(pool); + assert_eq!(secret.secret(), &[0; N]); + } + + /// check that a secrete can be reborn, freshly initialized with zero + #[test] + fn secret_memory_pool_release() { + init(); + const N: usize = 1; + let mut pool = SecretMemoryPool::new(); + let mut secret: Secret = pool.take(); + let old_secret_ptr = secret.ptr; + + secret.secret_mut()[0] = 0x13; + pool.release(secret); + + // now check that we get the same ptr + let new_secret: Secret = pool.take(); + assert_eq!(old_secret_ptr, new_secret.ptr); + + // and that the secret was zeroized + assert_eq!(new_secret.secret(), &[0; N]); + } +} diff --git a/src/labeled_prf.rs b/src/labeled_prf.rs new file mode 100644 index 0000000..c9f9248 --- /dev/null +++ b/src/labeled_prf.rs @@ -0,0 +1,45 @@ +use { + crate::{prftree::PrfTree, sodium::KEY_SIZE}, + anyhow::Result, +}; + +pub fn protocol() -> Result { + PrfTree::zero().mix("Rosenpass v1 mceliece460896 Kyber512 ChaChaPoly1305 BLAKE2s".as_bytes()) +} + +// TODO Use labels that can serve as idents +macro_rules! prflabel { + ($base:ident, $name:ident, $($lbl:expr),* ) => { + pub fn $name() -> Result { + let t = $base()?; + $( let t = t.mix($lbl.as_bytes())?; )* + Ok(t) + } + } +} + +prflabel!(protocol, mac, "mac"); +prflabel!(protocol, cookie, "cookie"); +prflabel!(protocol, peerid, "peer id"); +prflabel!(protocol, biscuit_ad, "biscuit additional data"); +prflabel!(protocol, ckinit, "chaining key init"); +prflabel!(protocol, _ckextract, "chaining key extract"); + +macro_rules! prflabel_leaf { + ($base:ident, $name:ident, $($lbl:expr),* ) => { + pub fn $name() -> Result<[u8; KEY_SIZE]> { + let t = $base()?; + $( let t = t.mix($lbl.as_bytes())?; )* + Ok(t.into_value()) + } + } +} + +prflabel_leaf!(_ckextract, mix, "mix"); +prflabel_leaf!(_ckextract, hs_enc, "handshake encryption"); +prflabel_leaf!(_ckextract, ini_enc, "initiator handshake encryption"); +prflabel_leaf!(_ckextract, res_enc, "responder handshake encryption"); + +prflabel!(_ckextract, _user, "user"); +prflabel!(_user, _rp, "rosenpass.eu"); +prflabel_leaf!(_rp, osk, "wireguard psk"); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3533253 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,56 @@ +#[macro_use] +pub mod util; +#[macro_use] +pub mod sodium; +pub mod coloring; +pub mod labeled_prf; +pub mod msgs; +pub mod pqkem; +pub mod prftree; +pub mod protocol; + +#[derive(thiserror::Error, Debug)] +pub enum RosenpassError { + #[error("error in OQS")] + Oqs, + #[error("error from external library while calling OQS")] + OqsExternalLib, + #[error("buffer size mismatch, required {required_size} but only found {actual_size}")] + BufferSizeMismatch { + required_size: usize, + actual_size: usize, + }, + #[error("invalid message type")] + InvalidMessageType(u8), +} + +impl RosenpassError { + /// Helper function to check a buffer size + fn check_buffer_size(required_size: usize, actual_size: usize) -> Result<(), Self> { + if required_size != actual_size { + Err(Self::BufferSizeMismatch { + required_size, + actual_size, + }) + } else { + Ok(()) + } + } +} + +/// Extension trait to attach function calls to foreign types. +trait RosenpassMaybeError { + /// Checks whether something is an error or not + fn to_rg_error(&self) -> Result<(), RosenpassError>; +} + +impl RosenpassMaybeError for oqs_sys::common::OQS_STATUS { + fn to_rg_error(&self) -> Result<(), RosenpassError> { + use oqs_sys::common::OQS_STATUS; + match self { + OQS_STATUS::OQS_SUCCESS => Ok(()), + OQS_STATUS::OQS_ERROR => Err(RosenpassError::Oqs), + OQS_STATUS::OQS_EXTERNAL_LIB_ERROR_OPENSSL => Err(RosenpassError::OqsExternalLib), + } + } +} diff --git a/src/lprf.rs b/src/lprf.rs new file mode 100644 index 0000000..5f14bad --- /dev/null +++ b/src/lprf.rs @@ -0,0 +1,106 @@ +//! The rosenpass protocol relies on a special type +//! of hash function for most of its hashing or +//! message authentication needs: an incrementable +//! pseudo random function. +//! +//! This is a generalization of a PRF operating +//! on a sequence of inputs instead of a single input. +//! +//! Like a Dec function the Iprf features efficient +//! incrementability. +//! +//! You can also think of an Iprf as a Dec function with +//! a fixed size output. +//! +//! The idea behind a Iprf is that it can be efficiently +//! constructed from an Dec function as well as a PRF. +//! +//! TODO Base the construction on a proper Dec function + +pub struct Iprf([u8; KEY_SIZE]); +pub struct IprfBranch([u8; KEY_SIZE]); +pub struct SecretIprf(Secret); +pub struct SecretIprfBranch(Secret); + +pub fn prf_into(out: &mut [u8], key: &[u8], data: &[u8]) { + // TODO: The error handling with sodium is a scurge + hmac_into(out, key, data).unwrap() +} + +pub fn prf(key: &[u8], data: &[u8]) -> [u8; KEY_SIZE]{ + mutating([0u8; KEY_SIZE], |r| prf_into(r, key, data)) +} + +impl Iprf { + fn zero() -> Self { + Self([0u8; KEY_SIZE]) + } + + fn dup(self) -> IprfBranch { + IprfBranch(self.0) + } + + // TODO: Protocol! Use domain separation to ensure that + fn mix(self, v: &[u8]) -> Self { + Self(prf(&self.0, v)) + } + + fn mix_secret(self, v: Secret) -> SecretIprf { + SecretIprf::prf_invoc(&self.0, v.secret()) + } + + fn into_value(self) -> [u8; KEY_SIZE] { + self.0 + } + + fn extract(self, v: &[u8], dst: &mut [u8]) { + prf_into(&self.0, v, dst) + } +} + +impl IprfBranch { + fn mix(&self, v: &[u8]) -> Iprf { + Iprf(prf(self.0, v)) + } + + fn mix_secret(&self, v: Secret) -> SecretIprf { + SecretIprf::prf_incov(self.0, v.secret()) + } +} + +impl SecretIprf { + fn prf_invoc(k: &[u8], d: &[u8]) -> SecretIprf { + mutating(SecretIprf(Secret::zero()), |r| + prf_into(k, d, r.secret_mut())) + } + + fn from_key(k: Secret) -> SecretIprf { + Self(k) + } + + fn mix(self, v: &[u8]) -> SecretIprf { + Self::prf_invoc(self.0.secret(), v) + } + + fn mix_secret(self, v: Secret) -> SecretIprf { + Self::prf_invoc(self.0.secret(), v.secret()) + } + + fn into_secret(self) -> Secret { + self.0 + } + + fn into_secret_slice(self, v: &[u8], dst: &[u8]) { + prf_into(self.0.secret(), v, dst) + } +} + +impl SecretIprfBranch { + fn mix(&self, v: &[u8]) -> SecretIprf { + SecretIprf::prf_invoc(self.0.secret(), v) + } + + fn mix_secret(&self, v: Secret) -> SecretIprf { + SecretIprf::prf_invoc(self.0.secret(), v.secret()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..761c173 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,646 @@ +use anyhow::{bail, ensure, Context, Result}; +use log::{error, info}; +use rosenpass::{ + attempt, + coloring::{Public, Secret}, + multimatch, + pqkem::{SKEM, KEM}, + protocol::{SPk, SSk, MsgBuf, PeerPtr, Server as CryptoServer, SymKey, Timing}, + sodium::sodium_init, + util::{b64_reader, b64_writer, fmt_b64}, +}; +use std::{ + fs::{File, OpenOptions}, + io::{ErrorKind, Read, Write}, + net::{SocketAddr, ToSocketAddrs, UdpSocket}, + path::Path, + process::{exit, Command, Stdio}, + time::Duration, +}; + +/// Open a file writable +pub fn fopen_w>(path: P) -> Result { + Ok(OpenOptions::new() + .read(false) + .write(true) + .create(true) + .truncate(true) + .open(path)?) +} +/// Open a file readable +pub fn fopen_r>(path: P) -> Result { + Ok(OpenOptions::new() + .read(true) + .write(false) + .create(false) + .truncate(false) + .open(path)?) +} + +pub trait ReadExactToEnd { + fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()>; +} + +impl ReadExactToEnd for R { + fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()> { + let mut dummy = [0u8; 8]; + self.read_exact(buf)?; + ensure!(self.read(&mut dummy)? == 0, "File too long!"); + Ok(()) + } +} + +pub trait LoadValue { + fn load>(path: P) -> Result + where + Self: Sized; +} + +pub trait LoadValueB64 { + fn load_b64>(path: P) -> Result + where + Self: Sized; +} + +trait StoreValue { + fn store>(&self, path: P) -> Result<()>; +} + +trait StoreSecret { + unsafe fn store_secret>(&self, path: P) -> Result<()>; +} + +impl StoreSecret for T { + unsafe fn store_secret>(&self, path: P) -> Result<()> { + self.store(path) + } +} + +impl LoadValue for Secret { + fn load>(path: P) -> Result { + let mut v = Self::random(); + let p = path.as_ref(); + fopen_r(p)? + .read_exact_to_end(v.secret_mut()) + .with_context(|| format!("Could not load file {p:?}"))?; + Ok(v) + } +} + +impl LoadValueB64 for Secret { + fn load_b64>(path: P) -> Result { + let mut v = Self::random(); + let p = path.as_ref(); + // This might leave some fragments of the secret on the stack; + // in practice this is likely not a problem because the stack likely + // will be overwritten by something else soon but this is not exactly + // guaranteed. It would be possible to remedy this, but since the secret + // data will linger in the linux page cache anyways with the current + // implementation, going to great length to erase the secret here is + // not worth it right now. + b64_reader(&mut fopen_r(p)?) + .read_exact(v.secret_mut()) + .with_context(|| format!("Could not load base64 file {p:?}"))?; + Ok(v) + } +} + +impl StoreSecret for Secret { + unsafe fn store_secret>(&self, path: P) -> Result<()> { + std::fs::write(path, self.secret())?; + Ok(()) + } +} + +impl LoadValue for Public { + fn load>(path: P) -> Result { + let mut v = Self::random(); + fopen_r(path)?.read_exact_to_end(&mut *v)?; + Ok(v) + } +} + +impl StoreValue for Public { + fn store>(&self, path: P) -> Result<()> { + std::fs::write(path, **self)?; + Ok(()) + } +} + +macro_rules! bail_usage { + ($args:expr, $($pt:expr),*) => {{ + error!($($pt),*); + cmd_help()?; + exit(1); + }} +} + +macro_rules! ensure_usage { + ($args:expr, $ck:expr, $($pt:expr),*) => {{ + if !$ck { + bail_usage!($args, $($pt),*); + } + }} +} + +macro_rules! mandatory_opt { + ($args:expr, $val:expr, $name:expr) => {{ + ensure_usage!($args, $val.is_some(), "{0} option is mandatory", $name) + }}; +} + +pub struct ArgsWalker { + pub argv: Vec, + pub off: usize, +} + +impl ArgsWalker { + pub fn get(&self) -> Option<&str> { + self.argv.get(self.off).map(|s| s as &str) + } + + pub fn prev(&mut self) -> Option<&str> { + assert!(self.off > 0); + self.off -= 1; + self.get() + } + + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Option<&str> { + assert!(self.todo() > 0); + self.off += 1; + self.get() + } + + pub fn opt(&mut self, dst: &mut Option) -> Result<()> { + let cmd = &self.argv[self.off - 1]; + ensure_usage!(&self, self.todo() > 0, "Option {} takes a value", cmd); + ensure_usage!(&self, dst.is_none(), "Cannot set {} multiple times.", cmd); + *dst = Some(String::from(self.next().unwrap())); + Ok(()) + } + + fn todo(&self) -> usize { + self.argv.len() - self.off + } +} + +#[derive(Default, Debug)] +pub struct WireguardOut { + // impl KeyOutput + dev: String, + pk: String, + extra_params: Vec, +} + +#[derive(Default, Debug)] +pub struct AppPeer { + pub outfile: Option, + pub outwg: Option, + pub tx_addr: Option, +} + +#[derive(Debug)] +pub enum Verbosity { + Quiet, + Verbose, +} + +/// Holds the state of the application, namely the external IO +#[derive(Debug)] +pub struct AppServer { + pub crypt: CryptoServer, + pub sock: UdpSocket, + pub peers: Vec, + pub verbosity: Verbosity, +} + +/// Index based pointer to a Peer +#[derive(Debug)] +pub struct AppPeerPtr(pub usize); + +impl AppPeerPtr { + /// Takes an index based handle and returns the actual peer + pub fn lift(p: PeerPtr) -> Self { + Self(p.0) + } + + /// Returns an index based handle to one Peer + pub fn lower(&self) -> PeerPtr { + PeerPtr(self.0) + } + + pub fn get_app<'a>(&self, srv: &'a AppServer) -> &'a AppPeer { + &srv.peers[self.0] + } + + pub fn get_app_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut AppPeer { + &mut srv.peers[self.0] + } +} + +#[derive(Debug)] +pub enum AppPollResult { + DeleteKey(AppPeerPtr), + SendInitiation(AppPeerPtr), + SendRetransmission(AppPeerPtr), + ReceivedMessage(usize, SocketAddr), +} + +#[derive(Debug)] +pub enum KeyOutputReason { + Exchanged, + Stale, +} + +/// Catches errors, prints them through the logger, then exits +pub fn main() { + env_logger::init(); + match rosenpass_main() { + Ok(_) => {} + Err(e) => { + error!("{e}"); + exit(1); + } + } +} + +/// Entry point to the whole program +pub fn rosenpass_main() -> Result<()> { + sodium_init()?; + + let mut args = ArgsWalker { + argv: std::env::args().collect(), + off: 0, // skipping executable path + }; + + // Command parsing + match args.next() { + Some("help") | Some("-h") | Some("-help") | Some("--help") => cmd_help()?, + Some("keygen") => cmd_keygen(args)?, + Some("exchange") => cmd_exchange(args)?, + Some(cmd) => bail_usage!(&args, "No such command {}", cmd), + None => bail_usage!(&args, "Expected a command!"), + }; + + Ok(()) +} + +/// Print the usage information +pub fn cmd_help() -> Result<()> { + eprint!(include_str!("usage.md"), env!("CARGO_BIN_NAME")); + Ok(()) +} + +/// Generate a keypair +pub fn cmd_keygen(mut args: ArgsWalker) -> Result<()> { + let mut sf: Option = None; + let mut pf: Option = None; + + // Arg parsing + loop { + match args.next() { + Some("private-key") => args.opt(&mut sf)?, + Some("public-key") => args.opt(&mut pf)?, + Some(opt) => bail_usage!(&args, "Unknown option `{}`", opt), + None => break, + }; + } + + mandatory_opt!(&args, sf, "private-key"); + mandatory_opt!(&args, pf, "private-key"); + + // Cmd + let (mut ssk, mut spk) = (SSk::random(), SPk::random()); + unsafe { + SKEM::keygen(ssk.secret_mut(), spk.secret_mut())?; + ssk.store_secret(sf.unwrap())?; + spk.store_secret(pf.unwrap())?; + } + + Ok(()) +} + +pub fn cmd_exchange(mut args: ArgsWalker) -> Result<()> { + // Argument parsing + let mut sf: Option = None; + let mut pf: Option = None; + let mut listen: Option = None; + let mut verbosity = Verbosity::Quiet; + + // Global parameters + loop { + match args.next() { + Some("private-key") => args.opt(&mut sf)?, + Some("public-key") => args.opt(&mut pf)?, + Some("listen") => args.opt(&mut listen)?, + Some("verbose") => { + verbosity = Verbosity::Verbose; + } + Some("peer") => { + args.prev(); + break; + } + Some(opt) => bail_usage!(&args, "Unknown option `{}`", opt), + None => break, + }; + } + + mandatory_opt!(&args, sf, "private-key"); + mandatory_opt!(&args, pf, "public-key"); + + let mut srv = std::boxed::Box::::new(AppServer::new( + // sk, pk, addr + SSk::load(&sf.unwrap())?, + SPk::load(&pf.unwrap())?, + listen.as_deref().unwrap_or("[0::0]:0"), + verbosity, + )?); + + // Peer parameters + '_parseAllPeers: while args.todo() > 0 { + let mut pf: Option = None; + let mut outfile: Option = None; + let mut outwg: Option = None; + let mut endpoint: Option = None; + let mut pskf: Option = None; + + args.next(); // skip "peer" starter itself + + 'parseOnePeer: loop { + match args.next() { + // Done with this peer + Some("peer") => { + args.prev(); + break 'parseOnePeer; + } + None => break 'parseOnePeer, + // Options + Some("public-key") => args.opt(&mut pf)?, + Some("endpoint") => args.opt(&mut endpoint)?, + Some("preshared-key") => args.opt(&mut pskf)?, + Some("outfile") => args.opt(&mut outfile)?, + // Wireguard out + Some("wireguard") => { + ensure_usage!( + &args, + outwg.is_none(), + "Cannot set wireguard output for the same peer multiple times." + ); + ensure_usage!(&args, args.todo() >= 2, "Option wireguard takes to values"); + let dev = String::from(args.next().unwrap()); + let pk = String::from(args.next().unwrap()); + let wg = outwg.insert(WireguardOut { + dev, + pk, + extra_params: Vec::new(), + }); + '_parseWgOutExtra: loop { + match args.next() { + Some("peer") => { + args.prev(); + break 'parseOnePeer; + } + None => break 'parseOnePeer, + Some(xtra) => wg.extra_params.push(xtra.to_string()), + }; + } + } + // Invalid + Some(opt) => bail_usage!(&args, "Unknown peer option `{}`", opt), + }; + } + + mandatory_opt!(&args, pf, "private-key"); + ensure_usage!( + &args, + outfile.is_some() || outwg.is_some(), + "Either of the outfile or wireguard option is mandatory" + ); + + let tx_addr = endpoint + .map(|e| { + e.to_socket_addrs()? + .next() + .context("Expected address in endpoint parameter") + }) + .transpose()?; + + srv.add_peer( + // psk, pk, outfile, outwg, tx_addr + pskf.map(SymKey::load_b64).transpose()?, + SPk::load(&pf.unwrap())?, + outfile, + outwg, + tx_addr, + )?; + } + + srv.listen_loop() +} + +impl AppServer { + pub fn new( + sk: SSk, + pk: SPk, + addr: A, + verbosity: Verbosity, + ) -> Result { + Ok(Self { + crypt: CryptoServer::new(sk, pk), + sock: UdpSocket::bind(addr)?, + peers: Vec::new(), + verbosity, + }) + } + + pub fn verbose(&self) -> bool { + matches!(self.verbosity, Verbosity::Verbose) + } + + pub fn add_peer( + &mut self, + psk: Option, + pk: SPk, + outfile: Option, + outwg: Option, + tx_addr: Option, + ) -> Result { + let PeerPtr(pn) = self.crypt.add_peer(psk, pk)?; + assert!(pn == self.peers.len()); + self.peers.push(AppPeer { + outfile, + outwg, + tx_addr, + }); + Ok(AppPeerPtr(pn)) + } + + pub fn listen_loop(&mut self) -> Result<()> { + const INIT_SLEEP: f64 = 0.01; + const MAX_FAILURES: i32 = 10; + let mut failure_cnt = 0; + + loop { + let msgs_processed = 0usize; + let err = match self.event_loop() { + Ok(()) => return Ok(()), + Err(e) => e, + }; + + // This should not happen… + failure_cnt = if msgs_processed > 0 { + 0 + } else { + failure_cnt + 1 + }; + let sleep = INIT_SLEEP * 2.0f64.powf(f64::from(failure_cnt - 1)); + let tries_left = MAX_FAILURES - (failure_cnt - 1); + error!( + "unexpected error after processing {} messages: {:?} {}", + msgs_processed, + err, + err.backtrace() + ); + if tries_left > 0 { + error!("reinitializing networking in {sleep}! {tries_left} tries left."); + std::thread::sleep(self.crypt.timebase.dur(sleep)); + continue; + } + + bail!("too many network failures"); + } + } + + pub fn event_loop(&mut self) -> Result<()> { + let (mut rx, mut tx) = (MsgBuf::zero(), MsgBuf::zero()); + macro_rules! tx_maybe_with { + ($peer:expr, $fn:expr) => { + attempt!({ + let p = $peer.get_app(self); + if let Some(addr) = p.tx_addr { + let len = $fn()?; + self.sock.send_to(&tx[..len], addr)?; + } + Ok(()) + }) + }; + } + + loop { + use rosenpass::protocol::HandleMsgResult; + use AppPollResult::*; + use KeyOutputReason::*; + match self.poll(&mut *rx)? { + SendInitiation(peer) => tx_maybe_with!(peer, || self + .crypt + .initiate_handshake(peer.lower(), &mut *tx))?, + SendRetransmission(peer) => tx_maybe_with!(peer, || self + .crypt + .retransmit_handshake(peer.lower(), &mut *tx))?, + DeleteKey(peer) => self.output_key(peer, Stale, &SymKey::random())?, + + ReceivedMessage(len, addr) => { + multimatch!(self.crypt.handle_msg(&rx[..len], &mut *tx), + Err(ref e) => + self.verbose().then(|| + info!("error processing incoming message from {:?}: {:?} {}", addr, e, e.backtrace())), + + Ok(HandleMsgResult { resp: Some(len), .. }) => { + self.sock.send_to(&tx[0..len], addr)? + }, + + Ok(HandleMsgResult { exchanged_with: Some(p), .. }) => { + let ap = AppPeerPtr::lift(p); + ap.get_app_mut(self).tx_addr = Some(addr); + // TODO: Maybe we should rather call the key "rosenpass output"? + self.output_key(ap, Exchanged, &self.crypt.osk(p)?)?; + } + ); + } + }; + } + } + + pub fn output_key(&self, peer: AppPeerPtr, why: KeyOutputReason, key: &SymKey) -> Result<()> { + let peerid = peer.lower().get(&self.crypt).pidt()?; + let ap = peer.get_app(self); + + if self.verbose() { + let msg = match why { + KeyOutputReason::Exchanged => "Exchanged key with peer", + KeyOutputReason::Stale => "Erasing outdated key from peer", + }; + info!("{} {}", msg, fmt_b64(&*peerid)); + } + + if let Some(of) = ap.outfile.as_ref() { + // This might leave some fragments of the secret on the stack; + // in practice this is likely not a problem because the stack likely + // will be overwritten by something else soon but this is not exactly + // guaranteed. It would be possible to remedy this, but since the secret + // data will linger in the linux page cache anyways with the current + // implementation, going to great length to erase the secret here is + // not worth it right now. + b64_writer(fopen_w(of)?).write_all(key.secret())?; + let why = match why { + KeyOutputReason::Exchanged => "exchanged", + KeyOutputReason::Stale => "stale", + }; + println!( + "output-key peer {} key-file {} {}", + fmt_b64(&*peerid), + of, + why + ); + } + + if let Some(owg) = ap.outwg.as_ref() { + let child = Command::new("wg") + .arg("set") + .arg(&owg.dev) + .arg("peer") + .arg(&owg.pk) + .arg("preshared-key") + .arg("/dev/stdin") + .stdin(Stdio::piped()) + .args(&owg.extra_params) + .spawn()?; + b64_writer(child.stdin.unwrap()).write_all(key.secret())?; + } + + Ok(()) + } + + pub fn poll(&mut self, rx_buf: &mut [u8]) -> Result { + use rosenpass::protocol::PollResult as C; + use AppPollResult as A; + loop { + return Ok(match self.crypt.poll()? { + C::DeleteKey(PeerPtr(no)) => A::DeleteKey(AppPeerPtr(no)), + C::SendInitiation(PeerPtr(no)) => A::SendInitiation(AppPeerPtr(no)), + C::SendRetransmission(PeerPtr(no)) => A::SendRetransmission(AppPeerPtr(no)), + C::Sleep(timeout) => match self.try_recv(rx_buf, timeout)? { + Some((len, addr)) => A::ReceivedMessage(len, addr), + None => continue, + }, + }); + } + } + + pub fn try_recv(&self, buf: &mut [u8], timeout: Timing) -> Result> { + if timeout == 0.0 { + return Ok(None); + } + self.sock + .set_read_timeout(Some(Duration::from_secs_f64(timeout)))?; + match self.sock.recv_from(buf) { + Ok(x) => Ok(Some(x)), + Err(e) => match e.kind() { + ErrorKind::WouldBlock => Ok(None), + ErrorKind::TimedOut => Ok(None), + _ => Err(anyhow::Error::new(e)), + }, + } + } +} diff --git a/src/msgs.rs b/src/msgs.rs new file mode 100644 index 0000000..09f9160 --- /dev/null +++ b/src/msgs.rs @@ -0,0 +1,384 @@ +//! # Messages +//! +//! This module contains data structures that help in the +//! serialization/deserialization (ser/de) of messages. Thats kind of a lie, +//! since no actual ser/de happens. Instead, the structures offer views into +//! mutable byte slices (`&mut [u8]`), allowing to modify the fields of an +//! always serialized instance of the data in question. This is closely related +//! to the concept of lenses in function programming; more on that here: +//! [https://sinusoid.es/misc/lager/lenses.pdf](https://sinusoid.es/misc/lager/lenses.pdf) +//! +//! # Example +//! +//! The following example uses the [`data_lense` macro](crate::data_lense) to create a lense that +//! might be useful when dealing with UDP headers. +//! +//! ``` +//! use rosenpass::{data_lense, RosenpassError, msgs::LenseView}; +//! # fn main() -> Result<(), RosenpassError> { +//! +//! data_lense! {UdpDatagramHeader := +//! source_port: 2, +//! dest_port: 2, +//! length: 2, +//! checksum: 2 +//! } +//! +//! let mut buf = [0u8; 8]; +//! +//! // read-only lense, no check of size: +//! let lense = UdpDatagramHeader(&buf); +//! assert_eq!(lense.checksum(), &[0, 0]); +//! +//! // mutable lense, runtime check of size +//! let mut lense = buf.as_mut().udp_datagram_header()?; +//! lense.source_port_mut().copy_from_slice(&53u16.to_be_bytes()); // some DNS, anyone? +//! +//! // the original buffer is still available +//! assert_eq!(buf, [0, 53, 0, 0, 0, 0, 0, 0]); +//! +//! // read-only lense, runtime check of size +//! let lense = buf.as_ref().udp_datagram_header()?; +//! assert_eq!(lense.source_port(), &[0, 53]); +//! # Ok(()) +//! # } +//! ``` + +use super::RosenpassError; +use crate::{pqkem::*, sodium}; + +// Macro magic //////////////////////////////////////////////////////////////// + +/// A macro to create data lenses. Refer to the [`msgs` mod](crate::msgs) for +/// an example and further elaboration +// TODO implement TryFrom<[u8]> and From<[u8; Self::len()]> +#[macro_export] +macro_rules! data_lense( + // prefix @ offset ; optional meta ; field name : field length, ... + (token_muncher_ref @ $offset:expr ; $( $attr:meta )* ; $field:ident : $len:expr $(, $( $tail:tt )+ )?) => { + ::paste::paste!{ + + #[allow(rustdoc::broken_intra_doc_links)] + $( #[ $attr ] )* + /// + #[doc = data_lense!(maybe_docstring_link $len)] + /// bytes long + pub fn $field(&self) -> &__ContainerType::Output { + &self.0[$offset .. $offset + $len] + } + + /// The bytes until the + #[doc = data_lense!(maybe_docstring_link Self::$field)] + /// field + pub fn [< until_ $field >](&self) -> &__ContainerType::Output { + &self.0[0 .. $offset] + } + + // if the tail exits, consume it as well + $( + data_lense!{token_muncher_ref @ $offset + $len ; $( $tail )+ } + )? + } + }; + + // prefix @ offset ; optional meta ; field name : field length, ... + (token_muncher_mut @ $offset:expr ; $( $attr:meta )* ; $field:ident : $len:expr $(, $( $tail:tt )+ )?) => { + ::paste::paste!{ + + #[allow(rustdoc::broken_intra_doc_links)] + $( #[ $attr ] )* + /// + #[doc = data_lense!(maybe_docstring_link $len)] + /// bytes long + pub fn [< $field _mut >](&mut self) -> &mut __ContainerType::Output { + &mut self.0[$offset .. $offset + $len] + } + + // if the tail exits, consume it as well + $( + data_lense!{token_muncher_mut @ $offset + $len ; $( $tail )+ } + )? + } + }; + + // switch that yields literals unchanged, but creates docstring links to + // constants + // TODO the doc string link doesn't work if $x is taken from a generic, + (maybe_docstring_link $x:literal) => (stringify!($x)); + (maybe_docstring_link $x:expr) => (stringify!([$x])); + + // struct name < optional generics > := optional doc string field name : field length, ... + ($type:ident $( < $( $generic:ident ),+ > )? := $( $( #[ $attr:meta ] )* $field:ident : $len:expr ),+) => (::paste::paste!{ + + #[allow(rustdoc::broken_intra_doc_links)] + /// A data lense to manipulate byte slices. + /// + //// # Fields + /// + $( + /// - ` + #[doc = stringify!($field)] + /// `: + #[doc = data_lense!(maybe_docstring_link $len)] + /// bytes + )+ + pub struct $type<__ContainerType $(, $( $generic ),+ )? > ( + __ContainerType, + // The phantom data is required, since all generics declared on a + // type need to be used on the type. + // https://doc.rust-lang.org/stable/error_codes/E0392.html + $( $( ::core::marker::PhantomData<$generic> ),+ )? + ); + + impl<__ContainerType $(, $( $generic: LenseView ),+ )? > $type<__ContainerType $(, $( $generic ),+ )? >{ + $( + /// Size in bytes of the field ` + #[doc = !($field)] + /// ` + pub const fn [< $field _len >]() -> usize{ + $len + } + )+ + + /// Verify that `len` is sufficiently long to hold [Self] + pub fn check_size(len: usize) -> Result<(), RosenpassError>{ + let required_size = $( $len + )+ 0; + let actual_size = len; + if required_size < actual_size { + Err(RosenpassError::BufferSizeMismatch { + required_size, + actual_size, + }) + }else{ + Ok(()) + } + } + } + + // read-only accessor functions + impl<'a, __ContainerType $(, $( $generic: LenseView ),+ )?> $type<&'a __ContainerType $(, $( $generic ),+ )?> + where + __ContainerType: std::ops::Index> + ?Sized, + { + data_lense!{token_muncher_ref @ 0 ; $( $( $attr )* ; $field : $len ),+ } + + /// View into all bytes belonging to this Lense + pub fn all_bytes(&self) -> &__ContainerType::Output { + &self.0[0..Self::LEN] + } + } + + // mutable accessor functions + impl<'a, __ContainerType $(, $( $generic: LenseView ),+ )?> $type<&'a mut __ContainerType $(, $( $generic ),+ )?> + where + __ContainerType: std::ops::IndexMut> + ?Sized, + { + data_lense!{token_muncher_ref @ 0 ; $( $( $attr )* ; $field : $len ),+ } + data_lense!{token_muncher_mut @ 0 ; $( $( $attr )* ; $field : $len ),+ } + + /// View into all bytes belonging to this Lense + pub fn all_bytes(&self) -> &__ContainerType::Output { + &self.0[0..Self::LEN] + } + + /// View into all bytes belonging to this Lense + pub fn all_bytes_mut(&mut self) -> &mut __ContainerType::Output { + &mut self.0[0..Self::LEN] + } + } + + // lense trait, allowing us to know the implementing lenses size + impl<__ContainerType $(, $( $generic: LenseView ),+ )? > LenseView for $type<__ContainerType $(, $( $generic ),+ )? >{ + /// Number of bytes required to store this type in binary format + const LEN: usize = $( $len + )+ 0; + } + + /// Extension trait to allow checked creation of a lense over + /// some byte slice that contains a + #[doc = data_lense!(maybe_docstring_link $type)] + pub trait [< $type Ext >] { + type __ContainerType; + + /// Create a lense to the byte slice + fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type, RosenpassError>; + } + + impl<'a> [< $type Ext >] for &'a [u8] { + type __ContainerType = &'a [u8]; + + fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type, RosenpassError> { + Ok($type ( self, $( $( ::core::marker::PhantomData::<$generic> ),+ )? )) + } + } + + impl<'a> [< $type Ext >] for &'a mut [u8] { + type __ContainerType = &'a mut [u8]; + + fn [< $type:snake >] $(< $($generic),* >)? (self) -> Result< $type, RosenpassError> { + Ok($type ( self, $( $( ::core::marker::PhantomData::<$generic> ),+ )? )) + } + } + }); +); + +/// Common trait shared by all Lenses +pub trait LenseView { + const LEN: usize; +} + +data_lense! { Envelope := + /// [MsgType] of this message + msg_type: 1, + /// Reserved for future use + reserved: 3, + /// The actual Paylod + payload: M::LEN, + /// Message Authentication Code (mac) over all bytes until (exclusive) + /// `mac` itself + mac: sodium::MAC_SIZE, + /// Currently unused, TODO: do something with this + cookie: sodium::MAC_SIZE +} + +data_lense! { InitHello := + /// Randomly generated connection id + sidi: 4, + /// Kyber 512 Ephemeral Public Key + epki: EKEM::PK_LEN, + /// Classic McEliece Ciphertext + sctr: SKEM::CT_LEN, + /// Encryped: 16 byte hash of McEliece initiator static key + pidic: sodium::AEAD_TAG_LEN + 32, + /// Encrypted TAI64N Time Stamp (against replay attacks) + auth: sodium::AEAD_TAG_LEN +} + +data_lense! { RespHello := + /// Randomly generated connection id + sidr: 4, + /// Copied from InitHello + sidi: 4, + /// Kyber 512 Ephemeral Ciphertext + ecti: EKEM::CT_LEN, + /// Classic McEliece Ciphertext + scti: SKEM::CT_LEN, + /// Empty encrypted message (just an auth tag) + auth: sodium::AEAD_TAG_LEN, + /// Responders handshake state in encrypted form + biscuit: BISCUIT_CT_LEN +} + +data_lense! { InitConf := + /// Copied from InitHello + sidi: 4, + /// Copied from RespHello + sidr: 4, + /// Responders handshake state in encrypted form + biscuit: BISCUIT_CT_LEN, + /// Empty encrypted message (just an auth tag) + auth: sodium::AEAD_TAG_LEN +} + +data_lense! { EmptyData := + /// Copied from RespHello + sid: 4, + /// Nonce + ctr: 8, + /// Empty encrypted message (just an auth tag) + auth: sodium::AEAD_TAG_LEN +} + +data_lense! { Biscuit := + /// H(spki) – Ident ifies the initiator + pidi: sodium::KEY_SIZE, + /// The biscuit number (replay protection) + biscuit_no: 12, + /// Chaining key + ck: sodium::KEY_SIZE +} + +data_lense! { DataMsg := + dummy: 4 +} + +data_lense! { CookieReply := + dummy: 4 +} + +// Traits ///////////////////////////////////////////////////////////////////// + +pub trait WireMsg: std::fmt::Debug { + const MSG_TYPE: MsgType; + const MSG_TYPE_U8: u8 = Self::MSG_TYPE as u8; + const BYTES: usize; +} + +// Constants ////////////////////////////////////////////////////////////////// + +pub const SESSION_ID_LEN: usize = 4; +pub const BISCUIT_ID_LEN: usize = 12; + +pub const WIRE_ENVELOPE_LEN: usize = 1 + 3 + 16 + 16; // TODO verify this + +/// Size required to fit any message in binary form +pub const MAX_MESSAGE_LEN: usize = 2500; // TODO fix this + +/// Recognized message types +#[repr(u8)] +#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)] +pub enum MsgType { + InitHello = 0x81, + RespHello = 0x82, + InitConf = 0x83, + EmptyData = 0x84, + DataMsg = 0x85, + CookieReply = 0x86, +} + +impl TryFrom for MsgType { + type Error = RosenpassError; + + fn try_from(value: u8) -> Result { + Ok(match value { + 0x81 => MsgType::InitHello, + 0x82 => MsgType::RespHello, + 0x83 => MsgType::InitConf, + 0x84 => MsgType::EmptyData, + 0x85 => MsgType::DataMsg, + 0x86 => MsgType::CookieReply, + _ => return Err(RosenpassError::InvalidMessageType(value)), + }) + } +} + +/// length in bytes of an unencrypted Biscuit (plain text) +pub const BISCUIT_PT_LEN: usize = Biscuit::<()>::LEN; + +/// Length in bytes of an encrypted Biscuit (cipher text) +pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + sodium::XAEAD_NONCE_LEN + sodium::XAEAD_TAG_LEN; + +#[cfg(test)] +mod test_constants { + use crate::{ + msgs::{BISCUIT_CT_LEN, BISCUIT_PT_LEN}, + sodium, + }; + + #[test] + fn sodium_keysize() { + assert_eq!(sodium::KEY_SIZE, 32); + } + + #[test] + fn biscuit_pt_len() { + assert_eq!(BISCUIT_PT_LEN, 2 * sodium::KEY_SIZE + 12); + } + + #[test] + fn biscuit_ct_len() { + assert_eq!( + BISCUIT_CT_LEN, + BISCUIT_PT_LEN + sodium::XAEAD_NONCE_LEN + sodium::XAEAD_TAG_LEN + ); + } +} diff --git a/src/pqkem.rs b/src/pqkem.rs new file mode 100644 index 0000000..14d1e47 --- /dev/null +++ b/src/pqkem.rs @@ -0,0 +1,176 @@ +//! This module contains Traits and implementations for Key Encapsulation +//! Mechanisms (KEM). KEMs are the interface provided by almost all post-quantum +//! secure key exchange mechanisms. +//! +//! Conceptually KEMs are akin to public-key encryption, but instead of encrypting +//! arbitrary data, KEMs are limited to the transmission of keys, randomly chosen during +//! +//! encapsulation. +//! The [KEM] Trait describes the basic API offered by a Key Encapsulation +//! Mechanism. Two implementations for it are provided, [SKEM] and [EKEM]. + +use crate::{RosenpassError, RosenpassMaybeError}; + +/// Key Encapsulation Mechanism +/// +/// The KEM interface defines three operations: Key generation, key encapsulation and key +/// decapsulation. +pub trait KEM { + /// Secrete Key length + const SK_LEN: usize; + /// Public Key length + const PK_LEN: usize; + /// Ciphertext length + const CT_LEN: usize; + /// Shared Secret length + const SHK_LEN: usize; + + /// Generate a keypair consisting of secret key (`sk`) and public key (`pk`) + /// + /// `keygen() -> sk, pk` + fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError>; + + /// From a public key (`pk`), generate a shared key (`shk`, for local use) + /// and a cipher text (`ct`, to be sent to the owner of the `pk`). + /// + /// `encaps(pk) -> shk, ct` + fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError>; + + /// From a secret key (`sk`) and a cipher text (`ct`) derive a shared key + /// (`shk`) + /// + /// `decaps(sk, ct) -> shk` + fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError>; +} + +/// A KEM that is secure against Chosen Ciphertext Attacks (CCA). +/// In the context of rosenpass this is used for static keys. +/// Uses [Classic McEliece](https://classic.mceliece.org/) 460896 from liboqs. +/// +/// Classic McEliece is chosen because of its high security margin and its small +/// ciphertexts. The public keys are humongous, but (being static keys) the are never transmitted over +/// the wire so this is not a big problem. +pub struct SKEM; + +/// # Safety +/// +/// This Trait impl calls unsafe [oqs_sys] functions, that write to byte +/// slices only identified using raw pointers. It must be ensured that the raw +/// pointers point into byte slices of sufficient length, to avoid UB through +/// overwriting of arbitrary data. This is checked in the following code before +/// the unsafe calls, and an early return with an Err occurs if the byte slice +/// size does not match the required size. +/// +/// __Note__: This requirement is stricter than necessary, it would suffice +/// to only check that the buffers are big enough, allowing them to be even +/// bigger. However, from a correctness point of view it does not make sense to +/// allow bigger buffers. +impl KEM for SKEM { + const SK_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_secret_key as usize; + const PK_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_public_key as usize; + const CT_LEN: usize = oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_ciphertext as usize; + const SHK_LEN: usize = + oqs_sys::kem::OQS_KEM_classic_mceliece_460896_length_shared_secret as usize; + + fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError> { + RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?; + RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?; + unsafe { + oqs_sys::kem::OQS_KEM_classic_mceliece_460896_keypair(pk.as_mut_ptr(), sk.as_mut_ptr()) + .to_rg_error() + } + } + + fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError> { + RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?; + RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?; + RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?; + unsafe { + oqs_sys::kem::OQS_KEM_classic_mceliece_460896_encaps( + ct.as_mut_ptr(), + shk.as_mut_ptr(), + pk.as_ptr(), + ) + .to_rg_error() + } + } + + fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError> { + RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?; + RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?; + RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?; + unsafe { + oqs_sys::kem::OQS_KEM_classic_mceliece_460896_decaps( + shk.as_mut_ptr(), + ct.as_ptr(), + sk.as_ptr(), + ) + .to_rg_error() + } + } +} + +/// Implements a KEM that is secure against Chosen Plaintext Attacks (CPA). +/// In the context of rosenpass this is used for ephemeral keys. +/// Currently the implementation uses +/// [Kyber 512](https://openquantumsafe.org/liboqs/algorithms/kem/kyber) from liboqs. +/// +/// This is being used for ephemeral keys; since these are use-once the first post quantum +/// wireguard paper claimed that CPA security would be sufficient. Nonetheless we choose kyber +/// which provides CCA security since there are no publicly vetted KEMs out there which provide +/// only CPA security. +pub struct EKEM; + +/// # Safety +/// +/// This Trait impl calls unsafe [oqs_sys] functions, that write to byte +/// slices only identified using raw pointers. It must be ensured that the raw +/// pointers point into byte slices of sufficient length, to avoid UB through +/// overwriting of arbitrary data. This is checked in the following code before +/// the unsafe calls, and an early return with an Err occurs if the byte slice +/// size does not match the required size. +/// +/// __Note__: This requirement is stricter than necessary, it would suffice +/// to only check that the buffers are big enough, allowing them to be even +/// bigger. However, from a correctness point of view it does not make sense to +/// allow bigger buffers. +impl KEM for EKEM { + const SK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_secret_key as usize; + const PK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_public_key as usize; + const CT_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_ciphertext as usize; + const SHK_LEN: usize = oqs_sys::kem::OQS_KEM_kyber_512_length_shared_secret as usize; + fn keygen(sk: &mut [u8], pk: &mut [u8]) -> Result<(), RosenpassError> { + RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?; + RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?; + unsafe { + oqs_sys::kem::OQS_KEM_kyber_512_keypair(pk.as_mut_ptr(), sk.as_mut_ptr()) + .to_rg_error() + } + } + fn encaps(shk: &mut [u8], ct: &mut [u8], pk: &[u8]) -> Result<(), RosenpassError> { + RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?; + RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?; + RosenpassError::check_buffer_size(pk.len(), Self::PK_LEN)?; + unsafe { + oqs_sys::kem::OQS_KEM_kyber_512_encaps( + ct.as_mut_ptr(), + shk.as_mut_ptr(), + pk.as_ptr(), + ) + .to_rg_error() + } + } + fn decaps(shk: &mut [u8], sk: &[u8], ct: &[u8]) -> Result<(), RosenpassError> { + RosenpassError::check_buffer_size(shk.len(), Self::SHK_LEN)?; + RosenpassError::check_buffer_size(sk.len(), Self::SK_LEN)?; + RosenpassError::check_buffer_size(ct.len(), Self::CT_LEN)?; + unsafe { + oqs_sys::kem::OQS_KEM_kyber_512_decaps( + shk.as_mut_ptr(), + ct.as_ptr(), + sk.as_ptr(), + ) + .to_rg_error() + } + } +} diff --git a/src/prftree.rs b/src/prftree.rs new file mode 100644 index 0000000..90f4422 --- /dev/null +++ b/src/prftree.rs @@ -0,0 +1,107 @@ +use { + crate::{ + coloring::Secret, + sodium::{hmac, hmac_into, KEY_SIZE}, + }, + anyhow::Result, +}; + +// TODO Use a proper Dec interface +#[derive(Clone, Debug)] +pub struct PrfTree([u8; KEY_SIZE]); +#[derive(Clone, Debug)] +pub struct PrfTreeBranch([u8; KEY_SIZE]); +#[derive(Clone, Debug)] +pub struct SecretPrfTree(Secret); +#[derive(Clone, Debug)] +pub struct SecretPrfTreeBranch(Secret); + +impl PrfTree { + pub fn zero() -> Self { + Self([0u8; KEY_SIZE]) + } + + pub fn dup(self) -> PrfTreeBranch { + PrfTreeBranch(self.0) + } + + pub fn into_secret_prf_tree(self) -> SecretPrfTree { + SecretPrfTree(Secret::from_slice(&self.0)) + } + + // TODO: Protocol! Use domain separation to ensure that + pub fn mix(self, v: &[u8]) -> Result { + Ok(Self(hmac(&self.0, v)?)) + } + + pub fn mix_secret(self, v: Secret) -> Result { + SecretPrfTree::prf_invoc(&self.0, v.secret()) + } + + pub fn into_value(self) -> [u8; KEY_SIZE] { + self.0 + } +} + +impl PrfTreeBranch { + pub fn mix(&self, v: &[u8]) -> Result { + Ok(PrfTree(hmac(&self.0, v)?)) + } + + pub fn mix_secret(&self, v: Secret) -> Result { + SecretPrfTree::prf_invoc(&self.0, v.secret()) + } +} + +impl SecretPrfTree { + pub fn prf_invoc(k: &[u8], d: &[u8]) -> Result { + let mut r = SecretPrfTree(Secret::zero()); + hmac_into(r.0.secret_mut(), k, d)?; + Ok(r) + } + + pub fn zero() -> Self { + Self(Secret::zero()) + } + + pub fn dup(self) -> SecretPrfTreeBranch { + SecretPrfTreeBranch(self.0) + } + + pub fn danger_from_secret(k: Secret) -> Self { + Self(k) + } + + pub fn mix(self, v: &[u8]) -> Result { + Self::prf_invoc(self.0.secret(), v) + } + + pub fn mix_secret(self, v: Secret) -> Result { + Self::prf_invoc(self.0.secret(), v.secret()) + } + + pub fn into_secret(self) -> Secret { + self.0 + } + + pub fn into_secret_slice(mut self, v: &[u8], dst: &[u8]) -> Result<()> { + hmac_into(self.0.secret_mut(), v, dst) + } +} + +impl SecretPrfTreeBranch { + pub fn mix(&self, v: &[u8]) -> Result { + SecretPrfTree::prf_invoc(self.0.secret(), v) + } + + pub fn mix_secret(&self, v: Secret) -> Result { + SecretPrfTree::prf_invoc(self.0.secret(), v.secret()) + } + + // TODO: This entire API is not very nice; we need this for biscuits, but + // it might be better to extract a special "biscuit" + // labeled subkey and reinitialize the chain with this + pub fn danger_into_secret(self) -> Secret { + self.0 + } +} diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..be8af1e --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,1601 @@ +//! # Overview +//! +//! The most important types in this module probably are [PollResult] & [Server]. +//! Once a [Server] was created, the server is provided with new messages via +//! the [Server::handle_msg] method. The [Server::poll] method can be used to +//! let the server work, which will eventually yield a [PollResult]. Said +//! [PollResult] contains prescriptive activities to be carried out. +//! +//! TODO explain briefly the role of epki +//! +//! # Example Handshake +//! +//! TODO finish doctest example +//! +//! ``` +//! use rosenpass::{ +//! pqkem::{SKEM, KEM}, +//! protocol::{SSk, SPk, MsgBuf, PeerPtr, Server, SymKey}, +//! }; +//! # fn main() -> Result<(), rosenpass::RosenpassError> { +//! +//! // always init libsodium before anything +//! rosenpass::sodium::sodium_init().unwrap(); +//! +//! // initialize public and private key for peer a ... +//! let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero()); +//! SKEM::keygen(peer_a_sk.secret_mut(), peer_a_pk.secret_mut())?; +//! +//! // ... and for peer b +//! let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero()); +//! SKEM::keygen(peer_b_sk.secret_mut(), peer_b_pk.secret_mut())?; +//! +//! // initialize server and a pre-shared key +//! let psk = SymKey::random(); +//! let mut a = Server::new(peer_a_sk, peer_a_pk.clone()); +//! let mut b = Server::new(peer_b_sk, peer_b_pk.clone()); +//! +//! // introduce peers to each other +//! a.add_peer(Some(psk.clone()), peer_b_pk).unwrap(); +//! b.add_peer(Some(psk), peer_a_pk).unwrap(); +//! +//! // let them talk +//! let (mut a_buf, mut b_buf) = (MsgBuf::zero(), MsgBuf::zero()); +//! let sz = a.initiate_handshake(PeerPtr(0), &mut *a_buf).unwrap(); +//! //let (a_key, b_key) = handle(a, &mut a_buf, sz, b, &mut b_buf).unwrap(); +//! //assert_eq!(a_key.unwrap().secret(), b_key.unwrap().secret(), +//! // "the key exchanged failed to establish a shared secret"); +//! # Ok(()) +//! # } +//! ``` + +use crate::{ + coloring::*, + labeled_prf as lprf, + msgs::*, + pqkem::*, + prftree::{SecretPrfTree, SecretPrfTreeBranch}, + sodium::*, + util::*, +}; +use anyhow::{bail, ensure, Context, Result}; +use std::collections::hash_map::{ + Entry::{Occupied, Vacant}, + HashMap, +}; + +// CONSTANTS & SETTINGS ////////////////////////// + +/// Size required to fit any message in binary form +pub const RTX_BUFFER_SIZE: usize = max_usize( + > as LenseView>::LEN, + > as LenseView>::LEN, +); + +/// A type for time, e.g. for backoff before re-tries +pub type Timing = f64; + +/// Before Common Era (or more practically: Definitely so old it needs refreshing) +/// +/// Using this instead of Timing::MIN or Timing::INFINITY to avoid floating +/// point math weirdness. +pub const BCE: Timing = -3600.0 * 24.0 * 356.0 * 10_000.0; + +// Actually it's eight hours; This is intentional to avoid weirdness +// regarding unexpectedly large numbers in system APIs as this is < i16::MAX +pub const UNENDING: Timing = 3600.0 * 8.0; + +// From the wireguard paper; rekey every two minutes, +// discard the key if no rekey is achieved within three +pub const REKEY_AFTER_TIME: Timing = 120.0; +pub const REJECT_AFTER_TIME: Timing = 180.0; + +// Seconds until the biscuit key is changed; we issue biscuits +// using one biscuit key for one epoch and store the biscuit for +// decryption for a second epoch +pub const BISCUIT_EPOCH: Timing = 300.0; + +// Retransmission pub constants; will retransmit for up to _ABORT ms; starting with a delay of +// _DELAY_BEG ms and increasing the delay exponentially by a factor of +// _DELAY_GROWTH up to _DELAY_END. An .secretadditional jitter factor of ±_DELAY_JITTER +// is added. +pub const RETRANSMIT_ABORT: Timing = 120.0; +pub const RETRANSMIT_DELAY_GROWTH: Timing = 2.0; +pub const RETRANSMIT_DELAY_BEGIN: Timing = 0.5; +pub const RETRANSMIT_DELAY_END: Timing = 10.0; +pub const RETRANSMIT_DELAY_JITTER: Timing = 0.5; + +pub const EVENT_GRACE: Timing = 0.0025; + +// UTILITY FUNCTIONS ///////////////////////////// + +// Event handling: For an event at T we sleep for T-now +// but we act on the event starting at T-EVENT_GRACE already +// to avoid sleeping for very short periods. This also avoids +// busy loop in case the sleep subsystem is imprecise. Our timing +// is therefor generally accurate up to ±2∙EVENT_GRACE +pub fn has_happened(ev: Timing, now: Timing) -> bool { + (ev - now) < EVENT_GRACE +} + +// DATA STRUCTURES & BASIC TRAITS & ACCESSORS //// + +pub type SPk = Secret<{ SKEM::PK_LEN }>; // Just Secret<> instead of Public<> so it gets allocated on the heap +pub type SSk = Secret<{ SKEM::SK_LEN }>; +pub type EPk = Public<{ EKEM::PK_LEN }>; +pub type ESk = Secret<{ EKEM::SK_LEN }>; + +pub type SymKey = Secret; +pub type SymHash = Public; + +pub type PeerId = Public; +pub type SessionId = Public; +pub type BiscuitId = Public; + +pub type XAEADNonce = Public; + +pub type MsgBuf = Public; + +pub type PeerNo = usize; + +/// Implementation of the actual cryptographic server +#[derive(Debug)] +pub struct Server { + pub timebase: Timebase, + + // Server Crypto + pub sskm: SSk, + pub spkm: SPk, + pub biscuit_ctr: BiscuitId, + pub biscuit_keys: [BiscuitKey; 2], + + // Peer/Handshake DB + pub peers: Vec, + pub index: HashMap, + + // Tick handling + pub peer_poll_off: usize, +} + +/// A Biscuit is like a fancy cookie. To avoid state disruption attacks, +/// the responder doesn't store state. Instead the state is stored in a +/// Biscuit, that is encrypted using the [BiscuitKey] which is only known to +/// the Respnder. Thus secrecy of the Responder state is not violated, still +/// the responder can avoid storing this state. +#[derive(Debug)] +pub struct BiscuitKey { + pub created_at: Timing, + pub key: SymKey, +} + +#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum IndexKey { + Peer(PeerId), + Sid(SessionId), +} + +#[derive(Debug)] +pub struct Peer { + pub psk: SymKey, + pub spkt: SPk, + pub biscuit_used: BiscuitId, + pub session: Option, + pub handshake: Option, + pub initiation_requested: bool, +} + +impl Peer { + pub fn zero() -> Self { + Self { + psk: SymKey::zero(), + spkt: SPk::zero(), + biscuit_used: BiscuitId::zero(), + session: None, + initiation_requested: false, + handshake: None, + } + } +} + +#[derive(Debug, Clone)] +pub struct HandshakeState { + /// Session ID of Initiator + pub sidi: SessionId, + /// Session ID of Responder + pub sidr: SessionId, + /// Chaining Key + pub ck: SecretPrfTreeBranch, +} + +#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum HandshakeRole { + Initiator, + Responder, +} + +impl HandshakeRole { + pub fn is_initiator(&self) -> bool { + match *self { + HandshakeRole::Initiator => true, + HandshakeRole::Responder => false, + } + } +} + +#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum HandshakeStateMachine { + RespHello, + RespConf, +} + +impl Default for HandshakeStateMachine { + fn default() -> Self { + HandshakeStateMachine::RespHello + } +} + +#[derive(Debug)] +pub struct InitiatorHandshake { + pub created_at: Timing, + pub next: HandshakeStateMachine, + pub core: HandshakeState, + /// Ephemeral Secret Key of Initiator + pub eski: ESk, + /// Ephemeral Public Key of Initiator + pub epki: EPk, + + // Retransmission + // TODO: Ensure that this is correct by typing + pub tx_at: Timing, + pub tx_retry_at: Timing, + pub tx_count: usize, + pub tx_len: usize, + pub tx_buf: MsgBuf, +} + +#[derive(Debug)] +pub struct Session { + // Metadata + pub created_at: Timing, + pub sidm: SessionId, + pub sidt: SessionId, + // Crypto + pub ck: SecretPrfTreeBranch, + /// Key for Transmission ("transmission key mine") + pub txkm: SymKey, + /// Key for Receival ("transmission key theirs") + pub txkt: SymKey, + /// Nonce for Transmission ("transmission nonce mine") + pub txnm: u64, + /// Nonce for Receival ("transmission nonce theirs") + pub txnt: u64, +} + +/// Lifecycle of a Secret +/// +/// The order implies the readiness for usage of a secret, the highes/biggest +/// variant ([Lifecycle::Young]) is the most preferable one in a class of +/// equal-role secrets. +#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] +enum Lifecycle { + /// Not even generated + Void = 0, + /// Secret must be zeroized, disposal adviced + Dead, + /// Soon to be dead: the secret might be used for receiving + /// data, but must not be used for future sending + Retired, + /// The secret might be used unconditionally + Young, +} + +/// Implemented for information (secret and public) that has an expire date +trait Mortal { + /// Time of creation, when [Lifecycle::Void] -> [Lifecycle::Young] + fn created_at(&self, srv: &Server) -> Option; + /// The time where [Lifecycle::Young] -> [Lifecycle::Retired] + fn retire_at(&self, srv: &Server) -> Option; + /// The time where [Lifecycle::Retired] -> [Lifecycle::Dead] + fn die_at(&self, srv: &Server) -> Option; +} + +// BUSINESS LOGIC DATA STRUCTURES //////////////// + +/// Valid index to [Server::peers] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct PeerPtr(pub usize); + +/// Valid index to [Server::peers] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct IniHsPtr(pub usize); + +/// Valid index to [Server::peers] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct SessionPtr(pub usize); + +/// Valid index to [Server::biscuit_keys] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct BiscuitKeyPtr(pub usize); + +impl PeerPtr { + pub fn get<'a>(&self, srv: &'a Server) -> &'a Peer { + &srv.peers[self.0] + } + + pub fn get_mut<'a>(&self, srv: &'a mut Server) -> &'a mut Peer { + &mut srv.peers[self.0] + } + + pub fn session(&self) -> SessionPtr { + SessionPtr(self.0) + } + + pub fn hs(&self) -> IniHsPtr { + IniHsPtr(self.0) + } +} + +impl IniHsPtr { + pub fn get<'a>(&self, srv: &'a Server) -> &'a Option { + &srv.peers[self.0].handshake + } + + pub fn get_mut<'a>(&self, srv: &'a mut Server) -> &'a mut Option { + &mut srv.peers[self.0].handshake + } + + pub fn peer(&self) -> PeerPtr { + PeerPtr(self.0) + } + + pub fn insert<'a>( + &self, + srv: &'a mut Server, + hs: InitiatorHandshake, + ) -> Result<&'a mut InitiatorHandshake> { + srv.register_session(hs.core.sidi, self.peer())?; + self.take(srv); + self.peer().get_mut(srv).initiation_requested = false; + Ok(self.peer().get_mut(srv).handshake.insert(hs)) + } + + pub fn take(&self, srv: &mut Server) -> Option { + let r = self.peer().get_mut(srv).handshake.take(); + if let Some(ref stale) = r { + srv.unregister_session_if_vacant(stale.core.sidi, self.peer()); + } + r + } +} + +impl SessionPtr { + pub fn get<'a>(&self, srv: &'a Server) -> &'a Option { + &srv.peers[self.0].session + } + + pub fn get_mut<'a>(&self, srv: &'a mut Server) -> &'a mut Option { + &mut srv.peers[self.0].session + } + + pub fn peer(&self) -> PeerPtr { + PeerPtr(self.0) + } + + pub fn insert<'a>(&self, srv: &'a mut Server, ses: Session) -> Result<&'a mut Session> { + self.take(srv); + srv.register_session(ses.sidm, self.peer())?; + Ok(self.peer().get_mut(srv).session.insert(ses)) + } + + pub fn take(&self, srv: &mut Server) -> Option { + let r = self.peer().get_mut(srv).session.take(); + if let Some(ref stale) = r { + srv.unregister_session_if_vacant(stale.sidm, self.peer()); + } + r + } +} + +impl BiscuitKeyPtr { + pub fn get<'a>(&self, srv: &'a Server) -> &'a BiscuitKey { + &srv.biscuit_keys[self.0] + } + + pub fn get_mut<'a>(&self, srv: &'a mut Server) -> &'a mut BiscuitKey { + &mut srv.biscuit_keys[self.0] + } +} + +// DATABASE ////////////////////////////////////// + +impl Server { + /// Initiate a new [Server] based on a secret key (`sk`) and a public key + /// (`pk`) + pub fn new(sk: SSk, pk: SPk) -> Server { + let tb = Timebase::default(); + Server { + sskm: sk, + spkm: pk, + + // Defaults + timebase: tb, + biscuit_ctr: BiscuitId::new([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), // 1, LSB + biscuit_keys: [BiscuitKey::new(), BiscuitKey::new()], + peers: Vec::new(), + index: HashMap::new(), + peer_poll_off: 0, + } + } + + /// Iterate over the many (2) biscuit keys + pub fn biscuit_key_ptrs(&self) -> impl Iterator { + (0..self.biscuit_keys.len()).map(BiscuitKeyPtr) + } + + pub fn pidm(&self) -> Result { + Ok(Public::new( + lprf::peerid()? + .mix(self.spkm.secret())? + .into_value())) + } + + /// Iterate over all peers, starting with the `n`th peer, wrapping at the + /// end of the peers vec so that also all peers from index 0 to `n - 1` are + /// yielded + pub fn peer_ptrs_off(&self, n: usize) -> impl Iterator { + let l = self.peers.len(); + (0..l).map(move |i| PeerPtr((i + n) % l)) + } + + /// Add a peer with an optional pre shared key (`psk`) and its public key (`pk`) + pub fn add_peer(&mut self, psk: Option, pk: SPk) -> Result { + let peer = Peer { + psk: psk.unwrap_or_else(SymKey::zero), + spkt: pk, + biscuit_used: BiscuitId::zero(), + session: None, + handshake: None, + initiation_requested: false, + }; + let peerid = peer.pidt()?; + let peerno = self.peers.len(); + match self.index.entry(IndexKey::Peer(peerid)) { + Occupied(_) => bail!( + "Cannot insert peer with id {:?}; peer with this id already registered.", + peerid + ), + Vacant(e) => e.insert(peerno), + }; + self.peers.push(peer); + Ok(PeerPtr(peerno)) + } + + /// Register a new session (during a sucesfull handshake, persisting longer + /// than the handshake). Might return an error on session id collision + pub fn register_session(&mut self, id: SessionId, peer: PeerPtr) -> Result<()> { + match self.index.entry(IndexKey::Sid(id)) { + Occupied(p) if PeerPtr(*p.get()) == peer => {} // Already registered + Occupied(_) => bail!("Cannot insert session with id {:?}; id is in use.", id), + Vacant(e) => { + e.insert(peer.0); + } + }; + Ok(()) + } + + pub fn unregister_session(&mut self, id: SessionId) { + self.index.remove(&IndexKey::Sid(id)); + } + + /// Remove the given session if it is neither an active session nor in + /// handshake phase + pub fn unregister_session_if_vacant(&mut self, id: SessionId, peer: PeerPtr) { + match (peer.session().get(self), peer.hs().get(self)) { + (Some(ses), _) if ses.sidm == id => {} /* nop */ + (_, Some(hs)) if hs.core.sidi == id => {} /* nop */ + _ => self.unregister_session(id), + }; + } + + pub fn find_peer(&self, id: PeerId) -> Option { + self.index.get(&IndexKey::Peer(id)).map(|no| PeerPtr(*no)) + } + + // lookup_session in whitepaper + pub fn lookup_handshake(&self, id: SessionId) -> Option { + self.index + .get(&IndexKey::Sid(id)) // lookup the session in the index + .map(|no| IniHsPtr(*no)) // convert to peer pointer + .filter(|hsptr| { + hsptr + .get(self) // lookup in the server + .as_ref() + .map(|hs| hs.core.sidi == id) // check that handshake id matches as well + .unwrap_or(false) // it didn't match?! + }) + } + + // also lookup_session in the whitepaper + pub fn lookup_session(&self, id: SessionId) -> Option { + self.index + .get(&IndexKey::Sid(id)) + .map(|no| SessionPtr(*no)) + .filter(|sptr| { + sptr.get(self) + .as_ref() + .map(|ses| ses.sidm == id) + .unwrap_or(false) + }) + } + + /// Swap the biscuit keys, also advancing both biscuit key's mortality + pub fn active_biscuit_key(&mut self) -> BiscuitKeyPtr { + let (a, b) = (BiscuitKeyPtr(0), BiscuitKeyPtr(1)); + let (t, u) = (a.get(self).created_at, b.get(self).created_at); + + // Return the youngest but only if it's youthful + // first being returned in case of a tie + let r = if t >= u { a } else { b }; + if r.lifecycle(self) == Lifecycle::Young { + return r; + } + + // Reap the oldest biscuit key and spawn a new young one otherwise + // last one being reaped in case of a tie + let r = if t < u { a } else { b }; + let tb = self.timebase.clone(); + r.get_mut(self).randomize(&tb); + r + } +} + +impl Peer { + pub fn new(psk: SymKey, pk: SPk) -> Peer { + Peer { + psk, + spkt: pk, + biscuit_used: BiscuitId::zero(), + session: None, + handshake: None, + initiation_requested: false, + } + } + + pub fn pidt(&self) -> Result { + Ok(Public::new( + lprf::peerid()? + .mix(self.spkt.secret())? + .into_value())) + } +} + +impl Session { + pub fn zero() -> Self { + Self { + created_at: 0.0, + sidm: SessionId::zero(), + sidt: SessionId::zero(), + ck: SecretPrfTree::zero().dup(), + txkm: SymKey::zero(), + txkt: SymKey::zero(), + txnm: 0, + txnt: 0, + } + } +} + +// BISCUIT KEY /////////////////////////////////// + +/// Biscuit Keys are always randomized, so that even if through a bug some +/// secrete is encrypted with an unitialized [BiscuitKey], nobody instead of +/// everybody may read the secret. +impl BiscuitKey { + // new creates a random value, that might be counterintuitive for a Default + // impl + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + created_at: BCE, + key: SymKey::random(), + } + } + + pub fn erase(&mut self) { + self.key.randomize(); + self.created_at = BCE; + } + + pub fn randomize(&mut self, tb: &Timebase) { + self.key.randomize(); + self.created_at = tb.now(); + } +} + +// LIFECYCLE MANAGEMENT ////////////////////////// + +impl Mortal for IniHsPtr { + fn created_at(&self, srv: &Server) -> Option { + self.get(srv).as_ref().map(|hs| hs.created_at) + } + + fn retire_at(&self, srv: &Server) -> Option { + self.die_at(srv) + } + + fn die_at(&self, srv: &Server) -> Option { + self.created_at(srv).map(|t| t + REJECT_AFTER_TIME) + } +} + +impl Mortal for SessionPtr { + fn created_at(&self, srv: &Server) -> Option { + self.get(srv).as_ref().map(|p| p.created_at) + } + + fn retire_at(&self, srv: &Server) -> Option { + self.created_at(srv).map(|t| t + REKEY_AFTER_TIME) + } + + fn die_at(&self, srv: &Server) -> Option { + self.created_at(srv).map(|t| t + REJECT_AFTER_TIME) + } +} + +impl Mortal for BiscuitKeyPtr { + fn created_at(&self, srv: &Server) -> Option { + let t = self.get(srv).created_at; + if t < 0.0 { + None + } else { + Some(t) + } + } + + fn retire_at(&self, srv: &Server) -> Option { + self.created_at(srv).map(|t| t + BISCUIT_EPOCH) + } + + fn die_at(&self, srv: &Server) -> Option { + self.retire_at(srv).map(|t| t + BISCUIT_EPOCH) + } +} + +/// Trait extension to the [Mortal] Trait, that enables nicer access to timing +/// information +trait MortalExt: Mortal { + fn life_left(&self, srv: &Server) -> Option; + fn youth_left(&self, srv: &Server) -> Option; + fn lifecycle(&self, srv: &Server) -> Lifecycle; +} + +impl MortalExt for T { + fn life_left(&self, srv: &Server) -> Option { + self.die_at(srv).map(|t| t - srv.timebase.now()) + } + + fn youth_left(&self, srv: &Server) -> Option { + self.retire_at(srv).map(|t| t - srv.timebase.now()) + } + + fn lifecycle(&self, srv: &Server) -> Lifecycle { + match (self.youth_left(srv), self.life_left(srv)) { + (_, Some(t)) if has_happened(t, 0.0) => Lifecycle::Dead, + (Some(t), _) if has_happened(t, 0.0) => Lifecycle::Retired, + (Some(_), Some(_)) => Lifecycle::Young, + _ => Lifecycle::Void, + } + } +} + +// MESSAGE HANDLING ////////////////////////////// + +impl Server { + /// Initiate a new handshake, put it to the `tx_buf` __and__ to the + /// retransmission storage + // NOTE retransmission? yes if initiator, no if responder + // TODO remove unecessary copying between global tx_buf and per-peer buf + // TODO move retransmission storage to io server + pub fn initiate_handshake(&mut self, peer: PeerPtr, tx_buf: &mut [u8]) -> Result { + let mut msg = tx_buf.envelope::>()?; // Envelope::::default(); // TODO + self.handle_initiation(peer, msg.payload_mut().init_hello()?)?; + let len = self.seal_and_commit_msg(peer, MsgType::InitHello, msg)?; + peer.hs() + .store_msg_for_retransmission(self, &tx_buf[..len])?; + Ok(len) + } +} + +#[derive(Debug)] +pub struct HandleMsgResult { + pub exchanged_with: Option, + pub resp: Option, +} + +impl Server { + /// Repsond to an incoming message + /// + /// # Overview + /// + /// The response is only dependent on the incoming message, thus this + /// function is called regardless of whether the the the calling context is + /// an iniator, a responder or both. The flow of is as follows: + /// + /// 1. check incoming message for valid [MsgType] + /// 2. check that the seal is intact, e.g. that the message is + /// authenticated + /// 3. call the respective handler function for this message (for example + /// [Self::handle_init_hello]) + /// 4. if the protocol foresees a response to this message, generate one + /// 5. seal the response with cryptographic authentication + /// 6. if the response is a ResponseHello, store the sealed response for + /// further retransmision + /// 7. return some peerpointer if the exchange completed with this message + /// 8. return the length of the response generated + /// + /// This is the sequence of a succesful handshake: + /// + /// | time | Initiator | direction | Responder | + /// | --- | ---: | :---: | :--- | + /// | t0 | `InitHello` | -> | | + /// | t1 | | <- | `RespHello` | + /// | t2 | `InitConf` | -> | | + /// | t3 | | <- | `EmptyData` | + pub fn handle_msg(&mut self, rx_buf: &[u8], tx_buf: &mut [u8]) -> Result { + let seal_broken = "Message seal broken!"; + // lengt of the response. We assume no response, so None for now + let mut len = 0; + let mut exchanged = false; + + let peer = match rx_buf[0].try_into() { + Ok(MsgType::InitHello) => { + let msg_in = rx_buf.envelope::>()?; + ensure!(msg_in.check_seal(self)?, seal_broken); + + let mut msg_out = tx_buf.envelope::>()?; + let peer = self.handle_init_hello( + msg_in.payload().init_hello()?, + msg_out.payload_mut().resp_hello()?, + )?; + len = self.seal_and_commit_msg(peer, MsgType::RespHello, msg_out)?; + peer + } + Ok(MsgType::RespHello) => { + let msg_in = rx_buf.envelope::>()?; + ensure!(msg_in.check_seal(self)?, seal_broken); + + let mut msg_out = tx_buf.envelope::>()?; + let peer = self.handle_resp_hello( + msg_in.payload().resp_hello()?, + msg_out.payload_mut().init_conf()?, + )?; + len = self.seal_and_commit_msg(peer, MsgType::InitConf, msg_out)?; + peer.hs() + .store_msg_for_retransmission(self, &tx_buf[..len])?; + exchanged = true; + peer + } + Ok(MsgType::InitConf) => { + let msg_in = rx_buf.envelope::>()?; + ensure!(msg_in.check_seal(self)?, seal_broken); + + let mut msg_out = tx_buf.envelope::>()?; + let peer = self.handle_init_conf( + msg_in.payload().init_conf()?, + msg_out.payload_mut().empty_data()?, + )?; + len = self.seal_and_commit_msg(peer, MsgType::EmptyData, msg_out)?; + exchanged = true; + peer + } + Ok(MsgType::EmptyData) => { + let msg_in = rx_buf.envelope::>()?; + ensure!(msg_in.check_seal(self)?, seal_broken); + + self.handle_resp_conf(msg_in.payload().empty_data()?)? + } + Ok(MsgType::DataMsg) => bail!("DataMsg handling not implemented!"), + Ok(MsgType::CookieReply) => bail!("CookieReply handling not implemented!"), + Err(_) => { + bail!("CookieReply handling not implemented!") + } + }; + + Ok(HandleMsgResult { + exchanged_with: exchanged.then_some(peer), + resp: if len == 0 { None } else { Some(len) }, + }) + } + + /// Serialize message to `tx_buf`, generating the `mac` in the process of + /// doing so + /// + /// The message type is explicitly required here because it is very easy to + /// forget setting that, which creates subtle but far ranging errors. + pub fn seal_and_commit_msg( + &mut self, + peer: PeerPtr, + msg_type: MsgType, + mut msg: Envelope<&mut [u8], M>, + ) -> Result { + msg.msg_type_mut()[0] = msg_type as u8; + msg.seal(peer, self)?; + Ok( as LenseView>::LEN) + } +} + +// EVENT POLLING ///////////////////////////////// + +#[derive(Debug, Copy, Clone)] +pub struct Wait(Timing); + +impl Wait { + fn immediate() -> Self { + Wait(0.0) + } + + fn hibernate() -> Self { + Wait(UNENDING) + } + + fn immediate_unless(cond: bool) -> Self { + if cond { + Self::hibernate() + } else { + Self::immediate() + } + } + + fn or_hibernate(t: Option) -> Self { + match t { + Some(u) => Wait(u), + None => Wait::hibernate(), + } + } + + fn or_immediate(t: Option) -> Self { + match t { + Some(u) => Wait(u), + None => Wait::immediate(), + } + } + + fn and>(&self, o: T) -> Self { + let (a, b) = (self.0, o.into().0); + Wait(if a > b { a } else { b }) + } +} + +impl From for Wait { + fn from(t: Timing) -> Wait { + Wait(t) + } +} + +impl From> for Wait { + fn from(t: Option) -> Wait { + Wait::or_hibernate(t) + } +} + +/// Result of a poll operation, containing prescriptive action for the outer +/// event loop +#[derive(Debug, Copy, Clone)] +pub enum PollResult { + Sleep(Timing), + DeleteKey(PeerPtr), + SendInitiation(PeerPtr), + SendRetransmission(PeerPtr), +} + +impl Default for PollResult { + fn default() -> Self { + Self::hibernate() + } +} + +impl PollResult { + pub fn hibernate() -> Self { + Self::Sleep(UNENDING) // Avoid excessive sleep times (might trigger bugs on some platforms) + } + + pub fn peer(&self) -> Option { + use PollResult::*; + match *self { + DeleteKey(p) | SendInitiation(p) | SendRetransmission(p) => Some(p), + _ => None, + } + } + + pub fn fold(&self, otr: PollResult) -> PollResult { + use PollResult::*; + match (*self, otr) { + (Sleep(a), Sleep(b)) => Sleep(f64::min(a, b)), + (a, Sleep(_b)) if a.saturated() => a, + (Sleep(_a), b) if b.saturated() => b, + _ => panic!( + "Do not fold two saturated poll results! It doesn't make sense; \ + we would have to discard one of the events. \ + As soon as some result that requires an action (i.e. something other than sleep \ + is reached you should just return and have the API consumer poll again." + ), + } + } + + pub fn try_fold_with Result>(&self, f: F) -> Result { + if self.saturated() { + Ok(*self) + } else { + Ok(self.fold(f()?)) + } + } + + pub fn poll_child(&self, srv: &mut Server, p: &P) -> Result { + self.try_fold_with(|| p.poll(srv)) + } + + pub fn poll_children(&self, srv: &mut Server, iter: I) -> Result + where + P: Pollable, + I: Iterator, + { + let mut acc = *self; + for e in iter { + if acc.saturated() { + break; + } + acc = acc.fold(e.poll(srv)?); + } + Ok(acc) + } + + /// Execute `f` if it is ready, as indicated by `wait` + pub fn sched, F: FnOnce() -> PollResult>(&self, wait: W, f: F) -> PollResult { + let wait = wait.into().0; + if self.saturated() { + *self + } else if has_happened(wait, 0.0) { + self.fold(f()) + } else { + self.fold(Self::Sleep(wait)) + } + } + + pub fn try_sched, F: FnOnce() -> Result>( + &self, + wait: W, + f: F, + ) -> Result { + let wait = wait.into().0; + if self.saturated() { + Ok(*self) + } else if has_happened(wait, 0.0) { + Ok(self.fold(f()?)) + } else { + Ok(self.fold(Self::Sleep(wait))) + } + } + + pub fn ok(&self) -> Result { + Ok(*self) + } + + pub fn saturated(&self) -> bool { + use PollResult::*; + !matches!(self, Sleep(_)) + } +} + +pub fn begin_poll() -> PollResult { + PollResult::default() +} + +/// Takes a closure `f`, returns another closure which internally calls f and +/// then returns a default [PollResult] +pub fn void_poll T>(f: F) -> impl FnOnce() -> PollResult { + || { + f(); + PollResult::default() + } +} + +pub trait Pollable { + fn poll(&self, srv: &mut Server) -> Result; +} + +impl Server { + /// Implements something like [Pollable::poll] for the server, with a + /// notable difference: since `self` already is the server, the signature + /// has to be different; `self` must be a `&mut` and already is a borrow to + /// the server, eluding the need for a second arg. + pub fn poll(&mut self) -> Result { + let r = begin_poll() // Poll each biscuit and peer until an event is found + .poll_children(self, self.biscuit_key_ptrs())? + .poll_children(self, self.peer_ptrs_off(self.peer_poll_off))?; + self.peer_poll_off = match r.peer() { + Some(p) => p.0 + 1, // Event found while polling peer p; will poll peer p+1 next + None => 0, // No peer ev found. Resetting to 0 out of an irrational fear of non-zero numbers + }; + r.ok() + } +} + +impl Pollable for BiscuitKeyPtr { + fn poll(&self, srv: &mut Server) -> Result { + begin_poll() + .sched(self.life_left(srv), void_poll(|| self.get_mut(srv).erase())) // Erase stale biscuits + .ok() + } +} + +impl Pollable for PeerPtr { + fn poll(&self, srv: &mut Server) -> Result { + let (ses, hs) = (self.session(), self.hs()); + begin_poll() + .sched(hs.life_left(srv), void_poll(|| hs.take(srv))) // Silently erase old handshakes + .sched(ses.life_left(srv), || { + // Erase old sessions + ses.take(srv); + PollResult::DeleteKey(*self) + }) + // Initialize the handshake + // IF if initiation hasn't been requested (consumer of the API is free to + // ignore the request hence there is a need to do record keeping on that) + // AND after the existing session becomes stale or if there is session at all + // AND after the current handshake becomes stale or there is no handshake at all + .sched( + Wait::immediate_unless(self.get(srv).initiation_requested) + .and(Wait::or_immediate(ses.youth_left(srv))) + .and(Wait::or_immediate(hs.youth_left(srv))), + || { + self.get_mut(srv).initiation_requested = true; + PollResult::SendInitiation(*self) + }, + ) + .poll_child(srv, &hs) // Defer to the handshake for polling (retransmissions) + } +} + +impl Pollable for IniHsPtr { + fn poll(&self, srv: &mut Server) -> Result { + begin_poll().try_sched(self.retransmission_in(srv), || { + // Registering retransmission even if app does not retransmit. + // This explicitly permits applications to ignore the event. + self.register_retransmission(srv)?; + Ok(PollResult::SendRetransmission(self.peer())) + }) + } +} + +// MESSAGE RETRANSMISSION //////////////////////// + +impl Server { + pub fn retransmit_handshake(&mut self, peer: PeerPtr, tx_buf: &mut [u8]) -> Result { + peer.hs().apply_retransmission(self, tx_buf) + } +} + +impl IniHsPtr { + pub fn store_msg_for_retransmission(&self, srv: &mut Server, msg: &[u8]) -> Result<()> { + let ih = self + .get_mut(srv) + .as_mut() + .with_context(|| format!("No current handshake for peer {:?}", self.peer()))?; + cpy_min(msg, &mut *ih.tx_buf); + ih.tx_count = 0; + ih.tx_len = msg.len(); + self.register_retransmission(srv)?; + Ok(()) + } + + pub fn apply_retransmission(&self, srv: &mut Server, tx_buf: &mut [u8]) -> Result { + let ih = self + .get_mut(srv) + .as_mut() + .with_context(|| format!("No current handshake for peer {:?}", self.peer()))?; + cpy_min(&ih.tx_buf[..ih.tx_len], tx_buf); + Ok(ih.tx_len) + } + + pub fn register_retransmission(&self, srv: &mut Server) -> Result<()> { + let tb = srv.timebase.clone(); + let ih = self + .get_mut(srv) + .as_mut() + .with_context(|| format!("No current handshake for peer {:?}", self.peer()))?; + // Base delay, exponential increase, ±50% jitter + ih.tx_retry_at = tb.now() + + RETRANSMIT_DELAY_BEGIN + * RETRANSMIT_DELAY_GROWTH.powf( + (RETRANSMIT_DELAY_END / RETRANSMIT_DELAY_BEGIN) + .log(RETRANSMIT_DELAY_GROWTH) + .min(ih.tx_count as f64), + ) + * RETRANSMIT_DELAY_JITTER + * (rand_f64() + 1.0); + ih.tx_count += 1; + Ok(()) + } + + pub fn retransmission_in(&self, srv: &mut Server) -> Option { + self.get(srv) + .as_ref() + .map(|hs| hs.tx_retry_at - srv.timebase.now()) + } +} + +// CRYPTO/HANDSHAKE HANDLING ///////////////////// + +impl Envelope<&mut [u8], M> +where + M: LenseView, +{ + /// Calculate the message authentication code (`mac`) + pub fn seal(&mut self, peer: PeerPtr, srv: &Server) -> Result<()> { + let mac = lprf::mac()? + .mix(peer.get(srv).spkt.secret())? + .mix(self.until_mac())?; + self.mac_mut() + .copy_from_slice(mac.into_value()[..16].as_ref()); + Ok(()) + } +} + +impl Envelope<&[u8], M> +where + M: LenseView, +{ + /// Check the message authentication code + pub fn check_seal(&self, srv: &Server) -> Result { + let expected = lprf::mac()?.mix(srv.spkm.secret())?.mix(self.until_mac())?; + Ok(sodium_memcmp(self.mac(), &expected.into_value()[..16])) + } +} + +impl InitiatorHandshake { + pub fn zero_with_timestamp(srv: &Server) -> Self { + InitiatorHandshake { + created_at: srv.timebase.now(), + next: HandshakeStateMachine::RespHello, + core: HandshakeState::zero(), + eski: ESk::zero(), + epki: EPk::zero(), + tx_at: 0.0, + tx_retry_at: 0.0, + tx_count: 0, + tx_len: 0, + tx_buf: MsgBuf::zero(), + } + } +} + +impl HandshakeState { + pub fn zero() -> Self { + Self { + sidi: SessionId::zero(), + sidr: SessionId::zero(), + ck: SecretPrfTree::zero().dup(), + } + } + + pub fn erase(&mut self) { + self.ck = SecretPrfTree::zero().dup(); + } + + pub fn init(&mut self, spkr: &[u8]) -> Result<&mut Self> { + self.ck = lprf::ckinit()?.mix(spkr)?.into_secret_prf_tree().dup(); + Ok(self) + } + + pub fn mix(&mut self, a: &[u8]) -> Result<&mut Self> { + self.ck = self.ck.mix(&lprf::mix()?)?.mix(a)?.dup(); + Ok(self) + } + + pub fn encrypt_and_mix(&mut self, ct: &mut [u8], pt: &[u8]) -> Result<&mut Self> { + let k = self.ck.mix(&lprf::hs_enc()?)?.into_secret(); + aead_enc_into(ct, k.secret(), &NONCE0, &NOTHING, pt)?; + self.mix(ct) + } + + pub fn decrypt_and_mix(&mut self, pt: &mut [u8], ct: &[u8]) -> Result<&mut Self> { + let k = self.ck.mix(&lprf::hs_enc()?)?.into_secret(); + aead_dec_into(pt, k.secret(), &NONCE0, &NOTHING, ct)?; + self.mix(ct) + } + + // I loathe "error: constant expression depends on a generic parameter" + pub fn encaps_and_mix( + &mut self, + ct: &mut [u8], + pk: &[u8], + ) -> Result<&mut Self> { + let mut shk = Secret::::zero(); + T::encaps(shk.secret_mut(), ct, pk)?; + self.mix(pk)?.mix(shk.secret())?.mix(ct) + } + + pub fn decaps_and_mix( + &mut self, + sk: &[u8], + pk: &[u8], + ct: &[u8], + ) -> Result<&mut Self> { + let mut shk = Secret::::zero(); + T::decaps(shk.secret_mut(), sk, ct)?; + self.mix(pk)?.mix(shk.secret())?.mix(ct) + } + + pub fn store_biscuit( + &mut self, + srv: &mut Server, + peer: PeerPtr, + biscuit_ct: &mut [u8], + ) -> Result<&mut Self> { + let mut biscuit = Secret::::zero(); // pt buffer + let mut biscuit = (&mut biscuit.secret_mut()[..]).biscuit()?; // lens view + + // calculate pt contents + biscuit + .pidi_mut() + .copy_from_slice(peer.get(srv).pidt()?.as_slice()); + biscuit.biscuit_no_mut().copy_from_slice(&*srv.biscuit_ctr); + biscuit + .ck_mut() + .copy_from_slice(self.ck.clone().danger_into_secret().secret()); + + // calculate ad contents + let ad = lprf::biscuit_ad()? + .mix(srv.spkm.secret())? + .mix(self.sidi.as_slice())? + .mix(self.sidr.as_slice())? + .into_value(); + + // consume biscuit no + sodium_bigint_inc(&mut *srv.biscuit_ctr); + + // The first bit of the nonce indicates which biscuit key was used + // TODO: This is premature optimiaztion. Remove! + let bk = srv.active_biscuit_key(); + let mut n = XAEADNonce::random(); + n[0] &= 0b0111_1111; + n[0] |= (bk.0 as u8 & 0x1) << 7; + + let k = bk.get(srv).key.secret(); + let pt = biscuit.all_bytes(); + xaead_enc_into(biscuit_ct, k, &*n, &ad, pt)?; + + self.mix(biscuit_ct) + } + + /// Takes an encrypted biscuit and tries to decrypt the contained + /// information + pub fn load_biscuit( + srv: &Server, + biscuit_ct: &[u8], + sidi: SessionId, + sidr: SessionId, + ) -> Result<(PeerPtr, BiscuitId, HandshakeState)> { + // The first bit of the biscuit indicates which biscuit key was used + let bk = BiscuitKeyPtr(((biscuit_ct[0] & 0b1000_0000) >> 7) as usize); + + // Calculate addtional data fields + let ad = lprf::biscuit_ad()? + .mix(srv.spkm.secret())? + .mix(sidi.as_slice())? + .mix(sidr.as_slice())? + .into_value(); + + // Allocate and decrypt the biscuit data + let mut biscuit = Secret::::zero(); // pt buf + let mut biscuit = (&mut biscuit.secret_mut()[..]).biscuit()?; // slice + xaead_dec_into( + biscuit.all_bytes_mut(), + bk.get(srv).key.secret(), + &ad, + biscuit_ct, + )?; + + // Reconstruct the biscuit fields + let no = BiscuitId::from_slice(biscuit.biscuit_no()); + let ck = SecretPrfTree::danger_from_secret(Secret::from_slice(biscuit.ck())).dup(); + let pid = PeerId::from_slice(biscuit.pidi()); + + // Reconstruct the handshake state + let mut hs = Self { sidi, sidr, ck }; + hs.mix(biscuit_ct)?; + + // Look up the associated peer + let peer = srv + .find_peer(pid) // TODO: FindPeer should return a Result<()> + .with_context(|| format!("Could not decode biscuit for peer {pid:?}: No such peer."))?; + + // Defense against replay attacks; implementations may accept + // the most recent biscuit no again (bn = peer.bn_{prev}) which + // indicates retransmission + // TODO: Handle retransmissions without involving the crypto code + ensure!( + sodium_bigint_cmp(biscuit.biscuit_no(), &*peer.get(srv).biscuit_used) >= 0, + "Rejecting biscuit: Outdated biscuit number" + ); + + Ok((peer, no, hs)) + } + + pub fn enter_live(self, srv: &Server, role: HandshakeRole) -> Result { + let HandshakeState { ck, sidi, sidr } = self; + let tki = ck.mix(&lprf::ini_enc()?)?.into_secret(); + let tkr = ck.mix(&lprf::res_enc()?)?.into_secret(); + let created_at = srv.timebase.now(); + let (ntx, nrx) = (0, 0); + let (mysid, peersid, ktx, krx) = match role { + HandshakeRole::Initiator => (sidi, sidr, tki, tkr), + HandshakeRole::Responder => (sidr, sidi, tkr, tki), + }; + Ok(Session { + created_at, + sidm: mysid, + sidt: peersid, + ck, + txkm: ktx, + txkt: krx, + txnm: ntx, + txnt: nrx, + }) + } +} + +impl Server { + pub fn osk(&self, peer: PeerPtr) -> Result { + let session = peer + .session() + .get(self) + .as_ref() + .with_context(|| format!("No current session for peer {:?}", peer))?; + Ok(session.ck.mix(&lprf::osk()?)?.into_secret()) + } +} + +impl Server { + /// Implementation of the cryptographic protocol using the already + /// established primitives + pub fn handle_initiation( + &mut self, + peer: PeerPtr, + mut ih: InitHello<&mut [u8]>, + ) -> Result { + let mut hs = InitiatorHandshake::zero_with_timestamp(self); + + hs.core.init(peer.get(self).spkt.secret())?; // IHI1 + hs.core.sidi.randomize(); // IHI2 + ih.sidi_mut().copy_from_slice(&hs.core.sidi.value); + EKEM::keygen(hs.eski.secret_mut(), &mut *hs.epki)?; // IHI3 + ih.epki_mut().copy_from_slice(&hs.epki.value); + hs.core.mix(ih.sidi())?.mix(ih.epki())?; // IHI4 + hs.core.encaps_and_mix::( // IHI5 + ih.sctr_mut(), + peer.get(self).spkt.secret(), + )?; + hs.core // IHI6 + .encrypt_and_mix(ih.pidic_mut(), self.pidm()?.as_ref())?; + hs.core // IHI7 + .mix(self.spkm.secret())? + .mix(peer.get(self).psk.secret())?; + hs.core.encrypt_and_mix(ih.auth_mut(), &NOTHING)?; // IHI8 + + // Update the handshake hash last (not changing any state on prior error) + peer.hs().insert(self, hs)?; + + Ok(peer) + } + + pub fn handle_init_hello( + &mut self, + ih: InitHello<&[u8]>, + mut rh: RespHello<&mut [u8]>, + ) -> Result { + let mut core = HandshakeState::zero(); + + core.sidi = SessionId::from_slice(ih.sidi()); + + core.init(self.spkm.secret())?; // IHR1 + core.mix(ih.sidi())?.mix(ih.epki())?; // IHR4 + core.decaps_and_mix::( // IHR5 + self.sskm.secret(), + self.spkm.secret(), + ih.sctr(), + )?; + + let peer = { // IHR6 + let mut peerid = PeerId::zero(); + core.decrypt_and_mix(&mut *peerid, ih.pidic())?; + self.find_peer(peerid) + .with_context(|| format!("No such peer {peerid:?}."))? + }; + core.mix(peer.get(self).spkt.secret())? // IHR7 + .mix(peer.get(self).psk.secret())?; + core.decrypt_and_mix(&mut [0u8; 0], ih.auth())?; // IHR8 + + core.sidr.randomize(); // RHR1 + rh.sidi_mut().copy_from_slice(core.sidi.as_ref()); + rh.sidr_mut().copy_from_slice(core.sidr.as_ref()); + core.mix(rh.sidr())?.mix(rh.sidi())?; // RHR3 + core.encaps_and_mix::( // RHR4 + rh.ecti_mut(), ih.epki())?; + core.encaps_and_mix::( // RHR5 + rh.scti_mut(), + peer.get(self).spkt.secret(), + )?; + core.store_biscuit(self, peer, rh.biscuit_mut())?; // RHR6 + core.encrypt_and_mix(rh.auth_mut(), &NOTHING)?; // RHR7 + + Ok(peer) + } + + pub fn handle_resp_hello( + &mut self, + rh: RespHello<&[u8]>, + mut ic: InitConf<&mut [u8]>, + ) -> Result { + let peer = self // RHI2 + .lookup_handshake(SessionId::from_slice(rh.sidi())) + .with_context(|| { + format!( + "Got RespHello packet for non-existent session {:?}", + rh.sidi() + ) + })? + .peer(); + + macro_rules! hs { + () => { + peer.hs().get(self).as_ref().unwrap() + }; + } + macro_rules! hs_mut { + () => { + peer.hs().get_mut(self).as_mut().unwrap() + }; + } + + // TODO: Is this really necessary? The only possible state is "awaits resp hello"; + // no initiation created should be modeled as an Null option and a Session means + // we will not be able to find the handshake + let exp = hs!().next; + let got = HandshakeStateMachine::RespHello; + + ensure!( + exp == got, + "Unexpected package in session {:?}. Expected {:?}, got {:?}.", + SessionId::from_slice(rh.sidi()), + exp, + got + ); + + let mut core = hs!().core.clone(); + core.sidr.copy_from_slice(rh.sidr()); + + // TODO: decaps_and_mix should take Secret<> directly + // to save us from the repetitive secret unwrapping + + core.mix(rh.sidr())?.mix(rh.sidi())?; // RHI3 + core.decaps_and_mix::( // RHI4 + hs!().eski.secret(), + &*hs!().epki, + rh.ecti(), + )?; + core.decaps_and_mix::( // RHI5 + self.sskm.secret(), + self.spkm.secret(), + rh.scti(), + )?; + core.mix(rh.biscuit())?; // RHI6 + core.decrypt_and_mix(&mut [0u8; 0], rh.auth())?; // RHI7 + + // TODO: We should just authenticate the entire network package up to the auth + // tag as a pattern instead of mixing in fields separately + + ic.sidi_mut().copy_from_slice(rh.sidi()); + ic.sidr_mut().copy_from_slice(rh.sidr()); + + core.mix(ic.sidi())?.mix(ic.sidr())?; // ICI3 + ic.biscuit_mut().copy_from_slice(rh.biscuit()); + core.encrypt_and_mix(ic.auth_mut(), &NOTHING)?; // ICI4 + + // Split() – We move the secrets into the session; we do not + // delete the InitiatorHandshake, just clear it's secrets because + // we still need it for InitConf message retransmission to function. + peer.session() // ICI7 + .insert(self, core.enter_live(self, HandshakeRole::Initiator)?)?; + hs_mut!().core.erase(); + hs_mut!().next = HandshakeStateMachine::RespConf; + + Ok(peer) + } + + pub fn handle_init_conf( + &mut self, + ic: InitConf<&[u8]>, + mut rc: EmptyData<&mut [u8]>, + ) -> Result { + // (peer, bn) ← LoadBiscuit(InitConf.biscuit) + let (peer, biscuit_no, mut core) = HandshakeState::load_biscuit( // ICR1 + self, + ic.biscuit(), + SessionId::from_slice(ic.sidi()), + SessionId::from_slice(ic.sidr()), + )?; + core.encrypt_and_mix(&mut [0u8; AEAD_TAG_LEN], &NOTHING)?; // ICR2 + core.mix(ic.sidi())?.mix(ic.sidr())?; // ICR3 + core.decrypt_and_mix(&mut [0u8; 0], ic.auth())?; // ICR4 + + if sodium_bigint_cmp(&*biscuit_no, &*peer.get(self).biscuit_used) > 0 { // ICR5 + peer.get_mut(self).biscuit_used = biscuit_no; // ICR6 + peer.session() // ICR7 + .insert(self, core.enter_live(self, HandshakeRole::Responder)?)?; + } + + // TODO: Implementing RP should be possible without touching the live session stuff + + // Send ack – Implementing sending the empty acknowledgement here + // instead of a generic PeerPtr::send(&Server, Option<&[u8]>) -> Either + // because data transmission is a stub currently. This software is supposed to be used + // as a key exchange service feeding a PSK into some classical (i.e. non post quantum) + let ses = peer + .session() + .get_mut(self) + .as_mut() + .context("Cannot send acknoledgement. No session.")?; + rc.sid_mut().copy_from_slice(&ses.sidt.value); + rc.ctr_mut().copy_from_slice(&ses.txnm.to_le_bytes()); + ses.txnm += 1; // Increment nonce before encryption, just in case an error is raised + + let n = cat!(AEAD_NONCE_LEN; rc.ctr(), &[0u8; 4]); + let k = ses.txkm.secret(); + aead_enc_into(rc.auth_mut(), k, &n, &NOTHING, &NOTHING)?; // ct, k, n, ad, pt + + Ok(peer) + } + + pub fn handle_resp_conf(&mut self, rc: EmptyData<&[u8]>) -> Result { + let sid = SessionId::from_slice(rc.sid()); + let hs = self + .lookup_handshake(sid) + .with_context(|| format!("Got RespConf packet for non-existent session {sid:?}"))?; + let ses = hs.peer().session(); + + let exp = hs.get(self).as_ref().map(|h| h.next); + let got = Some(HandshakeStateMachine::RespConf); + ensure!( + exp == got, + "Unexpected package in session {:?}. Expected {:?}, got {:?}.", + sid, + exp, + got + ); + + // Validate the message + { + let s = ses.get_mut(self).as_mut().with_context(|| { + format!("Cannot validate EmptyData message. Missing encryption session for {sid:?}") + })?; + // the unwrap can not fail, because the slice returned by ctr() is + // guaranteed to have the correct size + let n = u64::from_le_bytes(rc.ctr().try_into().unwrap()); + ensure!(n >= s.txnt, "Stale nonce"); + s.txnt = n; + aead_dec_into( + // pt, k, n, ad, ct + &mut [0u8; 0], + s.txkt.secret(), + &cat!(AEAD_NONCE_LEN; rc.ctr(), &[0u8; 4]), + &NOTHING, + rc.auth(), + )?; + } + + // We can now stop retransmitting RespConf + hs.take(self); + + Ok(hs.peer()) + } +} diff --git a/src/sodium.rs b/src/sodium.rs new file mode 100644 index 0000000..57e0b9c --- /dev/null +++ b/src/sodium.rs @@ -0,0 +1,283 @@ +use crate::util::*; +use anyhow::{ensure, Result}; +use libsodium_sys as libsodium; +use log::trace; +use static_assertions::const_assert_eq; +use std::os::raw::{c_ulonglong, c_void}; +use std::ptr::{null as nullptr, null_mut as nullptr_mut}; + +pub const AEAD_TAG_LEN: usize = libsodium::crypto_aead_chacha20poly1305_IETF_ABYTES as usize; +pub const AEAD_NONCE_LEN: usize = libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize; +pub const XAEAD_TAG_LEN: usize = libsodium::crypto_aead_xchacha20poly1305_ietf_ABYTES as usize; +pub const XAEAD_NONCE_LEN: usize = libsodium::crypto_aead_xchacha20poly1305_IETF_NPUBBYTES as usize; +pub const NONCE0: [u8; libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize] = + [0u8; libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize]; +pub const NOTHING: [u8; 0] = [0u8; 0]; +pub const KEY_SIZE: usize = 32; +pub const MAC_SIZE: usize = 16; + +const_assert_eq!( + KEY_SIZE, + libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize +); +const_assert_eq!(KEY_SIZE, libsodium::crypto_generichash_BYTES as usize); + +macro_rules! sodium_call { + ($name:ident, $($args:expr),*) => { attempt!({ + ensure!(unsafe{libsodium::$name($($args),*)} > -1, + "Error in libsodium's {}.", stringify!($name)); + Ok(()) + })}; + ($name:ident) => { sodium_call!($name, ) }; +} + +#[inline] +pub fn sodium_init() -> Result<()> { + trace!("initializing libsodium"); + sodium_call!(sodium_init) +} + +#[inline] +pub fn sodium_memcmp(a: &[u8], b: &[u8]) -> bool { + a.len() == b.len() + && unsafe { + let r = libsodium::sodium_memcmp( + a.as_ptr() as *const c_void, + b.as_ptr() as *const c_void, + a.len(), + ); + r == 0 + } +} + +#[inline] +pub fn sodium_bigint_cmp(a: &[u8], b: &[u8]) -> i32 { + assert!(a.len() == b.len()); + unsafe { libsodium::sodium_compare(a.as_ptr(), b.as_ptr(), a.len()) } +} + +#[inline] +pub fn sodium_bigint_inc(v: &mut [u8]) { + unsafe { + libsodium::sodium_increment(v.as_mut_ptr(), v.len()); + } +} + +#[inline] +pub fn rng(buf: &mut [u8]) { + unsafe { libsodium::randombytes_buf(buf.as_mut_ptr() as *mut c_void, buf.len()) }; +} + +#[inline] +pub fn zeroize(buf: &mut [u8]) { + unsafe { libsodium::sodium_memzero(buf.as_mut_ptr() as *mut c_void, buf.len()) }; +} + +#[inline] +pub fn aead_enc_into( + ciphertext: &mut [u8], + key: &[u8], + nonce: &[u8], + ad: &[u8], + plaintext: &[u8], +) -> Result<()> { + assert!(ciphertext.len() == plaintext.len() + AEAD_TAG_LEN); + assert!(key.len() == libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize); + assert!(nonce.len() == libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize); + let mut clen: u64 = 0; + sodium_call!( + crypto_aead_chacha20poly1305_ietf_encrypt, + ciphertext.as_mut_ptr(), + &mut clen, + plaintext.as_ptr(), + plaintext.len() as c_ulonglong, + ad.as_ptr(), + ad.len() as c_ulonglong, + nullptr(), // nsec is not used + nonce.as_ptr(), + key.as_ptr() + )?; + assert!(clen as usize == ciphertext.len()); + Ok(()) +} + +#[inline] +pub fn aead_dec_into( + plaintext: &mut [u8], + key: &[u8], + nonce: &[u8], + ad: &[u8], + ciphertext: &[u8], +) -> Result<()> { + assert!(ciphertext.len() == plaintext.len() + AEAD_TAG_LEN); + assert!(key.len() == libsodium::crypto_aead_chacha20poly1305_IETF_KEYBYTES as usize); + assert!(nonce.len() == libsodium::crypto_aead_chacha20poly1305_IETF_NPUBBYTES as usize); + let mut mlen: u64 = 0; + sodium_call!( + crypto_aead_chacha20poly1305_ietf_decrypt, + plaintext.as_mut_ptr(), + &mut mlen as *mut c_ulonglong, + nullptr_mut(), // nsec is not used + ciphertext.as_ptr(), + ciphertext.len() as c_ulonglong, + ad.as_ptr(), + ad.len() as c_ulonglong, + nonce.as_ptr(), + key.as_ptr() + )?; + assert!(mlen as usize == plaintext.len()); + Ok(()) +} + +#[inline] +pub fn xaead_enc_into( + ciphertext: &mut [u8], + key: &[u8], + nonce: &[u8], + ad: &[u8], + plaintext: &[u8], +) -> Result<()> { + assert!(ciphertext.len() == plaintext.len() + XAEAD_NONCE_LEN + XAEAD_TAG_LEN); + assert!(key.len() == libsodium::crypto_aead_xchacha20poly1305_IETF_KEYBYTES as usize); + let (n, ct) = ciphertext.split_at_mut(XAEAD_NONCE_LEN); + n.copy_from_slice(nonce); + let mut clen: u64 = 0; + sodium_call!( + crypto_aead_xchacha20poly1305_ietf_encrypt, + ct.as_mut_ptr(), + &mut clen, + plaintext.as_ptr(), + plaintext.len() as c_ulonglong, + ad.as_ptr(), + ad.len() as c_ulonglong, + nullptr(), // nsec is not used + nonce.as_ptr(), + key.as_ptr() + )?; + assert!(clen as usize == ct.len()); + Ok(()) +} + +#[inline] +pub fn xaead_dec_into( + plaintext: &mut [u8], + key: &[u8], + ad: &[u8], + ciphertext: &[u8], +) -> Result<()> { + assert!(ciphertext.len() == plaintext.len() + XAEAD_NONCE_LEN + XAEAD_TAG_LEN); + assert!(key.len() == libsodium::crypto_aead_xchacha20poly1305_IETF_KEYBYTES as usize); + let (n, ct) = ciphertext.split_at(XAEAD_NONCE_LEN); + let mut mlen: u64 = 0; + sodium_call!( + crypto_aead_xchacha20poly1305_ietf_decrypt, + plaintext.as_mut_ptr(), + &mut mlen as *mut c_ulonglong, + nullptr_mut(), // nsec is not used + ct.as_ptr(), + ct.len() as c_ulonglong, + ad.as_ptr(), + ad.len() as c_ulonglong, + n.as_ptr(), + key.as_ptr() + )?; + assert!(mlen as usize == plaintext.len()); + Ok(()) +} + +#[inline] +fn blake2b_flexible(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> { + const KEY_MIN: usize = libsodium::crypto_generichash_KEYBYTES_MIN as usize; + const KEY_MAX: usize = libsodium::crypto_generichash_KEYBYTES_MAX as usize; + const OUT_MIN: usize = libsodium::crypto_generichash_BYTES_MIN as usize; + const OUT_MAX: usize = libsodium::crypto_generichash_BYTES_MAX as usize; + assert!(key.is_empty() || (KEY_MIN <= key.len() && key.len() <= KEY_MAX)); + assert!(OUT_MIN <= out.len() && out.len() <= OUT_MAX); + let kptr = match key.len() { + // NULL key + 0 => nullptr(), + _ => key.as_ptr(), + }; + sodium_call!( + crypto_generichash_blake2b, + out.as_mut_ptr(), + out.len(), + data.as_ptr(), + data.len() as c_ulonglong, + kptr, + key.len() + ) +} + +// TODO: Use proper streaming hash; for mix_hash too. +#[inline] +pub fn hash_into(out: &mut [u8], data: &[u8]) -> Result<()> { + assert!(out.len() == KEY_SIZE); + blake2b_flexible(out, &NOTHING, data) +} + +#[inline] +pub fn hash(data: &[u8]) -> Result<[u8; KEY_SIZE]> { + let mut r = [0u8; KEY_SIZE]; + hash_into(&mut r, data)?; + Ok(r) +} + +#[inline] +pub fn mac_into(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> { + assert!(out.len() == KEY_SIZE); + assert!(key.len() == KEY_SIZE); + blake2b_flexible(out, key, data) +} + +#[inline] +pub fn mac(key: &[u8], data: &[u8]) -> Result<[u8; KEY_SIZE]> { + let mut r = [0u8; KEY_SIZE]; + mac_into(&mut r, key, data)?; + Ok(r) +} + +#[inline] +pub fn mac16(key: &[u8], data: &[u8]) -> Result<[u8; 16]> { + assert!(key.len() == KEY_SIZE); + let mut out = [0u8; 16]; + blake2b_flexible(&mut out, key, data)?; + Ok(out) +} + +#[inline] +pub fn hmac_into(out: &mut [u8], key: &[u8], data: &[u8]) -> Result<()> { + // Not bothering with padding; the implementation + // uses appropriately sized keys. + ensure!(key.len() == KEY_SIZE); + + const IPAD: [u8; KEY_SIZE] = [0x36u8; KEY_SIZE]; + let mut temp_key = [0u8; KEY_SIZE]; + temp_key.copy_from_slice(key); + xor_into(&mut temp_key, &IPAD); + let outer_data = mac(&temp_key, data)?; + + const OPAD: [u8; KEY_SIZE] = [0x5Cu8; KEY_SIZE]; + temp_key.copy_from_slice(key); + xor_into(&mut temp_key, &OPAD); + mac_into(out, &temp_key, &outer_data) +} + +#[inline] +pub fn hmac(key: &[u8], data: &[u8]) -> Result<[u8; KEY_SIZE]> { + let mut r = [0u8; KEY_SIZE]; + hmac_into(&mut r, key, data)?; + Ok(r) +} + +// Choose a fully random u64 +pub fn rand_u64() -> u64 { + let mut buf = [0u8; 8]; + rng(&mut buf); + u64::from_le_bytes(buf) +} + +// Choose a random f64 in [0; 1] inclusive; quick and dirty +pub fn rand_f64() -> f64 { + (rand_u64() as f64) / (u64::MAX as f64) +} diff --git a/src/usage.md b/src/usage.md new file mode 100644 index 0000000..c3528dd --- /dev/null +++ b/src/usage.md @@ -0,0 +1,51 @@ +NAME + + {0} – Perform post-quantum secure key exchanges for wireguard and other services. + +SYNOPSIS + + {0} [ COMMAND ] [ OPTIONS ]... [ ARGS ]... + +DESCRIPTION + {0} performs cryptographic key exchanges that are secure against quantum-computers and outputs the keys. + These keys can then be passed to various services such as wireguard or other vpn services + as pre-shared-keys to achieve security against attackers with quantum computers. + + This is a research project and quantum computers are not thought to become practical in less than ten years. + If you are not specifically tasked with developing post-quantum secure systems, you probably do not need this tool. + +COMMANDS + + keygen private-key public-key + Generate a keypair to use in the exchange command later. Send the public-key file to your communication partner + and keep the private-key file a secret! + + exchange private-key public-key [ OPTIONS ]... PEER...\n" + Start a process to exchange keys with the specified peers. You should specify at least one peer. + + OPTIONS + listen [:] + Instructs {0} to listen on the specified interface and port. By default {0} will listen on all interfaces and select a random port. + + verbose + Extra logging + + PEER := peer public-key [endpoint [:]] [preshared-key ] [outfile ] [wireguard ] + Instructs {0} to exchange keys with the given peer and write the resulting PSK into the given output file. + You must either specify the outfile or wireguard output option. + + endpoint [:] + Specifies the address where the peer can be reached. This will be automatically updated after the first sucessfull + key exchange with the peer. If this is unspecified, the peer must initiate the connection. + + preshared-key + You may specifie a pre-shared key which will be mixied into the final secret. + + outfile + You may specify a file to write the exchanged keys to. If this option is specified, {0} will + write a notification to standard out every time the key is updated. + + wireguard + This allows you to directly specify a wireguard peer to deploy the pre-shared-key to. + You may specify extra parameters you would pass to `wg set` besides the preshared-key parameter which is used by {0}. + This makes it possible to add peers entirely from {0}. diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..f129fed --- /dev/null +++ b/src/util.rs @@ -0,0 +1,123 @@ +use base64::{ + display::Base64Display as B64Display, read::DecoderReader as B64Reader, + write::EncoderWriter as B64Writer, +}; +use std::{ + borrow::{Borrow, BorrowMut}, + cmp::min, + io::{Read, Write}, + time::{Duration, Instant}, +}; + +#[inline] +pub fn xor_into(a: &mut [u8], b: &[u8]) { + assert!(a.len() == b.len()); + for (av, bv) in a.iter_mut().zip(b.iter()) { + *av ^= *bv; + } +} + +// TODO: Zeroize result? +/** Concatenate two byte arrays */ +#[macro_export] +macro_rules! cat { + ($len:expr; $($toks:expr),+) => {{ + let mut buf = [0u8; $len]; + let mut off = 0; + $({ + let tok = $toks; + let tr = ::std::borrow::Borrow::<[u8]>::borrow(tok); + (&mut buf[off..(off + tr.len())]).copy_from_slice(tr); + off += tr.len(); + })+ + assert!(off == buf.len(), "Size mismatch in cat!()"); + buf + }} +} + +// TODO: consistent inout ordering +pub fn cpy + ?Sized, F: Borrow<[u8]> + ?Sized>(src: &F, dst: &mut T) { + dst.borrow_mut().copy_from_slice(src.borrow()); +} + +pub fn cpy_min + ?Sized, F: Borrow<[u8]> + ?Sized>(src: &F, to: &mut T) { + let src = src.borrow(); + let dst = to.borrow_mut(); + let len = min(src.len(), dst.len()); + dst[..len].copy_from_slice(&src[..len]); +} + +/// Try block basically…returns a result and allows the use of the question mark operator inside +#[macro_export] +macro_rules! attempt { + ($block:expr) => { + (|| -> ::anyhow::Result<_> { $block })() + }; +} + +const B64TYPE: base64::Config = base64::STANDARD; + +pub fn fmt_b64<'a>(payload: &'a [u8]) -> B64Display<'a> { + B64Display::<'a>::with_config(payload, B64TYPE) +} + +pub fn b64_writer(w: W) -> B64Writer { + B64Writer::new(w, B64TYPE) +} + +pub fn b64_reader(r: &mut R) -> B64Reader<'_, R> { + B64Reader::new(r, B64TYPE) +} + +// TODO remove this once std::cmp::max becomes const +pub const fn max_usize(a: usize, b: usize) -> usize { + if a > b { + a + } else { + b + } +} + +#[derive(Clone, Debug)] +pub struct Timebase(Instant); + +impl Default for Timebase { + fn default() -> Self { + Self(Instant::now()) + } +} + +impl Timebase { + pub fn now(&self) -> f64 { + self.0.elapsed().as_secs_f64() + } + + pub fn dur(&self, t: f64) -> Duration { + Duration::from_secs_f64(t) + } +} + +#[macro_export] +macro_rules! multimatch { + ($val:expr) => {{ () }}; + ($val:expr, $($p:pat => $thn:expr),*) => {{ + let v = $val; + ($(if let $p = v { Some($thn) } else { None }),*) + }}; +} + +pub fn mutating(mut v: T, f: F) -> T +where + F: Fn(&mut T), +{ + f(&mut v); + v +} + +pub fn sideeffect(v: T, f: F) -> T +where + F: Fn(&T), +{ + f(&v); + v +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..32a90d3 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,120 @@ +use std::{fs, net::UdpSocket, path::PathBuf, process::Stdio, time::Duration}; + +const BIN: &str = "rosenpass"; + +// check that we can generate keys +#[test] +fn generate_keys() { + let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("keygen"); + fs::create_dir_all(&tmpdir).unwrap(); + + let priv_key_path = tmpdir.join("private-key"); + let pub_key_path = tmpdir.join("public-key"); + + let output = test_bin::get_test_bin(BIN) + .args(["keygen", "private-key"]) + .arg(&priv_key_path) + .arg("public-key") + .arg(&pub_key_path) + .output() + .expect("Failed to start {BIN}"); + + assert_eq!(String::from_utf8_lossy(&output.stdout), ""); + + assert!(priv_key_path.is_file()); + assert!(pub_key_path.is_file()); + + // cleanup + fs::remove_dir_all(&tmpdir).unwrap(); +} + +fn find_udp_socket() -> u16 { + for port in 1025..=u16::MAX { + match UdpSocket::bind(("127.0.0.1", port)) { + Ok(_) => { + return port; + } + _ => {} + } + } + panic!("no free UDP port found"); +} + +// check that we can exchange keys +#[test] +fn check_exchange() { + let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange"); + fs::create_dir_all(&tmpdir).unwrap(); + + let priv_key_paths = [tmpdir.join("private-key-0"), tmpdir.join("private-key-1")]; + let pub_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")]; + let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")]; + + // generate key pairs + for (priv_key_path, pub_key_path) in priv_key_paths.iter().zip(pub_key_paths.iter()) { + let output = test_bin::get_test_bin(BIN) + .args(["keygen", "private-key"]) + .arg(&priv_key_path) + .arg("public-key") + .arg(&pub_key_path) + .output() + .expect("Failed to start {BIN}"); + + assert_eq!(String::from_utf8_lossy(&output.stdout), ""); + assert!(priv_key_path.is_file()); + assert!(pub_key_path.is_file()); + } + + // start first process, the server + let port = find_udp_socket(); + let listen_addr = format!("localhost:{port}"); + let mut server = test_bin::get_test_bin(BIN) + .args(["exchange", "private-key"]) + .arg(&priv_key_paths[0]) + .arg("public-key") + .arg(&pub_key_paths[0]) + .args(["listen", &listen_addr, "verbose", "peer", "public-key"]) + .arg(&pub_key_paths[1]) + .arg("outfile") + .arg(&shared_key_paths[0]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start {BIN}"); + + // start second process, the client + let mut client = test_bin::get_test_bin(BIN) + .args(["exchange", "private-key"]) + .arg(&priv_key_paths[1]) + .arg("public-key") + .arg(&pub_key_paths[1]) + .args(["verbose", "peer", "public-key"]) + .arg(&pub_key_paths[0]) + .args(["endpoint", &listen_addr]) + .arg("outfile") + .arg(&shared_key_paths[1]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start {BIN}"); + + // give them some time to do the key exchange + std::thread::sleep(Duration::from_secs(2)); + + // time's up, kill the childs + server.kill().unwrap(); + client.kill().unwrap(); + + // read the shared keys they created + let shared_keys: Vec<_> = shared_key_paths + .iter() + .map(|p| fs::read_to_string(p).unwrap()) + .collect(); + + // check that they created two equal keys + assert_eq!(shared_keys.len(), 2); + assert_eq!(shared_keys[0], shared_keys[1]); + + // cleanup + fs::remove_dir_all(&tmpdir).unwrap(); +}