sign_verify

Example of the component embedblocks/msg_auth v0.1.0
# msg_auth sign_verify

A demonstration and acceptance test for the msg_auth component. It runs every accept and reject case of msg_auth in one task, on any ESP32 board, with **no Wi-Fi and no network**: msg_auth needs nothing but bytes, so a "channel" here is just a buffer passed from one context to another.

The app creates four contexts from the same key and purpose:

| Context | Node ID | Key | Role |
| --- | --- | --- | --- |
| A | 0 | test key | sender, epoch *e* |
| A′ | 0 | test key | the same node "after a reboot", epoch *e* + 1 |
| B | 1 | test key | the receiver in every scenario |
| outsider | 0 (claimed) | test key with one bit changed | a node outside the installation |

Node 2 exists only as an address, for the misaddressed case.

## Requirements

- ESP-IDF **6.0.x**. msg_auth's build checks and its manifest reject other releases.
- The msg_auth component folder must be named `msg_auth`, because ESP-IDF names a component after its folder. The example finds it two levels up (`../..`, set in this folder's `CMakeLists.txt`).

`sdkconfig.defaults` sets what msg_auth needs. These are ESP-IDF's defaults anyway; they are listed so a changed global configuration can't silently remove them:

```
CONFIG_MBEDTLS_THREADING_C=y
CONFIG_MBEDTLS_SHA256_C=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096
```

## Run

```sh
cd examples/sign_verify
idf.py set-target esp32c3          # your chip
idf.py build flash monitor
```

The output, one line per scenario:

```
W (312) sign_verify: using the INSECURE example key from menuconfig; never ship it
I (322) sign_verify: epoch 7 (committed to NVS before create)
I (332) sign_verify: PASS A signs, B verifies                          got ESP_OK                       want ESP_OK
I (332) sign_verify: PASS bit flip: message                            got ESP_ERR_MSG_AUTH_BAD_TAG     want ESP_ERR_MSG_AUTH_BAD_TAG
...
I (452) sign_verify: PASS older epoch after msg_auth_reset_peer        got ESP_OK                       want ESP_OK
I (462) sign_verify: timing   64 B: sign  ... us, verify  ... us (mean of 200)
I (512) sign_verify: timing 1432 B: sign  ... us, verify  ... us (mean of 200)
I (512) sign_verify: B <- node 0: accepted 406, bad_tag 5, replay 2, last (8, 405)
I (512) sign_verify: B <- node 2: accepted 0, bad_tag 0, replay 0, last (0, 0), not seen
I (522) sign_verify: PASS: 19 passed, 0 failed
```

**Pass criterion:** the last line says `PASS` with 0 failed. The epoch rises by 2 on every boot; press reset a few times and check that it does.

## Scenarios

| Scenario | Expected |
| --- | --- |
| A signs, B verifies | `ESP_OK`, message and sender ID intact |
| One bit flipped in the message, the counter, the epoch or the tag | `BAD_TAG` |
| One bit flipped in the receiver ID | `NOT_FOR_ME` |
| One bit flipped in the version | `MALFORMED` |
| The unflipped original after all the flips | `ESP_OK`: no forgery moved B's state |
| Outsider signs, B verifies | `BAD_TAG` |
| Same envelope verified twice | `ESP_OK`, then `REPLAY` |
| Envelope for node 2 given to B | `NOT_FOR_ME` |
| 1461 bytes / 27 bytes / plain unsigned bytes | `INVALID_SIZE` / `MALFORMED` / `MALFORMED` |
| The largest envelope, 1460 bytes | `ESP_OK` |
| A′ (epoch + 1, counter back to 0) | `ESP_OK` |
| A again (older epoch) | `REPLAY`; after `msg_auth_reset_peer` `ESP_OK` |

Then the app times 200 sign/verify pairs at 64 and at 1432 bytes and prints B's statistics. The 5 bad tags for node 0 are the four bit flips that reach the MAC plus the outsider. The 2 replays are the repeated envelope and the older epoch.

## How the epoch is handled

`reserve_epochs()` is how a real application should meet the epoch contract (`MSG_AUTH_SPEC.md` §6.2):

1. read the boot counter from NVS;
2. write the incremented value and **commit** it;
3. only then create the contexts. Any NVS error stops the app before a context exists: it never signs under an epoch that might be reused.

This app reserves **two** epochs per boot because it also plays A′, a rebooted A. A normal application reserves one.

Erasing flash (`idf.py erase-flash`) resets the counter. That is the recovery case: receivers that remember the node reject its messages as `REPLAY` until they call `msg_auth_reset_peer()` for it. In this example nothing remembers between boots, because B's state is in RAM too.

## The test key

`EXAMPLE_PSK_HEX` in menuconfig (**msg_auth sign_verify example**) holds the installation key as 32–128 hex digits. The default is public and **insecure**. A real installation provisions a random key per installation and keeps it in encrypted NVS.

## Troubleshooting

| Symptom | Cause |
| --- | --- |
| `#error "msg_auth: verified on ESP-IDF 6.0.x only"` | Wrong ESP-IDF release. |
| `#error "msg_auth: requires ..."` | A PSA algorithm or Mbed TLS threading was disabled in menuconfig (Component config → mbedTLS). Re-enable it, or run `idf.py fullclean` after restoring `sdkconfig.defaults`. |
| `Failed to resolve component 'msg_auth'` | The component folder is not named `msg_auth`, or it is not at `../..` relative to this folder. |
| `msg_auth_create` aborts with `ESP_FAIL` and the log shows `key derivation: <n>` | The target's PSA rejected the key policy. Rebuild with `idf.py -DMSG_AUTH_MAC_FALLBACK=1 fullclean build` and record the result in `MSG_AUTH_SPEC.md` §9.3. |
| `EXAMPLE_PSK_HEX must be 32..128 hex digits` | The menuconfig key is too short, has an odd number of digits, or contains non-hex characters. |

## Files

| File | What |
| --- | --- |
| `main/sign_verify.c` | the app: contexts, scenarios, timing, statistics |
| `main/Kconfig.projbuild` | menuconfig option for the test key |
| `sdkconfig.defaults` | settings msg_auth relies on |
| `CMakeLists.txt` | finds msg_auth at `../..`; no network components |

To create a project from this example, run:

idf.py create-project-from-example "embedblocks/msg_auth=0.1.0:sign_verify"

or download archive (~7.55 KB)