Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 57 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,104 +10,94 @@ Your everyday SSH secured serial access.

## Description

The **SSH Stamp** is a secure wireless to UART bridge
implemented in Rust (no_std, no_alloc and no_unsafe whenever possible)
with simplicity and robustness as its main design tenets.

The firmware runs on a microcontroller running Secure SHell Protocol
(RFC 4253 and related IETF standards series). This firmware can be
used for multiple purposes, conveniently avoiding physical
tethering and securely tunneling traffic via SSH by default: easily
add telemetry to a (moving) robot, monitor and operate any (domestic)
appliance remotely, conduct remote cybersecurity audits on
network gear of a company, reverse engineer hardware and software for
right to repair purposes, just to name a few examples.
The **SSH Stamp** is a secure wireless to UART bridge implemented in Rust (no_std, no_alloc and no_unsafe whenever possible) with simplicity and robustness as its main design tenets.

The firmware runs on a microcontroller running Secure SHell Protocol (RFC 4253 and related IETF standards series). This firmware can be used for multiple purposes, conveniently avoiding physical tethering and securely tunneling traffic via SSH by default: easily add telemetry to a (moving) robot, monitor and operate any (domestic) appliance remotely, conduct remote cybersecurity audits on network gear of a company, reverse engineer hardware and software for right to repair purposes, just to name a few examples.

A "low level to SSH Swiss army knife".

# Building

Rust versions are controlled via `rust-toolchain.toml` and the equivalent defined on the CI workflow.

On a fresh system the following should be enough to build and run on the relevant ESP32 dev boards.
Tooling is controlled by `rust-toolchain.toml`. On a fresh host you'll typically need the Rust source component and a flasher (we use `espflash` below as an example):

## Required for all targets:
```
rustup toolchain install stable --component rust-src
cargo install espflash --locked
```

## ESP32-C6
Build/flash for your board using the short command pattern (replace `<target>` with the concrete chip you have):

| Machine target | Rust toolchain target |
| --- | --- |
| `esp32` | `xtensa-esp32-none-elf` |
| `esp32c2` | `riscv32imc-unknown-none-elf` |
| `esp32c3` | `riscv32imc-unknown-none-elf` |
| `esp32c5` | `riscv32imac-unknown-none-elf` |
| `esp32c6` | `riscv32imac-unknown-none-elf` |
| `esp32c61` | `riscv32imac-unknown-none-elf` |
| `esp32s2` | `xtensa-esp32s2-none-elf` |
| `esp32s3` | `xtensa-esp32s3-none-elf` |

```
rustup target add riscv32imac-unknown-none-elf
cargo build-esp32c6
cargo run-esp32c6
rustup target add <rust-toolchain-target>
cargo build-<machine-target> # e.g. cargo build-esp32c6, cargo build-esp32c3, cargo build-esp32
cargo run-<machine-target> # convenience helper (if supported) that builds + flashes
```

## ESP32-C2 / ESP32-C3
```
rustup target add riscv32imc-unknown-none-elf
```
### ESP32-C2
```
cargo build-esp32c2
cargo run-esp32c2
```
### ESP32-C3
Xtensa targets (ESP32/ESP32-S2/S3) do require `espup` in addition to the `rustup` command above:

```
cargo build-esp32c2
cargo run-esp32c3
cargo install espup
espup install
source $HOME/export-esp.sh
```

## First boot & provisioning

## ESP32 / ESP32-S2 / ESP32-S3 (Xtensa Cores)
Install esp toolchain first: https://github.com/esp-rs/espup
```
cargo install espup
espup install
source $HOME/export-esp.sh
```
Comment thread
brainstorm marked this conversation as resolved.
1. Flash the firmware and open the serial console (example):

### ESP32
```
cargo +esp build-esp32
cargo +esp run-esp32
```
### ESP32-S2
```
cargo +esp build-esp32s2
cargo +esp run-esp32s2
```
### ESP32-S3
```
cargo +esp build-esp32s3
cargo +esp run-esp32s3
# build & flash (example for esp32c6)
cargo build-esp32c6 --release
cargo run-esp32c6
```

### Using rustup toolchain override (Doesn't require `+esp`)
To set rustup override:
```
rustup override set esp
```
To remove rustup override:
1. On first boot the device generates a random WPA2 PSK and prints it to the serial console with the following (or similar) info messages:

```
cargo override unset
(...)
INFO - WIFI PSK: <PSK>
INFO - Connect to the AP `ssh-stamp` as a DHCP client with IP: 192.168.4.1
```
Build:

2. Connect a laptop/phone to the `ssh-stamp` AP using the printed PSK, then SSH into the device at `root@192.168.4.1`.

3. Provisioning via SSH environment variables

You can provision the device by sending these environment variables with your SSH client. Examples below use OpenSSH and `SendEnv` to forward local environment variables to the device.

- Add your public key (first-boot only):

```
cargo build-esp32
cargo build-esp32s2
cargo build-esp32s3
export SSH_STAMP_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)"
ssh -o SendEnv=SSH_STAMP_PUBKEY root@192.168.4.1
```
Run:

- Set a custom SSID and WPA2 PSK (allowed on first-boot or any authenticated session):

```
cargo run-esp32
cargo run-esp32s2
cargo run-esp32s3
export SSH_STAMP_WIFI_SSID="MyHomeSSID"
export SSH_STAMP_WIFI_PSK="my-super-secret-psk"
ssh -o SendEnv=SSH_STAMP_WIFI_SSID -o SendEnv=SSH_STAMP_WIFI_PSK root@192.168.4.1
```

