Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions scripts/README.md
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

```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced code block.

This avoids markdownlint MD040 and improves rendering consistency.

Proposed fix
-```
+```text
 [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.
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/README.md` at line 79, The fenced code block missing a language tag
should be updated to use a language identifier (e.g., add "text") so
markdownlint MD040 is satisfied; change the opening fence from ``` to ```text
for the block that contains the "[OK]   hpqt:..." lines and ensure the closing
fence remains ``` so rendering and linting are correct.

[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.
241 changes: 241 additions & 0 deletions scripts/test-hybrid-pqc.sh
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard flags that require a value.

With set -u, missing values for options like --algorithms or --kas-url can crash with a shell error instead of returning the documented misuse exit path.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
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) 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
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-hybrid-pqc.sh` around lines 38 - 50, The option parsing loop
currently reads values for flags like --algorithms, --platform-endpoint,
--kas-url, --attr, --client-id, and --client-secret without verifying a
following argument, which under set -u can cause a shell error; update the case
branches that assign to ALGORITHMS, PLATFORM_ENDPOINT, KAS_URL, DATA_ATTR,
CLIENT_ID, and CLIENT_SECRET to first validate that "$2" exists and is not
another option (e.g., [[ -n "${2-}" && "${2:0:1}" != "-" ]]) and if the check
fails print the usage/help and exit with the same misuse exit code (2), leaving
the boolean flags (--skip-build, --skip-kas-check) unchanged.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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.sh

Repository: opentdf/java-sdk

Length of output: 1340


🌐 Web query:

base64 -d vs -D GNU BSD macOS portability differences

💡 Result:

On GNU/Linux (coreutils), base64 -d and base64 --decode are the decode flags; there is no separate -D option in the GNU coreutils interface. [1][2] On macOS/BSD, base64 -D (or --decode) is the decode flag; base64’s BSD/GNU option set differs (e.g., BSD uses -D for decode in its man page). [3][4] So the portability difference is simply that you can’t use the same single-letter flag across GNU vs BSD/macOS: use --decode/-d on GNU, and --decode/-D on macOS/BSD. [1][2][3][4] Practical portable approach: prefer the long option --decode when available in both implementations (GNU coreutils documents base64 --decode; BSD/macOS man page documents --decode too). [1][3] If you must write a strictly portable shell snippet that works with both and long options ever differ, detect which variant is present (GNU coreutils vs BSD/macOS), but the evidence here supports --decode as the shared, portable spelling. [1][3]

Citations:


🌐 Web query:

xxd vs od hex dump portability macOS BSD Linux

💡 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 base64 -d, which is unsupported on macOS/BSD (they require base64 -D). This causes false envelope-check failures on those platforms. Additionally, replace xxd with od for better POSIX portability.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-hybrid-pqc.sh` around lines 197 - 198, The envelope-check fails
on macOS/BSD because the script calls `base64 -d` and `xxd`; update the
decoding/byte-extraction to be portable by trying `base64 -d` and falling back
to `base64 -D` (or vice versa) when decoding the `wrapped` variable, and replace
the `xxd -p -l 1` usage with an `od` invocation (e.g. `od -An -tx1 -N1`) to
reliably produce the first byte in hex; modify the assignment around
`first_byte=$(... )` and any place referencing `xxd`/`base64` so it uses this
portable approach while preserving the existing `wrapped` variable and the
subsequent `if [[ "$first_byte" != "30" ]]` check.

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
Loading
Loading