Skip to content

Commit 12f0b08

Browse files
committed
Test scripts
1 parent 38943bc commit 12f0b08

2 files changed

Lines changed: 354 additions & 0 deletions

File tree

scripts/README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# scripts/
2+
3+
Developer scripts for the OpenTDF Java SDK. Not bundled with the published
4+
artifacts.
5+
6+
## `test-hybrid-pqc.sh`
7+
8+
End-to-end test of the Java SDK's hybrid post-quantum key wrapping
9+
(`hpqt:xwing`, `hpqt:secp256r1-mlkem768`, `hpqt:secp384r1-mlkem1024`) against
10+
a locally running OpenTDF platform. Per algorithm it:
11+
12+
1. Confirms the KAS publishes a hybrid PEM for that algorithm (`grpcurl`
13+
pre-flight, optional).
14+
2. Encrypts a small payload via the `cmdline` jar using
15+
`--encap-key-type=<Hybrid…Key>`.
16+
3. Asserts the resulting TDF manifest has:
17+
- `keyAccess[0].type == "hybrid-wrapped"`
18+
- `keyAccess[0].ephemeralPublicKey` empty (the ephemeral material is
19+
carried inside the ASN.1 envelope in `wrappedKey`)
20+
- `keyAccess[0].wrappedKey` starts with the ASN.1 SEQUENCE byte `0x30`
21+
4. Decrypts the TDF (this is the step that actually exercises hybrid
22+
decapsulation on the KAS rewrap path).
23+
5. Diffs the decrypted payload against the original.
24+
25+
On success the script also prints the plaintext, the full `keyAccess[0]`
26+
(KAO), and the decrypted output for each algorithm so you can eyeball the
27+
artifacts.
28+
29+
### Prerequisites
30+
31+
| Requirement | Notes |
32+
|---|---|
33+
| **JDK 17** | The project's Kotlin compiler can't parse newer JDK version strings. Use Corretto/Temurin/etc. 17. On macOS: `export JAVA_HOME=$(/usr/libexec/java_home -v 17)`. |
34+
| **Maven 3.9+** | Project uses standard `mvn clean install`. |
35+
| **Buf token** | Proto generation requires auth. Either `buf registry login` once, or export `BUF_INPUT_HTTPS_USERNAME` / `BUF_INPUT_HTTPS_PASSWORD`. |
36+
| **Local platform with PQC support** | `opentdf/platform` checked out on a branch that implements `hpqt:*` KAS keys + the `hybrid-wrapped` rewrap path. See the platform repo for bring-up (`docker compose` / `make start`). |
37+
| **Hybrid KAS keys registered** | The local platform must have a KAS key registered for each `hpqt:*` algorithm you intend to test. Use `otdfctl` (or platform tooling) to register them. |
38+
| **CLI tools** | `java`, `mvn`, `unzip`, `jq` on `PATH`. `grpcurl` optional but recommended (drives the pre-flight check). |
39+
40+
### Run it
41+
42+
From the repo root:
43+
44+
```bash
45+
# Full run — builds cmdline, pre-flight check, all 3 algorithms
46+
PLATFORM_ENDPOINT=http://localhost:8080 scripts/test-hybrid-pqc.sh
47+
48+
# Reuse an already-built cmdline jar (much faster on iterative runs)
49+
scripts/test-hybrid-pqc.sh --skip-build
50+
51+
# One algorithm only
52+
scripts/test-hybrid-pqc.sh --algorithms HybridXWingKey
53+
54+
# Multiple specific algorithms (comma-separated)
55+
scripts/test-hybrid-pqc.sh --algorithms HybridXWingKey,HybridSecp256r1MLKEM768Key
56+
57+
# Skip the grpcurl pre-flight (use when grpcurl isn't installed)
58+
scripts/test-hybrid-pqc.sh --skip-kas-check
59+
```
60+
61+
### Configuration
62+
63+
All defaults match the existing CI workflow (`.github/workflows/checks.yaml`).
64+
Override via flag or env var:
65+
66+
| Flag / Env | Default | Description |
67+
|---|---|---|
68+
| `--platform-endpoint` / `PLATFORM_ENDPOINT` | `http://localhost:8080` | Platform base URL |
69+
| `--kas-url` / `KAS_URL` | same as platform endpoint | KAS URL passed to cmdline `encrypt` |
70+
| `--client-id` / `CLIENT_ID` | `opentdf-sdk` | OIDC client id |
71+
| `--client-secret` / `CLIENT_SECRET` | `secret` | OIDC client secret |
72+
| `--attr` / `DATA_ATTR` | `https://example.com/attr/attr1/value/value1` | Attribute FQN attached to encrypt |
73+
| `--algorithms` | all three | Comma-separated subset of `KeyType` enum names |
74+
| `--skip-build` | (off) | Reuse `cmdline/target/cmdline.jar` |
75+
| `--skip-kas-check` | (off) | Skip the `grpcurl` pre-flight |
76+
77+
### Expected output
78+
79+
```
80+
[OK] hpqt:xwing: KAS returns hybrid PEM (-----BEGIN XWING PUBLIC KEY-----)
81+
[OK] hpqt:secp256r1-mlkem768: KAS returns hybrid PEM (-----BEGIN SECP256R1 MLKEM768 PUBLIC KEY-----)
82+
[OK] hpqt:secp384r1-mlkem1024: KAS returns hybrid PEM (-----BEGIN SECP384R1 MLKEM1024 PUBLIC KEY-----)
83+
...
84+
[OK] HybridXWingKey: manifest OK (hybrid-wrapped, ASN.1 envelope, no ephemeralPublicKey)
85+
[OK] HybridXWingKey: round-trip OK
86+
...
87+
All 3 hybrid algorithm(s) passed round-trip.
88+
```
89+
90+
Exit code is 0 on success, 1 on any algorithm failure (other algorithms still
91+
attempted), 2 on misuse.
92+
93+
### Troubleshooting
94+
95+
| Symptom | Likely cause / fix |
96+
|---|---|
97+
| `Maven build failed ... Buf API token` | Run `buf registry login`, or export `BUF_INPUT_HTTPS_USERNAME` and `BUF_INPUT_HTTPS_PASSWORD`. |
98+
| `Maven build failed ... Kotlin ... isAtLeastJava9` (stack trace) | JDK too new. `export JAVA_HOME=$(/usr/libexec/java_home -v 17)` and rerun. |
99+
| `KAS returned no publicKey` | Platform isn't running, or isn't reachable at `$PLATFORM_ENDPOINT`. |
100+
| `KAS returned a non-hybrid PEM` | The platform is up but no hybrid KAS key is registered for that algorithm. Register one and rerun. |
101+
| `keyType='null'` (manifest assertion) | You're on an old branch where `TDF.java` doesn't yet route hybrid algorithms. Pull the latest branch HEAD. |
102+
| `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. |
103+
104+
### Known SDK gap
105+
106+
`KeyType.fromAlgorithm` and `KeyType.fromPublicKeyAlgorithm`
107+
(`sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java`) don't yet map the
108+
hybrid algorithm protobuf enums. Auto-discovery via the KAS registry
109+
(`Config.KASInfo.fromKeyAccessServer`) will throw `IllegalArgumentException`
110+
once the platform's proto definitions include `KAS_PUBLIC_KEY_ALG_ENUM_HPQT_*`
111+
values. This script bypasses that path by using `--encap-key-type` explicitly;
112+
extending the script to also exercise registry-discovery should wait until the
113+
mapping is added.

scripts/test-hybrid-pqc.sh

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
#!/usr/bin/env bash
2+
#
3+
# test-hybrid-pqc.sh — round-trip the Java SDK's hybrid post-quantum key
4+
# wrapping against a locally running OpenTDF platform.
5+
#
6+
# Per algorithm: encrypt → assert manifest → KAS rewrap → decrypt → diff.
7+
#
8+
# Prereqs:
9+
# * Local platform up at $PLATFORM_ENDPOINT with hybrid KAS keys registered
10+
# for hpqt:xwing, hpqt:secp256r1-mlkem768, hpqt:secp384r1-mlkem1024
11+
# * java, mvn (JDK 17), unzip, jq on PATH
12+
# * grpcurl optional (used only for the pre-flight key-publication check)
13+
#
14+
# Usage:
15+
# scripts/test-hybrid-pqc.sh # full run, all 3 algorithms
16+
# scripts/test-hybrid-pqc.sh --skip-build # reuse existing jar
17+
# scripts/test-hybrid-pqc.sh --skip-kas-check # skip grpcurl pre-flight
18+
# scripts/test-hybrid-pqc.sh --algorithms HybridXWingKey # subset
19+
# PLATFORM_ENDPOINT=http://localhost:8080 scripts/test-hybrid-pqc.sh
20+
#
21+
# See scripts/README.md for a full prereq + troubleshooting guide.
22+
23+
set -euo pipefail
24+
25+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
27+
JAR="$REPO_ROOT/cmdline/target/cmdline.jar"
28+
29+
PLATFORM_ENDPOINT="${PLATFORM_ENDPOINT:-http://localhost:8080}"
30+
KAS_URL="${KAS_URL:-$PLATFORM_ENDPOINT}"
31+
CLIENT_ID="${CLIENT_ID:-opentdf-sdk}"
32+
CLIENT_SECRET="${CLIENT_SECRET:-secret}"
33+
DATA_ATTR="${DATA_ATTR:-https://example.com/attr/attr1/value/value1}"
34+
ALGORITHMS=(HybridXWingKey HybridSecp256r1MLKEM768Key HybridSecp384r1MLKEM1024Key)
35+
SKIP_BUILD=0
36+
SKIP_KAS_CHECK=0
37+
38+
while [[ $# -gt 0 ]]; do
39+
case "$1" in
40+
--skip-build) SKIP_BUILD=1; shift ;;
41+
--skip-kas-check) SKIP_KAS_CHECK=1; shift ;;
42+
--algorithms) IFS=, read -r -a ALGORITHMS <<< "$2"; shift 2 ;;
43+
--platform-endpoint) PLATFORM_ENDPOINT="$2"; shift 2 ;;
44+
--kas-url) KAS_URL="$2"; shift 2 ;;
45+
--attr) DATA_ATTR="$2"; shift 2 ;;
46+
--client-id) CLIENT_ID="$2"; shift 2 ;;
47+
--client-secret) CLIENT_SECRET="$2"; shift 2 ;;
48+
-h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
49+
*) echo "unknown option: $1" >&2; exit 2 ;;
50+
esac
51+
done
52+
53+
# Map KeyType enum name → the hpqt:* algorithm string the KAS expects.
54+
# Function form (instead of `declare -A`) so this works on macOS bash 3.2.
55+
alg_to_string() {
56+
case "$1" in
57+
HybridXWingKey) echo "hpqt:xwing" ;;
58+
HybridSecp256r1MLKEM768Key) echo "hpqt:secp256r1-mlkem768" ;;
59+
HybridSecp384r1MLKEM1024Key) echo "hpqt:secp384r1-mlkem1024" ;;
60+
*) return 1 ;;
61+
esac
62+
}
63+
64+
WORK_DIR="$(mktemp -d -t hybrid-pqc-XXXXXX)"
65+
trap 'rm -rf "$WORK_DIR"' EXIT
66+
67+
if [[ -t 1 ]]; then
68+
GREEN=$'\033[0;32m'; RED=$'\033[0;31m'; YELLOW=$'\033[0;33m'; RESET=$'\033[0m'
69+
else
70+
GREEN=''; RED=''; YELLOW=''; RESET=''
71+
fi
72+
pass() { echo "${GREEN}[OK]${RESET} $*"; }
73+
fail() { echo "${RED}[FAIL]${RESET} $*"; }
74+
info() { echo "${YELLOW}[..]${RESET} $*"; }
75+
76+
require() { command -v "$1" >/dev/null 2>&1 || { fail "missing required tool: $1"; exit 2; }; }
77+
require java; require unzip; require jq
78+
[[ $SKIP_BUILD -eq 1 ]] || require mvn
79+
80+
run_cmdline() {
81+
java -jar "$JAR" \
82+
--client-id="$CLIENT_ID" \
83+
--client-secret="$CLIENT_SECRET" \
84+
--platform-endpoint="$PLATFORM_ENDPOINT" \
85+
-h "$@"
86+
}
87+
88+
##### 1. Build
89+
if [[ $SKIP_BUILD -eq 0 ]]; then
90+
info "Building cmdline (mvn clean install -DskipTests)"
91+
build_log="$WORK_DIR/build.log"
92+
if ! (cd "$REPO_ROOT" && mvn --batch-mode clean install -DskipTests) > "$build_log" 2>&1; then
93+
fail "Maven build failed. Tail of build log:"
94+
tail -40 "$build_log" | sed 's/^/ /'
95+
if grep -q "Buf API token" "$build_log" 2>/dev/null; then
96+
fail "Hint: run 'buf registry login' or export BUF_INPUT_HTTPS_USERNAME / BUF_INPUT_HTTPS_PASSWORD before retrying."
97+
fi
98+
exit 1
99+
fi
100+
pass "Build complete"
101+
else
102+
info "Skipping build (--skip-build)"
103+
fi
104+
[[ -f "$JAR" ]] || { fail "jar not found at $JAR — run without --skip-build"; exit 1; }
105+
106+
##### 2. Pre-flight: confirm KAS publishes hybrid keys
107+
if [[ $SKIP_KAS_CHECK -eq 0 ]] && command -v grpcurl >/dev/null 2>&1; then
108+
info "Pre-flight: querying KAS for hybrid public keys"
109+
host="${PLATFORM_ENDPOINT#http://}"; host="${host#https://}"
110+
for alg_name in "${ALGORITHMS[@]}"; do
111+
if ! alg=$(alg_to_string "$alg_name"); then
112+
fail "unknown algorithm: $alg_name"; exit 2
113+
fi
114+
resp=$(grpcurl -plaintext -d "{\"algorithm\":\"$alg\"}" \
115+
"$host" kas.AccessService/PublicKey 2>&1 || true)
116+
pem=$(jq -r '.publicKey // empty' <<<"$resp" 2>/dev/null || true)
117+
if [[ -z "$pem" ]]; then
118+
fail "$alg: KAS returned no publicKey. Response was:"
119+
echo "$resp" | head -5 | sed 's/^/ /'
120+
fail "Is the platform running with the PQC-capable KAS branch and the key registered?"
121+
exit 1
122+
fi
123+
# Hybrid PEMs have XWING or MLKEM markers; RSA/EC PEMs don't.
124+
first_line=$(echo "$pem" | head -1)
125+
if [[ "$first_line" != *"XWING"* && "$first_line" != *"MLKEM"* && "$first_line" != *"HPQT"* && "$first_line" != *"HYBRID"* ]]; then
126+
fail "$alg: KAS returned a non-hybrid PEM (first line: $first_line)"
127+
fail "The KAS doesn't appear to have a hybrid key registered for $alg"
128+
exit 1
129+
fi
130+
pass "$alg: KAS returns hybrid PEM ($first_line)"
131+
done
132+
else
133+
info "Skipping KAS pre-flight check"
134+
fi
135+
136+
##### 3. Round-trip each algorithm
137+
PAYLOAD="$WORK_DIR/payload"
138+
printf 'hybrid pqc round-trip payload @ %s\n' "$(date)" > "$PAYLOAD"
139+
PAYLOAD_BYTES=$(wc -c < "$PAYLOAD" | tr -d ' ')
140+
info "Test payload: $PAYLOAD_BYTES bytes"
141+
echo " --- plaintext ---"
142+
sed 's/^/ /' < "$PAYLOAD"
143+
echo " --- end plaintext ---"
144+
145+
failures=()
146+
for alg_name in "${ALGORITHMS[@]}"; do
147+
tdf="$WORK_DIR/test-${alg_name}.tdf"
148+
out="$WORK_DIR/out-${alg_name}"
149+
enc_log="$WORK_DIR/encrypt-${alg_name}.log"
150+
dec_log="$WORK_DIR/decrypt-${alg_name}.log"
151+
152+
info "[$alg_name] encrypt"
153+
if ! run_cmdline encrypt \
154+
--kas-url="$KAS_URL" \
155+
--mime-type=text/plain \
156+
--attr="$DATA_ATTR" \
157+
--autoconfigure=false \
158+
--encap-key-type="$alg_name" \
159+
-f "$PAYLOAD" > "$tdf" 2> "$enc_log"; then
160+
fail "$alg_name: encrypt failed"
161+
sed 's/^/ /' < "$enc_log"
162+
failures+=("$alg_name (encrypt)")
163+
continue
164+
fi
165+
166+
info "[$alg_name] verify manifest"
167+
manifest_entry=$(unzip -l "$tdf" 2>/dev/null | awk '/manifest\.json$/ {print $NF; exit}')
168+
if [[ -z "$manifest_entry" ]]; then
169+
fail "$alg_name: no manifest.json entry inside $tdf"
170+
failures+=("$alg_name (manifest entry missing)")
171+
continue
172+
fi
173+
manifest=$(unzip -p "$tdf" "$manifest_entry")
174+
# In Manifest.java, the Java field `keyType` is annotated with
175+
# @SerializedName("type"), so the JSON key is "type" (not "keyType").
176+
keyType=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"$manifest")
177+
ephem=$(jq -r '.encryptionInformation.keyAccess[0].ephemeralPublicKey // ""' <<<"$manifest")
178+
wrapped=$(jq -r '.encryptionInformation.keyAccess[0].wrappedKey // ""' <<<"$manifest")
179+
if [[ "$keyType" != "hybrid-wrapped" ]]; then
180+
fail "$alg_name: type='$keyType' (expected 'hybrid-wrapped')"
181+
echo " keyAccess[0]:"
182+
jq '.encryptionInformation.keyAccess[0]' <<<"$manifest" 2>/dev/null | sed 's/^/ /'
183+
failures+=("$alg_name (bad type: $keyType)")
184+
continue
185+
fi
186+
if [[ -n "$ephem" ]]; then
187+
fail "$alg_name: ephemeralPublicKey unexpectedly set ('$ephem')"
188+
failures+=("$alg_name (stray ephemeralPublicKey)")
189+
continue
190+
fi
191+
if [[ -z "$wrapped" ]]; then
192+
fail "$alg_name: wrappedKey is empty"
193+
failures+=("$alg_name (empty wrappedKey)")
194+
continue
195+
fi
196+
# ASN.1 SEQUENCE always starts with 0x30 — same invariant HybridCryptoTest checks.
197+
first_byte=$(base64 -d <<<"$wrapped" 2>/dev/null | xxd -p -l 1 || true)
198+
if [[ "$first_byte" != "30" ]]; then
199+
fail "$alg_name: wrappedKey does not start with ASN.1 SEQUENCE (got 0x$first_byte)"
200+
failures+=("$alg_name (bad envelope)")
201+
continue
202+
fi
203+
pass "$alg_name: manifest OK (hybrid-wrapped, ASN.1 envelope, no ephemeralPublicKey)"
204+
echo " --- keyAccess[0] (KAO) ---"
205+
jq '.encryptionInformation.keyAccess[0]' <<<"$manifest" | sed 's/^/ /'
206+
echo " --- end keyAccess[0] ---"
207+
208+
info "[$alg_name] decrypt (rewrap via KAS)"
209+
if ! run_cmdline decrypt -f "$tdf" > "$out" 2> "$dec_log"; then
210+
fail "$alg_name: decrypt failed"
211+
sed 's/^/ /' < "$dec_log"
212+
failures+=("$alg_name (decrypt)")
213+
continue
214+
fi
215+
if ! diff -q "$PAYLOAD" "$out" >/dev/null; then
216+
fail "$alg_name: decrypted payload differs from original"
217+
echo " --- expected (first 200 bytes) ---"
218+
head -c 200 "$PAYLOAD" | sed 's/^/ /'
219+
echo
220+
echo " --- got (first 200 bytes) ---"
221+
head -c 200 "$out" | sed 's/^/ /'
222+
echo
223+
failures+=("$alg_name (payload mismatch)")
224+
continue
225+
fi
226+
pass "$alg_name: round-trip OK"
227+
out_bytes=$(wc -c < "$out" | tr -d ' ')
228+
echo " --- decrypted ($out_bytes bytes) ---"
229+
sed 's/^/ /' < "$out"
230+
echo " --- end decrypted ---"
231+
done
232+
233+
echo
234+
if [[ ${#failures[@]} -eq 0 ]]; then
235+
echo "${GREEN}All ${#ALGORITHMS[@]} hybrid algorithm(s) passed round-trip.${RESET}"
236+
exit 0
237+
else
238+
echo "${RED}FAILURES (${#failures[@]}):${RESET}"
239+
printf ' - %s\n' "${failures[@]}"
240+
exit 1
241+
fi

0 commit comments

Comments
 (0)