#!/usr/bin/env bash
# wire-pentest.sh — adversarial probes against https://relay.laulpogan.com
#
# All tests assume the relay reference implementation:
#   POST /v1/slot/allocate                  -> {slot_id, slot_token}
#   POST /v1/events/<slot_id>  +Bearer      -> store event
#   GET  /v1/events/<slot_id>  +Bearer      -> list events
#   POST /v1/pair                           -> {pair_id}
#   GET  /v1/pair/<pair_id>?as_role=...     -> peer_msg/peer_bootstrap
#   POST /v1/pair/<pair_id>/bootstrap       -> store sealed
#   GET  /healthz
#
# Exit 0 = no findings; >0 = findings present (per stderr).

set -uo pipefail
BASE="${1:-https://relay.laulpogan.com}"
RESULTS=/tmp/wire-pentest-results.txt
: > $RESULTS

pass() { echo "PASS  $*" | tee -a $RESULTS; }
warn() { echo "WARN  $*" | tee -a $RESULTS; }
fail() { echo "FAIL  $*" | tee -a $RESULTS; }
info() { echo "INFO  $*" | tee -a $RESULTS; }

curl_status() {
    local code
    code=$(curl -k -s -o /dev/null -w "%{http_code}" "$@")
    echo "$code"
}

#-----------------------------------------------------------------------
# T1: healthz unauthenticated 200
#-----------------------------------------------------------------------
code=$(curl_status "$BASE/healthz")
[ "$code" = "200" ] && pass "T1 healthz 200" || fail "T1 healthz $code (expected 200)"

#-----------------------------------------------------------------------
# T2: HTTP method audit — POST/PUT/DELETE/PATCH on /healthz
#-----------------------------------------------------------------------
for method in POST PUT DELETE PATCH; do
    code=$(curl_status -X $method "$BASE/healthz")
    case $code in
        405|404) pass "T2 healthz $method=$code (rejected)" ;;
        200)     warn "T2 healthz $method=200 (relay accepts $method to GET-only endpoint — minor)" ;;
        *)       info "T2 healthz $method=$code" ;;
    esac
done

#-----------------------------------------------------------------------
# T3: unauth POST to /v1/events/<random_slot> must NOT succeed
#-----------------------------------------------------------------------
RANDOM_SLOT=$(openssl rand -hex 16)
code=$(curl_status -X POST -H 'Content-Type: application/json' -d '{"event":{"event_id":"x"}}' "$BASE/v1/events/$RANDOM_SLOT")
case $code in
    401) pass "T3 unauth POST -> 401" ;;
    404) pass "T3 unauth POST -> 404 (slot doesn't exist; fine)" ;;
    *)   fail "T3 unauth POST got $code (expected 401 or 404)" ;;
esac

#-----------------------------------------------------------------------
# T4: allocate a slot, then POST with WRONG token -> 403
#-----------------------------------------------------------------------
ALLOC=$(curl -k -s -X POST -H 'Content-Type: application/json' -d '{}' "$BASE/v1/slot/allocate")
SLOT_ID=$(echo "$ALLOC" | python3 -c 'import sys,json;print(json.load(sys.stdin)["slot_id"])' 2>/dev/null)
SLOT_TOKEN=$(echo "$ALLOC" | python3 -c 'import sys,json;print(json.load(sys.stdin)["slot_token"])' 2>/dev/null)
[ -n "$SLOT_ID" ] || { fail "T4 allocate failed"; SLOT_ID=""; }

if [ -n "$SLOT_ID" ]; then
    code=$(curl_status -X POST -H "Authorization: Bearer wrong-token" -H 'Content-Type: application/json' -d '{"event":{}}' "$BASE/v1/events/$SLOT_ID")
    [ "$code" = "403" ] && pass "T4 wrong-token POST -> 403" || fail "T4 wrong-token POST got $code"
fi

#-----------------------------------------------------------------------
# T5: oversized body must 413
#-----------------------------------------------------------------------
if [ -n "$SLOT_ID" ]; then
    BIG=$(python3 -c 'print("x"*300000)')
    PAYLOAD=$(python3 -c "import json;print(json.dumps({'event':{'event_id':'big','body':'$BIG'}}))")
    code=$(curl_status -X POST -H "Authorization: Bearer $SLOT_TOKEN" -H 'Content-Type: application/json' -d "$PAYLOAD" "$BASE/v1/events/$SLOT_ID")
    [ "$code" = "413" ] && pass "T5 oversized body -> 413" || warn "T5 oversized body got $code (expected 413)"