Notes:
- `SSH_STAMP_PUBKEY` is accepted on first-boot to add the initial admin key.
- `SSH_STAMP_WIFI_SSID` and `SSH_STAMP_WIFI_PSK` may be applied while authenticated via pubkey (or on first-boot). After a successful change the device persists the settings and performs a software reset so the new WiFi settings take effect.
- If you prefer a single-step provisioning, export all three env vars locally and forward them with `SendEnv` in the same SSH invocation.

If your SSH client doesn't forward environment variables by default, use the `-o SendEnv=VAR` option as shown above or configure `SendEnv` in your SSH client config.

# Default UART Pins
| Target | RX | TX |
| ---- | -- | -- |
Expand Down
43 changes: 26 additions & 17 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use log::{debug, info, warn};
use log::{debug, warn};

use core::net::Ipv4Addr;
#[cfg(feature = "ipv6")]
Expand All @@ -17,7 +17,9 @@ use sunset::{
};

use crate::errors::Error;
use crate::settings::{DEFAULT_SSID, DEFAULT_UART_RX_PIN, DEFAULT_UART_TX_PIN, KEY_SLOTS};
use crate::settings::{
DEFAULT_SSID, DEFAULT_UART_RX_PIN, DEFAULT_UART_TX_PIN, KEY_SLOTS, WIFI_PASSWORD_CHARS,
};

#[derive(Debug, PartialEq)]
pub struct SSHStampConfig {
Expand All @@ -40,8 +42,8 @@ pub struct SSHStampConfig {
pub ipv6_static: Option<StaticConfigV6>,
/// UART
pub uart_pins: UartPins,
/// True until the device has been provisioned for the first time.
pub first_boot: bool,
/// True until a pubkey is provisioned. Further changes require authentication.
pub first_login: bool,
}

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -72,17 +74,14 @@ impl SSHStampConfig {

let wifi_ssid = Self::default_ssid();
let mac = random_mac()?;
let wifi_pw = None;
let wifi_pw = Some(Self::generate_wifi_password()?);

let uart_pins = UartPins::default();
info!(
debug!(
"SSH Stamp Config new() - RX Pin: {} TX Pin: {}",
uart_pins.rx, uart_pins.tx
);

// No password config fields nor logic: only pubkey auth supported.
// Leave password fields out (except wifi one).

Ok(SSHStampConfig {
hostkey,
pubkeys: Default::default(),
Expand All @@ -93,10 +92,20 @@ impl SSHStampConfig {
#[cfg(feature = "ipv6")]
ipv6_static: None,
uart_pins,
first_boot: true,
first_login: true,
})
}

fn generate_wifi_password() -> Result<String<63>> {
let mut rnd = [0u8; 24];
sunset::random::fill_random(&mut rnd)?;
let mut pw = String::<63>::new();
for &byte in rnd.iter() {
let _ = pw.push(WIFI_PASSWORD_CHARS[(byte as usize) % 62] as char);
}
Ok(pw)
}

// Password functions removed; pubkey-only auth supported.

pub(crate) fn default_ssid() -> String<32> {
Expand All @@ -110,14 +119,14 @@ impl SSHStampConfig {
// validate it is an Ed25519 key. Insert into the first empty slot or
// overwrite slot 0 if none empty.

info!(
debug!(
"Checking pubkey string passed through ENV: {}",
key_str.trim()
);

let openssh = ssh_key::PublicKey::from_str(key_str.trim())?;

info!("Public key format valid, continuing to parse");
debug!("Public key format valid, continuing to parse");

match openssh.key_data() {
ssh_key::public::KeyData::Ed25519(k) => {
Expand All @@ -126,7 +135,7 @@ impl SSHStampConfig {
key: sunset::sshwire::Blob(bytes),
};

info!("Parsed Ed25519 public key, adding to config");
debug!("Parsed Ed25519 public key, adding to config");
for slot in self.pubkeys.iter_mut() {
if slot.is_none() {
*slot = Some(newk);
Expand Down Expand Up @@ -289,8 +298,8 @@ impl SSHEncode for SSHStampConfig {
self.uart_pins.rx.enc(s)?;
self.uart_pins.tx.enc(s)?;

// Persist first-boot marker
self.first_boot.enc(s)?;
// Persist first-login marker
self.first_login.enc(s)?;

Ok(())
}
Expand Down Expand Up @@ -323,7 +332,7 @@ impl<'de> SSHDecode<'de> for SSHStampConfig {
let tx: u8 = SSHDecode::dec(s)?;
let uart_pins = UartPins { rx, tx };

let first_boot = SSHDecode::dec(s)?;
let first_login = SSHDecode::dec(s)?;

Ok(Self {
hostkey,
Expand All @@ -335,7 +344,7 @@ impl<'de> SSHDecode<'de> for SSHStampConfig {
#[cfg(feature = "ipv6")]
ipv6_static,
uart_pins,
first_boot,
first_login,
})
}
}
6 changes: 3 additions & 3 deletions src/espressif/buffered_uart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use portable_atomic::{AtomicUsize, Ordering};
use static_cell::StaticCell;
use sunset_async::SunsetMutex;

use log::info;
use log::debug;

// Sizes of the software buffers. Inward is more
// important as an overrun here drops bytes. A full outward
Expand Down Expand Up @@ -132,14 +132,14 @@ impl Default for BufferedUart {

pub async fn uart_buffer_disable() -> () {
// disable uart buffer
info!("UART buffer disabled: WIP");
debug!("UART buffer disabled: WIP");
// TODO: Correctly disable/restart UART buffer and/or send messsage to user over SSH
}
// use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

pub async fn uart_disable() -> () {
// disable uart
info!("UART disabled: WIP");
debug!("UART disabled: WIP");
// TODO: Correctly disable/restart UART and/or send messsage to user over SSH
}

Expand Down
Loading
Loading