﻿# Operational (NOC) keys in ESP-TEE

On SoCs that support ESP-TEE (Trusted Execution Environment; currently
ESP32-C6), the Matter **operational (NOC) private key** can be held entirely
inside the secure world. The key is generated in ESP-TEE secure storage, the CSR
is signed there, and every CASE signature is produced there. The private key is
never present in application RAM or in plaintext in flash.

This is the operational-key counterpart to the Device Attestation Certificate
(DAC) key protection described in
[`secure_cert_partition.md`](secure_cert_partition.md) §1.7. The two are
independent and can be enabled together — with both on, **neither** the DAC key
nor any NOC key ever leaves the TEE.

## 1. Enabling

The TEE operational keystore is wired automatically whenever ESP-TEE is enabled;
there is no separate Matter option to turn on.

```ini
CONFIG_SECURE_ENABLE_TEE=y
```

At server start-up (`Esp32AppServer::Init`) the application installs an
`ESP32TEEOperationalKeystore` into `initParams.operationalKeystore`, replacing
the default software `PersistentStorageOperationalKeystore`. Confirm it is
active in the device log:

```text
chip[SVR]: Operational keystore: ESP-TEE secure storage
```

The bundled `examples/lighting-app/esp32/sdkconfig.defaults.esp32c6_tee` enables
ESP-TEE (and the PBKDF2 TEE DAC), so a lighting-app built with that defaults
file gets the TEE operational keystore as well.

### Requirements

-   A target with ESP-TEE support and the `tee_sec_storage` component
    (`esp_tee_sec_storage_*` APIs).
-   The `tee_sec_storage` component must be **discoverable by the build**. It
    lives under `esp_tee/subproject` and is _not_ added just by setting
    `CONFIG_SECURE_ENABLE_TEE`; the application has to include it via
    `EXTRA_COMPONENT_DIRS`. The bundled lighting-app does this automatically
    when its `sdkconfig` defaults enable TEE (see
    `examples/lighting-app/esp32/CMakeLists.txt`); a custom app must add
    `$IDF_PATH/components/esp_tee/subproject/components/tee_sec_storage` itself.
    (The build fails fast with an actionable message if it is missing.)
-   A `secure_storage` NVS partition for the TEE (present in the
    `partitions_tee.csv` layout used by the `_tee` defaults).

## 2. How keys are stored

`OperationalKeystore` has fail-safe semantics: a key created by
`NewOpKeypairForFabric` is _pending_ and must survive a fail-safe expiry
(`RevertPendingKeypair`) without disturbing the previously committed key, and is
only made permanent by `CommitOpKeypairForFabric`.

The software keystore keeps the pending key in RAM and writes it to storage only
on commit. A TEE key **cannot** live in RAM — `esp_tee_sec_storage_gen_key`
persists it immediately. To preserve the fail-safe semantics anyway, each fabric
uses **two secure-storage slots** plus a small persisted pointer:

| Item           | Secure-storage / NVS id          | Contents                                |
| -------------- | -------------------------------- | --------------------------------------- |
| Slot A         | `opk-<fabricIndex>-A`            | a `SECP256R1` key in TEE secure storage |
| Slot B         | `opk-<fabricIndex>-B`            | a `SECP256R1` key in TEE secure storage |
| Active pointer | KVS `tso/<fabricIndex>` (1 byte) | `'A'` or `'B'` — the committed slot     |

Lifecycle → slot mapping:

-   **NewOpKeypairForFabric** — generate into the _inactive_ slot (the one the
    pointer does **not** name), so the committed key stays usable. Build and
    sign the CSR in the TEE.
-   **ActivateOpKeypairForFabric** — verify the pending slot's public key
    matches the incoming NOC public key.
-   **CommitOpKeypairForFabric** — write the active pointer to the pending slot
    (the atomic commit point), then delete the superseded slot's key (rotation).
-   **RevertPendingKeypair** — clear the pending (inactive) slot's key; the
    committed key and pointer are untouched.
-   **RemoveOpKeypairForFabric** — delete both slot keys and the pointer.
-   **SignWithOpKeypair** — sign with the pending slot if a pending key is
    active for the fabric, otherwise with the committed slot.

Because signing goes through `esp_tee_sec_storage_ecdsa_sign`, the CASE
signature is computed inside the TEE. The only application-visible NVS record
per fabric is the 1-byte `tso/<idx>` pointer; the key material lives in the
`secure_storage` partition, which the REE cannot read.

### CSR generation

The CSR is **not** built through the mbedTLS opaque-key wrapper. The public key
is read from the TEE, the PKCS#10 `CertificationRequestInfo` is assembled from
fixed P-256 ASN.1 templates, and its signature is produced with the same raw
`esp_tee_sec_storage_ecdsa_sign` primitive used for CASE. This keeps a single
signing path and avoids depending on the mbedTLS TEE-pk integration.

## 3. Specification notes

