Skip to content

feat(xtest): adds hybrid PQ/T KEM round trip testing#427

Merged
dmihalcik-virtru merged 17 commits into
mainfrom
DSPX-2791-xwing
Apr 29, 2026
Merged

feat(xtest): adds hybrid PQ/T KEM round trip testing#427
dmihalcik-virtru merged 17 commits into
mainfrom
DSPX-2791-xwing

Conversation

@dmihalcik-virtru
Copy link
Copy Markdown
Member

@dmihalcik-virtru dmihalcik-virtru commented Apr 8, 2026

Add integration test infrastructure for the X-Wing post-quantum/traditional
hybrid KEM algorithm (draft-connolly-cfrg-xwing-kem-10) to prepare for Q-Day
readiness testing.

  • Register hpqt:* algorithms in abac.py
  • Add "mechanism-xwing" and "mechanism-secpmlkem" feature type with platform version gating (>= 0.14.0)
  • Add SDK feature detection in via encrypt help grep
  • Create test_pqc_xwing.py with roundtrip tests for X-Wing-only and
    mixed X-Wing + EC multi-mechanism encryption
  • Assert X-Wing KEM-specific sizes (1216-byte encapsulation key,
    1120-byte ciphertext) on KAO and registered public key
  • TK add assertions for NIST curve hybrids

Tests will gracefully skip until platform and SDKs ship X-Wing support.

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Added otdf-local configure command for managing platform features
    • Added support for X-Wing hybrid post-quantum cryptography algorithms
    • Added install variant command for building custom SDK distributions
    • Introduced automated post-quantum cryptography test matrix
    • Enhanced Keycloak TLS certificate and keystore generation
  • Improvements

    • Improved platform key initialization during service startup
    • Enhanced JSON output handling for better scripting support

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

Warning

Rate limit exceeded

@dmihalcik-virtru has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 43 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5bb1c6a3-18d3-4ab9-92b0-9565f49a6ddf

📥 Commits

Reviewing files that changed from the base of the PR and between 1a70f96 and 7d73b6e.

📒 Files selected for processing (7)
  • xtest/abac.py
  • xtest/fixtures/keys.py
  • xtest/sdk/go/cli.sh
  • xtest/sdk/java/cli.sh
  • xtest/sdk/js/cli.sh
  • xtest/tdfs.py
  • xtest/test_pqc.py
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive post-quantum cryptography (X-Wing hybrid) support system across multiple repositories. Changes include a new platform feature override configuration system, Keycloak TLS key/certificate/keystore generation, enhanced CLI commands for platform configuration, new KAS algorithms and fixtures for X-Wing testing, SDK variant building capabilities, and a complete PQC test matrix infrastructure with roundtrip validation tests.

Changes

Cohort / File(s) Summary
Platform Configuration System
otdf-local/src/otdf_local/config/overrides.py, otdf-local/src/otdf_local/config/settings.py
New feature-override persistence layer mapping external identifiers to YAML paths; updated config paths to use opentdf.yaml (generated) and opentdf-dev.yaml (template) with platform directory validation.
CLI & Service Integration
otdf-local/src/otdf_local/cli.py, otdf-local/src/otdf_local/services/docker.py, otdf-local/src/otdf_local/services/platform.py, otdf-local/src/otdf_local/utils/console.py
New configure CLI command for feature selection/toggling; up command enhanced with pre-start key generation and override application; Docker service accepts keys_dir parameter; JSON output separated to stdout while diagnostics go to stderr.
Keycloak Key Management
otdf-local/src/otdf_local/utils/keys.py
Extensive new functions generating CA keypairs, localhost/sampleuser certificates with SANs, PKCS12 exports, and JKS keystores via Docker; includes M4 SIGILL workaround and combined orchestration helpers.
KAS Service Configuration
otdf-local/src/otdf_local/services/kas.py
Added hybrid_tdf_enabled preview flag for key-management KAS instances.
ABAC & Algorithm Support
xtest/abac.py
Added three new HPQT algorithm constants (HPQT_XWING, HPQT_SECP256R1_MLKEM768, HPQT_SECP384R1_MLKEM1024) and updated kas_algorithm_type literal with corresponding string values.
SDK Manager & Variant Building
otdf-sdk-mgr/src/otdf_sdk_mgr/cli_install.py, otdf-sdk-mgr/src/otdf_sdk_mgr/installers.py
New install variant subcommand; cmd_variant function generates variant-specific go.work, validates platform modules, and triggers make build-variant for target SDK builds.
Test Infrastructure & Fixtures
xtest/conftest.py, xtest/tdfs.py, xtest/fixtures/keys.py
SDK spec parser supporting sdk:version syntax; get_platform_features() function with require() helper; new X-Wing key fixtures and attribute fixtures combining X-Wing with EC keys.
PQC Test Suite & Orchestration
xtest/test_pqc.py, xtest/run-pqc-matrix.sh, xtest/sdk/go/Makefile, xtest/sdk/go/cli.sh, xtest/sdk/java/cli.sh, xtest/sdk/js/cli.sh
Complete X-Wing roundtrip test suite with manifest structure and cryptographic size validation; shell script support detection for mechanism-xwing across SDKs; build-variant Makefile target; test matrix orchestrator for multi-SDK/backend PQC testing.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as otdf-local CLI
    participant Keys as utils/keys.py
    participant Docker as DockerService
    participant Config as config/platform.py
    participant Overrides as config/overrides

    User->>CLI: otdf-local up
    CLI->>Keys: ensure_all_temp_keys(platform_dir, keys_dir)
    Keys->>Keys: ensure_keycloak_keys (CA, certs, PKCS12, JKS)
    Keys-->>CLI: ✓ Keys generated
    CLI->>Config: _generate_config()
    Config->>Overrides: load_overrides(xtest_root)
    Overrides-->>Config: override dict
    Config->>Config: apply_overrides(config_data)
    Config-->>CLI: ✓ Config with overrides applied
    CLI->>Docker: DockerService.start(keys_dir=...)
    Docker->>Docker: Set KEYS_DIR env variable
    Docker->>Docker: docker compose up -d
    Docker-->>CLI: ✓ Services started
    CLI-->>User: Platform ready with X-Wing support
