From e27bf32d83bf536207ad7f62ca033d93315a8096 Mon Sep 17 00:00:00 2001 From: Matt Johnston <matt@ucc.asn.au> Date: Mon, 22 May 2023 00:18:57 +0800 Subject: [PATCH] Progress on picow config menu A new GlobalState struct is passed around, with sessions taking a local copy of SSHConfig. - Can switch to usb serial mode, various other improvements to usb serial, can exit cleanly. - Wifi config code written, but is unsted because it's only accessible by wifi... - hostkeys() behaviour now returns a heapless::Vec --- embassy/demos/common/Cargo.toml | 1 + embassy/demos/common/src/config.rs | 28 ++- embassy/demos/common/src/lib.rs | 1 + embassy/demos/common/src/menu.rs | 1 - embassy/demos/common/src/server.rs | 43 ++-- embassy/demos/picow/Cargo.lock | 2 + embassy/demos/picow/Cargo.toml | 1 + embassy/demos/picow/src/flashconfig.rs | 4 +- embassy/demos/picow/src/main.rs | 200 +++++++++------- embassy/demos/picow/src/picowmenu.rs | 318 +++++++++++++++++++++++++ embassy/demos/picow/src/takepipe.rs | 10 + embassy/demos/picow/src/wifi.rs | 17 +- embassy/demos/std/Cargo.lock | 1 + embassy/demos/std/src/main.rs | 21 +- embassy/demos/std/src/setupmenu.rs | 127 ++++++++++ src/behaviour.rs | 4 +- src/kex.rs | 3 +- src/sign.rs | 2 +- 18 files changed, 659 insertions(+), 125 deletions(-) create mode 100644 embassy/demos/picow/src/picowmenu.rs create mode 100644 embassy/demos/std/src/setupmenu.rs diff --git a/embassy/demos/common/Cargo.toml b/embassy/demos/common/Cargo.toml index 15e3728..cbdc68c 100644 --- a/embassy/demos/common/Cargo.toml +++ b/embassy/demos/common/Cargo.toml @@ -16,6 +16,7 @@ embassy-sync = { version = "0.2.0" } embassy-net = { version = "0.1.0", features = ["tcp", "dhcpv4", "medium-ethernet", "nightly"] } embassy-net-driver = { version = "0.1.0" } embassy-futures = { version = "0.1.0" } +embassy-time = { version = "0.1" } heapless = "0.7.15" # using local fork diff --git a/embassy/demos/common/src/config.rs b/embassy/demos/common/src/config.rs index 1cb6ec5..1d7e7e1 100644 --- a/embassy/demos/common/src/config.rs +++ b/embassy/demos/common/src/config.rs @@ -13,21 +13,26 @@ use { #[cfg(feature = "defmt")] use defmt::{debug, info, warn, panic, error, trace}; -use heapless::String; +use heapless::{String, Vec}; use sunset_sshwire_derive::*; use sunset::sshwire; use sunset::sshwire::{BinString, SSHEncode, SSHDecode, WireResult, SSHSource, SSHSink, WireError}; use sunset::{SignKey, KeyType}; +use sunset::packets::Ed25519PubKey; // Be sure to bump picow flash_config::CURRENT_VERSION // if this struct changes (or encode/decode impls). -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SSHConfig { pub hostkey: SignKey, + /// login password pub pw_hash: Option<[u8; 32]>, + // 3 slots + pub auth_keys: [Option<Ed25519PubKey>; 3], + /// SSID pub wifi_net: String<32>, /// WPA2 passphrase. None is Open network. @@ -41,11 +46,12 @@ impl SSHConfig { pub fn new() -> Result<Self> { let hostkey = SignKey::generate(KeyType::Ed25519, None)?; - let wifi_net = option_env!("WIFI_NETWORK").unwrap_or("guest").into(); - let wifi_pw = option_env!("WIFI_PASSWORD").map(|p| p.into()); + let wifi_net = option_env!("WIFI_NET").unwrap_or("guest").into(); + let wifi_pw = option_env!("WIFI_PW").map(|p| p.into()); Ok(SSHConfig { hostkey, pw_hash: None, + auth_keys: Default::default(), wifi_net, wifi_pw, }) @@ -68,9 +74,15 @@ fn dec_signkey<'de, S>(s: &mut S) -> WireResult<SignKey> where S: SSHSource<'de> impl SSHEncode for SSHConfig { fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { enc_signkey(&self.hostkey, s)?; + self.pw_hash.is_some().enc(s)?; self.pw_hash.enc(s)?; + for k in self.auth_keys.iter() { + k.is_some().enc(s)?; + k.enc(s)?; + } + self.wifi_net.as_str().enc(s)?; self.wifi_pw.is_some().enc(s)?; @@ -88,6 +100,13 @@ impl<'de> SSHDecode<'de> for SSHConfig { let have_pw_hash = bool::dec(s)?; let pw_hash = have_pw_hash.then(|| SSHDecode::dec(s)).transpose()?; + let mut auth_keys = [None, None, None]; + for k in auth_keys.iter_mut() { + if bool::dec(s)? { + *k = Some(SSHDecode::dec(s)).transpose()?; + } + } + let wifi_net = <&str>::dec(s)?.into(); let have_wifi_pw = bool::dec(s)?; @@ -99,6 +118,7 @@ impl<'de> SSHDecode<'de> for SSHConfig { Ok(Self { hostkey, pw_hash, + auth_keys, wifi_net, wifi_pw, }) diff --git a/embassy/demos/common/src/lib.rs b/embassy/demos/common/src/lib.rs index 426fd96..7cdcecb 100644 --- a/embassy/demos/common/src/lib.rs +++ b/embassy/demos/common/src/lib.rs @@ -12,3 +12,4 @@ pub mod demo_menu; pub use server::{Shell, listener}; pub use config::SSHConfig; +pub use demo_menu::BufOutput; diff --git a/embassy/demos/common/src/menu.rs b/embassy/demos/common/src/menu.rs index 633f411..4c7afa5 100644 --- a/embassy/demos/common/src/menu.rs +++ b/embassy/demos/common/src/menu.rs @@ -6,7 +6,6 @@ //! //! A basic command-line interface for `#![no_std]` Rust programs. Peforms //! zero heap allocation. -#![no_std] #![deny(missing_docs)] /// The type of function we call when we enter/exit a menu. diff --git a/embassy/demos/common/src/server.rs b/embassy/demos/common/src/server.rs index 52cccaf..ab116b0 100644 --- a/embassy/demos/common/src/server.rs +++ b/embassy/demos/common/src/server.rs @@ -15,13 +15,15 @@ use embassy_net::tcp::TcpSocket; use embassy_net::Stack; use embassy_net_driver::Driver; use embassy_futures::join::join; +use embassy_futures::select::{select, Either}; +use embassy_time::{Duration, Timer}; use embedded_io::asynch; use heapless::String; use sunset::*; -use sunset_embassy::SSHServer; +use sunset_embassy::{SSHServer, SunsetMutex}; use crate::SSHConfig; @@ -46,7 +48,7 @@ macro_rules! singleton { // common entry point pub async fn listener<D: Driver, S: Shell>(stack: &'static Stack<D>, - config: &SSHConfig, + config: &SunsetMutex<SSHConfig>, init: S::Init) -> ! { // TODO: buffer size? // Does it help to be larger than ethernet MTU? @@ -58,7 +60,7 @@ pub async fn listener<D: Driver, S: Shell>(stack: &'static Stack<D>, loop { let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); - // TODO: disable nagle. smoltcp supports it, requires embassy-net addition + // socket.set_nagle_enabled(false); info!("Listening on TCP:22..."); if let Err(_) = socket.accept(22).await { @@ -71,11 +73,18 @@ pub async fn listener<D: Driver, S: Shell>(stack: &'static Stack<D>, // warn!("Ended with error: {:?}", e); warn!("Ended with error"); } + + // Make sure a TCP socket reset is sent to the remote host + socket.abort(); + + // TODO: Replace this with something proper like + // https://github.com/embassy-rs/embassy/pull/1471 + Timer::after(Duration::from_millis(200)).await; } } /// Run a SSH session when a socket accepts a connection -async fn session<S: Shell>(socket: &mut TcpSocket<'_>, config: &SSHConfig, +async fn session<S: Shell>(socket: &mut TcpSocket<'_>, config: &SunsetMutex<SSHConfig>, init: &S::Init) -> sunset::Result<()> { // OK unwrap: has been accepted let src = socket.remote_endpoint().unwrap(); @@ -83,7 +92,8 @@ async fn session<S: Shell>(socket: &mut TcpSocket<'_>, config: &SSHConfig, let shell = S::new(init); - let app = DemoServer::new(&shell, config)?; + let conf = config.lock().await.clone(); + let app = DemoServer::new(&shell, conf)?; let app = Mutex::<NoopRawMutex, _>::new(app); let mut ssh_rxbuf = [0; 2000]; @@ -97,18 +107,17 @@ async fn session<S: Shell>(socket: &mut TcpSocket<'_>, config: &SSHConfig, let run = serv.run(&mut rsock, &mut wsock, &app); - let (r1, r2) = join(run, session).await; - r1?; - r2?; + let f = select(run, session).await; + match f { + Either::First(r) => r?, + Either::Second(r) => r?, + } Ok(()) } struct DemoServer<'a, S: Shell> { - config: &'a SSHConfig, - - // references config - hostkeys: [&'a SignKey; 1], + config: SSHConfig, handle: Option<ChanHandle>, sess: Option<ChanNum>, @@ -117,21 +126,21 @@ struct DemoServer<'a, S: Shell> { } impl<'a, S: Shell> DemoServer<'a, S> { - fn new(shell: &'a S, config: &'a SSHConfig) -> Result<Self> { + fn new(shell: &'a S, config: SSHConfig) -> Result<Self> { Ok(Self { handle: None, sess: None, config, shell, - hostkeys: [&config.hostkey], }) } } impl<'a, S: Shell> ServBehaviour for DemoServer<'a, S> { - fn hostkeys(&mut self) -> BhResult<&[&SignKey]> { - Ok(&self.hostkeys) + fn hostkeys(&mut self) -> BhResult<heapless::Vec<&SignKey, 2>> { + // OK unwrap: only one element + Ok(heapless::Vec::from_slice(&[&self.config.hostkey]).unwrap()) } async fn auth_unchallenged(&mut self, username: TextString<'_>) -> bool { @@ -174,7 +183,7 @@ impl<'a, S: Shell> ServBehaviour for DemoServer<'a, S> { } pub trait Shell { - type Init; + type Init: Copy; fn new(init: &Self::Init) -> Self; diff --git a/embassy/demos/picow/Cargo.lock b/embassy/demos/picow/Cargo.lock index 5c3578e..363ef47 100644 --- a/embassy/demos/picow/Cargo.lock +++ b/embassy/demos/picow/Cargo.lock @@ -1742,6 +1742,7 @@ dependencies = [ "embassy-net", "embassy-net-driver", "embassy-sync 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "embassy-time 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "embedded-io 0.4.0", "heapless", "log", @@ -1774,6 +1775,7 @@ dependencies = [ "embedded-hal 1.0.0-alpha.10", "embedded-hal-async", "embedded-io 0.4.0", + "futures", "getrandom", "heapless", "log", diff --git a/embassy/demos/picow/Cargo.toml b/embassy/demos/picow/Cargo.toml index f70f973..0612258 100644 --- a/embassy/demos/picow/Cargo.toml +++ b/embassy/demos/picow/Cargo.toml @@ -33,6 +33,7 @@ defmt = { version = "0.3", optional = true } defmt-rtt = "0.3" panic-probe = { version = "0.3", features = ["print-defmt"] } log = { version = "0.4" } +futures = { version = "0.3", default-features = false } cortex-m = { version = "0.7.6", features = ["critical-section-single-core"]} cortex-m-rt = "0.7.0" diff --git a/embassy/demos/picow/src/flashconfig.rs b/embassy/demos/picow/src/flashconfig.rs index 5cdba04..ccf9d93 100644 --- a/embassy/demos/picow/src/flashconfig.rs +++ b/embassy/demos/picow/src/flashconfig.rs @@ -27,11 +27,11 @@ use sunset::sshwire::OwnOrBorrow; use crate::demo_common::SSHConfig; // bump this when the format changes -const CURRENT_VERSION: u8 = 1; +const CURRENT_VERSION: u8 = 2; // TODO: unify offsets with wifi's romfw feature const CONFIG_OFFSET: u32 = 0x150000; -const FLASH_SIZE: usize = 2*1024*1024; +pub const FLASH_SIZE: usize = 2*1024*1024; #[derive(SSHEncode, SSHDecode)] struct FlashConfig<'a> { diff --git a/embassy/demos/picow/src/main.rs b/embassy/demos/picow/src/main.rs index fb4b19f..5095f8e 100644 --- a/embassy/demos/picow/src/main.rs +++ b/embassy/demos/picow/src/main.rs @@ -19,7 +19,9 @@ use {defmt_rtt as _, panic_probe as _}; use embassy_executor::Spawner; use embassy_net::Stack; use embassy_futures::join::join; +use embassy_futures::select::select; use embassy_rp::{pio::PioPeripheral, interrupt}; +use embassy_rp::peripherals::FLASH; use embedded_io::{asynch, Io}; use embedded_io::asynch::Write; @@ -41,6 +43,7 @@ use crate::demo_common::singleton; mod flashconfig; mod wifi; mod usbserial; +mod picowmenu; mod takepipe; use demo_common::{SSHConfig, demo_menu, Shell}; @@ -68,8 +71,12 @@ async fn main(spawner: Spawner) { flashconfig::load_or_create(&mut flash).unwrap() }; - let ssh_config = &*singleton!( - config + let flash = &*singleton!( + SunsetMutex::new(flash) + ); + + let config = &*singleton!( + SunsetMutex::new(config) ); let usb_pipe = singleton!(takepipe::TakePipe::new()); @@ -77,131 +84,158 @@ async fn main(spawner: Spawner) { let usb_irq = interrupt::take!(USBCTRL_IRQ); spawner.spawn(usb_serial_task(p.USB, usb_irq, usb_pipe)).unwrap(); - let (_, sm, _, _, _) = p.PIO0.split(); - let wifi_net = ssh_config.wifi_net.as_str(); - let wifi_pw = ssh_config.wifi_pw.as_ref().map(|p| p.as_str()); + let (wifi_net, wifi_pw) = { + let c = config.lock().await; + (c.wifi_net.clone(), c.wifi_pw.clone()) + }; // spawn the wifi stack + let (_, sm, _, _, _) = p.PIO0.split(); let (stack, wifi_control) = wifi::wifi_stack(&spawner, p.PIN_23, p.PIN_24, p.PIN_25, p.PIN_29, p.DMA_CH0, sm, wifi_net, wifi_pw).await; let stack = &*singleton!(stack); let wifi_control = singleton!(SunsetMutex::new(wifi_control)); spawner.spawn(net_task(&stack)).unwrap(); - let init = DemoShellInit { + let state = GlobalState { usb_pipe, wifi_control, + config, + flash, }; - let init = singleton!(init); + let state = singleton!(state); for _ in 0..NUM_LISTENERS { - spawner.spawn(listener(&stack, &ssh_config, init)).unwrap(); + spawner.spawn(listener(&stack, config, state)).unwrap(); } } // TODO: pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] async fn listener(stack: &'static Stack<cyw43::NetDriver<'static>>, - config: &'static SSHConfig, - ctx: &'static DemoShellInit) -> ! { + config: &'static SunsetMutex<SSHConfig>, + ctx: &'static GlobalState) -> ! { demo_common::listener::<_, DemoShell>(stack, config, ctx).await } -struct DemoShellInit { - usb_pipe: &'static TakeBase<'static>, - wifi_control: &'static SunsetMutex<cyw43::Control<'static>>, +pub(crate) struct GlobalState { + // If taking multiple mutexes, lock in the order below avoid inversion. + + pub usb_pipe: &'static TakeBase<'static>, + pub wifi_control: &'static SunsetMutex<cyw43::Control<'static>>, + pub config: &'static SunsetMutex<SSHConfig>, + pub flash: &'static SunsetMutex<embassy_rp::flash::Flash<'static, + FLASH, { flashconfig::FLASH_SIZE }>>, } struct DemoShell { notify: Signal<NoopRawMutex, ChanHandle>, - ctx: &'static DemoShellInit, + ctx: &'static GlobalState, // Mutex is a bit of a bodge username: SunsetMutex<String<20>>, } -impl DemoShell { - async fn menu<C>(&self, mut stdio: C) -> Result<()> - where C: asynch::Read + asynch::Write + Io<Error=sunset::Error> { - let mut menu_buf = [0u8; 64]; - let menu_out = demo_menu::BufOutput::default(); +async fn menu<R, W>(mut chanr: R, mut chanw: W, + state: &'static GlobalState) -> Result<()> + where R: asynch::Read+Io<Error=sunset::Error>, + W: asynch::Write+Io<Error=sunset::Error> { + let mut menu_buf = [0u8; 64]; + let menu_ctx = picowmenu::MenuCtx::new(state); - let mut menu = MenuRunner::new(&demo_menu::ROOT_MENU, &mut menu_buf, menu_out); + let mut menu = MenuRunner::new(&picowmenu::SETUP_MENU, &mut menu_buf, menu_ctx); - // bodge - for c in "help\r\n".bytes() { - menu.input_byte(c); - } - menu.context.flush(&mut stdio).await?; + // bodge + for c in "help\r\n".bytes() { + menu.input_byte(c); + } + menu.context.out.flush(&mut chanw).await?; - loop { - let mut b = [0u8; 20]; - let lr = stdio.read(&mut b).await?; - if lr == 0 { - break + 'io: loop { + let mut b = [0u8; 20]; + let lr = chanr.read(&mut b).await?; + if lr == 0 { + break + } + let b = &mut b[..lr]; + for c in b.iter() { + menu.input_byte(*c); + menu.context.out.flush(&mut chanw).await?; + + // TODO: move this to a function or something + if menu.context.switch_usb1 { + serial(chanr, chanw, state).await?; + // TODO we could return to the menu on serial error? + break 'io; } - let b = &mut b[..lr]; - for c in b.iter() { - menu.input_byte(*c); - menu.context.flush(&mut stdio).await?; + + if menu.context.need_save { + // clear regardless of success, don't want a tight loop. + menu.context.need_save = false; + + let conf = state.config.lock().await; + let mut fl = state.flash.lock().await; + if let Err(_e) = flashconfig::save(&mut fl, &conf) { + warn!("Error writing flash"); + } } } - Ok(()) } + Ok(()) +} + +async fn serial<R, W>(mut chanr: R, mut chanw: W, state: &'static GlobalState) -> Result<()> + where R: asynch::Read+Io<Error=sunset::Error>, + W: asynch::Write+Io<Error=sunset::Error> { - async fn serial<R, W>(&self, mut sshr: R, mut sshw: W) -> Result<()> - where R: asynch::Read+Io<Error=sunset::Error>, - W: asynch::Write+Io<Error=sunset::Error> { - - let (mut rx, mut tx) = self.ctx.usb_pipe.take().await; - let r = async { - // TODO: could have a single buffer to translate in-place. - const DOUBLE: usize = 2*takepipe::READ_SIZE; - let mut b = [0u8; takepipe::READ_SIZE]; - let mut btrans = Vec::<u8, DOUBLE>::new(); - loop { - let n = rx.read(&mut b).await?; - let b = &mut b[..n]; - btrans.clear(); - for c in b { - if *c == b'\n' { - // OK unwrap: btrans.len() = 2*b.len() - btrans.push(b'\r').unwrap(); - } - btrans.push(*c).unwrap(); + let (mut rx, mut tx) = state.usb_pipe.take().await; + let r = async { + // TODO: could have a single buffer to translate in-place. + const DOUBLE: usize = 2*takepipe::READ_SIZE; + let mut b = [0u8; takepipe::READ_SIZE]; + let mut btrans = Vec::<u8, DOUBLE>::new(); + loop { + let n = rx.read(&mut b).await?; + let b = &mut b[..n]; + btrans.clear(); + for c in b { + if *c == b'\n' { + // OK unwrap: btrans.len() = 2*b.len() + btrans.push(b'\r').unwrap(); } - sshw.write_all(&btrans).await?; + btrans.push(*c).unwrap(); } - #[allow(unreachable_code)] - Ok::<(), sunset::Error>(()) - }; - let w = async { - let mut b = [0u8; 64]; - loop { - let n = sshr.read(&mut b).await?; - if n == 0 { - return Err(sunset::Error::ChannelEOF); - } - let b = &mut b[..n]; - for c in b.iter_mut() { - // input translate CR to LF - if *c == b'\r' { - *c = b'\n'; - } + chanw.write_all(&btrans).await?; + } + #[allow(unreachable_code)] + Ok::<(), sunset::Error>(()) + }; + let w = async { + let mut b = [0u8; 64]; + loop { + let n = chanr.read(&mut b).await?; + if n == 0 { + return Err(sunset::Error::ChannelEOF); + } + let b = &mut b[..n]; + for c in b.iter_mut() { + // input translate CR to LF + if *c == b'\r' { + *c = b'\n'; } - tx.write_all(b).await?; } - #[allow(unreachable_code)] - Ok::<(), sunset::Error>(()) - }; + tx.write_all(b).await?; + } + #[allow(unreachable_code)] + Ok::<(), sunset::Error>(()) + }; - join(r, w).await; - info!("serial task completed"); - Ok(()) - } + select(r, w).await; + info!("serial task completed"); + Ok(()) } impl Shell for DemoShell { - type Init = &'static DemoShellInit; + type Init = &'static GlobalState; fn new(ctx: &Self::Init) -> Self { Self { @@ -228,9 +262,9 @@ impl Shell for DemoShell { let stdio = serv.stdio(chan_handle).await?; if *self.username.lock().await == "serial" { - self.serial(stdio.clone(), stdio).await + serial(stdio.clone(), stdio, self.ctx).await } else { - self.menu(stdio).await + menu(stdio.clone(), stdio, self.ctx).await } }; diff --git a/embassy/demos/picow/src/picowmenu.rs b/embassy/demos/picow/src/picowmenu.rs new file mode 100644 index 0000000..ae5c4e7 --- /dev/null +++ b/embassy/demos/picow/src/picowmenu.rs @@ -0,0 +1,318 @@ +use core::fmt::Write; +use core::future::{poll_fn, Future}; +use core::sync::atomic::Ordering::{Relaxed, SeqCst}; +use core::ops::DerefMut; + + +use embassy_sync::waitqueue::MultiWakerRegistration; + +use crate::demo_common; +use crate::GlobalState; +use demo_common::{BufOutput, SSHConfig}; + +use demo_common::menu::*; + +pub(crate) struct MenuCtx { + pub out: BufOutput, + pub state: &'static GlobalState, + + // flags to be handled by the calling async loop + pub switch_usb1: bool, + pub need_save: bool, +} + +impl MenuCtx { + pub fn new(state: &'static GlobalState) -> Self { + Self { state, out: Default::default(), switch_usb1: false, need_save: false } + } + + fn with_config<F>(&mut self, f: F) -> bool + where F: FnOnce(&mut SSHConfig, &mut BufOutput) + { + let mut c = match self.state.config.try_lock() { + Ok(c) => c, + Err(e) => { + writeln!(self, "Lock problem, try again."); + return false; + } + }; + f(c.deref_mut(), &mut self.out); + true + } +} + +impl core::fmt::Write for MenuCtx { + fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { + self.out.write_str(s) + } +} + +pub(crate) const SETUP_MENU: Menu<MenuCtx> = Menu { + label: "setup", + items: &[ + &AUTH_ITEM, + &GPIO_ITEM, + &SERIAL_ITEM, + &WIFI_ITEM, + &Item { + command: "reset", + help: Some("Reset picow. Will log out."), + item_type: ItemType::Callback { function: do_reset, parameters: &[] }, + }, + &Item { + command: "erase_config", + item_type: ItemType::Callback { + function: do_erase_config, + parameters: &[Parameter::Optional { + parameter_name: "", + help: None, + }], + }, + help: Some("Erase all config."), + }, + &Item { + command: "about", + item_type: ItemType::Callback { function: do_about, parameters: &[] }, + help: None, + }, + ], + entry: Some(enter_top), + exit: None, +}; + +const AUTH_ITEM: Item<MenuCtx> = Item { + command: "auth", + item_type: ItemType::Menu(&Menu { + label: "auth", + items: &[ + &Item { + command: "show", + item_type: ItemType::Callback { + parameters: &[], + function: do_auth_show, + }, + help: None, + }, + &Item { + command: "key", + item_type: ItemType::Callback { + parameters: &[ + Parameter::Mandatory { parameter_name: "slot", help: None }, + Parameter::Mandatory { + parameter_name: "ssh-ed25519", + help: None, + }, + Parameter::Mandatory { + parameter_name: "base64", + help: None, + }, + Parameter::Optional { + parameter_name: "comment", + help: None, + }, + ], + // help: Some( + // "An OpenSSH style ed25519 key, eg + // key ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AA...", + // ), + function: do_auth_key, + }, + help: None, + }, + &Item { + command: "password", + item_type: ItemType::Callback { + parameters: &[Parameter::Mandatory { + parameter_name: "pw", + help: None, + }], + function: do_auth_pw, + }, + help: None, + }, + ], + entry: Some(enter_auth), + exit: None, + }), + help: Some("Passwords and Keys."), +}; + +const WIFI_ITEM: Item<MenuCtx> = Item { + command: "wifi", + item_type: ItemType::Menu(&Menu { + label: "wifi", + items: &[ + &Item { + command: "net", + item_type: ItemType::Callback { + parameters: &[ + Parameter::Mandatory { parameter_name: "ssid", help: None }, + ], + function: do_wifi_net, + }, + help: None, + }, + &Item { + command: "wpa2", + item_type: ItemType::Callback { + parameters: &[ + Parameter::Mandatory { parameter_name: "password", help: None }, + ], + function: do_wifi_wpa2, + }, + help: None, + }, + &Item { + command: "open", + item_type: ItemType::Callback { + parameters: &[], + function: do_wifi_open, + }, + help: None, + }, + ], + entry: Some(wifi_entry), + exit: None, + }), + help: None, +}; + +const GPIO_ITEM: Item<MenuCtx> = Item { + command: "gpio", + item_type: ItemType::Menu(&Menu { + label: "gpio", + items: &[ + &Item { + command: "show", + item_type: ItemType::Callback { + parameters: &[], + function: do_gpio_show, + }, + help: None, + }, + &Item { + command: "set", + item_type: ItemType::Callback { + parameters: &[ + Parameter::Mandatory { parameter_name: "pin", help: None }, + Parameter::Mandatory { + parameter_name: "state", + help: Some("0/1/Z"), + }, + ], + function: do_gpio_set, + }, + help: None, + }, + ], + entry: None, + exit: None, + }), + help: Some("GPIO, todo"), +}; + + +const SERIAL_ITEM: Item<MenuCtx> = Item { + command: "serial", + item_type: ItemType::Menu(&Menu { + label: "serial", + items: &[ + &Item { + command: "usb0", + item_type: ItemType::Callback { + parameters: &[], + function: do_usb1, + }, + help: Some("Connect to if00 serial port. Disconnect to exit."), + }, + ], + entry: None, + exit: None, + }), + help: Some("Passwords and Keys."), +}; + +fn enter_top(context: &mut MenuCtx) { + writeln!(context, "In setup menu").unwrap(); +} + +fn enter_auth(context: &mut MenuCtx) { + writeln!(context, "In auth menu").unwrap(); +} + +fn do_auth_show(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) { + writeln!(context, "auth key"); +} + +fn do_auth_key(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) { + writeln!(context, "auth key"); +} + +fn do_auth_pw(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) { + writeln!(context, "this is auth pw"); +} + +fn do_gpio_show(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) { + writeln!(context, "gpio show here"); +} + +fn do_gpio_set(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) {} + +fn do_erase_config(_item: &Item<MenuCtx>, args: &[&str], context: &mut MenuCtx) { +} + +fn do_reset(_item: &Item<MenuCtx>, args: &[&str], context: &mut MenuCtx) {} + +fn do_about(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) { + let _ = writeln!(context, "Sunset SSH, USB serial\nMatt Johnston <matt@ucc.asn.au>\n"); +} + +fn do_usb1(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) { + writeln!(context, "USB serial"); + context.switch_usb1 = true; +} + +fn wifi_entry(context: &mut MenuCtx) { + context.with_config(|c, out| { + write!(out, "Wifi net {} ", c.wifi_net); + if c.wifi_pw.is_some() { + writeln!(out, "wpa2"); + } else { + writeln!(out, "open"); + } + }); +} + +fn do_wifi_net(_item: &Item<MenuCtx>, args: &[&str], context: &mut MenuCtx) { + context.with_config(|c, out| { + let net = args[0]; + if c.wifi_net.capacity() > net.len() { + writeln!(out, "Too long"); + return; + } + c.wifi_net = net.into(); + }); + context.need_save = true; + wifi_entry(context); +} + +fn do_wifi_wpa2(_item: &Item<MenuCtx>, args: &[&str], context: &mut MenuCtx) { + context.with_config(|c, out| { + let pw = args[0]; + if pw.len() > 63 { + writeln!(out, "Too long"); + return; + } + c.wifi_pw = Some(pw.into()) + }); + context.need_save = true; + wifi_entry(context); +} + +fn do_wifi_open(_item: &Item<MenuCtx>, args: &[&str], context: &mut MenuCtx) { + context.with_config(|c, out| { + c.wifi_pw = None; + }); + context.need_save = true; + wifi_entry(context); +} diff --git a/embassy/demos/picow/src/takepipe.rs b/embassy/demos/picow/src/takepipe.rs index 9f455e9..98f8f82 100644 --- a/embassy/demos/picow/src/takepipe.rs +++ b/embassy/demos/picow/src/takepipe.rs @@ -1,3 +1,13 @@ +#[allow(unused_imports)] +#[cfg(not(feature = "defmt"))] +pub use { + log::{debug, error, info, log, trace, warn}, +}; + +#[allow(unused_imports)] +#[cfg(feature = "defmt")] +pub use defmt::{debug, info, warn, panic, error, trace}; + use core::ops::DerefMut; use embedded_io::{asynch, Io}; diff --git a/embassy/demos/picow/src/wifi.rs b/embassy/demos/picow/src/wifi.rs index 180bab7..6f3c53a 100644 --- a/embassy/demos/picow/src/wifi.rs +++ b/embassy/demos/picow/src/wifi.rs @@ -21,6 +21,7 @@ use embassy_net::{Stack, StackResources}; use cyw43_pio::PioSpi; use static_cell::StaticCell; +use heapless::String; use rand::rngs::OsRng; use rand::RngCore; @@ -42,7 +43,8 @@ async fn wifi_task( pub(crate) async fn wifi_stack(spawner: &Spawner, p23: PIN_23, p24: PIN_24, p25: PIN_25, p29: PIN_29, dma: DMA_CH0, sm: PioStateMachineInstance<Pio0, Sm0>, - wifi_net: &str, wpa_password: Option<&str>, + wifi_net: String<32>, wpa_password: Option<String<63>>, + ) -> (embassy_net::Stack<cyw43::NetDriver<'static>>, cyw43::Control<'static>) { @@ -61,14 +63,19 @@ pub(crate) async fn wifi_stack(spawner: &Spawner, // control.set_power_management(cyw43::PowerManagementMode::None).await; // control.set_power_management(cyw43::PowerManagementMode::Performance).await; - if let Some(pw) = wpa_password { - info!("wifi net {} pw {}", wifi_net, pw); - control.join_wpa2(wifi_net, pw).await; + let st = if let Some(pw) = wpa_password { + info!("wifi net {} wpa2", wifi_net); + control.join_wpa2(&wifi_net, &pw).await } else { info!("wifi net {} open", wifi_net); - control.join_open(wifi_net).await; + control.join_open(&wifi_net).await + }; + if let Err(e) = st { + info!("wifi join failed, code {}", e.status); + let () = futures::future::pending().await; } + let config = embassy_net::Config::Dhcp(Default::default()); let seed = OsRng.next_u64(); diff --git a/embassy/demos/std/Cargo.lock b/embassy/demos/std/Cargo.lock index 22fe1b2..18d98ca 100644 --- a/embassy/demos/std/Cargo.lock +++ b/embassy/demos/std/Cargo.lock @@ -1054,6 +1054,7 @@ dependencies = [ "embassy-net", "embassy-net-driver", "embassy-sync 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "embassy-time 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "embedded-io", "heapless", "log", diff --git a/embassy/demos/std/src/main.rs b/embassy/demos/std/src/main.rs index 3f0e2c6..d8869a0 100644 --- a/embassy/demos/std/src/main.rs +++ b/embassy/demos/std/src/main.rs @@ -22,9 +22,10 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; use crate::tuntap::TunTapDevice; use sunset::*; -use sunset_embassy::SSHServer; +use sunset_embassy::{SSHServer, SunsetMutex}; mod tuntap; +mod setupmenu; pub(crate) use sunset_demo_embassy_common as demo_common; use crate::demo_common::singleton; @@ -43,7 +44,7 @@ async fn net_task(stack: &'static Stack<TunTapDevice>) -> ! { async fn main_task(spawner: Spawner) { // TODO config let opt_tap0 = "tap0"; - let config = Config::Dhcp(Default::default()); + let net_config = Config::Dhcp(Default::default()); // Init network device let device = TunTapDevice::new(opt_tap0).unwrap(); @@ -53,7 +54,7 @@ async fn main_task(spawner: Spawner) { // Init network stack let stack = &*singleton!(Stack::new( device, - config, + net_config, singleton!(StackResources::<NUM_SOCKETS>::new()), seed )); @@ -61,12 +62,12 @@ async fn main_task(spawner: Spawner) { // Launch network task spawner.spawn(net_task(stack)).unwrap(); - let ssh_config = &*singleton!( - SSHConfig::new().unwrap() + let config = &*singleton!( + SunsetMutex::new(SSHConfig::new().unwrap()) ); for _ in 0..NUM_LISTENERS { - spawner.spawn(listener(stack, &ssh_config)).unwrap(); + spawner.spawn(listener(stack, config)).unwrap(); } } @@ -95,10 +96,11 @@ impl Shell for DemoShell { let mut stdio = serv.stdio(chan_handle).await?; - let mut menu_buf = [0u8; 64]; + // input buffer, large enough for a ssh-ed25519 key + let mut menu_buf = [0u8; 150]; let menu_out = demo_menu::BufOutput::default(); - let mut menu = MenuRunner::new(&demo_menu::ROOT_MENU, &mut menu_buf, menu_out); + let mut menu = MenuRunner::new(&setupmenu::SETUP_MENU, &mut menu_buf, menu_out); // bodge for c in "help\r\n".bytes() { @@ -127,7 +129,8 @@ impl Shell for DemoShell { // TODO: pool_size should be NUM_LISTENERS but needs a literal #[embassy_executor::task(pool_size = 4)] -async fn listener(stack: &'static Stack<TunTapDevice>, config: &'static SSHConfig) -> ! { +async fn listener(stack: &'static Stack<TunTapDevice>, + config: &'static SunsetMutex<SSHConfig>) -> ! { demo_common::listener::<_, DemoShell>(stack, config, ()).await } diff --git a/embassy/demos/std/src/setupmenu.rs b/embassy/demos/std/src/setupmenu.rs new file mode 100644 index 0000000..0519921 --- /dev/null +++ b/embassy/demos/std/src/setupmenu.rs @@ -0,0 +1,127 @@ +use core::fmt::Write; +use demo_common::menu::*; +pub use demo_common::BufOutput; +pub(crate) use sunset_demo_embassy_common as demo_common; + +/* + +config + auth serial + auth admin + password + key + +*/ + +pub const SETUP_MENU: Menu<BufOutput> = Menu { + label: "setup", + items: &[ + &AUTH_ITEM, + &Item { + item_type: ItemType::Callback { + function: do_erase_config, + parameters: &[Parameter::Optional { + parameter_name: "", + help: None, + }], + }, + command: "erase_config", + help: Some("Erase all config."), + }, + &Item { + command: "about", + item_type: ItemType::Callback { function: do_about, parameters: &[] }, + help: None, + }, + ], + entry: Some(enter_top), + exit: None, +}; + +const AUTH_ITEM: Item<BufOutput> = Item { + command: "auth", + item_type: ItemType::Menu(&Menu { + label: "auth", + items: &[ + &Item { + command: "show", + item_type: ItemType::Callback { + parameters: &[], + function: do_auth_show, + }, + help: None, + }, + &Item { + command: "key", + item_type: ItemType::Callback { + parameters: &[ + Parameter::Mandatory { parameter_name: "slot", help: None }, + Parameter::Mandatory { + parameter_name: "ssh-ed25519", + help: None, + }, + Parameter::Mandatory { + parameter_name: "base64", + help: None, + }, + Parameter::Optional { + parameter_name: "comment", + help: None, + }, + ], + // help: Some( + // "An OpenSSH style ed25519 key, eg + // key ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AA...", + // ), + function: do_auth_key, + }, + help: None, + }, + &Item { + command: "password", + item_type: ItemType::Callback { + parameters: &[Parameter::Mandatory { + parameter_name: "pw", + help: None, + }], + function: do_auth_pw, + }, + help: None, + }, + ], + entry: Some(enter_auth), + exit: None, + }), + help: Some("Passwords and Keys."), +}; + +fn enter_top(context: &mut BufOutput) { + writeln!(context, "In setup menu").unwrap(); +} + +fn enter_auth(context: &mut BufOutput) { + writeln!(context, "In auth menu").unwrap(); +} + +fn do_auth_show(_item: &Item<BufOutput>, _args: &[&str], context: &mut BufOutput) { + writeln!(context, "auth key"); +} + +fn do_auth_key(_item: &Item<BufOutput>, _args: &[&str], context: &mut BufOutput) { + writeln!(context, "auth key"); +} + +fn do_auth_pw(_item: &Item<BufOutput>, _args: &[&str], context: &mut BufOutput) { + writeln!(context, "this is auth pw"); +} + +fn do_gpio_show(_item: &Item<BufOutput>, _args: &[&str], context: &mut BufOutput) { + writeln!(context, "gpio show here"); +} + +fn do_erase_config(_item: &Item<BufOutput>, args: &[&str], context: &mut BufOutput) { +} + +fn do_about(_item: &Item<BufOutput>, _args: &[&str], context: &mut BufOutput) { + writeln!(context, "Sunset SSH, USB serial\nMatt Johnston <matt@ucc.asn.au>\n"); +} diff --git a/src/behaviour.rs b/src/behaviour.rs index 7cb3bdd..471e7b5 100644 --- a/src/behaviour.rs +++ b/src/behaviour.rs @@ -204,7 +204,7 @@ pub trait ServBehaviour { // Also could make it take a closure to call with the key, lets it just // be loaded on the stack rather than kept in memory for the whole lifetime. // TODO: a slice of references is a bit awkward? - fn hostkeys(&mut self) -> BhResult<&[&sign::SignKey]>; + fn hostkeys(&mut self) -> BhResult<heapless::Vec<&SignKey, 2>>; #[allow(unused)] // TODO: or return a slice of enums @@ -314,7 +314,7 @@ impl CliBehaviour for UnusedCli { #[derive(Debug)] pub struct UnusedServ; impl ServBehaviour for UnusedServ { - fn hostkeys(&mut self) -> BhResult<&[&sign::SignKey]> { + fn hostkeys(&mut self) -> BhResult<heapless::Vec<&SignKey, 2>> { unreachable!() } fn open_session(&mut self, chan: ChanHandle) -> channel::ChanOpened { diff --git a/src/kex.rs b/src/kex.rs index 6c523eb..f251569 100644 --- a/src/kex.rs +++ b/src/kex.rs @@ -554,7 +554,8 @@ impl SharedSecret { ) -> Result<KexOutput> { // hostkeys list must contain the signature type trace!("hostkeys {:?}", b.hostkeys()); - let hostkey = b.hostkeys()?.iter().find(|k| k.can_sign(algos.hostsig)).trap()?; + let hk = b.hostkeys()?; + let hostkey = hk.as_slice().iter().find(|k| k.can_sign(algos.hostsig)).trap()?; kex_hash.prefinish(&hostkey.pubkey(), p.q_c.0, algos.kex.pubkey())?; let (kex_out, kex_pub) = match algos.kex { diff --git a/src/sign.rs b/src/sign.rs index fcf53ec..5357ef3 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -168,7 +168,7 @@ pub enum KeyType { /// /// This may hold the private key part locally /// or potentially send the signing requests to an SSH agent or other entity. -#[derive(ZeroizeOnDrop)] +#[derive(ZeroizeOnDrop, Clone)] pub enum SignKey { // 32 byte seed value is the private key Ed25519([u8; 32]), -- GitLab