Skip to content

Commit b1efa6a

Browse files
committed
Add STM32 CCB and STM32C5 HW PKA ECDSA support
1 parent b21a49a commit b1efa6a

6 files changed

Lines changed: 1521 additions & 20 deletions

File tree

.wolfssl_known_macro_extras

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,13 +520,15 @@ QAT_ENABLE_RNG
520520
QAT_USE_POLLING_CHECK
521521
RCC_AHB1ENR_PKAEN
522522
RCC_AHB2ENR1_AESEN
523+
RCC_AHB2ENR1_CCBEN
523524
RCC_AHB2ENR1_HASHEN
524525
RCC_AHB2ENR1_PKAEN
525526
RCC_AHB2ENR1_SAESEN
526527
RCC_AHB2ENR_AESEN
527528
RCC_AHB2ENR_HASHEN
528529
RCC_AHB2ENR_PKAEN
529530
RCC_AHB2ENR_SAESEN
531+
RCC_AHB2RSTR1_CCBRST
530532
RCC_AHB2RSTR_PKARST
531533
RCC_AHB3ENR_AESEN
532534
RCC_AHB3ENR_CRYPEN
@@ -985,6 +987,7 @@ WOLFSSL_STM32C5
985987
WOLFSSL_STM32F3
986988
WOLFSSL_STM32F427_RNG
987989
WOLFSSL_STM32U0
990+
WOLFSSL_STM32_CCB
988991
WOLFSSL_STM32_DHUK_UNWRAP
989992
WOLFSSL_STM32_USE_SAES
990993
WOLFSSL_STRONGEST_HASH_SIG

wolfcrypt/src/ecc.c

Lines changed: 178 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,9 @@ ECC Curve Sizes:
246246
#include <wolfssl/wolfcrypt/port/nxp/ksdk_port.h>
247247
#endif
248248

249-
#if defined(WOLFSSL_STM32_PKA)
249+
#if defined(WOLFSSL_STM32_PKA) || defined(WOLFSSL_STM32_CCB)
250+
/* CCB without PKA still needs stm32.h so its consistency #errors (e.g. CCB
251+
* requires DHUK + BARE/CUBEMX) are visible to this translation unit. */
250252
#include <wolfssl/wolfcrypt/port/st/stm32.h>
251253
#endif
252254

@@ -280,8 +282,15 @@ ECC Curve Sizes:
280282
!defined(WOLFSSL_CRYPTOCELL) && !defined(WOLFSSL_SILABS_SE_ACCEL) && \
281283
!defined(WOLFSSL_KCAPI_ECC) && !defined(WOLFSSL_SE050) && \
282284
!defined(WOLFSSL_XILINX_CRYPT_VERSAL) && \
283-
!defined(WOLFSSL_STM32_PKA) && \
285+
(!defined(WOLFSSL_STM32_PKA) || defined(WC_STM32_PKA_SIGN_ONLY)) && \
284286
!defined(WOLFSSL_PSOC6_CRYPTO)
287+
/* STM32 sign-only (e.g. C5): the HW PKA cannot run the integrated ECDSA
288+
* verify (mode 0x26) correctly, so use the SW verify helper rather than the
289+
* HW-accelerator sigRS path. Note the helper's scalar multiplications still
290+
* run on the HW PKA generic-mul (stm32.c provides wc_ecc_mulmod_ex under
291+
* !WC_STM32_PKA_VERIFY_ONLY) -- the C5 issue is specifically the verify-mode
292+
* wrapper, not the point math, which is exercised and correct (the lead for
293+
* a future real HW verify). */
285294
#undef HAVE_ECC_VERIFY_HELPER
286295
#define HAVE_ECC_VERIFY_HELPER
287296
#endif
@@ -7106,6 +7115,11 @@ int wc_ecc_import_wrapped_private(ecc_key* key, const byte* seed, word32 seedSz,
71067115
key->dhuk_wrapped_priv_len = wrappedLen;
71077116
key->dhuk_plain_priv_len = plainLen;
71087117
key->dhuk_seed_sz = seedSz;
7118+
#ifdef WOLFSSL_STM32_CCB
7119+
/* This is a DHUK seed-wrapped (non-CCB) scalar; clear any CCB blob
7120+
* routing left from a prior import so signing uses the DHUK path. */
7121+
key->dhuk_is_ccb = 0;
7122+
#endif
71097123
return 0;
71107124
}
71117125
#endif /* WOLFSSL_DHUK && WOLFSSL_STM32_BARE && WC_STM32_HAS_DHUK */
@@ -8099,6 +8113,163 @@ int wc_ecc_sign_set_k(const byte* k, word32 klen, ecc_key* key)
80998113
#endif /* WOLFSSL_ECDSA_SET_K || WOLFSSL_ECDSA_SET_K_ONE_LOOP */
81008114
#endif /* WOLFSSL_ATECC508A && WOLFSSL_CRYPTOCELL */
81018115

8116+
/* Guard must match the ecc.h prototype and the ccb_ / dhuk_ ecc_key struct
8117+
* members (both WOLFSSL_DHUK && WOLFSSL_STM32_CCB) -- the implementation must
8118+
* not be broader than the members it dereferences. */
8119+
#if defined(WOLFSSL_DHUK) && defined(WOLFSSL_STM32_CCB)
8120+
/* Load a previously provisioned device-protected ECDSA blob (wrapped scalar +
8121+
* AES-GCM iv/tag) and its public key onto the ecc_key. Sets the curve so the
8122+
* sign path can derive the parameters, and marks the key for the device
8123+
* crypto-callback. The caller enables the device with
8124+
* wc_ecc_init_ex(&key, heap, WC_DHUK_DEVID). Generic name -- the wrapping is
8125+
* the STM32 CCB, but the surface is not CCB-specific. */
8126+
int wc_ecc_import_wrapped_private_ex(ecc_key* key, int curve_id,
8127+
const byte* wrapped, word32 wrappedLen,
8128+
const byte* iv, word32 ivLen,
8129+
const byte* tag, word32 tagLen,
8130+
const byte* pub, word32 pubLen)
8131+
{
8132+
int modSz;
8133+
int ret;
8134+
8135+
if (key == NULL || wrapped == NULL || iv == NULL || tag == NULL ||
8136+
pub == NULL) {
8137+
return BAD_FUNC_ARG;
8138+
}
8139+
/* Fixed 16-byte AES-GCM iv/tag -- validate explicitly (no over-read). */
8140+
if (ivLen != sizeof(key->ccb_iv) || tagLen != sizeof(key->ccb_tag)) {
8141+
return BAD_FUNC_ARG;
8142+
}
8143+
modSz = wc_ecc_get_curve_size_from_id(curve_id);
8144+
if (modSz <= 0) {
8145+
return BAD_FUNC_ARG;
8146+
}
8147+
if (wrappedLen == 0u || wrappedLen > sizeof(key->dhuk_wrapped_priv)) {
8148+
return BAD_FUNC_ARG;
8149+
}
8150+
if (pubLen != (word32)(2 * modSz)) {
8151+
return BAD_FUNC_ARG;
8152+
}
8153+
/* Import public key (qx||qy) + set the curve (key->dp). */
8154+
ret = wc_ecc_import_unsigned(key, pub, pub + modSz, NULL, curve_id);
8155+
if (ret != 0) {
8156+
return ret;
8157+
}
8158+
XMEMCPY(key->dhuk_wrapped_priv, wrapped, wrappedLen);
8159+
key->dhuk_wrapped_priv_len = wrappedLen;
8160+
XMEMCPY(key->ccb_iv, iv, sizeof(key->ccb_iv));
8161+
XMEMCPY(key->ccb_tag, tag, sizeof(key->ccb_tag));
8162+
key->dhuk_is_ccb = 1;
8163+
/* Clear any DHUK seed-import state left from a prior import; the CCB
8164+
* path keys off the wrapped blob + iv/tag, not the seed. */
8165+
ForceZero(key->dhuk_seed, sizeof(key->dhuk_seed));
8166+
key->dhuk_seed_sz = 0;
8167+
key->dhuk_plain_priv_len = 0;
8168+
return 0;
8169+
}
8170+
8171+
/* Crypto-callback keygen handler (WC_PK_TYPE_EC_KEYGEN). Provisions a fresh
8172+
* device-protected key: generate a scalar, have the CCB wrap it into a
8173+
* device-bound blob and derive its public key, and store both on the ecc_key
8174+
* (the scalar is zeroized and never leaves this call / the hardware). The
8175+
* scalar is generated in software with the supplied rng on a throwaway key with
8176+
* no devId (so no callback recursion). Returns CRYPTOCB_UNAVAILABLE for curves
8177+
* the CCB cannot wrap so keygen falls back to software. Not a public entry
8178+
* point -- reached via wc_ecc_make_key() on a WC_DHUK_DEVID key. */
8179+
int wc_ecc_dev_make_key(WC_RNG* rng, int keysize, ecc_key* key, int curve_id)
8180+
{
8181+
#ifdef WOLFSSL_SMALL_STACK
8182+
ecc_key* tmp = NULL; /* ecc_key is ~1-2KB -- heap it on the
8183+
* small-stack embedded targets this port
8184+
* runs on (repo convention). */
8185+
#else
8186+
ecc_key tmp[1];
8187+
#endif
8188+
#ifdef WOLFSSL_SMALL_STACK
8189+
/* d || pub || wrapped in one heap scratch buffer (~326 B) -- keep the
8190+
* fixed byte buffers off the stack too, like tmp above. */
8191+
byte* scratch = NULL;
8192+
byte* d;
8193+
byte* pub; /* qx || qy, contiguous */
8194+
byte* wrapped;
8195+
#else
8196+
byte d[MAX_ECC_BYTES];
8197+
byte pub[2 * MAX_ECC_BYTES]; /* qx || qy, contiguous */
8198+
byte wrapped[96];
8199+
#endif
8200+
byte iv[16];
8201+
byte tag[16];
8202+
word32 dLen;
8203+
word32 wrappedSz = 0;
8204+
int modSz;
8205+
int ret;
8206+
int tmpInit = 0;
8207+
8208+
(void)keysize;
8209+
if (rng == NULL || key == NULL) {
8210+
return BAD_FUNC_ARG;
8211+
}
8212+
/* wc_ecc_set_curve() ran before the callback, so key->dp is resolved even
8213+
* when curve_id came in as the default. */
8214+
if (curve_id <= 0 && key->dp != NULL) {
8215+
curve_id = key->dp->id;
8216+
}
8217+
modSz = wc_ecc_get_curve_size_from_id(curve_id);
8218+
if (modSz <= 0 || (word32)modSz > MAX_ECC_BYTES) {
8219+
return CRYPTOCB_UNAVAILABLE; /* unsupported -> software keygen */
8220+
}
8221+
8222+
#ifdef WOLFSSL_SMALL_STACK
8223+
tmp = (ecc_key*)XMALLOC(sizeof(ecc_key), key->heap, DYNAMIC_TYPE_ECC);
8224+
if (tmp == NULL) {
8225+
return MEMORY_E;
8226+
}
8227+
scratch = (byte*)XMALLOC((3 * MAX_ECC_BYTES) + 96, key->heap,
8228+
DYNAMIC_TYPE_TMP_BUFFER);
8229+
if (scratch == NULL) {
8230+
XFREE(tmp, key->heap, DYNAMIC_TYPE_ECC);
8231+
return MEMORY_E;
8232+
}
8233+
d = scratch;
8234+
pub = scratch + MAX_ECC_BYTES;
8235+
wrapped = scratch + (3 * MAX_ECC_BYTES);
8236+
#endif
8237+
8238+
ret = wc_ecc_init_ex(tmp, key->heap, INVALID_DEVID);
8239+
if (ret == 0) {
8240+
tmpInit = 1;
8241+
ret = wc_ecc_make_key_ex(rng, modSz, tmp, curve_id);
8242+
}
8243+
if (ret == 0) {
8244+
dLen = (word32)modSz;
8245+
ret = wc_ecc_export_private_only(tmp, d, &dLen);
8246+
}
8247+
if (ret == 0) {
8248+
ret = wc_Stm32_Ccb_EccMakeBlob(curve_id, d, dLen, iv, tag, wrapped,
8249+
&wrappedSz, pub, pub + modSz);
8250+
if (ret != 0) {
8251+
ret = CRYPTOCB_UNAVAILABLE; /* curve not CCB-wrappable -> SW */
8252+
}
8253+
}
8254+
if (ret == 0) {
8255+
ret = wc_ecc_import_wrapped_private_ex(key, curve_id, wrapped, wrappedSz,
8256+
iv, (word32)sizeof(iv), tag,
8257+
(word32)sizeof(tag), pub,
8258+
(word32)(2 * modSz));
8259+
}
8260+
8261+
ForceZero(d, MAX_ECC_BYTES);
8262+
if (tmpInit) {
8263+
wc_ecc_free(tmp);
8264+
}
8265+
#ifdef WOLFSSL_SMALL_STACK
8266+
XFREE(scratch, key->heap, DYNAMIC_TYPE_TMP_BUFFER);
8267+
XFREE(tmp, key->heap, DYNAMIC_TYPE_ECC);
8268+
#endif
8269+
return ret;
8270+
}
8271+
#endif /* WOLFSSL_DHUK && WOLFSSL_STM32_CCB */
8272+
81028273
#endif /* !HAVE_ECC_SIGN */
81038274

81048275
#ifdef WOLFSSL_CUSTOM_CURVES
@@ -8945,7 +9116,7 @@ int wc_ecc_verify_hash(const byte* sig, word32 siglen, const byte* hash,
89459116

89469117
#ifndef WOLF_CRYPTO_CB_ONLY_ECC
89479118

8948-
#if !defined(WOLFSSL_STM32_PKA) && \
9119+
#if (!defined(WOLFSSL_STM32_PKA) || defined(WC_STM32_PKA_SIGN_ONLY)) && \
89499120
!defined(WOLFSSL_PSOC6_CRYPTO) && \
89509121
!defined(WOLF_CRYPTO_CB_ONLY_ECC)
89519122
static int wc_ecc_check_r_s_range(ecc_key* key, mp_int* r, mp_int* s)
@@ -9463,9 +9634,11 @@ static int ecc_verify_hash(mp_int *r, mp_int *s, const byte* hash,
94639634
int wc_ecc_verify_hash_ex(mp_int *r, mp_int *s, const byte* hash,
94649635
word32 hashlen, int* res, ecc_key* key)
94659636
{
9466-
#if defined(WOLFSSL_STM32_PKA)
9637+
#if defined(WOLFSSL_STM32_PKA) && !defined(WC_STM32_PKA_SIGN_ONLY)
94679638
/* HW ECDSA verify via STM32 PKA. Works under both the CubeMX-HAL
9468-
* and the bare-metal direct-register paths. Mirror the non-FIPS
9639+
* and the bare-metal direct-register paths. (Under WC_STM32_PKA_SIGN_ONLY
9640+
* -- e.g. STM32C5 -- verify falls through to the software body below.)
9641+
* Mirror the non-FIPS
94699642
* input-validation from the SW body below (length range, all-zero
94709643
* digest rejection) so HW + SW share the same input contract. */
94719644
#ifndef WC_ALLOW_ECC_ZERO_HASH

wolfcrypt/src/port/st/README.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Support for STM32 on-chip crypto hardware acceleration across the following fami
2222
| `WOLFSSL_STM32WBA` | WBA52 (TinyAES / HASH / RNG / SAES / V2 PKA / DHUK) |
2323
| `WOLFSSL_STM32WL` | WL55 (TinyAES / RNG / V1 PKA) |
2424
| `WOLFSSL_STM32C0` | C0xx (not yet supported in settings.h; SW only) |
25-
| `WOLFSSL_STM32C5` | C5xx (TinyAES / HASH / RNG / SAES / V2 PKA / DHUK) |
25+
| `WOLFSSL_STM32C5` | C5xx (TinyAES / HASH / RNG / SAES / V2 PKA sign-only / DHUK / CCB) |
2626
| `WOLFSSL_STM32N6` | N6xx (TinyAES / HASH / RNG / SAES / V2 PKA / DHUK; M55 core) |
2727
| `WOLFSSL_STM32MP13` | MP13 (CRYP / HASH / RNG / PKA; Cortex-A7) |
2828
| `WOLFSSL_STM32MP25` | MP25 (not yet supported in settings.h; Cortex-A35 + M33) |
@@ -144,6 +144,58 @@ ECDSA mirrors this: init the key with `wc_ecc_init_ex(&key, NULL, WC_DHUK_DEVID)
144144
`wc_Stm32_Aes_DhukOp[_ex]()` unwraps a previously DHUK-wrapped key into SAES KEYR and runs AES ECB/CBC with it (importing an externally-chosen key, vs deriving one from a seed). It is compiled only with `WOLFSSL_STM32_DHUK_UNWRAP`, is called explicitly (not auto-routed), and is not re-validated on current hardware.
145145
146146
147+
## STM32 CCB (Coupling and Chaining Bridge)
148+
149+
STM32U3 and STM32C5 silicon (e.g. U385 / C5A3; RM0487 ch 31 and RM0522, the `WC_STM32_HAS_CCB` gate) carry the CCB peripheral, which chains the PKA, SAES and RNG over a private local interconnect. This lets a DHUK-protected ECDSA private scalar be unwrapped by the SAES and consumed by the PKA entirely in hardware -- the scalar never crosses the system bus or enters software, not even into a short-lived buffer (unlike the generic DHUK ECDSA path above, which decrypts the scalar into a stack buffer). CCB builds on DHUK: the private key is held as a chip-bound AES-GCM blob (`iv` / `tag` / wrapped scalar) created under the silicon DHUK.
150+
151+
CCB is supported on both build paths: the bare-metal direct-register OPSTEP driver (`WOLFSSL_STM32_BARE`) and the CubeMX/HAL path (`WOLFSSL_STM32_CUBEMX`, via ST's `HAL_CCB_*` driver). It currently covers ECDSA over P-256.
152+
153+
### Enabling
154+
155+
```
156+
#define WOLFSSL_DHUK /* CCB is a DHUK feature */
157+
#define WOLF_CRYPTO_CB /* required -- transparent sign routes through crypto callbacks */
158+
#define WOLFSSL_STM32_CCB /* opt in to the CCB-protected ECDSA path */
159+
```
160+
161+
`WOLFSSL_STM32_CCB` requires CCB silicon (`WOLFSSL_STM32U3` or `WOLFSSL_STM32C5`) and either `WOLFSSL_STM32_BARE` or `WOLFSSL_STM32_CUBEMX` (a `#error` fires otherwise).
162+
163+
### API
164+
165+
The whole flow uses the **standard ECC API** -- there is no CCB-specific public API. Binding the key to `WC_DHUK_DEVID` routes keygen and sign through the STM32 crypto callback, which provisions and uses the CCB-protected key transparently (a drop-in for TLS and other consumers). The same flow works on both build paths.
166+
167+
```c
168+
ecc_key key;
169+
170+
/* one-time: register the STM32 DHUK/CCB crypto-callback device */
171+
wc_Stm32_DhukRegister(WC_DHUK_DEVID);
172+
173+
wc_ecc_init_ex(&key, NULL, WC_DHUK_DEVID);
174+
175+
/* provision a fresh device-bound key with the STANDARD keygen -- the crypto
176+
* callback intercepts it: the CCB generates the scalar, wraps it into a blob
177+
* and derives the public key, all in hardware. No CCB-specific API. */
178+
wc_ecc_make_key_ex(&rng, 32, &key, ECC_SECP256R1);
179+
180+
/* transparent sign -- the scalar is unwrapped SAES->PKA in HW and signed */
181+
wc_ecc_sign_hash(hash, hashLen, sig, &sigLen, &rng, &key);
182+
183+
/* verify with the in-clear public key, unchanged */
184+
wc_ecc_verify_hash(sig, sigLen, hash, hashLen, &verified, &key);
185+
186+
wc_ecc_free(&key);
187+
wc_Stm32_DhukUnRegister(WC_DHUK_DEVID);
188+
```
189+
190+
To reuse a key across resets, persist the blob from a provisioned key and reload it later with `wc_ecc_import_wrapped_private_ex(&key, curve_id, wrapped, wrappedLen, iv, tag, pub, pubLen)` (the public key in uncompressed `qx||qy` form), then sign as above. Both paths set `key->dhuk_is_ccb` and the device `devId`, so dispatch to the CCB happens automatically inside the crypto callback.
191+
192+
### Current state
193+
194+
- Validated on STM32U385 (NUCLEO-U385RG-Q, TZEN=0), P-256, on both the bare-metal and CubeMX/HAL build paths: `wc_ecc_make_key` -> `wc_ecc_sign_hash` -> `wc_ecc_verify_hash` round-trips, with the private scalar never present in software.
195+
- `Stm32Ccb_Init()` pulse-resets the PKA / SAES / RNG before each operation, so the first CCB op is robust even when prior standalone crypto (RNG seeding, ECC keygen) left an engine in a state that would otherwise stall the CCB's chained SAES GCM step. The family-specific reset register name is abstracted (`WC_STM32_CCB_RSTR`).
196+
- CCB requires the U3 at its full clock; the reference clock-tree bring-up (96 MHz) is in the bare example's `boards/u3/hw_init.c`.
197+
198+
147199
## STM32 BARE-metal port
148200

149201
`WOLFSSL_STM32_BARE` selects a direct-register integration with zero HAL or StdPeriLib dependency. Use this for:

0 commit comments

Comments
 (0)