A production-grade runtime package manager for MMU-less microcontrollers. Enables secure dynamic loading of signed, position-independent code modules with flash-aware delta updates.
- 🔐 Ed25519 Signatures: Compact 64-byte signatures with 32-byte public keys
- 📦 Optimized Delta Updates: Binary diff reduces OTA bandwidth by 50-95%
- ⚡ Flash-Aware Storage: Wear leveling, transactional updates, erase tracking
- 🛡️ Anti-Rollback Protection: Monotonic version counters prevent downgrade attacks
- 🔧 Position Independent: Supports runtime relocation for ARM Cortex-M, RISC-V
- 💾 Memory Efficient: Streaming parsers work in <1KB RAM
- 📊 Hardware Locked: Optional hardware revision binding
# Install CLI
cargo install speck-cli
# Generate signing key
speck keygen --output firmware.key --public-key firmware.pub
# Sign firmware binary
speck sign firmware.bin signed.spk --key firmware.key --version 2
# Verify signature
speck verify signed.spk
# Create delta update (v1 -> v2)
speck delta firmware_v1.bin firmware_v2.bin update.patch
# Apply delta
speck apply firmware_v1.bin update.patch firmware_v2_new.binOffset Size Description
0 4 Magic "SPK\x02"
4 2 Format version
6 4 Total size
10 4 Code size
14 4 Entry offset
18 4 Flags (signed, compressed, etc)
22 8 Monotonic version (anti-rollback)
30 4 Hardware revision requirement
34 4 CRC32 of code
38 10 Reserved
48 32 Ed25519 public key
80 64 Ed25519 signature
116 var Code payload
Total header overhead: 116 bytes
┌─────────────────────────────────────┐
│ Application │
├─────────────────────────────────────┤
│ Storage Manager │ Delta Applier │
├──────────┬──────────────────────────┤
│ Journal │ Wear Leveling │ Flash │
├──────────┴──────────────────────────┤
│ Module Parser/Loader │
├─────────────────────────────────────┤
│ Crypto (Ed25519, SHA256) │
└─────────────────────────────────────┘
use speck_core::{Module, KeyPair, DeltaBuilder};
// Generate keys
let keypair = KeyPair::generate();
// Create signed module
let module = Module::builder()
.code(firmware_bytes)
.entry_offset(0x1000)
.version(3) // Anti-rollback
.sign(&keypair)
.build()?;
// Verify and load
module.verify()?;
loader.load(&module.code)?;The delta algorithm uses a combination of COPY (from source) and INSERT (literal) operations:
Original: The quick brown fox jumps over the lazy dog
Modified: The very quick brown fox jumps over the lazy cat
Patch:
INSERT "very "
COPY 35 bytes from offset 0
INSERT "cat"
Typical compression ratios:
- Minor updates (bug fixes): 90-95% reduction
- Medium updates (features): 50-70% reduction
- Major updates: 20-40% reduction
The CLI includes a sophisticated NOR flash simulator:
# Install to offset with automatic page erase
speck flash-install firmware.spk --offset 0x1000 --size-kb 64
# Check wear statistics
speck flash-stats
# Hex dump of flash contents
speck flash-dump --start 0x1000 --len 256Features simulated:
- Page-level erase (4KB units)
- Write-before-erase protection
- Per-page erase cycle counting
- Wear leveling recommendations
- Key Management: Store signing keys in HSM or secure enclave
- Rollback Protection: Always increment monotonic version
- Timing Attacks: Verification is constant-time via ed25519-dalek
- Replay Protection: Include nonce/timestamp in manifest for network updates
- Physical Attacks: Combine with secure boot and flash encryption
| Operation | Time (STM32F4@168MHz) |
|---|---|
| Sign 16KB | ~2ms |
| Verify 16KB | ~3ms |
| Delta 16KB | ~10ms |
| Apply delta | ~5ms |
| Flash erase (4KB) | ~20ms |
| Flash write (256B) | ~1ms |
# Unit tests
cargo test
# Integration tests
cargo test --test integration_test
# Benchmarks
cargo bench
# With logging
cargo test -- --nocaptureSpeck works with any MMU-less MCU with:
- Flash: 64KB+ (NOR flash preferred)
- RAM: 4KB+ for delta apply buffer
- Crypto: Software Ed25519 (no hardware accelerator required)
Tested platforms:
| Platform | Flash | RAM | Notes |
|---|---|---|---|
| STM32F103 | 64KB | 20KB | Blue Pill, Maple Mini |
| STM32F405 | 1MB | 192KB | High-performance |
| nRF52840 | 1MB | 256KB | BLE + Crypto hardware |
| RP2040 | External | 264KB | QSPI flash support |
| ESP32-C3 | 4MB | 400KB | WiFi OTA ready |
Reserve space for Speck storage:
0x0800_0000: Bootloader (8KB)
0x0800_2000: Main App (48KB)
0x0800_E000: Speck Slot 0 (4KB) - Download buffer
0x0800_F000: Speck Metadata (4KB) - Version, state
// src/hal/flash.rs
use speck_core::{Flash, FlashError, PageId};
pub struct Stm32f103Flash {
base_addr: usize,
page_size: usize,
}
impl Flash for Stm32f103Flash {
const PAGE_SIZE: usize = 1024; // STM32F103 has 1KB pages
const NUM_PAGES: usize = 64;
fn erase_page(&mut self, page: PageId) -> Result<(), FlashError> {
let addr = self.base_addr + page.index() * Self::PAGE_SIZE;
// Unlock flash
unsafe {
let flash = &(*stm32f1::stm32f103::FLASH::ptr());
flash.keyr.write(|w| w.bits(0x45670123));
flash.keyr.write(|w| w.bits(0xCDEF89AB));
// Erase page
flash.ar.write(|w| w.bits(addr as u32));
flash.cr.modify(|_, w| w.per().set_bit());
flash.cr.modify(|_, w| w.strt().set_bit());
// Wait for completion
while flash.sr.read().bsy().bit() {}
// Lock flash
flash.cr.modify(|_, w| w.lock().set_bit());
}
Ok(())
}
fn write(&mut self, offset: usize, data: &[u8]) -> Result<(), FlashError> {
// Must be 16-bit aligned on STM32F1
assert!(offset % 2 == 0);
assert!(data.len() % 2 == 0);
unsafe {
let flash = &(*stm32f1::stm32f103::FLASH::ptr());
flash.keyr.write(|w| w.bits(0x45670123));
flash.keyr.write(|w| w.bits(0xCDEF89AB));
flash.cr.modify(|_, w| w.pg().set_bit());
for (i, chunk) in data.chunks_exact(2).enumerate() {
let addr = (self.base_addr + offset + i * 2) as *mut u16;
let halfword = u16::from_le_bytes([chunk[0], chunk[1]]);
addr.write_volatile(halfword);
while flash.sr.read().bsy().bit() {}
}
flash.cr.modify(|_, w| w.lock().set_bit());
}
Ok(())
}
fn read(&self, offset: usize, buf: &mut [u8]) -> Result<(), FlashError> {
let src = (self.base_addr + offset) as *const u8;
for (i, byte) in buf.iter_mut().enumerate() {
*byte = unsafe { src.add(i).read_volatile() };
}
Ok(())
}
}// src/main.rs
use speck_core::{SpeckManager, StorageConfig, KeySlot};
#[entry]
fn main() -> ! {
let flash = Stm32f103Flash::new(0x0800_E000, 1024);
let config = StorageConfig {
slot_count: 2,
slot_size: 4096,
metadata_page: PageId::new(63),
};
let mut speck = SpeckManager::new(flash, config)
.expect("Speck init failed");
// Load trusted public key (first 32 bytes of flash)
let public_key = include_bytes!("../keys/firmware.pub");
speck.trust_key(KeySlot::Primary, public_key);
// Check for pending updates
if speck.has_update_pending() {
defmt::info!("Applying update...");
match speck.apply_update() {
Ok(_) => defmt::info!("Update applied, rebooting..."),
Err(e) => defmt::error!("Update failed: {:?}", e),
}
cortex_m::peripheral::SCB::sys_reset();
}
// Check current app validity
if !speck.verify_current_app() {
defmt::error!("App signature invalid!");
// Fall back to recovery/bootloader
}
// Run main application
loop {
application_main_loop(&mut speck);
}
}// Called when BLE/WiFi receives new firmware chunk
fn on_ota_chunk(speck: &mut SpeckManager, chunk: &[u8], offset: usize) {
speck.write_slot_chunk(0, offset, chunk)
.expect("Write failed");
}
// Called when download complete
fn on_ota_complete(speck: &mut SpeckManager) {
let header = speck.read_slot_header(0);
// Verify signature before committing
match speck.validate_slot(0) {
Ok(valid) if valid => {
speck.mark_update_pending(0);
defmt::info!("Update ready, reboot to apply");
}
Ok(_) => defmt::error!("Signature verification failed!"),
Err(e) => defmt::error!("Validation error: {:?}", e),
}
}# Build for STM32F103
cargo build --release --target thumbv7m-none-eabi --example stm32f103_basic
# Convert to binary
cargo binutils --objcopy -- -O binary target/thumbv7m-none-eabi/release/examples/stm32f103_basic firmware.bin
# Sign the firmware
speck sign firmware.bin firmware.spk --key firmware.key --version 3
# Flash via ST-Link
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
-c "program firmware.spk verify reset exit 0x08002000"// Delta updates reduce OTA bandwidth significantly
fn apply_delta_update(speck: &mut SpeckManager, delta_patch: &[u8]) {
// Delta patch format: COPY(offset, len) + INSERT(bytes)
let mut applier = speck.delta_applier();
// Source: current firmware
applier.set_source(|offset, buf| {
speck.read_app(offset, buf);
});
// Destination: update slot
applier.set_destination(|offset, data| {
speck.write_slot_chunk(0, offset, data)
});
applier.apply(delta_patch)
.expect("Delta apply failed");
}See examples/ for complete working implementations on various platforms.
For no_std environments:
[dependencies]
speck-core = { version = "0.2", default-features = false, features = ["no-alloc"] }Implement the Flash trait for your hardware:
impl Flash for MyNorFlash {
fn erase_page(&mut self, page: PageId) -> Result<()> {
// Hardware-specific erase
}
fn write(&mut self, offset: usize, data: &[u8]) -> Result<()> {
// Hardware-specific program
}
}Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
See CONTRIBUTING.md for guidelines.
- Delta algorithm inspired by bsdiff
- Ed25519 implementation from dalek-cryptography
- Developed for embedded systems with 64KB flash constraints