nRF52840 — Rust Embassy USB Honeypot Sentinel
The nRF52840 serves as a USB honeypot sentinel and BLE mesh sensor. It monitors for unauthorized USB device insertions, broadcasts sensor data via BLE, and performs hardware-accelerated attestation using the CryptoCell CC310.
Role in the Architecture
Technical Specifications
| Feature | Detail |
|---|---|
| MCU | nRF52840 (ARM Cortex-M4F, 64MHz) |
| RAM | 256KB SRAM |
| Flash | 1MB |
| Framework | Embassy (async embedded Rust) |
| Language | Rust (#![no_std], #![no_main]) |
| Crypto | CryptoCell CC310 (ECDSA P-256, AES, SHA-256) |
| Connectivity | BLE 5.0 + USB 2.0 |
| Protocol | MQTT-SN over UDP (via BLE → ESP32-S3) |
| Key Storage | UICR (User Information Configuration Registers) |
| Heartbeat | 30 seconds |
Why Rust Embassy?
| Feature | Embassy | RTIC | Zephyr (C) | FreeRTOS (C) |
|---|---|---|---|---|
| Memory safety | Compile-time | Compile-time | Runtime checks | None |
| Async/await | Native | No | No | No |
| Zero-cost abstractions | Yes | Yes | No | No |
| No-alloc | Yes (#![no_std]) | Yes | Possible | Possible |
| Stack overflow | Impossible (no threads) | Possible | Possible | Possible |
Embassy uses Rust's async/await on bare metal — no RTOS, no threads, no heap. Tasks are state machines compiled to static memory, eliminating entire classes of embedded bugs.
Main Entry Point
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_nrf as _;
use embassy_time::{Duration, Timer};
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_nrf::init(Default::default());
// Spawn concurrent tasks (no threads — cooperative async)
spawner.spawn(usb_sentinel_task(/* ... */)).unwrap();
spawner.spawn(ble_advertiser_task(/* ... */)).unwrap();
spawner.spawn(attestation_task(/* ... */)).unwrap();
// Heartbeat loop
loop {
// Publish heartbeat via MQTT-SN
mqtt_sn_publish("aurora/sensors/nrf52_sentinel_01/status", heartbeat_payload);
Timer::after(Duration::from_secs(30)).await;
}
}
USB Sentinel (Honeypot)
The USB sentinel monitors the USB bus for unauthorized device insertions — a physical attack vector where an attacker plugs in a malicious USB device (Rubber Ducky, BadUSB, etc.).
#[embassy_executor::task]
async fn usb_sentinel_task(usb: embassy_nrf::usb::Driver<'static>) {
loop {
// Wait for USB bus event (device insertion)
let event = usb.wait_for_device().await;
// Extract device descriptor
let vid = event.vendor_id;
let pid = event.product_id;
let class = event.device_class;
// Check against allowlist
if !ALLOWLIST.contains(&(vid, pid)) {
// ALERT: Unauthorized USB device!
let alert = format!(
r#"{{"device_id":"nrf52_sentinel_01","alert_type":"unauthorized_usb","severity":"critical","vid":"0x{:04X}","pid":"0x{:04X}","class":"0x{:02X}"}}"#,
vid, pid, class
);
mqtt_sn_publish("aurora/sensors/nrf52_sentinel_01/alerts", &alert);
}
}
}
Why a USB Honeypot?
Physical access attacks bypass all network security controls. A USB Rubber Ducky can exfiltrate data in seconds. By placing nRF52840 sentinels in server rooms, the SOC gets immediate alerts on physical intrusion attempts.
BLE Sensor Broadcasting
#[derive(Clone, Copy)]
pub struct SensorData {
pub temperature_c: i16, // ×100 fixed-point (e.g., 2350 = 23.50°C)
pub motion_detected: bool,
pub tamper_status: u8, // 0=normal, 1=case_open, 2=wire_cut
pub battery_mv: u16, // Battery voltage in millivolts
pub boot_count: u16, // Monotonic boot counter
}
#[embassy_executor::task]
async fn ble_advertiser_task(/* ... */) {
loop {
let data = SensorData {
temperature_c: read_temperature(),
motion_detected: read_pir_sensor(),
tamper_status: read_tamper_pins(),
battery_mv: read_battery_adc(),
boot_count: get_boot_count(),
};
// Encode as BLE manufacturer-specific advertisement
let adv_data = encode_manufacturer_data(&data);
// Broadcast — ESP32-S3 will pick this up
ble_advertise(&adv_data).await;
Timer::after(Duration::from_secs(5)).await;
}
}
The nRF52840 broadcasts sensor data as BLE advertisements rather than establishing connections. This is more power-efficient and allows multiple ESP32-S3 gateways to receive the data simultaneously.
Hardware Attestation
pub struct AttestationState {
pub boot_count: u32,
pub firmware_hash: [u8; 32], // SHA-256 of firmware
pub last_attestation_ok: bool,
}
#[embassy_executor::task]
async fn attestation_task(/* ... */) {
// Read ECDSA private key from UICR
let private_key = read_uicr_key();
loop {
// 1. Compute firmware hash using CC310 SHA-256
let firmware_hash = cc310_sha256(FIRMWARE_START, FIRMWARE_SIZE);
// 2. Build attestation message
let message = format!("nrf52_sentinel_01{}{}",
hex::encode(&firmware_hash),
get_boot_count()
);
// 3. Sign with CC310 ECDSA P-256
let signature = cc310_ecdsa_sign(&private_key, &message);
// 4. Publish to Rust Core Engine via MQTT-SN
let payload = format!(
r#"{{"device_id":"nrf52_sentinel_01","firmware_hash":"{}","boot_count":{},"signature_hex":"{}"}}"#,
hex::encode(&firmware_hash),
get_boot_count(),
hex::encode(&signature)
);
mqtt_sn_publish("aurora/attestation/nrf52_sentinel_01/response", &payload);
Timer::after(Duration::from_secs(300)).await; // Every 5 minutes
}
}
CryptoCell CC310
The nRF52840 includes a dedicated hardware cryptographic accelerator:
| Operation | CC310 Speed | Software Speed |
|---|---|---|
| ECDSA P-256 Sign | ~50ms | ~2000ms |
| SHA-256 (1KB) | ~0.1ms | ~5ms |
| AES-128 | ~0.01ms/block | ~0.5ms/block |
Using the CC310 hardware means attestation completes in ~50ms instead of 2 seconds, critical for a battery-powered device.
MQTT-SN Protocol
The nRF52840 uses MQTT-SN (Sensor Networks) — a lightweight variant of MQTT designed for constrained devices:
pub enum QoS {
AtMostOnce, // Fire and forget
AtLeastOnce, // Acknowledged delivery
ExactlyOnce, // Two-phase delivery
}
pub struct MqttSnClient {
gateway_addr: [u8; 6], // BLE MAC of ESP32-S3 gateway
gateway_port: u16,
connected: bool,
}
impl MqttSnClient {
pub async fn connect(&mut self) -> Result<(), MqttSnError> {
// MQTT-SN CONNECT over UDP-over-BLE
}
pub async fn publish(&self, topic: &str, payload: &[u8], qos: QoS) -> Result<(), MqttSnError> {
// Pre-registered topic IDs for efficiency
// Topic string sent only during registration
}
}
Why MQTT-SN over regular MQTT?
| Feature | MQTT-SN | MQTT |
|---|---|---|
| Header size | 2 bytes | 2+ bytes |
| Topic handling | Pre-registered IDs (2 bytes) | Full string each time |
| Transport | Any (UDP, BLE, Zigbee) | TCP only |
| Sleep support | Built-in (device can sleep) | Keep-alive required |
| Gateway | Required (ESP32-S3) | Direct to broker |
For a device with 256KB RAM on a coin cell battery, every byte matters.
Memory Layout
Flash (1MB):
├── Bootloader (MCUboot) 16KB
├── Application 512KB
├── OTA staging 512KB (A/B update)
└── UICR 4KB (keys, config)
SRAM (256KB):
├── Stack 8KB
├── Static data 32KB
├── Embassy executor ~4KB
├── BLE stack ~32KB
├── MQTT-SN buffers ~2KB
└── Available ~178KB
Build and Flash
# Build with Embassy
cargo build --release --target thumbv7em-none-eabihf
# Flash via probe-rs
probe-rs run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/release/aurora-nrf52
# Debug
probe-rs attach --chip nRF52840_xxAA