Loading
sequenceDiagram
    participant Test as test_pqc.py
    participant Fixture as fixtures/keys.py
    participant Client as TDF Client
    participant KAS as KAS Service
    participant Platform as Platform Service

    Test->>Test: require('key_management/autoconfigure/mechanism-xwing')
    Test->>Fixture: key_xwing fixture
    Fixture->>KAS: Register X-Wing public key
    KAS-->>Fixture: ✓ Key registered
    Fixture-->>Test: X-Wing managed key
    Test->>Client: encrypt(plaintext, attribute_policy)
    Client->>KAS: Request key access object (X-Wing)
    KAS->>KAS: Generate wrapped key + ephemeral public key
    KAS-->>Client: Return KAO with X-Wing data
    Client-->>Test: TDF ciphertext (cached)
    Test->>Test: assert_xwing_kao_sizes()
    Test->>Test: assert_xwing_public_key_size()
    Test->>Client: decrypt(ciphertext)
    Client->>KAS: Unwrap key
    KAS-->>Client: Return plaintext key
    Client-->>Test: Decrypted plaintext
    Test->>Test: Verify plaintext matches original
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • pflynn-virtru
  • elizabethhealy
  • alkalescent

Poem

🐰 Keys and ciphers, quantum dreams abound,
X-Wing hybrids dance without a sound,
Keycloak keystores buried deep below,
While features override in CONFIG flow,
A rabbit's leap to PQC safety—hooray! 🔐✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 88.73% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title accurately captures the primary objective: adding X-Wing hybrid PQ/T KEM roundtrip testing support.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-2791-xwing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 7 minutes and 43 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for X-Wing hybrid post-quantum/traditional KEM (Key Encapsulation Mechanism). This includes updating ABAC definitions for X-Wing, adding new key management fixtures for X-Wing keys and attributes, and extending SDK CLI support checks to include the mechanism-xwing feature. A new test file test_pqc_xwing.py has been added to verify X-Wing encryption and decryption, including hybrid scenarios with EC keys. The review comments highlight a problematic global caching mechanism in the new X-Wing tests that could lead to test isolation issues and a PEP 8 violation. Additionally, there is a duplicate code block for platform feature caching and a redundant require method in tdfs.py that should be addressed for code cleanliness and API consistency.

Comment thread xtest/test_pqc.py
from abac import Attribute, KasKey
from tdfs import KeyAccessObject

