Skip to content

Commit 068caa7

Browse files
sujankotaclaudegemini-code-assist[bot]
authored
feat(sdk): DSPX-3383 add pure ML-KEM-768 and ML-KEM-1024 key wrapping (#373)
## Overview Adds pure **ML-KEM-768** (`mlkem:768`) and **ML-KEM-1024** (`mlkem:1024`) post-quantum key wrapping, on top of the `KemProvider` SPI introduced by PR #368. The BC-backed implementation lives in `sdk-pqc-bc` (already on the classpath for hybrid PQC); the core `sdk` jar only gets two `KeyType` constants and an `isMLKEM()` predicate. **No new BC compile-time references** — the FIPS Maven profile still builds cleanly without `sdk-pqc-bc`. Conforms to **open** platform PR [opentdf/platform#3562](opentdf/platform#3562) (replacement for closed #3491). ## Wire format wrappedKey = base64( ASN.1 SEQUENCE { [0] mlkem_ct, [1] AES-GCM(IV(12)||DEK||tag(16)) } ) AES wrap key = mlkem_decaps_shared_secret ← used DIRECTLY, no HKDF keyAccess.type = "mlkem-wrapped" ← its own KAO scheme ephemeralPublicKey = absent Envelope is byte-identical to the hybrid path. Only the **wrap-key derivation** differs: hybrid still uses HKDF (it's the combiner over two shared-secret halves); pure ML-KEM uses the 32-byte FIPS 203 Decaps output directly. Rationale: HSM-backed KAS (Thales Luna 7.15.1, strict-FIPS) emits Decaps output only as a non-extractable `CKK_AES` object — any KDF in the unwrap chain blocks HSM rewrap. FIPS 203's K is already uniformly random; the `"mlkem-wrapped"` wire type itself is the domain-separation tag. See [platform ADR `2026-06-16-mlkem-direct-key-wrap.md`](https://github.com/opentdf/platform/blob/main/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md). PEM: standard SPKI / PKCS#8 with NIST FIPS 203 OIDs `2.16.840.1.101.3.4.4.{2,3}`. ## Changes | File | Change | |---|---| | `sdk/.../KeyType.java` | + `MLKEM768Key`, `MLKEM1024Key`, `isMLKEM()` | | `sdk/.../TDF.java` | + `isMLKEM()` dispatch branch → `KemProviders.get(kt).wrapDEK(...)`, `kMlkemWrapped` constant | | `sdk-pqc-bc/.../MLKEMAlgorithm.java` *(new)* | stateless per-variant params + wrap/unwrap | | `sdk-pqc-bc/.../MLKEMKeyPair.java` *(new)* | keypair holder + PEM I/O | | `sdk-pqc-bc/.../HybridSpki.java` | + ML-KEM OID constants | | `sdk-pqc-bc/.../HybridCrypto.java` | + 2 cases each in wrap/unwrap dispatch | | `sdk-pqc-bc/.../BouncyCastleKemProvider.java` | + 2 entries in `SUPPORTED` | | `scripts/test-mlkem.sh` *(new)*, `scripts/README.md` | local-KAS round-trip + docs | Plus `MLKEMKeyPairTest` (10 tests) and `TDFMLKEMTest` (2 tests). ## Verification | Check | Result | |---|---| | `mvn clean install -DskipTests` (default) | All 6 modules build | | `mvn -P fips,!non-fips clean install -DskipTests` | sdk compiles without sdk-pqc-bc | | `mvn -pl sdk,sdk-pqc-bc -am test` | **201 tests, 0 failures** | ## Known gap Platform proto v0.34.0 doesn't yet define `ALGORITHM_MLKEM_768/_1024`, so `KeyType.fromAlgorithm` can't auto-map them. Workaround: `--encap-key-type=MLKEM*Key` explicit. Two switch cases when the proto bump lands. ## Dependency Platform PR #3562 is **open, not merged** — KAS rewrap requires its server-side changes. If reviewers change #3562 before merge, this PR may need a small follow-up. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for pure ML-KEM (FIPS 203) key wrapping to TDF encryption, supporting mlkem:768 and mlkem:1024 variants. * **Documentation** * Updated testing documentation with ML-KEM-specific procedures, manifest expectations, and known SDK mapping limitations with workarounds. * **Tests** * Added comprehensive test script and unit tests validating ML-KEM key wrapping and round-trip encryption/decryption functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 787039c commit 068caa7

11 files changed

Lines changed: 927 additions & 36 deletions

File tree

scripts/README.md

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,59 @@ attempted), 2 on misuse.
102102
| `keyType='null'` (manifest assertion) | You're on an old branch where `TDF.java` doesn't yet route hybrid algorithms. Pull the latest branch HEAD. |
103103
| `decrypt failed` after manifest passes | KAS-side rewrap doesn't yet support the `hybrid-wrapped` keyType. Check the platform branch has the matching server change. |
104104

