UBI Secure Architecture Guide
What this page covers: SECURE on-flash format for UBI — encrypted device and volume metadata, encrypted EC/VID/data records, key hierarchy, nonce/AAD rules, counter continuity, anchors, and application-facing freshness.
Prerequisites: Read the Overview and the Plain Architecture Guide first for the plain UBI mental model, volume_id, sqnum, reserved PEB mirroring, and the DATA -> VID crash model.
What you will learn: What SECURE mode changes, what it gives the application, how rollback detection plugs into UBI, and how future-write counters survive reclaim and volume removal.
1. 30-second summary
A UBI device works in exactly one mode: PLAIN or SECURE.
SECURE keeps the plain UBI object model and crash model, but wraps every commit-visible object in authenticated encryption:
secure device header,
secure volume header,
secure erase-counter (EC) header,
secure volume-identifier (VID) header,
secure LEB record.
The inner plain payloads stay the same; only their on-flash representation, keying, authentication, and recovery rules change.
1.1 What SECURE changes at a glance
Pillar |
What SECURE changes |
What the application gets |
|---|---|---|
Same UBI semantics |
The secure record wraps the existing plain payload instead of replacing the UBI model. |
Wear-leveling, logical volumes, |
Full encrypted format |
Device metadata, volume metadata, EC, VID, and user LEB data are encrypted and authenticated. Only the 32-byte common prefix stays parseable before authentication. |
No meaningful UBI metadata or user payload is left in plaintext on flash. |
Versioned key hierarchy |
One |
Strong domain separation and per-volume isolation for LEB data. |
Key lifecycle built into the format |
SECURE tracks |
The application does not need to reinvent key-rotation safety inside the filesystem or database. |
Freshness exported to the application |
UBI exports authenticated |
The application can implement rollback / replay detection against its own trusted freshness store. |
Future-write continuity is explicit |
Hidden per-volume anchors preserve per-volume LEB floors, and the secure device header preserves the global VID-domain floor. |
Secure writes can continue safely after reclaim, |
1.2 Why this is hard to tamper with
An attacker who controls raw flash, but does not know an accepted IKM[key_version], cannot make arbitrary modified state look valid to SECURE UBI:
changing ciphertext or authenticated plaintext fields breaks tag verification,
moving a record to another physical location or another logical identity breaks AAD binding,
replaying older authenticated state still has to pass the application’s freshness policy,
an accepted user mapping depends on authenticated secure EC, secure VID, and secure LEB state, not on one ciphertext blob in isolation.
In practice, accepted reads and writes are protected by:
domain-separated child keys,
authenticated record linkage,
location and identity binding in AAD,
monotonic counter usage and usage budgets,
application-visible freshness for rollback / replay decisions.
1.3 Plain-core baseline assumed by this document
This specification assumes that the plain UBI core already provides these baseline properties:
reclaimed data PEBs receive a valid EC header before they re-enter the free pool, and the mapping-visible write order on such a PEB is
DATA -> VID,initialization distinguishes
freefromuncommittedby checking both the VID area and the beginning of the LEB area,erased-state checks use the runtime flash erased value and never hardcode
0xFF,all commit-visible mutating operations pass through one central mutation gate,
volume_idis a durable, monotonically allocated identifier that is not reused during the lifetime of one formatted device.
Those assumptions matter because SECURE mode reuses the same logical object model and the same recovery points. In particular, the secure design relies on VID being the commit-visible mapping record and on volume_id being a stable namespace input for per-volume key derivation, usage recovery, and retirement decisions.
1.4 Mode detection and out-of-scope transitions
PLAIN and SECURE are different on-flash formats.
The library is multi-backend capable: both plain and secure backends may be compiled into the same binary. However, each ubi_device handle operates in exactly one mode — the mode is selected at runtime during ubi_device_init() based on the caller-supplied crypto_cfg pointer and the detected on-flash format:
crypto_cfg == NULLrequests plain mode,crypto_cfg != NULLrequests secure mode,for blank media the requested mode determines the format,
for non-blank media the detected on-flash format must match the requested mode.
This runtime selection model allows a single firmware image to manage both a plain UBI partition on internal flash and a secure UBI partition on external flash, each with its own independent ubi_device handle.
Normative rule:
a secure-mode attach shall accept only SECURE-formatted media,
a plain-mode attach shall accept only plain-formatted media,
a mode mismatch between the caller’s request and the detected format is a hard error,
silent fallback is forbidden,
automatic reformat is forbidden,
in-place migration between PLAIN and SECURE is out of scope and must be rejected.
This document intentionally does not define mixed-mode operation on one partition, automatic reformat, or any attach-time conversion path. If a product ever needs migration, that flow must be an explicit offline tool or explicit maintenance procedure outside the normal attach path.
The practical detection point is the reserved area. SECURE media are identified by the secure wrapper magic at the reserved-header locations. A mode mismatch is a format mismatch, not a conversion opportunity.
Periodic runtime re-audit of the medium against the freshness store is intentionally out of scope. The check_freshness callback remains an attach-time-only interaction. If a product needs active periodic auditing, that should be a separate API or maintenance path, not a change to the check_freshness semantics.
2. High-level picture
2.1 Layer view
+--------------------------------------------------------------------------------------+
| Application / filesystem / database / trusted freshness store |
| - provides key IDs, allowlist, and policy callbacks |
| - stores trusted freshness state and decides rollback policy |
+--------------------------------------------------------------------------------------+
| UBI SECURE |
| - same UBI semantics: volume management, EBA mapping, wear-leveling, reclaim |
| - secure wrappers: device, volume, EC, VID, LEB |
| - key hierarchy, nonce/AAD, budgets, refcounts, hidden anchors, VID floor |
+--------------------------------------------------------------------------------------+
| Raw flash partition |
| - reserved PEBs: secure device header + secure volume headers |
| - data PEBs: secure EC + secure VID + secure LEB, plus hidden anchors |
+--------------------------------------------------------------------------------------+
Boundary summary:
Direction |
Interface |
|---|---|
Application → UBI |
PSA key identifiers for versioned root keys, allowlist of acceptable key versions, optional request to advance the write-active key version, init-time freshness policy callback, optional post-commit freshness-sync callback, event callback |
UBI → Application |
authenticated |
SECURE mode assumes that the plain-core central mutation gate already exists. Secure policy hooks attach to that same gate so that all commit-visible mutating operations are observed at one place.
Because the library supports runtime backend selection, a single firmware image may hold both a plain and a secure ubi_device simultaneously. Each handle uses its own backend, its own configuration, and its own lock. There is no shared mutable state between handles and no cross-talk between the plain and secure backends.
2.2 Flash view
UBI partition
================================================================================
Reserved PEB area (CONFIG_UBI_DEV_HDR_NR_OF_RES_PEBS, range 2..4)
+--------------------------------------------------------------------------------------+
| Reserved PEB 0 | secure device header + secure volume headers |
| Reserved PEB 1 | secure device header + secure volume headers |
| Reserved PEB 2 | optional spare mirror bank |
| Reserved PEB 3 | optional spare mirror bank |
+--------------------------------------------------------------------------------------+
Data PEB area
+--------------------------------------------------------------------------------------+
| Data PEB N | secure EC header | secure VID header | secure LEB record |
| Data PEB N+1 | secure EC header | secure VID header | secure LEB record |
| ... | ... | ... | ... |
+--------------------------------------------------------------------------------------+
Hidden anchor PEBs
+--------------------------------------------------------------------------------------+
| Internal per-volume data PEBs using the same secure EC / secure VID / secure LEB |
| layout. `vid_hdr.lnum = INTERNAL_ANCHOR_LNUM`; secure LEB payload is zero-length. |
+--------------------------------------------------------------------------------------+
2.3 One data PEB
Offset from start of physical eraseblock
================================================================================
0x0000 +---------------------------------------------------------------+
| secure EC header |
| prefix32 | ciphertext(ec_hdr) | tag16 |
+---------------------------------------------------------------+
0x0040 +---------------------------------------------------------------+
| secure VID header |
| prefix32 | ciphertext(vid_hdr + vid_secure_meta) | tag16 |
+---------------------------------------------------------------+
0x00A0 +---------------------------------------------------------------+
| secure LEB record |
| single-tag : prefix32 | ciphertext(payload_bytes) | tag16 |
| chunked : prefix32 | chunk0 ciphertext | tag0 | ... |
+---------------------------------------------------------------+
For the base single-tag layout, secure data-PEB metadata consumes 208 B:
secure EC header: 64 B
secure VID header: 96 B
secure LEB prefix: 32 B
secure LEB tag: 16 B
3. What SECURE mode gives and what it does not
3.1 What the application gets
SECURE UBI combines one secure on-flash format with the existing UBI storage model:
Area |
What the application gets |
|---|---|
Confidentiality at rest |
User data and UBI metadata are encrypted. |
Integrity and authenticity |
Every secure record is authenticated before it is trusted. |
Plain UBI behavior |
Logical volumes, wear-leveling, bad-block handling, dual-bank reserved metadata, and |
Rollback / replay hook |
UBI exports authenticated |
Key lifecycle handling |
|
High-write orientation |
The format is designed for repeated writes; nonce use, authenticated-byte usage, rotation thresholds, and reclaim-time continuity are explicit parts of the architecture. |
3.2 Security consequence in plain language
An attacker with raw-flash access but without an accepted root key version cannot:
read meaningful UBI metadata or user payload from secure ciphertext alone,
modify authenticated payload fields, counters, or metadata without failing AEAD verification,
move ciphertext to another physical eraseblock, volume, or logical number and still have it accepted,
replay older authenticated state without also defeating the application’s trusted freshness policy.
To make forged user data accepted, the attacker would need at least one of the following:
compromise an accepted
IKM[key_version], which yields all child keys for that version,break the relevant AEAD checks across the secure EC -> secure VID -> secure LEB chain,
or defeat the application’s trusted freshness store so that stale
(device_revision, global_sqnum)is accepted.
Knowledge of one child key alone is not sufficient to build a complete accepted mapping; accepted data depends on the authenticated chain and AAD binding, not on one record in isolation.
3.3 Explicit boundary
SECURE mode is intentionally scoped:
UBI does not assume a global journal or a hardware monotonic counter,
UBI does require fresh cryptographic randomness for every secure write,
UBI does export authenticated freshness values to the application,
complete anti-rollback still requires an external trusted freshness store or equivalent trust anchor.
So SECURE mode provides authenticated on-flash encryption plus the continuity state and policy hooks needed to keep writing safely. It does not claim that rollback can be prevented without any external trust anchor.
4. Terminology and invariants
4.1 Terms
Physical eraseblock index
The numeric index of a physical eraseblock inside the UBI partition. This document writes it out in full; it does not rely on the shorter pnum nickname.
Flash offset
A byte offset from the beginning of the UBI partition.
Commit-visible
State that survives power loss and is visible to init without consulting transient RAM state.
Authenticated object
A secure record whose AEAD verification succeeded with an allowlisted key version.
Live mapping
The winning (volume_id, lnum) mapping selected by the highest authenticated vid_sqnum.
Dirty PEB
A data PEB that still contains stale or interrupted state and must be erased before reuse.
Free data PEB
A data PEB with a valid secure EC header, an erased secure VID area, and an erased secure LEB-prefix area. “Erased” always means equal to the flash device’s configured erased value. It must never be hardcoded to 0xFF.
Uncommitted data PEB
A data PEB with a valid secure EC header and an erased secure VID area, but with non-erased bytes in the secure LEB-prefix area. This represents an interrupted DATA -> VID write and must not be classified as free.
Hidden anchor PEB
A live, internal, non-user-addressable data PEB owned by one secure volume. It uses a dedicated internal logical number, stores a zero-length secure LEB record, and can carry the authoritative per-volume LEB usage floor when user mappings disappear.
Volume identifier (volume_id)
A durable identifier assigned when a volume is created. volume_id is monotonic, persists across reboot, and is not reused during the lifetime of one formatted device. SECURE mode uses volume_id as the per-volume cryptographic identity.
VID next-counter floor
The monotonic floor for future secure VID writes under the current authenticated write_active_key_version. SECURE stores this value in the secure device header as vid_next_counter_floor.
Naming note
The term volume_id in this document names the durable per-volume identity currently carried by the plain-core vol_id field. The longer name is intentional: it makes the cryptographic identity explicit and avoids overloading local scan or RAM bookkeeping ordinals.
Export-width note for device_revision
The API may expose device_revision as a widened integer type for convenience. If a concrete implementation stores a narrower on-flash revision field, widening must preserve the numeric value and ordering semantics.
4.2 Core invariants
After successful initialization:
Every live user LEB maps to exactly one data PEB.
Every secure volume also owns exactly one live hidden anchor mapping, identified by a dedicated internal logical number that is never exposed through the user API.
Among competing authenticated VIDs for the same
(volume_id, lnum), the highervid_sqnumwins.A dirty PEB must be erased before it can re-enter the free pool.
Reserved PEBs are used only for secure device and volume metadata.
The authenticated secure device header stores:
write_active_key_versionvid_next_counter_floor
For secure LEB writes, the authoritative next write counter and total authenticated bytes are recovered from authenticated VID-side metadata, including the hidden anchor when user mappings disappear.
volume_idis unique for the lifetime of one formatted device and is the only per-volume cryptographic identity. No reusable RAM index or scan-local ordinal may appear in LEB key derivation, LEB usage recovery state, or retirement logic.The write-active key version is monotonic and must never move back to an older value on the same formatted device.
A key version becomes retirable only when no authenticated on-flash object still references it.
The device must not perform a secure write if it cannot construct a fresh nonce from cryptographically secure randomness.
All commit-visible mutating operations pass through one central mutation gate so that secure policy and freshness hooks observe the same transition points.
SECURE must preserve a path to one emergency free data PEB for hidden-anchor maintenance. It shall either keep one free data PEB available or first reclaim a dirty PEB that is not the last current writable witness before reclaiming a protected witness. It must never erase the last current writable witness first and only afterwards discover that there is nowhere to commit the inherited anchor state.
5. Cryptographic profile
5.1 Algorithm choice
UBI SECURE uses AES-128-CCM for all authenticated-encryption records.
This is a good match for UBI because UBI stores explicit records, not streams:
metadata records are naturally packet-sized,
AAD is explicit,
the nonce is explicit,
the format naturally fits the “authenticate then decrypt one complete record” model,
embedded platforms that expose hardware AEAD support commonly support CCM for this workload.
The child AEAD key size is therefore fixed:
AES child key size = 128 bits
This is independent from the root-key requirement for IKM[v].
5.2 CCM parameters
This architecture fixes:
nonce length = 13 bytes
tag length = 16 bytes
That implies the standard CCM relation:
n + q = 15
13 + 2 = 15
With q = 2, one CCM invocation can cover:
payload_len < 2^(8*q) = 65536 bytes
So the architecture requires:
secure_leb_payload_bytes < 65536
This must be enforced both:
as a compile-time guard for supported geometries, and
as a runtime rejection if a larger geometry is somehow presented.
For the base single-tag secure LEB layout, the maximum logical payload on one data PEB is:
secure_leb_payload_bytes_single = peb_size - 208
Therefore single-tag mode is valid only if:
peb_size - 208 < 65536
If that condition is false for the selected geometry, SECURE must either:
require chunked LEB mode for that geometry, or
reject SECURE mode at build time or initialization time.
5.3 Why UBI tracks both AEAD invocations and authenticated bytes
CCM has two properties that matter here:
every invocation under one key requires a unique nonce,
the security margin degrades as more authenticated data are processed under the same key.
For LEB keys, UBI therefore tracks two monotonic usage dimensions:
number of AEAD invocations already consumed under
{key_version, volume_id}total authenticated bytes already processed under
{key_version, volume_id}
Those two dimensions are represented as:
leb_write_counterleb_total_auth_bytes
leb_write_counter is the next unused AEAD / nonce-counter value for that {key_version, volume_id}. leb_total_auth_bytes is the cumulative authenticated-byte volume:
AAD bytes + payload plaintext bytes
For metadata keys, UBI also evaluates both dimensions, but the accounting is different:
metadata AEAD-invocation count is recovered directly from authenticated
prefix32.countervalues,metadata authenticated-byte usage is derived, not persisted, because each metadata record type has a fixed plaintext size and a fixed AAD shape.
That asymmetry is intentional:
metadata records have fixed sizes, so authenticated-byte usage can be reconstructed from
{domain, key_version}and the next counter value,LEB records have variable payload size, so cumulative authenticated-byte usage must be persisted in secure VID metadata,
the architecture budgets on AEAD invocations and authenticated bytes because those are the monotonic quantities directly visible at the UBI layer.
The design does not persist an internal AES-128 block-operation count. If an implementation wants a block-level estimate, it can conservatively derive it from authenticated bytes and the fixed CCM per-record formatting cost. The architectural policy surface, however, is expressed in:
AEAD invocations,
authenticated bytes.
Both dimensions are checked before a new write is committed.
5.4 Entropy source requirement
Every secure record prefix carries a fresh 6-byte salt. That salt must come from a cryptographically secure random source.
The architectural requirement is:
SECURE writes use PSA random generation or an equivalent cryptographically secure RNG,
non-cryptographic RNG APIs and test RNG backends are forbidden for secure nonce construction,
if secure randomness is unavailable, the write must fail closed.
In other words, the device must not attempt a secure write unless it can produce a fresh unpredictable nonce salt for that write.
6. Key material and key hierarchy
6.1 Root key model and minimum requirement
The root key material for one key version is written conceptually as:
IKM[v]
SECURE mode is PSA-only. IKM[v] remains a conceptual term in the architecture, but the normal implementation contract is:
the application provides a PSA key identifier for key version
v,that PSA key refers to device-unique secret root material,
UBI does not receive raw root-key bytes through its normal API surface,
child keys should remain non-exportable PSA keys whenever the platform supports that flow.
The root-key requirement is:
IKM[v]shall represent at least 256 bits of secret, device-unique input key material,IKM[v]must have high effective entropy,IKM[v]must be unique per device,the application may satisfy that by provisioning a device-unique root secret directly,
or by deriving
IKM[v]from a device-unique secret such as HUK/DHUK.
The following are not acceptable by themselves:
serial number,
MAC address,
UUID,
devicetree data,
any other low-entropy identifier.
This is mandatory. Without device-unique keying, a full flash clone could be readable on another device with the same root key material.
If a platform cannot provide all of the following, SECURE mode is unsupported on that platform:
PSA AEAD for
AES-128-CCM,PSA key derivation for
HKDF-SHA-256,cryptographically secure random generation.
6.2 Child keys
UBI derives child keys from IKM[v] using HKDF-SHA-256.
The KDF encoding is normative.
Extract step:
PRK[v] = HKDF-Extract(salt = "", IKM[v])
Expand-step output length:
L = 16 bytes
Exact expand labels in the current format:
K_device_header[v]
= HKDF-Expand(PRK[v],
"UBI" || 0x00 || "DEVICE-HEADER" || 0x00 || 0x01,
L)
K_volume_header[v]
= HKDF-Expand(PRK[v],
"UBI" || 0x00 || "VOLUME-HEADER" || 0x00 || 0x01,
L)
K_erase_counter[v]
= HKDF-Expand(PRK[v],
"UBI" || 0x00 || "ERASE-COUNTER" || 0x00 || 0x01,
L)
K_volume_identifier[v]
= HKDF-Expand(PRK[v],
"UBI" || 0x00 || "VOLUME-IDENTIFIER" || 0x00 || 0x01,
L)
K_leb[v][volume_id]
= HKDF-Expand(PRK[v],
"UBI" || 0x00 || "LEB" || 0x00 || 0x01 || be32(volume_id),
L)
Normative rules:
HKDF salt is the zero-length string in the definitions above,
0x01is the current format/KDF compatibility byte,all integer fields inside KDF context are big-endian,
volume_idis encoded asbe32(volume_id),the exact label bytes above are part of on-flash compatibility and must not change without an explicit format-version change.
The LEB key is volume-specific. That is why the LEB usage budget is tracked per:
{key_version, volume_id}
The key hierarchy itself is:
flowchart TD
IKM["IKM[key_version]"]
PRK["PRK[key_version]"]
DH["K_device_header[key_version]"]
VH["K_volume_header[key_version]"]
ECH["K_erase_counter[key_version]"]
VID["K_volume_identifier[key_version]"]
LEB["K_leb[key_version][volume_id]"]
IKM --> PRK
PRK --> DH
PRK --> VH
PRK --> ECH
PRK --> VID
PRK --> LEB
Runtime accounting is attached to those domains, but it is not part of the derived key material itself:
K_device_header[key_version]
-> next_counter[{device_header, key_version}]
K_volume_header[key_version]
-> next_counter[{volume_header, key_version}]
K_erase_counter[key_version]
-> next_counter[{erase_counter, key_version}]
K_volume_identifier[key_version]
-> next_counter[{volume_identifier, key_version}]
secure device header
-> write_active_key_version
-> vid_next_counter_floor
K_leb[key_version][volume_id]
-> next_leb_write_counter
-> next_leb_total_auth_bytes
This separation is intentional:
child keys come from HKDF only,
counters and usage floors are runtime / on-flash accounting state associated with those keys,
the counters are never fed back into HKDF as additional key-derivation input.
volume_id is the only durable per-volume namespace input allowed in LEB key derivation. Any reusable RAM index or scan-local ordinal is explicitly out of scope for secure identity.
By contrast, the VID-domain key remains global for one key version:
K_volume_identifier[key_version]
That is why SECURE preserves future secure VID-write continuity with the authenticated vid_next_counter_floor stored in the secure device header, instead of attempting to namespace the VID key by volume_id.
6.3 Chunked mode reuses the base LEB key
Chunked mode does not derive a second per-chunk key hierarchy.
The base key remains:
K_leb[v][volume_id]
and that same key is reused for every chunk of the current secure LEB record.
Nonce uniqueness comes from the combination of:
one fresh per-record
salt,one monotonic
counter_basestored inprefix32.counter,one per-chunk counter increment,
chunk_indexincluded in AAD.
This is sufficient because CCM requires nonce uniqueness per key. It does not require a different key per chunk.
The design therefore keeps chunked mode simple:
one LEB key per
{key_version, volume_id},one prefix for the whole record,
no extra chunk-key table,
no extra HKDF work during reads or writes beyond the normal LEB-key derivation.
Chunked mode still increases the LEB usage budget because one logical write can consume more than one AEAD invocation. That accounting is described in sections 8.2 and 9.7.
6.4 Why secure records are separate types
The architecture keeps plain and secure record types separate.
That is deliberate.
The plain structures remain the semantic payloads already used by UBI:
struct ubi_dev_hdrstruct ubi_vol_hdrstruct ubi_ec_hdrstruct ubi_vid_hdr
The secure records are distinct wrapper types that add:
common prefix,
secure-only metadata,
tag,
AAD rules,
versioning.
This makes the secure format easier to maintain, easier to version, and easier to reason about than trying to overload the plain record definitions themselves.
7. Secure record formats
7.1 Common prefix
Every secure record begins with the same 32-byte prefix:
struct ubi_crypto_prefix32 {
uint32_t magic; /* format magic */
uint8_t wrapper_version; /* current on-flash format version */
uint8_t domain; /* DEVICE_HEADER / VOLUME_HEADER / ERASE_COUNTER / VOLUME_IDENTIFIER / LEB */
uint8_t key_version; /* version of IKM[v] */
uint8_t flags; /* record flags */
uint8_t salt[6]; /* fresh RNG salt */
uint8_t counter[6]; /* monotonic AEAD counter or counter base */
uint8_t reserved[12]; /* zero in the current format */
};
Properties:
magicis 32 bits,wrapper_versionis fixed for the current format and reserved for explicit future compatibility decisions,saltstays 6 bytes,counterstays 6 bytes,the prefix is plaintext for parsing,
the prefix becomes trustworthy only after AEAD verification succeeds,
all multi-byte integers in the prefix are serialized in big-endian.
7.2 Secure record names
The document uses these names consistently:
secure device header
secure volume header
secure erase-counter (EC) header
secure volume-identifier (VID) header
secure LEB record
7.3 Secure device header
The secure device header encrypts two payloads together:
inner
struct ubi_dev_hdrsecure device-side crypto metadata
struct ubi_dev_secure_meta {
uint8_t write_active_key_version; /* authenticated current write-active version */
uint8_t reserved0[7]; /* zero in the current format */
uint64_t vid_next_counter_floor; /* next unused VID counter for write_active_key_version */
};
Normative rules:
write_active_key_versionis part of authenticated on-flash policy state,in the current format,
write_active_key_versionshall match the authenticatedprefix32.key_versionof the secure device header record,vid_next_counter_flooris monotonic and never decreases,when the write-active key version advances to a newer value,
vid_next_counter_flooris reinitialized to that newer version’s first valid VID counter and the old version never becomes write-active again.
+----------+------------------------------------------+--------+
| prefix32 | ciphertext(dev_hdr + dev_secure_meta) | tag16 |
+----------+------------------------------------------+--------+
32 B 48 B 16 B total = 96 B
7.4 Secure volume header
+----------+------------------------+--------+
| prefix32 | ciphertext(vol_hdr) | tag16 |
+----------+------------------------+--------+
32 B 48 B 16 B total = 96 B
7.5 Secure EC header
+----------+------------------------+--------+
| prefix32 | ciphertext(ec_hdr) | tag16 |
+----------+------------------------+--------+
32 B 16 B 16 B total = 64 B
7.6 Secure VID header
The secure VID header contains two plaintext domains encrypted together:
inner
struct ubi_vid_hdrsecure VID-side LEB metadata
struct ubi_vid_secure_meta {
uint64_t leb_write_counter; /* next unused AEAD counter for {key_version, volume_id} */
uint64_t leb_total_auth_bytes; /* cumulative authenticated bytes (AAD + payload) for {key_version, volume_id} */
};
+----------+----------------------------------------+--------+
| prefix32 | ciphertext(vid_hdr + vid_secure_meta) | tag16 |
+----------+----------------------------------------+--------+
32 B 48 B 16 B total = 96 B
These two fields are the authoritative write-usage recovery state for:
{key_version, volume_id}
They exist for one reason:
init already needs to authenticate secure VID records,
therefore init can recover future LEB write state without reading and authenticating every LEB payload.
Every committed secure VID record and its corresponding secure LEB record use the same key_version.
vid_hdr.volume_id is the durable, never-reused volume identity provided by the plain core. It is the LEB key-derivation input and the identity used by runtime usage accounting.
7.7 Secure LEB record (single-tag mode)
+----------+---------------------------+--------+
| prefix32 | ciphertext(payload_bytes) | tag16 |
+----------+---------------------------+--------+
Important points:
payload_bytesis the current logical payload length,payload_bytesis taken from authenticatedvid_hdr.data_size,if authenticated
vid_hdr.data_size == 0, SECURE still writes a zero-length secure LEB record using the baseprefix32 || tag16layout with zero ciphertext bytes,for that zero-length case, one AEAD invocation is still consumed and
leb_total_auth_bytesadvances by the fixed zero-length LEB AAD size,single-tag mode is valid only when authenticated
payload_bytes < 65536,the architecture does not require buffering the full maximum LEB capacity when the logical payload is shorter,
but single-tag mode still requires full authentication of the complete recorded payload before any plaintext may be returned,
if flash write alignment requires a longer terminal write, any extra bytes must appear only after
tag16, must be written as the flash erased value, and are outside the authenticated record,read-side record length comes only from authenticated
vid_hdr.data_size; trailing tail bytes aftertag16are ignored.
7.8 Secure LEB record (chunked mode)
Chunked mode keeps the same prefix and the same secure VID metadata. Only the payload body changes:
+----------+--------------------+------+--------------------+------+-----+
| prefix32 | chunk0 ciphertext | tag0 | chunk1 ciphertext | tag1 | ... |
+----------+--------------------+------+--------------------+------+-----+
Rules:
chunk size is fixed by Kconfig,
chunk index starts at
0,all chunks use the same base key
K_leb[key_version][volume_id],chunk index is included in AAD,
the chunk count is derived from authenticated
vid_hdr.data_size:
chunk_count = ceil(data_size / chunk_size) for data_size > 0
if
data_size == 0, SECURE falls back to the zero-length base recordprefix32 || tag16; no per-chunk tags are present,for
data_size > 0,leb_write_counteradvances bychunk_countandleb_total_auth_bytesadvances bydata_size + chunk_count * fixed_leb_aad_bytes_chunked,for the zero-length fallback,
leb_write_counteradvances by1andleb_total_auth_bytesadvances byfixed_leb_aad_bytes_single,if chunk ciphertext is written directly to flash,
chunk_sizeshould be a multiple of the flash write alignment,if one
(chunk ciphertext || tag)write would violate the target flash alignment rule, the implementation must either use an aligned staging buffer or reject the configuration.
Chunked mode is optional only for geometries where single-tag mode is valid. For geometries that violate the single-tag CCM payload limit, chunked mode becomes mandatory if SECURE is supported at all.
7.10 Integrity semantics of inner CRC fields
The inner plain payloads may still contain fields such as hdr_crc. In SECURE mode, their meaning is reduced to compatibility and optional format-consistency, not primary security.
Normative rule:
AEAD verification is the authoritative integrity and authenticity decision.
Inner CRC fields are not an independent trust primitive in SECURE mode.
If retained, inner CRC fields may be recomputed on write so that the embedded plain payload stays self-consistent.
If checked on read, an inner CRC mismatch after successful AEAD verification is a format violation of authenticated plaintext, not a separate authenticity result.
Inner CRC fields must not be used for nonce construction, AAD cross-record binding, rollback logic, or key-retirement logic.
8. Nonce and AAD
8.1 Base nonce
For all non-chunked secure records:
nonce = domain(1 B) || salt(6 B) || counter(6 B)
So the entire 13-byte CCM nonce comes directly from authenticated prefix fields.
8.2 LEB write counter identity
For secure LEB writes, these two values are intentionally related but not identical:
prefix32.countervid_secure_meta.leb_write_counter
They mean:
prefix32.counter = counter_base used by this committed record
vid_secure_meta.leb_write_counter = next unused counter after this committed record
So:
single-tag write or zero-length write
next = counter_base + 1chunked write with
chunk_count = N
next = counter_base + N
Why both exist:
prefix32.counteris required immediately to build the current record nonce or nonce sequence,vid_secure_meta.leb_write_counteris the authenticated, commit-visible copy that init trusts when reconstructing the next write state.
This split is what allows chunked mode to consume multiple AEAD invocations while still keeping recovery state in the secure VID record.
8.3 Chunked-mode nonce
Chunked mode keeps the prefix unchanged and reuses the same base LEB key for all chunks of the record.
Let:
counter_base = be48(prefix32.counter)
For chunk i (0 <= i < chunk_count), UBI derives:
chunk_nonce_i = domain(1 B) || salt(6 B) || be48(counter_base + i)
Chunk index is also included in AAD.
This keeps the on-flash structure simple:
one common prefix,
one key version,
one counter field,
one LEB key per
{key_version, volume_id},optional chunking without inventing a second prefix layout or a second key hierarchy.
8.4 AAD encoding rule
All AAD inputs must be serialized in a canonical binary form:
fixed-width fields only,
big-endian integers,
no text formatting such as
%u,no platform-dependent structure layout,
no
packeddependency.
8.5 AAD by record type
AAD fields are serialized in the exact order shown below.
Secure device header — 44 B total
prefix32: 32 Breserved-PEB physical eraseblock index
be32(...): 4 Bdevice-header flash offset from the start of the UBI partition
be64(...): 8 B
Secure volume header — 53 B total
prefix32: 32 Breserved-PEB physical eraseblock index
be32(...): 4 Bvolume-header flash offset from the start of the UBI partition
be64(...): 8 Bauthenticated
device_header.revisionbe64(...): 8 Bauthenticated parent secure-device
key_versionu8: 1 B
Secure EC header — 44 B total
prefix32: 32 Bdata-PEB physical eraseblock index
be32(...): 4 BEC-header flash offset from the start of the UBI partition
be64(...): 8 B
Secure VID header — 53 B total
prefix32: 32 Bdata-PEB physical eraseblock index
be32(...): 4 BVID-header flash offset from the start of the UBI partition
be64(...): 8 Bauthenticated
ec_hdr.ecbe64(...): 8 Bauthenticated parent secure-EC
key_versionu8: 1 B
Secure LEB record, single-tag or zero-length — 74 B total
prefix32: 32 Bdata-PEB physical eraseblock index
be32(...): 4 BLEB-data flash offset from the start of the UBI partition
be64(...): 8 Bauthenticated
ec_hdr.ecbe64(...): 8 Bauthenticated parent secure-EC
key_versionu8: 1 Bauthenticated
vid_hdr.volume_idbe32(...): 4 Bauthenticated
vid_hdr.lnumbe32(...): 4 Bauthenticated
vid_hdr.sqnumbe64(...): 8 Bauthenticated
vid_hdr.data_sizebe32(...): 4 Bauthenticated parent secure-VID
key_versionu8: 1 B
Secure LEB record, chunked — 78 B total
all fields from the single-tag / zero-length LEB AAD: 74 B
chunk_indexbe32(...): 4 B
For hidden anchors, vid_hdr.lnum is the dedicated internal anchor logical number. Because that value is authenticated both inside the secure VID payload and inside child AAD, an anchor record cannot be reinterpreted as a user-visible LEB mapping.
9. Freshness and recovery state
9.1 Reserved-area freshness
Reserved-area freshness is represented by authenticated:
device_header.revision
UBI exports that as:
device_revision
In the current format, device_revision advances on every commit-visible reserved metadata rewrite, including:
volume create,
volume delete,
volume resize,
write-active key-version change,
any other device-header mutation.
UBI itself uses device_revision to select the newest authenticated reserved generation.
9.2 Data-area freshness
Each inner struct ubi_vid_hdr contains:
vid_sqnum = vid_hdr.sqnum
UBI initialization computes:
global_sqnum = max(vid_sqnum over all live authenticated mappings)
So:
vid_sqnumis a field in one secure VID record,global_sqnumis the reconstructed device-wide high-watermark exported to the application.
global_sqnum advances only when a data mapping becomes commit-visible. Reserved-only mutations may advance device_revision while leaving global_sqnum unchanged.
Hidden anchor VIDs are internal and are excluded from the user-visible EBA table, but they are still live authenticated secure VID mappings. If one of them has the highest authenticated vid_sqnum, it contributes to global_sqnum.
9.3 Exported freshness descriptor
The exported freshness state is the pair:
(device_revision, global_sqnum)
The architecture exports both values because reserved metadata and live data mappings evolve on different timelines.
The architecture does not impose one mandatory total order over that pair.
Instead, UBI guarantees only that:
device_revisionis authenticated and selected from the winning reserved generation,global_sqnumis authenticated and reconstructed from winning live mappings,both values describe the same selected secure device state.
The application may then define product policy, for example by using:
a lexicographic comparison,
per-field minimum accepted values,
or stricter product-specific rules.
This keeps the on-flash format simple and keeps anti-rollback policy in the application, where product trust assumptions already live.
9.5 Where the counters come from
There are four counter families relevant to future writes.
Secure device header / secure volume header / secure EC header
For these metadata keys, the next counter is reconstructed from authenticated secure records themselves.
For each:
{domain, key_version}
UBI computes:
next_counter = 1 + max(authenticated prefix32.counter for that domain and key_version)
Metadata authenticated-byte usage is then derived from that next_counter and from the fixed per-domain record sizes.
Secure VID header
K_volume_identifier[key_version] is global for the VID domain. Future VID writes are needed only for the current authenticated write_active_key_version stored in the secure device header.
UBI therefore reconstructs:
next_vid_counter[write_active_key_version] =
max(device_header.vid_next_counter_floor,
1 + max(authenticated prefix32.counter over all live secure VID records
whose key_version == write_active_key_version))
Important points:
device_header.vid_next_counter_flooris the durable floor carried by dual-bank reserved metadata,live authenticated secure VID records may raise that floor further,
older non-write-active VID key versions may still be needed for reads, inventory, and retirement,
but future writes never return to an older write-active key version.
Secure LEB record
For LEB keys, init does not trust the data-area prefix to reconstruct future write state.
Instead it recovers:
next_leb_write_counter
next_leb_total_auth_bytes
from the maximum authenticated values found in secure VID metadata for the given:
{key_version, volume_id}
The recovery sources can be read as:
authenticated secure device / volume / erase-counter headers
-> prefix32.counter
-> next_counter[{domain, key_version}]
authenticated secure device header
-> write_active_key_version
-> vid_next_counter_floor
-> next_vid_counter[write_active_key_version]
authenticated secure VID header for one {key_version, volume_id}
-> vid_secure_meta.leb_write_counter
-> vid_secure_meta.leb_total_auth_bytes
-> next_leb_write_counter[{key_version, volume_id}]
-> next_leb_total_auth_bytes[{key_version, volume_id}]
For LEB recovery, the secure VID sources include:
live user mappings for that
volume_id,the live hidden anchor mapping for that
volume_id, if present.
9.6 Why there are two LEB metrics
leb_write_counter answers:
what is the next unused AEAD / nonce-counter value under this LEB key?
Equivalently, it captures how many AEAD invocations have already been consumed under that {key_version, volume_id}.
leb_total_auth_bytes answers:
how many authenticated bytes have already been processed under this LEB key?
That total is:
AAD bytes + payload plaintext bytes
UBI keeps both persisted values because neither one alone is sufficient:
AEAD-invocation count is needed for nonce uniqueness and counter overflow checks,
cumulative authenticated bytes are needed for usage budgets that reflect total CCM work under that key.
9.7 Write-budget enforcement
UBI maintains runtime usage state:
metadata_usage[{domain, key_version}] -> next_counter
vid_usage[write_active_key_version] -> next_counter
leb_usage[{key_version, volume_id}] -> next_write_counter, next_total_auth_bytes
Before a write is committed, UBI computes projected post-write values.
For metadata, where metadata_auth_bytes_per_record[domain] means plaintext bytes plus AAD bytes for that metadata domain:
projected_next_counter = next_counter + 1
projected_metadata_auth_bytes =
projected_next_counter * metadata_auth_bytes_per_record[domain]
For the current write-active VID domain, the same rule applies:
projected_next_vid_counter = next_vid_counter + 1
projected_vid_auth_bytes =
projected_next_vid_counter * metadata_auth_bytes_per_record[volume_identifier]
For LEB:
aead_invocations_this_write =
1 for zero-length or single-tag mode
ceil(payload_bytes / chunk_size) for chunked mode with payload_bytes > 0
leb_auth_bytes_this_write =
fixed_leb_aad_bytes_single for zero-length mode
fixed_leb_aad_bytes_single + payload_bytes for single-tag mode
payload_bytes + aead_invocations_this_write *
fixed_leb_aad_bytes_chunked for chunked mode
projected_next_write_counter = next_write_counter + aead_invocations_this_write
projected_total_auth_bytes = next_total_auth_bytes + leb_auth_bytes_this_write
Then UBI maps those to usage percentages with implementation policy limits:
metadata_invocation_pct = projected_next_counter / metadata_counter_budget
metadata_bytes_pct = projected_metadata_auth_bytes / metadata_total_auth_bytes_budget
metadata_usage_pct = max(metadata_invocation_pct, metadata_bytes_pct)
vid_invocation_pct = projected_next_vid_counter / metadata_counter_budget
vid_bytes_pct = projected_vid_auth_bytes / metadata_total_auth_bytes_budget
vid_usage_pct = max(vid_invocation_pct, vid_bytes_pct)
leb_invocation_pct = projected_next_write_counter / leb_write_budget
leb_bytes_pct = projected_total_auth_bytes / leb_total_auth_bytes_budget
leb_usage_pct = max(leb_invocation_pct, leb_bytes_pct)
Important clarification:
metadata_invocation_pct,vid_invocation_pct, andleb_invocation_pctrefer to AEAD invocations / consumed nonce-counter values,they do not refer directly to the internal AES-128 block-operation count,
authenticated-byte percentages cover the cumulative AAD + plaintext volume processed under the key,
if an implementation wants a conservative AES-block estimate, it can derive it from authenticated bytes and the fixed CCM per-record framing cost.
Policy thresholds:
ROTATE_SOONfires when the projected percentage crosses the configured soft threshold,ROTATE_NOWfires when it crosses the configured hard threshold.
Hidden-anchor maintenance writes use the same LEB accounting as ordinary zero-length writes:
they consume one AEAD invocation under
K_leb[key_version][volume_id],they advance
leb_write_counterby1,they advance
leb_total_auth_bytesbyfixed_leb_aad_bytes_single.
The architecture deliberately does not hardcode one numeric AES-CCM budget in this document. Products can choose different acceptable margins, but the budgeting dimensions and the enforcement points remain the same.
9.8 Counter lifecycle and continuity guarantees
Freshness values such as device_revision and global_sqnum must not be confused with AEAD-usage floors.
A counter continuity problem appears only when the last authenticated carrier of the newest committed value disappears from flash before another authenticated carrier inherits it.
9.8.1 Continuity matrix
Counter family |
Latest committed carrier |
Threat window |
Continuity mechanism |
|---|---|---|---|
secure device header / secure volume header |
dual-bank reserved generation |
reserved rewrite interrupted by crash |
authenticated highest |
secure EC header |
live secure EC headers on data PEBs |
crash around |
accepted best-effort continuity; child binding to authenticated |
secure VID header for current write-active key version |
live secure VID headers plus |
|
save the last VID-domain floor in the secure device header on every reserved metadata rewrite |
secure LEB usage state for |
live secure VID metadata of user mappings plus the hidden anchor |
|
hidden per-volume anchor PEB with zero-length secure LEB record |
The continuity mechanisms are intentionally different because the domains have different scopes:
LEB usage state is per volume and therefore uses a per-volume carrier,
VID usage state is global for one key version and therefore uses the secure device header,
DEV/VOL already live in dual-bank reserved metadata,
EC is accepted as best-effort and does not get an additional anchor.
9.8.2 Threat: unmap/shrink -> erase -> reboot/crash
The per-volume LEB floor can be lost only if all of these become true:
a dirty user PEB still carries the newest committed
{leb_write_counter, leb_total_auth_bytes}for one{key_version, volume_id},no other live user mapping of that volume carries the same or newer floor,
the dirty PEB is physically erased,
and the device then reboots before another authenticated carrier has inherited that floor.
Without another authenticated carrier, init could reopen an older per-volume floor for that {key_version, volume_id}. That is exactly the class of scenario that the hidden anchor solves.
9.8.3 Hidden per-volume anchor PEB
Each secure volume owns one hidden anchor PEB.
The anchor is:
a normal secure data PEB,
identified by
INTERNAL_ANCHOR_LNUM,not part of the user-visible
lnumrange,always encoded as a zero-length secure LEB record,
authenticated and selected by the same
vid_sqnumrules as ordinary mappings.
Its job is to keep one authenticated, commit-visible floor alive for:
{key_version, volume_id} -> leb_write_counter, leb_total_auth_bytes
Lifecycle:
Per-volume LEB floor lifecycle
================================================================================
volume create
|
+--> allocate hidden anchor PEB
| anchor secure LEB payload = zero-length
| anchor VID metadata = {initial next counter after anchor create,
| initial zero-length authenticated bytes}
|
ordinary user write
|
+--> new user secure VID becomes commit-visible
| user VID metadata = newer {next_counter, total_auth_bytes}
| old user PEB = dirty
|
unmap / shrink
|
+--> user PEB may become dirty
| while it still exists on flash, its VID metadata still carries the floor
|
erase of dirty PEB that is the last writable witness
|
+--> rewrite hidden anchor first
| anchor secure LEB payload = zero-length
| anchor total_auth_bytes = old_total_auth_bytes + fixed_leb_aad_bytes_single
| anchor next_counter = old_next_counter + 1
|
+--> only after committed anchor VID:
erase old dirty PEB
Normative rule:
before erasing a dirty PEB that is the last current writable witness of the newest per-volume floor, UBI shall first rewrite the hidden anchor so that the anchor inherits that floor,
SECURE shall keep one emergency free data PEB available for that rewrite whenever possible,
if the free pool is empty, reclaim shall first try to erase a dirty PEB that is not the last current writable witness for any protected per-volume floor, thereby recreating one free data PEB,
if no such safe candidate exists, the protected erase must be deferred or rejected by policy,
init reconstructs the per-volume floor as the maximum authenticated value over live user mappings and the live hidden anchor of that volume.
This solves the cases that motivated the anchor:
brand-new empty secure volumes,
volumes with only one user LEB,
volumes whose user LEBs were all unmapped,
shrink paths that leave only internal state behind.
The anchor is intentionally local and uses the same record types already present in SECURE. It is not rewritten on every user write; it is refreshed when create-time initialization or reclaim-time continuity requires it. No external journal is required.
9.8.4 Threat: volume_remove, including removing all volumes
The secure VID key is intentionally global for one key version:
K_volume_identifier[key_version]
So volume_id does not namespace the future-write counter for the VID domain.
That means volume_remove has a separate risk window:
reclaim can eventually erase every live secure VID record of the removed volumes,
if the device removes all remaining volumes, there may be zero live secure VID records left,
without another authenticated carrier, init could lose the newest committed global VID-domain floor for the current write-active key version.
A per-volume anchor does not solve this global VID-domain case by itself, because removing the volume also removes its hidden anchor.
9.8.5 Saving the last VID-domain floor in the secure device header
SECURE therefore stores the current global VID-domain floor in the secure device header:
device_header.write_active_key_version
device_header.vid_next_counter_floor
The rule is simple:
on every reserved metadata rewrite, UBI snapshots the current in-RAM VID-domain next counter for the authenticated
write_active_key_version,that snapshot is written into the new secure device header before removed volumes are reclaimed,
init later reconstructs the future VID-domain counter as the maximum of:
the authenticated device-header floor,
and the authenticated live VID records that still exist for the current write-active key version.
Lifecycle:
Global VID counter lifecycle
================================================================================
live secure VID writes
|
+--> RAM state:
next_vid_counter[write_active_key_version]
|
reserved metadata rewrite
|
+--> snapshot RAM next_vid_counter into:
| device_header.vid_next_counter_floor
+--> commit new secure device header
+--> then reclaim removed volumes / stale data
|
reboot / init
|
+--> next_vid_counter[write_active_key_version] =
max(device_header.vid_next_counter_floor,
1 + max(live authenticated VID prefix32.counter))
Because the authenticated write_active_key_version is monotonic and never moves backward, one device-header floor slot is sufficient in the current format. When the write-active key version advances to a newer value, SECURE starts the VID-domain floor for that newer key version from its first valid counter value and never returns to the older one for future writes.
This is why the current design chooses the secure device header for global VID continuity:
the state is global, not per volume,
the secure device header already has crash-safe dual-bank semantics,
the solution works even when the device temporarily has zero volumes.
9.8.6 Why this design is chosen
The chosen continuity mechanisms are intentionally minimal:
hidden per-volume anchor PEB for the per-volume LEB floor,
device-header VID floor snapshot for the global VID-domain floor of the current write-active key version.
This design is preferred because it keeps every continuity problem at the narrowest existing carrier:
per-volume state stays with a per-volume secure VID carrier,
global VID state stays in the dual-bank secure device header,
no external journal is introduced,
no new unauthenticated side channel is introduced,
the crash model continues to rely on existing UBI commit-visible objects and
vid_sqnumordering.
9.9 External trusted freshness store contract
Complete anti-rollback requires an external trusted freshness store or an equivalent trust anchor.
The architecture therefore defines two separate interactions with the application:
Init-time freshness check
after UBI selects the authenticated device state,
before UBI enables writes,
UBI calls the freshness-check callback once with the current authenticated descriptor:
device_revision,global_sqnum.
Post-commit freshness synchronization
after a commit-visible mutating operation,
UBI may call a freshness-sync callback so the application can update an external trusted store.
These callbacks remain separate on purpose:
the init-time callback returns an accept / reject verdict before writes are enabled,
the post-commit callback persists already-authenticated freshness after flash has already been committed,
failure of the second callback must not imply flash rollback,
runtime out-of-band media modification is later surfaced as authentication failure on access, not as a second freshness-verdict callback.
The sync cadence is controlled by freshness_sync_delta:
freshness_sync_delta == 0
UBI calls the sync callback after every commit-visible mutating operation.freshness_sync_delta > 0
UBI maintains an internal counter of commit-visible mutations since the last successful sync and calls the sync callback when that counter reaches the configured delta.
10. Initialization and recovery
10.1 Initialization overview
1. Scan reserved PEBs
2. Authenticate secure device header candidates
3. Select highest authenticated device_revision
4. Authenticate secure volume headers tied to that device revision
5. Scan all data PEBs
6. Authenticate secure EC headers
7. Classify secure VID area
8. Build:
- free / dirty / bad pools
- live user EBA mappings
- live hidden-anchor mappings
- global_sqnum
- per-key usage state, including device-header VID floor
- per-key object refcounts
9. Build exported freshness descriptor
10. Call init freshness policy
11. Emit policy and lifecycle events
10.2 Reserved-area selection
Reserved PEBs are mirrored copies. Initialization must:
enumerate all reserved PEBs configured by
CONFIG_UBI_DEV_HDR_NR_OF_RES_PEBS,authenticate secure device header candidates,
reject unauthenticated candidates,
select the highest authenticated
device_revision,authenticate the secure volume headers that belong to the selected generation,
extract from the authenticated secure device header:
write_active_key_versionvid_next_counter_floor.
10.3 Data-PEB classification
For every data PEB:
authenticate the secure EC header,
inspect the secure VID region,
if needed, inspect the beginning of the secure LEB region using the flash device’s erased value.
Classification rules:
Condition |
Classification |
|---|---|
secure EC authenticates, secure VID area erased, secure LEB-prefix area erased |
free data PEB |
secure EC authenticates, secure VID area erased, secure LEB-prefix area not erased |
uncommitted / dirty data PEB |
secure EC authenticates, secure VID authenticates |
mapped or stale data PEB, resolved by |
secure EC cannot be authenticated |
bad / unreadable according to policy |
This rule is critical.
Without the extra check on the secure LEB start, a DATA -> VID interrupted write could be misclassified as free.
If an authenticated secure VID carries INTERNAL_ANCHOR_LNUM, initialization tracks it as the hidden anchor of that volume_id, not as a user-visible EBA entry. Its vid_sqnum still participates in live/stale selection for the anchor mapping, and its secure VID metadata still participates in per-volume LEB usage recovery.
10.4 Why the mapping-visible write order is DATA -> VID
A free data PEB already carries a valid secure EC header before it is selected for a new mapping. Therefore the commit-visible order for one mapping update is:
DATA -> VID
Across the longer PEB lifecycle, reclaim still follows:
ERASE -> EC -> free-pool -> DATA -> VID
Reason:
EC must already exist so the data and VID AAD can bind to authenticated erase-count state,
data are written before VID so that a half-written newer payload is not made live by an earlier VID commit,
VID is written last because VID is the commit-visible mapping record.
This is also why initialization must distinguish:
VID erased + data erased-> freeVID erased + data present-> interrupted, must not be free
10.5 Recovery flow diagram
Data PEB init
================================================================================
read secure EC
|
+-- auth fail ------------------------------> bad / unreadable / policy action
|
+-- auth ok
|
+-- VID area erased?
|
+-- yes --> is secure LEB-prefix area erased?
| |
| +-- yes --> free
| |
| +-- no --> dirty (interrupted DATA->VID)
|
+-- no --> authenticate secure VID
|
+-- auth fail --> security event / policy action
|
+-- auth ok
|
+-- use vid_sqnum for live/stale selection
+-- update global_sqnum
+-- update VID / LEB usage recovery state
+-- update key refcounts
11. Secure write paths
11.1 Central mutation gate
SECURE mode assumes that plain UBI already funnels all commit-visible mutating operations through one central mutation gate.
In SECURE mode, that gate is where UBI:
checks that the write-active key version is available,
checks projected key-usage budgets,
checks that fresh cryptographic randomness is available,
executes the commit-visible mutation,
updates the exported freshness descriptor,
schedules or performs post-commit freshness synchronization,
emits lifecycle or security events,
optionally transitions to read-only mode when strict policy requires that.
Read-only operations do not pass through this gate.
11.2 Reserved metadata update
When reserved metadata changes, UBI writes a new authenticated reserved generation.
That includes:
creating or deleting a volume,
resizing a volume,
changing the write-active key version,
any operation that changes the device header.
A reserved metadata update must:
increment
device_header.revision,snapshot the current VID-domain runtime floor:
vid_next_counter_floor = next_vid_counter[write_active_key_version]
write the new secure device header carrying:
write_active_key_versionvid_next_counter_floor
write the new secure volume headers,
switch the selected reserved generation only after the new generation is complete,
update the exported freshness descriptor,
schedule or perform a post-commit freshness sync according to policy.
This ordering is especially important for volume_remove: the secure device header snapshot must commit before reclaim starts erasing the removed volume’s data PEBs. That remains true even when the operation removes the last remaining volume and leaves the device in the zero-volume state.
11.3 Key rotation and reserved metadata
Changing the write-active key version must itself trigger a reserved metadata rewrite.
This is important because otherwise secure device and secure volume objects could remain forever on an older key version if the volume layout never changes.
So key rotation is not just “future writes use a new key”. It also means:
device_revisionadvances,reserved metadata is immediately rewritten under the new key version,
the secure device header updates
write_active_key_version,vid_next_counter_flooris reinitialized to the newer write-active key version’s first valid VID counter,older key versions may remain readable until retirement, but they never become write-active again.
11.5 Data write path
For a secure LEB write:
choose a free data PEB while preserving the emergency-free-PEB reserve; if the write would consume the last free data PEB, reclaim shall first try to recreate the reserve by erasing a dirty PEB that is not a protected last current writable witness,
keep the chosen PEB’s existing secure EC header,
choose the secure LEB encoding:
if
payload_bytes == 0, build the zero-length base recordprefix32 || tag16,else if single-tag mode is valid for the selected geometry, build the single-tag secure LEB record,
else if chunked mode is enabled, build the chunked secure LEB record,
else reject the write,
compute
aead_invocations_this_write:1for zero-length or single-tag mode,ceil(payload_bytes / chunk_size)for chunked mode withpayload_bytes > 0,
compute
leb_auth_bytes_this_write:fixed_leb_aad_bytes_singlefor zero-length mode,fixed_leb_aad_bytes_single + payload_bytesfor single-tag mode,payload_bytes + aead_invocations_this_write * fixed_leb_aad_bytes_chunkedfor chunked mode,
check projected VID-domain and LEB-domain usage before any flash mutation:
if the projected usage crosses
ROTATE_SOON, emitKEY_ROTATE_SOON,if the projected usage crosses
ROTATE_NOW, emitKEY_ROTATE_NOWand reject the write before any flash mutation,
reserve
counter_base = next_leb_write_counter[{key_version, volume_id}],verify that the reserved AEAD counter range fits in the 48-bit nonce counter field:
if
counter_base + aead_invocations_this_write - 1 > 0xFFFFFFFFFFFF, emitKEY_ROTATE_NOWand reject the write before any flash mutation,
write the secure LEB record using
counter_base,build the secure VID header using:
new
vid_sqnum,new
leb_write_counter = counter_base + aead_invocations_this_write,new
leb_total_auth_bytes = old_total_auth_bytes + leb_auth_bytes_this_write,
write the secure VID header,
update the in-RAM EBA mapping so the new PEB becomes live,
mark the old PEB dirty,
update the exported freshness descriptor,
schedule or perform a post-commit freshness sync according to policy.
At the moment of step 11, the new write becomes commit-visible.
For zero-length writes, the empty secure LEB record is still written before VID so that the normal DATA -> VID classification and nonce/counter semantics remain unchanged.
Ordinary user writes do not need to rewrite the hidden anchor immediately. The newest per-volume floor may live in the newest user secure VID until the reclaim path detects that the last such witness would otherwise disappear.
11.7 Unmap and shrink semantics inherited from plain UBI
SECURE mode inherits the persistence semantics of the plain core:
ubi_leb_unmap()is an in-memory transition frommappedtodirty; it does not write a new on-flash tombstone or replacement mapping,therefore, until the dirty PEB is physically erased, the old authenticated secure VID and secure LEB record still exist on flash,
after a reboot before that erase, initialization may rediscover that surviving authenticated VID and reconstruct the old mapping again,
ubi_volume_resize()with shrink is different: the smallerleb_countis committed in reserved metadata first, so tail PEBs whose authenticatedlnumis now out of range are recovered as dirty after reboot even if they were not erased yet.
This distinction matters for secure lifecycle reasoning: logical unmap / shrink and physical disappearance at erase time are not the same event.
The hidden anchor is what turns that distinction into a strict continuity mechanism for the per-volume LEB floor: the newest user-carried floor may disappear during reclaim, but the anchor can inherit it first.
12. Secure read paths
12.1 Metadata reads
Secure metadata reads are simple:
authenticate,
then decrypt,
then return the plaintext payload.
12.2 Single-tag LEB reads
In single-tag mode, no plaintext may be returned before the entire recorded payload has been authenticated.
So a partial logical read means:
read the complete secure payload for that LEB,
authenticate the complete secure payload,
decrypt the complete secure payload,
return only the requested slice.
This is why single-tag mode is simple but RAM- and latency-heavy. Implementations may satisfy that RAM need with one reusable eraseblock-sized SECURE cache / staging buffer instead of ad-hoc heap allocation.
If authenticated data_size == 0, the read path authenticates the zero-length base record and returns length 0.
12.3 Chunked LEB reads
In chunked mode, UBI authenticates only the chunks that cover the requested byte range.
That allows:
lower RAM requirements,
lower read latency for small slices,
no need to authenticate unrelated chunks.
The cost is:
more tags,
more flash overhead,
more AEAD operations,
more implementation complexity.
If authenticated data_size == 0, chunked mode uses the same zero-length base record as single-tag mode.
13. Key lifecycle, inventory, and retirement
13.1 Allowlist
The application supplies an explicit allowlist of acceptable key versions.
The on-flash key_version field is 8-bit. The runtime API therefore models the allowlist as:
const uint8_t *allowed_key_versionssize_t allowed_key_versions_len
Normative rules:
each element is one allowed
key_versionvalue,allowed_key_versions_lenis the number of valid entries in that array,duplicate entries are invalid input,
an authenticated on-flash key version that is not present in the allowlist is a policy error,
the implementation may bound the maximum number of allowlist entries with Kconfig, but the wire value of one key version remains 8-bit.
13.2 Retirement means “no object anywhere still needs the key”
A key version is retirable only when no authenticated object on flash still uses it.
That includes:
reserved secure device headers,
reserved secure volume headers,
secure EC headers on free, dirty, and live data PEBs,
secure VID headers,
secure LEB records.
This point matters.
Changing the write-active key version does not instantly retire the old key. Old EC headers on untouched PEBs, stale reserved generations, and stale dirty data can keep the old key alive until those objects are reclaimed.
13.3 Refcount lifecycle
UBI maintains a runtime object refcount per key version:
key_object_refcount[key_version]
The count is over authenticated on-flash objects, not over logical volumes and not over live mappings only.
That means the counted objects are:
secure device headers,
secure volume headers,
secure EC headers,
secure VID headers,
secure LEB records.
A key becomes retirable only when all of those objects have disappeared from flash.
Step 1: initialization
During initialization, UBI authenticates present secure objects and increments key_object_refcount[key_version] once per authenticated object that still exists on flash and can matter for future initialization or recovery.
Examples:
free data PEB
counts its secure EC header,mapped data PEB
counts secure EC + secure VID + secure LEB,dirty data PEB
still counts the stale secure objects until erase removes them,reserved metadata
counts every authenticated secure device / secure volume object that still exists in a reserved generation and can still be encountered during recovery.
Step 2: what is counted for one data PEB
free PEB
[EC:v2]
refcount[v2] += 1
live mapped PEB
[EC:v2] [VID:v4] [LEB:v4]
refcount[v2] += 1
refcount[v4] += 2
dirty old PEB after overwrite elsewhere
[EC:v2] [VID:v4] [LEB:v4]
still counted exactly the same
A practical consequence is important here:
taking a free PEB for a new write does not decrement the old EC object’s refcount,
the old EC object is still physically present and still matters until that PEB is erased or rewritten.
Step 3: runtime overwrite
When a new mapping supersedes an old one:
the new secure VID and secure LEB become countable immediately,
the old PEB remains physically present and therefore still counted,
nothing is decremented yet.
Step 4: runtime erase
When that old PEB is erased:
its old secure objects disappear,
their counts are decremented,
the freshly written secure EC header for the reclaimed PEB is counted under the current EC key version.
For one common transition:
before erase:
[EC:v2] [VID:v4] [LEB:v4]
after erase + fresh EC write under v5:
old objects removed -> refcount[v2] -= 1, refcount[v4] -= 2
new free-PEB EC -> refcount[v5] += 1
Step 5: retirement edge
When:
key_object_refcount[v] == 0
UBI emits:
UBI_CRYPTO_EVENT_KEY_RETIRABLE
This is a lifecycle / informational event, not a tamper event.
13.4 Why runtime retirement detection is possible
UBI can detect retirement during runtime because it owns the object lifecycle:
it knows when new secure objects are committed,
it knows when old mappings become dirty,
it knows when erase physically removes old objects,
it knows when reserved generations are replaced.
So retirement is not only an init-time scan result. It can also be discovered later as garbage collection and maintenance progress.
13.5 Key-version reuse
key_version is 8-bit on flash.
Normal rule:
no wrap-around reuse during the lifetime of one formatted device,
the authenticated
write_active_key_versionstored in the secure device header may move only to a newer value and must never return to an older one.
SECURE does not define normal-operation reuse of a previously used key_version.
If a future external provisioning or full-device scrub flow ever exists outside this specification, that flow would need to define its own reset semantics explicitly. Until then, a previously used key_version is treated as unavailable for reuse.
13.6 Ownership of key destruction and zeroization
SECURE mode does not expose a raw-root-key API. That keeps raw IKM[v] buffers out of the normal UBI interface.
That means the ownership split is:
UBI detects when a key version becomes retirable,
the application decides when to destroy, purge, or otherwise retire the corresponding PSA-managed key material,
zeroization of root material is the responsibility of PSA, the secure element, or the application provisioning system.
UBI should therefore report retirement opportunities, but it should not claim ownership over the lifetime of raw root-key bytes that it never receives.
Implementation hygiene requirements:
any UBI-owned plaintext scratch buffer, aligned staging buffer, or software-derived child key material that exists outside PSA must be zeroized before release or reuse,
if derived child keys are cached in RAM, cache lifetime must be bounded to one attach session and entries must be invalidated on device detach, init failure, or when the corresponding key version is no longer usable,
platforms that can keep child keys as non-exportable PSA objects should prefer that flow.
13.7 Lazy rekey versus forced rekey after compromise
If the application considers one root key version compromised, two broad strategies exist:
lazy rekey
future writes use a new key version and old objects age out naturally,forced rekey
the application proactively rewrites all live objects under a new key version.
UBI provides the mechanisms needed for either strategy:
allowlist,
write-active key version,
usage budgets,
retirement detection,
crash-safe rewrite paths.
A critical operational point is that rewriting all live mappings under a new key version is not the same thing as retiring the old key immediately. Old EC headers on free PEBs, stale reserved generations, and dirty data pending erase can still keep the old key version operationally required.
So if a key version is considered compromised and the product wants immediate retirement semantics, the application may need more than ordinary live-data rewrite:
accelerated reclaim,
explicit scrub of stale media state,
stricter allowlist changes only after the old key refcount reaches zero.
Lazy rewrite alone is not a sufficient compromise-response guarantee.
The policy choice between lazy and forced rekey remains with the application.
13.8 Operational retirement levels
For operator clarity, SECURE should distinguish four states:
soft rotation
New writes use a newer key version.live rewrite completed
All currently live mappings have been rewritten under the newer key version.media scrub completed
Stale reserved generations, dirty data, and free-PEB EC objects that still reference the old key version have been eliminated.key retired
The old key version has on-flash refcount zero and can be removed from the allowlist.
KEY_RETIRABLE corresponds to the transition into state 4.
flowchart LR
A["old key is still write-active"] --> B["soft rotation\nnew writes use newer key"]
B --> C["live rewrite completed"]
C --> D["media scrub completed"]
D --> E["KEY_RETIRABLE\nrefcount == 0"]
E --> F["application may remove key from allowlist\nand purge PSA key material"]
14. Events, policy, and read-only transitions
14.1 Event classes
Recommended event types:
AUTH_FAILUREFORMAT_VIOLATIONKEY_VERSION_NOT_ALLOWLISTEDKEY_VERSION_UNAVAILABLEROLLBACK_POLICY_MISMATCHFRESHNESS_SYNC_FAILURERNG_FAILUREKEY_ROTATE_SOONKEY_ROTATE_NOWKEY_RETIRABLE
14.2 Which cases force write shutdown or read-only mode
The architecture should state this explicitly.
Condition |
Minimum required action |
|---|---|
RNG cannot provide fresh salt for a secure write |
reject write; if strict policy says so, enter read-only |
48-bit LEB nonce counter range would overflow on the requested write |
emit |
no write-active key material is available |
reject write; optionally enter read-only |
projected usage crosses |
reject write; optionally enter read-only |
no authenticated reserved generation can be selected |
init fails |
SECURE attach policy requires an object that cannot be authenticated |
init fails or read-only, per policy |
authenticated init freshness callback rejects the selected descriptor |
init fails or read-only, per policy |
post-commit freshness sync callback fails |
emit |
event callback explicitly requests escalation |
enter read-only for subsequent writes |
14.3 Events that imply tamper suspicion
These should be surfaced to the application as security-relevant events:
authentication failure,
unexpected wrapper version,
impossible layout / offset / length combination,
key version not allowlisted for an on-flash object,
key version required by an object but unavailable from the application.
This is stronger than merely “mark unreadable”. It allows the application to treat the situation as tamper-suspected if that matches product policy.
14.4 Read-only semantics
When SECURE UBI enters read-only mode at runtime, that state is sticky for the lifetime of the current attach session.
That means:
no further commit-visible writes are accepted,
authenticated reads and inventory operations may continue,
UBI does not silently leave read-only mode on its own,
leaving read-only requires detach / reattach, reboot, or explicit reinitialization.
Read-only mode is not itself persisted as an on-flash flag. After reboot or reattach, SECURE re-evaluates the medium again. Depending on the cause and on policy, the next attach may:
succeed normally,
fail,
or enter read-only again.
14.5 Who decides whether UBI keeps running
The design intentionally separates three roles:
Kconfig defines mandatory automatic behavior for classes such as RNG failure, policy failure, or freshness-sync failure,
direct API return codes decide the fate of the current operation,
event callbacks are notifications plus an optional escalation hook for future writes.
So the event callback may request:
continue,
or enter read-only for subsequent writes,
but it must not be able to override a mandatory rejection that the architecture already requires for the current operation.
15. Kconfig surface
Recommended Kconfig knobs for SECURE mode:
Kconfig symbol |
Meaning |
|---|---|
|
Enables SECURE mode support. |
|
Maximum number of distinct key versions that one attach session may inventory and track internally. The on-flash |
|
Maximum number of |
|
Soft threshold for usage-budget warnings. Crossing it emits |
|
Hard threshold for usage-budget exhaustion. Crossing it emits |
|
Maximum allowed metadata AEAD invocation count per |
|
Maximum allowed authenticated metadata bytes per |
|
Maximum allowed AEAD invocation count per |
|
Maximum allowed cumulative authenticated LEB bytes per |
|
Enables chunked secure LEB layout for geometries where single-tag mode is invalid or undesirable. |
|
Chunk size used by chunked LEB layout. It drives chunk count, tag overhead, RAM profile, and alignment requirements. |
|
Enables one reusable eraseblock-sized SECURE staging / cache buffer. This avoids mandatory heap allocation in paths that need full-PEB staging. |
|
Allocates the SECURE eraseblock-sized cache statically. This is the expected choice for a static-memory model. In a dynamic-memory model it may stay optional, especially when chunked mode avoids full-PEB staging. |
|
Number of commit-visible mutations between post-commit freshness-sync callbacks. |
|
Forces read-only or write shutdown when fresh secure-write salt cannot be generated. |
|
Forces read-only or init failure when freshness policy rejects the authenticated flash state. |
|
Forces read-only after post-commit freshness synchronization failures if product policy requires that. |
Existing plain UBI geometry knobs remain authoritative for:
reserved PEB count,
maximum volume count,
erase-block size constraints.
The current repository already constrains:
reserved PEB count to 2..4,
maximum volume count to 1..128.
The secure format must respect those existing bounds.
The hidden per-volume anchor and the secure-device-header VID floor are mandatory parts of the current format. They are therefore intentionally not optional Kconfig knobs.
15.1 Reserved-generation fit check
For one secure reserved generation:
reserved_generation_bytes = 96 + 96 * volume_count
where volume_count means the number of secure volume headers present in that generation.
SECURE is valid only if:
reserved_generation_bytes <= peb_size
This shall be enforced:
at build time for fixed geometries,
or at initialization time for runtime-discovered geometries.
Example maximum volume_count values for one reserved generation:
Reserved PEB size |
Max |
|---|---|
4 KiB |
41 |
8 KiB |
84 |
16 KiB |
169 |
If the configured or discovered geometry cannot satisfy this bound, SECURE mode must be rejected.
15.2 Single-tag CCM geometry check
For single-tag secure LEB mode, the maximum logical payload on one data PEB is:
secure_leb_payload_bytes_single = peb_size - 208
Because AES-CCM with nonce_len = 13 implies q = 2, single-tag SECURE requires:
peb_size - 208 < 65536
If this bound is violated for the selected geometry, single-tag mode shall not be used. The implementation must either require chunked mode for that geometry or reject SECURE mode.
15.3 Chunked-mode geometry check
If chunked secure LEB mode is enabled, the implementation must verify that the selected geometry still leaves space for user payload.
For a data PEB with physical size peb_size, chunk size C, tag size T = 16, and payload S, the secure layout must satisfy:
64 + 96 + 32 + S + T * ceil(S / C) <= peb_size
Equivalently:
192 + S + 16 * ceil(S / C) <= peb_size
If this cannot be satisfied for the configured geometry, SECURE chunked mode must be rejected at build time or at initialization time.
16. API shape (summary)
The detailed illustrative API is in Appendix A, but the architectural expectations are:
SECURE mode is PSA-only.
There is one public initialization entry point for both plain and secure mode:
int ubi_device_init(const struct ubi_mtd *mtd, const struct ubi_crypto_config *crypto_cfg, struct ubi_device **ubi);
crypto_cfg == NULL→ attach or format as plain,crypto_cfg != NULL→ attach or format as secure. The public headerubi.hforward-declaresstruct ubi_crypto_configso that plain callers do not need to includeubi_crypto.h.
The application provides (via
ubi_crypto_config):a callback that returns the PSA key identifier for one key version,
allowlist,
an optional request to advance the write-active key version,
init-time freshness-check callback,
optional post-commit freshness-sync callback,
event callback.
UBI exports:
the authenticated
write_active_key_versionfrom the secure device header,the authenticated freshness descriptor,
lifecycle events such as
KEY_RETIRABLE,security events such as
AUTH_FAILURE,optional event-driven escalation to read-only mode for future writes.
There is no raw
IKMbuffer callback in the normal API surface.If the application provides no external trusted freshness store, SECURE mode may still provide authenticated encryption and authenticated freshness signals, but it does not provide complete anti-rollback protection.
When
CONFIG_UBI_CRYPTOis not enabled and the caller passescrypto_cfg != NULL, the library returns a stable, documented error (-ENOTSUP).
The summary below omits a dedicated getter for the authenticated on-flash write_active_key_version, but the architecture expects that state to be available to the application.
Attach-time interaction:
sequenceDiagram
participant APP as Application
participant UBI as UBI SECURE
participant FLASH as Flash
APP->>UBI: policy {allowlist, optional requested_write_key_version}
UBI->>FLASH: authenticate reserved metadata
UBI->>FLASH: read write_active_key_version from secure device header
UBI->>APP: get_key_id(write_active_key_version)
UBI->>FLASH: scan data PEBs and select live mappings
UBI-->>APP: check_freshness({device_revision, global_sqnum})
APP-->>UBI: accept / reject
UBI-->>APP: events if policy or auth failures occur
Runtime interaction:
sequenceDiagram
participant APP as Application
participant UBI as UBI SECURE
participant FLASH as Flash
APP->>UBI: current policy {allowlist, optional requested_write_key_version}
UBI->>APP: get_key_id(write_active_key_version)
UBI->>FLASH: write secure LEB
UBI->>FLASH: write secure VID (commit-visible)
UBI-->>APP: KEY_ROTATE_SOON / KEY_ROTATE_NOW if budgets cross thresholds
UBI-->>APP: sync_freshness({device_revision, global_sqnum})
APP-->>UBI: ok / error
UBI-->>APP: FRESHNESS_SYNC_FAILURE on error
UBI-->>APP: KEY_RETIRABLE(key_version = old) when refcount reaches 0
17. Cost model
17.1 Reserved-area overhead
Compared with plain UBI:
secure device header:
32 B -> 96 B(+64 B)secure volume header:
48 B -> 96 B(+48 B)
One complete secure reserved generation therefore occupies:
reserved_generation_bytes = 96 + 96 * volume_count
Examples:
Volume count |
Secure reserved generation bytes |
|---|---|
1 |
192 B |
8 |
864 B |
32 |
3168 B |
128 |
12384 B |
If an implementation stages one entire reserved generation in RAM before writing it, the same formula is a conservative upper bound for that staging buffer. A streaming implementation may use less.
This is not only a cost figure. It is also a hard format constraint: one secure reserved generation must fit inside one reserved PEB.
17.2 Data-area overhead (single-tag mode)
Compared with plain UBI:
plain metadata per data PEB: 48 B
secure metadata per data PEB: 208 B
additional cost: 160 B per data PEB
17.3 Usable payload by erase-block size
Base single-tag mode:
Erase-block size |
Plain usable payload |
Secure usable payload |
Lost bytes |
Loss vs raw block |
Loss vs plain usable |
|---|---|---|---|---|---|
4 KiB |
4048 B |
3888 B |
160 B |
3.91% |
3.95% |
8 KiB |
8144 B |
7984 B |
160 B |
1.95% |
1.97% |
16 KiB |
16336 B |
16176 B |
160 B |
0.98% |
0.98% |
The two percentage columns use different denominators:
Loss vs raw block = lost_bytes / erase_block_size
Loss vs plain usable = lost_bytes / plain_usable_payload
Loss vs plain usable is therefore slightly larger, because plain_usable_payload is already smaller than the raw block by the plain UBI metadata cost.
Chunked mode adds:
16 B * (number_of_chunks - 1)
extra tag overhead beyond the base single-tag layout.
17.5 Why this cost is accepted
The architecture accepts this cost because it avoids a heavier design:
no separate journal area,
no extra per-volume state packed into reserved metadata,
no unauthenticated side channel for counters,
no dependence on hidden recovery heuristics after
unmap/shrink -> erase -> reboot.
The hidden anchor plus the one-PEB emergency reserve are therefore a deliberate space-for-simplicity trade-off.
For 64 KiB data PEBs, the base single-tag secure payload is still below the CCM q = 2 limit. For larger data PEBs, single-tag mode ceases to be valid and chunked mode becomes mandatory if SECURE is supported.
The exact maximum payload in chunked mode is the greatest S that satisfies:
192 + S + 16 * ceil(S / chunk_size) <= peb_size
17.6 RAM and latency
Let:
S = authenticated payload_bytes for one LEB
C = CONFIG_UBI_CRYPTO_LEB_CHUNK_SIZE
N = ceil(S / C)
T = 16
Single-tag mode
One secure LEB record occupies:
LEB_record_single = 32 + S + T
A committed data rewrite writes at least:
(32 + S + 16) + 96 = S + 144 bytes
of new secure payload and secure VID metadata to the target data PEB, not counting later reclaim of the old PEB.
For reads, the critical property is:
a partial logical read still requires authenticating the entire secure record,
so the flash read cost is the full secure LEB record:
S + 48bytes.
RAM implication:
the relevant data-length bound is
S, not the physical maximum LEB capacity,but single-tag mode still has an O(S) authentication/latency profile for reads,
whether the extra RAM is caller-owned, UBI scratch, or crypto-backend scratch depends on the implementation.
Chunked mode
One chunked secure LEB record occupies:
LEB_record_chunked = 32 + S + T * N
The extra overhead beyond single-tag mode is:
chunked_extra = 16 * (N - 1)
For a logical read of len bytes at offset off, the number of chunks that must be authenticated is:
chunks_touched = ceil((off mod C + len) / C)
So chunked mode changes the read profile from “authenticate the whole S” to “authenticate only the touched chunks”.
A practical upper bound for UBI-owned per-read chunk scratch is:
chunk_scratch <= C + 16
plus whatever opaque scratch the crypto backend itself needs.
This is why chunked mode trades:
higher flash overhead,
more AEAD calls,
more complex geometry checks,
for:
lower per-read RAM,
lower partial-read latency.
17.7 Alignment constraints for single-tag and chunked mode
Single-tag mode rules:
authenticated record length is exactly
32 + data_size + 16,if flash write alignment requires a longer terminal write, extra bytes may appear only after
tag16,those tail bytes must be written as the flash erased value,
tail bytes are outside ciphertext and AAD,
read-side record length is determined only from authenticated
vid_hdr.data_size.
Chunked mode minimum rules:
CONFIG_UBI_CRYPTO_LEB_CHUNK_SIZEshould be a multiple of the target flash write alignment.
If the implementation writes each (chunk ciphertext || tag) tuple directly to flash, it must also ensure that each such write obeys the target flash alignment and write-size rules.
If that is not true for the selected geometry, the implementation must do one of two things:
use an aligned staging buffer and split or pad writes as needed, or
reject the configuration.
This is a write-path constraint. Read alignment is usually less restrictive and does not change the on-flash format.
18. References
NIST SP 800-38C for AES-CCM.
RFC 5869 for HKDF.
Current UBI architecture guide for plain UBI geometry,
sqnum, and data-PEB classification assumptions.Current UBI codebase for plain-core commit order, reserved-generation revision behavior, and durable
volume_idassumptions.
Appendix A. Illustrative API surface with Doxygen
A.1 Runtime backend selection
The library uses one public initialization entry point. The crypto_cfg pointer selects the backend at runtime:
/**
* @brief Initialize a UBI device on the given flash partition.
*
* Scans the flash partition, formats it on first use, and builds the
* in-memory PEB and volume tables. On success, *ubi points to the
* allocated device handle; on failure, *ubi is set to NULL.
*
* Backend selection:
* - @p crypto_cfg == NULL → attach or format as plain.
* - @p crypto_cfg != NULL → attach or format as secure.
*
* If the detected on-flash format does not match the requested mode,
* initialization fails with a mode-mismatch error. Silent fallback and
* automatic reformat are forbidden.
*
* Only one active handle per flash partition is allowed.
*
* @param[in] mtd Flash partition descriptor (caller retains ownership).
* @param[in] crypto_cfg SECURE configuration, or NULL for plain mode.
* The caller retains ownership; UBI copies what it needs.
* @param[out] ubi Pointer to receive the UBI device handle (NULL on failure).
*
* @retval 0 Success.
* @retval -EINVAL NULL pointer or invalid geometry.
* @retval -ENOTSUP crypto_cfg != NULL but CONFIG_UBI_CRYPTO is disabled.
* @retval -EBUSY A handle for this partition is already active.
* @retval -EILSEQ Mode mismatch (plain media vs secure request, or vice versa).
* @retval -ENOMEM Allocation failure.
* @retval -EIO Unrecoverable flash I/O error.
*/
int ubi_device_init(const struct ubi_mtd *mtd,
const struct ubi_crypto_config *crypto_cfg,
struct ubi_device **ubi);
The ubi.h public header forward-declares struct ubi_crypto_config without including ubi_crypto.h. Plain callers never see PSA types.
A.2 Callbacks, types, and configuration
These callbacks remain separate on purpose:
init-time freshness validation returns an accept/reject verdict before writes are enabled,
post-commit freshness synchronization persists already-committed freshness and must not imply flash rollback,
event callbacks are notification and optional escalation hooks for future writes, not a replacement for deterministic write-path return codes.
The authenticated current write_active_key_version lives on flash in the secure device header. The illustrative API below therefore models the application field as an optional forward-rotation request, not as the source of truth for the currently active version.
ubi_crypto_sync_freshness_cb_t intentionally returns only success / failure, not ubi_crypto_rollback_verdict. The reason is architectural:
rollback / replay acceptance is decided at attach, when UBI selects one authenticated flash state,
post-commit freshness synchronization happens after UBI has already committed a new state,
out-of-band media modification during runtime is surfaced later as authentication failure on access, not as a second freshness-verdict callback.
/**
* @brief Authenticated freshness descriptor exported by UBI SECURE.
*
* The pair intentionally exposes two dimensions of freshness:
* - device_revision for reserved metadata
* - global_sqnum for live data mappings
*
* The application defines the acceptance policy for this pair.
*/
struct ubi_crypto_freshness {
/** Authenticated reserved-metadata revision selected at attach time. */
uint64_t device_revision;
/** Highest authenticated data-mapping sequence number selected at attach time. */
uint64_t global_sqnum;
};
/**
* @brief Security and lifecycle event types emitted by UBI SECURE.
*/
enum ubi_crypto_event_type {
/** Authentication of a secure record failed. */
UBI_CRYPTO_EVENT_AUTH_FAILURE,
/** The on-flash secure format violated a structural rule. */
UBI_CRYPTO_EVENT_FORMAT_VIOLATION,
/** An authenticated on-flash key version is outside the allowlist. */
UBI_CRYPTO_EVENT_KEY_VERSION_NOT_ALLOWLISTED,
/** An authenticated on-flash key version is required but not provisioned. */
UBI_CRYPTO_EVENT_KEY_VERSION_UNAVAILABLE,
/** The authenticated freshness descriptor was rejected by product policy. */
UBI_CRYPTO_EVENT_ROLLBACK_POLICY_MISMATCH,
/** Post-commit freshness synchronization failed. */
UBI_CRYPTO_EVENT_FRESHNESS_SYNC_FAILURE,
/** Fresh randomness required for a secure write was unavailable. */
UBI_CRYPTO_EVENT_RNG_FAILURE,
/** A projected usage budget crossed the soft rotation threshold. */
UBI_CRYPTO_EVENT_KEY_ROTATE_SOON,
/** A projected usage budget crossed the hard rotation threshold. */
UBI_CRYPTO_EVENT_KEY_ROTATE_NOW,
/** No authenticated on-flash object still references this key version. */
UBI_CRYPTO_EVENT_KEY_RETIRABLE,
};
/**
* @brief One security or lifecycle event emitted by UBI SECURE.
*
* The event uses a tagged union so that each event type carries only the
* fields relevant to it. The discriminator is @c type.
*
* @note KEY_RETIRABLE is informational. It indicates that no authenticated
* on-flash object still references the given key version. The application may
* then remove that key version from the allowlist and purge the corresponding
* PSA-managed key material.
*/
struct ubi_crypto_event {
/** Event discriminator. */
enum ubi_crypto_event_type type;
/** Authenticated freshness descriptor at the time of the event. */
struct ubi_crypto_freshness freshness;
/** Per-event-type payload. */
union {
/** Payload for AUTH_FAILURE, FORMAT_VIOLATION. */
struct {
uint32_t peb_index; /**< Physical eraseblock where the failure was detected. */
uint8_t domain; /**< Secure record domain (EC, VID, LEB, DEV, VOL). */
} auth;
/** Payload for KEY_VERSION_NOT_ALLOWLISTED, KEY_VERSION_UNAVAILABLE, KEY_RETIRABLE. */
struct {
uint8_t key_version; /**< Key version relevant to the event. */
} key;
/** Payload for KEY_ROTATE_SOON, KEY_ROTATE_NOW. */
struct {
uint8_t key_version; /**< Key version whose budget is exhausted. */
uint32_t volume_id; /**< Volume ID if LEB budget, or 0 for metadata/VID. */
uint8_t usage_pct; /**< Projected usage percentage (0..100). */
} rotation;
/** Payload for FRESHNESS_SYNC_FAILURE. */
struct {
int sync_errno; /**< errno returned by the sync callback. */
} sync;
/** Payload for RNG_FAILURE. */
struct {
int rng_errno; /**< errno returned by the RNG subsystem. */
} rng;
/** Payload for ROLLBACK_POLICY_MISMATCH (informational, no extra fields). */
struct {
uint8_t _reserved;
} rollback;
};
};
/**
* @brief Rollback-policy verdict supplied by the application.
*
* The application receives authenticated freshness values exported by UBI and
* decides whether they are acceptable for the product's trust model.
*/
enum ubi_crypto_rollback_verdict {
/** Authenticated flash state is acceptable. */
UBI_CRYPTO_ROLLBACK_ACCEPT = 0,
/** Authenticated flash state must be rejected. */
UBI_CRYPTO_ROLLBACK_REJECT = 1,
};
/**
* @brief Event-handling verdict supplied by the application.
*
* This verdict applies only to future writes. It must not override a mandatory
* rejection already required by the architecture for the current operation.
*/
enum ubi_crypto_event_verdict {
/** Keep operating normally after the callback returns. */
UBI_CRYPTO_EVENT_CONTINUE = 0,
/** Enter read-only mode for subsequent writes. */
UBI_CRYPTO_EVENT_ENTER_READ_ONLY = 1,
};
/**
* @brief Per-device SECURE policy configuration.
*/
struct ubi_crypto_policy {
/** Optional request to advance the on-flash write-active key version. 0 means no change requested. */
uint8_t requested_write_key_version;
/** Explicit allowlist of acceptable key versions. */
const uint8_t *allowed_key_versions;
/** Number of valid entries in allowed_key_versions. */
size_t allowed_key_versions_len;
};
/**
* @brief Callback that returns the PSA key identifier for one key version.
*
* @param key_version Requested secure key version.
* @param key_id_out Returned PSA key identifier.
*
* @retval 0 Success.
* @retval -ENOENT Key version is not provisioned.
* @retval negative errno Other failure.
*/
typedef int (*ubi_crypto_get_key_id_cb_t)(uint8_t key_version,
psa_key_id_t *key_id_out);
/**
* @brief Callback that lets the application validate authenticated freshness
* during initialization.
*
* This callback is called once after UBI selects the authenticated on-flash
* state and before SECURE writes are enabled.
*
* @param freshness Selected authenticated freshness descriptor.
* @param user_data User pointer supplied during configuration.
*
* @return Application verdict for rollback policy.
*/
typedef enum ubi_crypto_rollback_verdict
(*ubi_crypto_check_freshness_cb_t)(const struct ubi_crypto_freshness *freshness,
void *user_data);
/**
* @brief Callback that lets the application synchronize authenticated
* freshness to an external trusted store after commit-visible writes.
*
* The callback is called after a commit-visible mutating operation according
* to the configured freshness-sync cadence.
*
* Failure does not roll back the already-committed UBI flash state.
*
* @param freshness Current authenticated freshness descriptor.
* @param user_data User pointer supplied during configuration.
*
* @retval 0 Success.
* @retval negative errno Sync failed.
*/
typedef int (*ubi_crypto_sync_freshness_cb_t)(
const struct ubi_crypto_freshness *freshness,
void *user_data);
/**
* @brief Callback used for security and lifecycle notifications.
*
* The callback may request escalation to read-only mode for future writes.
*
* @param event Event payload owned by UBI for the duration of the callback.
* @param user_data User pointer supplied during configuration.
*
* @return Event-handling verdict for subsequent writes.
*/
typedef enum ubi_crypto_event_verdict
(*ubi_crypto_event_cb_t)(const struct ubi_crypto_event *event,
void *user_data);
/**
* @brief SECURE configuration passed during device initialization.
*/
struct ubi_crypto_config {
/** Per-device SECURE runtime policy, including allowlist and an optional forward-rotation request. */
struct ubi_crypto_policy policy;
/** Callback that returns the PSA key identifier for one key version. */
ubi_crypto_get_key_id_cb_t get_key_id;
/** Callback that validates authenticated freshness during attach. */
ubi_crypto_check_freshness_cb_t check_freshness;
/** Callback that persists authenticated freshness after commit-visible writes. */
ubi_crypto_sync_freshness_cb_t sync_freshness;
/** Callback that receives security and lifecycle events and may escalate to read-only mode. */
ubi_crypto_event_cb_t event_cb;
/** Opaque user pointer passed back to all callbacks. */
void *user_data;
};
Appendix B. Suggested roadmap items outside this spec
This specification assumes that the plain-core prerequisites are already implemented.
Remaining follow-up items that still sit outside the on-flash format itself are:
1. native_sim synthetic power-cut tests
- interrupt after DATA but before VID
- interrupt zero-length DATA record before VID
- interrupt reserved-generation writes at deterministic points
- reboot and verify selection / classification outcomes
2. secure-mode policy tests
- init freshness callback accept / reject
- post-commit freshness sync callback success / failure
- rollback freshness-store state older/newer than flash
- delta-based freshness sync scheduling
- strict read-only transitions on RNG or policy failure
3. secure-mode key lifecycle tests
- KEY_RETIRABLE transitions
- allowlist enforcement
- unavailable key-version handling
- mixed-key-version recovery during rotation
- forced-rekey behavior while stale free/dirty/reserved objects still exist
- key-usage exhaustion and `ROTATE_NOW`
4. replay / tamper validation
- replay stale EC / VID / LEB objects into other locations
- parent-child AAD binding failures
- mode mismatch and wrong-format attach rejection
5. layout and geometry validation
- zero-length LEB encoding
- reserved-generation fit guard
- single-tag CCM-size guard
- single-tag tail-padding / alignment guard
- chunked geometry guard
- chunked alignment guard
6. chunked-mode validation
- chunked partial-read correctness
- chunked cost / latency characterization
- 48-bit counter-overflow rejection for multi-chunk writes
7. lifecycle corner cases
- unmap followed by reboot before erase
- shrink followed by reboot before erase
- unmap / shrink followed by erase and then reboot
8. local hardware validation
- flash timing and latency measurements
- RAM-footprint measurements
- manual power-cut experiments on real boards
Appendix C. Release checklist for SECURE
C.1 Critical format constraints
enforce reserved-generation fit against geometry,
enforce the single-tag CCM payload limit and require chunked mode or reject SECURE,
keep the zero-length LEB encoding fixed,
keep single-tag tail-padding behavior fixed,
reject cross-mode attach; mixed-mode migration and automatic reformat remain out of scope.
C.2 Important implementation notes
use the operational retirement levels from section 13.8 when describing key lifecycle,
include authenticated parent
key_versionin every child AAD binding that has a parent,zeroize plaintext scratch and software-derived child-key buffers,
keep
volume_idas the durable cryptographic identity used by the secure design,keep the authenticated
write_active_key_versionmonotonic and never move it backward,if the API widens
device_revision, preserve on-flash numeric ordering semantics.
C.3 Validation expected before upstream
complete synthetic power-cut testing for
DATA -> VID, zero-length writes, hidden-anchor rewrites, and reserved-generation rewrites,verify PSA-only failure paths, including RNG failure and missing key material,
verify zero-length, mixed-key, chunked-mode, alignment, and hidden-anchor corner cases under recovery,
verify
unmap/shrink -> erase -> rebootwhen the erased PEB was the last current writable witness,verify
volume_remove, including removing all remaining volumes and rebooting into the zero-volume state,verify device-header
vid_next_counter_floorreconstruction and monotonic write-active-key transitions,verify refcount-driven
KEY_RETIRABLEtransitions during ordinary reclaim and rotation.