#!/usr/bin/env bash
# Install-free check of the known-answer vectors: needs only bash, sha256sum (coreutils) and OpenSSL >= 1.1.1
# (secp256k1 must be available; it is in stock OpenSSL builds). No npm, no network.
# Exit 0 and last line "ALL VECTORS OK" on success; non-zero on any failure.
set -uo pipefail
cd "$(dirname "$0")"
fail=0
ok()  { echo "ok   $*"; }
bad() { echo "FAIL $*"; fail=1; }
v() { openssl dgst -sha256 -verify "$1" -signature "$2" "$3" >/dev/null 2>&1; }

echo "== kav-1: synthetic secp256k1 / sha256(JCS) vector (public test key)"
( cd kav-1 && sha256sum -c --quiet SHA256SUMS ) && ok "kav-1 SHA256SUMS" || bad "kav-1 SHA256SUMS"
[ "$(openssl dgst -sha256 -r kav-1/message.bin | cut -d' ' -f1)" = "$(cat kav-1/message.sha256)" ] && ok "sha256(message.bin) == message.sha256" || bad "message hash"
v kav-1/pub.pem kav-1/signature.der kav-1/message.bin && ok "signature verifies" || bad "signature"
v kav-1/pub.pem kav-1/signature.der kav-1/message-tampered.bin && bad "tampered message verified (must not)" || ok "tampered message rejected"

