nRF52840 — Rust Embassy USB Honeypot Sentinel
يعمل nRF52840 كحارس USB VBUS للعبث ومستشعر شبكي BLE. فهو يراقب وجود VBUS للاتصالات المادية غير المصرح بها، ويبث بيانات المستشعر عبر إعلانات BLE باستخدام جهاز RADIO الطرفي الخام، وينفذ تصديق البرامج الثابتة باستخدام SHA-256 + ECDSA P-256.
دور في الهندسة المعمارية
المواصفات الفنية
| ميزة | التفاصيل |
|---|---|
| MCU | nRF52840 (ARM Cortex-M4F، 64 ميجاهرتز) |
| كبش | 256 كيلو بايت سرام |
| فلاش | 1 ميجا بايت |
| نطاق | السفارة 0.6 (مضمن غير متزامن Rust) |
| لغة | Rust (#![no_std]، #![no_main]) |
| التشفير | sha2 0.10 (SHA-256) + p256 0.13 (ECDSA P-256، RFC 6979) |
| ** الاتصال ** | بليه 5.0 (راديو خام) + أوسب 2.0 (وضع الجهاز) |
| بروتوكول | MQTT-SN v1.2 عبر embassy_sync::Channel → نقل BLE |
| ** تخزين المفاتيح ** | سجلات عملاء UICR (32 × 32 بت) |
| نبض القلب | 30 ثانية |
لماذا سفارة Rust؟
| ميزة | سفارة | RTIC | زفير (ج) | فريرتوس (ج) |
|---|---|---|---|---|
| سلامة الذاكرة | وقت التجميع | وقت التجميع | الشيكات وقت التشغيل | لا أحد |
| غير متزامن/انتظار | محلي | لا | لا | لا |
| ** تجريدات بدون تكلفة ** | نعم | نعم | لا | لا |
| عدم التخصيص | نعم (#![no_std]) | نعم | ممكن | ممكن |
| ** تجاوز سعة المكدس ** | مستحيل (بدون مواضيع) | ممكن | ممكن | ممكن |
يستخدم Embassy عدم المزامنة/الانتظار الخاص بـ Rust على المعدن العاري - بدون RTOS، ولا خيوط، ولا كومة. المهام هي أجهزة حالة يتم تجميعها في الذاكرة الثابتة، مما يؤدي إلى التخلص من فئات كاملة من الأخطاء المضمنة.
نقطة الدخول الرئيسية
#![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)
يراقب حارس USB وجود VBUS عبر سجلات أحداث POWER الطرفية. يعمل nRF52840 في USB وضع الجهاز — ولا يمكنه تعداد أجهزة USB الأخرى. وبدلاً من ذلك، تشير اتصالات VBUS غير المتوقعة على عقدة استشعار منشورة إلى محاولات تلاعب أو فحص مادية.
#[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;
}
}
لماذا مراقبة USB VBUS؟
تتجاوز هجمات الوصول الفعلي كافة عناصر التحكم في أمان الشبكة. من خلال نشر حراس nRF52840 في غرف الخادم دون أي نشاط USB متوقع، فإن أي حدث VBUS ينبه SOC على الفور بمحاولات التطفل الفعلي.
البث الاستشعار بليه
يقوم مستشعر BLE بقراءة مستشعر درجة الحرارة الموجود على الرقاقة من خلال الوصول المباشر إلى سجل TEMP، وإنشاء ADV_NONCONN_IND PDU، والإرسال على جميع قنوات إعلان BLE الثلاث (37، 38، 39) باستخدام جهاز RADIO الطرفي الخام. يتم أيضًا نشر البيانات عبر MQTT-SN كمسار تسليم متكرر.
#[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;
}
}
تنسيق إعلان بليه
| إزاحة | طول | مجال | قيمة |
|---|---|---|---|
| 0-5 | 6 | أدفا | FICR DEVICEADDR (عشوائي ثابت) |
| 6-8 | 3 | أعلام م | 0x02 0x01 0x06 (LE عام قابل للاكتشاف) |
| 9 | 1 | طول بيانات Mfr | 0x0C (12 بايت) |
| 10 | 1 | نوع الإعلان | 0xFF (خاص بالشركة المصنعة) |
| 11-12 | 2 | معرف الشركة | 0xFFFF (التطوير) |
| 13 | 1 | نوع المنارة | 0x01 (مستشعر AuroraSOC) |
| 14-15 | 2 | درجة حرارة | BE، 0.1 درجة مئوية وحدات |
| 16 | 1 | حركة | 0/1 |
| 17 | 1 | العبث | 0=موافق، 1=تحذير، 2=حرج |
| 18-19 | 2 | بطارية | بي، ميلي فولت |
| 20-21 | 2 | عدد التمهيد | يكون |
الإعلان الإذاعي الخام
لا يتطلب الإعلان عن BLE جهازًا ناعمًا أو مكدس BLE. يدعم الجهاز الطرفي nRF52840 RADIO أصلاً وضع BLE 1 Mbit. يقوم التنفيذ بتكوين RADIO باستخدام عنوان الوصول إلى إعلانات BLE (0x8E89BED6)، ومتعدد الحدود CRC، وتبييض البيانات، ثم ينقل على كل قناة إعلانية بالتسلسل.
تصديق الأجهزة
حالة التصديق محمية بواسطة embassy_sync::Mutex (استبدال static mut غير السليم). تحسب المهمة تجزئة SHA-256 لمنطقة البرامج الثابتة للتطبيق في الفلاش، وتقرأ المفتاح الخاص ECDSA P-256 من سجلات عملاء UICR، وتوقع رسالة التصديق، وتنشر عبر 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
}
}
التشفير
| عملية | قفص | الأداء (Cortex-M4F @ 64 ميجاهرتز) |
|---|---|---|
| SHA-256 (860 كيلو بايت البرامج الثابتة) | sha2 0.10 (no_std) | ~200 مللي ثانية |
| علامة ECDSA P-256 | p256 0.13 (RFC 6979) | ~500 مللي ثانية |
| ترميز JSON | serde-json-core 0.6 | < 1 مللي ثانية |
يستخدم صندوق p256 الأرقام الحسابية والحتمية في الوقت الثابت (RFC 6979)، مما يجعله مناسبًا للاستخدام المضمن بدون RNG خارجي. يتم توفير المفتاح الخاص لـ ECDSA في سجلات عملاء UICR أثناء التصنيع.
بروتوكول MQTT-SN
يقوم عميل MQTT-SN بتنفيذ إنشاء حزم OASIS MQTT-SN v1.2. يمكن الوصول إلى مثيل MqttSnClient المشترك خلف embassy_sync::Mutex من جميع المهام. يتم دفع الإطارات الصادرة إلى Channel الثابت الذي تستنزفه طبقة النقل BLE.
/// 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) { /* ... */ }
}
المواضيع المسجلة مسبقا
| معرف الموضوع | مسار الموضوع | الناشر |
|---|---|---|
0x0010 | aurora/attestation/nrf_usb_sentinel_01/response | تصديق |
0x0020 | aurora/sensors/nrf_usb_sentinel_01/telemetry | مستشعر بليه |
0x0030 | aurora/sensors/nrf_usb_sentinel_01/alerts | USB الحارس |
لماذا MQTT-SN على MQTT العادي؟
| ميزة | MQTT-SN | MQTT |
|---|---|---|
| حجم الرأس | 2 بايت | 2+ بايت |
| التعامل مع الموضوع | المعرفات المسجلة مسبقًا (2 بايت) | سلسلة كاملة في كل مرة |
| ينقل | أي (UDP، بليه، زيجبي) | TCP فقط |
| دعم النوم | مدمج (يمكن للجهاز النوم) | البقاء على قيد الحياة مطلوب |
| بوابة | مطلوب (ESP32-S3) | مباشرة إلى الوسيط |
بالنسبة لجهاز مزود بذاكرة وصول عشوائي (RAM) سعة 256 كيلو بايت على بطارية خلوية صغيرة، فإن كل بايت مهم.
تخطيط الذاكرة
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 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