cipherTexts: dict[str, Path] = {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The global cipherTexts dictionary used for caching encrypted TDF files is problematic. Since tmp_dir is typically a function-scoped fixture (like the standard tmp_path), the directory and its contents are deleted or rotated after each test iteration. Subsequent tests (e.g., when parametrized across different SDKs) will attempt to use a path from a previous test's tmp_dir that may no longer exist or is not associated with the current test execution.

Additionally, the variable name cipherTexts violates PEP 8 naming conventions (should be cipher_texts).

It is recommended to remove this caching logic and re-encrypt for each test case to ensure test isolation and reliability, or use a session-scoped fixture if caching is strictly necessary for performance.

Comment thread xtest/tdfs.py Outdated
Comment on lines +155 to +168
_cached_pfs: PlatformFeatureSet | None = None


def get_platform_features() -> PlatformFeatureSet:
"""Return a cached PlatformFeatureSet singleton."""
global _cached_pfs
if _cached_pfs is None:
_cached_pfs = PlatformFeatureSet()
return _cached_pfs


_cached_pfs: PlatformFeatureSet | None = None


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This entire block (lines 155-168) is a duplicate of the implementation provided immediately below (lines 169-174). It appears that _cached_pfs and get_platform_features were added twice in this file, likely due to a merge or copy-paste error. Please remove the redundant first definition.

Comment thread xtest/tdfs.py Outdated
Comment on lines +138 to +144
def require(self, *features: feature_type):
"""Skip the current test if any of the given features are unsupported."""
for feature in features:
if feature not in self.features:
pytest.skip(
f"platform service {self.version} doesn't yet support [{feature}]"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The require method is redundant as it provides nearly identical functionality to the existing skip_if_unsupported method (lines 146-152). To maintain a clean and consistent API, consider removing require and using skip_if_unsupported instead. skip_if_unsupported is slightly more informative as it identifies all missing features in a single skip message rather than stopping at the first one.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

junit used to call this method assume. Maybe I should do that...

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2026

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

X-Test Failure Report

✅ java-main
✅ java-v0.14.0

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

@dmihalcik-virtru dmihalcik-virtru marked this pull request as ready for review April 29, 2026 14:43
@dmihalcik-virtru dmihalcik-virtru requested review from a team as code owners April 29, 2026 14:43
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (9)
xtest/tdfs.py (1)

160-169: Remove duplicated singleton declarations for platform features.

_cached_pfs and get_platform_features() are defined twice in the module; keep a single definition to avoid drift and confusion.

♻️ Suggested cleanup
-_cached_pfs: PlatformFeatureSet | None = None
-
-
-def get_platform_features() -> PlatformFeatureSet:
-    """Return a cached PlatformFeatureSet singleton."""
-    global _cached_pfs
-    if _cached_pfs is None:
-        _cached_pfs = PlatformFeatureSet()
-    return _cached_pfs
-
-
 _cached_pfs: PlatformFeatureSet | None = None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/tdfs.py` around lines 160 - 169, There are duplicate definitions of the
singleton variable and accessor — remove the redundant copy so only one
_cached_pfs variable and one get_platform_features() function remain; locate
both symbol occurrences (get_platform_features and _cached_pfs), keep the
intended implementation (ensure _cached_pfs: PlatformFeatureSet | None is
declared and initialized once and get_platform_features() uses it to lazily
construct PlatformFeatureSet), and delete the other duplicate declarations to
avoid drift and confusion.
xtest/fixtures/keys.py (1)

229-233: Extraneous blank lines after docstrings.

There are blank lines between the docstrings and the return statements in key_secpmlkem_3 and key_secpmlkem_5. While not a functional issue, it's inconsistent with the other fixture definitions in this file.

♻️ Proposed fix
 def key_secpmlkem_3(
     ...
 ) -> abac.KasKey:
     """Get or create X-Wing hybrid hpqt:secp256r1-mlkem768 managed key on km1."""
- 
     return _get_or_create_key(
 def key_secpmlkem_5(
     ...
 ) -> abac.KasKey:
     """Get or create X-Wing hybrid hpqt:secp384r1-mlkem1024 managed key on km1."""
- 
     return _get_or_create_key(

Also applies to: 241-245

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/fixtures/keys.py` around lines 229 - 233, Remove the extra blank line
between the docstring and the return statement in the fixtures key_secpmlkem_3
and key_secpmlkem_5 so they match the other fixtures; locate the functions by
name and simply place the return _get_or_create_key(...) immediately after the
closing triple-quoted docstring (no empty line) to make formatting consistent.
xtest/test_pqc.py (1)

17-17: Module-level mutable state may cause issues with parallel test execution.

The cipherTexts dictionary caches encrypted files at module level. While this optimization avoids re-encrypting for each decrypt SDK, it can cause test pollution if tests run in parallel (e.g., pytest-xdist). If parallel execution isn't planned, this is acceptable; otherwise, consider using pytest's caching mechanisms or session-scoped fixtures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/test_pqc.py` at line 17, The module-level mutable dict cipherTexts can
cause test pollution under parallel runs; replace it with a session-scoped
pytest fixture (or use pytest's cache) that returns the dict so encryption
artifacts are shared safely across tests; specifically, remove the top-level
cipherTexts and implement a fixture (e.g., cipher_texts or session_cipher_texts)
with scope="session" which initializes and caches encrypted files and is
injected into tests that previously referenced cipherTexts.
xtest/run-pqc-matrix.sh (4)

44-46: Use [[ instead of [ for conditional tests.

As flagged by static analysis, [[ is safer and more feature-rich in Bash (proper handling of empty strings, no word splitting issues, pattern matching support).

♻️ Proposed fix
-  if [ ! -d "$platform_dir" ]; then
+  if [[ ! -d "$platform_dir" ]]; then
-  if [ ! -d "$backend_dir" ]; then
+  if [[ ! -d "$backend_dir" ]]; then
-    if $DIAGONAL_ONLY && [ "$si" != "$bi" ]; then
+    if $DIAGONAL_ONLY && [[ "$si" != "$bi" ]]; then

Also applies to: 73-77, 95-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/run-pqc-matrix.sh` around lines 44 - 46, Replace the POSIX `[` test
expressions with Bash `[[` in the conditional checks that verify
directories/paths so they handle empty strings and word splitting safely;
specifically update the `if [ ! -d "$platform_dir" ]; then` check (and the
similar checks around the other occurrences referenced) to use `[[ ! -d
"$platform_dir" ]]` (and the corresponding `[[ ... ]]` forms) while keeping the
same negation and `continue` behavior for the `variant` loop and other blocks
that check existence.

20-24: Hardcoded default paths reduce portability.

The default paths reference a specific developer's local directory structure ($HOME/Documents/GitHub/post-quantum-*). This will fail for other developers who don't have these exact paths. Consider either requiring the environment variables to be set or documenting the expected setup more prominently.

♻️ Proposed approach

Either fail fast if the env vars aren't set:

 VARIANTS=(gemini enhanced codex)
-PLATFORM_DIRS=(
-  "${PQC_GEMINI_DIR:-$HOME/Documents/GitHub/post-quantum-hybrid-gemini-2026-03-dm/platform}"
-  "${PQC_ENHANCED_DIR:-$HOME/Documents/GitHub/post-quantum-enhanced-2026-03-dm/platform}"
-  "${PQC_CODEX_DIR:-$HOME/Documents/GitHub/post-quantum-hybrid-codex-2026-03-dm/platform}"
-)
+: "${PQC_GEMINI_DIR:?PQC_GEMINI_DIR must be set to the gemini platform directory}"
+: "${PQC_ENHANCED_DIR:?PQC_ENHANCED_DIR must be set to the enhanced platform directory}"
+: "${PQC_CODEX_DIR:?PQC_CODEX_DIR must be set to the codex platform directory}"
+PLATFORM_DIRS=("$PQC_GEMINI_DIR" "$PQC_ENHANCED_DIR" "$PQC_CODEX_DIR")

Or use a relative/discoverable default path pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/run-pqc-matrix.sh` around lines 20 - 24, The PLATFORM_DIRS array uses
hardcoded defaults pointing to a specific developer's $HOME/Documents/GitHub
paths (see PLATFORM_DIRS, PQC_GEMINI_DIR, PQC_ENHANCED_DIR, PQC_CODEX_DIR);
change this to either (a) require the environment variables and exit with a
clear error if any of PQC_GEMINI_DIR, PQC_ENHANCED_DIR or PQC_CODEX_DIR are
unset, or (b) compute a discoverable relative/default path (e.g., relative to
the repository root or a configurable BASE_DIR) instead of embedding
user-specific $HOME/Documents/GitHub paths, and update the script to validate
each resolved path exists before proceeding.

85-86: Redirect error message to stderr.

Error messages should go to stderr for proper stream separation, allowing stdout to be piped/redirected independently.

♻️ Proposed fix
-    echo "ERROR: Failed to start backend $backend"
+    echo "ERROR: Failed to start backend $backend" >&2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/run-pqc-matrix.sh` around lines 85 - 86, The script prints the error
message with echo which goes to stdout; change the echo that prints "ERROR:
Failed to start backend $backend" so it writes to stderr instead (redirect the
echo/printf to stderr) while leaving the SUMMARY+="  BACKEND-FAIL $backend"$'\n'
update unchanged; update the echo invocation in run-pqc-matrix.sh (the line
referencing $backend and the SUMMARY variable) to send the error text to stderr.

105-109: Empty PYTEST_ARGS array causes issues when no extra arguments are passed.

When PYTEST_ARGS is empty, "${PYTEST_ARGS[@]}" expands to nothing which is fine, but if set -u is active (which it is via set -euo pipefail), some older bash versions may error. Additionally, ensure proper quoting is maintained.

♻️ Proposed defensive fix
-        "${PYTEST_ARGS[@]}" \
+        ${PYTEST_ARGS[@]+"${PYTEST_ARGS[@]}"} \

This syntax only expands the array if it's set and non-empty.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/run-pqc-matrix.sh` around lines 105 - 109, The if-line fails under set
-u when PYTEST_ARGS is unset/empty; update the pytest invocation to defensively
expand the array by replacing "${PYTEST_ARGS[@]}" with a safe expansion like
"${PYTEST_ARGS[@]+"${PYTEST_ARGS[@]}"}" (keep the existing quoting and placement
in the if condition that runs pytest so no other parts change).
otdf-local/src/otdf_local/utils/keys.py (2)

387-399: Subprocess errors are captured but not logged, making debugging difficult.

When subprocess.run(..., check=True, capture_output=True) fails, it raises CalledProcessError but the actual stderr output (which often contains the reason for failure) is captured and not displayed. Consider logging the error output on failure.

♻️ Proposed approach

Either use a helper that logs stderr on failure:

def _run_subprocess(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
    """Run subprocess, logging stderr on failure."""
    try:
        return subprocess.run(cmd, check=True, capture_output=True, **kwargs)
    except subprocess.CalledProcessError as e:
        logger.error("Command failed: %s\nstderr: %s", " ".join(cmd), e.stderr.decode())
        raise

Or log at the call sites in generate_ca_stores:

-    subprocess.run(cmd, check=True, capture_output=True)
+    result = subprocess.run(cmd, capture_output=True)
+    if result.returncode != 0:
+        logger.error("keytool failed: %s", result.stderr.decode())
+        result.check_returncode()  # raises CalledProcessError

Also applies to: 406-423

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@otdf-local/src/otdf_local/utils/keys.py` around lines 387 - 399, The
subprocess.run calls in generate_ca_stores (and similar calls around the PKCS12
and other openssl invocations) currently use check=True and capture_output=True
but do not log stderr on failure; wrap these calls in a try/except for
subprocess.CalledProcessError (or centralize into a helper like _run_subprocess)
and log the failing command and e.stderr.decode() via the module logger before
re-raising so you get the openssl error output; update every occurrence (e.g.,
the PKCS12 export block and the other openssl invocations around lines ~406-423)
to use the helper or the try/except pattern referencing the exact command list
passed to subprocess.run.

353-366: Image parsing assumes specific YAML format.

The _get_keycloak_image function uses simple string parsing to extract the image from a docker-compose file. This works for common formats but may fail on edge cases (e.g., quoted values, anchors, multi-line). Since this is for local dev tooling and has a sensible fallback, it's acceptable, but consider noting this limitation.

For more robust parsing, you could use PyYAML:

import yaml
data = yaml.safe_load(compose_file.read_text())
return data.get("services", {}).get("keycloak", {}).get("image", default)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@otdf-local/src/otdf_local/utils/keys.py` around lines 353 - 366, The
_get_keycloak_image function currently uses line-based string parsing which can
miss quoted values, anchors or multi-line YAML in docker-compose; replace the
heuristic with proper YAML parsing (e.g., use yaml.safe_load on
compose_file.read_text()) and read services -> keycloak -> image, returning the
default ("keycloak/keycloak:25.0") if the key path is missing or parsing fails;
ensure you catch and log OSError/YAML parsing exceptions and preserve the
existing fallback behavior in _get_keycloak_image.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@otdf-local/src/otdf_local/services/kas.py`:
- Around line 77-83: When self.is_key_management is true, the code currently
unconditionally sets updates["services.kas.preview.hybrid_tdf_enabled"], which
breaks on platforms lacking that field; change the block so you always set
updates["services.kas.preview.key_management"] and
updates["services.kas.preview.ec_tdf_enabled"] but only assign
updates["services.kas.preview.hybrid_tdf_enabled"] after verifying the platform
supports that config key (e.g., check the existing config/schema or a helper
like a platform feature list before setting), keep the registered_kas_uri
assignment as-is using self.port.

In `@otdf-local/src/otdf_local/utils/console.py`:
- Around line 11-13: You changed the default Console to write to stderr (console
= Console(stderr=True)) which will redirect any remaining JSON outputs that call
console.print(...) to stderr and break consumers; update code paths that emit
machine-readable data to use the dedicated _stdout_console instead (or change
those specific calls to _stdout_console.print(...))—search for uses of
console.print in places like the CLI JSON output (e.g., status --json /
functions invoking console.print) and replace them with _stdout_console.print to
ensure JSON goes to stdout while keeping diagnostics on stderr.

In `@xtest/abac.py`:
- Around line 185-197: Update the enum value for
KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING from 10 to 30 and ensure the mapping in
_KAS_ALG_TO_STR_MAP remains correct (the key KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING
should reference "hpqt:xwing"); locate the constant definition
KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING and change its numeric value to 30 so
serialization/deserialization uses the intended contract.

In `@xtest/conftest.py`:
- Around line 68-71: The validator currently accepts entries like "go:" because
it only extracts sdk_name and doesn't check for an empty version suffix; update
the loop that iterates over inputs (look for variables i, sdk_name, valid_sdks
and the function returning v) to split with two parts when ":" in i (e.g.,
sdk_name, version = i.split(":", 1)) and raise a ValueError if version is an
empty string (include the offending string and valid_sdks in the message); keep
the existing invalid-SDK check for sdk_name not in valid_sdks.

In `@xtest/fixtures/keys.py`:
- Around line 336-351: The docstrings for the fixtures
attribute_with_secpmlkem_3_key and attribute_with_secpmlkem_5_key incorrectly
say "assign an X-Wing key"; update each fixture's triple-quoted docstring to
accurately describe the key being assigned (for attribute_with_secpmlkem_3_key
mention secp256r1-mlkem768 / secpmlkem-3 and for attribute_with_secpmlkem_5_key
mention secp384r1-mlkem1024 / secpmlkem-5) so the description matches the keys
created by _create_keyed_attribute and the key fixtures key_secpmlkem_3 /
key_secpmlkem_5.

In `@xtest/sdk/go/cli.sh`:
- Around line 108-112: Update the case label in xtest/sdk/go/cli.sh from
"mechanism-secpmkem" to the correct feature name "mechanism-secpmlkem" so the
case branch matches the test framework; leave the body (set -o pipefail and the
"${cmd[@]}" help ... | grep ... and exit $?) unchanged so the existing behavior
executes when the correct feature string is encountered.

In `@xtest/sdk/go/Makefile`:
- Around line 54-58: The build step hardcodes the workspace path to
"$(MAKEFILE_DIR)/src/main" (used in the cd and GOWORK assignment) which prevents
branch-generated workspaces from being built; update the build-variant rules to
use a variable representing the generated workspace directory (e.g., replace
"$(MAKEFILE_DIR)/src/main" with a parameterized path like
"$(MAKEFILE_DIR)/src/$(VARIANT)" or a dedicated variable such as "$(GOWORK_DIR)"
and ensure the cd and GOWORK assignments reference that variable (the cd command
and the GOWORK=$(abspath ...) assignment) so branch-specific go.work files are
used instead of the hardcoded main folder.

In `@xtest/sdk/js/cli.sh`:
- Around line 94-97: The mechanism-xwing probe currently runs a global help
check using "npx $CTL help | grep -i xwing" which can miss mechanism-specific
flags; change the probe to call the CLI's mechanism-specific help command (use
"npx $CTL encrypt-help" or the CLI's encrypt-help subcommand) and grep that
output for "xwing" so mechanism-xwing support is detected reliably (update the
mechanism-xwing block where "npx $CTL help | grep -i xwing" appears).

In `@xtest/test_pqc.py`:
- Around line 172-177: The test currently iterates
manifest.encryptionInformation.keyAccess comparing kao.kid to xwing_kid but
never fails if no match is found; modify the loop in test_pqc.py to record
whether a matching X-Wing KAO was found (e.g., set a boolean found_xwing_kAO
when calling assert_xwing_kao_sizes(kao)) and after the loop add an assertion
that found_xwing_kAO is True so the test fails when no X-Wing KAO is present;
keep existing calls to assert_xwing_kao_sizes and
assert_xwing_public_key_size(key_xwing).

---

Nitpick comments:
In `@otdf-local/src/otdf_local/utils/keys.py`:
- Around line 387-399: The subprocess.run calls in generate_ca_stores (and
similar calls around the PKCS12 and other openssl invocations) currently use
check=True and capture_output=True but do not log stderr on failure; wrap these
calls in a try/except for subprocess.CalledProcessError (or centralize into a
helper like _run_subprocess) and log the failing command and e.stderr.decode()
via the module logger before re-raising so you get the openssl error output;
update every occurrence (e.g., the PKCS12 export block and the other openssl
invocations around lines ~406-423) to use the helper or the try/except pattern
referencing the exact command list passed to subprocess.run.
- Around line 353-366: The _get_keycloak_image function currently uses
line-based string parsing which can miss quoted values, anchors or multi-line
YAML in docker-compose; replace the heuristic with proper YAML parsing (e.g.,
use yaml.safe_load on compose_file.read_text()) and read services -> keycloak ->
image, returning the default ("keycloak/keycloak:25.0") if the key path is
missing or parsing fails; ensure you catch and log OSError/YAML parsing
exceptions and preserve the existing fallback behavior in _get_keycloak_image.

In `@xtest/fixtures/keys.py`:
- Around line 229-233: Remove the extra blank line between the docstring and the
return statement in the fixtures key_secpmlkem_3 and key_secpmlkem_5 so they
match the other fixtures; locate the functions by name and simply place the
return _get_or_create_key(...) immediately after the closing triple-quoted
docstring (no empty line) to make formatting consistent.

In `@xtest/run-pqc-matrix.sh`:
- Around line 44-46: Replace the POSIX `[` test expressions with Bash `[[` in
the conditional checks that verify directories/paths so they handle empty
strings and word splitting safely; specifically update the `if [ ! -d
"$platform_dir" ]; then` check (and the similar checks around the other
occurrences referenced) to use `[[ ! -d "$platform_dir" ]]` (and the
corresponding `[[ ... ]]` forms) while keeping the same negation and `continue`
behavior for the `variant` loop and other blocks that check existence.
- Around line 20-24: The PLATFORM_DIRS array uses hardcoded defaults pointing to
a specific developer's $HOME/Documents/GitHub paths (see PLATFORM_DIRS,
PQC_GEMINI_DIR, PQC_ENHANCED_DIR, PQC_CODEX_DIR); change this to either (a)
require the environment variables and exit with a clear error if any of
PQC_GEMINI_DIR, PQC_ENHANCED_DIR or PQC_CODEX_DIR are unset, or (b) compute a
discoverable relative/default path (e.g., relative to the repository root or a
configurable BASE_DIR) instead of embedding user-specific $HOME/Documents/GitHub
paths, and update the script to validate each resolved path exists before
proceeding.
- Around line 85-86: The script prints the error message with echo which goes to
stdout; change the echo that prints "ERROR: Failed to start backend $backend" so
it writes to stderr instead (redirect the echo/printf to stderr) while leaving
the SUMMARY+="  BACKEND-FAIL $backend"$'\n' update unchanged; update the echo
invocation in run-pqc-matrix.sh (the line referencing $backend and the SUMMARY
variable) to send the error text to stderr.
- Around line 105-109: The if-line fails under set -u when PYTEST_ARGS is
unset/empty; update the pytest invocation to defensively expand the array by
replacing "${PYTEST_ARGS[@]}" with a safe expansion like
"${PYTEST_ARGS[@]+"${PYTEST_ARGS[@]}"}" (keep the existing quoting and placement
in the if condition that runs pytest so no other parts change).

In `@xtest/tdfs.py`:
- Around line 160-169: There are duplicate definitions of the singleton variable
and accessor — remove the redundant copy so only one _cached_pfs variable and
one get_platform_features() function remain; locate both symbol occurrences
(get_platform_features and _cached_pfs), keep the intended implementation
(ensure _cached_pfs: PlatformFeatureSet | None is declared and initialized once
and get_platform_features() uses it to lazily construct PlatformFeatureSet), and
delete the other duplicate declarations to avoid drift and confusion.

In `@xtest/test_pqc.py`:
- Line 17: The module-level mutable dict cipherTexts can cause test pollution
under parallel runs; replace it with a session-scoped pytest fixture (or use
pytest's cache) that returns the dict so encryption artifacts are shared safely
across tests; specifically, remove the top-level cipherTexts and implement a
fixture (e.g., cipher_texts or session_cipher_texts) with scope="session" which
initializes and caches encrypted files and is injected into tests that
previously referenced cipherTexts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9ba500fb-83b7-4afc-97d1-9210dff2a70c

📥 Commits

Reviewing files that changed from the base of the PR and between feaff0e and 1a70f96.

⛔ Files ignored due to path filters (9)
  • xtest/results/codex-on-codex.log is excluded by !**/*.log
  • xtest/results/codex-on-enhanced.log is excluded by !**/*.log
  • xtest/results/codex-on-gemini.log is excluded by !**/*.log
  • xtest/results/enhanced-on-codex.log is excluded by !**/*.log
  • xtest/results/enhanced-on-enhanced.log is excluded by !**/*.log
  • xtest/results/enhanced-on-gemini.log is excluded by !**/*.log
  • xtest/results/gemini-on-codex.log is excluded by !**/*.log
  • xtest/results/gemini-on-enhanced.log is excluded by !**/*.log
  • xtest/results/gemini-on-gemini.log is excluded by !**/*.log
📒 Files selected for processing (20)
  • otdf-local/src/otdf_local/cli.py
  • otdf-local/src/otdf_local/config/overrides.py
  • otdf-local/src/otdf_local/config/settings.py
  • otdf-local/src/otdf_local/services/docker.py
  • otdf-local/src/otdf_local/services/kas.py
  • otdf-local/src/otdf_local/services/platform.py
  • otdf-local/src/otdf_local/utils/console.py
  • otdf-local/src/otdf_local/utils/keys.py
  • otdf-sdk-mgr/src/otdf_sdk_mgr/cli_install.py
  • otdf-sdk-mgr/src/otdf_sdk_mgr/installers.py
  • xtest/abac.py
  • xtest/conftest.py
  • xtest/fixtures/keys.py
  • xtest/run-pqc-matrix.sh
  • xtest/sdk/go/Makefile
  • xtest/sdk/go/cli.sh
  • xtest/sdk/java/cli.sh
  • xtest/sdk/js/cli.sh
  • xtest/tdfs.py
  • xtest/test_pqc.py

Comment on lines 77 to 83
if self.is_key_management:
updates["services.kas.preview.key_management"] = True
updates["services.kas.preview.ec_tdf_enabled"] = True
updates["services.kas.preview.hybrid_tdf_enabled"] = True
# registered_kas_uri should NOT have /kas suffix
updates["services.kas.registered_kas_uri"] = f"http://localhost:{self.port}"

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

Gate hybrid_tdf_enabled behind platform feature support.

Line 80 enables the HPQT preview flag for every KM KAS config. On platform versions without that field, this can cause config incompatibility during service startup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@otdf-local/src/otdf_local/services/kas.py` around lines 77 - 83, When
self.is_key_management is true, the code currently unconditionally sets
updates["services.kas.preview.hybrid_tdf_enabled"], which breaks on platforms
lacking that field; change the block so you always set
updates["services.kas.preview.key_management"] and
updates["services.kas.preview.ec_tdf_enabled"] but only assign
updates["services.kas.preview.hybrid_tdf_enabled"] after verifying the platform
supports that config key (e.g., check the existing config/schema or a helper
like a platform feature list before setting), keep the registered_kas_uri
assignment as-is using self.port.

Comment on lines +11 to +13
# Global console instance (stderr for all status/diagnostic output)
console = Console(stderr=True)
_stdout_console = Console() # stdout for machine-readable data 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 | 🟠 Major

Potential JSON output regression after moving default console to stderr.

This shift is good for diagnostics, but any remaining JSON paths using console.print(...) will now emit on stderr (e.g., status --json path in otdf-local/src/otdf_local/cli.py around Line 357), which can break consumers expecting stdout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@otdf-local/src/otdf_local/utils/console.py` around lines 11 - 13, You changed
the default Console to write to stderr (console = Console(stderr=True)) which
will redirect any remaining JSON outputs that call console.print(...) to stderr
and break consumers; update code paths that emit machine-readable data to use
the dedicated _stdout_console instead (or change those specific calls to
_stdout_console.print(...))—search for uses of console.print in places like the
CLI JSON output (e.g., status --json / functions invoking console.print) and
replace them with _stdout_console.print to ensure JSON goes to stdout while
keeping diagnostics on stderr.

Comment thread xtest/abac.py
Comment thread xtest/conftest.py Outdated
Comment on lines +68 to +71
sdk_name = i.split(":")[0] if ":" in i else i
if sdk_name not in valid_sdks:
raise ValueError(f"Invalid SDK '{sdk_name}' in '{i}', must be one of {valid_sdks}")
return v
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

Reject malformed qualified SDK specs like go: at option-parse time.

Right now empty version suffixes pass validation and fail later during SDK path resolution with a less actionable error.

✅ Suggested validation hardening
     def is_a(v: str) -> typing.Any:
         for i in v.split():
-            sdk_name = i.split(":")[0] if ":" in i else i
+            if ":" in i:
+                sdk_name, version = i.split(":", 1)
+                if not version:
+                    raise ValueError(f"Invalid SDK spec '{i}': missing version after ':'")
+            else:
+                sdk_name = i
             if sdk_name not in valid_sdks:
                 raise ValueError(f"Invalid SDK '{sdk_name}' in '{i}', must be one of {valid_sdks}")
         return v
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/conftest.py` around lines 68 - 71, The validator currently accepts
entries like "go:" because it only extracts sdk_name and doesn't check for an
empty version suffix; update the loop that iterates over inputs (look for
variables i, sdk_name, valid_sdks and the function returning v) to split with
two parts when ":" in i (e.g., sdk_name, version = i.split(":", 1)) and raise a
ValueError if version is an empty string (include the offending string and
valid_sdks in the message); keep the existing invalid-SDK check for sdk_name not
in valid_sdks.

Comment thread xtest/fixtures/keys.py
Comment thread xtest/sdk/go/cli.sh Outdated
Comment thread xtest/sdk/go/Makefile Outdated
Comment on lines +54 to +58
@test -f "$(MAKEFILE_DIR)/src/main/go.work.$(VARIANT)" || { echo "Error: $(MAKEFILE_DIR)/src/main/go.work.$(VARIANT) not found"; exit 1; }
@echo "Building variant $(VARIANT) with GOWORK=go.work.$(VARIANT)"
cd $(MAKEFILE_DIR)/src/main && \
GOWORK=$(abspath $(MAKEFILE_DIR)/src/main/go.work.$(VARIANT)) \
go build -o $(MAKEFILE_DIR)/binary-$(VARIANT) . || { \
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

build-variant is hardcoded to src/main, breaking branch-based variant builds.

Line 54 and Line 56 force src/main, so install variant --branch <non-main> cannot build the generated workspace for that branch.

Proposed fix
 # Build a variant using a specific go.work file from src/main/
-# Usage: make build-variant VARIANT=gemini
-# Requires: src/main/go.work.$(VARIANT) must exist
+# Usage: make build-variant VARIANT=gemini [BRANCH=main]
+# Requires: src/$(BRANCH)/go.work.$(VARIANT) must exist
 build-variant:
+	`@BRANCH_DIR`="$${BRANCH:-main}"; \
 	`@test` -n "$(VARIANT)" || { echo "Error: VARIANT is required (e.g., make build-variant VARIANT=gemini)"; exit 1; }
-	`@test` -f "$(MAKEFILE_DIR)/src/main/go.work.$(VARIANT)" || { echo "Error: $(MAKEFILE_DIR)/src/main/go.work.$(VARIANT) not found"; exit 1; }
+	`@test` -f "$(MAKEFILE_DIR)/src/$${BRANCH_DIR}/go.work.$(VARIANT)" || { echo "Error: $(MAKEFILE_DIR)/src/$${BRANCH_DIR}/go.work.$(VARIANT) not found"; exit 1; }
 	`@echo` "Building variant $(VARIANT) with GOWORK=go.work.$(VARIANT)"
-	cd $(MAKEFILE_DIR)/src/main && \
-		GOWORK=$(abspath $(MAKEFILE_DIR)/src/main/go.work.$(VARIANT)) \
+	cd "$(MAKEFILE_DIR)/src/$${BRANCH_DIR}" && \
+		GOWORK="$(abspath $(MAKEFILE_DIR)/src/$${BRANCH_DIR}/go.work.$(VARIANT))" \
 		go build -o $(MAKEFILE_DIR)/binary-$(VARIANT) . || { \
 			echo "Error: Go build failed for variant $(VARIANT)"; \
 			exit 1; \
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xtest/sdk/go/Makefile` around lines 54 - 58, The build step hardcodes the
workspace path to "$(MAKEFILE_DIR)/src/main" (used in the cd and GOWORK
assignment) which prevents branch-generated workspaces from being built; update
the build-variant rules to use a variable representing the generated workspace
directory (e.g., replace "$(MAKEFILE_DIR)/src/main" with a parameterized path
like "$(MAKEFILE_DIR)/src/$(VARIANT)" or a dedicated variable such as
"$(GOWORK_DIR)" and ensure the cd and GOWORK assignments reference that variable
(the cd command and the GOWORK=$(abspath ...) assignment) so branch-specific
go.work files are used instead of the hardcoded main folder.

Comment thread xtest/sdk/js/cli.sh
Comment thread xtest/test_pqc.py
dmihalcik-virtru and others added 10 commits April 29, 2026 13:20
…pport

Add integration test infrastructure for the X-Wing post-quantum/traditional
hybrid KEM algorithm (draft-connolly-cfrg-xwing-kem-10) to prepare for Q-Day
readiness testing.

- Register hpqt:xwing algorithm (enum 30) in abac.py
- Add "mechanism-xwing" feature type with platform version gating (>= 0.14.0)
- Add SDK feature detection in go/java/js cli.sh via encrypt help grep
- Create fixtures/pqc.py with key_xwing, attribute_with_xwing_key, and
  attribute_with_xwing_and_ec_keys fixtures
- Create test_pqc_xwing.py with roundtrip tests for X-Wing-only and
  mixed X-Wing + EC multi-mechanism encryption
- Assert X-Wing KEM-specific sizes (1216-byte encapsulation key,
  1120-byte ciphertext) on KAO and registered public key

Tests will gracefully skip until platform and SDKs ship X-Wing support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support building and testing multiple post-quantum platform variants
side by side. This enables comparing different X-Wing implementations
(e.g., from separate branches) by building variant-specific otdfctl
binaries and switching the platform backend at runtime.

- otdf-sdk-mgr: add `install variant` command that generates per-variant
  go.work files and builds otdfctl against a platform variant's modules
- otdf-local: enable OTDF_LOCAL_PLATFORM_DIR env var to override the
  auto-discovered platform directory
- Go SDK Makefile: add `build-variant` target using GOWORK env var
- xtest: extend --sdks to accept sdk:version qualifiers (e.g., go:gemini)
  for filtering specific SDK versions during test runs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When using OTDF_LOCAL_PLATFORM_DIR to point at a fresh platform
checkout (e.g. PQC variant branches), the required KAS and Keycloak
TLS keys may not exist yet. This adds automatic key generation during
`otdf-local up` so variant backends work out of the box.

KAS keys are per-variant (in platform_dir), while Keycloak CA/TLS
keys are shared in xtest/tmp/keys/ and passed via KEYS_DIR env var
to docker compose.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nv) works cleanly

All diagnostic/status console output now goes to stderr. Machine-readable
data (JSON output, shell export lines) goes to stdout. Adds print_json()
helper using a stdout Console for use in env and ls --json commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

X-Test Failure Report

@dmihalcik-virtru dmihalcik-virtru changed the title feat(xtest): add X-Wing hybrid PQ/T KEM (ML-KEM-768 + X25519) test support feat(xtest): adds hybrid PQ/T KEM round trip testing Apr 29, 2026
@github-actions
Copy link
Copy Markdown

X-Test Failure Report

@github-actions
Copy link
Copy Markdown

X-Test Failure Report

@github-actions
Copy link
Copy Markdown

X-Test Failure Report

1 similar comment
@github-actions
Copy link
Copy Markdown

X-Test Failure Report

@github-actions
Copy link
Copy Markdown

X-Test Failure Report

Comment thread xtest/fixtures/keys.py Outdated
Comment thread xtest/fixtures/keys.py Outdated
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
19.4% Duplication on New Code (required ≤ 8%)

See analysis details on SonarQube Cloud

@github-actions
Copy link
Copy Markdown

X-Test Failure Report

@github-actions
Copy link
Copy Markdown

@dmihalcik-virtru dmihalcik-virtru merged commit ea2976e into main Apr 29, 2026
14 of 15 checks passed
@dmihalcik-virtru dmihalcik-virtru deleted the DSPX-2791-xwing branch April 29, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants