Skip to main content

nRF52840 — Rust Embassy USB Honeypot Sentinel

The nRF52840 serves as a USB VBUS tamper sentinel and BLE mesh sensor. It monitors VBUS presence for unauthorized physical connections, broadcasts sensor data via BLE advertisements using the raw RADIO peripheral, and performs firmware attestation using SHA-256 + ECDSA P-256.

Role in the Architecture

Technical Specifications

FeatureDetail
MCUnRF52840 (ARM Cortex-M4F, 64 MHz)
RAM256 KB SRAM
Flash1 MB
FrameworkEmbassy 0.6 (async embedded Rust)
LanguageRust (#![no_std], #![no_main])
Cryptosha2 0.10 (SHA-256) + p256 0.13 (ECDSA P-256, RFC 6979)
ConnectivityBLE 5.0 (raw RADIO) + USB 2.0 (device mode)
ProtocolMQTT-SN v1.2 over embassy_sync::Channel → BLE transport
Key StorageUICR Customer Registers (32 × 32-bit)
Heartbeat30 seconds

Why Rust Embassy?

FeatureEmbassyRTICZephyr (C)FreeRTOS (C)
Memory safetyCompile-timeCompile-timeRuntime checksNone
Async/awaitNativeNoNoNo
Zero-cost abstractionsYesYesNoNo
No-allocYes (#![no_std])YesPossiblePossible
Stack overflowImpossible (no threads)PossiblePossiblePossible

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::{bind_interrupts, peripherals, usb};

bind_interrupts!(struct Irqs {
USBD => usb::InterruptHandler<peripherals::USBD>;
POWER_CLOCK => usb::vbus_detect::InterruptHandler;
});

#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_nrf::init(Default::default());

// Connect shared MQTT-SN client to gateway.
{
let mut mqtt = mqtt_sn::MQTT.lock().await;
mqtt.connect("nrf_usb_sentinel_01").await;
}

// Spawn concurrent tasks (no threads — cooperative async)
spawner.spawn(usb_sentinel_task(p.USBD, p.POWER)).unwrap();
spawner.spawn(ble_advertise_task(p.RADIO)).unwrap();
spawner.spawn(attestation_task()).unwrap();

// Heartbeat loop
loop {
embassy_time::Timer::after(embassy_time::Duration::from_secs(30)).await;
}
}

USB Sentinel (VBUS Tamper Detection)

The USB sentinel monitors VBUS presence via the POWER peripheral's event registers. The nRF52840 operates in USB device mode — it cannot enumerate other USB devices. Instead, unexpected VBUS connections on a deployed sensor node signal physical tampering or probing attempts.

#[embassy_executor::task]
pub async fn usb_sentinel_task(
_usbd: peripherals::USBD,
_power: peripherals::POWER,
) {
// Pre-register MQTT-SN topic.
{
let mut mqtt = mqtt_sn::MQTT.lock().await;
mqtt.register_topic("aurora/sensors/nrf_usb_sentinel_01/alerts", 0x0030);
}

// Clear stale POWER events, then poll in 100 ms loop.
let mut prev_vbus = read_vbus_present();
loop {
// Check EVENTS_USBDETECTED / EVENTS_USBREMOVED / EVENTS_USBPWRRDY.
// On any state change → publish JSON alert via MQTT-SN:
// {"event":"connected","vbus_present":true,"output_ready":false,"alert_count":1}

// Belt-and-suspenders: also edge-detect on USBREGSTATUS register.
let vbus = read_vbus_present();
if vbus != prev_vbus { publish_alert(/* ... */).await; }
prev_vbus = vbus;

Timer::after(Duration::from_millis(100)).await;
}
}

Why USB VBUS Monitoring?

Physical access attacks bypass all network security controls. By deploying nRF52840 sentinels in server rooms with no expected USB activity, any VBUS event immediately alerts the SOC of physical intrusion attempts.

BLE Sensor Broadcasting

The BLE sensor reads the on-chip temperature sensor via direct TEMP register access, constructs an ADV_NONCONN_IND PDU, and transmits on all three BLE advertising channels (37, 38, 39) using the raw RADIO peripheral. Data is also published via MQTT-SN as a redundant delivery path.

#[derive(defmt::Format, Serialize)]
pub struct SensorData {
pub temperature_c: i16, // 0.1 °C units
pub motion_detected: bool,
pub tamper_status: u8, // 0=OK, 1=warning, 2=critical
pub battery_mv: u16, // millivolts
pub boot_count: u16,
}

#[embassy_executor::task]
pub async fn ble_advertise_task(_radio: peripherals::RADIO) {
loop {
let data = SensorData {
temperature_c: read_temperature(), // TEMP peripheral (0.25°C → 0.1°C)
motion_detected: false, // GPIO: PIR sensor
tamper_status: 0, // GPIO: tamper switch
battery_mv: 3300, // SAADC: VDDHDIV5
boot_count: boot,
};

// Build PDU: [S0=ADV_NONCONN_IND|TxAdd=random][LENGTH][AdvA(6), Flags(3), MfrData(13)]
let mut pdu = [0u8; 42];
let plen = build_adv_payload(&data, &mut pdu[2..]);
pdu[0] = 0x42; // ADV_NONCONN_IND | TxAdd=random
pdu[1] = plen as u8;

// Transmit on all 3 advertising channels via raw RADIO.
configure_radio_ble(pdu.as_ptr() as u32);
for &(ch, freq) in &ADV_CHANNELS {
transmit_on_channel(ch, freq);
}

// Redundant MQTT-SN publish (JSON via serde-json-core).
let mut json_buf = [0u8; 128];
if let Ok(len) = serde_json_core::to_slice(&data, &mut json_buf) {
mqtt.publish("aurora/sensors/.../telemetry", &json_buf[..len], QoS::AtMostOnce).await;
}

Timer::after(Duration::from_secs(10)).await;
}
}

BLE Advertisement Format

OffsetLengthFieldValue
0–56AdvAFICR DEVICEADDR (random-static)
6–83Flags AD0x02 0x01 0x06 (LE General Discoverable)
91MfrData Length0x0C (12 bytes)
101AD Type0xFF (Manufacturer-Specific)
11–122Company ID0xFFFF (development)
131Beacon Type0x01 (AuroraSOC sensor)
14–152TemperatureBE, 0.1 °C units
161Motion0/1
171Tamper0=OK, 1=warning, 2=critical
18–192BatteryBE, millivolts
20–212Boot CountBE

Raw RADIO Advertising

BLE advertising does not require a softdevice or BLE stack. The nRF52840 RADIO peripheral natively supports BLE 1 Mbit mode. The implementation configures the RADIO with the BLE advertising access address (0x8E89BED6), CRC polynomial, and data whitening, then transmits on each advertising channel sequentially.

Hardware Attestation

Attestation state is protected by an embassy_sync::Mutex (replacing the unsound static mut). The task computes a SHA-256 hash of the application firmware region in flash, reads the ECDSA P-256 private key from UICR customer registers, signs the attestation message, and publishes via MQTT-SN.

pub static ATTEST_STATE: Mutex<CriticalSectionRawMutex, AttestationState> =
Mutex::new(AttestationState {
boot_count: 0,
firmware_hash: [0u8; 32],
last_attestation_ok: false,
});

#[embassy_executor::task]
pub async fn attestation_task() {
// Increment boot count via Mutex (sound, no `static mut`).
{ ATTEST_STATE.lock().await.boot_count += 1; }

loop {
// 1. SHA-256 over flash 0x26000..0xF8000 (4 KiB chunks).
let fw_hash = compute_firmware_hash();

// 2. Read ECDSA P-256 key from UICR customer registers (32 bytes).
let key_bytes = read_signing_key_bytes();
let signing_key = SigningKey::from_slice(&key_bytes)?;

// 3. Build attestation message: device_id ‖ fw_hash ‖ boot_count.
let msg = [device_id, fw_hash, boot_count.to_le_bytes()].concat();

// 4. Sign with ECDSA P-256 (RFC 6979 deterministic nonce).
let sig: Signature = signing_key.sign(&msg);

// 5. Serialize to JSON via serde-json-core.
// {"device_id":"...","firmware_hash":"...","boot_count":N,"signature":"..."}
let report = AttestationReport { device_id, firmware_hash, boot_count, signature };
let len = serde_json_core::to_slice(&report, &mut json_buf)?;

// 6. Publish via MQTT-SN (QoS 1).
mqtt.publish("aurora/attestation/.../response", &json_buf[..len], QoS::AtLeastOnce).await;

Timer::after(Duration::from_secs(300)).await; // Every 5 minutes
}
}

Cryptography

OperationCratePerformance (Cortex-M4F @ 64 MHz)
SHA-256 (860 KB firmware)sha2 0.10 (no_std)~200 ms
ECDSA P-256 Signp256 0.13 (RFC 6979)~500 ms
JSON encodeserde-json-core 0.6< 1 ms

The p256 crate uses constant-time arithmetic and deterministic nonces (RFC 6979), making it suitable for embedded use without an external RNG. The ECDSA private key is provisioned into UICR customer registers during manufacturing.

MQTT-SN Protocol

The MQTT-SN client implements OASIS MQTT-SN v1.2 packet construction. A shared MqttSnClient instance behind an embassy_sync::Mutex is accessible from all tasks. Outbound frames are pushed to a static Channel that the BLE transport layer drains.

/// Shared MQTT-SN client — all tasks access via Mutex.
pub static MQTT: Mutex<CriticalSectionRawMutex, MqttSnClient> =
Mutex::new(MqttSnClient::new([10, 0, 0, 1], 1883));

/// Outbound frame channel — BLE transport drains this.
pub static TX_CHANNEL: Channel<CriticalSectionRawMutex, Vec<u8, 128>, 8> =
Channel::new();

pub struct MqttSnClient {
gateway_addr: [u8; 4],
gateway_port: u16,
connected: bool,
msg_id: u16,
topics: [TopicEntry; 8], // FNV-1a hash → topic_id lookup
topic_count: usize,
}

impl MqttSnClient {
/// Build CONNECT frame: Length + 0x04 + Flags(CleanSession) + ProtocolID + Duration + ClientId
pub async fn connect(&mut self, client_id: &str) { /* ... */ }

/// Build PUBLISH frame with pre-registered topic IDs.
pub async fn publish(&mut self, topic: &str, payload: &[u8], qos: QoS) { /* ... */ }

/// Build SUBSCRIBE frame.
pub async fn subscribe(&mut self, topic: &str, qos: QoS) { /* ... */ }

/// Build DISCONNECT frame.
pub async fn disconnect(&mut self) { /* ... */ }
}

Pre-Registered Topics

Topic IDTopic PathPublisher
0x0010aurora/attestation/nrf_usb_sentinel_01/responseAttestation
0x0020aurora/sensors/nrf_usb_sentinel_01/telemetryBLE Sensor
0x0030aurora/sensors/nrf_usb_sentinel_01/alertsUSB Sentinel

Why MQTT-SN over regular MQTT?

FeatureMQTT-SNMQTT
Header size2 bytes2+ bytes
Topic handlingPre-registered IDs (2 bytes)Full string each time
TransportAny (UDP, BLE, Zigbee)TCP only
Sleep supportBuilt-in (device can sleep)Keep-alive required
GatewayRequired (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