Matter Core §6.4.6.1 (Node Operational CSR Procedure) requires the candidate
operational key pair to be:

-   valid only for the duration of the in-progress Fail-Safe Context (2a), and
-   committed to persistent storage only upon successful `AddNOC`/`UpdateNOC`
    with a NOC whose public key matches the candidate (2c).

**Deviation and how it is bounded.** A TEE key is persisted at generation, so
the candidate key is briefly on flash before commit — it cannot be held
RAM-only. The two-slot scheme keeps the _committed_ key the sole active key
until commit: the candidate lives in the inactive slot, is cleared by
`RevertPendingKeypair` on fail-safe expiry, and is never selected by
`SignWithOpKeypair` unless it was activated. If power is lost mid-fail-safe, a
candidate key can linger in the inactive slot; it is bounded to at most one
inactive slot per fabric and is cleared by the next `NewOpKeypairForFabric`
(which removes the inactive-slot key before generating) or by
`RemoveOpKeypairForFabric`. On reboot the RAM pending state is gone, so the
aborted candidate is never treated as active — matching the intent that a
fail-safe that did not complete leaves no usable key.

## 4. Behavior details

-   **ExportOpKeypairForFabric** returns `CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE` —
    TEE keys are non-exportable by design. Migration from another keystore into
    this one is therefore not supported.
-   **CASE ephemeral keys** (`AllocateEphemeralKeypairForCASE`) remain in
    software. These are short-lived per-session keys, not the operational key.
-   **Factory reset** clears the TEE NOC keys. `RemoveFabric` already routes
    through `RemoveOpKeypairForFabric`, but the device factory-reset path
    (`ConfigurationManagerImpl::DoFactoryReset`) only erases the CHIP KVS — not
    the separate `secure_storage` partition holding the key material. It
    therefore calls `ESP32TEEOperationalKeystore::RemoveAllOperationalKeys()`
    before erasing the KVS, so a reset does not orphan operational keys in the
    TEE. The factory-provisioned DAC key uses a different id and is left intact.
-   **Not an in-place upgrade for already-commissioned devices.** The TEE
    keystore is a factory / first-boot choice. A private key cannot be imported
    into ESP-TEE secure storage (`esp_tee_sec_storage` only _generates_ keys),
    so the operational keys of fabrics commissioned under the software keystore
    (stored as `f/<idx>/o`) cannot be migrated. A device already commissioned
    with the software keystore must be factory-reset and re-commissioned after
    enabling `CONFIG_SECURE_ENABLE_TEE`; otherwise those fabrics remain in
    storage but CASE signing has no matching key. Enable this on devices
    provisioned with it from the start.

## 5. Verifying on-device

### Self-test (no commissioning required)

Enable the bring-up self-test:

```ini
CONFIG_ENABLE_ESP32_TEE_OPKEY_SELFTEST=y
```

At boot the application runs `ESP32TEEOpKeySelfTest()` against the real TEE:
generate a throwaway key (id `mtr-op-selftest`, isolated from any fabric
namespace), derive its public key, build and verify a CSR, sign and verify a
message, then delete the key.

Expected log:

```text
chip[SVR]: TEE op-key self-test: PASSED
```

Leave this option off (default) for production builds.

### Live commissioning logs

During a normal commission and operation the keystore logs each operation, so
you can confirm from the device console that NOC keys are handled in the TEE:

```text
chip[Crypto]: TEE opkey: generated NOC keypair for fabric 0x1 in TEE secure storage (slot A), CSR signed in TEE
chip[Crypto]: TEE opkey: committed NOC keypair for fabric 0x1 to TEE secure storage (slot A)
chip[Crypto]: TEE opkey: signing (CASE) for fabric 0x1 with TEE key slot A     # Detail level
chip[Crypto]: TEE opkey: cleared TEE secure-storage NOC key slots for fabric 0x1
```

The absence of any software-keystore key write (no operational-key blob in the
application NVS) is the corresponding negative check.

## 6. Source

| File                                                     | Role                                                                                    |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `src/platform/ESP32/ESP32TEEOpKey.{h,cpp}`               | Low-level TEE key primitives (generate / public key / CSR / sign / remove) + self-test. |
| `src/platform/ESP32/ESP32TEEOperationalKeystore.{h,cpp}` | `Crypto::OperationalKeystore` implementation (two-slot + pointer scheme).               |
| `examples/platform/esp32/common/Esp32AppServer.cpp`      | Installs the keystore into `initParams` under `CONFIG_SECURE_ENABLE_TEE`.               |

> **Note (IDF version):** `esp_tee_sec_storage_ecdsa_sign_t` changed in IDF v6.0
> (separate `sign_r`/`sign_s` fields became a single `signature[]`). The code
> handles both via an `ESP_IDF_VERSION` guard, so it builds on IDF 5.5.x (with
> the TEE secure-storage additions) and on IDF 6.0.x.
