● Live notes + builds
Uncategorized
Reading mode

Edge Security by Design, Part 1

Security is the feature you don’t see until it fails

Every edge device is a small vault in someone else’s hands. Your bank card holds a private key that never leaves its chip. Your phone matches your fingerprint without handing it to the apps you install. A smart meter on your wall reports what you owe, and the supplier bills on its word.

These devices sit in kitchens, cars and substations, so an attacker can buy one, open it and take their time. How does a tiny chip keep a secret from the person who owns it, even when its own application has bugs?

This is Part 1 of two. We build a smart meter that signs every reading with a key its main firmware can use but never read. Part 1 uses the Nordic nRF5340 DK with Zephyr and Trusted Firmware-M.

Three questions every secure edge design must answer

  1. Where do secrets live? Somewhere ordinary code cannot reach, not in normal flash.
  2. What if the application is hacked? The secret must stay safe anyway, so the app should never hold it.
  3. Is this the firmware we intended? The device must check every stage it boots.

The answer to all three is isolation: a small trusted world holds the secrets, and the untrusted world can use them but never see them.

PSA, and welcome to Trusted Firmware

Arm’s Platform Security Architecture (PSA) is a method and a set of standard APIs for building secure devices. Its open reference code is Trusted Firmware-M (TF-M), which works with MCUboot for secure boot and Mbed TLS for cryptography (all under TrustedFirmware.org). Three PSA services matter here:

  • Crypto: create and use keys through an ID. The caller never gets the key bytes.
  • Secure storage: keep small secrets, such as a counter, in protected flash.
  • Attestation: a signed report of what firmware booted.

On a Cortex-M33, TF-M runs in the secure world and Zephyr runs in the non-secure world. TrustZone hardware enforces the split: a non-secure access to secure memory simply faults.

The practical: a smart meter that can’t lie

Each reading goes to the supplier with a counter and a signature. The supplier accepts it only if the signature checks out and the counter is higher than the last one it saw.

What the design must guarantee:

  • The main firmware can use the signing key but never read it.
  • A counter inside the secure world goes up on every signature, so old readings can’t be replayed.
  • The counter and reading are signed together, so they can’t be mixed and matched.

We assume the attacker controls all non-secure code, including the network stack. They cannot break ECDSA, and the debug port is locked on production units. Fault injection and side-channel attacks are out of scope. A hacked app can still ask for a false reading to be signed, but it cannot sign anything off the device or replay old readings; the last section says how to close that gap.

Architecture on the nRF5340

The nRF5340’s application core is a Cortex-M33 with TrustZone-M. Nordic’s System Protection Unit (SPU) marks each region of flash, RAM and peripherals as secure or non-secure, and TF-M sets it up at boot.

The app can reach exactly one door: our meter partition. The key and counter never cross into the non-secure world.

Why a custom partition? If the app held the counter, it could roll it back. If it could call the signing function directly, it could sign anything. Putting increment, then sign in one secure service makes the pair atomic and out of the attacker’s reach.

Building the secure partition

The full buildable project : Zephyr app, TF-M partition, manifests, and the supplier-side verifier is on GitHub: oluseyivictor/tfm_secure_meter.

The shared API header. Both worlds agree on message types and the exact bytes that get signed.


struct meter_response {
uint32_t counter;
uint8_t signature[METER_SIG_LEN]; /* 64 bytes, raw r || s */
};

Full header: meter_partition/meter_api.h


The manifest. TF-M’s IPC model: one connection-based service the non-secure app may call, plus the services our partition depends on.

{
"name": "TFM_SP_METER",
"type": "APPLICATION-ROT",
"model": "IPC",
"services": [{ "name": "TFM_METER_SIGN_READING", "connection_based": true }],
"dependencies": ["TFM_CRYPTO", "TFM_INTERNAL_TRUSTED_STORAGE_SERVICE"]
}

Full manifest: meter_partition/tfm_meter_partition.yaml


