Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion .github/workflows/ti-c2000-compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,51 @@ jobs:
# even though the C28x itself is little-endian. Those arms are
# otherwise unreachable in CI, which is how a WIDE_BYTE arm
# referencing a LITTLE_ENDIAN_ORDER-only local once went unnoticed.
sp_c32_octet_masks:
# sp_c32.c is generated. Two CHAR_BIT != 8 fixes live in it and have been
# dropped by a regeneration before, which breaks ECDSA/ECDH/RSA/DH on
# 16-bit-byte targets while every 8-bit build stays green. Fail loudly.
name: sp_c32.c CHAR_BIT != 8 masks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Octet masks present in every sp_*_to_bin_*
# Counting occurrences alone would pass a partial regression: if one
# serializer lost both masks the counts would simply fall together.
# Tie them to the number of generated functions instead, so a dropped
# mask or a newly added unmasked serializer both fail.
run: |
defs=$(grep -cE '^static (void|int) sp_[0-9]+_to_bin_[0-9]+\(' \
wolfcrypt/src/sp_c32.c || true)
n=$(grep -c 'a\[j--\] |= (byte)(((sp_uint32)r\[i\] << s) & 0xFF)' \
Comment thread
dgarske marked this conversation as resolved.
wolfcrypt/src/sp_c32.c || true)
m=$(grep -c 'a\[j--\] = (byte)((r\[i\] >> b) & 0xFF)' \
wolfcrypt/src/sp_c32.c || true)
echo "to_bin functions: $defs shift-left: $n shift-right: $m"
if [ "$defs" -eq 0 ] || [ "$n" -ne "$defs" ] || [ "$m" -ne "$defs" ]; then
echo "::error::sp_c32.c: every sp_*_to_bin_* must carry both & 0xFF"
echo "octet masks - expected $defs of each, found $n and $m."
echo "Regenerate with the fixed sp/conv.rb templates"
echo "(wolfSSL/scripts)."
exit 1
fi
- name: CHAR_BIT used in every sp_*_from_mp constant-time mask
run: |
defs=$(grep -cE '^static (void|int) sp_[0-9]+_from_mp\(' \
wolfcrypt/src/sp_c32.c || true)
n=$(grep -c 'sizeof(mp_digit) \* CHAR_BIT - 1' \
wolfcrypt/src/sp_c32.c || true)
echo "from_mp functions: $defs CHAR_BIT masks: $n"
if grep -q 'sizeof(mp_digit) \* 8 - 1' wolfcrypt/src/sp_c32.c; then
echo "::error::sp_c32.c uses sizeof(mp_digit) * 8; must be CHAR_BIT"
exit 1
fi
if [ "$defs" -eq 0 ] || [ "$n" -ne "$defs" ]; then
echo "::error::sp_c32.c: every sp_*_from_mp must use"
echo "sizeof(mp_digit) * CHAR_BIT - expected $defs, found $n."
exit 1
fi