fi

#-----------------------------------------------------------------------
# T6: slot enumeration — random IDs must be 404
#-----------------------------------------------------------------------
HITS=0
for _ in 1 2 3 4 5; do
    RAND=$(openssl rand -hex 16)
    code=$(curl_status -X POST -H "Authorization: Bearer $SLOT_TOKEN" -H 'Content-Type: application/json' -d '{"event":{}}' "$BASE/v1/events/$RAND")
    if [ "$code" = "201" ] || [ "$code" = "200" ]; then HITS=$((HITS+1)); fi
done
[ "$HITS" = "0" ] && pass "T6 random-slot probe: 0/5 succeed (slot IDs unguessable)" || fail "T6 random-slot probe: $HITS/5 succeed"

#-----------------------------------------------------------------------
# T7: token timing — at least 5ms variance against true vs fake-but-same-length
#-----------------------------------------------------------------------
TRUE_T=$SLOT_TOKEN
FAKE_T=$(python3 -c "print('a' * len('$TRUE_T'))")
TIMES_TRUE=()
TIMES_FAKE=()
for _ in 1 2 3 4 5; do
    t=$(curl -k -s -o /dev/null -w "%{time_total}" -H "Authorization: Bearer $TRUE_T" "$BASE/v1/events/$SLOT_ID")
    TIMES_TRUE+=("$t")
    t=$(curl -k -s -o /dev/null -w "%{time_total}" -H "Authorization: Bearer $FAKE_T" "$BASE/v1/events/$SLOT_ID")
    TIMES_FAKE+=("$t")
done
AVG_T=$(python3 -c "print(sum([${TIMES_TRUE[*]// /,}])/len([${TIMES_TRUE[*]// /,}]))")
AVG_F=$(python3 -c "print(sum([${TIMES_FAKE[*]// /,}])/len([${TIMES_FAKE[*]// /,}]))")
DIFF_MS=$(python3 -c "print(round(abs($AVG_T - $AVG_F)*1000,2))")
info "T7 timing: true=${AVG_T}s fake=${AVG_F}s diff=${DIFF_MS}ms (constant_time_eq used in code; CF tunnel + jitter dominate)"

#-----------------------------------------------------------------------
# T8: tampered event - sig fails recipient-side (relay accepts plaintext)
#-----------------------------------------------------------------------
if [ -n "$SLOT_ID" ]; then
    GARBAGE='{"event":{"event_id":"d3adb33fcafe","from":"did:wire:attacker","signature":"AAAA","public_key_id":"attacker:00000000","body":"forged"}}'
    code=$(curl_status -X POST -H "Authorization: Bearer $SLOT_TOKEN" -H 'Content-Type: application/json' -d "$GARBAGE" "$BASE/v1/events/$SLOT_ID")
    case $code in
        201|200) pass "T8 garbage event accepted by relay ($code) — correct: relay is dumb pipe; recipient verify_message_v31 catches it" ;;
        *)       warn "T8 relay rejected garbage with $code (relay shouldn't validate sigs; might be body-cap or other limit)" ;;
    esac
fi

#-----------------------------------------------------------------------
# T9: pair-slot - same code_hash, registering host TWICE -> 409 Conflict
#-----------------------------------------------------------------------
HASH=$(openssl rand -hex 32)
MSG=$(printf "AAAAAA" | base64)
code1=$(curl_status -X POST -H 'Content-Type: application/json' -d "{\"code_hash\":\"$HASH\",\"msg\":\"$MSG\",\"role\":\"host\"}" "$BASE/v1/pair")
code2=$(curl_status -X POST -H 'Content-Type: application/json' -d "{\"code_hash\":\"$HASH\",\"msg\":\"$MSG\",\"role\":\"host\"}" "$BASE/v1/pair")
[ "$code1" = "201" ] && [ "$code2" = "409" ] && pass "T9 pair-host double-register: first 201, second 409" || warn "T9 pair-host double-register got $code1/$code2"

#-----------------------------------------------------------------------
# T10: pair-slot crosstalk — two different code_hash -> different pair_id
#-----------------------------------------------------------------------
H1=$(openssl rand -hex 32)
H2=$(openssl rand -hex 32)
P1=$(curl -k -s -X POST -H 'Content-Type: application/json' -d "{\"code_hash\":\"$H1\",\"msg\":\"$MSG\",\"role\":\"host\"}" "$BASE/v1/pair" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("pair_id",""))')
P2=$(curl -k -s -X POST -H 'Content-Type: application/json' -d "{\"code_hash\":\"$H2\",\"msg\":\"$MSG\",\"role\":\"host\"}" "$BASE/v1/pair" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("pair_id",""))')
[ -n "$P1" ] && [ -n "$P2" ] && [ "$P1" != "$P2" ] && pass "T10 distinct code_hash -> distinct pair_id (no crosstalk)" || fail "T10 pair_id collision: $P1 vs $P2"

