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