Skip to content

Commit 436bcdd

Browse files
committed
security: Fix bug and gate it properly via asserts and warnings. Reported by @mkj, re-reviewed by @ramrunner, thanks and kudos to both
1 parent 3c9dff0 commit 436bcdd

1 file changed

Lines changed: 67 additions & 2 deletions

File tree

ssh-stamp-esp32/src/bin/ssh-stamp-esp32.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,35 @@ async fn main(spawner: Spawner) -> ! {
8787
cfg_if::cfg_if! {
8888
if #[cfg(any(feature = "esp32c5", feature = "esp32c61"))] {
8989
// ESP32-C5/C61 have no TRNG peripheral — use the basic Rng directly.
90+
// Until the TODO above is resolved, key material on these chips is
91+
// only as good as the bare RNG register, so say so out loud.
92+
warn!("No TRNG on this chip: RNG is not cryptographically secure until the radio is up");
9093
let rng = esp_hal::rng::Rng::new();
9194
register_custom_rng(rng);
9295
} else {
96+
// The RNG register only yields true randomness while an entropy
97+
// source is active. There are two, and this firmware uses both in
98+
// sequence:
99+
//
100+
// 1. the SAR ADC source enabled here, covering early boot, and
101+
// 2. the RF subsystem, once WiFi is up.
102+
//
103+
// `Trng::downgrade` returns `Rng`, a zero-sized handle that just
104+
// reads the register, so it carries no guarantee of its own —
105+
// whichever source is live at the time is what decides quality.
106+
//
107+
// The ADC source must therefore stay enabled until the radio takes
108+
// over, because everything minted in between depends on it: the
109+
// SSH host key, WiFi SSID/PSK and MAC all come from
110+
// `store::load_or_create` and `prepare_ap_config` below. It is
111+
// handed over (and explicitly dropped) just before the radio is
112+
// initialised — see the drop site further down for why it cannot
113+
// simply be left running.
93114
let trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1);
94-
let trng = Trng::try_new().unwrap();
115+
let trng = Trng::try_new()
116+
.expect("TrngSource was just created, so the TRNG must be available");
95117
let rng = trng.downgrade();
96118
register_custom_rng(rng);
97-
drop(trng_source);
98119
}
99120
}
100121

@@ -125,6 +146,18 @@ async fn main(spawner: Spawner) -> ! {
125146
tx: tx_num,
126147
};
127148

149+
// On first boot this mints the SSH host key and the WiFi PSK, so the
150+
// entropy source enabled above has to still be running. Guard the
151+
// invariant rather than trusting a comment: `debug-assertions` are on
152+
// even in release for this workspace, so reintroducing an early drop of
153+
// the `TrngSource` fails loudly on the bench instead of silently
154+
// producing predictable keys.
155+
#[cfg(not(any(feature = "esp32c5", feature = "esp32c61")))]
156+
debug_assert!(
157+
TrngSource::is_enabled(),
158+
"entropy source was disabled before host key generation"
159+
);
160+
128161
debug!("Loading config");
129162
let flash_config = {
130163
let Some(flash_storage_guard) = flash::get_flash_n_buffer() else {
@@ -197,10 +230,31 @@ async fn main(spawner: Spawner) -> ! {
197230

198231
debug!("Initialising radio");
199232

233+
// Last consumer of randomness before the radio: mints the WiFi PSK if the
234+
// config did not already carry one.
200235
let ap_config = app::prepare_ap_config(config, &platform)
201236
.await
202237
.expect("Failed to prepare AP config");
203238

239+
// Hand the entropy source over to the radio.
240+
//
241+
// The SAR ADC source cannot simply be left running: Espressif requires it
242+
// to be switched off "before RF subsystem features, ADC, or I2S (ESP32
243+
// only) are initialized", warning that it "is not safe to use if any other
244+
// subsystem is accessing the RF subsystem or the ADC at the same time"
245+
// (ESP-IDF, Random Number Generation). It also commandeers the SAR ADC —
246+
// and I2S0 on the classic ESP32 — which the radio needs back.
247+
//
248+
// Nothing is lost by dropping it here: `WifiController::new` inside
249+
// `bring_up()` enables the RF subsystem, which is itself an entropy
250+
// source, and esp-radio registers that fact with esp-hal. So the SSH
251+
// session and key-exchange material sunset draws per connection is still
252+
// covered, just by the radio rather than the ADC.
253+
//
254+
// https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/random.html
255+
#[cfg(not(any(feature = "esp32c5", feature = "esp32c61")))]
256+
drop(trng_source);
257+
204258
let mut wifi = EspWifi::new(spawner, peripherals.WIFI, rng, DEFAULT_IP);
205259
wifi.configure_ap(ap_config)
206260
.expect("Failed to configure AP");
@@ -220,6 +274,17 @@ async fn main(spawner: Spawner) -> ! {
220274
}
221275
}
222276

277+
// The radio should have picked up the entropy duty dropped above:
278+
// esp-radio bumps esp-hal's entropy-source count once the RF subsystem is
279+
// running. sunset draws fresh key-exchange material from `getrandom` for
280+
// every SSH connection served below, so if this does not hold the handover
281+
// has a hole in it.
282+
#[cfg(not(any(feature = "esp32c5", feature = "esp32c61")))]
283+
debug_assert!(
284+
TrngSource::is_enabled(),
285+
"no entropy source active after WiFi came up"
286+
);
287+
223288
if let Err(e) = app::run_app(stack.unwrap(), uart_buf, config, &platform).await {
224289
error!("run_app exited with error: {e}");
225290
}

0 commit comments

Comments
 (0)