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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
- name: Check lints and format
if: ${{ contains(fromJson('["esp32c6"]'), matrix.device.soc) }}
run: |
cargo +${{ matrix.device.toolchain }} clippy --release --features ${{ matrix.device.soc }} --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features -- -D warnings
cargo +${{ matrix.device.toolchain }} clippy --release --features ${{ matrix.device.soc }} --target riscv32imac-unknown-none-elf -p ssh-stamp-esp32 --bin ssh-stamp-esp32 --no-default-features -- -D warnings -A clippy::default_trait_access
cargo +${{ matrix.device.toolchain }} fmt -- --check
packer:
name: OTA Packer
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,27 @@ ssh -o SendEnv=SSH_STAMP_PUBKEY root@192.168.4.1
- Set a custom SSID and WPA2 PSK (allowed on first-boot or any authenticated session):

```
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
export SSH_STAMP_WIFI_AP_SSID="SshStampSSID"
export SSH_STAMP_WIFI_AP_PSK="my-super-secret-psk"
ssh -o SendEnv=SSH_STAMP_WIFI_AP_SSID -o SendEnv=SSH_STAMP_WIFI_AP_PSK root@192.168.4.1
```

- To connect the SSH Stamp to an existing access point with DHCP (Station Mode):
```
export SSH_STAMP_WIFI_STA_SSID="MyHomeSSID"
export SSH_STAMP_WIFI_STA_PSK="my-super-secret-psk"
ssh -o SendEnv=SSH_STAMP_WIFI_STA_SSID -o SendEnv=SSH_STAMP_WIFI_STA_PSK root@192.168.4.1
```

