diff --git a/.cargo/config.toml b/.cargo/config.toml index 4631d84..e949dcc 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,7 +4,8 @@ build-esp32c2 = "build --release --target riscv32imc-unknown-none-elf --features build-esp32c3 = "build --release --target riscv32imc-unknown-none-elf --features esp32c3" # Available but not supported by esp-hal (yet). #build-esp32c5 = "build --release --target riscv32imac-unknown-none-elf --features esp32c5" -build-esp32c6 = "build --release --target riscv32imac-unknown-none-elf --features esp32c6" +#build-esp32c6 = "build --release --target riscv32imac-unknown-none-elf --features esp32c6" +build-esp32c6 = "build --target riscv32imac-unknown-none-elf --features esp32c6" build-esp32s2 = "build --profile esp32s2 --target xtensa-esp32s2-none-elf --features esp32s2" build-esp32s3 = "build --release --target xtensa-esp32s3-none-elf --features esp32s3" @@ -13,7 +14,8 @@ run-esp32c2 = "run --release --target riscv32imc-unknown-none-elf --features esp run-esp32c3 = "run --release --target riscv32imc-unknown-none-elf --features esp32c3" # Available but not supported by esp-hal (yet). #run-esp32c5 = "run --release --target riscv32imac-unknown-none-elf --features esp32c5" -run-esp32c6 = "run --release --target riscv32imac-unknown-none-elf --features esp32c6" +#run-esp32c6 = "run --release --target riscv32imac-unknown-none-elf --features esp32c6" +run-esp32c6 = "run --target riscv32imac-unknown-none-elf --features esp32c6" run-esp32s2 = "run --profile esp32s2 --target xtensa-esp32s2-none-elf --features esp32s2" run-esp32s3 = "run --release --target xtensa-esp32s3-none-elf --features esp32s3" @@ -24,7 +26,8 @@ rustflags = ["-C", "link-arg=-nostartfiles", '--cfg=feature="esp32"'] runner = "espflash flash --baud=921600 --monitor" rustflags = [ "-C", "force-frame-pointers"] [target.riscv32imac-unknown-none-elf] -runner = "espflash flash --baud=921600 --monitor" +runner = "espflash flash --baud=921600 --partition-table partitions.csv --monitor" +#runner = "espflash flash --baud=921600 --monitor" rustflags = [ "-C", "force-frame-pointers"] [target.xtensa-esp32s2-none-elf] runner = "espflash flash --baud=921600 --monitor --chip esp32s2" @@ -33,10 +36,19 @@ rustflags = ["-C", "link-arg=-nostartfiles", '--cfg=feature="esp32s2"'] runner = "espflash flash --baud=921600 --monitor --chip esp32s3" rustflags = ["-C", "link-arg=-nostartfiles", '--cfg=feature="esp32s3"'] - +# https://docs.espressif.com/projects/rust/esp-hal/1.0.0-beta.1/esp32c6/esp_hal/index.html#additional-configuration [env] -ESP_LOG="INFO" +ESP_LOG = "INFO" +#ESP_HAL_CONFIG_PLACE_SWITCH_TABLES_IN_RAM=true +#ESP_HAL_CONFIG_PLACE_ANON_IN_RAM=false +#ESP_HAL_CONFIG_FLIP_LINK=false +#ESP_HAL_CONFIG_STACK_GUARD_OFFSET=4096 +#ESP_HAL_CONFIG_STACK_GUARD_VALUE=3740121773 +#ESP_HAL_CONFIG_IMPL_CRITICAL_SECTION=true + + +[build] target = "riscv32imac-unknown-none-elf" [unstable] diff --git a/.gitignore b/.gitignore index 0dd24c9..3868ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ target/ # VSCode workspace(s) *.code-workspace + +# Temporarily ignore book from this branch +docs/book diff --git a/.vscode/settings.json b/.vscode/settings.json index 979d509..f9a1f42 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { "rust-analyzer.check.allTargets": false, + // "rust-analyzer.cargo.features": [ + // "esp32c6" + // ] } diff --git a/Cargo.lock b/Cargo.lock index 5076662..58e1342 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,15 +49,21 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "basic-toml" @@ -68,6 +74,17 @@ dependencies = [ "serde", ] +[[package]] +name = "bcrypt" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" +dependencies = [ + "base64", + "blowfish", + "subtle", +] + [[package]] name = "bitfield" version = "0.19.1" @@ -96,9 +113,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -109,6 +126,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -117,9 +144,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -138,9 +165,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chacha20" @@ -204,6 +231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -218,9 +246,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.3" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "373b7c5dbd637569a2cca66e8d66b8c446a1e7bf064ea321d265d7b3dfe7c97e" dependencies = [ "cfg-if", "cpufeatures", @@ -280,9 +308,9 @@ dependencies = [ [[package]] name = "delegate" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b6483c2bbed26f97861cf57651d4f2b731964a28cd2257f934a4b452480d21" +checksum = "6178a82cf56c836a3ba61a7935cdb1c49bfaa6fa4327cd5bf554a503087de26b" dependencies = [ "proc-macro2", "quote", @@ -320,9 +348,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", @@ -510,9 +538,12 @@ dependencies = [ [[package]] name = "embassy-usb-driver" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc247028eae04174b6635104a35b1ed336aabef4654f5e87a8f32327d231970" +checksum = "340c5ce591ef58c6449e43f51d2c53efe1bf0bb6a40cbf80afa0d259c7d52c76" +dependencies = [ + "embedded-io-async", +] [[package]] name = "embassy-usb-synopsys-otg" @@ -622,18 +653,18 @@ dependencies = [ [[package]] name = "enumset" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a6b7c3d347de0a9f7bfd2f853be43fe32fa6fac30c70f6d6d67a1e936b87ee" +checksum = "d6ee17054f550fd7400e1906e2f9356c7672643ed34008a9e8abe147ccd2d821" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6da3ea9e1d1a3b1593e15781f930120e72aa7501610b2f82e5b6739c72e8eac5" +checksum = "76d07902c93376f1e96c34abc4d507c0911df3816cef50b01f5a2ff3ad8c370d" dependencies = [ "darling", "proc-macro2", @@ -720,7 +751,7 @@ checksum = "0d973697621cd3eef9c3f260fa8c1af77d8547cfc92734255d8e8ddf05c7d331" dependencies = [ "basic-toml", "bitfield", - "bitflags 2.9.0", + "bitflags 2.9.1", "bytemuck", "cfg-if", "critical-section", @@ -842,6 +873,19 @@ dependencies = [ "riscv-rt-macros", ] +[[package]] +name = "esp-storage" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b6502654e5750e8162e66512d7e8d84f7eb51d17ec6aa7e0da95cd102769425" +dependencies = [ + "critical-section", + "document-features", + "embedded-storage", + "esp-build", + "esp-metadata", +] + [[package]] name = "esp-synopsys-usb-otg" version = "0.4.2" @@ -954,9 +998,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "fnv" @@ -1021,9 +1065,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -1041,9 +1085,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heapless" @@ -1108,9 +1152,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", @@ -1162,9 +1206,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "linked_list_allocator" @@ -1205,15 +1249,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "minijinja" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98642a6dfca91122779a307b77cd07a4aa951fbe32232aaf5bad9febc66be754" +checksum = "4e60ac08614cc09062820e51d5d94c2fce16b94ea4e5003bb81b99a95f84e876" dependencies = [ "serde", ] @@ -1255,18 +1299,19 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro2", "quote", @@ -1334,9 +1379,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable_atomic_enum" @@ -1493,9 +1538,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -1549,18 +1594,18 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -1594,18 +1639,18 @@ dependencies = [ [[package]] name = "snafu" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +checksum = "320b01e011bf8d5d7a4a4a4be966d9160968935849c83b918827f6a435e7f627" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" dependencies = [ "heck", "proc-macro2", @@ -1654,34 +1699,47 @@ dependencies = [ name = "ssh-stamp" version = "0.1.0" dependencies = [ + "bcrypt", "cfg-if", + "digest", "ed25519-dalek", "edge-dhcp", "edge-nal", "edge-nal-embassy", + "embassy-embedded-hal", "embassy-executor", "embassy-futures", "embassy-net", "embassy-sync 0.7.0", "embassy-time", "embedded-io-async", + "embedded-storage", + "embedded-storage-async", "esp-alloc", "esp-backtrace", "esp-bootloader-esp-idf", "esp-hal", "esp-hal-embassy", "esp-println", + "esp-storage", "esp-wifi", "getrandom", "heapless", "hex", + "hmac", "log", + "paste", "portable-atomic", + "pretty-hex", + "sha2", "smoltcp", + "snafu", "ssh-key", "static_cell", + "subtle", "sunset", "sunset-async", + "sunset-sshwire-derive", ] [[package]] @@ -1692,9 +1750,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_cell" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89b0684884a883431282db1e4343f34afc2ff6996fe1f4a1664519b66e14c1e" +checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23" dependencies = [ "portable-atomic", ] @@ -1735,8 +1793,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sunset" -version = "0.2.0" -source = "git+https://github.com/mkj/sunset?rev=cb6c720#cb6c72008d332d6a066dbcc0b1712bacfb07ab0e" +version = "0.3.0" dependencies = [ "aes", "ascii", @@ -1766,14 +1823,12 @@ dependencies = [ [[package]] name = "sunset-async" -version = "0.2.0" -source = "git+https://github.com/mkj/sunset?rev=cb6c720#cb6c72008d332d6a066dbcc0b1712bacfb07ab0e" +version = "0.3.0" dependencies = [ "embassy-futures", "embassy-sync 0.7.0", "embedded-io-async", "log", - "pin-utils", "portable-atomic", "sunset", ] @@ -1781,16 +1836,15 @@ dependencies = [ [[package]] name = "sunset-sshwire-derive" version = "0.2.0" -source = "git+https://github.com/mkj/sunset?rev=cb6c720#cb6c72008d332d6a066dbcc0b1712bacfb07ab0e" dependencies = [ "virtue", ] [[package]] name = "syn" -version = "2.0.100" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -1808,9 +1862,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -1820,26 +1874,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "typenum" version = "1.18.0" @@ -1904,9 +1965,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" @@ -2109,9 +2170,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 57c0d22..80125fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,3 @@ -# TODO: Potentially switch to cargo_xtask for easy re-targetting, i.e: -# https://github.com/card-io-ecg/card-io-fw/blob/main/Cargo.toml - [package] name = "ssh-stamp" version = "0.1.0" @@ -30,41 +27,64 @@ hex = { version = "0.4", default-features = false } log = { version = "0.4" } static_cell = { version = "2", features = ["nightly"] } ssh-key = { version = "0.6", default-features = false, features = ["ed25519"] } +# sunset = { git = "https://github.com/mkj/sunset", rev = "5ff44bb", default-features = false, features = ["openssh-key", "embedded-io"]} +# sunset-async = { git = "https://github.com/mkj/sunset", rev = "5ff44bb", default-features = false, features = ["multi-thread"]} +# sunset-sshwire-derive = { git = "https://github.com/mkj/sunset", rev = "5ff44bb", default-features = false } +sunset = { path = "../sunset", default-features = false, features = ["openssh-key", "embedded-io"]} +sunset-async = { path = "../sunset/async", default-features = false, features = ["multi-thread"]} +sunset-sshwire-derive = { path = "../sunset/sshwire-derive", default-features = false } getrandom = { version = "0.2.10", features = ["custom"] } -sunset = { git="https://github.com/mkj/sunset", rev = "cb6c720", default-features = false, features = ["openssh-key", "embedded-io"]} -sunset-async = { git = "https://github.com/mkj/sunset", rev = "cb6c720", default-features = false} embassy-sync = "0.7" heapless = "0.8" embassy-futures = "0.1" edge-dhcp = "0.6" edge-nal = "0.5" edge-nal-embassy = "0.6" +#sequential-storage = { version = "4", features = ["heapless"] } +esp-storage = { version = "0.6" } +embedded-storage = "0.3.1" +embassy-embedded-hal = "0.3" +bcrypt = { version = "0.17", default-features = false } +subtle = { version = "2", default-features = false } +hmac = { version = "0.12", default-features = false } +sha2 = { version = "0.10", default-features = false } +digest = { version = "0.10", default-features = false, features = ["rand_core", "subtle"] } +embedded-storage-async = "0.4" portable-atomic = "1" esp-bootloader-esp-idf = "0.1" +snafu = { version = "0.8.6", default-features = false } +paste = "1" +pretty-hex = { version = "0.4", default-features = false } [profile.dev] # Rust debug is too slow. # For debug builds always builds with some optimization -opt-level = 3 +opt-level = 0 +debug = 2 +debug-assertions = true [profile.release] codegen-units = 1 # LLVM can perform better optimizations using a single thread debug = 2 -debug-assertions = false +debug-assertions = true incremental = false lto = 'fat' opt-level = 3 -overflow-checks = false +overflow-checks = true [profile.esp32s2] inherits = "release" opt-level = "s" # Optimize for size. -overflow-checks = false -lto = 'fat' -[features] -#default = ["esp32c6"] +[profile.dev.package.esp-storage] +opt-level = "s" + +[profile.dev.package.esp-wifi] +opt-level = "s" +[features] +ipv6 = [] +default = ["esp32c6"] # MCU options esp32 = [ "esp-hal/esp32", @@ -72,6 +92,7 @@ esp32 = [ "esp-wifi/esp32", "esp-hal-embassy/esp32", "esp-println/esp32", + "esp-storage/esp32", "embassy-executor/task-arena-size-40960", ] esp32c2 = [ @@ -80,6 +101,7 @@ esp32c2 = [ "esp-wifi/esp32c2", "esp-hal-embassy/esp32c2", "esp-println/esp32c2", + "esp-storage/esp32c2", "embassy-executor/task-arena-size-40960", ] esp32c3 = [ @@ -88,6 +110,7 @@ esp32c3 = [ "esp-wifi/esp32c3", "esp-hal-embassy/esp32c3", "esp-println/esp32c3", + "esp-storage/esp32c3", "embassy-executor/task-arena-size-40960", ] #esp32c5 = [ @@ -96,6 +119,7 @@ esp32c3 = [ # "esp-wifi/esp32c5", # "esp-hal-embassy/esp32c5", # "esp-println/esp32c5", +# "esp-storage/esp32c5", # "embassy-executor/task-arena-size-40960", #] esp32c6 = [ @@ -104,6 +128,7 @@ esp32c6 = [ "esp-wifi/esp32c6", "esp-hal-embassy/esp32c6", "esp-println/esp32c6", + "esp-storage/esp32c6", "embassy-executor/task-arena-size-40960", ] esp32s2 = [ @@ -112,6 +137,7 @@ esp32s2 = [ "esp-wifi/esp32s2", "esp-hal-embassy/esp32s2", "esp-println/esp32s2", + "esp-storage/esp32s2", "embassy-executor/task-arena-size-32768", ] esp32s3 = [ @@ -120,5 +146,6 @@ esp32s3 = [ "esp-wifi/esp32s3", "esp-hal-embassy/esp32s3", "esp-println/esp32s3", + "esp-storage/esp32s3", "embassy-executor/task-arena-size-40960", ] diff --git a/docs/nlnet/mou.md b/docs/nlnet/mou.md new file mode 100644 index 0000000..38bc0ea --- /dev/null +++ b/docs/nlnet/mou.md @@ -0,0 +1,369 @@ +takentaal v1.0 + + + +# SSH Stamp + + + +The aim of this project (available at https://github.com/brainstorm/ssh-stamp) is to write software that executes code on a microprocessor and allows transferring data between the internet and various electronic interfaces. In other words: a logic "bridge" between the internet and low level electronic interfaces. + + + +In its conception, the SSH Stamp is a secure bridge between wireless and a serial port (ubiquitous interface also known as "UART"), implemented in the Rust programming language. The Rust programming language offers convenient memory safety guarantees that are meaningful for this project from a systems security perspective. + + + +From a practical standpoint, SSH Stamp allows its users (from different levels of expertise) to audit, monitor, defend, operate, understand and maintain a variety of low level electronics remotely (and securely) through the internet at large. + + + +## {240} Password-based authentication (GH issue #20) + + + +Currently, SSH Stamp accepts ANY arbitrary password when logging in via the SSH protocol. This is a well known and big security issue that requires attention to be put on the password authentication handler. + + + +On the server side, there should be a persistent setting that completely disables password-based authentication (only accepting safer key-based authentication method (see related GH issues #21 and #23)). + + + +- {120} Implement password authentication server-side toggle. + +- {120} Implement handler business logic and connect it with GH issue #23. + + + +## {400} Have the server generate a unique server keypair and store it persistently (on boot) (GH issue #38) + + + +Currently, SSH Stamp accepts any (hardcoded) private key, as implied in the top-level documentation. This is a well known and big security issue that requires attention to be put on the public/private key authentication handler. + + + +Server keys are used by the client to identify the server in a secure way. Ideally those keys should be generated at first boot and optionally re-generated if the user desires to do so. + + + +- {400} Implement in-device private key generation and storage, test and audit its logic. + + + +## {200} Public key-based authentication (GH issue #21) + + + +The user should be able to provide concatenate public keys as "authorized_keys" via environment variable(s) (see related GH issue #23). + + + +- {200} Implement pubkey authentication environment variable provisioning. + + + +## {1000} Provisioning based on (SSH) environment variables (GH issue #23) + + + +Many SSH clients pass environment variables to the server for many purposes. + + + +This task is meant to exploit this commonly used pattern to simplify provisioning of new SSH Stamps. Environment variables will be used to, among other functions: + + + +- {200} Define and persist the UART bridge parameters such as BAUD speed, stop bits. + +- {200} Compile a spreadsheet of commercially available "stamp boards" and set a default, unused UART physical pins pair. Allow the user to alter that default via SSH env settings (see GH issue #23). + +- {200} Pass the password and store it for subsequent sessions, disallowing un-authenticated password changes after first initialization. + +- {200} Pass the private key(s) and/or trigger in-device private key generation. + +- {200} Have an environment variable (or command) that instructs the processor to factory reset all non-volatile storage but keeps the firmware intact. + + + +This approach avoids having complicated device-programming-time deployment(s) of sensitive cryptographic material. + + + +Furthermore, provisioning the device this way yields a superior user experience: a device can be shipped on an unprovisioned state and subsequently be **NON INTERACTIVELY** provisioned via SSH env vars. + + + +The emphasis on non-interactivity (and idempotency) is due to time tested patterns on the IT automation industry (see: Ansible, Puppet, Chef, SaltStack, etc...). + + + +## {1700} Firmware update via SFTP (GH issue #24) + + + +Instead of regular "out of band" firmware OTA update mechanisms, SSH Stamp firmware will be updated via its own secure protocol: SCP/SFTP. + + + +The reason behind this approach is that we can leverage the existing SSH server AAA mechanisms to deploy new firmware securely. + + + +Ideally, this process shouldn't have to involve the bootloader since this should happen at a "userspace + reboot" level. + + + +- {1300} Implement SCP/SFTP as per IETF RFCs. Only the functionality needed to support firmware update. + +- {300} Implement OTA mechanism on device. + +- {100} Audit and thoroughly check that new firmware cannot be altered pre-auth. + + + +The changes described above will most likely need to be applied to SSH Stamp's main current "core" dependency: "sunset". This will therefore constitute an external (to SSH Stamp) OSS contribution that will be used in our software. + + + +## {2000} FSMs + SansIO refactor (GH issue #25) + + + +This is a relatively big refactor but needed pre-requisite to introduce other low level I/O (protocols) beside UART such as SPI/I2C/CAN... it is also a research task since there's uncertainty on whether such a bridge between a (relatively slow) network stack and (generally faster) electrical protocols will tolerate strict timing demands. + + + +Moving to compile-time validated finite state machines (FSMs) will hopefully make pre-auth attacks less likely to be successful on SSH Stamp. Such attacks have been unfortunately common in recent SSH server explorations, see: + + + +https://threatprotect.qualys.com/2025/04/21/erlang-otp-ssh-server-remote-code-execution-vulnerability-cve-2025-32433/ + + + +https://www.runzero.com/blog/sshamble-unexpected-exposures-in-the-secure-shell/ + + + +The trigger of this refactoring idea is https://www.firezone.dev/blog/sans-io + + + +This approach should ideally be combined with some kind of performance profiling to ensure we don't regress after incorporating those changes. + + + +- {1100} Implement a PoC state machine on std outside SSH Stamp, validate corner cases there. + +- {500} Adapt PoC for no_std usage and structure. + +- {400} Refactor codebase for SansIO + FSM. + + + +## {900} General performance engineering (GH issue #28) + + + +Profiling and optimisation are tangentially mentioned towards the end of issue #25, but better tooling/process is needed to maximise the performance of this firmware on different fronts: + + + +- {100} Choose appropriate overall and per-task heap size allocations, i.e: bisect heap size until finding a clear performance degradation under different loads. + +- {100} Perform benchmarks for different RX/TX buffer sizes on both SSH network stack and UART packet queues. + +- {100} Minimise the build for size as much as possible without discarding security checks (overflow protections). + +- {300} Add CI/CD instrumentation to track all of the above over time, avoiding future regressions. + +- {300} Profile hot spots across the app, including cryptographic primitives, and optimise them. + + + +Those performance engineering techniques should be appropriately documented (preferably with examples) and ideally implemented in CI to monitor regressions. + + + +## {1000} Documentation (GH issue #30) + + + +Mid project, when the code based reaches relative maturity and stability, we should make a concerted effort to write and review docs at all levels: user, dev, architecture.md + + + +- {200} Make sure onboarding (and quickstart) is fully documented and flawless. + +- {200} Refer and implement an ARCHITECTURE.md document. Advice taken from Matklad's blogpost: https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html + +- {120} List and document all the effects of environment variables from GH issue #23. + +- {120} List and document both supported and unsupported devices and boards. + +- {120} List and document ways to debug/profile/tweak firmware for different feature flags and scenarios. + +- {120} Have at least one real world scenario documented. + +- {120} Define clearly what's our threat model, i.e: we'll not protect against any physical attacks. OTOH, pre-auth attacks (exploit bad code logic that Rust doesn't protect against) can be the worst case scenario for SSH Stamp. + + + +## {600} Testing (GH issue #37) + + + +Test that the firmware compiles, has good coverage and there are no performance regressions as mentioned in other tasks. This task is a more focused effort so that there is comprehensive testing in both software and hardware. + + + +- {200} Leverage embedded-test Rust crate for hardware testing (HIL). + +- {200} Physically build a test harness with most of the supported targets so that they can be easily tested (ideally via CI/CD hooks). + +- {200} Test that a IPV6 address is served over DHCPv6. ULA, DHCP-PD and other common scenarios work as they should. + + + +## {500} Compile project for all Espressif ESP32 WiFi targets (GH issue #18) + + + +The current project only compiles for the ESP32-C6, as in indicated in various feature flags and linker options. + + + +Find a non-intrusive way to compile for as many Espressif targets as possible. + + + +- {400} Implement cargo build targets for each Espressif microcontroller. + +- {100} Resolve compilation issues that arise for each target. + + + +## {800} Implement firmware support for alternative wifi enabled microcontrollers (other than Espressif) (GH issue #19) + + + +Implementing and testing support for other microcontrollers from a completely different manufacturer. + + + +- {200} Compile and flash SSH Stamp into this target with UART support. + +- {600} Try to support WiFi (currently not supported at the upstream HAL). + + + +## {2000} Add mlkem768x25519 (to sunset?) and integrate into SSH-Stamp (GH issue #34) + + + +Add mlkem768x25519 (post Quantum resistant crypto) to sunset dependency and integrate into SSH-Stamp. + + + +- {1500} Implement and test MLKEM upstream in sunset. + +- {500} Test in SSH Stamp. + + + +## {1080} Bootloader audit and UX improvements (GH issue #35) + + + +Some users will expect that the bridge is established against their own dev board USB2TTL converted UART0 port instead of custom discrete pins. + + + +The current approach is to use UART1 and custom pins (soon to be GPIO9 and GPIO10 by default) so that boot messages and early SSH2UART are not intermixed. + +This task will have to: + + + +- {600} Explore how the default-shipped ESP-IDF 5.x bootloader is packaged in esp-hal's firmware. + +- {240} Conditionally modify the bootloader accordingly so no boot messages are displayed when powering up the board and no println!/debug messages from the user level firmware itself are shown either. In other words: Only bridged SSH2UART messages should be conveyed. + +- {120} Make sure those bootloader modifications are not an impediment to basic operations such as re-flashing the IC (i.e: perhaps espflash expects some bootloader output to trigger a flashing operation?). + +- {120} Document a path towards a secure bootloader that is compatible with the HAL/device **and** respects the aforementioned points. + + + +This exploration into the bootloader inner workings can also pave the way to assess whether improvements in secure boot are interesting to pursue and how. + + + +## {840} Implement WiFi STA mode (GH issue #36) + + + +Currently SSH Stamp boots up in AP (Access Point) mode and expects DHCP clients. This tasks would put SSH Stamp in STA (Station mode) or acting as a WiFi client instead. + + + +This is attractive on more permanent installations where SSH Stamp can be part of a larger local wireless network. + + + +- {240} Make appropriate changes to device HAL initialization. + +- {240} Verify that those changes do not break prior assumptions. + +- {240} Align onboarding process for that mode's particularities. + +- {120} Make the setting configurable (see GH issue #23). + + + +## {200} Licensing (GH issue #22) + + + +There's some borrowed code from SSH Stamp's main dependency: sunset. Clarify this properly and/or find the right way to compose LICENSE wording. + + + +- {200} Process feedback and apply changes from NLNet licensing experts. + + + +## {2000} Implement bridging support for other electrical protocols such as: CAN, SPI and I2C. + + + +Encapsulate transaction-oriented protocols (CAN, SPI, I2C) over a plain byte stream. This task assumes that the the processor's timing characteristics are suitable. At a protocol level, for CAN packet encapsulation there's "slcan" and "GVRET" among other more exotic encapsulation protocols. + + + +Equivalents for SPI-over-Network or I2C-over-Network are a much more experimental and uncharted territory, perhaps the Bus Pirate command protocol would be the closest match. + + + +- {1000} Implement slcan and/or GVRET for SSH_to_CAN support. + +- {1000} Investigate and implement proof of concept for SPI and/or I2C. + + + +## {600} Final release + + + +Final review on all the work done and refinement, optimization and security audit handover. + + + +- {120} Stable release. + +- {240} Process feedback from security audit and accessibility scan. + +- {240} Final release with updated documentation. diff --git a/docs/nlnet/plan.md b/docs/nlnet/plan.md new file mode 100644 index 0000000..989acea --- /dev/null +++ b/docs/nlnet/plan.md @@ -0,0 +1,34 @@ +```mermaid +gantt + title SSH Stamp (a.k.a esp-ssh-rs) development plan under NLNet grant + dateFormat YYYY-MM-DD + excludes weekends + tickInterval 1day + weekday monday + todayMarker off + axisFormat %e + section Production + Fix password auth : passwd, 2025-05-01, 24h + Fix pubkey auth : pubkey, after passwd, 24h + UART perf : uart_intr, after pubkey, 12h + section Provisioning + Provisioning : prov, after uart_intr, 16h + OTA updates : ota, after prov, 12h + section Docs + usage docs : usage_docs, after ota, 4h + dev docs : dev_docs, after ota, 2h + section Robustness + #forbid(unsafe) : no_unsafe, after ota, 12h + sans-io refactor : sans_io, after uart_intr, 16h + section Multi-target + Espressif chips : all_espressif, after no_unsafe, 12h + Other chip1 : chip1, after all_espressif, 20h + Other chip2 : chip2, after chip1, 18h + section Testing + CI/CD : ci, after chip2, 16h + hardware in test loop : HIL, after ci, 21h + Users test : user_tests, after dev_docs, 9h + section Security + Self audit : self_sec_audit, after all_espressif, 10h + NLNet security audit? : nlnet_sec_audit, after all_espressif, 45h +``` diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..8eaf718 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,8 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +app_config, data, nvs, 0x9000, 0x2000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 0x200000, +ota_0, app, ota_0, 0x210000, 0x100000, +ota_1, app, ota_1, 0x310000, 0x100000, \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 17c702b..f3683df 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,5 @@ [toolchain] -channel = "stable-2025-04-03" +#channel = "stable-2025-04-03" +channel = "nightly" components = ["rust-src"] targets = ["riscv32imac-unknown-none-elf"] diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..1f4c0ff --- /dev/null +++ b/src/config.rs @@ -0,0 +1,571 @@ +use core::net::Ipv4Addr; +#[cfg(feature = "ipv6")] +use core::net::Ipv6Addr; +#[cfg(feature = "ipv6")] +use embassy_net::{Ipv6Cidr, StaticConfigV6}; +use embassy_net::{Ipv4Cidr, StaticConfigV4}; +use esp_hal::gpio::AnyPin; +use esp_hal::peripherals; +use heapless::{String, Vec}; + +use esp_println::dbg; + +use bcrypt; +use hmac::{Hmac, Mac}; +use sha2::Sha256; +use subtle::ConstantTimeEq; + +use sunset::error::TrapBug; +use sunset::{KeyType, Result}; +use sunset::{ + packets::Ed25519PubKey, + sshwire::{SSHDecode, SSHEncode, SSHSink, SSHSource, WireError, WireResult}, + SignKey, +}; +use embassy_sync::channel::Channel; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use sunset_sshwire_derive::SSHEncode; + +use crate::errors; +use crate::settings::{DEFAULT_SSID, KEY_SLOTS}; + +#[derive(Debug)] +pub struct SSHStampConfig { + pub hostkey: SignKey, + + pub password_authentication: bool, + pub admin_pw: Option, + pub admin_keys: [Option; KEY_SLOTS], + + /// WiFi + pub wifi_ssid: String<32>, + pub wifi_pw: Option>, // TODO: Why not 64? + + /// Networking + /// TODO: Populate this field from esp's hardware info or just refer it from HAL? + /// Only intended purpose I see for keeping it here is for spoofing? + pub mac: [u8; 6], + /// `None` for DHCP + pub ipv4_static: Option, + #[cfg(feature = "ipv6")] + pub ipv6_static: Option, + /// UART + pub uart_pins: SerdePinConfig, +} + +#[derive(Debug, Clone, SSHEncode)] +pub struct SerdePinConfig { + pub tx: u8, + pub rx: u8, + pub rts: Option, + pub cts: Option, +} + +impl Default for SerdePinConfig { + fn default() -> Self { + Self { + // TODO: This env comes from SSH env events/packets, not from system's std::env / core::env (if any)... so it shouldn't be unsafe() + tx: 10, + rx: 11, + rts: None, + cts: None + } + } +} + +impl<'de> SSHDecode<'de> for SerdePinConfig { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let tx = u8::dec(s)?; + let rx = u8::dec(s)?; + // TODO: Decoding Options is problematic since encode only writes them if they exist + let rts = dec_option(s)?; + let cts = dec_option(s)?; + Ok(SerdePinConfig { tx, rx, rts, cts }) + } +} + +pub struct GPIOConfig { + pub gpio10: Option>, + pub gpio11: Option>, +} + +pub struct PinChannel { + pub config: SerdePinConfig, + pub gpios: GPIOConfig, + pub tx: Channel::, + pub rx: Channel::, + // TODO: cts/rts pins +} + +impl PinChannel { + pub fn new(config: SerdePinConfig, gpios: GPIOConfig) -> Self { + Self { + config, + gpios, + tx: Channel::::new(), + rx: Channel::::new(), + } + } + + pub async fn recv_tx(&mut self) -> errors::Result> { + // tx needs to lock here. + //self.tx.receive().await; + + Ok(match self.config.tx { + 10 => self.gpios.gpio10.take().ok_or_else(|| errors::Error::InvalidPin)?, + 11 => self.gpios.gpio11.take().ok_or_else(|| errors::Error::InvalidPin)?, + _ => return Err(errors::Error::InvalidPin) + }) + } + + pub async fn send_tx(&mut self, pin: AnyPin<'static>) -> errors::Result<()> { + match self.config.tx { + 10 => self.gpios.gpio10 = Some(pin), + 11 => self.gpios.gpio11 = Some(pin), + _ => return Err(errors::Error::InvalidPin) + }; + + // tx lock needs to be released. + self.tx.send(()).await; + Ok(()) + } + + pub async fn recv_rx(&mut self) -> errors::Result> { + let res = Ok(match self.config.rx { + 10 => self.gpios.gpio10.take().ok_or_else(|| errors::Error::InvalidPin)?, + 11 => self.gpios.gpio11.take().ok_or_else(|| errors::Error::InvalidPin)?, + _ => return Err(errors::Error::InvalidPin) + }); + dbg!("recv_rx: no channel receive"); + // rx needs to lock here. + // dbg!("recv_rx: before rx.receive.await"); + // self.rx.receive().await; + // dbg!("recv_rx: after rx.receive.await"); + + res + } + + pub async fn send_rx(&mut self, pin: AnyPin<'static>) -> errors::Result<()> { + match self.config.rx { + 10 => self.gpios.gpio10 = Some(pin), + 11 => self.gpios.gpio11 = Some(pin), + _ => return Err(errors::Error::InvalidPin) + }; + + // rx lock needs to be released. + self.rx.send(()).await; + Ok(()) + } + + pub async fn with_channel(&mut self, f: F) -> errors::Result<()> + where F: for<'a> AsyncFnOnce(AnyPin<'a>, AnyPin<'a>) { + dbg!("inner: with_channel begin, recv_rx call"); + let mut rx = self.recv_rx().await?; + dbg!("inner: with_channel recv_tx call"); + let mut tx = self.recv_tx().await?; + + dbg!("inner: with_channel f-reborrow"); + f(rx.reborrow(), tx.reborrow()).await; + + dbg!("inner: with_channel, before send{rx/tx}"); + self.send_rx(rx).await.unwrap(); + self.send_tx(tx).await.unwrap(); + + Ok(()) + } +} + + +// TODO: This struct and resolve_pin() need to be re-thought for the different ICs and dev boards?.. implementing a suitable +// validation function for them and potentially writing a macro that adapts to each PAC (not all ICs have the same number +// of pins). +pub struct PinConfig { + pub tx: AnyPin<'static>, + pub rx: AnyPin<'static>, +} + +pub struct PinConfigAlt { + pub peripherals: peripherals::Peripherals, +} + +impl PinConfigAlt { + pub fn new(peripherals: peripherals::Peripherals) -> Self { + Self { + peripherals, + } + } + + pub fn take_pin<'a>(&'a mut self, pin: u8) -> AnyPin<'a> { + match pin { + 0 => self.peripherals.GPIO0.reborrow().into(), + 1 => self.peripherals.GPIO1.reborrow().into(), + _ => panic!(), + } + } +} + +impl PinConfig { + pub fn new(mut gpio_config: GPIOConfig, config_inner: SerdePinConfig) -> errors::Result { + if config_inner.rx == config_inner.tx { + return Err(errors::Error::InvalidPin); + } + + // SAFETY: Safe because moved in peripherals. + Ok(Self { + rx: match config_inner.rx { + 10 => gpio_config.gpio10.take().unwrap().into(), + 11 => gpio_config.gpio11.take().unwrap().into(), + _ => return Err(errors::Error::InvalidPin), + }, + tx: match config_inner.tx { + 10 => gpio_config.gpio10.take().unwrap().into(), + 11 => gpio_config.gpio11.take().unwrap().into(), + _ => return Err(errors::Error::InvalidPin), + } + }) + } + + /// Resolves a u8 pin number into an AnyPin GPIO type. + /// Returns None if the pin number is invalid or unsupported. + pub fn initialize_pin(peripherals: peripherals::Peripherals, pin_number: u8) -> errors::Result> { + match pin_number { + 0 => Ok(peripherals.GPIO0.into()), + + _ => Err(errors::Error::InvalidPin), + } + } +} + +impl SSHStampConfig { + /// Bump this when the format changes + pub const CURRENT_VERSION: u8 = 6; + + /// Creates a new config with default parameters. + /// + /// Will only fail on RNG failure. + pub fn new() -> Result { + let hostkey = SignKey::generate(KeyType::Ed25519, None)?; + let wifi_ssid: String<32> = + option_env!("WIFI_SSID").unwrap_or(DEFAULT_SSID).try_into().trap()?; + let wifi_pw: Option> = + option_env!("WIFI_PW").map(|s| s.try_into()).transpose().trap()?; + let mac = random_mac()?; + + let uart_pins = SerdePinConfig::default(); + + Ok(SSHStampConfig { + hostkey, + password_authentication: true, + admin_pw: None, + admin_keys: Default::default(), + wifi_ssid, + wifi_pw, + mac, + ipv4_static: None, + #[cfg(feature = "ipv6")] + ipv6_static: None, + uart_pins, + }) + } + + pub fn set_admin_pw(&mut self, pw: Option<&str>) -> Result<()> { + self.admin_pw = pw.map(|p| PwHash::new(p)).transpose()?; + Ok(()) + } + + pub fn check_admin_pw(&mut self, pw: &str) -> bool { + if let Some(ref p) = self.admin_pw { + p.check(pw) + } else { + false + } + } + + // pub fn config_change(&mut self, conf: SSHConfig) -> Result<()> { + // ServEvent::ConfigChange(); + // } +} + +fn random_mac() -> Result<[u8; 6]> { + // let mut mac = [0u8; 6]; + // sunset::random::fill_random(&mut mac)?; + // // unicast, locally administered + // mac[0] = (mac[0] & 0xfc) | 0x02; + let mac = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]; // TODO: Temporary fixed MAC for testing + Ok(mac) +} + +// a private encoding specific to demo config, not SSH defined. +fn enc_signkey(k: &SignKey, s: &mut dyn SSHSink) -> WireResult<()> { + // need to add a variant field if we support more key types. + match k { + SignKey::Ed25519(k) => k.to_bytes().enc(s), + _ => Err(WireError::UnknownVariant), + } +} + +fn dec_signkey<'de, S>(s: &mut S) -> WireResult +where + S: SSHSource<'de>, +{ + let k: ed25519_dalek::SecretKey = SSHDecode::dec(s)?; + let k = ed25519_dalek::SigningKey::from_bytes(&k); + Ok(SignKey::Ed25519(k)) +} + +// encode Option as a bool then maybe a value +fn enc_option(v: &Option, s: &mut dyn SSHSink) -> WireResult<()> { + v.is_some().enc(s)?; + v.enc(s) +} + +fn dec_option<'de, S, T: SSHDecode<'de>>(s: &mut S) -> WireResult> +where + S: SSHSource<'de>, +{ + bool::dec(s)?.then(|| SSHDecode::dec(s)).transpose() +} + +fn enc_ipv4_config(v: &Option, s: &mut dyn SSHSink) -> WireResult<()> { + v.is_some().enc(s)?; + if let Some(v) = v { + v.address.address().to_bits().enc(s)?; + dbg!("enc_ipv4_config: prefix", &v.address.prefix_len()); + v.address.prefix_len().enc(s)?; + // to u32 + let gw = v.gateway.map(|a| a.to_bits()); + enc_option(&gw, s)?; + } + Ok(()) +} + +#[cfg(feature = "ipv6")] +fn enc_ipv6_config(v: &Option, s: &mut dyn SSHSink) -> WireResult<()> { + v.is_some().enc(s)?; + if let Some(v) = v { + v.address.address().to_bits().enc(s)?; + v.address.prefix_len().enc(s)?; + let gw = v.gateway.map(|a| a.to_bits()); + enc_option(&gw, s)?; + } + Ok(()) +} + +fn dec_ipv4_config<'de, S>(s: &mut S) -> WireResult> +where + S: SSHSource<'de>, +{ + let opt = bool::dec(s)?; + opt.then(|| { + let ad: u32 = SSHDecode::dec(s)?; + let ad = Ipv4Addr::from_bits(ad); + let prefix: u8 = SSHDecode::dec(s)?; + if prefix > 32 { + // embassy panics, so test it here + return Err(WireError::PacketWrong); + } + let gw: Option = dec_option(s)?; + let gateway = gw.map(|gw| Ipv4Addr::from_bits(gw)); + Ok(StaticConfigV4 { + address: Ipv4Cidr::new(ad, prefix), + gateway, + dns_servers: Vec::new(), + }) + }) + .transpose() +} + +#[cfg(feature = "ipv6")] +fn dec_ipv6_config<'de, S>(s: &mut S) -> WireResult> +where + S: SSHSource<'de>, +{ + let opt = bool::dec(s)?; + opt.then(|| { + let ad: u128 = SSHDecode::dec(s)?; + let ad = Ipv6Addr::from_bits(ad); + let prefix = SSHDecode::dec(s)?; + if prefix > 32 { + // embassy panics, so test it here + return Err(WireError::PacketWrong); + } + let gw: Option = dec_option(s)?; + let gateway = gw.map(|gw| Ipv6Addr::from_bits(gw)); + Ok(StaticConfigV6 { + address: Ipv6Cidr::new(ad, prefix), + gateway, + dns_servers: Vec::new(), + }) + }) + .transpose() +} + +// fn dec_uart_pins<'de, S>(s: &mut S) -> WireResult +// where +// S: SSHSource<'de>, +// { +// let tx = u8::dec(s)?; +// let rx = u8::dec(s)?; +// let rts = dec_option(s)?; +// let cts = dec_option(s)?; +// Ok(SerdePinConfig { tx, rx, rts, cts }) +// } + +impl SSHEncode for SSHStampConfig { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + enc_signkey(&self.hostkey, s)?; + self.password_authentication.enc(s)?; + enc_option(&self.admin_pw, s)?; + + for k in self.admin_keys.iter() { + enc_option(k, s)?; + } + + self.wifi_ssid.as_str().enc(s)?; + enc_option(&self.wifi_pw, s)?; + + self.mac.enc(s)?; + + enc_ipv4_config(&self.ipv4_static, s)?; + #[cfg(feature = "ipv6")] + enc_ipv6_config(&self.ipv6_static, s)?; + + // Encode PinConfig + self.uart_pins.enc(s)?; + + Ok(()) + } +} + +impl<'de> SSHDecode<'de> for SSHStampConfig { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + + let hostkey = dec_signkey(s)?; + dbg!("Hostkey: ", &hostkey); + + dbg!(s.remaining()); + let password_authentication = SSHDecode::dec(s)?; + dbg!(s.remaining()); + let admin_pw = dec_option(s)?; + + let mut admin_keys = [None, None, None]; + for k in admin_keys.iter_mut() { + *k = dec_option(s)?; + } + + dbg!(s.remaining()); + let wifi_ssid = SSHDecode::dec(s)?; + let wifi_pw = dec_option(s)?; + + let mac = SSHDecode::dec(s)?; + + let ipv4_static = dec_ipv4_config(s)?; + #[cfg(feature = "ipv6")] + let ipv6_static = dec_ipv6_config(s)?; + + // Not supported by sshwire-derive nor virtue (no Option support) + // let uart_pins = SSHDecode::dec(s)?; + let uart_pins = SSHDecode::dec(s)?; + + Ok(Self { + hostkey, + password_authentication, + admin_pw, + admin_keys, + wifi_ssid, + wifi_pw, + mac, + ipv4_static, + #[cfg(feature = "ipv6")] + ipv6_static, + uart_pins, + }) + } +} + +/// Stores a bcrypt password hash. +/// +/// We use bcrypt because it seems the best password hashing option where +/// memory hardness isn't possible (the rp2040 is smaller than CPU or GPU memory). +/// +/// The cost is currently set to 6, taking ~500ms on a 125mhz rp2040. +/// Time converges to roughly 8.6ms * 2**cost +/// +/// Passwords are pre-hashed to avoid bcrypt's 72 byte limit. +/// rust-bcrypt allows nulls in passwords. +/// We use an hmac rather than plain hash to avoid password shucking +/// (an attacker bcrypts known hashes from some other breach, then +/// brute forces the weaker hash for any that match). +//#[derive(Clone, SSHEncode, SSHDecode, PartialEq)] +#[derive(Clone, PartialEq)] +pub struct PwHash { + salt: [u8; 16], + hash: [u8; 24], + cost: u8, +} + +impl PwHash { + const COST: u8 = 6; + /// `pw` must not be empty. + pub fn new(pw: &str) -> Result { + if pw.is_empty() { + return sunset::error::BadUsage.fail(); + } + + let mut salt = [0u8; 16]; + sunset::random::fill_random(&mut salt)?; + let prehash = Self::prehash(pw, &salt); + let cost = Self::COST; + let hash = bcrypt::bcrypt(cost as u32, salt, &prehash); + Ok(Self { salt, hash, cost }) + } + + pub fn check(&self, pw: &str) -> bool { + if pw.is_empty() { + return false; + } + let prehash = Self::prehash(pw, &self.salt); + let check_hash = bcrypt::bcrypt(self.cost as u32, self.salt.clone(), &prehash); + check_hash.ct_eq(&self.hash).into() + } + + fn prehash(pw: &str, salt: &[u8]) -> [u8; 32] { + // OK unwrap: can't fail, accepts any length + // TODO: Generalise, not only Espressif esp_hal + let mut prehash = Hmac::::new_from_slice(&salt).unwrap(); + prehash.update(pw.as_bytes()); + prehash.finalize().into_bytes().into() + } +} + +impl core::fmt::Debug for PwHash { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PwHash").finish_non_exhaustive() + } +} + +impl SSHEncode for PwHash { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + self.salt.enc(s)?; + self.hash.enc(s)?; + self.cost.enc(s) + } +} + +impl<'de> SSHDecode<'de> for PwHash { + fn dec(s: &mut S) -> WireResult + where + S: SSHSource<'de>, + { + let salt = <[u8; 16]>::dec(s)?; + let hash = <[u8; 24]>::dec(s)?; + let cost = u8::dec(s)?; + Ok(PwHash { salt, hash, cost }) + } +} \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..e0c9779 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,12 @@ +use snafu::Snafu; +use core::result; + +pub type Result = result::Result; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("Invalid PIN provided"))] + InvalidPin, + #[snafu(display("Flash storage error"))] + FlashStorageError, +} \ No newline at end of file diff --git a/src/espressif/buffered_uart.rs b/src/espressif/buffered_uart.rs index 71b48d4..3dbcfcd 100644 --- a/src/espressif/buffered_uart.rs +++ b/src/espressif/buffered_uart.rs @@ -51,6 +51,7 @@ impl BufferedUart { let rd_from = async { loop { let n = uart_rx.read_async(&mut uart_rx_buf).await.unwrap(); + let mut rx_slice = &uart_rx_buf[..n]; // Write rx_slice to 'inward' pipe, dropping bytes rather than blocking if diff --git a/src/espressif/hash.rs b/src/espressif/hash.rs new file mode 100644 index 0000000..8672bdc --- /dev/null +++ b/src/espressif/hash.rs @@ -0,0 +1,28 @@ +use esp_hal::hmac::Hmac; +use hmac::Hmac; +use sha2::Sha256; + +pub trait EspressifHmac { + fn new_from_slice(key: &[u8]) -> Result + where + Self: Sized; + fn update(&mut self, data: &[u8]); + fn finalize(self) -> [u8; 32]; +} + +impl EspressifHmac for Hmac { + fn new_from_slice(key: &[u8]) -> Result { + Hmac::new_from_slice(key).map_err(|_| ()) + } + + fn update(&mut self, data: &[u8]) { + self.update(data); + } + + fn finalize(self) -> [u8; 32] { + let result = self.finalize(); + let mut arr = [0u8; 32]; + arr.copy_from_slice(&result.into_bytes()); + arr + } +} \ No newline at end of file diff --git a/src/espressif/mod.rs b/src/espressif/mod.rs index 88fc177..a86e6cc 100644 --- a/src/espressif/mod.rs +++ b/src/espressif/mod.rs @@ -1,3 +1,6 @@ pub mod buffered_uart; pub mod net; pub mod rng; +// TODO: Specialise for Espressif, tricky since it seems to require burning eFuses?: +// https://github.com/esp-rs/esp-hal/blob/main/examples/src/bin/hmac.rs +//pub mod hash; diff --git a/src/espressif/net.rs b/src/espressif/net.rs index 1a79792..75e0a2f 100644 --- a/src/espressif/net.rs +++ b/src/espressif/net.rs @@ -14,6 +14,7 @@ use esp_println::{dbg, println}; use esp_wifi::wifi::{AccessPointConfiguration, Configuration, WifiController, WifiDevice}; use esp_wifi::wifi::{WifiEvent, WifiState}; use esp_wifi::EspWifiController; +use sunset_async::SunsetMutex; use core::net::SocketAddrV4; use edge_dhcp; @@ -25,9 +26,9 @@ use edge_dhcp::{ use edge_nal::UdpBind; use edge_nal_embassy::{Udp, UdpBuffers}; -use super::buffered_uart::BufferedUart; +use crate::config::SSHStampConfig; -const GW_IP_ADDR_ENV: Option<&'static str> = option_env!("GATEWAY_IP"); +use super::buffered_uart::BufferedUart; // When you are okay with using a nightly compiler it's better to use https://docs.rs/static_cell/2.1.0/static_cell/macro.make_static.html macro_rules! mk_static { @@ -44,16 +45,25 @@ pub async fn if_up( wifi_controller: EspWifiController<'static>, wifi: WIFI<'static>, rng: &mut Rng, + config: &'static SunsetMutex ) -> Result, sunset::Error> { let wifi_init = &*mk_static!(EspWifiController<'static>, wifi_controller); let (controller, interfaces) = esp_wifi::wifi::new(wifi_init, wifi).unwrap(); - let gw_ip_addr_str = GW_IP_ADDR_ENV.unwrap_or("192.168.0.1"); - let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip"); - - let config = embassy_net::Config::ipv4_static(StaticConfigV4 { - address: Ipv4Cidr::new(gw_ip_addr, 24), - gateway: Some(gw_ip_addr), + let gw_ip_addr_ipv4 = Ipv4Addr::from_str("192.168.0.1").expect("failed to parse gateway ip"); + + // let _gw_ip_addr = { + // let guard = config.lock().await; + // if let Some(ref s) = guard.ip4_static { + // embassy_net::Config::ipv4_static(s.clone()) + // } else { + // embassy_net::Config::dhcpv4(Default::default()) + // } + // }; + + let net_config = embassy_net::Config::ipv4_static(StaticConfigV4 { + address: Ipv4Cidr::new(gw_ip_addr_ipv4, 24), + gateway: Some(gw_ip_addr_ipv4), dns_servers: Default::default(), }); @@ -62,14 +72,14 @@ pub async fn if_up( // Init network stack let (ap_stack, runner) = embassy_net::new( interfaces.ap, - config, + net_config, mk_static!(StackResources<3>, StackResources::<3>::new()), seed, ); - spawner.spawn(wifi_up(controller)).ok(); + spawner.spawn(wifi_up(controller, config)).ok(); spawner.spawn(net_up(runner)).ok(); - spawner.spawn(dhcp_server(ap_stack, gw_ip_addr)).ok(); + spawner.spawn(dhcp_server(ap_stack, gw_ip_addr_ipv4)).ok(); loop { println!("Checking if link is up...\n"); @@ -82,7 +92,7 @@ pub async fn if_up( // TODO: Use wifi_manager instead? println!( "Connect to the AP `ssh-stamp` as a DHCP client with IP: {}", - gw_ip_addr_str + gw_ip_addr_ipv4 ); Ok(ap_stack) @@ -119,8 +129,17 @@ pub async fn accept_requests(stack: Stack<'static>, uart: &BufferedUart) -> ! { } #[embassy_executor::task] -async fn wifi_up(mut controller: WifiController<'static>) { +async fn wifi_up(mut controller: WifiController<'static>, config: &'static SunsetMutex) { println!("Device capabilities: {:?}", controller.capabilities()); + + let wifi_ssid = { + let guard = config.lock().await; + guard.wifi_ssid.clone() + // drop guard + }; + // TODO: No wifi password(s) yet... + //let wifi_password = config.lock().await.wifi_pw; + loop { if esp_wifi::wifi::wifi_state() == WifiState::ApStarted { // wait until we're no longer connected @@ -129,7 +148,7 @@ async fn wifi_up(mut controller: WifiController<'static>) { } if !matches!(controller.is_started(), Ok(true)) { let client_config = Configuration::AccessPoint(AccessPointConfiguration { - ssid: "ssh-stamp".into(), + ssid: wifi_ssid.to_ascii_lowercase(), ..Default::default() }); controller.set_configuration(&client_config).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index f32497f..ed48a46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,13 @@ #![no_std] #![no_main] -#![forbid(unsafe_code)] +// #![forbid(unsafe_code)] +#[deny(clippy::mem_forget)] // avoids any UB, forces use of Drop impl instead +pub mod config; pub mod espressif; pub mod keys; pub mod serial; pub mod serve; pub mod settings; +pub mod storage; +pub mod errors; diff --git a/src/main.rs b/src/main.rs index 9ab48dd..7c502aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,28 +5,28 @@ use core::marker::Sized; use esp_alloc as _; use esp_backtrace as _; use esp_hal::{ - gpio::AnyPin, - interrupt::{software::SoftwareInterruptControl, Priority}, - peripherals::UART1, - rng::Rng, - timer::timg::TimerGroup, - uart::{Config, RxConfig, Uart}, + gpio::Pin, interrupt::{software::SoftwareInterruptControl, Priority}, peripherals::UART1, rng::Rng, timer::timg::TimerGroup, uart::{Config, RxConfig, Uart} }; use esp_hal_embassy::InterruptExecutor; +use esp_println::dbg; +use esp_storage::FlashStorage; use embassy_executor::Spawner; -use ssh_stamp::espressif::{ +use ssh_stamp::{config::SSHStampConfig, espressif::{ buffered_uart::BufferedUart, net::{accept_requests, if_up}, rng, -}; +}, storage::Fl}; use static_cell::StaticCell; +use sunset_async::SunsetMutex; +use ssh_stamp::config::GPIOConfig; +use ssh_stamp::config::PinChannel; #[esp_hal_embassy::main] async fn main(spawner: Spawner) -> ! { cfg_if::cfg_if!( if #[cfg(feature = "esp32s2")] { - // TODO: This heap size will crash at runtime, we need to fix this + // TODO: This heap size will crash at runtime (only for the ESP32S2), we need to fix this // applying ideas from https://github.com/brainstorm/ssh-stamp/pull/41#issuecomment-2964775170 esp_alloc::heap_allocator!(size: 69 * 1024); } else { @@ -54,10 +54,21 @@ async fn main(spawner: Spawner) -> ! { } } + // Read SSH configuration from Flash (if it exists) + let mut flash_storage = Fl::new(FlashStorage::new()); + let config = ssh_stamp::storage::load_or_create(&mut flash_storage).await; + + static FLASH: StaticCell> = StaticCell::new(); + let _flash = FLASH.init(SunsetMutex::new(flash_storage)); + + static CONFIG: StaticCell> = StaticCell::new(); + let config = CONFIG.init(SunsetMutex::new(config.unwrap())); + let wifi_controller = esp_wifi::init(timg0.timer0, rng, peripherals.RADIO_CLK).unwrap(); // Bring up the network interface and start accepting SSH connections. - let tcp_stack = if_up(spawner, wifi_controller, peripherals.WIFI, &mut rng) + // Clone the reference to config to avoid borrow checker issues. + let tcp_stack = if_up(spawner, wifi_controller, peripherals.WIFI, &mut rng, config) .await .unwrap(); @@ -73,40 +84,62 @@ async fn main(spawner: Spawner) -> ! { let interrupt_spawner = interrupt_executor.start(Priority::Priority10); } } - cfg_if::cfg_if! { - if #[cfg(not(feature = "esp32c2"))] { - interrupt_spawner.spawn(uart_task(uart_buf, peripherals.UART1, peripherals.GPIO11.into(), peripherals.GPIO10.into())).unwrap(); - } else { - interrupt_spawner.spawn(uart_task(uart_buf, peripherals.UART1, peripherals.GPIO9.into(), peripherals.GPIO10.into())).unwrap(); - } - } + + let serde_pin_config = { + let guard = config.lock().await; + guard.uart_pins.clone() + }; + + + let available_gpios = GPIOConfig { + gpio10: Some(peripherals.GPIO10.degrade()), + gpio11: Some(peripherals.GPIO11.degrade()), + }; + + static CHANNEL: StaticCell = StaticCell::new(); + let channel = CHANNEL.init({ + PinChannel::new(serde_pin_config, available_gpios) + }); + + // Grab UART1, typically not connected to dev board's TTL2USB IC nor builtin JTAG functionality + let uart1 = peripherals.UART1; + + // Use the same config reference for UART task. + interrupt_spawner.spawn(uart_task(uart_buf, uart1, channel)).unwrap(); + accept_requests(tcp_stack, uart_buf).await; } static UART_BUF: StaticCell = StaticCell::new(); - static INT_EXECUTOR: StaticCell> = StaticCell::new(); -#[embassy_executor::task] +#[embassy_executor::task()] async fn uart_task( buffer: &'static BufferedUart, uart_periph: UART1<'static>, - rx_pin: AnyPin<'static>, - tx_pin: AnyPin<'static>, + channel: &'static mut PinChannel, ) { + dbg!("spawning UART task..."); // Hardware UART setup let uart_config = Config::default().with_rx( RxConfig::default() .with_fifo_full_threshold(16) - .with_timeout(1), + .with_timeout(1) ); - let uart = Uart::new(uart_periph, uart_config) - .unwrap() - .with_rx(rx_pin) - .with_tx(tx_pin) - .into_async(); + dbg!("before with_channel"); + // Sync pin config via channels + channel.with_channel(async |rx, tx| { + dbg!("into with_channel"); + let uart = Uart::new(uart_periph, uart_config) + .unwrap() + .with_rx(rx) + .with_tx(tx) + .into_async(); - // Run the main buffered TX/RX loop - buffer.run(uart).await; + // Run the main buffered TX/RX loop + dbg!("before buffer_run"); + buffer.run(uart).await; + dbg!("after buffer_run"); + }).await.unwrap(); } diff --git a/src/serve.rs b/src/serve.rs index bc5eb0f..c2b790c 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -31,11 +31,13 @@ async fn connection_loop( loop { let mut ph = ProgressHolder::new(); let ev = serv.progress(&mut ph).await?; - dbg!(&ev); + //dbg!(&ev); #[allow(unreachable_patterns)] match ev { ServEvent::SessionShell(a) => { if let Some(ch) = session.take() { + debug_assert!(ch.num() == a.channel()); + a.succeed()?; dbg!("We got shell"); let _ = chan_pipe.try_send(ch); @@ -70,19 +72,26 @@ async fn connection_loop( } } } + ServEvent::Environment(a) => { + // TODO: Logic to serialise/validate env vars? I.e: + // config.validate(a) + // config.save(a) + // SSHConfig c = a.validate(); // Checks the input variable, sanitizes, assigns a target subsystem + // a.config_change(c)?; + a.succeed()?; // FIXME: Not just succeed... + }, ServEvent::SessionPty(a) => { a.succeed()?; - } + }, ServEvent::SessionExec(a) => { a.fail()?; - } + }, ServEvent::Defunct | ServEvent::SessionShell(_) => { println!("Expected caller to handle event"); error::BadUsage.fail()? } - ServEvent::PollAgain => (), - ServEvent::SessionSubsystem(_) => (), - } + _ => () + }; } } diff --git a/src/settings.rs b/src/settings.rs index 098e322..b935e27 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -3,8 +3,11 @@ // SSH server settings //pub(crate) const MTU: usize = 1536; //pub(crate) const PORT: u16 = 22; -pub(crate) const _SERVER_ID: &str = "SSH-2.0-ssh-stamp-0.1"; +pub(crate) const DEFAULT_SSID: &str = "ssh-stamp"; +//pub(crate) const SSH_SERVER_ID: &str = "SSH-2.0-ssh-stamp-0.1"; +pub(crate) const KEY_SLOTS: usize = 3; // TODO: Document whether this a "reasonable default"? Justify why? +//pub(crate) const PASSWORD_AUTHENTICATION: bool = true; // UART settings //pub(crate) const BAUD_RATE: u32 = 115200; -//pub(crate) const UART_SETTINGS: &str = "8N1"; +//pub(crate) const UART_SETTINGS: &str = "8N1"; \ No newline at end of file diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..bd02260 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,166 @@ +use esp_bootloader_esp_idf::partitions; +use esp_println::{println, dbg}; +use esp_storage::FlashStorage; +use embedded_storage::ReadStorage; + +use pretty_hex::PrettyHex; +use sha2::Digest; + +use core::borrow::Borrow; + +use embedded_storage::nor_flash::NorFlash; + +use sunset::error::Error as SunsetError; +use crate::errors::Error as SSHStampError; + +use sunset::sshwire::{self, OwnOrBorrow}; +use sunset_sshwire_derive::*; + +use crate::config::SSHStampConfig; + +pub const CONFIG_VERSION_SIZE: usize = 4; +pub const CONFIG_HASH_SIZE: usize = 32; +pub const CONFIG_AREA_SIZE: usize = 4096; +pub const CONFIG_OFFSET: usize = 0x9000; + +pub struct Fl { + flash: FlashStorage, + // Only a single task can write to flash at a time, + // keeping a buffer here saves duplicated buffer space in each task. + buf: [u8; FlashConfig::BUF_SIZE], +} + +impl<'a> Fl { + pub fn new(flash: FlashStorage) -> Self { + Self { flash, buf: [0u8; FlashConfig::BUF_SIZE] } + } +} + +// SSHConfig::CURRENT_VERSION must be bumped if any of this struct +#[derive(SSHEncode, SSHDecode)] +struct FlashConfig<'a> { + version: u8, + config: OwnOrBorrow<'a, SSHStampConfig>, + /// sha256 hash of config + hash: [u8; 32], +} + +impl FlashConfig<'_> { + const BUF_SIZE: usize = 460; // Must be enough to hold the whole config + + // TODO: Rework Error mapping with esp_storage errors + /// Finds the NVS partitions and retrieves information about it. + pub fn find_config_partition() -> Result<(), SSHStampError> { + let mut flash = FlashStorage::new(); + println!("Flash size = {} bytes", flash.capacity()); + + let mut pt_mem = [0u8; partitions::PARTITION_TABLE_MAX_LEN]; + let pt = partitions::read_partition_table(&mut flash, &mut pt_mem).unwrap(); + let nvs = pt + .find_partition(partitions::PartitionType::Data( + partitions::DataPartitionSubType::Nvs, + )) + .unwrap() + .unwrap(); + + let nvs_partition = nvs.as_embedded_storage(&mut flash); + + println!("NVS partition size = {}", nvs_partition.capacity()); + println!("NVS partition offset = 0x{:x}", nvs.offset()); + + Ok(()) + } +} + +fn config_hash(config: &SSHStampConfig) -> Result<[u8; 32], SunsetError> { + let mut h = sha2::Sha256::new(); + sshwire::hash_ser(&mut h, config)?; + Ok(h.finalize().into()) +} + +/// Loads a SSHConfig at startup. Good for persisting hostkeys. +pub async fn load_or_create(flash: &mut Fl) -> Result { + match load(flash).await { + Ok(c) => { + println!("Good existing config"); + return Ok(c); + } + Err(e) => println!("Existing config bad, making new. {e}"), + } + + create(flash).await +} + +pub async fn create(flash: &mut Fl) -> Result { + let c = SSHStampConfig::new()?; + dbg!("New config being serialised: ", &c); + save(flash, &c).await?; + + Ok(c) +} + +pub async fn load(fl: &mut Fl) -> Result { + fl.flash.read(CONFIG_OFFSET as u32, &mut fl.buf).map_err(|_e| { + dbg!("flash read error 0x{CONFIG_OFFSET:x} {e:?}"); + SunsetError::msg("flash error") + })?; + + if fl.buf[0] != SSHStampConfig::CURRENT_VERSION { + dbg!("Wrong config version pre-read_ssh decode: {}", fl.buf[0]); + return Err(SunsetError::msg("Wrong config version")); + } + + dbg!("Marko's flash: {}", &fl.buf.hex_dump()); + + let flash_config: FlashConfig = sshwire::read_ssh(&fl.buf, None).unwrap(); +// .map_err(|_| SunsetError::msg("failed to decode flash config"))?; + + if flash_config.version != SSHStampConfig::CURRENT_VERSION { + dbg!("wrong config version on decode: {}", flash_config.version); + return Err(SunsetError::msg("wrong config version")); + } + + let calc_hash = config_hash(flash_config.config.borrow())?; + + dbg!(&calc_hash.hex_dump()); + dbg!(&flash_config.hash.hex_dump()); + + if calc_hash != flash_config.hash { + return Err(SunsetError::msg("bad config hash")); + } + + if let OwnOrBorrow::Own(c) = flash_config.config { + Ok(c) + } else { + // OK panic - OwnOrBorrow always decodes to Own variant + panic!() + } +} + +pub async fn save(fl: &mut Fl, config: &SSHStampConfig) -> Result<(), SunsetError> { + let sc = FlashConfig { + version: SSHStampConfig::CURRENT_VERSION, + config: OwnOrBorrow::Borrow(&config), + hash: config_hash(&config)?, + }; + + FlashConfig::find_config_partition().unwrap(); + + let l = sshwire::write_ssh(&mut fl.buf, &sc)?; + let buf = &fl.buf[..l]; + + dbg!(CONFIG_OFFSET + FlashConfig::BUF_SIZE); + + dbg!("Erasing flash"); + + assert!(CONFIG_AREA_SIZE > FlashConfig::BUF_SIZE); + + fl.flash + .erase(CONFIG_OFFSET as u32, (CONFIG_OFFSET + CONFIG_AREA_SIZE) as u32).unwrap(); + + fl.flash + .write(CONFIG_OFFSET as u32, &buf).unwrap(); + + println!("flash save done"); + Ok(()) +}