diff --git a/.github/workflows/wolfssl-versions.yml b/.github/workflows/wolfssl-versions.yml
index ded20f6..74f98bc 100644
--- a/.github/workflows/wolfssl-versions.yml
+++ b/.github/workflows/wolfssl-versions.yml
@@ -19,7 +19,9 @@ jobs:
# bump this workflow every release. Floor (v5.8.0) and master are fixed:
# v5.8.0 is the first release with the public wc_ForceZero symbol and the
# current set of FIPS-204 final ML-DSA APIs wolfCOSE depends on; master
- # surfaces upstream drift on the nightly run.
+ # surfaces upstream drift on the nightly run. Master is tested both classic
+ # and with ML-DSA/PQC; latest-stable gains an explicit PQC row automatically
+ # once it is newer than v5.9.1-stable (i.e. when 5.9.2 ships).
discover-versions:
name: Resolve wolfSSL version matrix
runs-on: ubuntu-latest
@@ -52,17 +54,24 @@ jobs:
LATEST_PQC=false
fi
echo "latest-stable PQC eligible: $LATEST_PQC"
+ # PQC-able versions (master always; latest-stable once >v5.9.1-stable)
+ # get an explicit, separately-named ML-DSA row in addition to a
+ # classic (no-PQC) row, so PQC build+test is always visible and the
+ # non-PQC build is still exercised on the same wolfSSL.
MATRIX=$(jq -nc --arg latest "$LATEST" --argjson latest_pqc "$LATEST_PQC" '{
- include: [
+ include: ([
{"wolfssl-version":"v5.8.0-stable","wolfssl-ref":"v5.8.0-stable","cache-key":"wolfssl-nopqc-v5.8.0-v1","pqc":false},
- {"wolfssl-version":$latest,"wolfssl-ref":$latest,"cache-key":("wolfssl-" + (if $latest_pqc then "pqc" else "nopqc" end) + "-" + $latest + "-v1"),"pqc":$latest_pqc},
+ {"wolfssl-version":$latest,"wolfssl-ref":$latest,"cache-key":("wolfssl-nopqc-" + $latest + "-v1"),"pqc":false},
+ {"wolfssl-version":"master","wolfssl-ref":"master","cache-key":"","pqc":false},
{"wolfssl-version":"master","wolfssl-ref":"master","cache-key":"","pqc":true}
- ]
+ ] + (if $latest_pqc then [
+ {"wolfssl-version":$latest,"wolfssl-ref":$latest,"cache-key":("wolfssl-pqc-" + $latest + "-v1"),"pqc":true}
+ ] else [] end))
}')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
wolfssl-version-test:
- name: wolfSSL ${{ matrix.wolfssl-version }}
+ name: wolfSSL ${{ matrix.wolfssl-version }} ${{ matrix.pqc && '(+ML-DSA/PQC)' || '(classic)' }}
needs: discover-versions
runs-on: ubuntu-latest
timeout-minutes: 25
@@ -133,6 +142,26 @@ jobs:
make tool-test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \
LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl"
+ # ML-DSA / PQC-specific coverage: CLI keygen->sign->verify for all three
+ # ML-DSA levels and the ML-DSA sign+verify demo. Runs on the explicit pqc
+ # rows only, so the row is a clean PQC signal (no classic-alg coupling).
+ - name: Run ML-DSA PQC tests (CLI round-trip + demo)
+ if: matrix.pqc
+ run: |
+ export WOLFSSL_DIR=$HOME/wolfssl-install
+ export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib
+ CF="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include"
+ LF="-L$WOLFSSL_DIR/lib -lwolfssl"
+ make tool CFLAGS="$CF" LDFLAGS="$LF"
+ echo "wolfCOSE ML-DSA CLI round-trip" > /tmp/pqc-msg.txt
+ for alg in ML-DSA-44 ML-DSA-65 ML-DSA-87; do
+ echo "=== $alg CLI keygen -> sign -> verify ==="
+ ./tools/wolfcose_tool keygen -a "$alg" -o /tmp/pqc.key
+ ./tools/wolfcose_tool sign -k /tmp/pqc.key -a "$alg" -i /tmp/pqc-msg.txt -o /tmp/pqc.cose
+ ./tools/wolfcose_tool verify -k /tmp/pqc.key -i /tmp/pqc.cose
+ done
+ make mldsa-demo CFLAGS="$CF" LDFLAGS="$LF"
+
- name: wolfSSL version info
if: always()
run: |
diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
index 0000000..c945044
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,72 @@
+# wolfCOSE Release 1.0.0 (release date TBD, after wolfSSL 5.9.2)
+
+Release 1.0.0 is the first stable release of wolfCOSE, a complete,
+zero-allocation C implementation of CBOR (RFC 8949) and COSE (RFC 9052/9053)
+on top of wolfCrypt. It provides all six COSE message types in both
+single-actor and multi-actor forms, 40 algorithms across signing, encryption,
+MAC, and key distribution, and standardized post-quantum ML-DSA signatures
+(RFC 9964), all heap-allocation-free and within a tiny footprint.
+
+## Vulnerabilities
+
+* None. This is the initial release.
+
+## New Feature Additions
+
+* CBOR engine implementing RFC 8949 encode/decode with no external dependency,
+ enforcing deterministic/preferred-encoding rules and rejecting non-preferred
+ or trailing input on decode.
+* All six COSE message types (RFC 9052): `COSE_Sign1`, `COSE_Sign`,
+ `COSE_Encrypt0`, `COSE_Encrypt`, `COSE_Mac0`, and `COSE_Mac`, including the
+ multi-signer and multi-recipient variants.
+* 40 algorithms across signing, encryption, MAC, and key distribution
+ (RFC 9053): ES256/384/512, EdDSA (Ed25519/Ed448), PS256/384/512,
+ ML-DSA-44/65/87, AES-GCM (128/192/256), ChaCha20-Poly1305, AES-CCM variants,
+ HMAC-SHA256/384/512, AES-MAC, Direct, AES Key Wrap, and ECDH-ES+HKDF.
+* Standardized post-quantum signatures: ML-DSA (FIPS 204) at all three security
+ levels, conformant to RFC 9964 ("ML-DSA for JOSE and COSE"). COSE keys use the
+ RFC 9964 AKP key type (`kty` 7) with a required `alg`, the public key in `pub`
+ (-1), and the 32-byte seed private key in `priv` (-2).
+* `COSE_Key` / `COSE_KeySet` serialization for all supported key types,
+ including full RFC 8230 RSA private keys (n, e, d, p, q, dP, dQ, qInv).
+* Zero dynamic allocation: every operation uses caller-provided buffers, with no
+ heap, `.data`, or `.bss` usage.
+* Path to FIPS 140-3 through wolfCrypt FIPS Certificate #4718 (sole crypto
+ dependency).
+* `WOLFCOSE_LEAN` configuration layer with `WOLFCOSE_HAVE_*` feature gates,
+ `WOLFCOSE_LEAN_VERIFY` / ML-DSA lean profiles for verify-only targets, and a
+ `WOLFCOSE_MIN_BUFFERS` bounded-stack profile.
+* `LIBWOLFCOSE_VERSION_STRING` / `LIBWOLFCOSE_VERSION_HEX` in
+ `wolfcose/version.h` for compile-time version checks.
+
+## Fixes
+
+* RSA private `COSE_Key` encode/decode now emits the RFC 8230 MUST-present `dP`
+ (-6) and `dQ` (-7) CRT exponents and encodes `d` at full modulus width, so a
+ private RSA key round-trips reliably against strict RSA decoders.
+
+## Improvements/Optimizations
+
+* Minimal footprint: an ES256 `COSE_Sign1` build is ~5.1 KB verify-only and
+ ~6.8 KB sign + verify for the wolfCOSE COSE + CBOR engine; see the
+ [Footprint](https://github.com/wolfSSL/wolfCOSE/wiki/Footprint) page for
+ total-flash numbers including wolfCrypt.
+* MISRA C:2012 and C:2023 checked.
+* CI matrix covering Ubuntu/macOS, GCC 10-14 and Clang 14-18, ~240 algorithm
+ combination tests, static analysis (cppcheck, Clang analyzer, GCC
+ `-fanalyzer`), sanitizers (ASan/UBSan), Coverity, a wolfCOSE <-> t_cose
+ wire-interop conformance suite, and a wolfSSL version matrix with explicit
+ ML-DSA/PQC rows.
+
+---
+
+wolfCOSE 1.0.0 has been developed according to wolfSSL's development and QA
+process (see
+https://www.wolfssl.com/about/wolfssl-software-development-process-quality-assurance)
+and successfully passed the quality criteria.
+
+For additional vulnerability information visit the vulnerability page at
+https://www.wolfssl.com/docs/security-vulnerabilities/
+
+Requires wolfSSL 5.8.0 or later as the crypto backend; ML-DSA support requires
+wolfSSL 5.9.2 or later. See README.md for build instructions.
diff --git a/README.md b/README.md
index 77db45c..dcd1306 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
# wolfCOSE
-wolfCOSE is a lightweight C library implementing [CBOR (RFC 8949)](https://www.rfc-editor.org/rfc/rfc8949) and [COSE (RFC 9052/9053)](https://www.rfc-editor.org/rfc/rfc9052) using [wolfSSL](https://www.wolfssl.com/) as the crypto backend.
+wolfCOSE is a lightweight C library implementing [CBOR (RFC 8949)](https://www.rfc-editor.org/rfc/rfc8949), [COSE (RFC 9052/9053)](https://www.rfc-editor.org/rfc/rfc9052), and post-quantum [ML-DSA for COSE (RFC 9964)](https://www.rfc-editor.org/rfc/rfc9964) using [wolfSSL](https://www.wolfssl.com/) as the crypto backend.
## Main Features
- **Complete RFC 9052 message set**: all six COSE message types, including multi-signer
`COSE_Sign` and multi-recipient `COSE_Encrypt` / `COSE_Mac`
-- **Post-quantum signing**: ML-DSA (FIPS 204) at all three security levels
+- **Post-quantum signing**: ML-DSA (FIPS 204) at all three security levels, with RFC 9964 `COSE_Key` (AKP key type, seed-based private keys)
- **40 algorithms** across signing, encryption, MAC, and key distribution
- **Zero dynamic allocation**: heap-allocation-free and non-recursive. Every operation runs on caller-provided buffers
within a bounded, target-customizable stack ceiling (nothing on the heap, zero `.data`/`.bss`)
@@ -190,6 +190,12 @@ Full documentation is available in the [Wiki](https://github.com/wolfSSL/wolfCOS
- [MISRA Compliance](https://github.com/wolfSSL/wolfCOSE/wiki/MISRA-Compliance): MISRA C:2012 and C:2023 compliance status and deviation rationale
- [Project Structure](https://github.com/wolfSSL/wolfCOSE/wiki/Project-Structure): Source file layout
+## Release Notes
+
+The current release is **1.0.0**, the first stable release: the complete RFC 9052 COSE message set (all six message types, single- and multi-actor), 40 algorithms, and standardized post-quantum ML-DSA (RFC 9964), all with zero dynamic allocation. See [ChangeLog.md](ChangeLog.md) for the full release notes.
+
+wolfCOSE 1.0.0 has been developed according to wolfSSL's development and QA process (see https://www.wolfssl.com/about/wolfssl-software-development-process-quality-assurance) and successfully passed the quality criteria.
+
## License
wolfCOSE is free software licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html); see [LICENSE](LICENSE) for the full text.
diff --git a/docs/API-Reference.md b/docs/API-Reference.md
index edd15aa..16fa6b6 100644
--- a/docs/API-Reference.md
+++ b/docs/API-Reference.md
@@ -203,6 +203,10 @@ int wc_CoseKey_SetMlDsa(WOLFCOSE_KEY* key, int32_t alg, wc_MlDsaKey* mlDsaKey);
```
Associate an ML-DSA (FIPS 204) post-quantum key with a COSE key structure.
+The key is encoded as an RFC 9964 **AKP** COSE_Key (`kty` = 7, REQUIRED `alg`,
+public key in `pub` (-1)). Use this for public-key, sign, and verify use. To
+encode a *private* COSE_Key (whose private value is the 32-byte seed), use
+`wc_CoseKey_SetMlDsa_ex` and supply the seed.
**Parameters:**
| Name | Description |
@@ -217,6 +221,34 @@ Associate an ML-DSA (FIPS 204) post-quantum key with a COSE key structure.
---
+### wc_CoseKey_SetMlDsa_ex
+
+```c
+int wc_CoseKey_SetMlDsa_ex(WOLFCOSE_KEY* key, int32_t alg,
+ wc_MlDsaKey* mlDsaKey,
+ const uint8_t* seed, size_t seedLen);
+```
+
+Like `wc_CoseKey_SetMlDsa`, but also attaches the 32-byte ML-DSA seed used to
+create the key. RFC 9964 represents an ML-DSA private key as the seed, and
+wolfCrypt does not retain it, so the caller (who created the key via
+`wc_MlDsaKey_MakeKeyFromSeed`) must supply it here to encode a private COSE_Key.
+
+**Parameters:**
+| Name | Description |
+|------|-------------|
+| `key` | Pointer to initialized COSE key |
+| `alg` | Algorithm: `WOLFCOSE_ALG_ML_DSA_44`, `WOLFCOSE_ALG_ML_DSA_65`, or `WOLFCOSE_ALG_ML_DSA_87` |
+| `mlDsaKey` | Pointer to initialized wolfCrypt ML-DSA key (caller-owned) |
+| `seed` | 32-byte ML-DSA seed (caller-owned), or `NULL` for public/sign/verify use |
+| `seedLen` | Seed length; must be `WOLFCOSE_MLDSA_SEED_SZ` (32) when `seed` is non-NULL |
+
+**Returns:** `WOLFCOSE_SUCCESS` or error code
+
+**Requires:** `WOLFSSL_HAVE_MLDSA`
+
+---
+
### wc_CoseKey_SetRsa
```c
diff --git a/docs/Algorithms.md b/docs/Algorithms.md
index 5512616..acd42c1 100644
--- a/docs/Algorithms.md
+++ b/docs/Algorithms.md
@@ -84,10 +84,15 @@ Used with COSE_Encrypt and COSE_Mac for multi-recipient messages:
| COSE kty | Value | Guard | Algorithms |
|----------|-------|-------|------------|
-| OKP | 1 | `HAVE_ED25519` / `HAVE_ED448` / `WOLFSSL_HAVE_MLDSA` | EdDSA, ML-DSA |
+| OKP | 1 | `HAVE_ED25519` / `HAVE_ED448` | EdDSA |
| EC2 | 2 | `HAVE_ECC` | ES256, ES384, ES512 |
| RSA | 3 | `WC_RSA_PSS` | PS256, PS384, PS512 |
| Symmetric | 4 | always | AES-GCM, AES-CCM, ChaCha20, HMAC |
+| AKP | 7 | `WOLFSSL_HAVE_MLDSA` | ML-DSA (RFC 9964) |
+
+ML-DSA keys use the RFC 9964 **AKP** (Algorithm Key Pair) key type: the `alg`
+parameter is REQUIRED, the public key is in `pub` (-1), and the private key is
+the 32-byte seed in `priv` (-2). There is no `crv` parameter.
## Curves
diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md
index 66a1a79..c612396 100644
--- a/docs/Getting-Started.md
+++ b/docs/Getting-Started.md
@@ -228,7 +228,10 @@ int main(void)
wc_MlDsaKey_SetParams(&mlDsaKey, WC_ML_DSA_44);
wc_MlDsaKey_MakeKey(&mlDsaKey, &rng);
- /* Wrap in COSE key */
+ /* Wrap in COSE key. ML-DSA uses the RFC 9964 AKP key type; this is all
+ * that is needed for sign/verify. To export a *private* COSE_Key, create
+ * the key with wc_MlDsaKey_MakeKeyFromSeed and pass the 32-byte seed via
+ * wc_CoseKey_SetMlDsa_ex (RFC 9964 private keys are the seed). */
wc_CoseKey_Init(&coseKey);
wc_CoseKey_SetMlDsa(&coseKey, WOLFCOSE_ALG_ML_DSA_44, &mlDsaKey);
diff --git a/docs/Home.md b/docs/Home.md
index f818132..e75a0a7 100644
--- a/docs/Home.md
+++ b/docs/Home.md
@@ -36,6 +36,7 @@ It uses [wolfSSL](https://www.wolfssl.com/) as the cryptographic backend and is
| [[Footprint]] | Size and speed numbers, desktop and on-device |
| [[Testing]] | Unit tests, coverage, and failure injection |
| [[Project Structure]] | Source code layout and file descriptions |
+| [[Release Notes]] | Per-version changelog and release highlights |
## Supported Message Types
diff --git a/docs/Release-Notes.md b/docs/Release-Notes.md
new file mode 100644
index 0000000..071f474
--- /dev/null
+++ b/docs/Release-Notes.md
@@ -0,0 +1,70 @@
+# Release Notes
+
+## wolfCOSE 1.0.0 (release date TBD, after wolfSSL 5.9.2)
+
+Release 1.0.0 is the first stable release of wolfCOSE, a complete,
+zero-allocation C implementation of CBOR (RFC 8949) and COSE (RFC 9052/9053)
+on top of wolfCrypt. It provides all six COSE message types in both
+single-actor and multi-actor forms, 40 algorithms across signing, encryption,
+MAC, and key distribution, and standardized post-quantum ML-DSA signatures
+(RFC 9964), all heap-allocation-free and within a tiny footprint.
+
+### Vulnerabilities
+
+- None. This is the initial release.
+
+### New Feature Additions
+
+- CBOR engine implementing RFC 8949 encode/decode with no external dependency,
+ enforcing deterministic/preferred-encoding rules and rejecting non-preferred
+ or trailing input on decode.
+- All six COSE message types (RFC 9052): `COSE_Sign1`, `COSE_Sign`,
+ `COSE_Encrypt0`, `COSE_Encrypt`, `COSE_Mac0`, and `COSE_Mac`, including the
+ multi-signer and multi-recipient variants. See [[Message Types]].
+- 40 algorithms across signing, encryption, MAC, and key distribution
+ (RFC 9053): ES256/384/512, EdDSA (Ed25519/Ed448), PS256/384/512,
+ ML-DSA-44/65/87, AES-GCM (128/192/256), ChaCha20-Poly1305, AES-CCM variants,
+ HMAC-SHA256/384/512, AES-MAC, Direct, AES Key Wrap, and ECDH-ES+HKDF. See
+ [[Algorithms]].
+- Standardized post-quantum signatures: ML-DSA (FIPS 204) at all three security
+ levels, conformant to RFC 9964 ("ML-DSA for JOSE and COSE"). COSE keys use the
+ RFC 9964 AKP key type (`kty` 7) with a required `alg`, the public key in `pub`
+ (-1), and the 32-byte seed private key in `priv` (-2).
+- `COSE_Key` / `COSE_KeySet` serialization for all supported key types,
+ including full RFC 8230 RSA private keys (n, e, d, p, q, dP, dQ, qInv).
+- Zero dynamic allocation: every operation uses caller-provided buffers, with no
+ heap, `.data`, or `.bss` usage.
+- Path to FIPS 140-3 through wolfCrypt FIPS Certificate #4718 (sole crypto
+ dependency).
+- `WOLFCOSE_LEAN` configuration layer with `WOLFCOSE_HAVE_*` feature gates,
+ `WOLFCOSE_LEAN_VERIFY` / ML-DSA lean profiles for verify-only targets, and a
+ `WOLFCOSE_MIN_BUFFERS` bounded-stack profile. See [[Macros]].
+- `LIBWOLFCOSE_VERSION_STRING` / `LIBWOLFCOSE_VERSION_HEX` in
+ `wolfcose/version.h` for compile-time version checks.
+
+### Fixes
+
+- RSA private `COSE_Key` encode/decode now emits the RFC 8230 MUST-present `dP`
+ (-6) and `dQ` (-7) CRT exponents and encodes `d` at full modulus width, so a
+ private RSA key round-trips reliably against strict RSA decoders.
+
+### Improvements/Optimizations
+
+- Minimal footprint: an ES256 `COSE_Sign1` build is ~5.1 KB verify-only and
+ ~6.8 KB sign + verify for the wolfCOSE COSE + CBOR engine. See [[Footprint]].
+- MISRA C:2012 and C:2023 checked. See [[MISRA Compliance]].
+- CI matrix covering Ubuntu/macOS, GCC 10-14 and Clang 14-18, ~240 algorithm
+ combination tests, static analysis (cppcheck, Clang analyzer, GCC
+ `-fanalyzer`), sanitizers (ASan/UBSan), Coverity, a wolfCOSE <-> t_cose
+ wire-interop conformance suite, and a wolfSSL version matrix with explicit
+ ML-DSA/PQC rows. See [[Testing]].
+
+---
+
+wolfCOSE 1.0.0 has been developed according to wolfSSL's development and QA
+process (see the [wolfSSL Software Development Process and Quality
+Assurance](https://www.wolfssl.com/about/wolfssl-software-development-process-quality-assurance)
+page) and successfully passed the quality criteria.
+
+Requires wolfSSL 5.8.0 or later as the crypto backend; ML-DSA support requires
+wolfSSL 5.9.2 or later. See [[Getting Started]] for build instructions.
diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md
index 62aa5d1..2d4ee2e 100644
--- a/docs/_Sidebar.md
+++ b/docs/_Sidebar.md
@@ -10,3 +10,4 @@
- [[Testing]]
- [[MISRA Compliance]]
- [[Project Structure]]
+- [[Release Notes]]
diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h
index c7c4588..be40b95 100644
--- a/include/wolfcose/settings.h
+++ b/include/wolfcose/settings.h
@@ -473,9 +473,10 @@ extern "C" {
#endif
#ifndef WOLFCOSE_MAX_MAP_ITEMS
#if defined(WOLFCOSE_MIN_BUFFERS)
- /* Full private RSA key = 9 entries (kty,n,e,d,p,q,qInv,kid,alg). */
#if defined(WOLFCOSE_HAVE_RSAPSS)
- #define WOLFCOSE_MAX_MAP_ITEMS 9u
+ /* Full private RSA key = 11 entries
+ * (kty,n,e,d,p,q,dP,dQ,qInv,kid,alg) per RFC 8230. */
+ #define WOLFCOSE_MAX_MAP_ITEMS 11u
#else
#define WOLFCOSE_MAX_MAP_ITEMS 8u
#endif
diff --git a/include/wolfcose/version.h b/include/wolfcose/version.h
new file mode 100644
index 0000000..6dfd85f
--- /dev/null
+++ b/include/wolfcose/version.h
@@ -0,0 +1,35 @@
+/* version.h
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of wolfCOSE.
+ *
+ * wolfCOSE is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfCOSE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ */
+
+#ifndef WOLFCOSE_VERSION_H
+#define WOLFCOSE_VERSION_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define LIBWOLFCOSE_VERSION_STRING "1.0.0"
+#define LIBWOLFCOSE_VERSION_HEX 0x01000000
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* WOLFCOSE_VERSION_H */
diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h
index 0bb8cb0..d37284a 100644
--- a/include/wolfcose/wolfcose.h
+++ b/include/wolfcose/wolfcose.h
@@ -22,6 +22,7 @@
#define WOLFCOSE_H
#include
+#include
#ifdef HAVE_CONFIG_H
#include
@@ -214,11 +215,15 @@ extern "C" {
#define WOLFCOSE_ALG_ML_DSA_65 (-49) /* ML-DSA Level 3 */
#define WOLFCOSE_ALG_ML_DSA_87 (-50) /* ML-DSA Level 5 */
+/* RFC 9964: an ML-DSA private key is the 32-byte seed (FIPS 204). */
+#define WOLFCOSE_MLDSA_SEED_SZ 32u
+
/* Key types */
#define WOLFCOSE_KTY_OKP 1
#define WOLFCOSE_KTY_EC2 2
#define WOLFCOSE_KTY_RSA 3
#define WOLFCOSE_KTY_SYMMETRIC 4
+#define WOLFCOSE_KTY_AKP 7 /* RFC 9964: Algorithm Key Pair (ML-DSA) */
/* Curves */
#define WOLFCOSE_CRV_P256 1
@@ -226,7 +231,8 @@ extern "C" {
#define WOLFCOSE_CRV_P521 3
#define WOLFCOSE_CRV_ED25519 6
#define WOLFCOSE_CRV_ED448 7
-/* Provisional PQC curve IDs (not yet in IANA registry) */
+/* Internal ML-DSA level<->alg mapping only; RFC 9964 AKP keys carry the level
+ * in alg, not crv. These are never emitted in a COSE_Key. */
#define WOLFCOSE_CRV_ML_DSA_44 (-48)
#define WOLFCOSE_CRV_ML_DSA_65 (-49)
#define WOLFCOSE_CRV_ML_DSA_87 (-50)
@@ -240,8 +246,12 @@ extern "C" {
#define WOLFCOSE_KEY_LABEL_Y (-3)
#define WOLFCOSE_KEY_LABEL_D (-4)
#define WOLFCOSE_KEY_LABEL_K (-1) /* Symmetric key value */
+#define WOLFCOSE_KEY_LABEL_PUB (-1) /* RFC 9964: AKP public key */
+#define WOLFCOSE_KEY_LABEL_PRIV (-2) /* RFC 9964: AKP private key (seed) */
#define WOLFCOSE_KEY_LABEL_RSA_P (-4) /* RFC 8230: first prime */
#define WOLFCOSE_KEY_LABEL_RSA_Q (-5) /* RFC 8230: second prime */
+#define WOLFCOSE_KEY_LABEL_RSA_DP (-6) /* RFC 8230: d mod (p-1) */
+#define WOLFCOSE_KEY_LABEL_RSA_DQ (-7) /* RFC 8230: d mod (q-1) */
#define WOLFCOSE_KEY_LABEL_RSA_QINV (-8) /* RFC 8230: CRT coefficient */
/* AES-GCM constants */
@@ -329,6 +339,10 @@ typedef struct WOLFCOSE_KEY {
size_t keyLen; /**< Key material length */
} symm;
} key;
+#ifdef WOLFSSL_HAVE_MLDSA
+ const uint8_t* mldsaSeed; /**< RFC 9964 ML-DSA private seed (32B), caller-owned */
+ size_t mldsaSeedLen; /**< ML-DSA private seed length */
+#endif
uint8_t hasPrivate; /**< 1 if private key material present */
} WOLFCOSE_KEY;
@@ -592,6 +606,11 @@ WOLFCOSE_API int wc_CoseKey_SetEd448(WOLFCOSE_KEY* key, ed448_key* edKey);
#ifdef WOLFCOSE_HAVE_MLDSA
WOLFCOSE_API int wc_CoseKey_SetMlDsa(WOLFCOSE_KEY* key, int32_t alg,
wc_MlDsaKey* mlDsaKey);
+/* RFC 9964 private-key export needs the 32-byte seed, which wolfCrypt does not
+ * retain; the caller supplies it here (seed may be NULL for public/sign use). */
+WOLFCOSE_API int wc_CoseKey_SetMlDsa_ex(WOLFCOSE_KEY* key, int32_t alg,
+ wc_MlDsaKey* mlDsaKey,
+ const uint8_t* seed, size_t seedLen);
#endif
#ifdef WOLFCOSE_HAVE_RSAPSS
diff --git a/src/wolfcose.c b/src/wolfcose.c
index 41ee74d..8ffff20 100644
--- a/src/wolfcose.c
+++ b/src/wolfcose.c
@@ -1194,8 +1194,9 @@ int wc_CoseKey_SetEd448(WOLFCOSE_KEY* key, ed448_key* edKey)
#endif /* WOLFCOSE_HAVE_ED448 */
#ifdef WOLFCOSE_HAVE_MLDSA
-int wc_CoseKey_SetMlDsa(WOLFCOSE_KEY* key, int32_t alg,
- wc_MlDsaKey* mlDsaKey)
+int wc_CoseKey_SetMlDsa_ex(WOLFCOSE_KEY* key, int32_t alg,
+ wc_MlDsaKey* mlDsaKey,
+ const uint8_t* seed, size_t seedLen)
{
int ret;
@@ -1207,24 +1208,29 @@ int wc_CoseKey_SetMlDsa(WOLFCOSE_KEY* key, int32_t alg,
(alg != WOLFCOSE_ALG_ML_DSA_87)) {
ret = WOLFCOSE_E_COSE_BAD_ALG;
}
+ else if ((seed != NULL) && (seedLen != WOLFCOSE_MLDSA_SEED_SZ)) {
+ ret = WOLFCOSE_E_INVALID_ARG;
+ }
else {
- key->kty = WOLFCOSE_KTY_OKP; /* PQC uses OKP kty per COSE WG */
+ /* RFC 9964: ML-DSA uses the AKP key type, carries the level in alg, and
+ * has no crv. crv is left unset (internal mapping only). */
+ key->kty = WOLFCOSE_KTY_AKP;
key->alg = alg;
- if (alg == WOLFCOSE_ALG_ML_DSA_44) {
- key->crv = WOLFCOSE_CRV_ML_DSA_44;
- }
- else if (alg == WOLFCOSE_ALG_ML_DSA_65) {
- key->crv = WOLFCOSE_CRV_ML_DSA_65;
- }
- else {
- key->crv = WOLFCOSE_CRV_ML_DSA_87;
- }
+ key->crv = 0;
key->key.mldsa = mlDsaKey;
+ key->mldsaSeed = seed;
+ key->mldsaSeedLen = (seed != NULL) ? seedLen : (size_t)0;
key->hasPrivate = (mlDsaKey->prvKeySet != 0u) ? 1u : 0u;
ret = WOLFCOSE_SUCCESS;
}
return ret;
}
+
+int wc_CoseKey_SetMlDsa(WOLFCOSE_KEY* key, int32_t alg,
+ wc_MlDsaKey* mlDsaKey)
+{
+ return wc_CoseKey_SetMlDsa_ex(key, alg, mlDsaKey, NULL, 0);
+}
#endif /* WOLFCOSE_HAVE_MLDSA */
#ifdef WOLFCOSE_HAVE_RSAPSS
@@ -1480,7 +1486,7 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
}
#endif
/* Get n directly into output buffer, e into small stack buf */
- mapEntries = (rsaPriv != 0) ? (size_t)7 : (size_t)3;
+ mapEntries = (rsaPriv != 0) ? (size_t)9 : (size_t)3;
mapEntries += wolfCose_KeyOptionalEntries(key);
if (ret == WOLFCOSE_SUCCESS) {
ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries);
@@ -1596,6 +1602,18 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
ret = WOLFCOSE_E_BUFFER_TOO_SMALL;
}
else {
+ /* Left-pad d to the full modulus width: a
+ * leading-zero byte would otherwise yield a
+ * short bstr that stricter RSA decoders
+ * reject (RFC 8230 octet-string width). */
+ if (dSz < (word32)rsaEncSz) {
+ size_t pad = (size_t)rsaEncSz -
+ (size_t)dSz;
+ (void)XMEMMOVE(&ctx.buf[dOff + pad],
+ &ctx.buf[dOff], (size_t)dSz);
+ (void)XMEMSET(&ctx.buf[dOff], 0, pad);
+ dSz = (word32)rsaEncSz;
+ }
ctx.buf[hdrPos] = 0x59u;
ctx.buf[hdrPos + 1u] =
(uint8_t)((uint32_t)dSz >> 8u);
@@ -1630,6 +1648,17 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
ret = wolfCose_EncodeRsaMp(&ctx, WOLFCOSE_KEY_LABEL_RSA_Q,
&key->key.rsa->q, halfSz);
}
+ /* RFC 8230: dP and dQ are MUST-present for a two-prime private key.
+ * Emitting them also avoids wolfCrypt recomputing the CRT exponents
+ * on decode, which is fragile for some key values. */
+ if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) {
+ ret = wolfCose_EncodeRsaMp(&ctx, WOLFCOSE_KEY_LABEL_RSA_DP,
+ &key->key.rsa->dP, halfSz);
+ }
+ if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) {
+ ret = wolfCose_EncodeRsaMp(&ctx, WOLFCOSE_KEY_LABEL_RSA_DQ,
+ &key->key.rsa->dQ, halfSz);
+ }
if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) {
ret = wolfCose_EncodeRsaMp(&ctx, WOLFCOSE_KEY_LABEL_RSA_QINV,
&key->key.rsa->u, halfSz);
@@ -1644,23 +1673,33 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
else
#endif /* WOLFCOSE_HAVE_RSAPSS */
#ifdef WOLFCOSE_HAVE_MLDSA
- if ((key->kty == WOLFCOSE_KTY_OKP) &&
- ((key->crv == WOLFCOSE_CRV_ML_DSA_44) ||
- (key->crv == WOLFCOSE_CRV_ML_DSA_65) ||
- (key->crv == WOLFCOSE_CRV_ML_DSA_87))) {
- /* ML-DSA COSE_Key: OKP with PQC curve.
- * Keys are large (pub up to 2592B, priv up to 4896B),
- * so we export directly into the output buffer to
- * avoid large stack allocations. */
- size_t dlMapEntries;
- word32 dlKeyLen;
- size_t hdrPos;
-
- dlMapEntries = (key->hasPrivate != 0u) ? (size_t)4 : (size_t)3;
- dlMapEntries += wolfCose_KeyOptionalEntries(key);
- ret = wc_CBOR_EncodeMapStart(&ctx, dlMapEntries);
+ if (key->kty == WOLFCOSE_KTY_AKP) {
+ /* RFC 9964 AKP COSE_Key: kty=AKP(7), required alg, public key at
+ * pub(-1), 32-byte private seed at priv(-2). The public key is
+ * large (1312-2592B) so it is exported directly into the output
+ * buffer to avoid a large stack copy; the seed is small. */
+ int emitPriv = 0;
+
+ /* Emit priv only when a valid 32-byte seed is attached; wolfCrypt
+ * does not retain the seed, so a keypair without one (e.g. from
+ * wc_MlDsaKey_MakeKey) is exported as a public-only AKP key. */
+ if ((key->hasPrivate != 0u) && (key->mldsaSeed != NULL) &&
+ (key->mldsaSeedLen == WOLFCOSE_MLDSA_SEED_SZ)) {
+ emitPriv = 1;
+ }
+
+ if (key->alg == WOLFCOSE_ALG_UNSET) {
+ /* RFC 9964: alg is REQUIRED for AKP keys (it carries the
+ * ML-DSA level). Never emit a key without it. */
+ ret = WOLFCOSE_E_COSE_BAD_ALG;
+ }
+ else {
+ size_t dlMapEntries = (emitPriv != 0) ? (size_t)3 : (size_t)2;
+ dlMapEntries += wolfCose_KeyOptionalEntries(key);
+ ret = wc_CBOR_EncodeMapStart(&ctx, dlMapEntries);
+ }
- /* 1: kty = OKP (1) */
+ /* 1: kty = AKP (7) */
if (ret == WOLFCOSE_SUCCESS) {
ret = wc_CBOR_EncodeUint(&ctx,
(uint64_t)WOLFCOSE_KEY_LABEL_KTY);
@@ -1668,26 +1707,20 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
if (ret == WOLFCOSE_SUCCESS) {
ret = wc_CBOR_EncodeUint(&ctx, (uint64_t)key->kty);
}
+ /* optional kid and the RFC 9964 required alg */
if (ret == WOLFCOSE_SUCCESS) {
ret = wolfCose_EncodeKeyOptionalFields(&ctx, key);
}
- /* -1: crv (negative for ML-DSA) */
- if (ret == WOLFCOSE_SUCCESS) {
- ret = wc_CBOR_EncodeInt(&ctx,
- (int64_t)WOLFCOSE_KEY_LABEL_CRV);
- }
- if (ret == WOLFCOSE_SUCCESS) {
- ret = wc_CBOR_EncodeInt(&ctx, (int64_t)key->crv);
- }
- /* -2: x (public key bstr) - direct export into output */
+ /* -1: pub (public key bstr) - direct export into output */
if (ret == WOLFCOSE_SUCCESS) {
ret = wc_CBOR_EncodeInt(&ctx,
- (int64_t)WOLFCOSE_KEY_LABEL_X);
+ (int64_t)WOLFCOSE_KEY_LABEL_PUB);
}
if (ret == WOLFCOSE_SUCCESS) {
/* Reserve 3 bytes for CBOR bstr header (2-byte length).
* All ML-DSA pub sizes (1312-2592) need this form. */
- hdrPos = ctx.idx;
+ size_t hdrPos = ctx.idx;
+ word32 dlKeyLen;
if ((ctx.idx + 3u) > ctx.bufSz) {
ret = WOLFCOSE_E_BUFFER_TOO_SMALL;
}
@@ -1716,37 +1749,15 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
}
}
}
- /* -4: d (private key, optional) - direct export */
- if ((ret == WOLFCOSE_SUCCESS) && (key->hasPrivate != 0u)) {
+ /* -2: priv (32-byte seed, only when a seed is attached) */
+ if ((ret == WOLFCOSE_SUCCESS) && (emitPriv != 0)) {
ret = wc_CBOR_EncodeInt(&ctx,
- (int64_t)WOLFCOSE_KEY_LABEL_D);
+ (int64_t)WOLFCOSE_KEY_LABEL_PRIV);
if (ret == WOLFCOSE_SUCCESS) {
- hdrPos = ctx.idx;
- if ((ctx.idx + 3u) > ctx.bufSz) {
- ret = WOLFCOSE_E_BUFFER_TOO_SMALL;
- }
- else {
- ctx.idx += 3u;
- dlKeyLen = (word32)(ctx.bufSz - ctx.idx);
- INJECT_FAILURE(WOLF_FAIL_MLDSA_EXPORT_PRIV, -1,
- ret = wc_MlDsaKey_ExportPrivRaw(
- key->key.mldsa,
- &ctx.buf[ctx.idx], &dlKeyLen));
- if (ret != 0) {
- ret = WOLFCOSE_E_CRYPTO;
- }
- else if ((dlKeyLen < 256u) || (dlKeyLen > 65535u)) {
- ret = WOLFCOSE_E_BUFFER_TOO_SMALL;
- }
- else {
- ctx.buf[hdrPos] = 0x59u;
- ctx.buf[hdrPos + 1u] =
- (uint8_t)((uint32_t)dlKeyLen >> 8u);
- ctx.buf[hdrPos + 2u] =
- (uint8_t)((uint32_t)dlKeyLen & 0xFFu);
- ctx.idx += (size_t)dlKeyLen;
- }
- }
+ INJECT_FAILURE(WOLF_FAIL_MLDSA_EXPORT_PRIV,
+ WOLFCOSE_E_CRYPTO,
+ ret = wc_CBOR_EncodeBstr(&ctx, key->mldsaSeed,
+ key->mldsaSeedLen));
}
}
@@ -1926,6 +1937,10 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
const uint8_t* qData = NULL; /* RSA: q (second prime) */
size_t qLen = 0;
+ const uint8_t* dpData = NULL; /* RSA: dP = d mod (p-1) */
+ size_t dpLen = 0;
+ const uint8_t* dqData = NULL; /* RSA: dQ = d mod (q-1) */
+ size_t dqLen = 0;
const uint8_t* qiData = NULL; /* RSA: qInv (CRT coefficient) */
size_t qiLen = 0;
#endif
@@ -1949,6 +1964,10 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
key->kid = NULL;
key->kidLen = 0;
key->hasPrivate = 0;
+#ifdef WOLFSSL_HAVE_MLDSA
+ key->mldsaSeed = NULL;
+ key->mldsaSeedLen = 0;
+#endif
ret = wc_CBOR_DecodeMapStart(&ctx, &mapCount);
@@ -2054,6 +2073,14 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
(label == WOLFCOSE_KEY_LABEL_RSA_Q)) {
ret = wc_CBOR_DecodeBstr(&ctx, &qData, &qLen);
}
+ else if ((ret == WOLFCOSE_SUCCESS) &&
+ (label == WOLFCOSE_KEY_LABEL_RSA_DP)) {
+ ret = wc_CBOR_DecodeBstr(&ctx, &dpData, &dpLen);
+ }
+ else if ((ret == WOLFCOSE_SUCCESS) &&
+ (label == WOLFCOSE_KEY_LABEL_RSA_DQ)) {
+ ret = wc_CBOR_DecodeBstr(&ctx, &dqData, &dqLen);
+ }
else if ((ret == WOLFCOSE_SUCCESS) &&
(label == WOLFCOSE_KEY_LABEL_RSA_QINV)) {
ret = wc_CBOR_DecodeBstr(&ctx, &qiData, &qiLen);
@@ -2178,11 +2205,16 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
else if ((yData != NULL) && (dData != NULL) &&
(qData != NULL) && (qiData != NULL)) {
+ /* dP/dQ (when present) are forwarded as-is; an imported
+ * private COSE_Key is trusted as much as its source, and
+ * wolfCrypt's sign-then-verify CRT fault check guards
+ * against a tampered CRT exponent producing a faulty sig. */
INJECT_FAILURE(WOLF_FAIL_RSA_PUBLIC_DECODE, -1,
ret = wc_RsaPrivateKeyDecodeRaw(nData, (word32)nLen,
xData, (word32)xLen, yData, (word32)yLen,
qiData, (word32)qiLen, dData, (word32)dLen,
- qData, (word32)qLen, NULL, 0, NULL, 0,
+ qData, (word32)qLen,
+ dpData, (word32)dpLen, dqData, (word32)dqLen,
key->key.rsa));
if (ret != 0) {
ret = WOLFCOSE_E_CRYPTO;
@@ -2207,45 +2239,74 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
else
#endif
#ifdef WOLFCOSE_HAVE_MLDSA
- if ((key->kty == WOLFCOSE_KTY_OKP) &&
- (key->key.mldsa != NULL) &&
- ((key->crv == WOLFCOSE_CRV_ML_DSA_44) ||
- (key->crv == WOLFCOSE_CRV_ML_DSA_65) ||
- (key->crv == WOLFCOSE_CRV_ML_DSA_87))) {
+ if ((key->kty == WOLFCOSE_KTY_AKP) &&
+ (key->key.mldsa != NULL)) {
+ /* RFC 9964 AKP: pub(-1) bstr was stashed in nData, the priv(-2)
+ * 32-byte seed in xData. The ML-DSA level comes from alg. */
+ const uint8_t* akpPub = nData;
+ size_t akpPubLen = nLen;
+ const uint8_t* akpSeed = xData;
+ size_t akpSeedLen = xLen;
byte dlLevel;
- if (key->crv == WOLFCOSE_CRV_ML_DSA_44) {
+
+ if (key->alg == WOLFCOSE_ALG_ML_DSA_44) {
dlLevel = 2;
}
- else if (key->crv == WOLFCOSE_CRV_ML_DSA_65) {
+ else if (key->alg == WOLFCOSE_ALG_ML_DSA_65) {
dlLevel = 3;
}
- else {
+ else if (key->alg == WOLFCOSE_ALG_ML_DSA_87) {
dlLevel = 5;
}
+ else {
+ dlLevel = 0;
+ }
- if (xData == NULL) {
+ if (dlLevel == 0u) {
+ ret = WOLFCOSE_E_COSE_BAD_ALG;
+ }
+ else if (key->crv != 0) {
+ /* RFC 9964 AKP keys carry no crv. */
+ ret = WOLFCOSE_E_COSE_BAD_HDR;
+ }
+ else if (akpPub == NULL) {
+ /* RFC 9964: pub is REQUIRED for AKP keys, public or
+ * private. Reject a seed-only key with no public part. */
ret = WOLFCOSE_E_COSE_BAD_HDR;
}
else {
- /* Set level before import */
- ret = wc_MlDsaKey_SetParams(key->key.mldsa,
- dlLevel);
+ ret = wc_MlDsaKey_SetParams(key->key.mldsa, dlLevel);
if (ret != 0) {
ret = WOLFCOSE_E_CRYPTO;
}
- else if (dData != NULL) {
- INJECT_FAILURE(WOLF_FAIL_MLDSA_IMPORT_PRIV, -1,
- ret = wc_MlDsaKey_ImportKey(
- key->key.mldsa,
- dData, (word32)dLen,
- xData, (word32)xLen));
- if (ret == 0) { key->hasPrivate = 1; }
- else { ret = WOLFCOSE_E_CRYPTO; }
+ else if (akpSeed != NULL) {
+ /* Private key: the seed is authoritative and fully
+ * determines the keypair, so the (RFC-required, already
+ * present) pub is not cross-checked against it -- doing
+ * so would need a multi-KB stack copy of the derived
+ * public key, which the bounded-stack design avoids. */
+ if (akpSeedLen != WOLFCOSE_MLDSA_SEED_SZ) {
+ ret = WOLFCOSE_E_COSE_BAD_HDR;
+ }
+ else {
+ INJECT_FAILURE(WOLF_FAIL_MLDSA_IMPORT_PRIV, -1,
+ ret = wc_MlDsaKey_MakeKeyFromSeed(
+ key->key.mldsa, akpSeed));
+ if (ret == 0) {
+ key->hasPrivate = 1;
+ /* Retain the seed (zero-copy into the input,
+ * like kid) so a decode->encode round-trip can
+ * re-emit the private key. */
+ key->mldsaSeed = akpSeed;
+ key->mldsaSeedLen = akpSeedLen;
+ }
+ else { ret = WOLFCOSE_E_CRYPTO; }
+ }
}
else {
INJECT_FAILURE(WOLF_FAIL_MLDSA_IMPORT_PUB, -1,
ret = wc_MlDsaKey_ImportPubRaw(
- key->key.mldsa, xData, (word32)xLen));
+ key->key.mldsa, akpPub, (word32)akpPubLen));
if (ret != 0) { ret = WOLFCOSE_E_CRYPTO; }
}
}
@@ -3362,21 +3423,21 @@ static int wolfCose_BuildSigStructure(const uint8_t* protectedHdr,
}
#ifdef WOLFCOSE_HAVE_MLDSA
-/* Map an ML-DSA COSE algorithm to the curve identifier its key must carry, so
- * a key of the wrong security level cannot satisfy a higher-level alg label. */
-static int wolfCose_MlDsaAlgCrv(int32_t alg, int32_t* crv)
+/* Map an ML-DSA COSE algorithm to the FIPS 204 security level its key must
+ * report, so a key of the wrong level cannot satisfy a higher-level alg. */
+static int wolfCose_MlDsaAlgLevel(int32_t alg, byte* level)
{
int ret = WOLFCOSE_SUCCESS;
switch (alg) {
case WOLFCOSE_ALG_ML_DSA_44:
- *crv = WOLFCOSE_CRV_ML_DSA_44;
+ *level = 2;
break;
case WOLFCOSE_ALG_ML_DSA_65:
- *crv = WOLFCOSE_CRV_ML_DSA_65;
+ *level = 3;
break;
case WOLFCOSE_ALG_ML_DSA_87:
- *crv = WOLFCOSE_CRV_ML_DSA_87;
+ *level = 5;
break;
default:
ret = WOLFCOSE_E_COSE_BAD_ALG;
@@ -3384,6 +3445,27 @@ static int wolfCose_MlDsaAlgCrv(int32_t alg, int32_t* crv)
}
return ret;
}
+
+/* RFC 9964: validate that an ML-DSA key is AKP-typed and reports the level
+ * required by alg. Replaces the old OKP+crv level binding. */
+static int wolfCose_MlDsaCheckKey(const WOLFCOSE_KEY* key, int32_t alg)
+{
+ int ret;
+ byte reqLevel = 0;
+
+ if ((key == NULL) || (key->kty != WOLFCOSE_KTY_AKP) ||
+ (key->key.mldsa == NULL)) {
+ ret = WOLFCOSE_E_COSE_KEY_TYPE;
+ }
+ else {
+ ret = wolfCose_MlDsaAlgLevel(alg, &reqLevel);
+ }
+ if ((ret == WOLFCOSE_SUCCESS) &&
+ ((byte)key->key.mldsa->level != reqLevel)) {
+ ret = WOLFCOSE_E_COSE_KEY_TYPE;
+ }
+ return ret;
+}
#endif /* WOLFCOSE_HAVE_MLDSA */
#if defined(WOLFCOSE_SIGN1_SIGN)
@@ -3652,18 +3734,10 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg,
if ((ret == WOLFCOSE_SUCCESS) && ((alg == WOLFCOSE_ALG_ML_DSA_44) ||
(alg == WOLFCOSE_ALG_ML_DSA_65) || (alg == WOLFCOSE_ALG_ML_DSA_87))) {
size_t expectedSigSz = 0;
- int32_t reqCrv = 0;
-
- if ((key->kty != WOLFCOSE_KTY_OKP) || (key->key.mldsa == NULL)) {
- ret = WOLFCOSE_E_COSE_KEY_TYPE;
- }
- /* Key level must match the algorithm level. */
+ /* RFC 9964: AKP key whose level matches the algorithm. */
if (ret == WOLFCOSE_SUCCESS) {
- ret = wolfCose_MlDsaAlgCrv(alg, &reqCrv);
- }
- if ((ret == WOLFCOSE_SUCCESS) && (key->crv != reqCrv)) {
- ret = WOLFCOSE_E_COSE_KEY_TYPE;
+ ret = wolfCose_MlDsaCheckKey(key, alg);
}
if (ret == WOLFCOSE_SUCCESS) {
@@ -4076,17 +4150,10 @@ int wc_CoseSign1_Verify(WOLFCOSE_KEY* key,
((alg == WOLFCOSE_ALG_ML_DSA_44) || (alg == WOLFCOSE_ALG_ML_DSA_65) ||
(alg == WOLFCOSE_ALG_ML_DSA_87))) {
int verified = 0;
- int32_t reqCrv = 0;
- if ((key->kty != WOLFCOSE_KTY_OKP) || (key->key.mldsa == NULL)) {
- ret = WOLFCOSE_E_COSE_KEY_TYPE;
- }
- /* Key level must match the algorithm level. */
+ /* RFC 9964: AKP key whose level matches the algorithm. */
if (ret == WOLFCOSE_SUCCESS) {
- ret = wolfCose_MlDsaAlgCrv(alg, &reqCrv);
- }
- if ((ret == WOLFCOSE_SUCCESS) && (key->crv != reqCrv)) {
- ret = WOLFCOSE_E_COSE_KEY_TYPE;
+ ret = wolfCose_MlDsaCheckKey(key, alg);
}
if (ret == WOLFCOSE_SUCCESS) {
INJECT_FAILURE(WOLF_FAIL_MLDSA_VERIFY, -1,
@@ -4283,25 +4350,14 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount,
}
#endif
#ifdef WOLFCOSE_HAVE_MLDSA
+ /* RFC 9964: ML-DSA signer must be an AKP key at the algId's level. */
else if (((signers[i].algId == WOLFCOSE_ALG_ML_DSA_44) ||
(signers[i].algId == WOLFCOSE_ALG_ML_DSA_65) ||
(signers[i].algId == WOLFCOSE_ALG_ML_DSA_87)) &&
- (signers[i].key->kty != WOLFCOSE_KTY_OKP)) {
+ (wolfCose_MlDsaCheckKey(signers[i].key, signers[i].algId)
+ != WOLFCOSE_SUCCESS)) {
ret = WOLFCOSE_E_COSE_KEY_TYPE;
}
- /* ML-DSA level must match algId. */
- else if ((signers[i].algId == WOLFCOSE_ALG_ML_DSA_44) &&
- (signers[i].key->crv != WOLFCOSE_CRV_ML_DSA_44)) {
- ret = WOLFCOSE_E_COSE_BAD_ALG;
- }
- else if ((signers[i].algId == WOLFCOSE_ALG_ML_DSA_65) &&
- (signers[i].key->crv != WOLFCOSE_CRV_ML_DSA_65)) {
- ret = WOLFCOSE_E_COSE_BAD_ALG;
- }
- else if ((signers[i].algId == WOLFCOSE_ALG_ML_DSA_87) &&
- (signers[i].key->crv != WOLFCOSE_CRV_ML_DSA_87)) {
- ret = WOLFCOSE_E_COSE_BAD_ALG;
- }
#endif
else {
/* No action required */
@@ -4514,16 +4570,9 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount,
(signer->algId == WOLFCOSE_ALG_ML_DSA_65) ||
(signer->algId == WOLFCOSE_ALG_ML_DSA_87))) {
size_t expectedSigSz = 0;
- int32_t reqCrv = 0;
- if (signer->key->key.mldsa == NULL) {
- ret = WOLFCOSE_E_COSE_KEY_TYPE;
- }
- /* Key level must match the algorithm level. */
+ /* RFC 9964: AKP key whose level matches the algorithm. */
if (ret == WOLFCOSE_SUCCESS) {
- ret = wolfCose_MlDsaAlgCrv(signer->algId, &reqCrv);
- }
- if ((ret == WOLFCOSE_SUCCESS) && (signer->key->crv != reqCrv)) {
- ret = WOLFCOSE_E_COSE_KEY_TYPE;
+ ret = wolfCose_MlDsaCheckKey(signer->key, signer->algId);
}
if (ret == WOLFCOSE_SUCCESS) {
ret = wolfCose_SigSize(signer->algId, &expectedSigSz);
@@ -4976,17 +5025,9 @@ int wc_CoseSign_Verify(const WOLFCOSE_KEY* verifyKey,
((alg == WOLFCOSE_ALG_ML_DSA_44) || (alg == WOLFCOSE_ALG_ML_DSA_65) ||
(alg == WOLFCOSE_ALG_ML_DSA_87))) {
int verified = 0;
- int32_t reqCrv = 0;
- if ((verifyKey->kty != WOLFCOSE_KTY_OKP) ||
- (verifyKey->key.mldsa == NULL)) {
- ret = WOLFCOSE_E_COSE_KEY_TYPE;
- }
- /* Key level must match the algorithm level. */
+ /* RFC 9964: AKP key whose level matches the algorithm. */
if (ret == WOLFCOSE_SUCCESS) {
- ret = wolfCose_MlDsaAlgCrv(alg, &reqCrv);
- }
- if ((ret == WOLFCOSE_SUCCESS) && (verifyKey->crv != reqCrv)) {
- ret = WOLFCOSE_E_COSE_KEY_TYPE;
+ ret = wolfCose_MlDsaCheckKey(verifyKey, alg);
}
if (ret == WOLFCOSE_SUCCESS) {
ret = wc_MlDsaKey_VerifyCtx(
diff --git a/tests/force_failure.h b/tests/force_failure.h
index 804ef87..56c3e3d 100644
--- a/tests/force_failure.h
+++ b/tests/force_failure.h
@@ -75,9 +75,9 @@ typedef enum {
WOLF_FAIL_MLDSA_SIGN, /* wc_MlDsaKey_SignCtx */
WOLF_FAIL_MLDSA_VERIFY, /* wc_MlDsaKey_VerifyCtx */
WOLF_FAIL_MLDSA_EXPORT_PUB, /* wc_MlDsaKey_ExportPubRaw */
- WOLF_FAIL_MLDSA_EXPORT_PRIV, /* wc_MlDsaKey_ExportPrivRaw */
+ WOLF_FAIL_MLDSA_EXPORT_PRIV, /* RFC 9964 priv seed emit (CBOR bstr) */
WOLF_FAIL_MLDSA_IMPORT_PUB, /* wc_MlDsaKey_ImportPubRaw */
- WOLF_FAIL_MLDSA_IMPORT_PRIV, /* wc_MlDsaKey_ImportKey */
+ WOLF_FAIL_MLDSA_IMPORT_PRIV, /* wc_MlDsaKey_MakeKeyFromSeed */
/* HMAC failures */
WOLF_FAIL_HMAC_SET_KEY, /* wc_HmacSetKey */
diff --git a/tests/test_cose.c b/tests/test_cose.c
index 63b42d0..a85a516 100644
--- a/tests/test_cose.c
+++ b/tests/test_cose.c
@@ -1594,11 +1594,14 @@ static void test_cose_sign1_ml_dsa(const char* label, int32_t alg, byte level)
static void test_cose_sign1_ml_dsa_level_mismatch(void)
{
WOLFCOSE_KEY signKey;
+ WOLFCOSE_KEY verKey;
wc_MlDsaKey dlKey;
+ wc_MlDsaKey dlKey5;
WC_RNG rng;
int ret = 0;
int rngInited = 0;
int dlInited = 0;
+ int dl5Inited = 0;
uint8_t payload[] = "ML-DSA level payload";
uint8_t scratch[8192];
uint8_t out[8192];
@@ -1634,12 +1637,22 @@ static void test_cose_sign1_ml_dsa_level_mismatch(void)
scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng);
TEST_ASSERT(ret == 0 && outLen > 0, "ml-dsa level sign");
}
+ /* RFC 9964: the key level is intrinsic to the key. Verifying a level-2
+ * (ML-DSA-44) message with an actual level-5 key must be rejected on the
+ * mismatch, before any crypto. */
if (ret == 0) {
- /* Key declares level 5 with no alg pin while the message is level 2;
- * verify must reject on the level mismatch, not attempt the crypto. */
- signKey.crv = WOLFCOSE_CRV_ML_DSA_87;
- signKey.alg = WOLFCOSE_ALG_UNSET;
- ret = wc_CoseSign1_Verify(&signKey, out, outLen,
+ ret = wc_MlDsaKey_Init(&dlKey5, NULL, INVALID_DEVID);
+ if (ret == 0) { dl5Inited = 1; }
+ if (ret == 0) { ret = wc_MlDsaKey_SetParams(&dlKey5, WC_ML_DSA_87); }
+ if (ret == 0) { ret = wc_MlDsaKey_MakeKey(&dlKey5, &rng); }
+ TEST_ASSERT(ret == 0, "dl5 keygen");
+ }
+ if (ret == 0) {
+ (void)wc_CoseKey_Init(&verKey);
+ (void)wc_CoseKey_SetMlDsa(&verKey, WOLFCOSE_ALG_ML_DSA_87, &dlKey5);
+ /* Clear the alg pin so verify reaches the intrinsic level check. */
+ verKey.alg = WOLFCOSE_ALG_UNSET;
+ ret = wc_CoseSign1_Verify(&verKey, out, outLen,
NULL, 0, NULL, 0,
scratch, sizeof(scratch),
&hdr, &decPayload, &decPayloadLen);
@@ -1648,6 +1661,7 @@ static void test_cose_sign1_ml_dsa_level_mismatch(void)
}
if (dlInited != 0) { (void)wc_MlDsaKey_Free(&dlKey); }
+ if (dl5Inited != 0) { (void)wc_MlDsaKey_Free(&dlKey5); }
if (rngInited != 0) { (void)wc_FreeRng(&rng); }
}
#endif /* WOLFCOSE_HAVE_MLDSA */
@@ -1886,6 +1900,7 @@ static void test_cose_key_mldsa(const char* label, int32_t alg,
wc_MlDsaKey dlKey;
WC_RNG rng;
static const uint8_t kid[] = "ml-dsa-key-1";
+ uint8_t seed[WOLFCOSE_MLDSA_SEED_SZ];
int ret;
(void)label;
@@ -1903,13 +1918,18 @@ static void test_cose_key_mldsa(const char* label, int32_t alg,
(void)wc_MlDsaKey_Free(&dlKey); wc_FreeRng(&rng); return;
}
- ret = wc_MlDsaKey_MakeKey(&dlKey, &rng);
+ /* RFC 9964: derive the key from a seed so the conformant 32-byte private
+ * key (the seed) is available to encode. */
+ ret = wc_RNG_GenerateBlock(&rng, seed, (word32)sizeof(seed));
+ if (ret == 0) {
+ ret = wc_MlDsaKey_MakeKeyFromSeed(&dlKey, seed);
+ }
TEST_ASSERT(ret == 0, "dl keygen");
if (ret != 0) { wc_MlDsaKey_Free(&dlKey); wc_FreeRng(&rng); return; }
(void)wc_CoseKey_Init(&key);
- ret = wc_CoseKey_SetMlDsa(&key, alg, &dlKey);
- TEST_ASSERT(ret == 0 && key.kty == WOLFCOSE_KTY_OKP, "key set dl");
+ ret = wc_CoseKey_SetMlDsa_ex(&key, alg, &dlKey, seed, sizeof(seed));
+ TEST_ASSERT(ret == 0 && key.kty == WOLFCOSE_KTY_AKP, "key set dl");
key.kid = kid;
key.kidLen = sizeof(kid) - 1u;
@@ -1917,7 +1937,9 @@ static void test_cose_key_mldsa(const char* label, int32_t alg,
/* empty-brace-scan: allow - test-local temporary scope */
{
uint8_t cbuf[8192];
+ uint8_t rebuf[8192];
size_t cLen = 0;
+ size_t reLen = 0;
WOLFCOSE_KEY key2;
wc_MlDsaKey dlKey2;
@@ -1928,13 +1950,20 @@ static void test_cose_key_mldsa(const char* label, int32_t alg,
(void)wc_CoseKey_Init(&key2);
key2.key.mldsa = &dlKey2;
ret = wc_CoseKey_Decode(&key2, cbuf, cLen);
- TEST_ASSERT(ret == 0 && key2.kty == WOLFCOSE_KTY_OKP &&
- key2.crv == key.crv && key2.hasPrivate == 1 &&
+ TEST_ASSERT(ret == 0 && key2.kty == WOLFCOSE_KTY_AKP &&
+ key2.hasPrivate == 1 &&
key2.alg == alg &&
key2.kidLen == (sizeof(kid) - 1u) &&
memcmp(key2.kid, kid, sizeof(kid) - 1u) == 0,
"key dl decode");
+ /* Seed retained on decode, so re-encoding reproduces the same key. */
+ reLen = sizeof(rebuf);
+ ret = wc_CoseKey_Encode(&key2, rebuf, sizeof(rebuf), &reLen);
+ TEST_ASSERT(ret == 0 && reLen == cLen &&
+ memcmp(rebuf, cbuf, cLen) == 0,
+ "key dl decode->encode round-trip");
+
/* Verify decoded key can sign/verify */
/* empty-brace-scan: allow - test-local temporary scope */
{
@@ -3172,18 +3201,18 @@ static void test_cose_key_mldsa_public_only(void)
wc_MlDsaKey_MakeKey(&dlKey, &rng);
wc_MlDsaKey_ExportPubRaw(&dlKey, xBuf, &xSz);
- /* Build a public-only OKP key (no d label) */
+ /* Build a public-only AKP key (RFC 9964): kty=AKP, required alg, pub(-1) */
enc.buf = pubBuf; enc.bufSz = sizeof(pubBuf); enc.idx = 0;
wc_CBOR_EncodeMapStart(&enc, 3);
wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_KTY);
- wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_OKP);
- wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_CRV);
- wc_CBOR_EncodeInt(&enc, WOLFCOSE_CRV_ML_DSA_44);
- wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_X);
+ wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_AKP);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_ALG);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_ALG_ML_DSA_44);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_PUB);
wc_CBOR_EncodeBstr(&enc, xBuf, (size_t)xSz);
(void)wc_CoseKey_Init(&key);
- key.kty = WOLFCOSE_KTY_OKP;
+ key.kty = WOLFCOSE_KTY_AKP;
key.key.mldsa = &dlKey2;
ret = wc_CoseKey_Decode(&key, pubBuf, enc.idx);
TEST_ASSERT(ret == 0, "dl pub-only decode");
@@ -3225,6 +3254,116 @@ static void test_cose_key_mldsa_public_only(void)
(void)wc_MlDsaKey_Free(&dlKey2);
(void)wc_FreeRng(&rng);
}
+
+/* RFC 9964 AKP conformance: reject malformed ML-DSA COSE_Key encode/decode. */
+static void test_cose_key_mldsa_negative(void)
+{
+ WOLFCOSE_KEY key;
+ wc_MlDsaKey dlKey, dlKey2;
+ WC_RNG rng;
+ uint8_t seed[WOLFCOSE_MLDSA_SEED_SZ];
+ uint8_t pubBuf[2048];
+ word32 pubSz = sizeof(pubBuf);
+ uint8_t buf[2048];
+ uint8_t outBuf[8192];
+ size_t outLen;
+ WOLFCOSE_CBOR_CTX enc;
+ int ret;
+
+ TEST_LOG(" [Key ML-DSA negative]\n");
+
+ wc_InitRng(&rng);
+ wc_MlDsaKey_Init(&dlKey, NULL, INVALID_DEVID);
+ wc_MlDsaKey_Init(&dlKey2, NULL, INVALID_DEVID);
+ wc_MlDsaKey_SetParams(&dlKey, WC_ML_DSA_44);
+ wc_RNG_GenerateBlock(&rng, seed, (word32)sizeof(seed));
+ wc_MlDsaKey_MakeKeyFromSeed(&dlKey, seed);
+ wc_MlDsaKey_ExportPubRaw(&dlKey, pubBuf, &pubSz);
+
+ /* Encode: AKP key with no alg is rejected (RFC 9964 requires alg). */
+ (void)wc_CoseKey_Init(&key);
+ (void)wc_CoseKey_SetMlDsa_ex(&key, WOLFCOSE_ALG_ML_DSA_44, &dlKey,
+ seed, sizeof(seed));
+ key.alg = WOLFCOSE_ALG_UNSET;
+ outLen = sizeof(outBuf);
+ ret = wc_CoseKey_Encode(&key, outBuf, sizeof(outBuf), &outLen);
+ TEST_ASSERT(ret == WOLFCOSE_E_COSE_BAD_ALG, "dl encode rejects missing alg");
+
+ /* Encode: a private keypair with no seed attached falls back to a
+ * public-only AKP key (the RFC 9964 private value is the seed). */
+ (void)wc_CoseKey_Init(&key);
+ (void)wc_CoseKey_SetMlDsa(&key, WOLFCOSE_ALG_ML_DSA_44, &dlKey);
+ outLen = sizeof(outBuf);
+ ret = wc_CoseKey_Encode(&key, outBuf, sizeof(outBuf), &outLen);
+ TEST_ASSERT(ret == WOLFCOSE_SUCCESS, "dl encode public-only without seed");
+ (void)wc_CoseKey_Init(&key);
+ key.key.mldsa = &dlKey2;
+ ret = wc_CoseKey_Decode(&key, outBuf, outLen);
+ TEST_ASSERT(ret == WOLFCOSE_SUCCESS && key.hasPrivate == 0,
+ "dl public-only decode has no private");
+
+ /* Decode: AKP private key with no pub is rejected. */
+ enc.buf = buf; enc.bufSz = sizeof(buf); enc.idx = 0;
+ wc_CBOR_EncodeMapStart(&enc, 3);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_KTY);
+ wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_AKP);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_ALG);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_ALG_ML_DSA_44);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_PRIV);
+ wc_CBOR_EncodeBstr(&enc, seed, sizeof(seed));
+ (void)wc_CoseKey_Init(&key);
+ key.key.mldsa = &dlKey2;
+ ret = wc_CoseKey_Decode(&key, buf, enc.idx);
+ TEST_ASSERT(ret == WOLFCOSE_E_COSE_BAD_HDR, "dl decode rejects missing pub");
+
+ /* Decode: AKP key with no alg is rejected. */
+ enc.idx = 0;
+ wc_CBOR_EncodeMapStart(&enc, 2);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_KTY);
+ wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_AKP);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_PUB);
+ wc_CBOR_EncodeBstr(&enc, pubBuf, (size_t)pubSz);
+ (void)wc_CoseKey_Init(&key);
+ key.key.mldsa = &dlKey2;
+ ret = wc_CoseKey_Decode(&key, buf, enc.idx);
+ TEST_ASSERT(ret == WOLFCOSE_E_COSE_BAD_ALG, "dl decode rejects missing alg");
+
+ /* Decode: AKP private key with a wrong-length seed is rejected. */
+ enc.idx = 0;
+ wc_CBOR_EncodeMapStart(&enc, 4);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_KTY);
+ wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_AKP);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_ALG);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_ALG_ML_DSA_44);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_PUB);
+ wc_CBOR_EncodeBstr(&enc, pubBuf, (size_t)pubSz);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_PRIV);
+ wc_CBOR_EncodeBstr(&enc, seed, (size_t)16);
+ (void)wc_CoseKey_Init(&key);
+ key.key.mldsa = &dlKey2;
+ ret = wc_CoseKey_Decode(&key, buf, enc.idx);
+ TEST_ASSERT(ret == WOLFCOSE_E_COSE_BAD_HDR,
+ "dl decode rejects wrong seed length");
+
+ /* Decode: AKP key carrying a crv is rejected (AKP has no crv). */
+ enc.idx = 0;
+ wc_CBOR_EncodeMapStart(&enc, 3);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_KTY);
+ wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_AKP);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_ALG);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_ALG_ML_DSA_44);
+ wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_CRV);
+ wc_CBOR_EncodeInt(&enc, 5);
+ (void)wc_CoseKey_Init(&key);
+ key.key.mldsa = &dlKey2;
+ ret = wc_CoseKey_Decode(&key, buf, enc.idx);
+ TEST_ASSERT(ret == WOLFCOSE_E_COSE_BAD_HDR, "dl decode rejects crv on AKP");
+
+ wc_CoseKey_Free(&key);
+ (void)wc_MlDsaKey_Free(&dlKey);
+ (void)wc_MlDsaKey_Free(&dlKey2);
+ (void)wc_FreeRng(&rng);
+}
#endif /* WOLFCOSE_HAVE_MLDSA */
#ifdef WOLFCOSE_HAVE_ES256
@@ -12714,6 +12853,7 @@ static void test_force_failure_crypto(void)
uint8_t keyBuf[8192];
uint8_t dlScratch[4096]; /* Larger scratch for ML-DSA sig */
uint8_t dlCoseMsg[4096];
+ uint8_t dlSeed[WOLFCOSE_MLDSA_SEED_SZ];
size_t dlCoseMsgLen;
size_t keyLen;
@@ -12721,10 +12861,14 @@ static void test_force_failure_crypto(void)
wc_MlDsaKey_Init(&dlKey, NULL, INVALID_DEVID);
ret = wc_MlDsaKey_SetParams(&dlKey, WC_ML_DSA_44);
if (ret == 0) {
- ret = wc_MlDsaKey_MakeKey(&dlKey, &rng);
+ ret = wc_RNG_GenerateBlock(&rng, dlSeed, (word32)sizeof(dlSeed));
}
if (ret == 0) {
- (void)wc_CoseKey_SetMlDsa(&key, WOLFCOSE_ALG_ML_DSA_44, &dlKey);
+ ret = wc_MlDsaKey_MakeKeyFromSeed(&dlKey, dlSeed);
+ }
+ if (ret == 0) {
+ (void)wc_CoseKey_SetMlDsa_ex(&key, WOLFCOSE_ALG_ML_DSA_44, &dlKey,
+ dlSeed, sizeof(dlSeed));
/* Test ML-DSA export public failure */
keyLen = sizeof(keyBuf);
@@ -13074,6 +13218,7 @@ static void test_force_failure_crypto(void)
WOLFCOSE_KEY key;
wc_MlDsaKey dlKey;
uint8_t keyBuf[8192];
+ uint8_t impSeed[WOLFCOSE_MLDSA_SEED_SZ];
size_t keyLen;
WOLFCOSE_KEY decodedKey;
wc_MlDsaKey decodedDlKey;
@@ -13082,10 +13227,15 @@ static void test_force_failure_crypto(void)
wc_MlDsaKey_Init(&dlKey, NULL, INVALID_DEVID);
ret = wc_MlDsaKey_SetParams(&dlKey, WC_ML_DSA_44);
if (ret == 0) {
- ret = wc_MlDsaKey_MakeKey(&dlKey, &rng);
+ ret = wc_RNG_GenerateBlock(&rng, impSeed, (word32)sizeof(impSeed));
+ }
+ if (ret == 0) {
+ ret = wc_MlDsaKey_MakeKeyFromSeed(&dlKey, impSeed);
}
if (ret == 0) {
- (void)wc_CoseKey_SetMlDsa(&key, WOLFCOSE_ALG_ML_DSA_44, &dlKey);
+ /* Encode a private key (seed) so decode reaches the priv path. */
+ (void)wc_CoseKey_SetMlDsa_ex(&key, WOLFCOSE_ALG_ML_DSA_44, &dlKey,
+ impSeed, sizeof(impSeed));
/* Encode the key */
keyLen = sizeof(keyBuf);
@@ -16483,6 +16633,7 @@ int test_cose(void)
#endif
#ifdef WOLFCOSE_HAVE_MLDSA
test_cose_key_mldsa_public_only();
+ test_cose_key_mldsa_negative();
#endif
#if defined(WOLFCOSE_HAVE_ES256) || defined(WOLFCOSE_HAVE_EDDSA)
test_cose_key_decode_private_only();
diff --git a/tools/wolfcose_tool.c b/tools/wolfcose_tool.c
index 007c931..c2bf1f6 100644
--- a/tools/wolfcose_tool.c
+++ b/tools/wolfcose_tool.c
@@ -82,6 +82,18 @@
#define EXIT_CRYPTO 2
#define EXIT_IO 3
+/* Portable secure-zero for sensitive key material; volatile writes are not
+ * optimized away. wc_ForceZero is only public in wolfSSL >= 5.8.4, so the tool
+ * carries its own to match the library's supported range. */
+static void tool_force_zero(void* mem, size_t len)
+{
+ volatile unsigned char* p = (volatile unsigned char*)mem;
+ size_t i;
+ for (i = 0u; i < len; i++) {
+ p[i] = 0u;
+ }
+}
+
static void usage(void)
{
fprintf(stderr,
@@ -312,6 +324,7 @@ static int tool_keygen(int32_t alg, const char* algStr, const char* outPath)
alg == WOLFCOSE_ALG_ML_DSA_87) {
wc_MlDsaKey dl;
byte level;
+ uint8_t seed[WOLFCOSE_MLDSA_SEED_SZ];
if (alg == WOLFCOSE_ALG_ML_DSA_44) level = WC_ML_DSA_44;
else if (alg == WOLFCOSE_ALG_ML_DSA_65) level = WC_ML_DSA_65;
else level = WC_ML_DSA_87;
@@ -320,16 +333,23 @@ static int tool_keygen(int32_t alg, const char* algStr, const char* outPath)
ret = wc_MlDsaKey_SetParams(&dl, level);
}
if (ret == 0) {
- ret = wc_MlDsaKey_MakeKey(&dl, &rng);
+ /* RFC 9964: derive from a seed so the conformant private key
+ * (the 32-byte seed) can be written to the COSE_Key. */
+ ret = wc_RNG_GenerateBlock(&rng, seed, (word32)sizeof(seed));
+ }
+ if (ret == 0) {
+ ret = wc_MlDsaKey_MakeKeyFromSeed(&dl, seed);
}
if (ret != 0) {
fprintf(stderr, "ML-DSA keygen failed: %d\n", ret);
+ tool_force_zero(seed, sizeof(seed));
wc_MlDsaKey_Free(&dl);
wc_FreeRng(&rng);
return EXIT_CRYPTO;
}
- wc_CoseKey_SetMlDsa(&coseKey, alg, &dl);
+ wc_CoseKey_SetMlDsa_ex(&coseKey, alg, &dl, seed, sizeof(seed));
ret = wc_CoseKey_Encode(&coseKey, keyBuf, sizeof(keyBuf), &keyLen);
+ tool_force_zero(seed, sizeof(seed));
wc_MlDsaKey_Free(&dl);
}
else
@@ -693,10 +713,7 @@ static int tool_verify(const char* keyPath, const char* inPath)
else
#endif
#ifdef WOLFCOSE_HAVE_MLDSA
- if (ret == 0 && kty == WOLFCOSE_KTY_OKP &&
- (crv == WOLFCOSE_CRV_ML_DSA_44 ||
- crv == WOLFCOSE_CRV_ML_DSA_65 ||
- crv == WOLFCOSE_CRV_ML_DSA_87)) {
+ if (ret == 0 && kty == WOLFCOSE_KTY_AKP) {
wc_MlDsaKey dl;
keyMatched = 1;
wc_CoseKey_Init(&coseKey);