wide_byte_be_compile:
name: WIDE_BYTE + big-endian compile guard
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
Expand All @@ -169,7 +214,7 @@ jobs:
- name: Syntax-check the octet paths with WIDE_BYTE + BIG_ENDIAN_ORDER
run: |
set -e
for f in sha sha256 sha512 sha3 misc aes chacha random; do
for f in sha sha256 sha512 sha3 misc aes chacha random wc_port; do
echo "== wolfcrypt/src/$f.c =="
gcc -fsyntax-only -Werror -I. \
-DWOLFSSL_WIDE_BYTE -DBIG_ENDIAN_ORDER \
Expand Down
1 change: 1 addition & 0 deletions .wolfssl_known_macro_extras
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@ WOLFSSL_MAKE_SYSTEM_NAME_WSL
WOLFSSL_MANUALLY_SELECT_DEVICE_CONFIG
WOLFSSL_MDK5
WOLFSSL_MICROCHIP_AESGCM
WOLFSSL_MLDSA_VERIFY_PRECOMP_A
WOLFSSL_MLKEM_ASM_TEST
WOLFSSL_MLKEM_INVNTT_UNROLL
WOLFSSL_MLKEM_NO_MALLOC
Expand Down
61 changes: 55 additions & 6 deletions IDE/C2000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ etc.). On normal 8-bit-byte targets none of this code changes behavior.
- SHA-1; SHA-224/256, SHA-384/512, SHA-512/224, SHA-512/256
- SHA3-224/256/384/512, SHAKE128/256 (split-64 Keccak permutation auto-enabled
for `WOLFSSL_WIDE_BYTE`, ~53% faster than the generic C path)
- ML-DSA-44/65/87 (Dilithium) verify and full keygen/sign/verify;
- ML-DSA-44/65/87 (Dilithium) verify, including the HashML-DSA (pre-hash)
`wc_MlDsaKey_VerifyCtxHash()` path over SHA-256 and SHA-512 with a non-empty
context, and full ML-DSA-87 keygen/sign/verify;
ML-KEM-512/768/1024 (FIPS 203)
- AES-128/192/256 CBC/CTR/CFB/OFB/GCM/XTS; AES-CMAC, AES-CCM, AES-GMAC,
AES-SIV, AES-EAX
Expand All @@ -27,6 +29,48 @@ split-64 Keccak path is additionally validated on a host build with
`-DWC_SHA3_SPLIT64` forced, and the compile-only CI below guards every
`WOLFSSL_WIDE_BYTE` source against build breakage.

## Octet representation at the API boundary

**Every wolfCrypt `byte*` buffer holds exactly one octet per `byte` cell** -
keys, signatures, digests, ciphertext alike. On a normal target a cell is 8
bits, so that is a packed octet stream. On the C28x a cell is 16 bits, so the
same buffer costs twice the RAM and each cell reads as `0x00nn`. The octet
*values* are unchanged, so this is a footprint property, not an integrity one:
an ML-DSA-65 signature is 3309 octets = 3309 cells = 6618 bytes of RAM,
`sizeof(sig)` is 3309, and length arguments such as `sigLen` are octet counts
throughout.

Data arriving from outside the CPU - flash, SCI, CAN, a host tool - is
different: it is **packed**, `CHAR_BIT / 8` octets per cell, low octet first
(the order TI's `__byte()` uses). Convert at the boundary with
`wc_UnpackOctets()`, and `wc_PackOctets()` on the way back out:


```c
static byte sig[WC_MLDSA_65_SIG_SIZE]; /* 3309 cells, one octet each */

/* sigPacked: WC_PACKED_CELLS(3309) cells as stored in flash. */
if (wc_UnpackOctets(sig, (word32)sizeof(sig), sigPacked,
(word32)sizeof(sigPacked), WC_MLDSA_65_SIG_SIZE) == 0) {
ret = wc_MlDsaKey_VerifyCtxHash(key, sig, WC_MLDSA_65_SIG_SIZE,
ctx, ctxLen, hash, hashLen,
WC_HASH_TYPE_SHA256, &res);
}
```


Both are declared only under `WOLFSSL_WIDE_BYTE`, since on an 8-bit-byte target
the packed and unpacked layouts are the same buffer and there is nothing to
convert. `WC_OCTETS_PER_BYTE` and `WC_PACKED_CELLS()` are always available for
sizing. Source and destination must not overlap. Note that a C array literal
needs no unpacking - `static const byte sig[] = { 0xaa, ... }` is already one
octet per cell - and no API needs a "wide" variant; only the transport layout
differs.

The reference example's `make MLDSA=1` image exercises this on hardware:
ML-DSA-44/65/87 verify, `wc_MlDsaKey_VerifyCtxHash()` over SHA-256 and SHA-512,
and a verify from a packed key and signature.

## What `WOLFSSL_WIDE_BYTE` fixes

The `CHAR_BIT != 8` work falls into a few recurring classes, each a no-op on
Expand All @@ -47,9 +91,14 @@ The `CHAR_BIT != 8` work falls into a few recurring classes, each a no-op on
- `sizeof` counting cells, not octets. e.g. `CHACHA_CHUNK_BYTES` is `16 * 4`,
not `16 * sizeof(word32)` (= 32 on C28x, which halves the ChaCha block).

The SP backend file `wolfcrypt/src/sp_c32.c` is generated; the `& 0xFF` octet
masks added to its `sp_*_to_bin_*` serializers are also applied in the SP
generator templates so a regeneration preserves them (tracked separately).
The SP backend file `wolfcrypt/src/sp_c32.c` is generated and carries two
`CHAR_BIT != 8` fixes: the `& 0xFF` octet masks in `sp_*_to_bin_*`, and
`sizeof(mp_digit) * CHAR_BIT` (not `* 8`) in the `sp_*_from_mp` constant-time
mask. Both now come from the SP generator templates (`sp/conv.rb` in the
wolfSSL scripts repo). A regeneration with older templates drops them, which
breaks ECDSA, ECDH, RSA and DH on the C28x while leaving every 8-bit-byte build
and all hardware-free CI green - this has happened once already. After any SP
regeneration, re-check with `./regress.sh ecc rsa dh`.

## cl2000 compiler workarounds

Expand Down Expand Up @@ -81,8 +130,8 @@ math backend on a 16-bit-int target also set `WOLFSSL_SP_MATH`,
A complete bare-metal example with KATs, benchmark, linker scripts, and per-
algorithm build toggles is in wolfSSL Examples:
`embedded/ti-c2000-f28p55x/` (see its `README.md` for the `make` options:
`ECC`, `MLKEM`, `AES`, `AESEXTRA`, `X25519`, `HKDF`, `CHACHA`, `RSA`, `SIGN`,
`BENCH`).
`ECC`, `MLKEM`, `MLDSA`, `AES`, `AESEXTRA`, `X25519`, `HKDF`, `CHACHA`,
`RSA`, `SIGN`, `BENCH`).

