@@ -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