mbedtls: migrate the native backend to the PSA Crypto API (closes #267)
MbedTLS 4.x (TF-PSA-Crypto) split the crypto core out, made the classic
low-level API (mbedtls_rsa_*, mbedtls_ecp_*, mbedtls_gcm_*, ...) private
under mbedtls/private/, removed the low-level mbedtls_ecdh_* primitives
entirely, and reworked even some public APIs (mbedtls_md HMAC, nist_kw)
toward PSA. Reaching into the private headers would just re-break on the
next release, so move the whole native backend onto the public PSA Crypto
API (psa/crypto.h). PSA is also stable in MbedTLS 3.6 LTS, so this is a
single code path for both lines with no version #ifdefs; the CMake floor
stays mbedcrypto >= 3.6.0.
Key model: a parsed JWK now holds PSA-importable material (RSA PKCS#1 DER
built via the public asn1write.h; EC/OKP-X curve family + raw point/scalar)
rather than a native key object. Each operation imports a short-lived
volatile PSA key with exactly the policy it needs, then destroys it. This
sidesteps PSA's single-policy-per-key model and keeps RSA parse-time
leniency.
- sign/verify: psa_sign_message/psa_verify_message (RSA PSS/PKCS1v15,
ECDSA); HMAC via PSA MAC. Strict signature-size checks preserved.
- JWE: GCM via PSA AEAD, CBC via PSA cipher + truncated-MAC HMAC,
RSA-OAEP via PSA, RNG via psa_generate_random, ECDH via
psa_raw_key_agreement, and RFC 3394 AES-KW hand-rolled on PSA AES-ECB
(nist_kw changed shape in 4.x). EdDSA stays unsupported.
- EC keys are validated at parse via a throwaway PSA import; item->bits
is computed at parse.
Two PSA constraints handled: psa_generate_key cannot produce Montgomery
pairs (X25519/X448 ephemerals generate a random scalar and import it), and
secp256k1 is rejected for ECDH-ES (PSA would otherwise allow it).
PSA caps RSA at PSA_VENDOR_RSA_MAX_KEY_BITS (4096 by default), which the
classic API did not. Oversize RSA keys are now rejected at parse with a
clear message; the keyring and flip-flop tests skip the 8192-bit key on
the MbedTLS backend, mirroring the existing EdDSA skip.
Verified against MbedTLS 4.1: full suite passes (mbedtls-only and the
OpenSSL+GnuTLS+MbedTLS+libcurl combo) with no new uncovered lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ben Collins <bcollins@libjwt.io>
| Standard | RFC | Description |
|---|---|---|
JWS | :page_facing_up: RFC-7515 | JSON Web Signature |
JWE | :page_facing_up: RFC-7516 | JSON Web Encryption |
JWK | :page_facing_up: RFC-7517 | JSON Web Keys and Sets |
JWA | :page_facing_up: RFC-7518 | JSON Web Algorithms |
JWT | :page_facing_up: RFC-7519 | JSON 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.
[!NOTE] OpenSSL is required and used for JWK(S) operations.
JWS Algorithm alg | OpenSSL | GnuTLS | MbedTLS |
|---|---|---|---|
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: >= 3.8.8 | :x: |
PS256 PS384 PS512 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
ES256K | :white_check_mark: | :x: | :white_check_mark: |
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 serialization | Recipients | Supported |
|---|---|---|
| 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 · :large_blue_circle: supported, using OpenSSL as a fallback · :x: not supported
JWE key management alg | OpenSSL | GnuTLS | MbedTLS |
|---|---|---|---|
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: | :large_blue_circle: | :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 enc | OpenSSL | GnuTLS | MbedTLS |
|---|---|---|---|
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-ESsupports both Direct Key Agreement and the+A*KWkey wrapping modes, on the EC curves P-256/384/521 and the OKP curves X25519/X448, with optionalapu/apvPartyInfo.RSA1_5andzip(compression) are intentionally not supported. Each backend implements JWE natively, with one exception: GnuTLS/Nettle cannot perform RSA-OAEP with SHA-1, so plainRSA-OAEPfalls back to OpenSSL under the GnuTLS backend (RSA-OAEP-256is native).
:link: Current Docs
:link: Legacy Docs v2.1.1
:link: GitHub Repo
LibJWT is available in most Linux distributions as well as through Homebrew for Linux, macOS, and Windows.
$ mkdir build $ cd build $ cmake .. $ make