Representative throughput on the F28P55X at 150 MHz: SHA-256 ~284 KiB/s; SHA3-256
~264 KiB/s; SHAKE128 ~319 KiB/s; RNG Hash-DRBG ~122 KiB/s. ML-DSA-87 verify
Expand Down
8 changes: 3 additions & 5 deletions IDE/C2000/compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,9 @@ CFLAGS="-v28 --abi=eabi --float_support=fpu32 --tmu_support=tmu1 -O2 \

# wolfCrypt sources to compile-guard under CHAR_BIT==16. This is the set that
# carries the CHAR_BIT != 8 gated fixes (plus their direct deps) - the
# regression surface for this port. hash.c (an unmodified dispatch wrapper) is
# intentionally omitted: its wc_OidGetHash() OID switch needs the fuller ASN/OID
# config of a real build to avoid a 16-bit-int case-label fold, and it is
# covered by the on-target example build, not by this minimal guard.
SRCS="error wc_port memory logging misc coding \
# regression surface for this port, plus hash.c, which HashML-DSA
# (wc_MlDsaKey_VerifyCtxHash) calls for the digest size and OID.
SRCS="error wc_port memory logging misc coding hash \
sha sha256 sha512 sha3 wc_mldsa random ecc sp_int sp_c32 \
aes cmac chacha poly1305 \
curve25519 ed25519 fe_operations ge_operations \
Expand Down
16 changes: 13 additions & 3 deletions IDE/C2000/user_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
#define TI_C2000_CI_USER_SETTINGS_H

#define WOLFCRYPT_ONLY /* crypto only - no TLS (no MD5/SHA1 dep) */
/* The C28x has a 16-bit int as well as a 16-bit char, so this models the real
* target rather than a half-configured one. It is a broad switch, not just an
* OID knob: settings.h and types.h key several decisions off it - the old
* 16-bit-safe OID sums (without which the 32-bit sums in wc_OidGetHash()
* collide once truncated to int and hash.c will not compile), MP_16BIT for the
* big-int backends, the small GCM tables, WORD64_AVAILABLE handling, and the
* ML-DSA cl2000 codegen workarounds in dilithium.h. The reference example
* defines it too; keep the two in step. */
#define WC_16BIT_CPU
#define WOLFSSL_GENERAL_ALIGNMENT 2
#define HAVE_LIMITS_H
#define WOLFSSL_NO_ASM
Expand Down Expand Up @@ -61,10 +70,11 @@
#define HAVE_ED448
#define ED448_SMALL

/* ML-DSA-87 verify (smallest-mem streaming verifier) */
/* ML-DSA verify, all three parameter sets (smallest-mem streaming) */
#define WOLFSSL_HAVE_MLDSA
#define WOLFSSL_NO_ML_DSA_44
#define WOLFSSL_NO_ML_DSA_65
/* All three parameter sets: level 44 is the only one whose w1 commitment
* encoder packs 6-bit values (mldsa_encode_w1_88_c), so without it this guard
* would not compile the very code the CHAR_BIT != 8 masking protects. */
#define WOLFSSL_MLDSA_NO_ASN1
#define WOLFSSL_MLDSA_VERIFY_ONLY
#define WOLFSSL_MLDSA_VERIFY_SMALL_MEM
Expand Down
1 change: 1 addition & 0 deletions doc/ALGORITHM_DEFINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ Operations and size:
| `WOLFSSL_MLDSA_NO_MAKE_KEY` / `_NO_SIGN` / `_NO_VERIFY` | Drop an operation |
| `WOLFSSL_MLDSA_VERIFY_ONLY` | Verify only — the firmware-check case |
| `WOLFSSL_MLDSA_VERIFY_SMALL_MEM` | Stream the verify instead of expanding the key at once |
| `WOLFSSL_MLDSA_VERIFY_PRECOMP_A` | Allow a host-expanded matrix A to be attached with `wc_MlDsaKey_SetPrecompA()`, so verify skips the SHAKE128 expansion. Needs a verification key fixed at build time; works with both the default and small-memory verifiers. The stored matrix must be integrity-protected exactly as the public key is |
| `WOLFSSL_MLKEM_SMALL`, `WOLFSSL_MLKEM_NO_LARGE_CODE` | Loop rather than unroll |
| `WOLFSSL_MLDSA_SMALL`, `WOLFSSL_MLDSA_NO_LARGE_CODE` | As above for ML-DSA |
| `WOLFSSL_MLKEM_DYNAMIC_KEYS` | Allocate key buffers to the size actually needed, rather than carrying the largest in the key structure. Reduces handshake memory on constrained systems. **Cannot be used with `WOLFSSL_NO_MALLOC`** |
Expand Down
62 changes: 62 additions & 0 deletions doc/dox_comments/header_files/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,68 @@ char* wc_strsep(char **stringp, const char *delim);
*/
size_t wc_strlcpy(char *dst, const char *src, size_t dstSize);

/*!
\ingroup wolfCrypt
\brief Expands a packed octet stream into the one-octet-per-byte-cell form
every wolfCrypt byte* API expects. Declared only when WOLFSSL_WIDE_BYTE is
set (CHAR_BIT != 8); elsewhere the two layouts coincide and no conversion
is needed.

\return 0 on success
\return BAD_FUNC_ARG when out or in is NULL
\return BUFFER_E when out is smaller than octetSz, or in smaller than
WC_PACKED_CELLS(octetSz)

\param out Destination, one octet per cell
\param outSz Size of out in byte cells
\param in Packed source, WC_OCTETS_PER_BYTE octets per cell, low octet
first, and must not overlap out
\param inSz Size of in in byte cells; must be at least
WC_PACKED_CELLS(octetSz)
\param octetSz Number of octets to expand

_Example_
\code
byte sig[WC_MLDSA_65_SIG_SIZE];
int ret = wc_UnpackOctets(sig, (word32)sizeof(sig), sigPacked,
(word32)sizeof(sigPacked),
WC_MLDSA_65_SIG_SIZE);
\endcode

\sa wc_PackOctets
*/
int wc_UnpackOctets(byte* out, word32 outSz, const byte* in, word32 inSz,
word32 octetSz);

