#!/usr/bin/env bash
# Mutation tests for the offline vector checkers: every tampered copy of vectors/ must FAIL, including
# "self-consistent" forgeries where the attacker also rewrites SHA256SUMS (and dids.json) to match.
# Covers every convenience copy (op.json, dids.json, verify-response.json, test keys), the unsigned MDIP
# signature.signed field, base58 leading zeros, dotfiles/symlinks/duplicate SHA256SUMS lines, and the jq-less path.
# Also checks the clean copy passes without jq, and that a non-OpenSSL-3 binary is refused.
# Usage: bash test/vectors-negative.sh   (bash + OpenSSL 3 + node; no network; the verify-with-kit.js
# cases run only after `npm install`)
set -uo pipefail
cd "$(dirname "$0")/.."
root=$(mktemp -d "${TMPDIR:-/tmp}/mdip-neg.XXXXXX"); trap 'rm -rf "$root"' EXIT
fail=0; n=0
SH=${BASH:-bash}   # run the checker under the same bash as this script (e.g. /bin/bash 3.2 on macOS)
resum() { local d=$1 f; ( cd "$d" && for f in $(ls | LC_ALL=C sort); do [ "$f" = SHA256SUMS ] && continue
  printf '%s  %s\n' "$("${OPENSSL:-openssl}" dgst -sha256 -r "$f" | cut -d' ' -f1)" "$f"; done > "$root/sums.tmp" && mv "$root/sums.tmp" SHA256SUMS ); }
# the copy sits at $root/v next to $root/src and $root/node_modules, so verify-with-kit.js imports work unchanged
# node_modules may be hoisted (installed as a dependency): use the nearest one that has @mdip/cipher
nm=; d=$PWD; while :; do [ -d "$d/node_modules/@mdip/cipher" ] && { nm=$d/node_modules; break; }; [ "$d" = / ] && break; d=$(dirname "$d"); done
mkdir -p "$root/src"; cp src/index.js "$root/src/"; [ -n "$nm" ] && ln -s "$nm" "$root/node_modules"
fresh() { rm -rf "$root/v"; cp -R vectors "$root/v"; }
# expect_fail NAME CHECKER: CHECKER is "sh" (verify-openssl.sh), "shnojq" (same, jq hidden) or "js" (verify-with-kit.js)
havekit=0; [ -n "$nm" ] && havekit=1   # verify-with-kit.js needs `npm install`
expect_fail() { n=$((n+1)); local out
  if [ "$2" = js ] && [ $havekit = 0 ]; then echo "skip (no npm install): $1 [js]"; return; fi
  case $2 in js) out=$(cd "$root/v" && node verify-with-kit.js 2>&1);; shnojq) out=$(cd "$root/v" && PATH="$root/nojq" "$SH" verify-openssl.sh 2>&1);;
    *) out=$("$SH" "$root/v/verify-openssl.sh" 2>&1);; esac
  if [ $? -ne 0 ] && ! printf %s "$out" | grep -q '^ALL VECTORS OK$'; then echo "ok   rejected: $1 ($(printf %s "$out" | grep -m1 '^FAIL' | cut -c6-))"
  else echo "FAIL accepted: $1"; fail=1; fi; }
