From 1885b52b0601f4215c020cb17516c6a7fa1e2c9d Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Fri, 30 Aug 2024 09:23:23 -0400 Subject: [PATCH] Add Yubikey-based challenge for tech port unlocking (#274) See https://rfd.shared.oxide.computer/rfd/492#_sketch_of_an_unlock_policy for the backstory here. This adds a new kind of `UnlockChallenge`, which requires that the caller generate an SSH signature (https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig) of a particular blob of data, using trusted keys which are baked into the SP's firmware. --- Cargo.lock | 384 ++++++++++++++++++++--- Cargo.toml | 6 +- faux-mgs/Cargo.toml | 5 + faux-mgs/src/main.rs | 180 ++++++++++- gateway-messages/src/lib.rs | 2 +- gateway-messages/src/mgs_to_sp.rs | 46 +++ gateway-messages/tests/versioning/mod.rs | 1 + gateway-messages/tests/versioning/v15.rs | 167 ++++++++++ 8 files changed, 748 insertions(+), 43 deletions(-) create mode 100644 gateway-messages/tests/versioning/v15.rs diff --git a/Cargo.lock b/Cargo.lock index f39c4745..754cd820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -259,9 +265,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "bzip2" @@ -326,7 +332,17 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.3", + "windows-targets 0.52.6", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", ] [[package]] @@ -406,9 +422,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -484,6 +500,18 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -494,6 +522,32 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "der" version = "0.7.5" @@ -528,6 +582,7 @@ dependencies = [ "block-buffer", "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -576,6 +631,60 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -630,20 +739,41 @@ dependencies = [ "gateway-sp-comms", "glob", "hex", + "humantime", "nix", + "rand", "serde", "serde_json", "sha2", "slog", "slog-async", "slog-term", + "ssh-agent-client-rs", + "ssh-key", "termios", "tokio", "tokio-stream", "tokio-util", "uuid", + "zerocopy 0.6.6", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.23" @@ -847,6 +977,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -883,6 +1014,17 @@ dependencies = [ "scroll", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -978,6 +1120,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "0.2.12" @@ -1053,6 +1204,12 @@ dependencies = [ "zip", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.30" @@ -1144,6 +1301,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1523,6 +1689,44 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + [[package]] name = "packed_struct" version = "0.10.1" @@ -1712,6 +1916,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1872,6 +2085,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -2051,6 +2274,20 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "semver" version = "1.0.17" @@ -2178,9 +2415,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core", @@ -2316,14 +2553,70 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a5be806ab6f127c3da44b7378837ebf01dadca8510a0e572460216b228bd0e" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", ] +[[package]] +name = "ssh-agent-client-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc9798a2e1390157853f81735b9815be2a45dd2200c36646aad942de9074d78" +dependencies = [ + "bytes", + "signature", + "ssh-encoding", + "ssh-key", + "thiserror", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "cipher", + "ssh-encoding", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc" +dependencies = [ + "ed25519-dalek", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core", + "rsa", + "sec1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2449,14 +2742,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix 0.38.34", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3032,7 +3326,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.6", ] [[package]] @@ -3063,7 +3357,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -3083,17 +3386,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.3", - "windows_aarch64_msvc 0.52.3", - "windows_i686_gnu 0.52.3", - "windows_i686_msvc 0.52.3", - "windows_x86_64_gnu 0.52.3", - "windows_x86_64_gnullvm 0.52.3", - "windows_x86_64_msvc 0.52.3", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -3104,9 +3408,9 @@ checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -3122,9 +3426,9 @@ checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -3140,9 +3444,15 @@ checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_gnu" -version = "0.52.3" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -3158,9 +3468,9 @@ checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_i686_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -3176,9 +3486,9 @@ checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnu" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -3188,9 +3498,9 @@ checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -3206,9 +3516,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "windows_x86_64_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index ef287b5b..1d4b8033 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,17 +23,19 @@ async-trait = "0.1" backoff = { version = "0.4.0", features = ["tokio"] } bitflags = "2.6.0" camino = "1.1.9" -clap = { version = "4.5", features = ["derive"] } +clap = { version = "4.5", features = ["derive", "env"] } futures = "0.3.30" fxhash = "0.2.1" glob = "0.3.1" hex = "0.4.3" hubpack = "0.1.2" +humantime = "2.1.0" lru-cache = "0.1.2" nix = { version = "0.27.1", features = ["net"] } omicron-zone-package = "0.11.0" once_cell = "1.19.0" paste = "1.0.15" +rand = "0.8.5" serde = { version = "1.0", default-features = false, features = ["derive"] } serde-big-array = "0.5.1" serde_json = "1.0.127" @@ -44,6 +46,8 @@ slog-async = "2.8" slog-term = "2.9" smoltcp = { version = "0.9", default-features = false, features = ["proto-ipv6"] } socket2 = "0.5.7" +ssh-agent-client-rs = "0.9.1" +ssh-key = { version = "0.6.6", features = ["p256"] } static_assertions = "1.1.0" strum_macros = "0.25" string_cache = "0.8.7" diff --git a/faux-mgs/Cargo.toml b/faux-mgs/Cargo.toml index 9e608048..e2733475 100644 --- a/faux-mgs/Cargo.toml +++ b/faux-mgs/Cargo.toml @@ -11,18 +11,23 @@ clap.workspace = true futures.workspace = true glob.workspace = true hex.workspace = true +humantime.workspace = true nix.workspace = true +rand.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true slog.workspace = true slog-async.workspace = true slog-term.workspace = true +ssh-agent-client-rs.workspace = true +ssh-key.workspace = true termios.workspace = true tokio.workspace = true tokio-stream.workspace = true tokio-util.workspace = true uuid = { workspace = true, features = ["std", "v4"] } +zerocopy.workspace = true gateway-messages = { workspace = true, features = ["std"] } gateway-sp-comms.workspace = true diff --git a/faux-mgs/src/main.rs b/faux-mgs/src/main.rs index 3f1c5074..cb0a93dd 100644 --- a/faux-mgs/src/main.rs +++ b/faux-mgs/src/main.rs @@ -38,6 +38,7 @@ use gateway_sp_comms::SwitchPortConfig; use gateway_sp_comms::VersionedSpState; use gateway_sp_comms::MGS_PORT; use serde_json::json; +use slog::debug; use slog::info; use slog::o; use slog::warn; @@ -56,6 +57,7 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use uuid::Uuid; +use zerocopy::AsBytes; mod picocom_map; mod usart; @@ -419,14 +421,38 @@ enum LedCommand { enum MonorailCommand { /// Unlock the technician port, allowing access to other SPs Unlock { - /// How long to unlock for - time_sec: u32, + #[clap(flatten)] + cmd: UnlockGroup, + + /// Public key for SSH signing challenge + /// + /// This is either a path to a public key (ending in `.pub`), or a + /// substring to match against known keys (which can be printed with + /// `faux-mgs monorail unlock --list`). + #[clap(short, long, conflicts_with = "list")] + key: Option, + + /// Path to the SSH agent socket + #[clap(long, env)] + ssh_auth_sock: Option, }, /// Lock the technician port Lock, } +#[derive(Clone, Debug, clap::Args)] +#[group(required = true, multiple = false)] +pub struct UnlockGroup { + /// How long to unlock for + #[clap(short, long)] + time: Option, + + /// List available keys + #[clap(short, long)] + list: bool, +} + #[derive(ValueEnum, Debug, Clone)] enum CfpaSlot { Active, @@ -778,6 +804,21 @@ async fn main() -> Result<()> { Ok(()) } +fn get_ssh_client + std::fmt::Debug>( + socket: P, +) -> Result { + let client = ssh_agent_client_rs::Client::connect(socket.as_ref()) + .with_context(|| { + format!("failed to connect to SSH agent on {socket:?}") + })?; + Ok(client) +} + +fn ssh_list_keys(socket: &PathBuf) -> Result> { + let mut client = get_ssh_client(socket)?; + client.list_identities().context("failed to list identities") +} + async fn run_command( sp: SingleSp, command: Command, @@ -1318,8 +1359,32 @@ async fn run_command( ) .await? } - MonorailCommand::Unlock { time_sec } => { - monorail_unlock(&log, &sp, time_sec).await?; + MonorailCommand::Unlock { + cmd: UnlockGroup { time, list }, + key, + ssh_auth_sock, + } => { + if list { + let Some(ssh_auth_sock) = ssh_auth_sock else { + bail!("must provide --ssh-auth-sock"); + }; + for k in ssh_list_keys(&ssh_auth_sock)? { + println!("{}", k.to_openssh()?); + } + } else { + let time_sec = time.unwrap().as_secs_f32() as u32; + if time_sec == 0 { + bail!("--time must be >= 1 second"); + } + monorail_unlock( + &log, + &sp, + time_sec, + ssh_auth_sock, + key, + ) + .await?; + } } } if json { @@ -1435,6 +1500,8 @@ async fn monorail_unlock( log: &Logger, sp: &SingleSp, time_sec: u32, + socket: Option, + pub_key: Option, ) -> Result<()> { let r = sp .component_action_with_response( @@ -1457,6 +1524,83 @@ async fn monorail_unlock( UnlockChallenge::Trivial { timestamp } => { UnlockResponse::Trivial { timestamp } } + UnlockChallenge::EcdsaSha2Nistp256(data) => { + let Some(socket) = socket else { + bail!("must provide --ssh-auth-sock"); + }; + let keys = ssh_list_keys(&socket)?; + let pub_key = if keys.len() == 1 && pub_key.is_none() { + keys[0].clone() + } else { + let Some(pub_key) = pub_key else { + bail!( + "need --key for ECDSA challenge; \ + multiple keys are available" + ); + }; + if pub_key.ends_with(".pub") { + ssh_key::PublicKey::read_openssh_file(Path::new(&pub_key)) + .with_context(|| { + format!("could not read key from {pub_key:?}") + })? + } else { + let mut found = None; + for k in keys.iter() { + if k.to_openssh()?.contains(&pub_key) { + if found.is_some() { + bail!("multiple keys contain '{pub_key}'"); + } + found = Some(k); + } + } + let Some(found) = found else { + bail!( + "could not match '{pub_key}'; \ + use `faux-mgs monorail unlock --list` \ + to print keys" + ); + }; + found.clone() + } + }; + + let mut data = data.as_bytes().to_vec(); + let signer_nonce: [u8; 8] = rand::random(); + data.extend(signer_nonce); + + let signed = ssh_keygen_sign(socket, pub_key, &data)?; + debug!(log, "got signature {signed:?}"); + + let key_bytes = + signed.public_key().ecdsa().unwrap().as_sec1_bytes(); + assert_eq!(key_bytes.len(), 65, "invalid key length"); + let mut key = [0u8; 65]; + key.copy_from_slice(key_bytes); + + // Signature bytes are encoded per + // https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2 + // + // They are a pair of `mpint` values, per + // https://datatracker.ietf.org/doc/html/rfc4251 + // + // Each one is either 32 bytes or 33 bytes with a leading zero, so + // we'll awkwardly allow for both cases. + let mut r = std::io::Cursor::new(signed.signature_bytes()); + use std::io::Read; + let mut signature = [0u8; 64]; + for i in 0..2 { + let mut size = [0u8; 4]; + r.read_exact(&mut size)?; + match u32::from_be_bytes(size) { + 32 => (), + 33 => r.read_exact(&mut [0u8])?, // eat the leading byte + _ => bail!("invalid length {i}"), + } + r.read_exact(&mut signature[i * 32..][..32])?; + } + + UnlockResponse::EcdsaSha2Nistp256 { key, signer_nonce, signature } + } }; sp.component_action( SpComponent::MONORAIL, @@ -1471,6 +1615,34 @@ async fn monorail_unlock( Ok(()) } +fn ssh_keygen_sign( + socket: PathBuf, + pub_key: ssh_key::PublicKey, + data: &[u8], +) -> Result { + use ssh_key::{Algorithm, EcdsaCurve, HashAlg, SshSig}; + + let mut client = get_ssh_client(socket)?; + + const NAMESPACE: &str = "monorail-unlock"; + const HASH: HashAlg = HashAlg::Sha256; + let blob = SshSig::signed_data(NAMESPACE, HASH, data)?; + + let sig = client.sign(&pub_key, &blob)?; + let sig = SshSig::new(pub_key.into(), NAMESPACE, HASH, sig)?; + + // Confirm that the signature is of the expected form + match sig.algorithm() { + Algorithm::Ecdsa { curve: EcdsaCurve::NistP256 } => {} + h => bail!("invalid signature algorithm {h:?}"), + } + match sig.hash_alg() { + HashAlg::Sha256 => {} + h => bail!("invalid hash algorithm {h:?}"), + } + Ok(sig) +} + fn handle_cxpa( name: &str, data: [u8; ROT_PAGE_SIZE], diff --git a/gateway-messages/src/lib.rs b/gateway-messages/src/lib.rs index c3b680b2..82863e44 100644 --- a/gateway-messages/src/lib.rs +++ b/gateway-messages/src/lib.rs @@ -66,7 +66,7 @@ pub const ROT_PAGE_SIZE: usize = 512; /// for more detail and discussion. pub mod version { pub const MIN: u32 = 2; - pub const CURRENT: u32 = 14; + pub const CURRENT: u32 = 15; /// MGS protocol version in which SP watchdog messages were added pub const WATCHDOG_VERSION: u32 = 12; diff --git a/gateway-messages/src/mgs_to_sp.rs b/gateway-messages/src/mgs_to_sp.rs index d30aee5b..86f1f71a 100644 --- a/gateway-messages/src/mgs_to_sp.rs +++ b/gateway-messages/src/mgs_to_sp.rs @@ -16,6 +16,7 @@ use crate::UpdateId; use hubpack::SerializedSize; use serde::Deserialize; use serde::Serialize; +use serde_big_array::BigArray; #[derive( Debug, Clone, Copy, SerializedSize, Serialize, Deserialize, PartialEq, Eq, @@ -278,6 +279,7 @@ pub struct ComponentUpdatePrepare { #[derive( Copy, Clone, Serialize, SerializedSize, Deserialize, PartialEq, Eq, Debug, )] +#[allow(clippy::large_enum_variant)] pub enum ComponentAction { Led(LedComponentAction), Monorail(MonorailComponentAction), @@ -297,6 +299,7 @@ pub enum LedComponentAction { #[derive( Copy, Clone, Serialize, SerializedSize, Deserialize, PartialEq, Eq, Debug, )] +#[allow(clippy::large_enum_variant)] pub enum MonorailComponentAction { /// Request an `UnlockChallenge` /// @@ -322,6 +325,34 @@ pub enum MonorailComponentAction { pub enum UnlockChallenge { /// Unlock given an [UnlockResponse::Trivial] with the same timestamp Trivial { timestamp: u64 }, + + /// Hash and sign the given data with a key that the SP trusts + /// + /// Trusted keys are hardcoded into SP firmware. + EcdsaSha2Nistp256(EcdsaSha2Nistp256Challenge), +} + +#[derive( + Copy, + Clone, + Serialize, + SerializedSize, + Deserialize, + PartialEq, + Eq, + Debug, + zerocopy::AsBytes, +)] +#[repr(C)] +pub struct EcdsaSha2Nistp256Challenge { + /// Hardware ID, e.g. serial name + pub hw_id: [u8; 32], + /// Software version ID + pub sw_id: [u8; 4], + /// Time (according to the SP's internal clock) + pub time: [u8; 8], + /// Additional nonce chosen by the SP + pub nonce: [u8; 32], } /// Response to an [`UnlockChallenge`] @@ -329,7 +360,22 @@ pub enum UnlockChallenge { Copy, Clone, Serialize, SerializedSize, Deserialize, PartialEq, Eq, Debug, )] pub enum UnlockResponse { + /// Unlocks [UnlockChallenge::Trivial] Trivial { timestamp: u64 }, + + /// Here's your signature! + EcdsaSha2Nistp256 { + /// SEC1 encoding of the corresponding public key + #[serde(with = "BigArray")] + key: [u8; 65], + + /// Additional nonce chosen by the signer + signer_nonce: [u8; 8], + + /// Signature data, as an (x, y) tuple (32 bytes each) + #[serde(with = "BigArray")] + signature: [u8; 64], + }, } #[derive( diff --git a/gateway-messages/tests/versioning/mod.rs b/gateway-messages/tests/versioning/mod.rs index 4a05b737..ca957fbb 100644 --- a/gateway-messages/tests/versioning/mod.rs +++ b/gateway-messages/tests/versioning/mod.rs @@ -19,6 +19,7 @@ mod v11; mod v12; mod v13; mod v14; +mod v15; pub fn assert_serialized( out: &mut [u8], diff --git a/gateway-messages/tests/versioning/v15.rs b/gateway-messages/tests/versioning/v15.rs new file mode 100644 index 00000000..d7939593 --- /dev/null +++ b/gateway-messages/tests/versioning/v15.rs @@ -0,0 +1,167 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! This source file is named after the protocol version being tested, +//! e.g. v01.rs implements tests for protocol version 1. +//! The tested protocol version is represented by "$VERSION" below. +//! +//! The tests in this module check that the serialized form of messages from MGS +//! protocol version $VERSION have not changed. +//! +//! If a test in this module fails, _do not change the test_! This means you +//! have changed, deleted, or reordered an existing message type or enum +//! variant, and you should revert that change. This will remain true until we +//! bump the `version::MIN` to a value higher than $VERSION, at which point these +//! tests can be removed as we will stop supporting $VERSION. + +use super::assert_serialized; +use gateway_messages::ComponentAction; +use gateway_messages::ComponentActionResponse; +use gateway_messages::EcdsaSha2Nistp256Challenge; +use gateway_messages::MonorailComponentAction; +use gateway_messages::MonorailComponentActionResponse; +use gateway_messages::SerializedSize; +use gateway_messages::SpResponse; +use gateway_messages::UnlockChallenge; +use gateway_messages::UnlockResponse; + +#[test] +fn monorail_component_action() { + let mut out = [0; ComponentAction::MAX_SIZE]; + + #[rustfmt::skip] + let action = ComponentAction::Monorail(MonorailComponentAction::Unlock { + challenge: UnlockChallenge::EcdsaSha2Nistp256( + EcdsaSha2Nistp256Challenge { + hw_id: [ + 8, 8, 9, 0, 3, 3, 3, 3, + 1, 1, 1, 1, 2, 2, 2, 2, + 5, 5, 5, 5, 5, 6, 7, 8, + 6, 6, 6, 6, 6, 7, 8, 9, + ], + sw_id: [8, 8, 9, 0], + time: [0, 0, 0, 0, 1, 2, 3, 4], + nonce: [ + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + ], + } + ), + response: UnlockResponse::EcdsaSha2Nistp256 { + key: [ + 123, + 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 8, 8, 8, + ], + signer_nonce: [1, 2, 3, 4, 5, 6, 7, 8], + signature: [ + 8, 8, 8, 8, 8, 8, 8, 8, + 7, 7, 7, 7, 7, 7, 7, 7, + 6, 6, 6, 6, 6, 6, 6, 6, + 5, 5, 5, 5, 5, 5, 5, 5, + 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, + ], + }, + time_sec: 0x1234, + }); + #[rustfmt::skip] + let expected = vec![ + 1, // ComponentAction::Monorail + 1, // MonorailComponentAction::Unlock + 1, // UnlockChallenge::EcdsaSha2Nistp256 + 8, 8, 9, 0, 3, 3, 3, 3, // hw_id + 1, 1, 1, 1, 2, 2, 2, 2, + 5, 5, 5, 5, 5, 6, 7, 8, + 6, 6, 6, 6, 6, 7, 8, 9, + 8, 8, 9, 0, // sw_id + 0, 0, 0, 0, 1, 2, 3, 4, // time + 1, 2, 3, 4, 5, 6, 7, 8, // nonce + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + + 1, // UnlockResponse::EcdsaSha2Nistp256 + 123, // key + 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 8, 8, 8, + + // signer_nonce + 1, 2, 3, 4, 5, 6, 7, 8, + + 8, 8, 8, 8, 8, 8, 8, 8, // signature + 7, 7, 7, 7, 7, 7, 7, 7, + 6, 6, 6, 6, 6, 6, 6, 6, + 5, 5, 5, 5, 5, 5, 5, 5, + 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, + + 0x34, 0x12, 0, 0, // time_s + ]; + assert_serialized(&mut out, &expected, &action); +} +#[test] +fn component_action_response() { + let mut out = [0; SpResponse::MAX_SIZE]; + + #[rustfmt::skip] + let r = SpResponse::ComponentAction(ComponentActionResponse::Monorail( + MonorailComponentActionResponse::RequestChallenge( + UnlockChallenge::EcdsaSha2Nistp256( + EcdsaSha2Nistp256Challenge { + hw_id: [ + 8, 8, 9, 0, 3, 3, 3, 3, + 1, 1, 1, 1, 2, 2, 2, 2, + 5, 5, 5, 5, 5, 6, 7, 8, + 6, 6, 6, 6, 6, 7, 8, 9, + ], + sw_id: [8, 8, 9, 0], + time: [0, 0, 0, 0, 1, 2, 3, 4], + nonce: [ + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + ], + } + ) + ) + )); + #[rustfmt::skip] + let expected = vec![ + 46, // ComponentAction + 1, // MonorailAction + 0, // RequestChallenge + 1, // EcdsaSha2Nistp256 + 8, 8, 9, 0, 3, 3, 3, 3, // hw_id + 1, 1, 1, 1, 2, 2, 2, 2, + 5, 5, 5, 5, 5, 6, 7, 8, + 6, 6, 6, 6, 6, 7, 8, 9, + 8, 8, 9, 0, // sw_id + 0, 0, 0, 0, 1, 2, 3, 4, // time + 1, 2, 3, 4, 5, 6, 7, 8, // nonce + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, + ]; + assert_serialized(&mut out, &expected, &r); +}