A Java JSSE TLS provider backed by s2n-tls over JNI, enabling ML-KEM (post-quantum) key exchange in TLSv1.3.
Melitta uses s2n-tls with the "default_pq" security policy and aws-lc as the cryptographic backend, negotiating hybrid post-quantum key exchange (X25519MLKEM768) for TLSv1.3 connections.
Status: Proof of concept. See Current Limitations.
- JDK 8+
- CMake 3.16+
- C compiler (clang or gcc)
- Network access (tests connect to AWS KMS)
# Clone with submodules
git clone --recurse-submodules https://github.com/WillChilds-Klein/melitta.git
cd melitta
# Build and test
./gradlew buildimport com.melitta.MelittaProvider;
import java.security.Security;
import javax.net.ssl.SSLContext;
import javax.net.ssl.HttpsURLConnection;
// Register the provider
Security.insertProviderAt(new MelittaProvider(), 1);
// Create an SSLContext
SSLContext ctx = SSLContext.getInstance("TLSv1.3", "Melitta");
ctx.init(null, null, null);
// Use with HttpsURLConnection
URL url = new URL("https://example.com");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ctx.getSocketFactory());
int status = conn.getResponseCode();Required JVM flags (JDK 9+ only, for socket file descriptor extraction):
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
Gradle orchestrates three staged CMake builds:
- aws-lc (static library) — cryptographic backend with ML-KEM support
- s2n-tls (shared library) — TLS implementation using
"default_pq"security policy - libmelitta_native (shared JNI library) — bridges s2n-tls to Java
./gradlew build # full build + tests (~1 min clean, ~20s incremental)
./gradlew test # run tests only
./gradlew clean # remove all build artifacts| Test | Description |
|---|---|
KmsPingTest |
Raw s2n-tls handshake + HTTPS GET to KMS /ping via HttpsURLConnection |
KmsListKeysV2Test |
AWS SDK for Java v2 KMS ListKeys via default HTTP client |
KmsListKeysV1Test |
AWS SDK for Java v1 KMS ListKeys via Apache HttpClient |
All tests connect to kms.us-east-1.amazonaws.com:443 and assert ML-KEM key exchange. The SDK tests skip gracefully when AWS credentials are unavailable.
./gradlew test --tests "com.melitta.KmsPingTest.httpsGetKmsPing" # single testMelittaProvider (java.security.Provider)
└── MelittaSSLContextSpi (SSLContextSpi)
└── MelittaSSLSocketFactory (SSLSocketFactory)
└── MelittaSSLSocket (SSLSocket)
└── S2nBinding (JNI) ↔ s2n_binding.c ↔ s2n-tls ↔ aws-lc
With the "default_pq" s2n-tls security policy, Melitta negotiates hybrid post-quantum key exchange. A successful handshake with a compatible server (e.g., AWS KMS) prefers X25519MLKEM768 — combining classical X25519 ECDH with ML-KEM-768 (FIPS 203).
A JMH benchmark suite compares Melitta against three other client-side JSSE providers on three scenarios: handshake-only, 1 KB transfer, and 1 MB transfer. Benchmarks run against an in-process TLS 1.3 loopback server with a self-signed CA, so results reflect provider overhead rather than network variance.
Providers under test:
| Provider | Backend |
|---|---|
melitta |
s2n-tls + aws-lc (this project) |
jsse |
Platform default (SunJSSE) |
conscrypt |
Conscrypt (BoringSSL) |
bouncycastle |
BouncyCastle BCJSSE |
Latest report: http://will.childs-kle.in/melitta/ (published from the gh-pages branch on every push to main, running on a shared ubuntu-latest GitHub runner).
The published numbers are meaningful for relative comparisons between providers in a single report. Absolute ms/op values depend on the runner's hardware and noisy-neighbor CPU contention and can shift by 5-10× between a laptop and a shared CI VM — do not compare across environments.
Run locally:
./gradlew :benchmarks:jmh # full run with default iteration counts
./gradlew :benchmarks:jmhReport # render HTML from build/results/jmh/results.jsonOverride iteration counts via -Pjmh.iterations=N, -Pjmh.fork=N, -Pjmh.include=<regex>, etc.
Apple Silicon note: Conscrypt does not publish a osx-aarch_64 native build. On Apple Silicon, use -Pjmh.providers=melitta,jsse,bouncycastle to skip it.
The JAR bundles the JNI shared library as a resource under native/<os>-<arch>/. libs2n and libcrypto (from aws-lc) are statically linked into libmelitta_native, so the shared library has no runtime dependency on either — a single file is loaded at startup.
At init, NativeLoader extracts the platform-appropriate shared library from the JAR into a temp directory and calls System.load. The extracted file is the same size as the copy inside the JAR (shown uncompressed below).
JARs are built per-platform. Sizes as of commit 48520e0:
| OS | Arch | JAR (packed) | Native lib (unpacked) |
|---|---|---|---|
| macOS | aarch64 | 1.5 MB | 3.6 MB |
| macOS | x86_64 | ?? | ?? |
| Linux | aarch64 | ?? | ?? |
| Linux | x86_64 | 1.8 MB | 5.0 MB |
Sizes are approximate and depend on compiler, toolchain, and aws-lc/s2n-tls commit. Only symbols actually referenced by libmelitta_native are pulled in from the static archives; the figures above reflect that dead-stripped footprint, not the full aws-lc + s2n-tls libraries.
This is a proof of concept with the following constraints:
- Client-only — no server sockets or
SSLEnginesupport - No client certificate authentication
- No session resumption or 0-RTT
- No Java TrustManager/KeyManager integration — uses system trust store via s2n-tls
- Requires
--add-opensJVM flags for socket fd extraction via reflection