If you are signing CBOR payloads on an embedded device and you have started worrying about “harvest now, decrypt later,” that worry now extends to signatures too. Long-lived firmware artifacts, attestation reports, supply-chain manifests: anything signed today with ECDSA or RSA can be retroactively forged by an adversary with a cryptographically relevant quantum computer.
wolfCOSE has native ML-DSA-44, ML-DSA-65, and ML-DSA-87 support. It is the first COSE implementation, in any language, with production-tested post-quantum digital signatures. The ML-DSA implementation it calls into is NIST CAVP-validated, so the quantum-safe signatures wolfCOSE produces come from algorithm code that has passed NIST’s known-answer test vectors.
The COSE PQC Landscape
| Library | PQC Support |
|---|---|
| wolfCOSE | ML-DSA-44 / 65 / 87 |
| t_cose | None |
| COSE-C | None |
| pycose | None |
| go-cose | None |
| libcose | None |
| COSE-JAVA | None |
What Is Actually in the Box
The cleanest way to describe wolfCOSE’s ML-DSA implementation is to be precise about which spec each layer comes from:
- The cryptographic primitive is ML-DSA from FIPS 204 final (published August 2024). We get it from wolfCrypt, whose ML-DSA implementation is NIST CAVP-validated under CAVP validation #41047. wolfCOSE calls the canonical FIPS 204 API (wc_MlDsaKey), and signs over the context-message form FIPS 204 final requires.
- The COSE algorithm registration comes from `draft-ietf-cose-dilithium` (consolidating into draft-ietf-cose-pqc-algs). That draft assigns COSE algorithm IDs -48, -49, -50 to ML-DSA-44 / 65 / 87 and defines the COSE_Key encoding (kty=OKP, crv=ML-DSA-*).
- The COSE message envelope is RFC 9052. Once you have a signature primitive and an algorithm ID, ML-DSA drops into COSE_Sign1 and COSE_Sign exactly the way ES256 does.
The honest framing: the cryptography is final and CAVP-validated at the algorithm level; the COSE algorithm IDs are still in IETF draft, which means the integer values could shift before the RFC is published. We track the latest draft and will update if IANA assigns different code points. The actual signatures you produce today are FIPS 204 ML-DSA from a CAVP-validated implementation. The integers we wrap them in are the only thing that is draft. (The full PQC FIPS 140-3 module, which packages these CAVP-validated algorithms under a CMVP certificate, is a separate validation that is still in progress; classical algorithms like ES256 are covered by wolfCrypt FIPS 140-3 module certificate #4718 today.)
Quantum-Safe Crypto That Is CAVP-Validated
The post-quantum signatures wolfCOSE emits are only as trustworthy as the crypto underneath them, and that crypto is wolfCrypt. wolfCrypt’s PQC family is CNSA 2.0-aligned and tracks the finalized NIST standards: ML-KEM for key establishment (FIPS 203), ML-DSA and SLH-DSA for digital signatures (FIPS 204 and FIPS 205), plus verify-only LMS and XMSS for stateful hash-based signing. Those implementations have passed NIST CAVP testing: the known-answer validation that confirms an implementation actually computes each algorithm correctly against NIST’s test vectors. CAVP validation is also the prerequisite step before a module can go to the CMVP for a FIPS 140-3 certificate.
For wolfCOSE this means the ML-DSA you sign with is not a from-scratch reimplementation: it is the same CAVP-validated wolfCrypt code used across wolfBoot, wolfTPM, wolfMQTT, and the wolfSSL TLS 1.3 stack. wolfSSL’s migration story is hybrid by design, pairing today’s FIPS-approved classical algorithms with PQC as SP 800-227 allows, which is exactly the COSE_Sign multi-signer pattern shown below. A dedicated FIPS 140-3 module that packages the PQC algorithms under a single CMVP certificate is still in progress; until it posts, the algorithm-level CAVP validation is what backs the quantum-safe claim, and that is an honest, verifiable thing to put on the box.
Signing with ML-DSA in COSE_Sign1
Once your wolfSSL is built with –enable-mldsa, signing a CBOR payload with a 2,420-byte ML-DSA-44 signature looks identical to signing it with ES256:
#include#include wc_MlDsaKey dlKey; WOLFCOSE_KEY coseKey; WC_RNG rng; wc_InitRng(&rng); wc_MlDsaKey_Init(&dlKey, NULL, INVALID_DEVID); wc_MlDsaKey_SetParams(&dlKey, WC_ML_DSA_44); wc_MlDsaKey_MakeKey(&dlKey, &rng); wc_CoseKey_Init(&coseKey); wc_CoseKey_SetMlDsa(&coseKey, WOLFCOSE_ALG_ML_DSA_44, &dlKey); uint8_t scratch[8192]; int ret = wc_CoseSign1_Sign(&coseKey, WOLFCOSE_ALG_ML_DSA_44, NULL, 0, /* kid, kidLen */ payload, payloadLen, NULL, 0, NULL, 0, /* detached payload, ext-AAD */ scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng);
That is the entire integration surface. The verifier side uses wc_CoseSign1_Verify with a public-only wc_MlDsaKey, and the COSE_Key serialization works for ML-DSA the same way it works for Ed25519: kty=OKP, with crv set to the ML-DSA level.
Hybrid Signatures with COSE_Sign
The reason wolfCOSE has full COSE_Sign support (not just Sign1) is that the most likely deployment path for ML-DSA over the next several years is alongside a classical signature, not as a replacement. Standards bodies are explicit that hybrid is the recommended migration approach, and COSE_Sign is the COSE structure for it.
Here is a firmware manifest signed by both ES256 (today’s verifier) and ML-DSA-65 (tomorrow’s verifier), in one COSE structure:
/* eccKey and mlDsaKey are WOLFCOSE_KEY*, set up earlier via
wc_CoseKey_SetEcc() and wc_CoseKey_SetMlDsa() respectively. */
WOLFCOSE_SIGNATURE signers[2] = {
{ .algId = WOLFCOSE_ALG_ES256,
.key = &eccKey,
.kid = (const uint8_t*)"vendor-classic", .kidLen = 14 },
{ .algId = WOLFCOSE_ALG_ML_DSA_65,
.key = &mlDsaKey,
.kid = (const uint8_t*)"vendor-pqc", .kidLen = 10 },
};
ret = wc_CoseSign_Sign(signers, 2,
firmware, firmwareLen,
NULL, 0, NULL, 0,
scratch, sizeof(scratch),
out, sizeof(out), &outLen, &rng);
Per RFC 9052 Section 4.1, the verifier walks the COSE_Signature array and selects the signer to validate by matching the alg and kid headers it knows about, not by array position. Devices in the field that still only know ES256 select the vendor-classic signer and skip the ML-DSA one. Newer devices select the vendor-pqc signer and skip the ECC one. When everyone has migrated, you drop the classical signer and your code path is one line shorter. No re-signing campaigns, no flag-day cutovers.
The Wire-Size Impact
Post-quantum signatures are not a free lunch. The wire-size impact is real and worth knowing before you architect a system around it.
| Algorithm | Public Key | Signature | NIST Level |
|---|---|---|---|
| ES256 (P-256) | 64 B | 64 B | (classical 128) |
| Ed25519 | 32 B | 64 B | (classical 128) |
| ML-DSA-44 | 1,312 B | 2,420 B | 2 |
| ML-DSA-65 | 1,952 B | 3,293 B | 3 |
| ML-DSA-87 | 2,592 B | 4,595 B | 5 |
A COSE_Sign1 with ML-DSA-44 is about 40x larger than the same message with Ed25519. If you are shipping firmware over LoRaWAN, that matters. If you are storing attestation reports in a database, it matters less. Plan accordingly.
What ML-DSA does not cost you, surprisingly, is verification time. ML-DSA verification is faster than ECDSA P-256 verification on a Cortex-M4, because there is no point multiplication. It is all small-integer arithmetic over polynomial rings. The expensive operation is signing, and even that is manageable. The real cost is the bytes on the wire.
Why We Did This in COSE Now
There is a fair question: why bother integrating ML-DSA into COSE now, before the IETF draft is final? Three reasons:
- CNSA 2.0 timelines. The NSA’s CNSA 2.0 guidance requires PQC algorithms in software/firmware signing by 2025, full PQC-only by 2030. Devices being designed today will outlive the deadline. Shipping the COSE integration now means people who need to start prototyping have something to build against.
- The crypto is final, the wire format is the easy part. FIPS 204 is not moving. Whatever IANA assigns as final COSE alg IDs, swapping -48/-49/-50 for the final values is a one-line change on our side and a recompile on yours.
- Constrained-device PQC needs a real home. Most PQC-in-protocol work has happened in TLS 1.3 and CMS. COSE is what you actually use on a microcontroller that does not have room for an X.509 stack: IoT firmware signing, attestation tokens, sensor authentication. If COSE does not get PQC, the embedded story has a hole in it.
Try It
Build wolfSSL with –enable-mldsa (or –enable-cryptonly –enable-mldsa for a PQC-only build). The renamed wc_MlDsaKey API lands in wolfSSL after v5.9.1-stable, so for now build ML-DSA against wolfSSL master; v5.8.0–v5.9.1 work for everything except ML-DSA. Then:
git clone https://github.com/wolfSSL/wolfCOSE cd wolfCOSE make tool ./tools/wolfcose_tool keygen -a ML-DSA-44 -o pqc.key ./tools/wolfcose_tool sign -k pqc.key -a ML-DSA-44 -i data.bin -o data.cose ./tools/wolfcose_tool verify -k pqc.key -i data.cose ./tools/wolfcose_tool test -a ML-DSA-87
A complete keygen / sign / verify lifecycle for ML-DSA-44 lives in examples/lifecycle_demo.c and runs via make demo. ML-DSA-65 and ML-DSA-87 round-trips go through the CLI: ./tools/wolfcose_tool test -a ML-DSA-65.
What Is Next
ML-DSA is the first PQC algorithm in wolfCOSE. The roadmap from here:
- SLH-DSA (FIPS 205, SPHINCS+): Stateless hash-based signatures. Slower than ML-DSA but with a different security assumption (hash functions vs. lattices). Useful for certificate roots where signing speed does not matter.
- LMS / XMSS (NIST SP 800-208): Stateful hash-based signatures. The right tool for firmware signing where you can manage the state.
- ML-KEM (FIPS 203, Kyber): For COSE_Encrypt recipient algorithms, replacing ECDH-ES.
If you have a deployment where one of these is on a critical path, get in touch. That is how we prioritize.
Resource
- Repo: https://github.com/wolfSSL/wolfCOSE
- Documentation: https://github.com/wolfSSL/wolfCOSE/wiki
- FIPS 140-3 + Post-Quantum (background): wolfCrypt FIPS 140-3 with Post-Quantum Cryptography
wolfCOSE is GPLv3 or commercial. Contact us for any support or questions at: facts@wolfssl.com
If you have questions about any of the above, please contact us at facts@wolfssl.com or call us at +1 425 245 8247.
Download wolfSSL Now