did_of() { node -e 'const c=require("crypto"),b=Buffer.concat([Buffer.from("0180041220","hex"),c.createHash("sha256").update(require("fs").readFileSync(process.argv[1])).digest()]);const A="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";let n=BigInt("0x"+b.toString("hex")),s="";while(n>0n){s=A[Number(n%58n)]+s;n/=58n}console.log("did:mdip:z"+s)' "$1"; }
# forge DIR OP JS: edit OP's signed op with JS (on `o`), rewrite signed-op.bin (JCS) + op.json (pretty) consistently,
# recompute OP's DID into dids.json and resum: a fully self-consistent forgery that keeps the original signature
forge() { node -e '
const fs=require("fs"),cr=require("crypto"),[d,k,ex]=process.argv.slice(1);
const sk=x=>Array.isArray(x)?x.map(sk):x&&typeof x==="object"?Object.fromEntries(Object.keys(x).sort().map(q=>[q,sk(x[q])])):x;
const o=JSON.parse(fs.readFileSync(d+"/"+k+".signed-op.bin"));new Function("o",ex)(o);const j=JSON.stringify(sk(o));
fs.writeFileSync(d+"/"+k+".signed-op.bin",j);fs.writeFileSync(d+"/"+k+".op.json",JSON.stringify(sk(o),null,2)+"\n");
const b=Buffer.concat([Buffer.from("0180041220","hex"),cr.createHash("sha256").update(j).digest()]);const A="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
let n=BigInt("0x"+b.toString("hex")),s="";while(n>0n){s=A[Number(n%58n)]+s;n/=58n}
const ds=JSON.parse(fs.readFileSync(d+"/dids.json"));ds[k]="did:mdip:z"+s;fs.writeFileSync(d+"/dids.json",JSON.stringify(sk(ds),null,2)+"\n");' "$@" && resum "$1"; }
# jedit FILE JS: edit a pretty JSON convenience file (on `o`), keep it sorted + pretty, resum its directory
jedit() { node -e '
const fs=require("fs"),[f,ex]=process.argv.slice(1);const sk=x=>Array.isArray(x)?x.map(sk):x&&typeof x==="object"?Object.fromEntries(Object.keys(x).sort().map(q=>[q,sk(x[q])])):x;
const o=JSON.parse(fs.readFileSync(f));new Function("o",ex)(o);fs.writeFileSync(f,JSON.stringify(sk(o),null,2)+"\n");' "$@" && resum "$(dirname "$1")"; }
both() { expect_fail "$1" sh; expect_fail "$1" js; }
sub() { local f=$1; shift; sed "$@" "$f" > "$f.tmp" && mv "$f.tmp" "$f"; }
C2=$root/v/mdip-challenge-2

# clean copy with jq hidden: a PATH of symlinks to everything on the current PATH except jq
mkdir -p "$root/nojq"; IFS=: read -r -a pdirs <<< "$PATH"
for pd in "${pdirs[@]}"; do for x in "$pd"/*; do b=${x##*/}; [ "$b" = jq ] || [ -e "$root/nojq/$b" ] || ln -s "$x" "$root/nojq/$b" 2>/dev/null; done; done
fresh; out=$(cd "$root/v" && PATH="$root/nojq" "$SH" verify-openssl.sh 2>&1)
[ $? = 0 ] && printf %s "$out" | grep -q '(no jq: the strict shape check above is the parse)' && printf %s "$out" | grep -q '^ALL VECTORS OK$' && echo "ok   clean copy passes without jq (strict-shape parse)" || { echo "FAIL clean copy without jq"; fail=1; }
if [ $havekit = 0 ]; then echo "skip clean copy verify-with-kit.js (no npm install)"; else
fresh; out=$(cd "$root/v" && node verify-with-kit.js 2>&1) && printf %s "$out" | grep -q '^ALL VECTORS OK$' && echo "ok   clean copy passes verify-with-kit.js" || { echo "FAIL clean copy verify-with-kit.js"; fail=1; }; fi
fresh; out=$(OPENSSL=/bin/false "$SH" "$root/v/verify-openssl.sh" 2>&1) && { echo "FAIL non-OpenSSL-3 binary accepted"; fail=1; } || echo "ok   non-OpenSSL-3 binary refused"

fresh; printf 'x' >> "$C2/challenge.message.bin"; expect_fail "challenge.message.bin changed, SHA256SUMS not updated" sh
fresh; : > "$root/v/kav-1/extra.bin"; expect_fail "unlisted extra file" sh
C1=$root/v/mdip-challenge-1 K=$root/v/kav-1
fresh; forge "$C2" challenge 'o.mdip.validUntil="2026-10-27T17:52:16.038Z"'
  both "forged challenge signed-op (later expiry) + op.json + recomputed DID + resummed"
fresh; forge "$C2" response 'o.mdip.validUntil="2099-01-01T00:00:00.000Z"'
  both "forged response signed-op (2099 expiry) + op.json + recomputed DID + resummed [accepted by d93162d]"
fresh; forge "$C2" response 'o.signature.signed="2099-01-01T00:00:00.000Z"'
  both "response signature.signed (unsigned by MDIP) moved to 2099 + recomputed DID [accepted by c41252f]"
  expect_fail "response signature.signed moved to 2099 + recomputed DID" shnojq
fresh; forge "$C1" response 'o.signature.signed="2099-01-01T00:00:00.000Z"'
  both "mdip-challenge-1 response signature.signed moved to 2099 + recomputed DID [accepted by c41252f]"
fresh; forge "$C2" response 'o.signature.signed="2026-10-26T16:52:16.080"'
  both "response signature.signed without a zone + recomputed DID"
fresh; forge "$C2" response 'o.signature.note="relayed"'
  both "extra key inside the response signature block + recomputed DID [accepted by c41252f]"
fresh; forge "$C2" verifier-agent 'o.signature.signer="did:mdip:z3v8AuabH5z2RyBFCP8KzhS98jSL8Y62JPfis8EJiBo6io6QoRA"'
  both "signer added to a self-signed agent create + recomputed DID"
fresh; sub "$C2/response.plaintext.json" 's/"challenge":"did:mdip:z3v8AuaUJb/"challenge":"did:mdip:z3v8AuaUJc/'; resum "$C2"
  expect_fail "response.plaintext.json names another challenge (resummed)" sh
fresh; sub "$C2/dids.json" 's/"challenge": "did:mdip:z3v8AuaUJbQT6/"challenge": "did:mdip:z3v8AuaUJbQT7/'; resum "$C2"
  expect_fail "dids.json challenge DID changed (resummed)" sh
fresh; cp "$C2/verifier-agent.pub.pem" "$C2/responder-agent.pub.pem"; resum "$C2"
  expect_fail "responder pub.pem replaced by verifier key (resummed)" sh
fresh; cp "$C2/challenge.sig.der" "$C2/response.sig.der"; resum "$C2"
  expect_fail "response.sig.der swapped for challenge.sig.der (resummed)" sh
fresh; printf '\377' | dd of="$C2/response.cipher_receiver.bin" bs=1 seek=40 conv=notrunc 2>/dev/null; resum "$C2"
  expect_fail "ciphertext byte flipped (resummed)" sh
fresh; cp "$root/v/mdip-challenge-1/verifier-agent.pub.pem" "$root/v/kav-1/pub.pem"; resum "$root/v/kav-1"
  expect_fail "kav-1 pub.pem replaced (resummed)" sh
fresh; sub "$root/v/kav-1/signature.hex" 's/^2b/2c/'; resum "$root/v/kav-1"
  expect_fail "kav-1 signature.hex no longer matches signature.der (resummed)" sh
fresh; sub "$root/v/expiry-cases.txt" 's/| reject | 2 h + 1 ms later/| accept | 2 h + 1 ms later/'
  expect_fail "expiry table: 2 h + 1 ms later marked accept" sh
  expect_fail "expiry table: 2 h + 1 ms later marked accept" js
fresh; sub "$root/v/expiry-cases.txt" 's/| reject | 2099 (forged far future)/| accept | 2099 (forged far future)/'
  expect_fail "expiry table: 2099 response marked accept" sh
  expect_fail "expiry table: 2099 response marked accept" js
fresh; sub "$root/v/expiry-cases.txt" 's/| accept | stock keymaster skew/| reject | stock keymaster skew/'
  expect_fail "expiry table: stock keymaster skew marked reject" sh
  expect_fail "expiry table: stock keymaster skew marked reject" js
fresh; sub "$root/v/expiry-cases.txt" 's/| reject | leap second/| accept | leap second/'
  expect_fail "expiry table: leap second marked accept" sh
  expect_fail "expiry table: leap second marked accept" js
fresh; sub "$C2/challenge.op.json" 's/"validUntil": "2026-10-26T17:52:16.038Z"/"validUntil": "2026-10-27T17:52:16.038Z"/'
  expect_fail "challenge.op.json expiry changed" js
fresh; node -e 'const f=process.argv[1],o=JSON.parse(require("fs").readFileSync(f));o.signature.signer=o.controller="did:mdip:z3v8AuafFPtizoWoRCuV71sNVen1MUCrFuvfFNjhrYmasfnEFy6";require("fs").writeFileSync(f,JSON.stringify(o,null,2)+"\n")' "$C2/challenge.op.json"
  expect_fail "challenge.op.json controller/signer swapped to responder" js

fresh; sub "$C2/dids.json" 's/"response": "did:mdip:z/"response": "did:mdip:z1/'; resum "$C2"
  both "response DID with a leading base58 1 (extra 0x00 byte) [accepted by c41252f's verify-openssl.sh]"
fresh; sub "$C1/dids.json" 's/"challenge": "did:mdip:z/"challenge": "did:mdip:z1/'; resum "$C1"
  both "mdip-challenge-1 challenge DID with a leading base58 1"
fresh; jedit "$C1/dids.json" '[o.challenge,o.response]=[o.response,o.challenge]'
  both "mdip-challenge-1 dids.json challenge/response swapped [accepted by c41252f's verify-openssl.sh]"
fresh; jedit "$C1/response.op.json" 'o.created="2026-10-26T15:19:54.000Z"'
  both "mdip-challenge-1 response.op.json edited (unsigned copy) [accepted by c41252f's verify-openssl.sh]"
fresh; jedit "$C2/challenge.op.json" 'o.mdip.validUntil="2099-01-01T00:00:00.000Z"'
  both "mdip-challenge-2 challenge.op.json edited (unsigned copy)"
fresh; jedit "$C1/verify-response.json" 'o.match=false'
  both "mdip-challenge-1 verify-response.json match=false [accepted by c41252f's verify-openssl.sh]"
fresh; jedit "$C2/verify-response.json" 'o.extra=1'
  both "mdip-challenge-2 verify-response.json extra field"
fresh; jedit "$C2/verify-response.json" 'o.responseNonce="x"+o.responseNonce.slice(1)'
  both "mdip-challenge-2 verify-response.json nonce changed"
fresh; jedit "$C2/test-verifier.json" 'o.privateJwk.d="A"+o.privateJwk.d.slice(1)'
  both "test-verifier.json private key changed [accepted by c41252f's verify-openssl.sh]"
fresh; jedit "$C2/test-verifier.json" 'const w=o.mnemonic.split(" ");[w[0],w[1]]=[w[1],w[0]];o.mnemonic=w.join(" ")'
  expect_fail "test-verifier.json mnemonic words swapped (BIP32 check is verify-with-kit.js only)" js
fresh; jedit "$C2/test-verifier.json" 'o.did=o.did.replace("z3v8Auab","z3v8Auac")'
  both "test-verifier.json DID changed"
fresh; jedit "$K/signed.json" 'o.message="Known-answer vector, edited."'
  both "kav-1 signed.json message edited (unsigned copy) [accepted by c41252f's verify-openssl.sh]"
fresh; jedit "$K/signed.json" 'o.signature.extra=1'
  both "kav-1 signed.json extra key in signature block"
fresh; jedit "$K/payload.json" 'o.nonce="00"'
  both "kav-1 payload.json edited [accepted by c41252f's verify-openssl.sh]"
fresh; jedit "$K/test-key.json" 'o.privateKeyHex="00"+o.privateKeyHex.slice(2)'
  both "kav-1 test-key.json private key changed [accepted by c41252f's verify-openssl.sh]"
fresh; jedit "$K/test-key.json" 'o.label+="!"'
  both "kav-1 test-key.json label no longer derives the key"
fresh; : > "$K/.hidden"; expect_fail "unlisted dotfile [accepted by c41252f]" sh
fresh; mv "$K/pub.pem" "$root/pub.pem"; ln -s "$root/pub.pem" "$K/pub.pem"; expect_fail "listed file replaced by a symlink (same bytes) [accepted by c41252f]" sh
fresh; tail -1 "$K/SHA256SUMS" >> "$K/SHA256SUMS"; expect_fail "SHA256SUMS lists a file twice" sh
fresh; sub "$C2/response.plaintext.json" 's/{"response":{/{"response":{"challenge":"did:mdip:zX",/'; resum "$C2"
  both "response.plaintext.json with a duplicate challenge key (jq/sed parse differential)"
  expect_fail "response.plaintext.json with a duplicate challenge key" shnojq

# --- 0.1.5 crypto/binding review: forgeries that a pre-review checker accepted ---
# high-S twin (r, n-s): OpenSSL verifies it, MDIP/noble does not
highs() { node -e 'const fs=require("fs"),f=process.argv[1],d=fs.readFileSync(f),n=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;let i=2;const rd=()=>{i++;const l=d[i++];const v=d.subarray(i,i+l);i+=l;return BigInt("0x"+v.toString("hex"))};const r=rd(),s=n-rd();const e=x=>{let h=x.toString(16);if(h.length%2)h="0"+h;let b=Buffer.from(h,"hex");if(b[0]&128)b=Buffer.concat([Buffer.from([0]),b]);return Buffer.concat([Buffer.from([2,b.length]),b])};const body=Buffer.concat([e(r),e(s)]);fs.writeFileSync(f,Buffer.concat([Buffer.from([48,body.length]),body]));console.log(r.toString(16).padStart(64,"0")+s.toString(16).padStart(64,"0"))' "$1"; }
fresh; highs "$root/v/kav-1/signature.der" > "$root/v/kav-1/signature.hex"; resum "$root/v/kav-1"
  expect_fail "kav-1 high-S twin signature (der + hex, resummed) [accepted by c41252f]" sh
fresh; highs "$root/v/mdip-challenge-1/response.sig.der" >/dev/null; resum "$root/v/mdip-challenge-1"
  expect_fail "mdip-challenge-1 response.sig.der high-S twin (resummed) [accepted by c41252f]" sh
  expect_fail "mdip-challenge-1 response.sig.der high-S twin (resummed) [accepted by c41252f]" js
fresh; highs "$C2/response.sig.der" >/dev/null; resum "$C2"
  expect_fail "mdip-challenge-2 response.sig.der high-S twin (resummed) [js accepted at c41252f]" js
# SHA256SUMS whose last line has no newline: the old loop never hash-checked that entry
fresh; f=$root/v/mdip-challenge-1/SHA256SUMS; printf 'x' >> "$root/v/mdip-challenge-1/verify-response.json"
  { grep -v ' verify-response.json$' "$f"; printf '%064d  verify-response.json' 0; } > "$f.tmp" && mv "$f.tmp" "$f"
  expect_fail "SHA256SUMS last line unterminated, wrong hash for a changed file [accepted by c41252f]" sh
fresh; ln -s ../kav-1/pub.pem "$root/v/mdip-challenge-1/extra.pem"; resum "$root/v/mdip-challenge-1"
  expect_fail "symlink inside a vector dir (resummed) [accepted by c41252f]" sh
fresh; sub "$root/v/kav-1/SHA256SUMS" 's/  message.bin$/  .\/message.bin/'; expect_fail "SHA256SUMS entry with a path, not a plain name" sh
# kav-1 re-pointed at the OTHER public key that also verifies (r,s) (ECDSA key recovery, recovery bit flipped)
if [ $havekit = 1 ]; then fresh; node --input-type=module -e '
import fs from "node:fs"; import crypto from "node:crypto"; import { secp256k1 } from "@noble/curves/secp256k1.js";
const [d] = process.argv.slice(1), pem = d + "/pub.pem", h = crypto.createHash("sha256").update(fs.readFileSync(d + "/message.bin")).digest();
const orig = crypto.createPublicKey(fs.readFileSync(pem, "utf8")).export({ format: "jwk" });
const b = x => Buffer.from(x.toString(16).padStart(64, "0"), "hex").toString("base64url");
for (const bit of [0, 1]) { const a = secp256k1.Signature.fromBytes(Buffer.from(fs.readFileSync(d + "/signature.hex", "utf8").trim(), "hex"), "compact").addRecoveryBit(bit).recoverPublicKey(h).toAffine();
  if (b(a.x) !== orig.x) { fs.writeFileSync(pem, crypto.createPublicKey({ key: { kty: "EC", crv: "secp256k1", x: b(a.x), y: b(a.y) }, format: "jwk" }).export({ type: "spki", format: "pem" })); break; } }' "$root/v/kav-1"
  openssl dgst -sha256 -verify "$root/v/kav-1/pub.pem" -signature "$root/v/kav-1/signature.der" "$root/v/kav-1/message.bin" >/dev/null 2>&1 || { echo "FAIL recovered-key setup"; fail=1; }
  resum "$root/v/kav-1"
  expect_fail "kav-1 pub.pem = the other recovered key that also verifies the signature (resummed) [accepted by c41252f]" sh
  expect_fail "kav-1 pub.pem = the other recovered key that also verifies the signature (resummed) [accepted by c41252f]" js; fi
# kav-1 regenerated wholesale with a fresh key (message.bin unchanged, new pub.pem/sig.der/sig.hex, resummed)
fresh; k=$root/k.pem; openssl ecparam -name secp256k1 -genkey -noout -out "$k" 2>/dev/null
  openssl pkey -in "$k" -pubout -out "$root/v/kav-1/pub.pem"; openssl dgst -sha256 -sign "$k" -out "$root/v/kav-1/signature.der" "$root/v/kav-1/message.bin"
  openssl asn1parse -inform DER -in "$root/v/kav-1/signature.der" | sed -n 's/.*INTEGER *://p' | while read -r i; do printf %064s "$i" | tr ' A-F' '0a-f'; done > "$root/v/kav-1/signature.hex"; echo >> "$root/v/kav-1/signature.hex"; resum "$root/v/kav-1"
  expect_fail "kav-1 re-signed with a fresh key (pub.pem, sig.der, hex, resummed) [accepted by c41252f]" sh
# mdip-challenge-1 has no public key, so a swapped dids.json was only caught via the controller fields
fresh; sub "$root/v/mdip-challenge-1/dids.json" 's/"response": "did:mdip:z3v8AuaXWLR/"response": "did:mdip:z3v8AuaXWLS/'; resum "$root/v/mdip-challenge-1"
  expect_fail "mdip-challenge-1 dids.json response DID changed (resummed) [accepted by c41252f]" sh
  expect_fail "mdip-challenge-1 dids.json response DID changed (resummed) [accepted by c41252f]" js
fresh; printf '\n\n' >> "$C2/response.plaintext.json"; resum "$C2"
  expect_fail "response.plaintext.json with extra trailing newlines (resummed) [sh accepted at c41252f]" sh
fresh; sub "$root/v/kav-1/message-tampered.bin" 's/Known-answer/Unknown-answer/; s/Non-emptY/Non-empty/'; resum "$root/v/kav-1"
  expect_fail "message-tampered.bin is not a one-byte change (resummed) [accepted by c41252f]" sh
  expect_fail "message-tampered.bin is not a one-byte change [accepted by c41252f]" js
fresh; node -e 'const f=process.argv[1],o=JSON.parse(require("fs").readFileSync(f));o.privateJwk.d="AAAA"+o.privateJwk.d.slice(4);require("fs").writeFileSync(f,JSON.stringify(o,null,2)+"\n")' "$C2/test-verifier.json"
  expect_fail "test-verifier.json privateJwk.d altered" js
fresh; sub "$root/v/mdip-challenge-1/response.op.json" 's/"signed": "2026/"signed": "2027/'
  expect_fail "mdip-challenge-1 response.op.json signature.signed changed [accepted by c41252f]" js


# ---- reviewer A (0.1.5): every human-readable file must equal what the signed / derived bytes say. Each
# forgery below keeps the generator layout (sorted keys, 2-space indent) and is resummed, so only the new
# exact-content checks can catch it. [accepted by 29b81bb] = both checkers passed it at 29b81bb; [sh/js ...] = that one did;
# untagged = already rejected at 29b81bb (kept so a regression is caught).
# jmut FILE JS: edit parsed JSON `o` in place, rewrite in generator layout, resum the dir
jmut() { node -e 'const fs=require("fs"),f=process.argv[1];let o=JSON.parse(fs.readFileSync(f,"utf8"));eval(process.argv[2]);const S=v=>Array.isArray(v)?v.map(S):v&&typeof v==="object"?Object.fromEntries(Object.keys(v).sort().map(k=>[k,S(v[k])])):v;fs.writeFileSync(f,JSON.stringify(S(o),null,2)+"\n")' "$1" "$2" && resum "$(dirname "$1")"; }
fresh; jmut "$C2/response.op.json" 'o.mdip.validUntil="2099-01-01T00:00:00.000Z"'; both "mdip-challenge-2 response.op.json validUntil 2099 (resummed)"
fresh; jmut "$C1/challenge.op.json" 'o.mdip.validUntil="2099-01-01T00:00:00.000Z"'; both "mdip-challenge-1 challenge.op.json validUntil 2099 (resummed)"
fresh; jmut "$C1/response.op.json" 'o.signature.signer=o.controller.replace(/.$/,"x")'; both "mdip-challenge-1 response.op.json signature.signer changed (resummed)"
fresh; jmut "$C2/challenge.op.json" 'o.extra=1'; both "mdip-challenge-2 challenge.op.json extra member (resummed)"
fresh; jmut "$C1/verify-response.json" 'o.match=false'; both "mdip-challenge-1 verify-response.json match=false (resummed)"
fresh; jmut "$C2/verify-response.json" 'o.match=false'; both "mdip-challenge-2 verify-response.json match=false (resummed)"
fresh; jmut "$C2/verify-response.json" 'o.responseNonce="x"+o.responseNonce.slice(1)'; both "mdip-challenge-2 verify-response.json responseNonce != plaintext (resummed)"
fresh; jmut "$C2/verify-response.json" 'o.responder=o.challenge'; both "mdip-challenge-2 verify-response.json responder changed (resummed)"
fresh; jmut "$C1/dids.json" 'o.extra="did:mdip:z1"'; both "mdip-challenge-1 dids.json extra member (resummed)"
fresh; jmut "$C2/test-verifier.json" 'o.privateJwk.d="AAAA"+o.privateJwk.d.slice(4)'; both "test-verifier.json privateJwk.d altered, layout kept (resummed)"
fresh; jmut "$C2/test-verifier.json" 'o.mnemonic=o.mnemonic.replace("two ","ten ")'; both "test-verifier.json mnemonic changed (resummed) [sh accepted by 29b81bb]"
fresh; jmut "$C2/test-verifier.json" 'o.derivationPath="m/44'"'"'/0'"'"'/0'"'"'/0/1"'; both "test-verifier.json derivationPath changed (resummed)"
fresh; jmut "$K/signed.json" 'o.signature.signer="did:example:mallory"'; both "kav-1 signed.json signature.signer changed (resummed)"
fresh; jmut "$K/signed.json" 'o.signature.signed="2027-01-01T00:00:00.000Z"'; both "kav-1 signed.json signature.signed changed (resummed) [accepted by 29b81bb]"
fresh; jmut "$K/signed.json" 'o.message="pay 1000"'; both "kav-1 signed.json message changed (resummed)"
fresh; jmut "$K/payload.json" 'o.message="pay 1000"'; both "kav-1 payload.json message changed (resummed)"
fresh; jmut "$K/test-key.json" 'o.privateKeyHex="00"+o.privateKeyHex.slice(2)'; both "kav-1 test-key.json privateKeyHex changed (resummed)"
fresh; jmut "$K/test-key.json" 'o.WARNING="safe to use"'; both "kav-1 test-key.json WARNING changed (resummed) [accepted by 29b81bb]"
fresh; jmut "$C2/test-verifier.json" 'o.WARNING="safe to use"'; both "test-verifier.json WARNING changed (resummed) [accepted by 29b81bb]"
fresh; printf '\n' >> "$K/signature.hex"; resum "$K"; both "kav-1 signature.hex with an extra newline (resummed) [accepted by 29b81bb]"
fresh; printf '\n' >> "$K/message.sha256"; resum "$K"; both "kav-1 message.sha256 with an extra newline (resummed) [accepted by 29b81bb]"
fresh; printf 'junk\n' >> "$C1/responder-agent.pub.pem"; resum "$C1"; both "pub.pem with trailing junk (resummed) [accepted by 29b81bb]"
fresh; printf 'junk\n' >> "$C2/verifier-agent.PUBLIC-TEST-KEY.priv.pem"; resum "$C2"; both "PUBLIC-TEST-KEY.priv.pem with trailing junk (resummed) [accepted by 29b81bb]"
fresh; node -e 'const fs=require("fs"),f=process.argv[1],o=JSON.parse(fs.readFileSync(f,"utf8"));fs.writeFileSync(f,JSON.stringify(o,Object.keys(o).reverse(),2)+"\n")' "$C1/dids.json"; resum "$C1"
  both "dids.json same members, keys not in JCS order (resummed) [js accepted by 29b81bb]"
fresh; sub "$C2/verify-response.json" 's/^}$/,"vps":[]}/'; resum "$C2"; both "verify-response.json duplicate key appended (resummed) [js accepted by 29b81bb]"
# verify-with-kit.js now enforces SHA256SUMS itself (it never read it before)
fresh; : > "$C2/.hidden"; expect_fail "unlisted dotfile [js accepted by 29b81bb]" js
fresh; printf 'x' >> "$C1/challenge.message.bin"; expect_fail "file changed, SHA256SUMS not updated" js
fresh; sed -i.bak '$d' "$K/SHA256SUMS"; rm -f "$K/SHA256SUMS.bak"; expect_fail "SHA256SUMS missing an entry [js accepted by 29b81bb]" js
fresh; tail -1 "$K/SHA256SUMS" >> "$K/SHA256SUMS"; expect_fail "SHA256SUMS duplicate entry [js accepted by 29b81bb]" js
echo "$n mutations"
[ $fail = 0 ] && echo "ALL MUTATIONS REJECTED" || { echo "MUTATION TEST FAILED"; exit 1; }
