- Specter Bootloader
- Source of firmware: SD/MicroSD card
- File systems: FAT32
- Access method: read-only
- Integrity check: CRC32
- Digital signature: ECDSA, secp256k1, SHA-256
- Versioning: semantic versioning in a 32-bit number
- Bootloader upgrade: two copies of bootloader and non-upgradable start-up code
- Downgrade: prohibited for the Bootloader and the Main Firmware
- Firmware file contains the following sections:
- One or several payload sections, including "main" and "boot"
- Signature section "sign"
- Each section of the firmware file has its own header and is protected by a separate CRC code
- The digital signature is calculated over all payload sections
- Key hierarchy:
- Vendor keys capable to sign Bootloader and/or Main Firmware
- Maintainer keys capable to sign MainFirmware only
- Key management: Bootloader stores a set of Vendor public keys and a set of Maintainer public keys
- New keys are added and compromised keys are revoked by issuing a new Bootloader
- Multisignature support with configurable thresholds
The Bootloader stores minimum signature thresholds for the Bootloader and for the Main Firmware. The payload is considered valid only if it has a number of valid signatures not less than the corresponding threshold. The Main Firmware may have a mixed set of signatures produced by Vendor and Maintainer keys. All signatures of the Bootloader must be produced using Vendor keys only.
In case an upgrade file contains both the Bootloader and the Main Firmware it must be signed following the same rules as for the Bootloader alone.
These steps are performed by the Start-up code, non-upgradable part of Bootloader that is executed straight on device reset.
- Read Bootloader’s integrity check records stored at the end of 2 pre-determined sectors, containing copies of Bootloader.
- Select a copy of the Bootloader that meets the following criteria:
- Its integrity check record exists and is correct (checking record’s own CRC).
- Its contents (executable code and data) also passes the integrity check.
- Its version is the latest in case two correct Bootloader copies exist. If both copies are correct and have identical versions the first copy is selected.
- If no Bootloader copy is selected, the Start-up code enters an endless loop with a specific LED indication.
- Remap interrupt vectors and branch to the entry point of selected Bootloader (its reset vector).
In case any of the steps 1-5 fails, the Bootloader unmounts the SD card and proceeds with a normal boot described under "Normal boot procedure". In other cases, the microcontroller is rebooted, unmounting the SD card beforehand.
- Ensure that the SD card is inserted.
- Mount the file system.
- Find a firmware file having a predetermined name format and extension, like "specter_upgrade_v1.05.01.bin" using a pattern "specter_upgrade*.bin" in the root directory of SD card. Presence of more than one matching files aborts upgrade process.
- Read fixed-size headers of all sections from the firmware file into RAM, check their CRC. The presence of any unknown section aborts the firmware upgrade process.
- Obtain and verify version information from the headers, including:
- Version of Firmware, check if it is later than currently programmed
- Version of Bootloader if "boot" section exists, check if it is later than programmed
- Verify the correctness of all parameters from headers and that the firmware is compatible with the platform on which the Bootloader is running.
- Verify that the last section is the "sign" section. Parse its contents extracting fingerprint-signature pairs, creating a table in RAM. Ensure that: 3. A public key with a provided fingerprint exists in the pre-defined table stored within the Bootloader. Otherwise, remove the signature from the RAM table. 4. The key referenced by the fingerprint is capable to sign the given payload. Otherwise, remove the signature from the RAM table. The use of Maintainer keys is not allowed to sign a firmware file containing the "boot" section. 5. The key referenced by fingerprint was not encountered in the RAM table before. Otherwise, remove the duplicating signature from the RAM table. 6. The number of remaining signatures is not less than a predefined minimum signature threshold (a separate threshold for the Firmware and for the Bootloader).
- Verify the integrity of all payload sections using the CRC algorithm.
- Perform partial erase of internal flash memory as needed to store the new firmware, excluding sectors occupied by the currently executed copy of the Bootloader, the Start-up code, internal file systems and the key storage. A version check record is created to protect from the downgrade of the Main Firmware storing the latest version ever programmed in the device.
- Copy payload sections from an upgrade file file to internal flash memory.
- Perform verification of signature(s) using a prepared signature table in RAM. Verified data includes: 7. Section headers in RAM (not from SD card) 8. Payload data as read from non-removable Flash devices (not from SD card)
- In case the signature verification is successful, the Bootloader creates an integrity check record in Internal Flash memory containing a CRC code of firmware sections along with version. In case the Bootloader is upgraded as well, an integrity check record is created at the end of its sector.
- Unmount the SD card and reboot.
- Ensure that the integrity check record for the Main Firmware exists and not corrupted by verifying its CRC. The absence of this record means that the device is blank or signature verification has failed. In this case, the Bootloader enters an endless loop with a specific LED and LCD indication.
- Verify the integrity of the Firmware using CRC stored in the integrity check record. In case of failure, indicate error and/or reboot.
- Remap interrupt vectors and branch to the entry point of the Main Firmware (its reset vector).
Semantic versioning is used in the following format MAJOR.MINOR.PATCH[-rcREVISION]. All four components of the version are coded using decimal orders of a 32-bit number, as follows:
- MAJOR: (0 ... 41) x 1e8
- MINOR: (0 ... 999) x 1e5
- PATCH: (0 ... 999) x 1e2
- REVISION: 0 ... 98 - release candidate, 99 - stable version
For example, version "1.22.134-rc5" is coded as 102213405 decimal (0x617a71d). REVISION component is used only for release candidates. For the stable versions, revision is always equal to 99 to position them in history "later" than any release candidate and blocking downgrading to non-stable versions. Another example: a stable version "12.0.15" is coded as 1200001599. Maximally supported version is "41.999.999", which equals to 4199999999 (0xfa56e9ff). Versions above this number are considered invalid. Version constant 0x00000000 is reserved for "undefined" value.
The Bootloader may have an option allowing only stable versions (REVISION equal to 99) to be flashed into end-user devices.
Each section stored in the firmware file has a fixed size header, as defined in the following C language structure:
// Section header
//
// This structure has a fixed size of 256 bytes. All 32-bit words are stored in
// little-endian format. CRC is calculated over first 252 bytes of this
// structure.
typedef struct {
uint32_t magic; // Magic word, BL_SECT_MAGIC ("SECT", 0x54434553 LE)
uint32_t struct_rev; // Revision of structure format
char name[16]; // Name, zero terminated, unused bytes are 0x00
uint32_t pl_ver; // Payload version, 0 if not available
uint32_t pl_size; // Payload size
uint32_t pl_crc; // Payload CRC
uint8_t attr_list[216]; // Attributes, list of: { key, size [, value] }
uint32_t struct_crc; // CRC of this structure using LE representation
} bl_section_t;
Parameter name
contains a zero-terminated string with a section name, one of "main", "boot", "sign" or probably other variants in the future versions.
Array attr_list[]
contains a list of required and optional attributes, specific to each type of section. Each attribute record consists of key byte, size byte, and optionally value field (0...214 bytes) whose length is specified in the size byte. Keys are unique within the attribute list and have the same meaning for each section.
Numerical arguments are coded as variable length integers in little-endian format. For example, a 32-bit number 0x00012345 is coded as three bytes 0x45, 0x23, 0x01. Unused bytes of attr_list[] are filled with 0x00.
String attributes are stored without terminating null characters and are limited in size to 32 characters (per each attribute).
The signature section has a standard section header with the following specifics:
.name = "sign"
.pl_ver = 0
.attr_list = { bl_attr_algorithm, 16, 's', 'e', 'c', 'p', '2', '5', '6', 'k', '1', '-', 's', 'h', 'a', '2', '5', '6', ... }
Section name "sign" is used to identify the signature section. Only one signature section is allowed and it must be the last section in an upgrade file.
Attribute array must contain at least one required attribute, bl_attr_algorithm
specifying digital signature algorithm as a string. Currently, only "secp256k1-sha256" is supported.
The contents of the signature section is a list of fingerprint-signature pairs. When "secp256k1-sha256" is specified, the fingerprint is 16 first bytes of SHA-256 hash of the uncompressed public key (65 bytes, beginning with 0x04), and the signature is a 64-byte compact signature:
0x00000000 [16]: SHA-256(pubkey1), [64]: signature1
0x00000050 [16]: SHA-256(pubkey2), [64]: signature2
0x000000A0 [16]: SHA-256(pubkey3), [64]: signature3
0x000000F0 [16]: SHA-256(pubkey4), [64]: signature4
...
(N-1) * 80 [16]: SHA-256(pubkeyN), [64]: signatureN
Calculation of digital signature is a multi-step process using secp256k1-sha256
algorithm:
- A separate SHA-256 hash is calculated over each payload section including its header:
hi = SHA-256( headeri | payloadi ) - Resulting hash values are concatenated together, hashed again and mapped to 5-bit symbols to produce the data part for a Bech32 message:
data = MAP_5BIT( SHA-256( h0 | ... | hi ) ) - A human readable part for a Bech32 message is produced by concatenating brief section name and a textual representation of version of each payload section. Information realted to each payload section is terminated by dash '-' symbol for a better visual separation.
hrp = BRIEF( name0 ) | version0 | '-' | ... | BRIEF( namei ) | versioni | '-' - The message to sign is produced from data and hrp components according to Bech32 standard:
M = Bech32( hrp, data ) - The message is signed according to widely used Bitcoin message signing protocol with a private key d:
m = 0x18 | "Bitcoin Signed Message:\n" | COMPACT_ENCODE( LEN( M ) ) | M
mh = SHA-256( SHA-256( m ) )
signaturei = SECP256K1_SIGN( d, mh )
A data part for a Bech32 message, data, is an array of 5-bit values according to Bech32 standard. Output of SHA-256 hash function is mapped MSB-first to 52 5-bit values. MSB of the first byte of hash function's output is placed in the MSB of the first 5-bit value. LSB of the last byte is placed in the MSB of the last 5-bit value. Remaining 4 least significant bist of the last 5-bit value are initialized with zeroes.
Example:
sha256_output[32] = { 0xAB, 0xC1, 0x00, ... , 0xFF }
data[52] = { 0x15, 0x0F, 0x00, 0x10, ... , 0x1F, 0x10 }
A human readable part for a Bech32 message, hrp, is produced by concatenating brief section name and a textual representation of version of each payload section. Information realted to each payload section is terminated by dash '-' symbol for a better visual separation. Version is represented as text with no leading zeroes and no dash before "rc" suffix. Brief section name is "b" for the "boot" section and "" (empty string) for the "main" section. Other sections (if any) are not included in generation of human readable part. For additional information please see Version format.
Example 1:
sections = {
{ .name = "boot", .version = 102213405 },
{ .name = "main", .version = 200000199 }
}
hrp = "b1.22.134rc5-2.0.1-"
Example 2:
sections = {
{ .name = "main", .version = 1 }
}
hrp = "0.0.0rc1-"
The Bech32 message as an intermediate product allows delegation of the step 5 to an air-gapped device, like Specter itself. In this scenario only a Bech32 message is transferred instead of full payload sections and headers. Name and version fields inside human readable part provide additional information for visual confirmation on the screen.
Example of a Bech32 message transferred to an external device for signing:
b1.22.134rc5-2.0.1-1xcak8quhfh0uauaxdlp6k6sx96jys8ua4s3q8htdx06xzy2k4a6qamphtk
Additional signatures can be added later by re-writing the signature section of an upgrade file. All signatures must be produced using the same algorithm.
All firmware components used to generate payload sections are initially supplied in Intel HEX format. Before placing into payload sections and signing, these files are converted to binary form. During this conversion, file name, starting address, entry point and other metadata are not preserved. All holes in the address space are filled with 0xFF bytes producing a linear binary file with its size equal to the difference between the first and the last address in the source HEX file.
Entry point for the firmware modules intended for ARM Cortex-M4 platform is stored in the second ISR vector, as usual and not needed to be specified explicitly. It is supposed that for other platforms not supporting this feature, entry point will be specified in section’s attribute.
In the source firmware files payload version is defined with an embedded XML-like version tag: <version:tag10> followed by exactly 10 decimal digits specifying semantic version as defined in "Version Format" subsection. For example, version "1.22.134-rc5" is defined by:
<version:tag10>0102213405</version:tag10>
This tag could be included anywhere within the firmware body. Upgrade generator searches the firmware image for an embedded version tag after conversion from Intel HEX format to linear binary image. If the version tag is not found, the firmware version is considered "undefined". If the firmware includes more than one version tag (of the same format), the firmware is considered invalid.
To support additional tools like a script composing firmware for initial programming, the Bootloader may include an embedded memory map. This is a data structure containing address and length properties of firmware components specific for the platform for which the Bootloader is built.
The embedded memory map is identified by outside XML-like tag:
<memory_map:lebin>...</memory_map:lebin>
But unlike canonical XML internal contents is a binary structure with numerical values stored in little-endian format. At the moment of writing of this document an embedded memory consists of the listed below values. But more values may be added later without breaking compatibility.
// XML-like memory map record containing elements in LE binary format
typedef struct {
// Opening tag: "<memory_map:lebin>"
char opening[18];
// Size of one element in bytes: 4 for 32-bit platform
uint8_t elem_size;
// Size in flash memory reserved for the Bootloader
uintptr_t bootloader_size;
// Start of the Main Firmware in flash memory
uintptr_t main_firmware_start;
// Size reserved for the Main Firmware
uintptr_t main_firmware_size;
// Closing tag: "</memory_map:lebin>"
char closing[19];
} bl_memmap_rec_t;
An integrity check record (ICR) contains the size and CRC values for at most two sections: the Main section and the Auxiliary section. In the current version Auxiliary section is reserved and its parameters are set to 0x00000000.
// One section of integrity check record
typedef struct {
uint32_t pl_size; // Payload size
uint32_t pl_crc; // Payload CRC
} bl_icr_sect_t;
// Integrity check record
//
// This structure has a fixed size of 32 bytes. All 32-bit words are stored in
// little-endian format. CRC is calculated over first 28 bytes of this
// structure.
typedef struct {
uint32_t magic; // Magic word, BL_ICR_MAGIC ("INTG", 0x47544E49 LE)
uint32_t struct_rev; // Revision of structure format
uint32_t pl_ver; // Payload version, 0 if not available
bl_icr_sect_t main_sect; // Main section
bl_icr_sect_t aux_sect; // Auxiliary section (if available)
uint32_t struct_crc; // CRC of this structure using LE representation
} bl_integrity_check_rec_t;
An integrity check record occupying exactly 32 bytes is stored starting from offset -64 relating to the end of a section. For example, if the section has size 131072 bytes (128k), an integrity check record is stored in bytes 131008-131039.
A version check record (VCR) contains the latest version that was ever programmed in the device for robust downgrade protection. This record is created and updated when the Main Firmware section of the flash memory is erased. To avoid data loss due to unexpected reset or power interruption, this process is implemented in the following steps:
- If there is a VCR at the beginning of the section, skip steps 2-3
- First part of the section is erased
- A VCR is created at the beginning of the section
- Second part of the section is erased
- A VCR is created at the end of the section
- First part of the section is erased
The exact size of the section's parts is not important and determined by the platform, assuming that:
- Each part can be erased independently
- Each part is large enough to contain a VCR
When a VCR is created, it is assigned with the latest known version of the Main Firmware that is determined as the greatest of the following three numbers:
- Payload version from a version check record, if it exists
- Version from a VCR at the beginning of the section, if it exists
- Version from a VCR at the end of the section, if it exists
Format of the version check record:
// Magic string for version check record: 16 bytes with terminating '\0'
#define BL_VCR_MAGIC "VERSIONCHECKREC"
// Version check record
//
// This structure has fixed size of 32 bytes. All 32-bit words are stored in
// little-endian format. CRC is calculated over first 28 bytes of this
// structure.
typedef struct {
char magic[sizeof(BL_VCR_MAGIC)]; // Magic string, BL_VCR_MAGIC
uint32_t struct_rev; // Revision of structure format
uint32_t pl_ver; // Payload version, 0 if not available
uint32_t rsv[1]; // Reserved word
uint32_t struct_crc; // CRC of this structure using LE representation
} bl_version_check_rec_t;
A version check record occupying exactly 32 bytes is stored starting from offset -32 relating to the end of a section. For example, if the section has size 131072 bytes (128k), an integrity check record is stored in bytes 131040-131071.
Memory map of the internal Flash memory is provided for STM32F469NI microcontroller. Occupied sectors are chosen to be compatible with MicroPython firmware so the specified Bootloader can replace Mboot. The only change that needs to be done is to reduce the size of FLASH_TEXT section in the platform-specific linker script to free the last two sectors for copies of Bootloader.
Section | Sectors | Starting address | Size, bytes |
Start-up code | 0 | 0x08000000 | 16k |
Key storage | 1 | 0x08004000 | 16k |
MicroPython internal FS | 2-4 | 0x08008000 | 96k |
MicroPython firmware | 5-21 | 0x08020000 | 1664k |
Bootloader, copy 1 | 22 | 0x081C0000 | 128k |
Bootloader, copy 2 | 23 | 0x081E0000 | 128k |