Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6583675
ci: bump platform action SHA and enable dpop-nonce-challenge
dmihalcik-virtru Jun 15, 2026
c579df3
fix(js-shim): wire CLIENTID, CLIENTSECRET, and DPoP into cli.sh
dmihalcik-virtru Jun 16, 2026
6141d7d
fix(ci): pass OTDFCTL_HEADS to all pytest steps so otdfctl.sh uses di…
dmihalcik-virtru Jun 16, 2026
b05e397
ci: bump platform action SHA to pick up DPoP htu fix
dmihalcik-virtru Jun 16, 2026
c23d467
ci: bump platform action SHA to pick up strict_htu feature flag
dmihalcik-virtru Jun 16, 2026
f55e25d
feat(java-sdk): delegate dpop_nonce_challenge detection to binary
dmihalcik-virtru Jun 16, 2026
ccee061
fix(ci): correct SHA pin for platform action (24d7101c094b)
dmihalcik-virtru Jun 16, 2026
d47082e
fix(java-cli): enable --verbose when available for silent failure dia…
dmihalcik-virtru Jun 16, 2026
154f9ea
fix(java-cli): check root help for --verbose (it is ScopeType.INHERIT…
dmihalcik-virtru Jun 16, 2026
140ac4a
fix(ci): update platform action SHA pins to 70cb173a (fix DPoP htm va…
dmihalcik-virtru Jun 17, 2026
a44acfe
Cache java cli.sh help probes to cut JVM startup overhead
dmihalcik-virtru Jun 18, 2026
4d9e3f7
Address PR review: atomic cache write, [[ ]] tests, redact auth secret
dmihalcik-virtru Jun 18, 2026
9dd5f37
fix(ci): remove duplicate OTDFCTL_HEADS env key in xtest.yml
dmihalcik-virtru Jun 24, 2026
14d2361
ci(dpop): enforce require_nonce on additional KAS instances
dmihalcik-virtru Jun 24, 2026
a5b5508
ci(dpop): repin platform test actions to 4a19b297; make bearer test n…
dmihalcik-virtru Jun 25, 2026
424aed9
ci(dpop): repin platform test actions to 8ccc608d (enforce now opt-in)
dmihalcik-virtru Jun 25, 2026
7bd3ad1
test(dpop): entitle opentdf-dpop client in shared subject condition s…
dmihalcik Jun 26, 2026
6a2ecd6
chore: repin platform actions to main (0612ea89)
dmihalcik-virtru Jun 30, 2026
64e64b0
xtest: add dpop-challenge boolean input, default false
dmihalcik-virtru Jul 1, 2026
1e00adb
xtest: fix dpop-challenge empty string on PR trigger
dmihalcik-virtru Jul 1, 2026
7cbcc13
style(js/cli): use [[ ]] for all conditionals (SonarCloud SC2292)
dmihalcik-virtru Jul 1, 2026
984184d
style(cli): clear SonarCloud smells in js/java shims
dmihalcik-virtru Jul 1, 2026
92b902f
Merge branch 'main' into fix-dpop-nonce-challenge
dmihalcik-virtru Jul 1, 2026
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
30 changes: 23 additions & 7 deletions .github/workflows/xtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ on:
type: string
default: all
description: "SDK to focus on (go, js, java, all)"
dpop-challenge:
required: false
type: boolean
default: false
description: "Enable DPoP nonce challenge on KAS instances"
workflow_call:
inputs:
platform-ref:
Expand All @@ -50,6 +55,10 @@ on:
required: false
type: string
default: all
dpop-challenge:
required: false
type: boolean
default: false
schedule:
- cron: "30 6 * * *" # 0630 UTC
- cron: "0 5 * * 1,3" # 500 UTC (Monday, Wednesday)
Expand Down Expand Up @@ -297,13 +306,14 @@ jobs:
######## SPIN UP PLATFORM BACKEND #############
- name: Check out and start up platform with deps/containers
id: run-platform
uses: opentdf/platform/test/start-up-with-containers@11af44a5d4826ed281bf2e0e4e31d6ff6154b393 # pqc-enabled
uses: opentdf/platform/test/start-up-with-containers@0612ea897264e3c621ce076029a5e1e3f2d3971e # main
with:
platform-ref: ${{ fromJSON(needs.resolve-versions.outputs.platform-tag-to-sha)[matrix.platform-tag] }}
ec-tdf-enabled: true
extra-keys: ${{ steps.load-extra-keys.outputs.EXTRA_KEYS }}
log-type: json
pqc-enabled: ${{ steps.pqc-check.outputs.supported == 'true' }}
dpop-challenge-enabled: ${{ inputs.dpop-challenge || false }}

- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
Expand Down Expand Up @@ -591,55 +601,59 @@ jobs:
- name: Start additional kas
id: kas-alpha
if: ${{ steps.multikas.outputs.supported == 'true' }}
uses: opentdf/platform/test/start-additional-kas@11af44a5d4826ed281bf2e0e4e31d6ff6154b393 # pqc-enabled
uses: opentdf/platform/test/start-additional-kas@0612ea897264e3c621ce076029a5e1e3f2d3971e # main: require_nonce on additional KAS
with:
ec-tdf-enabled: true
kas-name: alpha
kas-port: 8181
log-type: json
pqc-enabled: ${{ steps.pqc-check.outputs.supported == 'true' }}
root-key: ${{ steps.km-check.outputs.root_key }}
dpop-challenge-enabled: ${{ inputs.dpop-challenge || false }}

- name: Start additional kas
id: kas-beta
if: ${{ steps.multikas.outputs.supported == 'true' }}
uses: opentdf/platform/test/start-additional-kas@11af44a5d4826ed281bf2e0e4e31d6ff6154b393 # pqc-enabled
uses: opentdf/platform/test/start-additional-kas@0612ea897264e3c621ce076029a5e1e3f2d3971e # main: require_nonce on additional KAS
with:
ec-tdf-enabled: true
kas-name: beta
kas-port: 8282
log-type: json
pqc-enabled: ${{ steps.pqc-check.outputs.supported == 'true' }}
root-key: ${{ steps.km-check.outputs.root_key }}
dpop-challenge-enabled: ${{ inputs.dpop-challenge || false }}

- name: Start additional kas
id: kas-gamma
if: ${{ steps.multikas.outputs.supported == 'true' }}
uses: opentdf/platform/test/start-additional-kas@11af44a5d4826ed281bf2e0e4e31d6ff6154b393 # pqc-enabled
uses: opentdf/platform/test/start-additional-kas@0612ea897264e3c621ce076029a5e1e3f2d3971e # main: require_nonce on additional KAS
with:
ec-tdf-enabled: true
kas-name: gamma
kas-port: 8383
log-type: json
pqc-enabled: ${{ steps.pqc-check.outputs.supported == 'true' }}
root-key: ${{ steps.km-check.outputs.root_key }}
dpop-challenge-enabled: ${{ inputs.dpop-challenge || false }}

- name: Start additional kas
id: kas-delta
if: ${{ steps.multikas.outputs.supported == 'true' }}
uses: opentdf/platform/test/start-additional-kas@11af44a5d4826ed281bf2e0e4e31d6ff6154b393 # pqc-enabled
uses: opentdf/platform/test/start-additional-kas@0612ea897264e3c621ce076029a5e1e3f2d3971e # main: require_nonce on additional KAS
with:
ec-tdf-enabled: true
kas-port: 8484
kas-name: delta
log-type: json
pqc-enabled: ${{ steps.pqc-check.outputs.supported == 'true' }}
root-key: ${{ steps.km-check.outputs.root_key }}
dpop-challenge-enabled: ${{ inputs.dpop-challenge || false }}

- name: Start additional KM kas (km1)
id: kas-km1
if: ${{ steps.multikas.outputs.supported == 'true' }}
uses: opentdf/platform/test/start-additional-kas@11af44a5d4826ed281bf2e0e4e31d6ff6154b393 # pqc-enabled
uses: opentdf/platform/test/start-additional-kas@0612ea897264e3c621ce076029a5e1e3f2d3971e # main: require_nonce on additional KAS
with:
ec-tdf-enabled: true
key-management: ${{ steps.km-check.outputs.supported }}
Expand All @@ -648,11 +662,12 @@ jobs:
log-type: json
pqc-enabled: ${{ steps.pqc-check.outputs.supported == 'true' }}
root-key: ${{ steps.km-check.outputs.root_key }}
dpop-challenge-enabled: ${{ inputs.dpop-challenge || false }}

- name: Start additional KM kas (km2)
id: kas-km2
if: ${{ steps.multikas.outputs.supported == 'true' }}
uses: opentdf/platform/test/start-additional-kas@11af44a5d4826ed281bf2e0e4e31d6ff6154b393 # pqc-enabled
uses: opentdf/platform/test/start-additional-kas@0612ea897264e3c621ce076029a5e1e3f2d3971e # main: require_nonce on additional KAS
with:
ec-tdf-enabled: true
kas-name: km2
Expand All @@ -661,6 +676,7 @@ jobs:
log-type: json
pqc-enabled: ${{ steps.pqc-check.outputs.supported == 'true' }}
root-key: ${{ steps.km-check.outputs.root_key }}
dpop-challenge-enabled: ${{ inputs.dpop-challenge || false }}

- name: Run attribute based configuration tests
if: ${{ steps.multikas.outputs.supported == 'true' }}
Expand Down
9 changes: 7 additions & 2 deletions xtest/fixtures/obligations.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
def otdf_client_scs(otdfctl: OpentdfCommandLineTool) -> abac.SubjectConditionSet:
"""
Creates a standard subject condition set for OpenTDF clients.
This condition set matches client IDs 'opentdf' or 'opentdf-sdk'.
This condition set matches client IDs 'opentdf', 'opentdf-sdk', or
'opentdf-dpop' (the DPoP-bound client used by the DPoP tests).

Returns:
abac.SubjectConditionSet: The created subject condition set
Expand All @@ -32,7 +33,11 @@ def otdf_client_scs(otdfctl: OpentdfCommandLineTool) -> abac.SubjectConditionSet
abac.Condition(
subject_external_selector_value=".clientId",
operator=abac.SubjectMappingOperatorEnum.IN,
subject_external_values=["opentdf", "opentdf-sdk"],
subject_external_values=[
"opentdf",
"opentdf-sdk",
"opentdf-dpop",
],
)
],
)
Expand Down
34 changes: 31 additions & 3 deletions xtest/sdk/java/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,30 @@ else
exit 1
fi

# Cache `java -jar cmdline.jar help [...]` output to avoid paying JVM startup
# (typically 150-500ms) for the capability probes on every encrypt/decrypt.
# Keyed by the jar's mtime so a reinstall invalidates the cache. stderr is
# discarded to keep JVM warnings (reflective-access, agent notices) out of logs.
jar_help() {
local jar="$SCRIPT_DIR/cmdline.jar"
local mtime
mtime=$(stat -c %Y "$jar" 2>/dev/null || stat -f %m "$jar" 2>/dev/null || echo 0)
local key
key=$(printf '%s' "$*" | tr -c 'a-zA-Z0-9' '_')
local uid
uid=$(id -u 2>/dev/null || echo default)
local cache="${TMPDIR:-/tmp}/xtest-java-help-${uid}-${mtime}-${key}"
if [[ ! -f "$cache" ]]; then
# Write to a process-unique temp file, then rename: concurrent xdist
# workers see either no cache or the complete file, never a partial read.
local tmp="${cache}.$$"
java -jar "$jar" help "$@" >"$tmp" 2>/dev/null
mv -f "$tmp" "$cache"
fi
cat "$cache"
return 0
}

if [ "$1" == "supports" ]; then
case "$2" in
autoconfigure | ns_grants)
Expand Down Expand Up @@ -118,8 +142,8 @@ if [ "$1" == "supports" ]; then
exit $?
;;
dpop_nonce_challenge)
echo "dpop_nonce_challenge not supported"
exit 1
java -jar "$SCRIPT_DIR"/cmdline.jar supports dpop_nonce_challenge
exit $?
;;
*)
echo "Unknown feature: $2"
Expand All @@ -135,7 +159,7 @@ args=(
)

# when we added support for KAS allowlist, we changed the platform endpoint format to require scheme
if java -jar "$SCRIPT_DIR"/cmdline.jar help decrypt | grep kas-allowlist; then
if jar_help decrypt | grep -q kas-allowlist; then
args+=("--platform-endpoint=$PLATFORMURL")
else
args+=("--platform-endpoint=$PLATFORMENDPOINT")
Expand Down Expand Up @@ -197,5 +221,9 @@ if [ -n "$XT_WITH_TARGET_MODE" ]; then
args+=(--with-target-mode "$XT_WITH_TARGET_MODE")
fi

if jar_help | grep -q -- '--verbose'; then
args+=(--verbose)
fi

echo java -jar "$SCRIPT_DIR"/cmdline.jar "${args[@]}" --file="$2" ">" "$3"
java -jar "$SCRIPT_DIR"/cmdline.jar "${args[@]}" --file="$2" >"$3"
90 changes: 63 additions & 27 deletions xtest/sdk/js/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
# XT_WITH_ATTRIBUTES [string] - Attributes to be used for encryption
# XT_WITH_MIME_TYPE [string] - MIME type for the encrypted file
# XT_WITH_TARGET_MODE [string] - Target spec mode for the encrypted file
# XT_WITH_DPOP [string] - Enable DPoP token binding; value selects algorithm (e.g. ES256)
# XT_WITH_DPOP_KEY [string] - Path to PEM-encoded PKCS8 private key for DPoP signing
# CLIENTID [string] - Override OIDC client ID (default: opentdf)
# CLIENTSECRET [string] - Override OIDC client secret (default: secret)
#
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)

Expand All @@ -26,7 +30,7 @@ if grep opentdf/cli "$SCRIPT_DIR/package.json"; then
CTL=@opentdf/cli
fi

if [ "$1" == "supports" ]; then
if [[ "$1" == "supports" ]]; then
if ! cd "$SCRIPT_DIR"; then
echo "failed: [cd $SCRIPT_DIR]"
exit 1
Expand Down Expand Up @@ -112,36 +116,51 @@ if [ "$1" == "supports" ]; then
fi

XTEST_DIR=$SCRIPT_DIR
while [ "$XTEST_DIR" != "/" ]; do
if [ -f "$XTEST_DIR/pyproject.toml" ] && grep -q 'name = "xtest"' "$XTEST_DIR/pyproject.toml"; then
while [[ "$XTEST_DIR" != "/" ]]; do
if [[ -f "$XTEST_DIR/pyproject.toml" ]] && grep -q 'name = "xtest"' "$XTEST_DIR/pyproject.toml"; then
break
fi
XTEST_DIR=$(dirname "$XTEST_DIR")
done

if [ "$XTEST_DIR" = "/" ]; then
if [[ "$XTEST_DIR" = "/" ]]; then
echo "xtest root (pyproject.toml with name = \"xtest\") not found."
exit 1
fi

# Capture any caller-set overrides before test.env unconditionally resets them.
_pre_clientid="${CLIENTID:-}"
_pre_clientsecret="${CLIENTSECRET:-}"

# shellcheck disable=SC1091
source "$XTEST_DIR"/test.env

# Restore caller overrides (e.g. from pytest monkeypatch for DPoP client).
[[ -n "$_pre_clientid" ]] && CLIENTID="$_pre_clientid"
[[ -n "$_pre_clientsecret" ]] && CLIENTSECRET="$_pre_clientsecret"

src_file=$(realpath "$2")
dst_file=$(realpath "$(dirname "$3")")/$(basename "$3")

args=(
--output "$dst_file"
--kasEndpoint "$KASURL"
--oidcEndpoint "$KCFULLURL"
--auth opentdf:secret
--auth "${CLIENTID:-opentdf}:${CLIENTSECRET:-secret}"
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if [[ -n "$XT_WITH_DPOP" ]]; then
args+=(--dpop "$XT_WITH_DPOP")
fi
if [[ -n "$XT_WITH_DPOP_KEY" ]]; then
args+=(--dpop-key "$XT_WITH_DPOP_KEY")
fi

args+=(--containerType tdf3)

if [ -n "$XT_WITH_ATTRIBUTES" ]; then
if [[ -n "$XT_WITH_ATTRIBUTES" ]]; then
attributes="$XT_WITH_ATTRIBUTES"
if [ -f "$attributes" ]; then
if [[ -f "$attributes" ]]; then
attributes=$(realpath "$attributes")
echo "Attributes are a file: $attributes"
args+=(--attributes "$attributes")
Expand All @@ -152,13 +171,13 @@ if [ -n "$XT_WITH_ATTRIBUTES" ]; then
fi
fi

if [ -n "$XT_WITH_ASSERTIONS" ]; then
if [[ -n "$XT_WITH_ASSERTIONS" ]]; then
assertions="$XT_WITH_ASSERTIONS"
if [ -f "$assertions" ]; then
if [[ -f "$assertions" ]]; then
assertions=$(realpath "$assertions")
echo "Assertions are a file: $assertions"
args+=(--assertions "$assertions")
elif [ "$(echo "$assertions" | jq -e . >/dev/null 2>&1 && echo valid || echo invalid)" == "valid" ]; then
elif [[ "$(echo "$assertions" | jq -e . >/dev/null 2>&1 && echo valid || echo invalid)" == "valid" ]]; then
# Assertions are plain json
echo "Assertions are plain json: $assertions"
args+=(--assertions "$assertions")
Expand All @@ -168,9 +187,9 @@ if [ -n "$XT_WITH_ASSERTIONS" ]; then
fi
fi

if [ -n "$XT_WITH_ASSERTION_VERIFICATION_KEYS" ]; then
if [[ -n "$XT_WITH_ASSERTION_VERIFICATION_KEYS" ]]; then
verification_keys="$XT_WITH_ASSERTION_VERIFICATION_KEYS"
if [ -f "$verification_keys" ]; then
if [[ -f "$verification_keys" ]]; then
verification_keys=$(realpath "$verification_keys")
echo "Verification keys are a file: $verification_keys"
args+=(--assertionVerificationKeys "$verification_keys")
Expand All @@ -185,39 +204,56 @@ if ! cd "$SCRIPT_DIR"; then
exit 1
fi

if [ "$1" == "encrypt" ]; then
# Echo a CLI invocation with the --auth secret masked, so CI logs never capture
# client credentials. The real (unmasked) args are still used for execution.
echo_redacted() {
local out=() a mask_next=0
for a in "$@"; do
if [[ "$mask_next" == 1 ]]; then
out+=("${a%%:*}:***")
mask_next=0
elif [[ "$a" == "--auth" ]]; then
out+=("$a")
mask_next=1
else
out+=("$a")
fi
done
echo "${out[@]}"
return 0
}

if [[ "$1" == "encrypt" ]]; then
if npx $CTL help | grep autoconfigure; then
args+=(--policyEndpoint "$PLATFORMURL" --autoconfigure true)
fi
if [ -n "$XT_WITH_ECDSA_BINDING" ]; then
if [ "$XT_WITH_ECDSA_BINDING" == "true" ]; then
args+=(--policyBinding ecdsa)
fi
if [[ "$XT_WITH_ECDSA_BINDING" == "true" ]]; then
args+=(--policyBinding ecdsa)
fi
if [ "$XT_WITH_ECWRAP" == 'true' ]; then
if [[ "$XT_WITH_ECWRAP" == 'true' ]]; then
args+=(--encapKeyType "ec:secp256r1")
fi

if [ "$XT_WITH_PLAINTEXT_POLICY" == "true" ]; then
if [[ "$XT_WITH_PLAINTEXT_POLICY" == "true" ]]; then
args+=(--policyType plaintext)
fi
if [ -n "$XT_WITH_TARGET_MODE" ]; then
if [[ -n "$XT_WITH_TARGET_MODE" ]]; then
args+=(--tdfSpecVersion "$XT_WITH_TARGET_MODE")
fi

echo npx $CTL encrypt "$src_file" "${args[@]}"
echo_redacted npx $CTL encrypt "$src_file" "${args[@]}"
npx $CTL encrypt "$src_file" "${args[@]}"
elif [ "$1" == "decrypt" ]; then
if [ "$XT_WITH_VERIFY_ASSERTIONS" == 'false' ]; then
elif [[ "$1" == "decrypt" ]]; then
if [[ "$XT_WITH_VERIFY_ASSERTIONS" == 'false' ]]; then
args+=(--noVerifyAssertions)
fi
if [ "$XT_WITH_ECWRAP" == 'true' ]; then
if [[ "$XT_WITH_ECWRAP" == 'true' ]]; then
args+=(--rewrapKeyType "ec:secp256r1")
fi
if [ -n "$XT_WITH_KAS_ALLOW_LIST" ]; then
if [[ -n "$XT_WITH_KAS_ALLOW_LIST" ]]; then
args+=(--allowList "$XT_WITH_KAS_ALLOW_LIST")
fi
if [ "$XT_WITH_IGNORE_KAS_ALLOWLIST" == "true" ]; then
if [[ "$XT_WITH_IGNORE_KAS_ALLOWLIST" == "true" ]]; then
args+=(--ignoreAllowList)
fi
# only ignore allowlist if the kas allowlist fetching from kas registry has not been implemented
Expand All @@ -227,7 +263,7 @@ elif [ "$1" == "decrypt" ]; then
args+=(--ignoreAllowList)
fi

echo npx $CTL decrypt "$src_file" "${args[@]}"
echo_redacted npx $CTL decrypt "$src_file" "${args[@]}"
npx $CTL decrypt "$src_file" "${args[@]}"
else
echo "Incorrect argument provided"
Expand Down
Loading
Loading