Skip to content

Add BouncyCastle JSSE ML-KEM integration test; make ACCP ML-KEM usable by JSSE - #561

Draft
WillChilds-Klein wants to merge 1 commit into
corretto:mainfrom
WillChilds-Klein:bcjsse-mlkem-integration-test
Draft

Add BouncyCastle JSSE ML-KEM integration test; make ACCP ML-KEM usable by JSSE#561
WillChilds-Klein wants to merge 1 commit into
corretto:mainfrom
WillChilds-Klein:bcjsse-mlkem-integration-test

Conversation

@WillChilds-Klein

Copy link
Copy Markdown
Contributor

What

Adds an end-to-end integration test proving that BouncyCastle's JSSE provider (BCJSSE) can source ML-KEM from ACCP over the standard JCA during a real TLS 1.3 handshake, when ACCP is installed as the top provider. Wiring up that test surfaced two spots where ACCP's ML-KEM SPIs were stricter than the standard JCA contract that JSSE callers rely on, which this PR also fixes.

Why

BCJSSE, left in its default (unpinned) configuration, resolves ML-KEM via JEP-452's javax.crypto.KEM API with no explicit provider, so normal JCA precedence applies and a foreign top provider can serve the request. This test is a positive-signal, cross-JDK probe that ACCP actually gets used on that path -- a RecordingProvider wrapping ACCP observes the KEM / KeyPairGenerator / KeyFactory lookups a genuine handshake drives.

Changes

ACCP interop fixes (src/)

  • MlKemGen.initialize(AlgorithmParameterSpec, SecureRandom) previously threw UnsupportedOperationException unconditionally. JSSE stacks (and ordinary callers) invoke KeyPairGenerator.initialize(new NamedParameterSpec("ML-KEM-768"), random) before generateKeyPair(). It now accepts a NamedParameterSpec matching the SPI's bound parameter set -- mirroring the existing reflective, JDK8-target-safe pattern in XDHGen -- and rejects a mismatch with InvalidAlgorithmParameterException so the JCA can fail over.
  • MlKemSpi.engineNewEncapsulator/Decapsulator rejected a null AlgorithmParameterSpec and a non-null SecureRandom. The javax.crypto.KEM convenience overloads (KEM.newEncapsulator(pk, random), KEM.newDecapsulator(pk)) always pass exactly those. A null spec now defaults to the key's parameter set (each ACCP ML-KEM key already carries it); a supplied SecureRandom is accepted and ignored (AWS-LC draws from its own DRBG). A non-null spec is still validated against the key's parameter set.

Test (tst/.../jdk17plus/BouncyCastleJsseMlKemIntegrationTest.java)

  • Drives BCJSSE-to-BCJSSE TLS 1.3 handshakes over the pure MLKEM512/768/1024 groups and the hybrid X25519MLKEM768 group, asserting ACCP served the ML-KEM lookups. Includes a negative control (ACCP absent -> no lookups observed).
  • Uses an RSA leaf certificate purely to authenticate the handshake so the ML-KEM key exchange runs (an EC leaf trips BCJSSE server-side credential selection in this setup; cert type is incidental to what's under test).
  • Takes the standard provider-mutation test locks (SAME_THREAD + RESOURCE_GLOBAL / RESOURCE_PROVIDER) so it does not race other provider-touching tests.
  • Skips (does not fail) unless the delegation path is actually reachable: javax.crypto.KEMSpi present (JDK 21+ or KEM-backported JDK 17), bctls carries KEMSpiUtil (BC >= 1.84), and ACCP was built with ML-KEM enabled (-DTARGET_JDK_VERSION=17+).

Build (build.gradle)

  • Bumps the test-scoped BouncyCastle deps 1.81 -> 1.85 and adds bctls-jdk18on (which carries KEMSpiUtil, the delegation path first shipped in 1.84).

Testing

  • New test: 6/6 pass on JDK 21 with -DTARGET_JDK_VERSION=21 (pure 512/768/1024, hybrid, parametric set, negative control).
  • Regression: MlKemTest (29), MlKemUtilsTest (4), EvpKeyFactoryTest (143) all pass -- the SPI relaxations preserve existing behavior (mismatched-parameter-set specs are still rejected).
  • Full ./gradlew build (spotless + static analysis) is green.

Notes for reviewers

  • Draft: the ACCP src/ behavior changes are the substantive part and deserve the closest look -- particularly whether tolerating (and ignoring) a caller-supplied SecureRandom is the desired contract, versus documenting ACCP's DRBG-only stance some other way.
  • The test is gated to skip on JDKs / BC versions / ACCP builds where the path isn't reachable, so it is safe across the existing CI matrix (it will skip on the ML-KEM-less shipped artifacts and on non-KEM JDKs).

…e by JSSE

Adds an end-to-end test proving BouncyCastle's JSSE provider (BCJSSE) can
source ML-KEM from ACCP over the standard JCA during a real TLS 1.3
handshake, when ACCP is installed as the top provider. A RecordingProvider
wrapping ACCP observes the KEM / KeyPairGenerator lookups BCJSSE routes
through JEP-452's javax.crypto.KEM API.

Wiring up that test surfaced two interop gaps where ACCP's ML-KEM SPIs were
stricter than the standard JCA contract callers rely on:

- MlKemGen.initialize(AlgorithmParameterSpec, SecureRandom) unconditionally
  threw UnsupportedOperationException. JSSE stacks call
  KeyPairGenerator.initialize(new NamedParameterSpec("ML-KEM-768"), random)
  before generateKeyPair(). Now accepts a NamedParameterSpec matching the
  SPI's bound parameter set (mirroring XDHGen's reflective, JDK8-safe
  pattern) and rejects mismatches so the JCA can fail over.

- MlKemSpi.engineNewEncapsulator/Decapsulator rejected a null
  AlgorithmParameterSpec and a non-null SecureRandom. The javax.crypto.KEM
  convenience overloads (KEM.newEncapsulator(pk, random),
  KEM.newDecapsulator(pk)) always pass exactly those. Null spec now defaults
  to the key's parameter set; a supplied SecureRandom is accepted and
  ignored (AWS-LC uses its own DRBG).

Test uses an RSA leaf certificate (an EC leaf trips BCJSSE server
credential selection here) purely to authenticate the handshake, and takes
the standard provider-mutation test locks (SAME_THREAD + RESOURCE_GLOBAL /
RESOURCE_PROVIDER) so it does not race other provider-touching tests.

Bumps the test-scoped BouncyCastle dependencies to 1.85 and adds bctls
(KEMSpiUtil, the delegation path, first shipped in 1.84). The test skips
unless javax.crypto.KEMSpi is present (JDK 21+ or KEM-backported JDK 17),
bctls carries KEMSpiUtil, and ACCP was built with ML-KEM enabled.
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 40.90909% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.46%. Comparing base (7813506) to head (0d4eb5e).

Files with missing lines Patch % Lines
.../com/amazon/corretto/crypto/provider/MlKemGen.java 40.90% 9 Missing and 4 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #561      +/-   ##
============================================
- Coverage     78.55%   78.46%   -0.10%     
- Complexity     1217     1220       +3     
============================================
  Files           127      127              
  Lines          7759     7781      +22     
  Branches        982      985       +3     
============================================
+ Hits           6095     6105      +10     
- Misses         1133     1140       +7     
- Partials        531      536       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

try {
clazz = Class.forName("java.security.spec.NamedParameterSpec");
} catch (final ClassNotFoundException e) {
// JDK 10 or older; getNamedParameter will reject all specs, which is correct there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do a check on JDK version and rethrow if runtime version is >10

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