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, sqnum-based recovery, and the plain crash model still apply.

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 IKM[key_version] fans out into device, volume, EC, VID, and per-volume LEB child keys.

Strong domain separation and per-volume isolation for LEB data.

Key lifecycle built into the format

SECURE tracks write_active_key_version, allowlisted versions, usage budgets, refcounts, retirement, and continuity floors.

The application does not need to reinvent key-rotation safety inside the filesystem or database.

Freshness exported to the application

UBI exports authenticated (device_revision, global_sqnum) and provides freshness callbacks.

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, unmap, shrink, and even after removing all volumes.

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 free from uncommitted by 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_id is 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 == NULL requests plain mode,

  • crypto_cfg != NULL requests 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 write_active_key_version, authenticated freshness values (device_revision, global_sqnum), key lifecycle events, security events, retirement notifications, post-commit freshness-sync requests

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 sqnum-based recovery remain.

Rollback / replay hook

UBI exports authenticated (device_revision, global_sqnum) so the application can compare them against its trusted freshness store.

Key lifecycle handling

write_active_key_version, allowlist enforcement, budgets, refcounts, retirement, and continuity floors live in UBI instead of being rebuilt above it.

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:

  1. Every live user LEB maps to exactly one data PEB.

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

  3. Among competing authenticated VIDs for the same (volume_id, lnum), the higher vid_sqnum wins.

  4. A dirty PEB must be erased before it can re-enter the free pool.

  5. Reserved PEBs are used only for secure device and volume metadata.

  6. The authenticated secure device header stores:

    • write_active_key_version

    • vid_next_counter_floor

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

  8. volume_id is 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.

  9. The write-active key version is monotonic and must never move back to an older value on the same formatted device.

  10. A key version becomes retirable only when no authenticated on-flash object still references it.

  11. The device must not perform a secure write if it cannot construct a fresh nonce from cryptographically secure randomness.

  12. All commit-visible mutating operations pass through one central mutation gate so that secure policy and freshness hooks observe the same transition points.

  13. 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:

  1. every invocation under one key requires a unique nonce,

  2. 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_counter

  • leb_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.counter values,

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

  • 0x01 is the current format/KDF compatibility byte,

  • all integer fields inside KDF context are big-endian,

  • volume_id is encoded as be32(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_base stored in prefix32.counter,

  • one per-chunk counter increment,

  • chunk_index included 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_hdr

  • struct ubi_vol_hdr

  • struct ubi_ec_hdr

  • struct 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:

  • magic is 32 bits,

  • wrapper_version is fixed for the current format and reserved for explicit future compatibility decisions,

  • salt stays 6 bytes,

  • counter stays 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_hdr

  • secure 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_version is part of authenticated on-flash policy state,

  • in the current format, write_active_key_version shall match the authenticated prefix32.key_version of the secure device header record,

  • vid_next_counter_floor is monotonic and never decreases,

  • when the write-active key version advances to a newer value, vid_next_counter_floor is 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_hdr

  • secure 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_bytes is the current logical payload length,

  • payload_bytes is taken from authenticated vid_hdr.data_size,

  • if authenticated vid_hdr.data_size == 0, SECURE still writes a zero-length secure LEB record using the base prefix32 || tag16 layout with zero ciphertext bytes,

  • for that zero-length case, one AEAD invocation is still consumed and leb_total_auth_bytes advances 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 after tag16 are 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 record prefix32 || tag16; no per-chunk tags are present,

  • for data_size > 0, leb_write_counter advances by chunk_count and leb_total_auth_bytes advances by data_size + chunk_count * fixed_leb_aad_bytes_chunked,

  • for the zero-length fallback, leb_write_counter advances by 1 and leb_total_auth_bytes advances by fixed_leb_aad_bytes_single,

  • if chunk ciphertext is written directly to flash, chunk_size should 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.9 Hidden per-volume anchor PEB

Each secure volume owns one hidden anchor PEB.

Properties:

  • it is not user-addressable,

  • it uses the same secure EC / secure VID / secure LEB record types as an ordinary data PEB,

  • it uses a dedicated internal logical number INTERNAL_ANCHOR_LNUM that is outside the user-visible lnum range,

  • its secure LEB record always uses the zero-length base layout prefix32 || tag16,

  • its secure VID metadata carries the same two per-volume recovery fields:

    • leb_write_counter

    • leb_total_auth_bytes

  • user-visible leb_count excludes the hidden anchor.

The anchor exists for one purpose:

  • if all user mappings of a volume are later unmapped or shrunk away,

  • or if the last dirty user witness carrying the newest per-volume floor is about to be erased,

  • SECURE still has one authenticated, commit-visible carrier that can preserve the per-volume LEB usage state.

Because the anchor uses the normal secure VID and secure LEB layouts, it benefits from the same properties as ordinary mappings:

  • DATA -> VID commit order,

  • higher vid_sqnum wins,

  • authenticated parent binding,

  • authenticated recovery of leb_write_counter and leb_total_auth_bytes.

Anchor-liveness policy:

  • SECURE shall preserve one emergency free data PEB for secure maintenance writes such as hidden-anchor rescue.

  • If that invariant is temporarily broken, reclaim shall first erase a dirty PEB that is not the last current writable witness for any protected per-volume floor in order to re-establish one.

  • SECURE must never erase the last current writable witness and only afterwards discover that no free data PEB exists for the required anchor rewrite.

  • If neither a free data PEB nor a safe non-witness dirty PEB exists, the operation must be deferred or rejected by policy. SECURE does not break counter continuity to make progress.

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

  • vid_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 + 1

  • chunked write with chunk_count = N
    next = counter_base + N

Why both exist:

  • prefix32.counter is required immediately to build the current record nonce or nonce sequence,

  • vid_secure_meta.leb_write_counter is 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 packed dependency.

8.5 AAD by record type

AAD fields are serialized in the exact order shown below.

  • Secure device header — 44 B total

    • prefix32: 32 B

    • reserved-PEB physical eraseblock index be32(...): 4 B

    • device-header flash offset from the start of the UBI partition be64(...): 8 B

  • Secure volume header — 53 B total

    • prefix32: 32 B

    • reserved-PEB physical eraseblock index be32(...): 4 B

    • volume-header flash offset from the start of the UBI partition be64(...): 8 B

    • authenticated device_header.revision be64(...): 8 B

    • authenticated parent secure-device key_version u8: 1 B

  • Secure EC header — 44 B total

    • prefix32: 32 B

    • data-PEB physical eraseblock index be32(...): 4 B

    • EC-header flash offset from the start of the UBI partition be64(...): 8 B

  • Secure VID header — 53 B total

    • prefix32: 32 B

    • data-PEB physical eraseblock index be32(...): 4 B

    • VID-header flash offset from the start of the UBI partition be64(...): 8 B

    • authenticated ec_hdr.ec be64(...): 8 B

    • authenticated parent secure-EC key_version u8: 1 B

  • Secure LEB record, single-tag or zero-length — 74 B total

    • prefix32: 32 B

    • data-PEB physical eraseblock index be32(...): 4 B

    • LEB-data flash offset from the start of the UBI partition be64(...): 8 B

    • authenticated ec_hdr.ec be64(...): 8 B

    • authenticated parent secure-EC key_version u8: 1 B

    • authenticated vid_hdr.volume_id be32(...): 4 B

    • authenticated vid_hdr.lnum be32(...): 4 B

    • authenticated vid_hdr.sqnum be64(...): 8 B

    • authenticated vid_hdr.data_size be32(...): 4 B

    • authenticated parent secure-VID key_version u8: 1 B

  • Secure LEB record, chunked — 78 B total

    • all fields from the single-tag / zero-length LEB AAD: 74 B

    • chunk_index be32(...): 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_sqnum is a field in one secure VID record,

  • global_sqnum is 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_revision is authenticated and selected from the winning reserved generation,

  • global_sqnum is 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.4 Authoritative per-volume LEB usage-recovery state

For each {key_version, volume_id}, UBI needs to continue writing without reusing a nonce and without losing sight of cumulative key usage.

The authoritative per-volume state is:

leb_write_counter
leb_total_auth_bytes

These values are always carried by authenticated secure VID metadata.

There are two carriers:

  • ordinary live secure VID records of user mappings,

  • the hidden anchor secure VID of that volume.

That is why the secure VID header contains more than the plain VID payload: it is both

  • the logical mapping record,

  • and the trusted recovery state for future LEB writes.

The secure hidden anchor exists precisely so that this per-volume floor can remain commit-visible even when user mappings disappear.

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_floor is 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, and leb_invocation_pct refer 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_SOON fires when the projected percentage crosses the configured soft threshold,

  • ROTATE_NOW fires 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_counter by 1,

  • they advance leb_total_auth_bytes by fixed_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 device_revision wins

secure EC header

live secure EC headers on data PEBs

crash around ERASE -> EC

accepted best-effort continuity; child binding to authenticated ec_hdr.ec remains the main security property

secure VID header for current write-active key version

live secure VID headers plus device_header.vid_next_counter_floor

volume_remove, including removing all volumes

save the last VID-domain floor in the secure device header on every reserved metadata rewrite

secure LEB usage state for {key_version, volume_id}

live secure VID metadata of user mappings plus the hidden anchor

unmap/shrink -> erase -> reboot/crash, or all user LEBs unmapped

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:

  1. a dirty user PEB still carries the newest committed {leb_write_counter, leb_total_auth_bytes} for one {key_version, volume_id},

  2. no other live user mapping of that volume carries the same or newer floor,

  3. the dirty PEB is physically erased,

  4. 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 lnum range,

  • always encoded as a zero-length secure LEB record,

  • authenticated and selected by the same vid_sqnum rules 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_sqnum ordering.

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:

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

  2. 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:

  1. enumerate all reserved PEBs configured by CONFIG_UBI_DEV_HDR_NR_OF_RES_PEBS,

  2. authenticate secure device header candidates,

  3. reject unauthenticated candidates,

  4. select the highest authenticated device_revision,

  5. authenticate the secure volume headers that belong to the selected generation,

  6. extract from the authenticated secure device header:

    • write_active_key_version

    • vid_next_counter_floor.

10.3 Data-PEB classification

For every data PEB:

  1. authenticate the secure EC header,

  2. inspect the secure VID region,

  3. 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 vid_sqnum

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

  • VID 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:

  1. checks that the write-active key version is available,

  2. checks projected key-usage budgets,

  3. checks that fresh cryptographic randomness is available,

  4. executes the commit-visible mutation,

  5. updates the exported freshness descriptor,

  6. schedules or performs post-commit freshness synchronization,

  7. emits lifecycle or security events,

  8. 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:

  1. increment device_header.revision,

  2. snapshot the current VID-domain runtime floor:

    vid_next_counter_floor = next_vid_counter[write_active_key_version]
    
  3. write the new secure device header carrying:

    • write_active_key_version

    • vid_next_counter_floor

  4. write the new secure volume headers,

  5. switch the selected reserved generation only after the new generation is complete,

  6. update the exported freshness descriptor,

  7. 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_revision advances,

  • reserved metadata is immediately rewritten under the new key version,

  • the secure device header updates write_active_key_version,

  • vid_next_counter_floor is 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.4 Volume creation and hidden-anchor initialization

A successful secure volume creation shall establish exactly one live hidden anchor for that volume before user writes are accepted.

The hidden anchor:

  • uses INTERNAL_ANCHOR_LNUM,

  • writes a zero-length secure LEB record,

  • initializes per-volume LEB recovery state for the current {write_active_key_version, volume_id} using the post-commit values of that initial zero-length secure write,

  • does not change the user-visible leb_count.

If anchor creation cannot be completed, the volume creation is not complete for SECURE write purposes. Implementations may fail the create operation or keep the new volume unwritable until the anchor is materialized, but they must not allow ordinary secure user writes to that volume without a live authenticated anchor.

Anchor creation also participates in the emergency-free-PEB policy:

  • SECURE should keep one free data PEB available for later hidden-anchor rescue,

  • if creating the new volume’s anchor would consume the last free data PEB, reclaim shall first try to refresh the reserve by erasing a dirty PEB that is not a last current writable witness,

  • if that cannot be done safely, volume creation must fail or be deferred.

11.5 Data write path

For a secure LEB write:

  1. 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,

  2. keep the chosen PEB’s existing secure EC header,

  3. choose the secure LEB encoding:

    • if payload_bytes == 0, build the zero-length base record prefix32 || 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,

  4. compute aead_invocations_this_write:

    • 1 for zero-length or single-tag mode,

    • ceil(payload_bytes / chunk_size) for chunked mode with payload_bytes > 0,

  5. compute 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,

  6. check projected VID-domain and LEB-domain usage before any flash mutation:

    • if the projected usage crosses ROTATE_SOON, emit KEY_ROTATE_SOON,

    • if the projected usage crosses ROTATE_NOW, emit KEY_ROTATE_NOW and reject the write before any flash mutation,

  7. reserve counter_base = next_leb_write_counter[{key_version, volume_id}],

  8. verify that the reserved AEAD counter range fits in the 48-bit nonce counter field:

    • if counter_base + aead_invocations_this_write - 1 > 0xFFFFFFFFFFFF, emit KEY_ROTATE_NOW and reject the write before any flash mutation,

  9. write the secure LEB record using counter_base,

  10. 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,

  1. write the secure VID header,

  2. update the in-RAM EBA mapping so the new PEB becomes live,

  3. mark the old PEB dirty,

  4. update the exported freshness descriptor,

  5. 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.6 Erase / reclaim path with hidden-anchor preservation

When a dirty data PEB is selected for erase, SECURE performs one extra continuity check before reclaim destroys the old secure contents.

For the current authenticated write_active_key_version, reclaim must determine whether the dirty PEB is the last current writable witness of the newest per-volume LEB floor for its {key_version, volume_id}.

If it is not the last current writable witness:

  1. erase the PEB,

  2. decrement refcounts of the old secure objects,

  3. write a fresh secure EC header under the current write-active EC key version,

  4. count the new secure EC object,

  5. return the PEB to the free pool.

If it is the last current writable witness:

  1. choose a free data PEB for the hidden anchor rewrite,

  2. rewrite the hidden anchor first:

    • secure LEB payload remains zero-length,

    • leb_total_auth_bytes is copied forward and then advanced by fixed_leb_aad_bytes_single,

    • leb_write_counter advances by 1 because the anchor rewrite itself consumes one AEAD invocation under the same K_leb[key_version][volume_id],

    • the new anchor secure VID receives a higher vid_sqnum,

  3. commit the new anchor secure VID,

  4. only then erase the old dirty PEB,

  5. decrement refcounts of the old secure objects,

  6. write a fresh secure EC header and return the reclaimed PEB to the free pool.

SECURE should reach this point with one emergency free data PEB already available. If that reserve is missing, reclaim shall first try to erase a dirty PEB that is not the last current writable witness for any protected per-volume floor, write its fresh secure EC header, and return it to the free pool. Only then may reclaim process the protected dirty PEB.

If no such safe candidate exists and no free data PEB is available for the required anchor rewrite, the protected erase must be deferred or rejected by policy. SECURE must not destroy the last current writable witness and only afterwards discover that there was no place to commit the inherited anchor state.

Because the anchor rewrite is a normal secure VID + secure LEB commit, it may also advance global_sqnum.

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 from mapped to dirty; 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 smaller leb_count is committed in reserved metadata first, so tail PEBs whose authenticated lnum is 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:

  1. read the complete secure payload for that LEB,

  2. authenticate the complete secure payload,

  3. decrypt the complete secure payload,

  4. 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_versions

  • size_t allowed_key_versions_len

Normative rules:

  • each element is one allowed key_version value,

  • allowed_key_versions_len is 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_version stored 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:

  1. soft rotation
    New writes use a newer key version.

  2. live rewrite completed
    All currently live mappings have been rewritten under the newer key version.

  3. media scrub completed
    Stale reserved generations, dirty data, and free-PEB EC objects that still reference the old key version have been eliminated.

  4. 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_FAILURE

  • FORMAT_VIOLATION

  • KEY_VERSION_NOT_ALLOWLISTED

  • KEY_VERSION_UNAVAILABLE

  • ROLLBACK_POLICY_MISMATCH

  • FRESHNESS_SYNC_FAILURE

  • RNG_FAILURE

  • KEY_ROTATE_SOON

  • KEY_ROTATE_NOW

  • KEY_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 KEY_ROTATE_NOW and reject write before flash mutation

no write-active key material is available

reject write; optionally enter read-only

projected usage crosses ROTATE_NOW and no replacement key is provisioned

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 FRESHNESS_SYNC_FAILURE; optionally enter read-only, but do not roll back committed flash state

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

CONFIG_UBI_CRYPTO

Enables SECURE mode support.

CONFIG_UBI_CRYPTO_MAX_KEY_VERSIONS

Maximum number of distinct key versions that one attach session may inventory and track internally. The on-flash key_version field itself remains 8-bit.

CONFIG_UBI_CRYPTO_MAX_ALLOWLIST_LEN

Maximum number of uint8_t key-version entries accepted in the runtime allowlist array.

CONFIG_UBI_CRYPTO_ROTATE_SOON_PCT

Soft threshold for usage-budget warnings. Crossing it emits KEY_ROTATE_SOON.

CONFIG_UBI_CRYPTO_ROTATE_NOW_PCT

Hard threshold for usage-budget exhaustion. Crossing it emits KEY_ROTATE_NOW and may reject further writes by policy.

CONFIG_UBI_CRYPTO_METADATA_COUNTER_BUDGET

Maximum allowed metadata AEAD invocation count per {domain, key_version}.

CONFIG_UBI_CRYPTO_METADATA_TOTAL_AUTH_BYTES_BUDGET

Maximum allowed authenticated metadata bytes per {domain, key_version}. This is derived from fixed record sizes; no extra on-flash byte counter is needed.

CONFIG_UBI_CRYPTO_LEB_WRITE_BUDGET

Maximum allowed AEAD invocation count per {key_version, volume_id}.

CONFIG_UBI_CRYPTO_LEB_TOTAL_AUTH_BYTES_BUDGET

Maximum allowed cumulative authenticated LEB bytes per {key_version, volume_id}.

CONFIG_UBI_CRYPTO_LEB_CHUNKED

Enables chunked secure LEB layout for geometries where single-tag mode is invalid or undesirable.

CONFIG_UBI_CRYPTO_LEB_CHUNK_SIZE

Chunk size used by chunked LEB layout. It drives chunk count, tag overhead, RAM profile, and alignment requirements.

CONFIG_UBI_CRYPTO_PEB_CACHE

Enables one reusable eraseblock-sized SECURE staging / cache buffer. This avoids mandatory heap allocation in paths that need full-PEB staging.

CONFIG_UBI_CRYPTO_PEB_CACHE_STATIC

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.

CONFIG_UBI_CRYPTO_FRESHNESS_SYNC_DELTA

Number of commit-visible mutations between post-commit freshness-sync callbacks. 0 means sync after every commit-visible mutation.

CONFIG_UBI_CRYPTO_STRICT_RO_ON_RNG_FAILURE

Forces read-only or write shutdown when fresh secure-write salt cannot be generated.

CONFIG_UBI_CRYPTO_STRICT_RO_ON_POLICY_FAILURE

Forces read-only or init failure when freshness policy rejects the authenticated flash state.

CONFIG_UBI_CRYPTO_STRICT_RO_ON_FRESHNESS_SYNC_FAILURE

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 volume_count

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:

  1. SECURE mode is PSA-only.

  2. 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 header ubi.h forward-declares struct ubi_crypto_config so that plain callers do not need to include ubi_crypto.h.

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

  4. UBI exports:

    • the authenticated write_active_key_version from 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.

  5. There is no raw IKM buffer callback in the normal API surface.

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

  7. When CONFIG_UBI_CRYPTO is not enabled and the caller passes crypto_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.4 Hidden-anchor capacity cost

The hidden anchor is intentionally simple, but it is not free.

Each secure volume permanently reserves one additional data PEB for its hidden anchor.

SECURE also preserves one global emergency free data PEB so that reclaim can rewrite a hidden anchor before erasing a protected last current writable witness.

So the structural capacity cost is:

anchor_raw_capacity_cost            = secure_volume_count * peb_size
emergency_free_reserve_raw_cost     = peb_size
total_secure_continuity_raw_cost    = (secure_volume_count + 1) * peb_size

In single-tag terms, the user-visible payload opportunity cost is approximately:

anchor_user_payload_cost_single         = secure_volume_count * (peb_size - 208)
emergency_free_reserve_payload_cost     = peb_size - 208
total_secure_continuity_payload_cost    = (secure_volume_count + 1) * (peb_size - 208)

because each hidden anchor PEB and the emergency free reserve could otherwise have hosted one ordinary secure user LEB.

The hidden-anchor continuity design therefore trades one full data PEB per secure volume plus one per-device reserve PEB for strict continuity of:

  • leb_write_counter

  • leb_total_auth_bytes

  • a live secure VID witness even when all user LEBs of that volume are unmapped.

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 + 48 bytes.

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_SIZE should 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


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_version in every child AAD binding that has a parent,

  • zeroize plaintext scratch and software-derived child-key buffers,

  • keep volume_id as the durable cryptographic identity used by the secure design,

  • keep the authenticated write_active_key_version monotonic 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 -> reboot when 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_floor reconstruction and monotonic write-active-key transitions,

  • verify refcount-driven KEY_RETIRABLE transitions during ordinary reclaim and rotation.