105-
### Known SDK gap
106-
107-
`KeyType.fromAlgorithm` and `KeyType.fromPublicKeyAlgorithm`
108-
(`sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java`) don't yet map the
109-
hybrid algorithm protobuf enums. Auto-discovery via the KAS registry
110-
(`Config.KASInfo.fromKeyAccessServer`) will throw `IllegalArgumentException`
111-
once the platform's proto definitions include `KAS_PUBLIC_KEY_ALG_ENUM_HPQT_*`
112-
values. This script bypasses that path by using `--encap-key-type` explicitly;
113-
extending the script to also exercise registry-discovery should wait until the
114-
mapping is added.
105+
## `test-mlkem.sh`
106+
107+
End-to-end test of the Java SDK's pure ML-KEM (FIPS 203) key wrapping
108+
(`mlkem:768`, `mlkem:1024`) against a locally running OpenTDF platform.
109+
Same shape as `test-hybrid-pqc.sh` (encrypt → assert manifest → KAS rewrap
110+
→ decrypt → diff) with two pure-ML-KEM specifics:
111+
112+
- `keyAccess[0].type` is `"mlkem-wrapped"` — its own KAO scheme, distinct
113+
from `"hybrid-wrapped"` and from RSA's `"wrapped"`. The KAS uses this
114+
to skip HKDF on the wrap key (pure ML-KEM uses the 32-byte FIPS 203
115+
Decaps shared secret directly as the AES-256 key; HKDF is dropped per
116+
the platform ADR
117+
[`2026-06-16-mlkem-direct-key-wrap.md`](https://github.com/opentdf/platform/blob/main/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md)).
118+
- Pre-flight OIDs are the NIST FIPS 203 OIDs:
119+
`2.16.840.1.101.3.4.4.2` for ML-KEM-768 and
120+
`2.16.840.1.101.3.4.4.3` for ML-KEM-1024.
121+
122+
The wire envelope (ASN.1 SEQUENCE of two implicit OCTET STRINGs —
123+
`{ [0] kemCiphertext, [1] AES-GCM(IV ‖ DEK ‖ tag) }`) is byte-identical
124+
to the hybrid path; the script's 0x30-first-byte check is the same
125+
invariant `test-hybrid-pqc.sh` uses.
126+
127+
### Run it
128+
129+
```bash
130+
# Full run — builds cmdline, pre-flight check, both variants
131+
PLATFORM_ENDPOINT=http://localhost:8080 scripts/test-mlkem.sh
132+
133+
# One variant only
134+
scripts/test-mlkem.sh --algorithms MLKEM768Key
135+
136+
# Reuse an already-built cmdline jar (much faster on iterative runs)
137+
scripts/test-mlkem.sh --skip-build
138+
```
139+
140+
All other flags (`--platform-endpoint`, `--kas-url`, `--client-id`,
141+
`--client-secret`, `--attr`, `--skip-kas-check`) match
142+
`test-hybrid-pqc.sh` — see the configuration table above.
143+
144+
### Prerequisites
145+
146+
Same as `test-hybrid-pqc.sh`. The KAS-side requirement is that
147+
`mlkem:768` (and optionally `mlkem:1024`) public keys are registered.
148+
149+
### Known SDK gap (pure ML-KEM)
150+
151+
`KeyType.fromAlgorithm` / `fromPublicKeyAlgorithm` don't yet map the pure
152+
ML-KEM protobuf enums. The platform proto stubs we currently build against
153+
(`protocol/go/v0.34.0`) only have the hybrid `ALGORITHM_HPQT_*` set — no
154+
`ALGORITHM_MLKEM_768` / `_1024`. Until the platform release we depend on
155+
adds those values, registry-discovery via
156+
`Config.KASInfo.fromKeyAccessServer` will throw `IllegalArgumentException`
157+
for ML-KEM. This script sidesteps it by passing `--encap-key-type=MLKEM*Key`
158+
explicitly. When the proto bump lands, add two cases to each switch in
159+
`KeyType.java` and the script can also be extended to exercise the
160+
registry-discovery path.

scripts/test-mlkem.sh

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
#!/usr/bin/env bash
2+
#
3+
# test-mlkem.sh — round-trip the Java SDK's pure ML-KEM (FIPS 203) key
4+
# wrapping against a locally running OpenTDF platform.
5+
#
6+
# Per algorithm: encrypt → assert manifest → KAS rewrap → decrypt → diff.
7+
#
8+
# Differs from test-hybrid-pqc.sh in two places:
9+
# * keyAccess[0].type == "mlkem-wrapped" (its own KAO scheme — distinct
10+
# from "hybrid-wrapped" and from RSA's "wrapped"). The KAS uses this
11+
# to skip HKDF on the wrap key. See platform PR #3562 and the ADR at
12+
# adr/decisions/2026-06-16-mlkem-direct-key-wrap.md.
13+
# * SPKI OIDs are the NIST FIPS 203 ones (2.16.840.1.101.3.4.4.{2,3}).
14+
# Wire envelope (ASN.1 SEQUENCE of two implicit OCTET STRINGs) and the
15+
# 0x30-prefix invariant match the hybrid path byte-for-byte.
16+
#
17+
# Prereqs:
18+
# * Local platform up at $PLATFORM_ENDPOINT with ML-KEM KAS keys registered
19+
# for mlkem:768 and (optionally) mlkem:1024
20+
# * java, mvn (JDK 17), unzip, jq on PATH
21+
# * grpcurl optional (used only for the pre-flight key-publication check)
22+
#
23+
# Usage:
24+
# scripts/test-mlkem.sh # full run, both variants
25+
# scripts/test-mlkem.sh --skip-build # reuse existing jar
26+
# scripts/test-mlkem.sh --skip-kas-check # skip grpcurl pre-flight
27+
# scripts/test-mlkem.sh --algorithms MLKEM768Key # subset
28+
# PLATFORM_ENDPOINT=http://localhost:8080 scripts/test-mlkem.sh
29+
#
30+
# See scripts/README.md for a full prereq + troubleshooting guide.
31+
32+
set -euo pipefail
33+
34+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
36+
JAR="$REPO_ROOT/cmdline/target/cmdline.jar"
37+
38+
PLATFORM_ENDPOINT="${PLATFORM_ENDPOINT:-http://localhost:8080}"
39+
KAS_URL="${KAS_URL:-$PLATFORM_ENDPOINT}"
40+
CLIENT_ID="${CLIENT_ID:-opentdf-sdk}"
41+
CLIENT_SECRET="${CLIENT_SECRET:-secret}"
42+
DATA_ATTR="${DATA_ATTR:-https://example.com/attr/attr1/value/value1}"
43+
ALGORITHMS=(MLKEM768Key MLKEM1024Key)
44+
SKIP_BUILD=0
45+
SKIP_KAS_CHECK=0
46+
47+
# With `set -u`, a bare `$2` for a value-taking flag with no argument would
48+
# crash with "unbound variable" instead of the documented exit 2 misuse path.
49+
require_opt_value() {
50+
local opt="$1"
51+
local val="${2-}"
52+
if [[ -z "$val" || "$val" == --* ]]; then
53+
echo "missing value for $opt" >&2
54+
exit 2
55+
fi
56+
}
57+
58+
while [[ $# -gt 0 ]]; do
59+
case "$1" in
60+
--skip-build) SKIP_BUILD=1; shift ;;
61+
--skip-kas-check) SKIP_KAS_CHECK=1; shift ;;
62+
--algorithms) require_opt_value "$1" "${2-}"; IFS=, read -r -a ALGORITHMS <<< "$2"; shift 2 ;;
63+
--platform-endpoint) require_opt_value "$1" "${2-}"; PLATFORM_ENDPOINT="$2"; shift 2 ;;
64+
--kas-url) require_opt_value "$1" "${2-}"; KAS_URL="$2"; shift 2 ;;
65+
--attr) require_opt_value "$1" "${2-}"; DATA_ATTR="$2"; shift 2 ;;
66+
--client-id) require_opt_value "$1" "${2-}"; CLIENT_ID="$2"; shift 2 ;;
67+
--client-secret) require_opt_value "$1" "${2-}"; CLIENT_SECRET="$2"; shift 2 ;;
68+
-h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
69+
*) echo "unknown option: $1" >&2; exit 2 ;;
70+
esac
71+
done
72+
73+
# Map KeyType enum name → the mlkem:* algorithm string the KAS expects.
74+
# Function form (instead of `declare -A`) so this works on macOS bash 3.2.
75+
alg_to_string() {
76+
case "$1" in
77+
MLKEM768Key) echo "mlkem:768" ;;
78+
MLKEM1024Key) echo "mlkem:1024" ;;
79+
*) return 1 ;;
80+
esac
81+
}
82+
83+
# Map KeyType enum name → expected SPKI OID inside the standard PUBLIC KEY PEM
84+
# (NIST FIPS 203). The pre-flight extracts via openssl asn1parse and compares.
85+
alg_to_oid() {
86+
case "$1" in
87+
MLKEM768Key) echo "2.16.840.1.101.3.4.4.2" ;;
88+
MLKEM1024Key) echo "2.16.840.1.101.3.4.4.3" ;;
89+
*) return 1 ;;
90+
esac
91+
}
92+
93+
WORK_DIR="$(mktemp -d -t mlkem-XXXXXX)"
94+
trap 'rm -rf "$WORK_DIR"' EXIT
95+
96+
if [[ -t 1 ]]; then
97+
GREEN=$'\033[0;32m'; RED=$'\033[0;31m'; YELLOW=$'\033[0;33m'; RESET=$'\033[0m'
98+
else
99+
GREEN=''; RED=''; YELLOW=''; RESET=''
100+
fi
101+
pass() { echo "${GREEN}[OK]${RESET} $*"; }
102+
fail() { echo "${RED}[FAIL]${RESET} $*"; }
103+
info() { echo "${YELLOW}[..]${RESET} $*"; }
104+
105+
require() { command -v "$1" >/dev/null 2>&1 || { fail "missing required tool: $1"; exit 2; }; }
106+
require java; require unzip; require jq
107+
[[ $SKIP_BUILD -eq 1 ]] || require mvn
108+
109+
# Portable base64 decode: GNU/BusyBox accept `-d`, BSD/macOS prior to 12 use `-D`.
110+
if printf 'MA==\n' | base64 -d >/dev/null 2>&1; then
111+
BASE64_DECODE_FLAG="-d"
112+
elif printf 'MA==\n' | base64 -D >/dev/null 2>&1; then
113+
BASE64_DECODE_FLAG="-D"
114+
else
115+
fail "neither 'base64 -d' nor 'base64 -D' works on this system"; exit 2
116+
fi
117+
b64decode() { base64 "$BASE64_DECODE_FLAG"; }
118+
119+
run_cmdline() {
120+
java -jar "$JAR" \
121+
--client-id="$CLIENT_ID" \
122+
--client-secret="$CLIENT_SECRET" \
123+
--platform-endpoint="$PLATFORM_ENDPOINT" \
124+
-h "$@"
125+
}
126+
127+
##### 1. Build
128+
if [[ $SKIP_BUILD -eq 0 ]]; then
129+
info "Building cmdline (mvn clean install -DskipTests)"
130+
build_log="$WORK_DIR/build.log"
131+
if ! (cd "$REPO_ROOT" && mvn --batch-mode clean install -DskipTests) > "$build_log" 2>&1; then
132+
fail "Maven build failed. Tail of build log:"
133+
tail -40 "$build_log" | sed 's/^/ /'
134+
if grep -q "Buf API token" "$build_log" 2>/dev/null; then
135+
fail "Hint: run 'buf registry login' or export BUF_INPUT_HTTPS_USERNAME / BUF_INPUT_HTTPS_PASSWORD before retrying."
136+
fi
137+
exit 1
138+
fi
139+
pass "Build complete"
140+
else
141+
info "Skipping build (--skip-build)"
142+
fi
143+
[[ -f "$JAR" ]] || { fail "jar not found at $JAR — run without --skip-build"; exit 1; }
144+
145+
##### 2. Pre-flight: confirm KAS publishes ML-KEM keys
146+
if [[ $SKIP_KAS_CHECK -eq 0 ]] && command -v grpcurl >/dev/null 2>&1; then
147+
info "Pre-flight: querying KAS for ML-KEM public keys"
148+
host="${PLATFORM_ENDPOINT#http://}"; host="${host#https://}"; host="${host%/}"
149+
for alg_name in "${ALGORITHMS[@]}"; do
150+
if ! alg=$(alg_to_string "$alg_name"); then
151+
fail "unknown algorithm: $alg_name"; exit 2
152+
fi
153+
resp=$(grpcurl -plaintext -d "{\"algorithm\":\"$alg\"}" \
154+
"$host" kas.AccessService/PublicKey 2>&1 || true)
155+
pem=$(jq -r '.publicKey // empty' <<<"$resp" 2>/dev/null || true)
156+
if [[ -z "$pem" ]]; then
157+
fail "$alg: KAS returned no publicKey. Response was:"
158+
echo "$resp" | head -5 | sed 's/^/ /'
159+
fail "Is the platform running with the ML-KEM-capable KAS branch and the key registered?"
160+
exit 1
161+
fi
162+
first_line=$(echo "$pem" | head -1)
163+
if [[ "$first_line" != *"BEGIN PUBLIC KEY"* ]]; then
164+
fail "$alg: KAS returned a non-SPKI PEM (first line: $first_line)"
165+
exit 1
166+
fi
167+
expected_oid=$(alg_to_oid "$alg_name")
168+
if command -v openssl >/dev/null 2>&1; then
169+
actual_oid=$(printf '%s\n' "$pem" | openssl asn1parse 2>/dev/null \
170+
| awk '/OBJECT/ {sub(/^:/, "", $NF); print $NF; exit}')
171+
if [[ -z "$actual_oid" ]]; then
172+
fail "$alg: could not extract SPKI OID via openssl asn1parse"
173+
exit 1
174+
fi
175+
if [[ "$actual_oid" != "$expected_oid" ]]; then
176+
fail "$alg: SPKI OID mismatch — expected $expected_oid, got $actual_oid"
177+
exit 1
178+
fi
179+
pass "$alg: KAS returns SPKI PEM with OID $actual_oid"
180+
else
181+
pass "$alg: KAS returns SPKI PEM (openssl not available; OID not verified)"
182+
fi
183+
done
184+
else
185+
info "Skipping KAS pre-flight check"
186+
fi
187+
188+
##### 3. Round-trip each algorithm
189+
PAYLOAD="$WORK_DIR/payload"
190+
printf 'pure ml-kem round-trip payload @ %s\n' "$(date)" > "$PAYLOAD"
191+
PAYLOAD_BYTES=$(wc -c < "$PAYLOAD" | tr -d ' ')
192+
info "Test payload: $PAYLOAD_BYTES bytes"
193+
echo " --- plaintext ---"
194+
sed 's/^/ /' < "$PAYLOAD"
195+
echo " --- end plaintext ---"
196+
197+
failures=()
198+
for alg_name in "${ALGORITHMS[@]}"; do
199+
tdf="$WORK_DIR/test-${alg_name}.tdf"
200+
out="$WORK_DIR/out-${alg_name}"
201+
enc_log="$WORK_DIR/encrypt-${alg_name}.log"
202+
dec_log="$WORK_DIR/decrypt-${alg_name}.log"
203+
204+
info "[$alg_name] encrypt"
205+
if ! run_cmdline encrypt \
206+
--kas-url="$KAS_URL" \
207+
--mime-type=text/plain \
208+
--attr="$DATA_ATTR" \
209+
--autoconfigure=false \
210+
--encap-key-type="$alg_name" \
211+
-f "$PAYLOAD" > "$tdf" 2> "$enc_log"; then
212+
fail "$alg_name: encrypt failed"
213+
sed 's/^/ /' < "$enc_log"
214+
failures+=("$alg_name (encrypt)")
215+
continue
216+
fi
217+
218+
info "[$alg_name] verify manifest"
219+
manifest_entry=$(unzip -l "$tdf" 2>/dev/null | awk '/manifest\.json$/ {print $NF; exit}')
220+
if [[ -z "$manifest_entry" ]]; then
221+
fail "$alg_name: no manifest.json entry inside $tdf"
222+
failures+=("$alg_name (manifest entry missing)")
223+
continue
224+
fi
225+
manifest=$(unzip -p "$tdf" "$manifest_entry")
226+
# In Manifest.java, the Java field `keyType` is annotated with
227+
# @SerializedName("type"), so the JSON key is "type" (not "keyType").
228+
keyType=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"$manifest")
229+
ephem=$(jq -r '.encryptionInformation.keyAccess[0].ephemeralPublicKey // ""' <<<"$manifest")
230+
wrapped=$(jq -r '.encryptionInformation.keyAccess[0].wrappedKey // ""' <<<"$manifest")
231+
if [[ "$keyType" != "mlkem-wrapped" ]]; then
232+
fail "$alg_name: type='$keyType' (expected 'mlkem-wrapped')"
233+
echo " keyAccess[0]:"
234+
jq '.encryptionInformation.keyAccess[0]' <<<"$manifest" 2>/dev/null | sed 's/^/ /'
235+
failures+=("$alg_name (bad type: $keyType)")
236+
continue
237+
fi
238+
if [[ -n "$ephem" ]]; then
239+
fail "$alg_name: ephemeralPublicKey unexpectedly set ('$ephem')"
240+
failures+=("$alg_name (stray ephemeralPublicKey)")
241+
continue
242+
fi
243+
if [[ -z "$wrapped" ]]; then
244+
fail "$alg_name: wrappedKey is empty"
245+
failures+=("$alg_name (empty wrappedKey)")
246+
continue
247+
fi
248+
# ASN.1 SEQUENCE always starts with 0x30 — same invariant the hybrid path checks.
249+
first_byte=$(b64decode <<<"$wrapped" 2>/dev/null | od -An -tx1 -N1 | tr -d ' \n' || true)
250+
if [[ "$first_byte" != "30" ]]; then
251+
fail "$alg_name: wrappedKey does not start with ASN.1 SEQUENCE (got 0x$first_byte)"
252+
failures+=("$alg_name (bad envelope)")
253+
continue
254+
fi
255+
pass "$alg_name: manifest OK (mlkem-wrapped, ASN.1 envelope, no ephemeralPublicKey)"
256+
echo " --- keyAccess[0] (KAO) ---"
257+
jq '.encryptionInformation.keyAccess[0]' <<<"$manifest" | sed 's/^/ /'
258+
echo " --- end keyAccess[0] ---"
259+
260+
info "[$alg_name] decrypt (rewrap via KAS)"
261+
if ! run_cmdline decrypt -f "$tdf" > "$out" 2> "$dec_log"; then
262+
fail "$alg_name: decrypt failed"
263+
sed 's/^/ /' < "$dec_log"
264+
failures+=("$alg_name (decrypt)")
265+
continue
266+
fi
267+
if ! diff -q "$PAYLOAD" "$out" >/dev/null; then
268+
fail "$alg_name: decrypted payload differs from original"
269+
echo " --- expected (first 200 bytes) ---"
270+
head -c 200 "$PAYLOAD" | sed 's/^/ /'
271+
echo
272+
echo " --- got (first 200 bytes) ---"
273+
head -c 200 "$out" | sed 's/^/ /'
274+
echo
275+
failures+=("$alg_name (payload mismatch)")
276+
continue
277+
fi
278+
pass "$alg_name: round-trip OK"
279+
out_bytes=$(wc -c < "$out" | tr -d ' ')
280+
echo " --- decrypted ($out_bytes bytes) ---"
281+
sed 's/^/ /' < "$out"
282+
echo " --- end decrypted ---"
283+
done
284+
285+
echo
286+
if [[ ${#failures[@]} -eq 0 ]]; then
287+
echo "${GREEN}All ${#ALGORITHMS[@]} ML-KEM algorithm(s) passed round-trip.${RESET}"
288+
exit 0
289+
else
290+
echo "${RED}FAILURES (${#failures[@]}):${RESET}"
291+
printf ' - %s\n' "${failures[@]}"
292+
exit 1
293+
fi

sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemProvider.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
import java.util.Set;
88

99
/**
10-
* BouncyCastle-backed {@link KemProvider}. Supports the three hybrid PQC
10+
* BouncyCastle-backed {@link KemProvider}. Supports the post-quantum
1111
* {@link KeyType}s currently defined in the SDK:
1212
* <ul>
13-
* <li>{@link KeyType#HybridXWingKey} (X-Wing)</li>
14-
* <li>{@link KeyType#HybridSecp256r1MLKEM768Key}</li>
15-
* <li>{@link KeyType#HybridSecp384r1MLKEM1024Key}</li>
13+
* <li><b>Hybrid:</b> {@link KeyType#HybridXWingKey},
14+
* {@link KeyType#HybridSecp256r1MLKEM768Key},
15+
* {@link KeyType#HybridSecp384r1MLKEM1024Key}</li>
16+
* <li><b>Pure ML-KEM (FIPS 203):</b> {@link KeyType#MLKEM768Key},
17+
* {@link KeyType#MLKEM1024Key}</li>
1618
* </ul>
1719
*
1820
* <p>Discovered by {@link io.opentdf.platform.sdk.spi.KemProviders} via
@@ -25,7 +27,9 @@ public final class BouncyCastleKemProvider implements KemProvider {
2527
private static final Set<KeyType> SUPPORTED = EnumSet.of(
2628
KeyType.HybridXWingKey,
2729
KeyType.HybridSecp256r1MLKEM768Key,
28-
KeyType.HybridSecp384r1MLKEM1024Key);
30+
KeyType.HybridSecp384r1MLKEM1024Key,
31+
KeyType.MLKEM768Key,
32+
KeyType.MLKEM1024Key);
2933

3034
/** Public no-arg constructor required by {@link java.util.ServiceLoader}. */
3135
public BouncyCastleKemProvider() {

0 commit comments

Comments
 (0)