echo "== mdip-challenge-1: real MDIP create operations (challenge -> response)"
( cd mdip-challenge-1 && sha256sum -c --quiet SHA256SUMS ) && ok "mdip-challenge-1 SHA256SUMS" || bad "mdip-challenge-1 SHA256SUMS"
for k in verifier-agent:verifier-agent challenge:verifier-agent responder-agent:responder-agent response:responder-agent; do
  op=${k%%:*}; key=${k##*:}
  v "mdip-challenge-1/$key.pub.pem" "mdip-challenge-1/$op.sig.der" "mdip-challenge-1/$op.message.bin" \
    && ok "$op op signed by $key key" || bad "$op op signature"
done
v mdip-challenge-1/verifier-agent.pub.pem mdip-challenge-1/response.sig.der mdip-challenge-1/response.message.bin \
  && bad "response verified under verifier key (must not)" || ok "response NOT valid under verifier key"

echo "== mdip-challenge-2: offline challenge <-> response binding (PUBLIC TEST verifier key; thanks @homies)"
d=mdip-challenge-2
( cd $d && sha256sum -c --quiet SHA256SUMS ) && ok "mdip-challenge-2 SHA256SUMS" || bad "mdip-challenge-2 SHA256SUMS"
for k in verifier-agent:verifier-agent challenge:verifier-agent responder-agent:responder-agent response:responder-agent; do
  op=${k%%:*}; key=${k##*:}
  v "$d/$key.pub.pem" "$d/$op.sig.der" "$d/$op.message.bin" && ok "$op op signed by $key key" || bad "$op op signature"
done
# 1. each DID string IS the hash of its signed create op: did:mdip:z<base58btc(01 8004 1220 || sha256(JCS(op)))>
b58hex() { local A=123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz s=$1 b=() i j c x; local -i carry
  for ((i=0;i<${#s};i++)); do x=${A%%"${s:i:1}"*}; carry=${#x}
    for ((j=${#b[@]}-1;j>=0;j--)); do carry+=$((b[j]*58)); b[j]=$((carry&255)); carry=$((carry>>8)); done
    while ((carry)); do b=($((carry&255)) "${b[@]}"); carry=$((carry>>8)); done; done
  for c in "${b[@]}"; do printf %02x "$c"; done; }
did() { sed -n "s/.*\"$1\": \"\\(did:mdip:[^\"]*\\)\".*/\\1/p" $d/dids.json; }
jget() { sed -n "s/.*\"$1\":\"\\([^\"]*\\)\".*/\\1/p" "$2" | head -1; }
for k in verifier-agent challenge responder-agent response; do
  sfx=$(did $k); sfx=${sfx#did:mdip:z}
  [ "$(b58hex "$sfx")" = "0180041220$(openssl dgst -sha256 -r $d/$k.signed-op.bin | cut -d' ' -f1)" ] \
    && ok "$k DID == CID(sha256(signed-op.bin))" || bad "$k DID does not match its op"
done
# 2. the private test key really is the verifier agent's key
[ "$(openssl pkey -in $d/verifier-agent.PUBLIC-TEST-KEY.priv.pem -pubout 2>/dev/null)" = "$(cat $d/verifier-agent.pub.pem)" ] \
  && ok "published private test key matches verifier-agent.pub.pem" || bad "test key mismatch"
# 3. controllers: challenge by verifier, response by responder (fields read from the signed bytes)
[ "$(jget controller $d/challenge.message.bin)" = "$(did verifier-agent)" ] && ok "challenge controller == verifier DID" || bad "challenge controller"
[ "$(jget controller $d/response.message.bin)" = "$(did responder-agent)" ] && [ "$(jget sender $d/response.message.bin)" = "$(did responder-agent)" ] \
  && ok "response controller == encrypted.sender == responder DID" || bad "response controller/sender"
# 4. decrypt the signed response ciphertext with the PUBLIC TEST verifier key (ECDH + XChaCha20-Poly1305)
[ "$(jget cipher_receiver $d/response.message.bin)" = "$(openssl base64 -A < $d/response.cipher_receiver.bin | tr '+/' '-_' | tr -d '=')" ] \
  && ok "response.cipher_receiver.bin == cipher_receiver in the signed response op" || bad "ciphertext not from signed op"
pt=$(bash decrypt-openssl.sh $d/verifier-agent.PUBLIC-TEST-KEY.priv.pem $d/responder-agent.pub.pem $d/response.cipher_receiver.bin) \
  && ok "response decrypts, Poly1305 tag OK" || bad "response decrypt"
[ "$pt" = "$(cat $d/response.plaintext.json)" ] && ok "decrypted bytes == response.plaintext.json" || bad "plaintext mismatch"
# 5. THE BINDING: the signed+encrypted response names this challenge DID
# parse as JSON (key order independent) when jq exists; the anchored sed is only a no-jq fallback
if command -v jq >/dev/null; then bound=$(printf %s "$pt" | jq -r '.response.challenge // empty'); else
  echo "  (no jq: falling back to anchored sed; install jq for an encoder-independent parse)"
  bound=$(printf %s "$pt" | sed -n 's/^{"response":{"challenge":"\(did:mdip:[^"]*\)".*/\1/p'); fi
[ -n "$bound" ] && [ "$bound" = "$(did challenge)" ] \
  && ok "BINDING: decrypted response.challenge == challenge DID" || bad "BINDING: response does not answer this challenge"
printf %s "$pt" | grep -q '"match":true' && printf %s "$pt" | grep -q '"responseNonce":"[^"]\+"' && ok "response match=true, has responseNonce" || bad "response fields"
# negative: one flipped ciphertext byte must fail the tag
tmpc=$(mktemp); cp $d/response.cipher_receiver.bin "$tmpc"; printf '\xff' | dd of="$tmpc" bs=1 seek=30 conv=notrunc 2>/dev/null
bash decrypt-openssl.sh $d/verifier-agent.PUBLIC-TEST-KEY.priv.pem $d/responder-agent.pub.pem "$tmpc" >/dev/null 2>&1 \
  && bad "tampered ciphertext decrypted (must not)" || ok "tampered ciphertext rejected (tag)"; rm -f "$tmpc"

echo "== strict expiry: response.validUntil <= challenge.validUntil (equal allowed, 1 ms later rejected)"
# compare as epoch ms, not strings: accepts with/without fractional seconds; a timestamp with no Z/offset is rejected (fail closed)
tms() { [[ "$1" =~ (Z|[+-][0-9]{2}:?[0-9]{2})$ ]] || return 1; date -u -d "$1" +%s%3N 2>/dev/null; }
expiry_ok() { [ "$1" = - ] && return 0; [ "$2" = - ] && return 1; local c r; c=$(tms "$1") && r=$(tms "$2") || return 1; [ "$r" -le "$c" ]; }
for dd in mdip-challenge-1 mdip-challenge-2; do
  c=$(jget validUntil $dd/challenge.message.bin); r=$(jget validUntil $dd/response.message.bin)
  expiry_ok "$c" "$r" && ok "$dd response $r <= challenge $c$([ "$r" = "$c" ] && echo ' (equal: allowed)')" || bad "$dd expiry"
done
while read -r c r want note; do
  case "$c" in ''|\#*) continue;; esac
  expiry_ok "$c" "$r" && got=accept || got=reject
  [ "$got" = "$want" ] && ok "expiry case: $note -> $got" || bad "expiry case: $note -> $got (want $want)"
done < expiry-cases.txt

[ $fail = 0 ] && echo "ALL VECTORS OK" || { echo "VECTORS FAILED"; exit 1; }