The partition source. The key has no export flag, so nobody can read it back. The counter is saved before signing, so a power cut can skip a number but never repeat one:

psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_SIGN_HASH); /* no EXPORT */
...
++current; /* counter saved first ... */
psa_its_set(METER_COUNTER_UID, sizeof(current), &current, PSA_STORAGE_FLAG_NONE);
... /* ... then bound into the signed record and signed */

Full source: meter_partition/meter_partition.c


Notice what is not here: no call returns the private key, and no call sets the counter. The only way to move the counter is to produce a signature.

The non-secure host code

The Zephyr app is ordinary firmware. It reads the meter, calls meter_sign_reading() and sends the frame. It never touches a key:


psa_status_t status = meter_sign_reading(reading_wh, &response);
/* response.counter, response.signature[64] — build the frame from these */

Full source: src/main.c and src/meter_client.c (the PSA client wrapper)


The uplink frame is the 13 signed bytes (version, counter and reading, all little-endian) followed by the 64-byte signature.

Build.


git clone https://github.com/oluseyivictor/tfm_secure_meter
west build -p always -b nrf5340dk/nrf5340/cpuapp/ns tfm_secure_meter
west flash

The supplier’s side: enrol once, verify every reading

Enrolment. At the factory, record each meter’s public key against its serial number. To get it, add a second partition command that returns the key with psa_export_public_key(). This works even without the export flag, because only the public half leaves.

Verification. For each frame, check the signature, then check that the counter is higher than the last accepted one.

python tools/verify_frame.py frame.bin pubkey.pem --last-counter 41

Full script: tools/verify_frame.py

PSA gives a raw r || s signature, but most server libraries want DER, hence the conversion. A gap in the counter is fine (a lost uplink or a power cut). A repeat or a lower number never is.

Double assurance: is this the firmware we intended?

If an attacker can flash a modified TF-M, the whole design fails. Three things close that gap.

1. Secure boot. Each stage verifies the next before running it. In the nRF Connect SDK, turn it on in sysbuild.conf:


# sysbuild.conf
SB_CONFIG_SECURE_BOOT_APPCORE=y # immutable first stage (NSIB)
SB_CONFIG_BOOTLOADER_MCUBOOT=y # upgradable second stage
SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${APP_DIR}/keys/mcuboot-priv.pem"

The immutable bootloader checks MCUboot, and MCUboot checks TF-M plus Zephyr. Keep the signing keys on your build server, and turn on downgrade prevention so old vulnerable images can’t be installed.

2. Attestation. With CONFIG_TFM_PARTITION_INITIAL_ATTESTATION=y, the app can call psa_initial_attest_get_token() with a nonce from the server and get back a signed report of what booted. The supplier can then check the firmware remotely. Provision the attestation key first; your SDK’s TF-M docs show how.

3. Lock debug. Production units must not expose a debugger that can read secure flash:


# prj.conf (production builds)
CONFIG_NRF_APPROTECT_LOCK=y
CONFIG_NRF_SECURE_APPROTECT_LOCK=y

Leave this off during development and on for every unit that ships.

Pitfalls, and what comes next

  • Flash wear. One write every 30 minutes is about 17,500 a year, so size the storage area for the meter’s lifetime, or reserve counter values in batches.
  • The reading itself. We prove which device signed a reading and in what order, not that the number is true. To protect it fully, give the meterology ADC to the secure world and read it inside the partition.
  • Check every input. Everything crossing into the secure world is attacker-controlled, so check every size, as the partition does.

The pattern is the same for payment terminals, fingerprint readers and car keys: a small trusted world owns the secret, a narrow door lets untrusted code use it, and a boot chain proves the trusted world is genuine.

In Part 2 we build the same meter on a Xilinx Zynq UltraScale+ MPSoC, with TF-A and OP-TEE in place of TF-M.

Discover more from NeuralonEdge

Subscribe now to keep reading and get access to the full archive.

Continue reading