Skip to content

feat(security): ES256 and Ed25519 JWT signing/verification (closes #401) - #414

Merged
gg582 merged 2 commits into
devfrom
feat/jwt-asymmetric-algs
Oct 18, 2026
Merged

gg582 merged 2 commits into
devfrom
feat/jwt-asymmetric-algs

Conversation

@gg582

@gg582 gg582 commented Oct 18, 2026

Copy link
Copy Markdown
Member

Summary

Closes #401. JWT gains asymmetric algorithms alongside the unchanged HS256 entry points:

  • cwist_jwt_sign_es256 / cwist_jwt_verify_es256 — ECDSA over P-256 with SHA-256; JOSE raw R||S signatures (RFC 7518 §3.4); optional kid header claim
  • cwist_jwt_sign_ed25519 / cwist_jwt_verify_ed25519 — EdDSA (RFC 8037)

Keys are PEM (SEC 1 or PKCS#8). Verifiers accept a public key or the public half of a private key.

Security properties

  • Alg confusion closed: each verifier requires the header to declare exactly its own alg, so HS256/ES256/EdDSA tokens never cross-verify (RFC 8725 §3.1).
  • Key type pinned: ES256 rejects any non-P-256 key (wrong curve or non-EC), Ed25519 rejects any non-Ed25519 key — signing and verification both fail cleanly on garbage PEM.
  • HS256 keeps its constant-time compare; existing cwist_jwt_sign/cwist_jwt_verify signatures and behavior are unchanged (the bodies are folded into shared helpers).

Implementation notes

  • ES256 converts between ASN.1 DER (BoringSSL EVP) and JOSE raw R||S with ECDSA_SIG_from_bytes/ECDSA_SIG_to_bytes + BN_bn2bin_padded.
  • Public-key loading falls back from PEM_read_bio_PUBKEY to PEM_read_bio_PrivateKey with a BIO_reset (a failed first read consumes the buffer).
  • exp/nbf validation with the existing 300 s leeway applies to all algorithms.

Tests

  • tests/test_jwt_asym.c (new, in TEST_TARGETS): ES256/Ed25519 round trips, kid header, wrong-key, wrong-curve, wrong-type, garbage-PEM, expired-token, and full cross-algorithm confusion matrix.
  • test_jwt and test_middleware_jwt pass unmodified (HS256 regression check).
  • Rust bindings: sign_es256/verify_es256/sign_ed25519/verify_ed25519 in cwist::jwt with two new tests in tests/advanced.rs (14/14 pass).
  • docs/api/v4.0-api-baseline.txt regenerated (make api-baseline; make api-check green).

New API next to the unchanged HS256 entry points:

- cwist_jwt_sign_es256 / cwist_jwt_verify_es256 (ECDSA P-256, JOSE raw
  R||S signatures, optional kid header)
- cwist_jwt_sign_ed25519 / cwist_jwt_verify_ed25519 (EdDSA, RFC 8037)

Keys are PEM; verifiers accept a public key or the public half of a
private key. Every verifier requires the header to declare exactly its
own alg, so HS256/ES256/EdDSA tokens never cross-verify (RFC 8725).
Key type is checked too: an off-curve or wrong-type key fails signing
and verification outright.

The HS256 sign/verify bodies are folded into shared helpers with the new
algorithms; behaviour of the existing API is unchanged (test_jwt and
test_middleware_jwt pass unmodified).

Rust bindings expose sign_es256/verify_es256/sign_ed25519/verify_ed25519
with tests covering round trips, kid, wrong-key and alg confusion.
@gg582

gg582 commented Oct 18, 2026

Copy link
Copy Markdown
Member Author
make[1]: Entering directory '/home/runner/work/CWIST/CWIST/example/jwt/step-1-sign-verify'
cc -fsanitize=address,undefined -Wall -Wextra -g -I../../../include -I../../../lib -I../../../lib/libttak/include main.c -L../../../ -L../../../lib/cjson -L../../../lib/libttak/lib -lcwist -lcjson -lssl -lcrypto -lttak -lpthread -ldl -lstdc++ -lm -lz -o step-1-sign-verify
/usr/bin/ld: ../../..//libcwist.a(jwt.o): in function `jwt_sign_bytes':
/home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:231:(.text+0x4701): undefined reference to `EVP_PKEY_id'
/usr/bin/ld: ../../..//libcwist.a(jwt.o): in function `jwt_key_is_p256':
/home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:200:(.text+0x5002): undefined reference to `EVP_PKEY_id'
/usr/bin/ld: ../../..//libcwist.a(jwt.o): in function `jwt_sign_bytes':
/home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:264:(.text+0x50c4): undefined reference to `ECDSA_SIG_from_bytes'
/usr/bin/ld: /home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:273:(.text+0x5138): undefined reference to `BN_bn2bin_padded'
/usr/bin/ld: /home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:274:(.text+0x518a): undefined reference to `BN_bn2bin_padded'
/usr/bin/ld: ../../..//libcwist.a(jwt.o): in function `jwt_verify_bytes':
/home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:320:(.text+0x5ddc): undefined reference to `EVP_PKEY_id'
/usr/bin/ld: ../../..//libcwist.a(jwt.o): in function `jwt_key_is_p256':
/home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:200:(.text+0x5e29): undefined reference to `EVP_PKEY_id'
/usr/bin/ld: ../../..//libcwist.a(jwt.o): in function `jwt_load_public_key':
/home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:191:(.text+0x640c): undefined reference to `BIO_reset'
/usr/bin/ld: ../../..//libcwist.a(jwt.o): in function `jwt_verify_bytes':
/home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:343:(.text+0x681a): undefined reference to `ECDSA_SIG_to_bytes'
/usr/bin/ld: /home/runner/work/CWIST/CWIST/src/security/jwt/jwt.c:345:(.text+0x6b9e): undefined reference to `OPENSSL_free'
collect2: error: ld returned 1 exit status

Build fails

jwt.o now references BoringSSL-only symbols (ECDSA_SIG_from_bytes,
ECDSA_SIG_to_bytes, BIO_reset, EVP_PKEY_id) after the ES256/Ed25519
work, so the example must resolve -lssl/-lcrypto from
lib/boringssl/build like the tutorials do.
@gg582
gg582 merged commit afa881c into dev Oct 18, 2026
26 checks passed
gg582 added a commit that referenced this pull request Oct 19, 2026
…) (#414)

* feat(security): ES256 and Ed25519 JWT signing/verification (closes #401)

New API next to the unchanged HS256 entry points:

- cwist_jwt_sign_es256 / cwist_jwt_verify_es256 (ECDSA P-256, JOSE raw
  R||S signatures, optional kid header)
- cwist_jwt_sign_ed25519 / cwist_jwt_verify_ed25519 (EdDSA, RFC 8037)

Keys are PEM; verifiers accept a public key or the public half of a
private key. Every verifier requires the header to declare exactly its
own alg, so HS256/ES256/EdDSA tokens never cross-verify (RFC 8725).
Key type is checked too: an off-curve or wrong-type key fails signing
and verification outright.

The HS256 sign/verify bodies are folded into shared helpers with the new
algorithms; behaviour of the existing API is unchanged (test_jwt and
test_middleware_jwt pass unmodified).

Rust bindings expose sign_es256/verify_es256/sign_ed25519/verify_ed25519
with tests covering round trips, kid, wrong-key and alg confusion.

* fix(example/jwt): link vendored BoringSSL instead of system OpenSSL

jwt.o now references BoringSSL-only symbols (ECDSA_SIG_from_bytes,
ECDSA_SIG_to_bytes, BIO_reset, EVP_PKEY_id) after the ES256/Ed25519
work, so the example must resolve -lssl/-lcrypto from
lib/boringssl/build like the tutorials do.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant