-
Notifications
You must be signed in to change notification settings - Fork 3
feat(sdk): DSPX-3309 add hybrid post-quantum key wrapping for KAS (X-Wing, ECDH+ML-KEM) #368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| # scripts/ | ||
|
|
||
| Developer scripts for the OpenTDF Java SDK. Not bundled with the published | ||
| artifacts. | ||
|
|
||
| ## `test-hybrid-pqc.sh` | ||
|
|
||
| End-to-end test of the Java SDK's hybrid post-quantum key wrapping | ||
| (`hpqt:xwing`, `hpqt:secp256r1-mlkem768`, `hpqt:secp384r1-mlkem1024`) against | ||
| a locally running OpenTDF platform. Per algorithm it: | ||
|
|
||
| 1. Confirms the KAS publishes a hybrid PEM for that algorithm (`grpcurl` | ||
| pre-flight, optional). | ||
| 2. Encrypts a small payload via the `cmdline` jar using | ||
| `--encap-key-type=<Hybrid…Key>`. | ||
| 3. Asserts the resulting TDF manifest has: | ||
| - `keyAccess[0].type == "hybrid-wrapped"` | ||
| - `keyAccess[0].ephemeralPublicKey` empty (the ephemeral material is | ||
| carried inside the ASN.1 envelope in `wrappedKey`) | ||
| - `keyAccess[0].wrappedKey` starts with the ASN.1 SEQUENCE byte `0x30` | ||
| 4. Decrypts the TDF (this is the step that actually exercises hybrid | ||
| decapsulation on the KAS rewrap path). | ||
| 5. Diffs the decrypted payload against the original. | ||
|
|
||
| On success the script also prints the plaintext, the full `keyAccess[0]` | ||
| (KAO), and the decrypted output for each algorithm so you can eyeball the | ||
| artifacts. | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| | Requirement | Notes | | ||
| |---|---| | ||
| | **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)`. | | ||
| | **Maven 3.9+** | Project uses standard `mvn clean install`. | | ||
| | **Buf token** | Proto generation requires auth. Either `buf registry login` once, or export `BUF_INPUT_HTTPS_USERNAME` / `BUF_INPUT_HTTPS_PASSWORD`. | | ||
| | **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`). | | ||
| | **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. | | ||
| | **CLI tools** | `java`, `mvn`, `unzip`, `jq` on `PATH`. `grpcurl` optional but recommended (drives the pre-flight check). | | ||
|
|
||
| ### Run it | ||
|
|
||
| From the repo root: | ||
|
|
||
| ```bash | ||
| # Full run — builds cmdline, pre-flight check, all 3 algorithms | ||
| PLATFORM_ENDPOINT=http://localhost:8080 scripts/test-hybrid-pqc.sh | ||
|
|
||
| # Reuse an already-built cmdline jar (much faster on iterative runs) | ||
| scripts/test-hybrid-pqc.sh --skip-build | ||
|
|
||
| # One algorithm only | ||
| scripts/test-hybrid-pqc.sh --algorithms HybridXWingKey | ||
|
|
||
| # Multiple specific algorithms (comma-separated) | ||
| scripts/test-hybrid-pqc.sh --algorithms HybridXWingKey,HybridSecp256r1MLKEM768Key | ||
|
|
||
| # Skip the grpcurl pre-flight (use when grpcurl isn't installed) | ||
| scripts/test-hybrid-pqc.sh --skip-kas-check | ||
| ``` | ||
|
|
||
| ### Configuration | ||
|
|
||
| All defaults match the existing CI workflow (`.github/workflows/checks.yaml`). | ||
| Override via flag or env var: | ||
|
|
||
| | Flag / Env | Default | Description | | ||
| |---|---|---| | ||
| | `--platform-endpoint` / `PLATFORM_ENDPOINT` | `http://localhost:8080` | Platform base URL | | ||
| | `--kas-url` / `KAS_URL` | same as platform endpoint | KAS URL passed to cmdline `encrypt` | | ||
| | `--client-id` / `CLIENT_ID` | `opentdf-sdk` | OIDC client id | | ||
| | `--client-secret` / `CLIENT_SECRET` | `secret` | OIDC client secret | | ||
| | `--attr` / `DATA_ATTR` | `https://example.com/attr/attr1/value/value1` | Attribute FQN attached to encrypt | | ||
| | `--algorithms` | all three | Comma-separated subset of `KeyType` enum names | | ||
| | `--skip-build` | (off) | Reuse `cmdline/target/cmdline.jar` | | ||
| | `--skip-kas-check` | (off) | Skip the `grpcurl` pre-flight | | ||
|
|
||
| ### Expected output | ||
|
|
||
| ``` | ||
| [OK] hpqt:xwing: KAS returns hybrid PEM (-----BEGIN XWING PUBLIC KEY-----) | ||
| [OK] hpqt:secp256r1-mlkem768: KAS returns hybrid PEM (-----BEGIN SECP256R1 MLKEM768 PUBLIC KEY-----) | ||
| [OK] hpqt:secp384r1-mlkem1024: KAS returns hybrid PEM (-----BEGIN SECP384R1 MLKEM1024 PUBLIC KEY-----) | ||
| ... | ||
| [OK] HybridXWingKey: manifest OK (hybrid-wrapped, ASN.1 envelope, no ephemeralPublicKey) | ||
| [OK] HybridXWingKey: round-trip OK | ||
| ... | ||
| All 3 hybrid algorithm(s) passed round-trip. | ||
| ``` | ||
|
|
||
| Exit code is 0 on success, 1 on any algorithm failure (other algorithms still | ||
| attempted), 2 on misuse. | ||
|
|
||
| ### Troubleshooting | ||
|
|
||
| | Symptom | Likely cause / fix | | ||
| |---|---| | ||
| | `Maven build failed ... Buf API token` | Run `buf registry login`, or export `BUF_INPUT_HTTPS_USERNAME` and `BUF_INPUT_HTTPS_PASSWORD`. | | ||
| | `Maven build failed ... Kotlin ... isAtLeastJava9` (stack trace) | JDK too new. `export JAVA_HOME=$(/usr/libexec/java_home -v 17)` and rerun. | | ||
| | `KAS returned no publicKey` | Platform isn't running, or isn't reachable at `$PLATFORM_ENDPOINT`. | | ||
| | `KAS returned a non-hybrid PEM` | The platform is up but no hybrid KAS key is registered for that algorithm. Register one and rerun. | | ||
| | `keyType='null'` (manifest assertion) | You're on an old branch where `TDF.java` doesn't yet route hybrid algorithms. Pull the latest branch HEAD. | | ||
| | `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. | | ||
|
|
||
| ### Known SDK gap | ||
|
|
||
| `KeyType.fromAlgorithm` and `KeyType.fromPublicKeyAlgorithm` | ||
| (`sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java`) don't yet map the | ||
| hybrid algorithm protobuf enums. Auto-discovery via the KAS registry | ||
| (`Config.KASInfo.fromKeyAccessServer`) will throw `IllegalArgumentException` | ||
| once the platform's proto definitions include `KAS_PUBLIC_KEY_ALG_ENUM_HPQT_*` | ||
| values. This script bypasses that path by using `--encap-key-type` explicitly; | ||
| extending the script to also exercise registry-discovery should wait until the | ||
| mapping is added. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,241 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # test-hybrid-pqc.sh — round-trip the Java SDK's hybrid post-quantum key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # wrapping against a locally running OpenTDF platform. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Per algorithm: encrypt → assert manifest → KAS rewrap → decrypt → diff. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Prereqs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # * Local platform up at $PLATFORM_ENDPOINT with hybrid KAS keys registered | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # for hpqt:xwing, hpqt:secp256r1-mlkem768, hpqt:secp384r1-mlkem1024 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # * java, mvn (JDK 17), unzip, jq on PATH | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # * grpcurl optional (used only for the pre-flight key-publication check) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Usage: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # scripts/test-hybrid-pqc.sh # full run, all 3 algorithms | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # scripts/test-hybrid-pqc.sh --skip-build # reuse existing jar | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # scripts/test-hybrid-pqc.sh --skip-kas-check # skip grpcurl pre-flight | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # scripts/test-hybrid-pqc.sh --algorithms HybridXWingKey # subset | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # PLATFORM_ENDPOINT=http://localhost:8080 scripts/test-hybrid-pqc.sh | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # See scripts/README.md for a full prereq + troubleshooting guide. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAR="$REPO_ROOT/cmdline/target/cmdline.jar" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PLATFORM_ENDPOINT="${PLATFORM_ENDPOINT:-http://localhost:8080}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KAS_URL="${KAS_URL:-$PLATFORM_ENDPOINT}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CLIENT_ID="${CLIENT_ID:-opentdf-sdk}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CLIENT_SECRET="${CLIENT_SECRET:-secret}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DATA_ATTR="${DATA_ATTR:-https://example.com/attr/attr1/value/value1}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ALGORITHMS=(HybridXWingKey HybridSecp256r1MLKEM768Key HybridSecp384r1MLKEM1024Key) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SKIP_BUILD=0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SKIP_KAS_CHECK=0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while [[ $# -gt 0 ]]; do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "$1" in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --skip-build) SKIP_BUILD=1; shift ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --skip-kas-check) SKIP_KAS_CHECK=1; shift ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --algorithms) IFS=, read -r -a ALGORITHMS <<< "$2"; shift 2 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --platform-endpoint) PLATFORM_ENDPOINT="$2"; shift 2 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --kas-url) KAS_URL="$2"; shift 2 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --attr) DATA_ATTR="$2"; shift 2 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --client-id) CLIENT_ID="$2"; shift 2 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --client-secret) CLIENT_SECRET="$2"; shift 2 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *) echo "unknown option: $1" >&2; exit 2 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| esac | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard flags that require a value. With Proposed fix+require_opt_value() {
+ local opt="$1"
+ local val="${2-}"
+ if [[ -z "$val" || "$val" == --* ]]; then
+ echo "missing value for $opt" >&2
+ exit 2
+ fi
+}
+
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-build) SKIP_BUILD=1; shift ;;
--skip-kas-check) SKIP_KAS_CHECK=1; shift ;;
- --algorithms) IFS=, read -r -a ALGORITHMS <<< "$2"; shift 2 ;;
- --platform-endpoint) PLATFORM_ENDPOINT="$2"; shift 2 ;;
- --kas-url) KAS_URL="$2"; shift 2 ;;
- --attr) DATA_ATTR="$2"; shift 2 ;;
- --client-id) CLIENT_ID="$2"; shift 2 ;;
- --client-secret) CLIENT_SECRET="$2"; shift 2 ;;
+ --algorithms) require_opt_value "$1" "${2-}"; IFS=, read -r -a ALGORITHMS <<< "$2"; shift 2 ;;
+ --platform-endpoint) require_opt_value "$1" "${2-}"; PLATFORM_ENDPOINT="$2"; shift 2 ;;
+ --kas-url) require_opt_value "$1" "${2-}"; KAS_URL="$2"; shift 2 ;;
+ --attr) require_opt_value "$1" "${2-}"; DATA_ATTR="$2"; shift 2 ;;
+ --client-id) require_opt_value "$1" "${2-}"; CLIENT_ID="$2"; shift 2 ;;
+ --client-secret) require_opt_value "$1" "${2-}"; CLIENT_SECRET="$2"; shift 2 ;;
-h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
done📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Map KeyType enum name → the hpqt:* algorithm string the KAS expects. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Function form (instead of `declare -A`) so this works on macOS bash 3.2. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alg_to_string() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "$1" in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| HybridXWingKey) echo "hpqt:xwing" ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| HybridSecp256r1MLKEM768Key) echo "hpqt:secp256r1-mlkem768" ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| HybridSecp384r1MLKEM1024Key) echo "hpqt:secp384r1-mlkem1024" ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *) return 1 ;; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| esac | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WORK_DIR="$(mktemp -d -t hybrid-pqc-XXXXXX)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trap 'rm -rf "$WORK_DIR"' EXIT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -t 1 ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GREEN=$'\033[0;32m'; RED=$'\033[0;31m'; YELLOW=$'\033[0;33m'; RESET=$'\033[0m' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GREEN=''; RED=''; YELLOW=''; RESET='' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass() { echo "${GREEN}[OK]${RESET} $*"; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail() { echo "${RED}[FAIL]${RESET} $*"; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info() { echo "${YELLOW}[..]${RESET} $*"; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require() { command -v "$1" >/dev/null 2>&1 || { fail "missing required tool: $1"; exit 2; }; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require java; require unzip; require jq | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [[ $SKIP_BUILD -eq 1 ]] || require mvn | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run_cmdline() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| java -jar "$JAR" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --client-id="$CLIENT_ID" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --client-secret="$CLIENT_SECRET" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --platform-endpoint="$PLATFORM_ENDPOINT" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -h "$@" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ##### 1. Build | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ $SKIP_BUILD -eq 0 ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info "Building cmdline (mvn clean install -DskipTests)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| build_log="$WORK_DIR/build.log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ! (cd "$REPO_ROOT" && mvn --batch-mode clean install -DskipTests) > "$build_log" 2>&1; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "Maven build failed. Tail of build log:" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail -40 "$build_log" | sed 's/^/ /' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if grep -q "Buf API token" "$build_log" 2>/dev/null; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "Hint: run 'buf registry login' or export BUF_INPUT_HTTPS_USERNAME / BUF_INPUT_HTTPS_PASSWORD before retrying." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass "Build complete" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info "Skipping build (--skip-build)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [[ -f "$JAR" ]] || { fail "jar not found at $JAR — run without --skip-build"; exit 1; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ##### 2. Pre-flight: confirm KAS publishes hybrid keys | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ $SKIP_KAS_CHECK -eq 0 ]] && command -v grpcurl >/dev/null 2>&1; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info "Pre-flight: querying KAS for hybrid public keys" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host="${PLATFORM_ENDPOINT#http://}"; host="${host#https://}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for alg_name in "${ALGORITHMS[@]}"; do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ! alg=$(alg_to_string "$alg_name"); then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "unknown algorithm: $alg_name"; exit 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resp=$(grpcurl -plaintext -d "{\"algorithm\":\"$alg\"}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "$host" kas.AccessService/PublicKey 2>&1 || true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pem=$(jq -r '.publicKey // empty' <<<"$resp" 2>/dev/null || true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -z "$pem" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg: KAS returned no publicKey. Response was:" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "$resp" | head -5 | sed 's/^/ /' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "Is the platform running with the PQC-capable KAS branch and the key registered?" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Hybrid PEMs have XWING or MLKEM markers; RSA/EC PEMs don't. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| first_line=$(echo "$pem" | head -1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ "$first_line" != *"XWING"* && "$first_line" != *"MLKEM"* && "$first_line" != *"HPQT"* && "$first_line" != *"HYBRID"* ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg: KAS returned a non-hybrid PEM (first line: $first_line)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "The KAS doesn't appear to have a hybrid key registered for $alg" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass "$alg: KAS returns hybrid PEM ($first_line)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info "Skipping KAS pre-flight check" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ##### 3. Round-trip each algorithm | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PAYLOAD="$WORK_DIR/payload" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| printf 'hybrid pqc round-trip payload @ %s\n' "$(date)" > "$PAYLOAD" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PAYLOAD_BYTES=$(wc -c < "$PAYLOAD" | tr -d ' ') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info "Test payload: $PAYLOAD_BYTES bytes" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " --- plaintext ---" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sed 's/^/ /' < "$PAYLOAD" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " --- end plaintext ---" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures=() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for alg_name in "${ALGORITHMS[@]}"; do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tdf="$WORK_DIR/test-${alg_name}.tdf" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out="$WORK_DIR/out-${alg_name}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enc_log="$WORK_DIR/encrypt-${alg_name}.log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dec_log="$WORK_DIR/decrypt-${alg_name}.log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info "[$alg_name] encrypt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ! run_cmdline encrypt \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --kas-url="$KAS_URL" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --mime-type=text/plain \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --attr="$DATA_ATTR" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --autoconfigure=false \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --encap-key-type="$alg_name" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -f "$PAYLOAD" > "$tdf" 2> "$enc_log"; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg_name: encrypt failed" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sed 's/^/ /' < "$enc_log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures+=("$alg_name (encrypt)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info "[$alg_name] verify manifest" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| manifest_entry=$(unzip -l "$tdf" 2>/dev/null | awk '/manifest\.json$/ {print $NF; exit}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -z "$manifest_entry" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg_name: no manifest.json entry inside $tdf" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures+=("$alg_name (manifest entry missing)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| manifest=$(unzip -p "$tdf" "$manifest_entry") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # In Manifest.java, the Java field `keyType` is annotated with | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # @SerializedName("type"), so the JSON key is "type" (not "keyType"). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keyType=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"$manifest") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ephem=$(jq -r '.encryptionInformation.keyAccess[0].ephemeralPublicKey // ""' <<<"$manifest") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wrapped=$(jq -r '.encryptionInformation.keyAccess[0].wrappedKey // ""' <<<"$manifest") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ "$keyType" != "hybrid-wrapped" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg_name: type='$keyType' (expected 'hybrid-wrapped')" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " keyAccess[0]:" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jq '.encryptionInformation.keyAccess[0]' <<<"$manifest" 2>/dev/null | sed 's/^/ /' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures+=("$alg_name (bad type: $keyType)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -n "$ephem" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg_name: ephemeralPublicKey unexpectedly set ('$ephem')" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures+=("$alg_name (stray ephemeralPublicKey)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -z "$wrapped" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg_name: wrappedKey is empty" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures+=("$alg_name (empty wrappedKey)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ASN.1 SEQUENCE always starts with 0x30 — same invariant HybridCryptoTest checks. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| first_byte=$(base64 -d <<<"$wrapped" 2>/dev/null | xxd -p -l 1 || true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ "$first_byte" != "30" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+197
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'MA==\n' | base64 -d >/dev/null 2>&1 && echo "base64 -d: supported" || echo "base64 -d: unsupported"
printf 'MA==\n' | base64 -D >/dev/null 2>&1 && echo "base64 -D: supported" || echo "base64 -D: unsupported"Repository: opentdf/java-sdk Length of output: 104 🏁 Script executed: fd -t f "test-hybrid-pqc.sh"Repository: opentdf/java-sdk Length of output: 87 🏁 Script executed: git ls-files | grep -i "test-hybrid-pqc"Repository: opentdf/java-sdk Length of output: 87 🏁 Script executed: cat -n scripts/test-hybrid-pqc.sh | sed -n '190,205p'Repository: opentdf/java-sdk Length of output: 916 🏁 Script executed: head -30 scripts/test-hybrid-pqc.shRepository: opentdf/java-sdk Length of output: 1340 🌐 Web query:
💡 Result: On GNU/Linux (coreutils), Citations:
🌐 Web query:
💡 Result: xxd vs od for hex dumps portability across macOS (BSD/Darwin) and BSD/Linux: 1) Portability: prefer od for “works everywhere” shell scripting - od is in coreutils on Linux and is also present in base userlands on macOS and BSD, with a consistent POSIX-style interface. [1][2] - BSD/GNU variants of od/hexdump options differ; for true portability you should use od options that exist across implementations (notably -A for offset radix, -t for hex element format, -v to avoid line compression). [2][3] 2) xxd: great ergonomics, but availability/behavior can vary - xxd produces hex dumps and can also reverse a hex dump back to binary (“-r”), and supports a variety of formatting styles and options. [4] - However, xxd is not “POSIX core”: on some minimal environments it may be missing (e.g., busybox). [1] 3) Practical guidance (portable one-liners) A. Most portable “byte-per-line hex (two hex chars)” using od The following approach uses only POSIX utilities around od, so it adapts to flag differences across od implementations: [5] - Example technique (from a cross-platform discussion): use od -An -tx1 -v to emit hex bytes, then post-process to get one byte per line, since BSD od may differ in flags like -w. [5] B. If you can tolerate different output styles, od -t is the key OpenBSD’s od documents -t TYPE strings where x selects hex output and you can add size/type suffixes like 1, 2, 4, etc. [2] So, for single-byte hex you can use forms like -t x1 with -A x (hex addresses) and -v (no collapsing). [2] 4) Recommendations by use case - For portable scripts across macOS + Linux + BSD: use od with -A/-t/-v, or use the “od + standard text filters” pattern. [1][2][5] - For interactive “nice-looking” dumps and reversible workflows (hexdump -> patch/binary): use xxd when available; it’s feature-rich (including reverse/patching). [4] Bottom line: - od is the most portable choice for hex dumping across macOS/BSD/Linux because it’s broadly available and supports hex output via -t. [1][2] - xxd is best when you want its richer UX and reverse/patch capabilities, but don’t assume it exists in every Unix-like environment. [4][1] Citations:
Fix base64 portability for macOS/BSD environments. The script uses Proposed fix+b64decode() {
+ if printf 'MA==\n' | base64 -d >/dev/null 2>&1; then
+ base64 -d
+ else
+ base64 -D
+ fi
+}
+
- first_byte=$(base64 -d <<<"$wrapped" 2>/dev/null | xxd -p -l 1 || true)
+ first_byte=$(b64decode <<<"$wrapped" 2>/dev/null | od -An -tx1 -N1 | tr -d ' \n' || true)🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg_name: wrappedKey does not start with ASN.1 SEQUENCE (got 0x$first_byte)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures+=("$alg_name (bad envelope)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass "$alg_name: manifest OK (hybrid-wrapped, ASN.1 envelope, no ephemeralPublicKey)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " --- keyAccess[0] (KAO) ---" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jq '.encryptionInformation.keyAccess[0]' <<<"$manifest" | sed 's/^/ /' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " --- end keyAccess[0] ---" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info "[$alg_name] decrypt (rewrap via KAS)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ! run_cmdline decrypt -f "$tdf" > "$out" 2> "$dec_log"; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg_name: decrypt failed" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sed 's/^/ /' < "$dec_log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures+=("$alg_name (decrypt)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ! diff -q "$PAYLOAD" "$out" >/dev/null; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail "$alg_name: decrypted payload differs from original" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " --- expected (first 200 bytes) ---" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| head -c 200 "$PAYLOAD" | sed 's/^/ /' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " --- got (first 200 bytes) ---" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| head -c 200 "$out" | sed 's/^/ /' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures+=("$alg_name (payload mismatch)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass "$alg_name: round-trip OK" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out_bytes=$(wc -c < "$out" | tr -d ' ') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " --- decrypted ($out_bytes bytes) ---" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sed 's/^/ /' < "$out" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " --- end decrypted ---" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ ${#failures[@]} -eq 0 ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "${GREEN}All ${#ALGORITHMS[@]} hybrid algorithm(s) passed round-trip.${RESET}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "${RED}FAILURES (${#failures[@]}):${RESET}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| printf ' - %s\n' "${failures[@]}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a language tag to the fenced code block.
This avoids markdownlint
MD040and improves rendering consistency.Proposed fix
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 79-79: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents