Skip to content

NXP EdgeLock (ELS) crypto callback port: hashing, AES, CMAC and DRBG - #11703

Open
Frauschi wants to merge 10 commits into
wolfSSL:masterfrom
Frauschi:els_pkc_pr1
Open

Frauschi wants to merge 10 commits into
wolfSSL:masterfrom
Frauschi:els_pkc_pr1

Conversation

@Frauschi

@Frauschi Frauschi commented Oct 9, 2026

Copy link
Copy Markdown
Member

Description

Adds a crypto callback port for the EdgeLock secure subsystem (ELS) on the NXP RW612 (wolfcrypt/src/port/nxp/els_pkc_port.c, WOLFSSL_ELS_PKC). It offloads SHA-256/384/512, AES-ECB/CBC/CTR, AES-GCM, AES-CMAC and the DRBG. Anything the hardware does not serve is declined to software, so enabling the port never removes functionality.

First of three PRs: the second adds the PKC coprocessor (RSA, X25519, ECDSA) and the ELS key store, the third a CI workflow under the m33mu emulator. NXP's CLNS library (els_pkc) is linked by the application and not vendored, as with the SE050 port.

Design notes

  • One lock per command. Hash and CMAC state lives in the caller's object, so the lock is never held across calls (TLS 1.3 keeps several transcript hashes open). Such a context must be finished before the last wolfCrypt_Cleanup().
  • Validate before issuing. ELS answers a bad slot request by resetting the SoC, so every slot is checked before each command.
  • Interrupt-driven waits under Zephyr instead of the vendor's unbounded spin.
  • Declining is not failing. Unsupported requests (AES-192, non-12-byte GCM IV, SHA-224, SHA3, ...) run in software; a hardware error returns WC_HW_E. A key held in a slot has no software copy, so there such a request fails instead.
  • Key slots: wc_ElsPkc_AesUseSlot() / wc_ElsPkc_CmacUseSlot() bind a key to a slot through its id blob.
  • Bare-metal seeding: a new WOLFSSL_ELS_PKC branch of wc_GenerateSeed() in random.c, after the Zephyr one, seeds the Hash-DRBG from the ELS DRBG (rated at 128-bit strength). wolfCrypt_Init() must run before the first wc_InitRng().
  • HMAC, HKDF and the TLS PRF reach the engine through the hash path.

Building

  • Zephyr: CONFIG_WOLFSSL_ELS_PKC=y on frdm_rw612; it also selects the new CONFIG_WOLFSSL_SYS_INIT, which initialises wolfSSL at boot. Settings-file builds must define WOLFSSL_ELS_PKC and WOLF_CRYPTO_CB.
  • autoconf: --with-els-pkc=PATH --with-mcux-sdk=PATH. Test, benchmark and example programs are not built, since they cannot link without CLNS.
  • MCUXpresso SDK: IDE/MCUEXPRESSO/RW612 has a user_settings.h and build steps.

Benchmark

frdm_rw612, Zephyr 4.4, 1 KiB blocks, one run. Software rows use the module defaults plus CONFIG_WOLFSSL_GHASH_TABLE_4BIT=y and WOLFSSL_AES_COUNTER.

Algorithm Software EdgeLock Speedup
SHA-256 4.74 MiB/s 48.63 MiB/s 10.3x
SHA-512 2.00 MiB/s 46.53 MiB/s 23.2x
HMAC-SHA256 4.71 MiB/s 41.97 MiB/s 8.9x
AES-128-CBC enc 3.47 MiB/s 45.34 MiB/s 13.1x
AES-256-CBC enc 2.66 MiB/s 40.62 MiB/s 15.3x
AES-128-CTR 3.59 MiB/s 40.09 MiB/s 11.2x
AES-128-GCM enc 1.81 MiB/s 14.09 MiB/s 7.8x
AES-256-GCM enc 1.54 MiB/s 13.26 MiB/s 8.6x
GMAC 3.71 MiB/s 19.80 MiB/s 5.3x
AES-128-CMAC 3.25 MiB/s 10.23 MiB/s 3.2x
Hash-DRBG (SHA-256) 1.34 MiB/s 35.06 MiB/s 26.1x

@Frauschi Frauschi self-assigned this Oct 9, 2026
Copilot AI balanced review requested due to automatic review settings October 9, 2026 14:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

SHA-512 length handling is incomplete, and callback registration can incorrectly adopt and remove another device.

2 open findings

🧠 Review effort: Balanced

Comment thread wolfcrypt/src/port/nxp/els_pkc_port.c
Comment thread wolfcrypt/src/port/nxp/els_pkc_port.c Outdated
Routes wolfCrypt through the on-chip EdgeLock subsystem (ELS) on RW612 via the
crypto callback interface, with SHA-256 as the first offloaded primitive.
Anything the hardware does not implement declines to software, so enabling the
port never removes functionality.

Three properties of the hardware shape the design.

ELS is one peripheral with global busy state: every operation is an _Async call
followed by a wait, and nothing may start between the two, so one lock is held
across each pair.

ELS answers a rejected request by signalling the tamper controller, which
resets the SoC, rather than by returning an error. Validation therefore happens
in software before the call: getting it wrong reboots the device.

mcuxClEls_WaitForOperation() busy-spins with no timeout, and would do so
holding that lock. Completion is taken from the ELS interrupt instead, wired
with IRQ_CONNECT because the IRQ has no devicetree node, with polling as the
fallback for ISR context and for anything before the IRQ is armed. A late
interrupt degrades to the synchronous wait rather than cancelling: measured,
MCUXCLELS_RESET_CANCEL on an in-flight operation reboots the SoC.

For SHA-256 the engine round-trips its intermediate state (hashoe writes it,
hashld reloads it), so the state lives in the caller's wc_Sha256 and the lock
covers one call. Holding it from update to final deadlocks TLS 1.3, which keeps
several transcript hashes alive at once. ELS never pads, so the port carries the
residual block and appends the padding itself.

Registration happens inside wolfCrypt_Init(), which is also what zeroes the
callback device table. Zephyr gains CONFIG_WOLFSSL_SYS_INIT to run that at boot
from POST_KERNEL rather than leaving it to whichever library call happens to be
first, which starts to matter once initialization brings up a peripheral. It
defaults on when the port is enabled and stays available on its own, since
nothing about it is specific to this hardware. The hook calls wolfSSL_Init() so
the TLS layer is initialized too, and wolfCrypt_Init() in a WOLFCRYPT_ONLY
build, where wolfSSL_Init() is not compiled.

The port's Kconfig symbol is restricted to SOC_SERIES_RW6XX, the only series for
which els_pkc exports src/platforms/<soc>, where mcux_els.h lives.
ELS serves SHA-2 in all four widths. SHA-384 and SHA-512 share the engine's
512-bit state and 1024-bit block, so they differ from SHA-256 only in the mode
selector, a 128-bit padding length field, and SHA-384 taking the leading 48
bytes of the final state.

HMAC benefits without an arm of its own: HmacKeyInitHash passes the Hmac's devId
to the inner hash, so HMAC-SHA384, HMAC-SHA512, HKDF and the SHA-384 TLS PRF all
compress on the engine.

The ELS HMAC command stays unused. It is one shot, with no equivalent of the
hash command's state load, while the wolfCrypt callback is incremental, so
serving it would mean buffering whole messages on parts routinely built without
an allocator.
Adds the AES cipher modes, and with them the slot reference that lets a
wolfCrypt key name a key living inside the ELS key store.

A callback sees only key->id[], its length and the devId, so the reference is
self-describing and travels as the key's id blob: sixteen bytes of magic,
version, key class, slot, flags and eight bytes binding it to a public point.
wolfPSA stores the same bytes for a vendor-location key. Fields are never
redefined; the version bumps and the format appends. Each key class maps onto
exactly one ELS permission bit and one entry point, and every entry point reads
the slot's properties back and checks that permission before issuing a command,
because a permission violation resets the part instead of returning an error.

AES-192 and any trailing partial block decline to software: ELS knows only 128-
and 256-bit keys and only whole blocks.

CBC chaining is maintained here. The documentation says ELS "will always read
and write" pIV, but it only reads it, so aes->reg still held the original IV
after a one-block encrypt: correct for a single call and wrong from the second
onward. The next IV is the last ciphertext block either way, saved before an
in-place decrypt overwrites it. cphsie/cphsoe are set only for CTR; for CBC they
are documented as ignored, but setting them made ELS treat pIV as an internal
state blob and produce output that was self-consistent yet disagreed with
software.

The ECC classes include a key generation seed, the ukgsrc permission, for a
P-256 key that arrives as its scalar and becomes a key pair only once the engine
generates from it. WC_ELSPKC_KEY_MAX names the last class, so the bounds checks
on building and parsing a reference stay as they are when a class is added.
The whole Init/UpdateAad/UpdateData/Finalize sequence runs under one lock, which
it can because the callback hands over the entire message at once.

Every stage consumes whole blocks: the final partial data block is zero-padded
with msgendw carrying its real byte count, and the AAD length reaches the
hardware only through Finalize. An exact block multiple needs no padding and so
comes out correct either way, which means a test exercising only 16, 32 and 64
bytes passes against a port that is wrong for every other length.

J0 is IV || 0x00000001, the single-block form Aead_Init takes; other IV lengths
need the GHASH-based derivation through Aead_PartialInit and are declined.

Finalize outputs the expected tag on decrypt rather than checking it, so the
comparison is done here in constant time, and a mismatch clears the plaintext so
a caller that ignores the return value is not handed unauthenticated data.
The input must arrive already padded to a whole block per SP 800-38B, 0x80 then
zeros, while inputLength stays the true pre-padding count, because that count is
what selects subkey K1 or K2.

Only the last block needs holding back, since only it takes the other subkey, so
everything before it goes in a single command. Every ELS command costs a fixed
~8us for AES, measured by sweeping buffer size on frdm_rw612, so a command per
16-byte block is what throughput is actually made of: AES-128-CMAC over 1KB runs
at 7.2 MiB/s this way against 775 KiB/s one block at a time.

pMac is [in, out] and carries the intermediate state, so it lives in the caller's
Cmac rather than in a pool.
Serves WC_ALGO_TYPE_SEED as well as WC_ALGO_TYPE_RNG, so wolfCrypt's own
Hash-DRBG is seeded from the hardware rather than only the direct generate path
being accelerated.

The engine takes at least four bytes and only whole words. With the driver's
parameter checks compiled out, a sub-word length would program the DMA past the
end of the caller's buffer, and odd sizes are ordinary (wc_RNG_GenerateByte), so
the tail comes from a word-sized scratch.
The port was reachable only through the Zephyr module, on the assumption that
NXP's els_pkc is a Zephyr artifact. It is not: els_pkc is NXP's CLNS middleware,
shipped as an MCUXpresso SDK component that additionally carries a Zephyr module,
so a project using the SDK directly should be able to build the port too.

Add --with-els-pkc=DIR alongside --with-mcux-sdk=DIR for the SoC device headers
its platform layer includes, and --with-els-pkc-platform=SOC defaulting to rw61x.
The component include directories are globbed rather than enumerated: the list
runs to roughly fifty entries and moves with each vendor release.

The port states its prerequisites as #error in els_pkc_port.h, so promote
WOLF_CRYPTO_CB_COPY and WOLF_CRYPTO_CB_FREE here the way --enable-rtl8735b does.
A wrong or missing path is diagnosed at configure time, naming the option, rather
than several hundred lines into the build with a missing mcuxClEls.h.

The vendor library stays link-only, so the archive carries unresolved mcuxCl*
references by design, as the SE050 port does.
An IDE/MCUEXPRESSO/RW612 directory with a known-good user_settings.h and the
steps to build the port against the SDK rather than Zephyr, plus the port
README's pointer to it. The README itself grew alongside the offloads it
documents.
Nothing committed exercised the port through wolfCrypt's own suites. Give both
samples an frdm_rw612 board overlay that turns the offload on; settings.h already
maps WC_USE_DEVID at WOLFSSL_ELS_PKC_DEVID, which is what the unmodified test and
benchmark read, and defining it also defines BENCH_DEVID, so the benchmark
measures every algorithm twice and labels the rows HW and SW.

Both suites pass on frdm_rw612.
WOLFSSL_ELS_PKC depended on !WOLFSSL_HAS_SETTINGS_FILE, which read the option
as nothing but a writer of user_settings.h. It does two other things a
settings file cannot express: select MCUX_ELS_PKC, which puts the vendor
headers on the include path, and select WOLFSSL_SYS_INIT, which brings the
subsystem up at boot. Drop the dependency so a settings-file build can turn
the port on, and keep the crypto-affecting defines out of that build, since
injecting WOLF_CRYPTO_CB behind the user's back changes struct layouts. A
settings file that leaves WOLFSSL_ELS_PKC or WOLF_CRYPTO_CB undefined now fails
the build instead of silently compiling the port to nothing.
@Frauschi

Frauschi commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

@wolfSSL-Fenrir-bot review max

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #11703

Scan targets checked: wolfcrypt-src, wolfcrypt-bugs, wolfcrypt-port-bugs, wolfssl-src, wolfssl-bugs
Coverage: 5 of 5 in-scope changed file(s) opened by the reviewer

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

Review tier: Max

@Frauschi Frauschi assigned wolfSSL-Bot and unassigned Frauschi Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants