feat: add GnuTLS ML-DSA (FIPS 204 / RFC 9964) backend

Implement the GnuTLS side of ML-DSA, deferred from #227. GnuTLS does ML-DSA
when built against a PQC provider (e.g. --with-leancrypto); it is enabled when
GnuTLS >= 3.8.10 is present and WITH_ML_DSA is set.

GnuTLS has no raw ML-DSA import, so gnutls_process_mldsa() builds the encoded
key from the JWK's raw "pub" / 32-byte "priv" seed (a SubjectPublicKeyInfo for
public keys, a seed-form PKCS#8 for private keys) and imports that. Signing
uses gnutls_privkey_sign_data2() / gnutls_pubkey_verify_data2() with
GNUTLS_SIGN_MLDSA44/65/87; the sign path pins the expected GNUTLS_PK_MLDSA*
so a mismatched-variant key is rejected. Native-key -> JWK export recovers the
raw public key from the SPKI and the 32-byte seed from a seed-form PKCS#8.

- A seedless (expanded-only) private key has no FIPS-204 seed and cannot be a
  private AKP JWK. GnuTLS's seed PKCS#8 export null-derefs on such a key, so
  export probes for the seed via a plain PKCS#8 export first and downgrades to
  a public export when absent (matching the OpenSSL backend) instead of
  crashing.
- The private seed is wiped from its stack buffer after import.

LIBJWT_HAVE_ML_DSA is now defined for OpenSSL >= 3.5 OR GnuTLS >= 3.8.10, and
each backend's ML-DSA code carries its own version guard so a build can enable
the feature via either backend independently. MbedTLS still has no ML-DSA and
rejects AKP keys via the common null-guard.

The ML-DSA tests gain a runtime capability probe so the sign/verify/export and
seedless-downgrade tests run on every ML-DSA-capable backend (OpenSSL and a
PQC-enabled GnuTLS) and skip the rest. README and the public header note the
GnuTLS path.

closes #301

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ben Collins <bcollins@libjwt.io>
12 files changed
tree: 44534c6057ca5762c782de3ea8190992cf0f1f4e
  1. .github/
  2. cmake/
  3. doxygen/
  4. images/
  5. include/
  6. libjwt/
  7. tests/
  8. tools/
  9. .gitignore
  10. CLAUDE.md
  11. CMakeLists.txt
  12. codecov.yml
  13. LICENSE
  14. README.md
  15. SECURITY.md
README.md

LibJWT - The C JWT Library

codecov

maClara

:bulb: Supported Standards

StandardRFCDescription
JWS:page_facing_up: RFC-7515JSON Web Signature
JWE:page_facing_up: RFC-7516JSON Web Encryption
JWK:page_facing_up: RFC-7517JSON Web Keys and Sets
JWA:page_facing_up: RFC-7518JSON Web Algorithms
JWT:page_facing_up: RFC-7519JSON Web Token

[!NOTE] Throughout this documentation you will see links such as the ones above to RFC documents. These are relevant to that particular part of the library and are helpful to understand some of the specific standards that shaped the development of LibJWT.

:construction: Build Prerequisites

Required

  • A JSON library: either Jansson (>= 2.0, the default) or json-c (>= 0.16, selected with -DWITH_JSON_C=ON). The two are interchangeable.
  • CMake (>= 3.7)

Crypto support

  • OpenSSL (>= 3.0.0)
  • GnuTLS (>= 3.8.8)
  • MbedTLS (>= 3.6.0)

[!NOTE] At least one crypto backend is required, but any non-empty combination works. OpenSSL is enabled by default and can be disabled with -DWITH_OPENSSL=OFF. Each backend parses and converts JWK(S) natively.

Algorithm support matrix

JWS Algorithm algOpenSSLGnuTLSMbedTLS
HS256 HS384 HS512:white_check_mark::white_check_mark::white_check_mark:
ES256 ES384 ES512:white_check_mark::white_check_mark::white_check_mark:
RS256 RS384 RS512:white_check_mark::white_check_mark::white_check_mark:
EdDSA using ED25519:white_check_mark::white_check_mark::x:
EdDSA using ED448:white_check_mark::white_check_mark::x:
PS256 PS384 PS512:white_check_mark::white_check_mark::white_check_mark:
ES256K:white_check_mark::x::white_check_mark:
ML-DSA-44/65/87 [^mldsa]:white_check_mark::white_check_mark::x:

[^mldsa]: ML-DSA (FIPS 204, registered for JOSE by RFC 9964) is experimental and off by default. Build with -DWITH_ML_DSA=ON; it is only enabled when a backend with ML-DSA support is present: OpenSSL >= 3.5, or GnuTLS >= 3.8.10 built against a PQC provider (e.g. --with-leancrypto). When built in, the public header defines LIBJWT_HAVE_ML_DSA. ML-DSA keys use the "AKP" key type with a "pub" member and a "priv" member holding the 32-byte FIPS-204 seed. MbedTLS has no ML-DSA and rejects AKP keys.

JWE

LibJWT supports JWE (RFC 7516) in both the Compact Serialization and the JSON Serialization (the Flattened form and the General form with one or more recipients). A JWE uses two algorithms: a key management algorithm (alg) and a content encryption algorithm (enc).

JWE serializationRecipientsSupported
Compact (RFC 7516 §7.1)one:white_check_mark:
JSON Flattened (RFC 7516 §7.2.2)one:white_check_mark:
JSON General (RFC 7516 §7.2.1)one or more:white_check_mark:

With the JSON serializations the plaintext is encrypted once with a single CEK; each recipient wraps that CEK independently, so any recipient's key can decrypt the token. They also carry an optional shared unprotected header, per-recipient headers, and an application AAD member.

Legend: :white_check_mark: native implementation  ·  :x: not supported

JWE key management algOpenSSLGnuTLSMbedTLS
dir (Direct Encryption):white_check_mark::white_check_mark::white_check_mark:
A128KW A192KW A256KW:white_check_mark::white_check_mark::white_check_mark:
RSA-OAEP (SHA-1):white_check_mark::x::white_check_mark:
RSA-OAEP-256:white_check_mark::white_check_mark::white_check_mark:
ECDH-ES (+ +A128KW/+A192KW/+A256KW):white_check_mark::white_check_mark::white_check_mark:
JWE content encryption encOpenSSLGnuTLSMbedTLS
A128GCM A192GCM A256GCM:white_check_mark::white_check_mark::white_check_mark:
A128CBC-HS256 A192CBC-HS384 A256CBC-HS512:white_check_mark::white_check_mark::white_check_mark:

[!NOTE] ECDH-ES supports both Direct Key Agreement and the +A*KW key wrapping modes, on the EC curves P-256/384/521 and the OKP curves X25519/X448, with optional apu/apv PartyInfo. RSA1_5 and zip (compression) are intentionally not supported. Each backend implements JWE natively. GnuTLS/Nettle cannot perform RSA-OAEP with SHA-1, so the GnuTLS backend does not support plain RSA-OAEP (RSA-OAEP-256 is native).

Optional

:books: Docs and Source

:link: Current Docs

:link: Legacy Docs v2.1.1

:link: GitHub Repo

:package: Pre-built Packages

LibJWT is available in most Linux distributions as well as through Homebrew for Linux, macOS, and Windows.

:hammer: Build Instructions

With CMake:

$ mkdir build
$ cd build
$ cmake ..
$ make