- To return to the default Access Point mode, clear the Station SSID:
```
export SSH_STAMP_WIFI_STA_SSID=""
ssh -o SendEnv=SSH_STAMP_WIFI_STA_SSID 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.
- `SSH_STAMP_WIFI_AP_SSID` and `SSH_STAMP_WIFI_AP_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.
Expand Down
12 changes: 7 additions & 5 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@ pub async fn prepare_ap_config<P: PlatformServices>(
) -> Result<WifiApConfigStatic, sunset::Error> {
let mut guard = config.lock().await;

if guard.wifi_pw.is_empty() {
if guard.wifi_ap_pw.is_empty() {
let pw = generate_wifi_password()?;
warn!("wifi_pw missing from config, generated new password");
guard.wifi_pw = pw;
guard.wifi_ap_pw = pw;
platform
.save_config(&guard)
.await
.map_err(|_| sunset::error::BadUsage.build())?;
}
info!("WIFI PSK: {}", guard.wifi_pw);
info!("WIFI PSK: {}", guard.wifi_ap_pw);

let mac = guard
.resolve_mac()
Expand All @@ -73,8 +73,10 @@ pub async fn prepare_ap_config<P: PlatformServices>(
print_hostkey_fingerprint(&guard.hostkey);

Ok(WifiApConfigStatic {
ssid: guard.wifi_ssid.clone(),
password: guard.wifi_pw.clone(),
ap_ssid: guard.wifi_ap_ssid.clone(),
ap_password: guard.wifi_ap_pw.clone(),
sta_ssid: guard.wifi_sta_ssid.clone(),
sta_password: guard.wifi_sta_pw.clone(),
channel: 1,
mac,
})
Expand Down
52 changes: 37 additions & 15 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ pub struct SSHStampConfig {
pub pubkeys: [Option<Ed25519PubKey>; KEY_SLOTS],

/// `WiFi`
pub wifi_ssid: String<32>,
pub wifi_pw: String<63>,

/// Access Point Mode
pub wifi_ap_ssid: String<32>,
pub wifi_ap_pw: String<63>,
/// Station Mode
pub wifi_sta_ssid: String<32>,
pub wifi_sta_pw: String<63>,
/// Networking
/// MAC address. Special values:
/// - `[0xFF; 6]`: Generate random MAC on each boot
Expand Down Expand Up @@ -103,9 +106,13 @@ impl SSHStampConfig {
pub fn new(default_mac: [u8; 6]) -> Result<Self> {
let hostkey = SignKey::generate(KeyType::Ed25519, None)?;

let wifi_ssid = Self::generate_wifi_ssid()?;
// Wifi Access Point Mode
let wifi_ap_ssid = Self::generate_wifi_ssid()?;
let wifi_ap_pw = Self::generate_wifi_password()?;
// Wifi Station Mode
let wifi_sta_ssid = String::<32>::new();
let wifi_sta_pw = String::<63>::new();
let mac = default_mac;
let wifi_pw = Self::generate_wifi_password()?;

let uart_pins = UartPins::default();
debug!(
Expand All @@ -116,8 +123,10 @@ impl SSHStampConfig {
Ok(SSHStampConfig {
hostkey,
pubkeys: Default::default(),
wifi_ssid,
wifi_pw,
wifi_ap_ssid,
wifi_ap_pw,
wifi_sta_ssid,
wifi_sta_pw,
mac,
ipv4_static: None,
#[cfg(feature = "ipv6")]
Expand Down Expand Up @@ -312,8 +321,12 @@ impl SSHEncode for SSHStampConfig {
enc_option(k.as_ref(), s)?;
}

self.wifi_ssid.as_str().enc(s)?;
self.wifi_pw.as_str().enc(s)?;
// Wifi Access Point Mode
self.wifi_ap_ssid.as_str().enc(s)?;
self.wifi_ap_pw.as_str().enc(s)?;
// Wifi Station Mode
self.wifi_sta_ssid.as_str().enc(s)?;
self.wifi_sta_pw.as_str().enc(s)?;
self.mac.enc(s)?;

enc_ipv4_config(self.ipv4_static.as_ref(), s)?;
Expand Down Expand Up @@ -343,10 +356,17 @@ impl<'de> SSHDecode<'de> for SSHStampConfig {
*k = dec_option(s)?;
}

let wifi_ssid_str: &str = SSHDecode::dec(s)?;
let wifi_ssid = String::try_from(wifi_ssid_str).map_err(|_| WireError::BadString)?;
let wifi_pw_str: &str = SSHDecode::dec(s)?;
let wifi_pw = String::try_from(wifi_pw_str).map_err(|_| WireError::BadString)?;
// Wifi Access Point Mode
let wifi_ap_ssid_str: &str = SSHDecode::dec(s)?;
let wifi_ap_ssid = String::try_from(wifi_ap_ssid_str).map_err(|_| WireError::BadString)?;
let wifi_ap_pw_str: &str = SSHDecode::dec(s)?;
let wifi_ap_pw = String::try_from(wifi_ap_pw_str).map_err(|_| WireError::BadString)?;
// Wifi Station Mode
let wifi_sta_ssid_str: &str = SSHDecode::dec(s)?;
let wifi_sta_ssid =
String::try_from(wifi_sta_ssid_str).map_err(|_| WireError::BadString)?;
let wifi_sta_pw_str: &str = SSHDecode::dec(s)?;
let wifi_sta_pw = String::try_from(wifi_sta_pw_str).map_err(|_| WireError::BadString)?;

let mac = SSHDecode::dec(s)?;

Expand All @@ -365,8 +385,10 @@ impl<'de> SSHDecode<'de> for SSHStampConfig {
Ok(Self {
hostkey,
pubkeys,
wifi_ssid,
wifi_pw,
wifi_ap_ssid,
wifi_ap_pw,
wifi_sta_ssid,
wifi_sta_pw,
mac,
ipv4_static,
#[cfg(feature = "ipv6")]
Expand Down
111 changes: 93 additions & 18 deletions src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub mod env_parser {
///
/// Returns `None` if the value contains non-ASCII-graphic characters.
#[must_use]
pub fn parse_wifi_ssid(value: &str) -> Option<String<32>> {
pub fn parse_wifi_ap_ssid(value: &str) -> Option<String<32>> {
if !env_sanitize(value) {
return None;
}
Expand All @@ -67,6 +67,19 @@ pub mod env_parser {
Some(s)
}

/// Parses and validates a `WiFi` SSID from an environment variable value.
///
/// Returns `None` if the value contains non-ASCII-graphic characters.
#[must_use]
pub fn parse_wifi_station_ssid(value: &str) -> Option<String<32>> {
if !value.is_empty() && !env_sanitize(value) {
return None;
}
let mut s = String::new();
s.push_str(value).ok()?;
Some(s)
}

/// Parses and validates a `WiFi` PSK from an environment variable value.
///
/// Returns `None` if the value is not between 8 and 63 characters
Expand Down Expand Up @@ -352,11 +365,17 @@ pub async fn session_env(
"SSH_STAMP_PUBKEY" => {
pubkey_env(a, config, ctx).await?;
}
"SSH_STAMP_WIFI_SSID" => {
wifi_ssid_env(a, config, ctx).await?;
"SSH_STAMP_WIFI_AP_SSID" => {
wifi_ap_ssid_env(a, config, ctx).await?;
}
"SSH_STAMP_WIFI_PSK" => {
wifi_psk_env(a, config, ctx).await?;
"SSH_STAMP_WIFI_AP_PSK" => {
wifi_ap_psk_env(a, config, ctx).await?;
}
"SSH_STAMP_WIFI_STA_SSID" => {
wifi_sta_ssid_env(a, config, ctx).await?;
}
"SSH_STAMP_WIFI_STA_PW" => {
wifi_sta_psk_env(a, config, ctx).await?;
}
"SSH_STAMP_WIFI_MAC_ADDRESS" => {
wifi_mac_address_env(a, config, ctx).await?;
Expand Down Expand Up @@ -414,57 +433,113 @@ pub async fn pubkey_env(
Ok(())
}

/// Handles `SSH_STAMP_WIFI_SSID` environment variable requests.
/// Handles `SSH_STAMP_WIFI_AP_SSID` environment variable requests.
///
/// # Errors
/// Returns an error if SSH protocol operations fail or if the SSID is invalid.
pub async fn wifi_ssid_env(
pub async fn wifi_ap_ssid_env(
a: sunset::event::ServEnvironmentRequest<'_, '_>,
config: &SunsetMutex<SSHStampConfig>,
ctx: &mut EventContext<'_>,
) -> Result<(), sunset::Error> {
let mut config_guard = config.lock().await;
if *ctx.auth_checked || config_guard.first_login {
if let Some(s) = env_parser::parse_wifi_ssid(a.value()?) {
config_guard.wifi_ssid = s;
debug!("Set wifi SSID from ENV");
if let Some(s) = env_parser::parse_wifi_ap_ssid(a.value()?) {
config_guard.wifi_ap_ssid = s;
debug!("Set wifi Access Point SSID from ENV");
a.succeed()?;
*ctx.config_changed = true;
*ctx.needs_reset = true;
} else {
warn!("SSH_STAMP_WIFI_SSID invalid and/or too long");
warn!("SSH_STAMP_WIFI_AP_SSID invalid and/or too long");
a.fail()?;
}
} else {
warn!("SSH_STAMP_WIFI_SSID env received but not authenticated; rejecting");
warn!("SSH_STAMP_WIFI_AP_SSID env received but not authenticated; rejecting");
a.fail()?;
}
Ok(())
}

/// Handles `SSH_STAMP_WIFI_PSK` environment variable requests.
/// Handles `SSH_STAMP_WIFI_AP_PSK` environment variable requests.
///
/// # Errors
/// Returns an error if SSH protocol operations fail or if the PSK is invalid.
pub async fn wifi_psk_env(
pub async fn wifi_ap_psk_env(
a: sunset::event::ServEnvironmentRequest<'_, '_>,
config: &SunsetMutex<SSHStampConfig>,
ctx: &mut EventContext<'_>,
) -> Result<(), sunset::Error> {
let mut config_guard = config.lock().await;
if *ctx.auth_checked || config_guard.first_login {
if let Some(s) = env_parser::parse_wifi_psk(a.value()?) {
config_guard.wifi_ap_pw = s;
debug!("Set WIFI AP PSK from ENV");
a.succeed()?;
*ctx.config_changed = true;
*ctx.needs_reset = true;
} else {
warn!("SSH_STAMP_WIFI_AP_PSK invalid and/or not within 8-63 characters");
a.fail()?;
}
} else {
warn!("SSH_STAMP_WIFI_AP_PSK env received but not authenticated; rejecting");
a.fail()?;
}
Ok(())
}

/// Handles `SSH_STAMP_WIFI_STA_SSID` environment variable requests.
///
/// # Errors
/// Returns an error if SSH protocol operations fail or if the SSID is invalid.
pub async fn wifi_sta_ssid_env(
a: sunset::event::ServEnvironmentRequest<'_, '_>,
config: &SunsetMutex<SSHStampConfig>,
ctx: &mut EventContext<'_>,
) -> Result<(), sunset::Error> {
let mut config_guard = config.lock().await;
if *ctx.auth_checked || config_guard.first_login {
if let Some(s) = env_parser::parse_wifi_station_ssid(a.value()?) {
config_guard.wifi_sta_ssid = s;
debug!("Set wifi STATION SSID from ENV");
a.succeed()?;
*ctx.config_changed = true;
*ctx.needs_reset = true;
} else {
warn!("SSH_STAMP_WIFI_STA_SSID invalid and/or too long");
a.fail()?;
}
} else {
warn!("SSH_STAMP_WIFI_STA_SSID env received but not authenticated; rejecting");
a.fail()?;
}
Ok(())
}

/// Handles `SSH_STAMP_WIFI_STA_PSK` environment variable requests.
///
/// # Errors
/// Returns an error if SSH protocol operations fail or if the SSID is invalid.
pub async fn wifi_sta_psk_env(
a: sunset::event::ServEnvironmentRequest<'_, '_>,
config: &SunsetMutex<SSHStampConfig>,
ctx: &mut EventContext<'_>,
) -> Result<(), sunset::Error> {
let mut config_guard = config.lock().await;
if *ctx.auth_checked || config_guard.first_login {
if let Some(s) = env_parser::parse_wifi_psk(a.value()?) {
config_guard.wifi_pw = s;
debug!("Set WIFI PSK from ENV");
config_guard.wifi_sta_pw = s;
debug!("Set wifi STATION PSK from ENV");
a.succeed()?;
*ctx.config_changed = true;
*ctx.needs_reset = true;
} else {
warn!("SSH_STAMP_WIFI_PSK invalid and/or not within 8-63 characters");
warn!("SSH_STAMP_WIFI_STA_PSK invalid and/or not within 8-63 characters");
a.fail()?;
}
} else {
warn!("SSH_STAMP_WIFI_PSK env received but not authenticated; rejecting");
warn!("SSH_STAMP_WIFI_STA_PSK env received but not authenticated; rejecting");
a.fail()?;
}
Ok(())
Expand Down
10 changes: 5 additions & 5 deletions src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ where
match load(flash, buf) {
Ok(mut c) => {
debug!("Good existing config");
if c.wifi_ssid.as_str() == "ssh-stamp" {
debug!("Migrating insecure default SSID, regenerating randomly");
c.wifi_ssid = SSHStampConfig::generate_wifi_ssid()?;
if c.wifi_pw.is_empty() {
c.wifi_pw = SSHStampConfig::generate_wifi_password()?;
if c.wifi_ap_ssid.as_str() == "ssh-stamp" {
debug!("Migrating insecure default Access Point SSID, regenerating randomly");
c.wifi_ap_ssid = SSHStampConfig::generate_wifi_ssid()?;
if c.wifi_ap_pw.is_empty() {
c.wifi_ap_pw = SSHStampConfig::generate_wifi_password()?;
}
save(flash, buf, &c)?;
}
Expand Down
Loading
Loading