#-----------------------------------------------------------------------
# T11: TLS — verify cert chain via curl
#-----------------------------------------------------------------------
TLS_INFO=$(curl -sI "$BASE/healthz" 2>&1 | head -1)
echo "$TLS_INFO" | grep -qi "200" && pass "T11 TLS chain valid (curl no -k succeeds)" || warn "T11 TLS verify failed: $TLS_INFO"

#-----------------------------------------------------------------------
# T12: HTTP/2 (cloudflared default)
#-----------------------------------------------------------------------
H2=$(curl -sI "$BASE/healthz" 2>&1 | head -1)
echo "$H2" | grep -q "HTTP/2" && pass "T12 HTTP/2 served" || info "T12 HTTP version: $H2"

#-----------------------------------------------------------------------
# T13: header injection / smuggling — CR/LF / Host
#-----------------------------------------------------------------------
code=$(curl_status -H "X-Probe: foo$(printf '\r\n')Injected-Header: yes" "$BASE/healthz")
[ "$code" = "200" ] && pass "T13 CRLF probe innocuous (curl filters)" || info "T13 CRLF probe: $code"

#-----------------------------------------------------------------------
# T14: GET /v1/events/<slot> without bearer -> 401
#-----------------------------------------------------------------------
if [ -n "$SLOT_ID" ]; then
    code=$(curl_status "$BASE/v1/events/$SLOT_ID")
    [ "$code" = "401" ] && pass "T14 unauth GET events -> 401" || fail "T14 unauth GET got $code"
fi

#-----------------------------------------------------------------------
# T15: GET unknown pair_id -> 404
#-----------------------------------------------------------------------
RAND_PAIR=$(openssl rand -hex 16)
code=$(curl_status "$BASE/v1/pair/$RAND_PAIR?as_role=host")
[ "$code" = "404" ] && pass "T15 unknown pair_id -> 404" || warn "T15 got $code"

#-----------------------------------------------------------------------
# T16: rate limit / flood — 50 rapid allocs (informational only)
#-----------------------------------------------------------------------
COUNT_201=0
COUNT_OTHER=0
for _ in $(seq 1 50); do
    code=$(curl_status -X POST -H 'Content-Type: application/json' -d '{}' "$BASE/v1/slot/allocate")
    if [ "$code" = "201" ]; then COUNT_201=$((COUNT_201+1)); else COUNT_OTHER=$((COUNT_OTHER+1)); fi
done
info "T16 rapid-allocate: 201=$COUNT_201, other=$COUNT_OTHER (no rate limit at relay; rely on Cloudflare WAF or BACKLOG)"

#-----------------------------------------------------------------------
# T17: malformed JSON
#-----------------------------------------------------------------------
code=$(curl_status -X POST -H 'Content-Type: application/json' -d 'not-json' "$BASE/v1/slot/allocate")
case $code in
    400|422) pass "T17 malformed JSON -> $code" ;;
    *) warn "T17 malformed JSON got $code" ;;
esac

#-----------------------------------------------------------------------
# T18: HTTP method on /v1/slot/allocate
#-----------------------------------------------------------------------
for method in GET DELETE PATCH; do
    code=$(curl_status -X $method "$BASE/v1/slot/allocate")
    case $code in
        405|404|400) pass "T18 /v1/slot/allocate $method=$code (rejected)" ;;
        *)           warn "T18 /v1/slot/allocate $method=$code" ;;
    esac
done

echo
echo "==== summary ===="
PASS_N=$(grep -c "^PASS" $RESULTS || true)
WARN_N=$(grep -c "^WARN" $RESULTS || true)
FAIL_N=$(grep -c "^FAIL" $RESULTS || true)
INFO_N=$(grep -c "^INFO" $RESULTS || true)
echo "pass=$PASS_N  warn=$WARN_N  fail=$FAIL_N  info=$INFO_N"
[ "$FAIL_N" = "0" ] && echo "[OK no FAIL findings]" && exit 0
echo "[FINDINGS PRESENT]"; exit 1