/*!
\ingroup wolfCrypt
\brief Packs a one-octet-per-byte-cell buffer into an octet stream, for
storing to flash or handing to a byte-oriented peripheral. Inverse of
wc_UnpackOctets(); requires WOLFSSL_WIDE_BYTE.

\return 0 on success
\return BAD_FUNC_ARG when out or in is NULL
\return BUFFER_E when out is smaller than WC_PACKED_CELLS(octetSz), or
in smaller than octetSz

\param out Destination for the packed stream
\param outSz Size of out in byte cells
\param in Source, one octet per cell, and must not overlap out
\param inSz Size of in in byte cells; must be at least octetSz
\param octetSz Number of octets to pack

_Example_
\code
byte packed[WC_PACKED_CELLS(sizeof(sig))];
int ret = wc_PackOctets(packed, (word32)sizeof(packed), sig,
(word32)sizeof(sig), (word32)sizeof(sig));
\endcode

\sa wc_UnpackOctets
*/
int wc_PackOctets(byte* out, word32 outSz, const byte* in, word32 inSz,
word32 octetSz);

/*!
\ingroup String
\brief Safely concatenates strings with size limit.
Expand Down
59 changes: 59 additions & 0 deletions doc/dox_comments/header_files/wc_mldsa.h
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,65 @@ int wc_MlDsaKey_VerifyCtxHash(wc_MlDsaKey* key, const byte* sig, word32 sigLen,
int wc_MlDsaKey_VerifyMu(wc_MlDsaKey* key, const byte* sig, word32 sigLen,
const byte* mu, word32 muLen, int* res);

/*!
\ingroup ML_DSA

\brief Attaches a matrix A that was expanded off target, so verification
can skip the SHAKE128 rejection sampling that otherwise dominates it.
A is a function of the public seed rho alone, so wherever the verification
key is fixed at build time - secure boot being the usual case - A can be
computed once on a host and stored in flash. Available only when
WOLFSSL_MLDSA_VERIFY_PRECOMP_A is defined, and honoured by both the
default and the small-memory verifiers.

The matrix is borrowed, never copied and never freed: it must stay valid
and unchanged for as long as the key uses it. It holds
k * l * MLDSA_N sword32 elements (not bytes) in row-major (r, s) order and
in the NTT domain, exactly as ExpandA produces them.

Import the public key first: the matrix is bound to it by comparing rho.
The binding is re-checked on every verify, so replacing the key or the
parameters afterwards simply discards the matrix and falls back to
expanding A rather than using a stale one.

\note A is derived from the public key and must be integrity-protected
exactly as the public key is. An attacker able to substitute it can
influence verification. In a secure-boot design the key already lives in
protected flash, so store the two together.

\return 0 on success.
\return BAD_FUNC_ARG if a pointer is NULL, rhoLen is not
MLDSA_PUB_SEED_SZ, aLen is not k * l * MLDSA_N, or no public key has been
imported yet.
\return PUBLIC_KEY_E if rho does not match the imported public key.

\param [in,out] key Pointer to a wc_MlDsaKey holding the public key.
\param [in] a Expanded matrix A, k * l * MLDSA_N elements, caller owned.
\param [in] aLen Number of sword32 elements in a.
\param [in] rho Public seed the matrix was expanded from.
\param [in] rhoLen Length of rho; must be MLDSA_PUB_SEED_SZ.

_Example_
\code
wc_MlDsaKey key;
int res = 0;

wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID);
wc_MlDsaKey_SetParams(&key, WC_ML_DSA_87);
wc_MlDsaKey_ImportPubRaw(&key, pub, pubLen);
if (wc_MlDsaKey_SetPrecompA(&key, matrixA, matrixALen, pub,
MLDSA_PUB_SEED_SZ) != 0) {
// handle error
}
wc_MlDsaKey_VerifyMu(&key, sig, sigLen, mu, muLen, &res);
\endcode

\sa wc_MlDsaKey_VerifyMu
\sa wc_MlDsaKey_ImportPubRaw
*/
int wc_MlDsaKey_SetPrecompA(wc_MlDsaKey* key, const sword32* a, word32 aLen,
const byte* rho, word32 rhoLen);

/*!
\ingroup ML_DSA

Expand Down
Loading
Loading