Skip to content

Amn04/speck

Repository files navigation

Speck

CI Crates.io Documentation

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.

Features

  • 🔐 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

Quick Start

# 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.bin

Module Format

Offset  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

Architecture

┌─────────────────────────────────────┐
│           Application               │
├─────────────────────────────────────┤
│  Storage Manager  │  Delta Applier  │
├──────────┬──────────────────────────┤
│  Journal │  Wear Leveling  │ Flash  │
├──────────┴──────────────────────────┤
│         Module Parser/Loader        │
├─────────────────────────────────────┤
│     Crypto (Ed25519, SHA256)        │
└─────────────────────────────────────┘

Library Usage

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)?;

Delta Compression

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

Flash Simulation

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 256

Features simulated:

  • Page-level erase (4KB units)
  • Write-before-erase protection
  • Per-page erase cycle counting
  • Wear leveling recommendations

Security Considerations

  1. Key Management: Store signing keys in HSM or secure enclave
  2. Rollback Protection: Always increment monotonic version
  3. Timing Attacks: Verification is constant-time via ed25519-dalek
  4. Replay Protection: Include nonce/timestamp in manifest for network updates
  5. Physical Attacks: Combine with secure boot and flash encryption

Performance

Operation Time (STM32F4@168MHz)
Sign 16KB ~2ms
Verify 16KB ~3ms
Delta 16KB ~10ms
Apply delta ~5ms
Flash erase (4KB) ~20ms
Flash write (256B) ~1ms

Testing

# Unit tests
cargo test

# Integration tests
cargo test --test integration_test

# Benchmarks
cargo bench

# With logging
cargo test -- --nocapture

Real Device Integration

Hardware Requirements

Speck 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

Step-by-Step Integration

1. Partition Your Flash

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

2. Implement Flash Trait

// 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(())
    }
}

3. Initialize Speck in Main

// 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);
    }
}

4. Handle OTA Download

// 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),
    }
}

5. Build and Flash

# 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 Update Workflow

// 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.


Embedded Integration (no_std)

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

License

Licensed under either of:

at your option.

Contributing

See CONTRIBUTING.md for guidelines.

Acknowledgments

  • Delta algorithm inspired by bsdiff
  • Ed25519 implementation from dalek-cryptography
  • Developed for embedded systems with 64KB flash constraints

About

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.

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages