Repository navigation
Conversation
|
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The RSA accelerator path accepts unsupported operation types and rejects raw decrypt inputs that the software API allows, breaking compatibility with existing behavior.
2 open issues
What changed in this PR
Extends the RW612 EdgeLock crypto callback port introduced in #11703 with public-key acceleration and hardware key-store support.
Changes:
- Adds RSA, X25519 and ECDSA offload through PKC and ELS.
- Adds P-256 slot-key generation, signing, validation and wrapped-key management.
- Updates build settings, Zephyr sample configurations and integration documentation.
| File | Description |
|---|---|
zephyr/user_settings.h |
Enables and checks the required key-store callback define. |
zephyr/samples/wolfssl_test/sample.yaml |
Enables additional offloaded curves for RW612 tests. |
zephyr/samples/wolfssl_benchmark/sample.yaml |
Enables additional offloaded curves for benchmarks. |
zephyr/Kconfig |
Documents required settings-file defines. |
wolfssl/wolfcrypt/port/nxp/els_pkc_port.h |
Declares slot-key APIs and clarifies reference fields. |
wolfcrypt/src/port/nxp/README.md |
Documents acceleration, slot permissions and key-store behavior. |
wolfcrypt/src/port/nxp/els_pkc_port.c |
Implements public-key offload, key-store operations and PKC lifecycle handling. |
IDE/MCUEXPRESSO/RW612/user_settings.h |
Enables key-store callbacks, ECC, X25519 and RSA settings. |
IDE/MCUEXPRESSO/RW612/README.md |
Updates build instructions and slot-key guidance. |
configure.ac |
Enables key-store callbacks with the EdgeLock port. |
.wolfssl_known_macro_extras |
Registers the vendor internal-public-key feature macro. |
Review effort: Balanced
The three P-256 operations that can use a slot reference: in-slot generation, signing and verification. Key generation asks for as little as it can. The usage bit, the key size and the slot kind are set by the engine from kgtypedh, and a property word ELS disagrees with is a tamper event rather than an error return, so only the two access-protection bits are passed, plus wrpok when the reference asks for an exportable key. The properties are read back before first use. Verification carries the sharpest trap in this port: ELS does not report a bad signature through its return status. It recomputes R and hands it back, and the signature is valid only if that equals the R the caller supplied. A port that trusted the status would pass every positive test and accept every forgery, so the comparison is done here, in constant time. An all-zero digest is declined. wolfCrypt rejects one too, but below the callback, so an offload would otherwise accept what software refuses. ECDH is absent by design. mcuxClEls_EccKeyExchange_Async deposits the shared secret in a slot, which cannot be read back in the clear, while the ecdh callback must hand a buffer back. An in-slot agreement would need a key store operation, which this port does not provide. A generated key stays typed ECC_PUBLICKEY, as wc_ecc_import_unsigned() leaves it: the private half is in the slot, so export and software ECDH refuse the key instead of using the zero scalar its ecc_key holds. For the same reason key generation does not decline once a slot reference parses: a wrong class or curve is BAD_STATE_E. wolfCrypt already refuses a software fallback for a key with an id, but not under WOLF_CRYPTO_CB_KEYGEN_SW_FALLBACK, where a decline would let software fill the key while its id still names the slot. Generation also needs a free pair of general purpose slots. Slots 0 and 1 are retention and slot 2 is hardware-out, and the key store refuses them as targets for the same reason. ELS speaks raw X9.62 throughout, points as X||Y and signatures as R||S, while the callback boundary is DER, so the conversion happens here. A P-256 key can also arrive as a key generation seed, which is how EdgeLock 2GO delivers one. The seed is the private scalar, and ELS will not sign with it until a deterministic key generation has turned it into a key pair in the same slots. wc_ElsPkc_EccKeyGenFromSeed() takes a reference to the seed, runs that generation, binds the ecc_key to the result and loads the public point, since that is again the only moment the hardware gives it out. It refuses a slot that does not hold a whole 256-bit seed before issuing anything. The engine sets the generated key's properties itself, and marks it wrap-exportable whatever the seed carried. The reference format reserves bytes 8 to 15 for a binding to the public point, but nothing computes or checks one, and ELS cannot hand out a stored key's public point to check it against. Until that exists, wc_ElsPkc_MakeKeyRef() and wc_ElsPkc_ParseKeyRef() refuse the BIND flag, so the field stays zero instead of carrying a value nobody verifies. wc_ecc_check_key() is what shows that a slot still holds the key a reference was made for. Every slot path requires the reference to name the first slot of a key over 128 bits, and every ECC key is one; the engine marks that slot with its base-slot property bit, so a reference to the second slot of a pair is refused before any command is issued. Verification range-checks r and s itself, since wolfCrypt does so only on its software path, and fails with the MP_ZERO_E or MP_VAL software would give. ELS answers an off-curve public key with a mismatched R, so that fails as it does in software. Verification declines a DER signature with a negative r or s, or with bytes after its SEQUENCE, both of which software rejects (the latter unless NO_STRICT_ECDSA_LEN), and reads the public point straight from the key, so it needs no key export support. If the public point cannot be loaded after an in-slot generation, the pair is deleted again so a retry finds it free, and a build without key import refuses a slot key's generation with NOT_COMPILED_IN.
Adds the second engine on this part. Where ELS is a fixed-function block driven by a single async command, the PKC is a general modular-arithmetic coprocessor reached through a session owning two workareas. Only the CPU half is the caller's to allocate. The PKC half is a fixed hardware region that mcuxClSession_init() takes as a pointer, which makes it look like memory the caller chose. That shared region is also why PKC work runs under the same lock as ELS: two concurrent operations would corrupt each other's intermediates rather than merely race. The session uses mcuxClRandomModes_Mode_ELS_Drbg, which carries 128-bit strength and is instantiated in microseconds, and the full SP800-90A CTR_DRBG only for RSA-4096 and curves over 256 bits, which is the split the vendor's own PSA driver makes. DRG.3 costs about 90ms to instantiate, so one session serves every PKC operation: it is opened on first use, the CTR_DRBG is instantiated the first time it is needed and then kept, and mcuxClSession_setRandom() picks the DRBG per operation. Each operation ends with mcuxClSession_cleanup(), which wipes the work areas. Some CLNS error exits keep their work area claimed (the MontDH small-subgroup exit, for one), so the session is torn down after any operation that leaves more claimed than it found, and after an engine failure; wc_ElsPkc_Cleanup() closes it, and init forgets any session a failed cleanup left behind, since the PKC reset invalidates it. Tearing a session down also resets the PKC, because the same error exits leave it mid-operation, and init skips that reset while the port is already up, where it would wedge an operation in flight. RSA is claimed at the raw primitive, since the callback sits in wc_RsaFunction_ex beneath all padding: NoVerify is RSAVP1 and NoEncode is RSASP1. That is also the well-formed corner of the vendor API, as every encrypt and decrypt workarea macro on this part still carries an unsubstituted template placeholder. CLNS takes a CRT key's modulus length as bits(p) + bits(q), and its raw private operation assumes a modulus of whole bytes. A modulus that is not a multiple of 8 bits is therefore declined; on the board an RSA-2047 key gave a wrong private result. A private operation uses CRT only when every CRT member fits in half the modulus and bits(p) + bits(q) equals bits(n), and any other key takes the plain form. A shorter p is fine: the engine reads it correctly from its zero-padded entry, as a 2040-bit key built from 1016-bit and 1024-bit primes shows. The raw public operation reports VERIFYPRIMITIVE_OK, not VERIFY_OK; the latter belongs to the padded modes. Accepting only VERIFY_OK rejects every successful RSAVP1. wolfCrypt bounds a decrypt input to 1 < c < n - 1 only on its software path, and there only for its own padded callers, which the callback cannot tell apart from a raw wc_RsaFunction() call. The port therefore runs RsaFunctionCheckIn() and declines an input that fails it, so software applies its own rule: RSA_OUT_OF_RANGE_E for a padded caller, a result for a raw call. An unknown operation type gives RSA_WRONG_TYPE_E, as in software, instead of running as a public operation. ELS_PowerDownWakeupInit() leaves the PKC alone, so init also runs PKC_PowerDownWakeupInit(), which clocks and resets the PKC and clears its RAM. Without it the port relies on the boot ROM having left the PKC clocked, which does not hold after a power-down. A session whose DRBG fails to come up is torn down without mcuxClRandom_uninit(), which would read the mode that the failed init never set. Nothing here is vaulted: the PKC tier accelerates keys in ordinary memory, which is precisely what the ELS key store cannot hold. The public path copies the exponent into the room the scratch block leaves after the modulus, so an exponent longer than the modulus is declined instead of overrunning it. Where WOLF_CRYPTO_CB_ONLY_RSA or WOLFSSL_NO_RSA_SW compiles RsaFunctionCheckIn() out, the port applies the same 1 < c < n - 1 bound itself, since no software path would. An even modulus is declined, so software rejects it with MP_VAL as it does on its own path.
The agreement only. Key generation stays in software so the private key keeps whatever provenance wolfCrypt gave it, and the agreement is where the scalar multiplication, and so all of the time, actually is. A big-endian request is declined rather than byte-swapped behind the caller. The engine performs the RFC 7748 all-zero check itself and reports a small-order peer key as ERROR_SMALL_SUBGROUP, which maps to ECC_OUT_OF_RANGE_E, the error software returns for it. CLNS reports a fault it detects during the multiplication through the same status, so such a fault surfaces as ECC_OUT_OF_RANGE_E as well. EdDSA is not claimed because it cannot be: the Ed25519 entry points are present as symbols but their Twisted-Edwards internals are compiled out for this part, so a build that calls them fails to link, and Ed448 has no domain parameters at all. Symbol presence is not capability here; only a link test settles it.
The complement to the ELS tier, which verifies only on P-256 and signs only with a P-256 key that lives in a slot. The two do not overlap, so the dispatch tries ELS first and falls through on CRYPTOCB_UNAVAILABLE, which is exactly the set ELS declines. Unlike ELS, the PKC has no built-in notion of a curve: a, b, p, G and n arrive as octet strings on every call. wolfCrypt keeps those as hex text, so they are converted into scratch held under the existing lock rather than onto the stack, which would otherwise cost around half a kilobyte per signature. The PKC draws its own nonce, so an RFC 6979 or wc_ecc_sign_set_k() signature is declined and stays in software. secp160r1 and secp160r2 also stay in software: their order is a byte longer than the prime, and the domain parameters are carried at the prime's width. The PKC verify path applies the same DER checks to the signature as the ELS one. The converted constants of a curve from wolfCrypt's own table are kept for the next operation on it, so the hex parsing runs once per curve change rather than once per signature, inside the lock. A caller-supplied curve set may live anywhere and is converted every time, and the precomputed point derived for a custom curve is keyed on p, a, b and G together, since every explicit-parameter curve reports ECC_CURVE_CUSTOM. An off-curve public key makes mcuxClEcc_Verify() return INVALID_PARAMS, which is declined so software gives its own answer, instead of being reported as a hardware failure, and the same status from the generator precomputation is declined too. As on ELS, r and s are range-checked against the curve order with software's MP_ZERO_E and MP_VAL.
Wires WC_ALGO_TYPE_KEYSTORE to EdgeLock: import a wrapped blob straight into a slot, wrap a stored key back out, derive one slot from another, delete, and report what a slot holds. None of these can be expressed by an algorithm callback, since import and export carry a container that never becomes plaintext on this side and derive moves a key slot to slot without it entering memory. Validation matters more here than anywhere else in the port: these calls name two slots rather than one, and a permission mistake resets the SoC. Delete checks the slot is occupied first for the same reason. The wrapping key's two permissions are not interchangeable. ukwk both wraps and unwraps while ukuok only unwraps, so an export demands more than an import does, which is the entire point of an unwrap-only key. A derived wrapping key is given ukwk and nothing else. Exportability is decided at creation and cannot be added afterwards, so a key made without wrpok is refused an export rather than attempted. Only the vendor container is accepted: a bare RFC 3394 blob has no property word for ELS to set the imported key's permissions from. Slots 0 and 1 are retention and slot 2 is hardware-out, holding the die key and the flash encryption key, so an ordinary key is required to name a general purpose slot, both for reserve and delete and as an import or derive target. An ECC key or seed always takes a slot pair. An imported key's permissions come from the property word inside the container, which is only readable after the unwrap. Import therefore checks the new key against the target reference's class afterwards and deletes it again on a mismatch. Derive refuses an ECC key pair class as its target, since those only come out of key generation, refuses a seed under 256 bits, and accepts only WC_KDF_TYPE_NONE: the engine runs the SP800-108 CMAC KDF, and WC_KDF_TYPE_TWOSTEP_CMAC names the different SP 800-56C two-step construction. ELS feeds the requested property word into every derivation, so the same parent and derivation data give a different key when the protection bits or wrpok differ. The derive path asks for the same bits NXP's own key chains do, which is what lets an application reproduce keys like the EdgeLock 2GO import keys. A wrap-capable key may only be derived from the die unique key. From any derived parent the engine trips the tamper controller, measured on hardware, so derive refuses that case up front. An unwrap-only key derives from any source. The README records both rules and what an EdgeLock 2GO import needs from the port. Export, delete, GetInfo and the wrapping key also require a base slot, and delete, export and GetInfo also require the slot to carry the permission of the reference's class, so a stale reference cannot destroy or export a key that has since reused its slot. An ECC import needs a 256-bit container, and a seed target derives at 256 bits when keySz is 0. A 256-bit target that would run past the last slot gives BAD_FUNC_ARG, the same as key generation, and so does an export whose wrapping key is the key being exported.
Importing an RFC 3394 key blob needs a wrapping key the application had to arrange for itself, which is awkward: the blob is wrapped at provisioning time under NXP_DIE_KEK_SK, and the boot ROM deletes its own copy when SB file loading finishes, so nothing is resident by the time the application runs. It does not have to be. NXP_DIE_KEK_SK is a CKDF child of the die master key and NXP publishes both the parent and the twelve byte derivation constant, so wc_ElsPkc_DeriveDieKek() derives a fresh copy into a free slot pair and hands back a reference. Import against that reference and delete the slot afterwards. Slot 0 has to hold an active key with CKDF and DUK before anything is derived from it, since deriving from an unfit parent resets the SoC instead of failing. What is not verified is that this is the same key the provisioning tooling used, which would need a blob programmed into OTP on a provisioned part. The header says so rather than implying more than was tested. Slot 0 holds NXP_DIE_INT_MK_SK, and the macro naming it now says so. The header points at the README for the EdgeLock 2GO import keys, which descend from a different root and are not this key. Now that slot 0 has a name, derive's rule that only the die unique key may parent a wrap-capable KWK checks the slot as well as the DUK property bit, so another ROM key carrying DUK cannot reach the derivation that would trip the tamper controller.
An ecc_key built by wc_ecc_init_id() carries a key id and nothing else: no curve, because an id says nothing about one. ElsEccCurveOk() required key->dp->id == ECC_SECP256R1, so the port declined every signature from such a key, the operation fell through to the PKC path that has no private scalar, and the handshake failed with ECC_BAD_ARG_E. That is the path wolfSSL_CTX_use_PrivateKey_Id() takes, so an application could reference a slot key from TLS but never sign with it. ElsEccPinCurve() now pins P-256 when the key has no curve, which the key reference already implies, the same way wolfPKCS11's Pkcs11GetEccParams() fills the curve in from the token. Once the key reference parses, nothing declines: the PKC and software paths would sign with the zero scalar the ecc_key holds, so a wrong key class, a curve other than P-256, a curve-table miss or an RFC 6979 or set-k request, which ELS cannot honour, is returned as an error. ElsEccSign() and ElsEccVerify() also rejected any digest that was not exactly 32 bytes, on the grounds that padding or truncating would be guesswork. It is not: FIPS 186-4 6.4 and RFC 6979 bits2int take the leftmost min(N, outlen) bits, which over a 256-bit order is a plain byte truncation with no bit shift, and ecc.c does exactly that at its own truncation sites. ElsEccDigest() now truncates a longer digest and zero-extends a shorter one, so SHA-384 and SHA-512 ECDSA reach the hardware instead of being declined. Both functions gain a single exit, so the error returns after the digest buffer is populated reach the ForceZero the success path already performed.
The MCUXpresso user_settings.h and README now turn on ECC, X25519 and RSA with the SP math they need and ECC timing resistance, and the key store operations, and the autoconf example passes the matching options. The frdm_rw612 test and benchmark scenarios add P-384, P-521 and X25519, the curves the PKC offloads beyond the module's default P-256.
A P-256 key generated in or referenced from an ELS slot holds no scalar in its ecc_key, so the software check says nothing about the slot, and a key bound with wc_ElsPkc_EccUseSlot() has no public point either and fails it outright. The port now answers WC_PK_TYPE_EC_CHECK_PUB_KEY for keys that carry a slot reference. A public point, when the key has one, goes through wc_ecc_point_is_on_curve(), which also bounds both coordinates below p; P-256 has cofactor 1, so that covers the order check as well. With checkPriv, the slot has to be occupied with the usage bit its class claims, and a signing slot with a public point also signs a fixed digest and verifies the result against the public point. That is the only way to show the slot and the point belong together, since ELS cannot recompute the public key of a stored key. A key agreement slot cannot sign and gets the slot check alone, at the cost of one ELS sign and one verify for a signing slot. The self-test signs whatever deterministic or set-k state the key is in, since that state concerns the caller's signatures, not the test's. With no public point and no checkPriv the slot has nothing to answer, so that case goes back to software. If the self-test cannot run at all, the check fails with ECC_PRIV_KEY_E instead of declining, since a decline would let wc_ecc_check_key() fall back to a software check that cannot see the slot. Keys without a slot reference still go to software.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
Second of three PRs for the NXP RW612 EdgeLock port (
WOLFSSL_ELS_PKC), following #11703. The third adds a CI workflow under the m33mu emulator.WC_ALGO_TYPE_KEYSTOREoperations, pluswc_ElsPkc_DeriveDieKek(), so an RFC 3394 blob can be imported without a provisioned wrapping key.wolfSSL_CTX_use_PrivateKey_Id()signs on ELS, andwc_ecc_check_key()checks a slot key against its slot.Anything the hardware does not serve still runs in software. Each commit message carries the reasoning for its part.
Design notes
ecc_keyholds no scalar, so a request ELS cannot serve is an error.Building
WOLF_CRYPTO_CB_KEYSTOREis now required alongsideWOLFSSL_ELS_PKCandWOLF_CRYPTO_CB; Zephyr and--with-els-pkcset it. The MCUXpressouser_settings.hand the frdm_rw612 test and benchmark scenarios turn on the offloaded curves and RSA.Testing
-Werrorin 12 configurations (RSA, ECC sign/verify/import/export and curve variants).Benchmark
frdm_rw612, Zephyr 4.4, one run of about a second per row. Software is the same build with the port off.