diff --git a/.machine_readable/REGISTRY.a2ml b/.machine_readable/REGISTRY.a2ml index aad0e721..2c95e67a 100644 --- a/.machine_readable/REGISTRY.a2ml +++ b/.machine_readable/REGISTRY.a2ml @@ -234,7 +234,7 @@ name = "DYADT — Did-You-Actually-Do-That" stream = "governance" home = "did-you-actually-do-that/" canonical_doc = "did-you-actually-do-that/README.adoc" -source_hash = "sha256:445359ddcc92b56dfc8e8a3bdc16062439f1236b5fd0f42099113e7afa86d2e0" +source_hash = "sha256:2ae635b9ede51e76781cb7c171108f2a4505b0aae9ac97fb05c910915141eb2a" route = "post-action agent-claim verification (Tier 4 accountability)" [[spec]] diff --git a/did-you-actually-do-that/spec/VERIFICATION-PROTOCOL.adoc b/did-you-actually-do-that/spec/VERIFICATION-PROTOCOL.adoc index d25e8d33..2786feef 100644 --- a/did-you-actually-do-that/spec/VERIFICATION-PROTOCOL.adoc +++ b/did-you-actually-do-that/spec/VERIFICATION-PROTOCOL.adoc @@ -92,6 +92,45 @@ discharged by `command-transcript`. NOT fall back to reading the agent's cited evidence. . *Stale evidence is unverifiable.* If `not_before` is set and the primary evidence predates it, the verdict is `unverifiable` (reason `stale-evidence`). + A verifier that does not collect evidence timestamps MUST return + `unverifiable` for any claim carrying `not_before` (it cannot honour the + guarantee) rather than verify possibly-stale evidence as fresh. + +== Fail-safe requirements (normative) + +Every place a verifier *cannot* collect trustworthy primary evidence is +`unverifiable`, never a confident wrong verdict. A conforming verifier MUST +return `unverifiable` (not `confirmed`/`refuted`) when: + +* the diff base ref is missing or does not resolve (`no-base-ref`) for a + `created`/`modified`/`deleted` claim; +* a required field (`claim_class`, `target`, `expect`, `verifier`) is absent + (`missing-field`), or a `[[claim]]` block carries no `id` (it MUST still + appear in the output — no silent drop); +* the `target` of a file verifier is an absolute path, contains `..`, or is a + symlink (`unsafe-path` — evidence could be redirected); +* an `expect` predicate is empty or otherwise always-matching + (`empty-pattern`), or its regex is malformed (`bad-regex`); +* a `contains:`/`sha256:` target is not a readable regular file + (`not-a-regular-file` / `unreadable`). + +And a licence/SPDX claim — detected in ANY of `claim_class`, `target`, +`expect`, or `statement` — MUST be `unverifiable` with reason `manual-only` +(the Manual-Only licence policy; never auto-`confirmed`). + +== Command execution & sandboxing (normative) + +The `command-transcript` verifier EXECUTES the claim's `target`. Consequences: + +* `stdout-contains:` MUST match the command's *stdout only*; stderr MUST be + captured separately so a marker printed to stderr cannot false-confirm. +* Because the claims file is agent-authored, executing its commands is code + execution. A verifier running over UNTRUSTED claims MUST sandbox the command + (no network, constrained filesystem, resource limits). The reference verifier + does NOT sandbox and is for TRUSTED (repo-authored, PR-reviewed) claims only; + a conforming production verifier MUST document its execution boundary. +* `claims-compose` recursion MUST be depth-bounded to defeat self- or + mutually-referential claim files (the reference verifier caps depth at 8). == Multi-verifier corroboration (SHOULD) diff --git a/did-you-actually-do-that/spec/conformance/licence-in-statement.a2ml b/did-you-actually-do-that/spec/conformance/licence-in-statement.a2ml new file mode 100644 index 00000000..544436a0 --- /dev/null +++ b/did-you-actually-do-that/spec/conformance/licence-in-statement.a2ml @@ -0,0 +1,10 @@ +[claims] +schema = "dyadt/claim@1" +actor = "conformance" +[[claim]] +id = "C1" +claim_class = "command-ran" +statement = "added the SPDX-License-Identifier header" +target = "true" +expect = "exit==0" +verifier = "command-transcript" diff --git a/did-you-actually-do-that/spec/conformance/licence-in-statement.expected b/did-you-actually-do-that/spec/conformance/licence-in-statement.expected new file mode 100644 index 00000000..450c8c22 --- /dev/null +++ b/did-you-actually-do-that/spec/conformance/licence-in-statement.expected @@ -0,0 +1 @@ +C1 unverifiable diff --git a/did-you-actually-do-that/spec/conformance/missing-field.a2ml b/did-you-actually-do-that/spec/conformance/missing-field.a2ml new file mode 100644 index 00000000..8e15dd55 --- /dev/null +++ b/did-you-actually-do-that/spec/conformance/missing-field.a2ml @@ -0,0 +1,9 @@ +[claims] +schema = "dyadt/claim@1" +actor = "conformance" +[[claim]] +id = "C1" +claim_class = "command-ran" +statement = "a claim missing its target field" +expect = "exit==0" +verifier = "command-transcript" diff --git a/did-you-actually-do-that/spec/conformance/missing-field.expected b/did-you-actually-do-that/spec/conformance/missing-field.expected new file mode 100644 index 00000000..450c8c22 --- /dev/null +++ b/did-you-actually-do-that/spec/conformance/missing-field.expected @@ -0,0 +1 @@ +C1 unverifiable diff --git a/did-you-actually-do-that/spec/conformance/unsafe-path.a2ml b/did-you-actually-do-that/spec/conformance/unsafe-path.a2ml new file mode 100644 index 00000000..11835eee --- /dev/null +++ b/did-you-actually-do-that/spec/conformance/unsafe-path.a2ml @@ -0,0 +1,10 @@ +[claims] +schema = "dyadt/claim@1" +actor = "conformance" +[[claim]] +id = "C1" +claim_class = "file-changed" +statement = "a path-traversal target" +target = "../../etc/passwd" +expect = "created" +verifier = "git-diff" diff --git a/did-you-actually-do-that/spec/conformance/unsafe-path.expected b/did-you-actually-do-that/spec/conformance/unsafe-path.expected new file mode 100644 index 00000000..450c8c22 --- /dev/null +++ b/did-you-actually-do-that/spec/conformance/unsafe-path.expected @@ -0,0 +1 @@ +C1 unverifiable diff --git a/scripts/tests/wave4-dyadt-test.sh b/scripts/tests/wave4-dyadt-test.sh index 048b687f..938438a7 100755 --- a/scripts/tests/wave4-dyadt-test.sh +++ b/scripts/tests/wave4-dyadt-test.sh @@ -107,6 +107,76 @@ verifier = "command-transcript" EOF ( cd "$ROOT" && bash "$V" "$TMP/g.a2ml" >/dev/null 2>&1 ); [ $? -eq 0 ] && ok "all-confirmed file exits 0" || bad "all-confirmed file did not exit 0" +echo "== hardening (adversarial-review fixes) ==" +mk() { printf '%s\n' "$2" > "$TMP/$1"; } +reason_of() { # file id + cd "$ROOT" && DYADT_ALLOW_UNVERIFIABLE=1 bash "$V" "$1" 2>/dev/null \ + | grep -E "$2| unverifiable, still counted +mk mf.a2ml '[claims] +[[claim]] +id = "C1" +claim_class = "command-ran" +expect = "exit==0" +verifier = "command-transcript"' +[[ "$(reason_of "$TMP/mf.a2ml" C1)" == unverifiable*missing-field ]] && ok "missing field -> unverifiable" || bad "missing field not caught" +# claim with no id is NOT silently dropped (appears as a block, unverifiable) +mk noid.a2ml '[claims] +[[claim]] +claim_class = "command-ran" +target = "true" +expect = "exit==0" +verifier = "command-transcript"' +( cd "$ROOT" && DYADT_ALLOW_UNVERIFIABLE=1 bash "$V" "$TMP/noid.a2ml" 2>/dev/null | grep -q 'no-id' ) && ok "missing id not silently dropped" || bad "missing id was dropped" +# empty pattern -> unverifiable (not an always-match confirm) +mk ep.a2ml '[claims] +[[claim]] +id = "C1" +claim_class = "file-changed" +target = "README.adoc" +expect = "contains:" +verifier = "git-diff"' +[[ "$(reason_of "$TMP/ep.a2ml" C1)" == unverifiable*empty-pattern ]] && ok "empty contains pattern -> unverifiable" || bad "empty pattern not caught" +# path traversal -> unverifiable +mk up.a2ml '[claims] +[[claim]] +id = "C1" +claim_class = "file-changed" +target = "../etc/passwd" +expect = "created" +verifier = "git-diff"' +[[ "$(reason_of "$TMP/up.a2ml" C1)" == unverifiable*unsafe-path ]] && ok "path traversal -> unverifiable" || bad "unsafe path not caught" +# unresolvable base -> unverifiable (not confident-wrong created) +mk nb.a2ml '[claims] +[[claim]] +id = "C1" +claim_class = "file-changed" +target = "README.adoc" +expect = "created" +verifier = "git-diff"' +r="$(cd "$ROOT" && DYADT_BASE=definitely-not-a-ref DYADT_ALLOW_UNVERIFIABLE=1 bash "$V" "$TMP/nb.a2ml" 2>/dev/null | grep -oE 'unverifiable *\[[^]]*\] *[a-z-]+' | head -1)" +[[ "$r" == unverifiable*no-base-ref ]] && ok "unresolvable base -> unverifiable" || bad "unresolvable base not caught (got: $r)" +# stderr marker must NOT confirm a stdout-contains claim +mk se.a2ml '[claims] +[[claim]] +id = "C1" +claim_class = "command-ran" +target = "echo marker >&2; true" +expect = "stdout-contains:marker" +verifier = "command-transcript"' +[[ "$(reason_of "$TMP/se.a2ml" C1)" == REFUTED* ]] && ok "stderr does not satisfy stdout-contains" || bad "stderr false-confirmed stdout claim" +# licence claim phrased only in the statement is still manual-only +mk lic.a2ml '[claims] +[[claim]] +id = "C1" +claim_class = "command-ran" +statement = "added the SPDX licence header" +target = "true" +expect = "exit==0" +verifier = "command-transcript"' +[[ "$(reason_of "$TMP/lic.a2ml" C1)" == unverifiable*manual-only ]] && ok "licence-in-statement -> manual-only" || bad "licence-in-statement auto-confirmed" + echo "== conformance suite ==" bash "$ROOT/did-you-actually-do-that/spec/conformance/run-conformance.sh" >/dev/null 2>&1 && ok "conformance vectors pass" || bad "conformance vectors failed" diff --git a/scripts/verify-claims.sh b/scripts/verify-claims.sh index aac0e1fc..86c8ed9e 100755 --- a/scripts/verify-claims.sh +++ b/scripts/verify-claims.sh @@ -10,10 +10,20 @@ # verdict per claim: confirmed | refuted | unverifiable. `unverifiable` is LOUD: # by default the run fails unless every claim is `confirmed`. # +# Hardened (Wave-4.1) against adversarial review: fail-SAFE everywhere — when +# trustworthy primary evidence cannot be collected, the verdict is +# `unverifiable`, never a confident wrong `confirmed`/`refuted`. Every claim +# appears in the output exactly once (no silent drops), missing required fields +# are `unverifiable`, and empty/always-matching expectations are rejected. +# +# SECURITY: the `command-transcript` verifier EXECUTES the claim's `target`. +# The claims file is trusted input (repo-authored, reviewed in PR). For +# untrusted claims a conforming verifier MUST sandbox execution; this reference +# impl does not sandbox and is for trusted claims only. +# # This reference impl handles the LOCAL verifiers (git-diff, command-transcript, # claims-compose). Network verifiers (ci-run, issue-state, pr-state) and the -# manual verifier return `unverifiable` with a reason — the production verifier -# in hyperpolymath/did-you-actually-do-that implements those against real APIs. +# manual verifier return `unverifiable` with a reason. # # Usage: verify-claims.sh [path/to/CLAIMS.a2ml] (default: ./CLAIMS.a2ml) # env DYADT_BASE git ref claims are diffed against (default: origin/main, then HEAD~1) @@ -25,46 +35,102 @@ set -uo pipefail CLAIMS="${1:-CLAIMS.a2ml}" [ -f "$CLAIMS" ] || { echo "error: claims file not found: $CLAIMS" >&2; exit 2; } +# Recursion depth guard for claims-compose (fork-bomb / cycle protection). +DYADT_DEPTH="${DYADT_DEPTH:-0}" +if [ "$DYADT_DEPTH" -gt 8 ]; then + echo "error: DYADT compose recursion too deep (>8) — possible claim cycle" >&2; exit 2 +fi + BASE="${DYADT_BASE:-}" if [ -z "$BASE" ]; then if git rev-parse --verify -q origin/main >/dev/null 2>&1; then BASE="origin/main" elif git rev-parse --verify -q HEAD~1 >/dev/null 2>&1; then BASE="HEAD~1" else BASE=""; fi fi +# A base ref that does not resolve is treated as NO base (fail safe): the +# created/modified/deleted verifiers then return `unverifiable no-base-ref` +# rather than confidently confirming against a phantom ref. +if [ -n "$BASE" ] && ! git rev-parse --verify -q "$BASE" >/dev/null 2>&1; then BASE=""; fi + +# --- helpers ---------------------------------------------------------------- + +# A target used by a file verifier must be a safe, repo-relative path: no +# absolute paths, no traversal, no symlink (symlinks could redirect evidence to +# a known-good file while the real artefact is untouched). +unsafe_path() { # path -> 0 if UNSAFE + case "$1" in + /*|../*|*/../*|*/..) return 0 ;; # absolute or traversal + esac + [ -L "$1" ] && return 0 # symlink + return 1 +} # --- primary-evidence verifiers --------------------------------------------- -# git-diff: file-changed. echoes confirmed|refuted|unverifiable + reason +# git-diff: file-changed. echoes " " v_git_diff() { # target expect - local target="$1" expect="$2" existed_now=0 existed_base=0 + local target="$1" expect="$2" existed_now=0 existed_base=0 tracked_now=0 + if unsafe_path "$target"; then echo "unverifiable unsafe-path"; return; fi + # created/modified/deleted need a resolvable base; without one we cannot tell + # "new" from "pre-existing" — fail safe, not confident-wrong. + case "$expect" in + created|modified|deleted) + [ -n "$BASE" ] || { echo "unverifiable no-base-ref"; return; } ;; + esac [ -e "$target" ] && existed_now=1 + [ "$existed_now" = 1 ] && git ls-files --error-unmatch -- "$target" >/dev/null 2>&1 && tracked_now=1 if [ -n "$BASE" ] && git cat-file -e "$BASE:$target" 2>/dev/null; then existed_base=1; fi case "$expect" in created) - { [ "$existed_now" = 1 ] && [ "$existed_base" = 0 ]; } && echo "confirmed created" || echo "refuted not-newly-created" ;; + if [ "$existed_now" = 1 ] && [ "$existed_base" = 0 ]; then + # a real "created" is a tracked file new in this change, not stray build output + [ "$tracked_now" = 1 ] && echo "confirmed created" || echo "refuted exists-but-untracked" + else echo "refuted not-newly-created"; fi ;; modified) if [ "$existed_now" = 1 ] && [ "$existed_base" = 1 ]; then - if [ -n "$BASE" ] && ! git diff --quiet "$BASE" -- "$target" 2>/dev/null; then echo "confirmed modified"; else echo "refuted unchanged"; fi + if ! git diff --quiet "$BASE" -- "$target" 2>/dev/null; then echo "confirmed modified"; else echo "refuted unchanged"; fi else echo "refuted not-modified-pair"; fi ;; deleted) { [ "$existed_now" = 0 ] && [ "$existed_base" = 1 ]; } && echo "confirmed deleted" || echo "refuted not-deleted" ;; contains:*) local re="${expect#contains:}" - if [ "$existed_now" = 1 ] && grep -Eq -- "$re" "$target" 2>/dev/null; then echo "confirmed contains"; else echo "refuted missing-pattern"; fi ;; + [ -n "$re" ] || { echo "unverifiable empty-pattern"; return; } + if [ ! -f "$target" ]; then echo "unverifiable not-a-regular-file"; return; fi + if [ ! -r "$target" ]; then echo "unverifiable unreadable"; return; fi + # distinguish "pattern absent" (refuted) from "bad regex" (unverifiable) + local gout grc + gout="$(grep -Eq -- "$re" "$target" 2>&1)"; grc=$? + if [ "$grc" -eq 0 ]; then echo "confirmed contains" + elif [ "$grc" -eq 1 ]; then echo "refuted missing-pattern" + else echo "unverifiable bad-regex"; fi ;; sha256:*) local want="${expect#sha256:}" got - if [ "$existed_now" = 1 ]; then got="$(sha256sum "$target" | cut -d' ' -f1)"; [ "$got" = "$want" ] && echo "confirmed sha256" || echo "refuted sha256-mismatch"; else echo "refuted absent"; fi ;; + [ -n "$want" ] || { echo "unverifiable empty-hash"; return; } + if [ ! -f "$target" ]; then echo "unverifiable not-a-regular-file"; return; fi + if [ ! -r "$target" ]; then echo "unverifiable unreadable"; return; fi + got="$(sha256sum "$target" 2>/dev/null | cut -d' ' -f1)" + [ "$got" = "$want" ] && echo "confirmed sha256" || echo "refuted sha256-mismatch" ;; *) echo "unverifiable bad-expect" ;; esac } -# command-transcript: run the command, judge by exit / stdout +# command-transcript: run the command, judge by exit / stdout (stdout ONLY — +# stderr is captured separately so a marker on stderr cannot false-confirm). v_command() { # target(command) expect local cmd="$1" expect="$2" out rc - out="$(bash -c "$cmd" 2>&1)"; rc=$? + [ -n "$cmd" ] || { echo "unverifiable empty-command"; return; } + local errf; errf="$(mktemp)" + out="$(bash -c "$cmd" 2>"$errf")"; rc=$? + rm -f "$errf" case "$expect" in - exit==*) [ "$rc" = "${expect#exit==}" ] && echo "confirmed exit=$rc" || echo "refuted exit=$rc" ;; - stdout-contains:*) grep -Fq -- "${expect#stdout-contains:}" <<< "$out" && echo "confirmed stdout-match" || echo "refuted stdout-nomatch" ;; + exit==*) + local want="${expect#exit==}" + case "$want" in ''|*[!0-9]*) echo "unverifiable bad-expect"; return ;; esac + [ "$rc" = "$want" ] && echo "confirmed exit=$rc" || echo "refuted exit=$rc" ;; + stdout-contains:*) + local pat="${expect#stdout-contains:}" + [ -n "$pat" ] || { echo "unverifiable empty-pattern"; return; } + grep -Fq -- "$pat" <<< "$out" && echo "confirmed stdout-match" || echo "refuted stdout-nomatch" ;; *) echo "unverifiable bad-expect" ;; esac } @@ -73,8 +139,11 @@ v_command() { # target(command) expect v_compose() { # target(path) expect local path="$1" expect="$2" [ "$expect" = "all-confirmed" ] || { echo "unverifiable bad-expect"; return; } + if unsafe_path "$path"; then echo "unverifiable unsafe-path"; return; fi [ -f "$path" ] || { echo "refuted no-such-claims"; return; } - if DYADT_ALLOW_UNVERIFIABLE=0 bash "$0" "$path" >/dev/null 2>&1; then echo "confirmed all-confirmed"; else echo "refuted child-not-all-confirmed"; fi + if DYADT_ALLOW_UNVERIFIABLE=0 DYADT_DEPTH="$((DYADT_DEPTH + 1))" bash "$0" "$path" >/dev/null 2>&1; then + echo "confirmed all-confirmed" + else echo "refuted child-not-all-confirmed"; fi } # --- dispatch ---------------------------------------------------------------- @@ -92,12 +161,21 @@ compatible() { # verifier claim_class esac } -verify_one() { # id class target expect verifier - local id="$1" class="$2" target="$3" expect="$4" verifier="$5" - # licence/SPDX claims are always manual-only (estate policy) - case "$class $target $expect" in - *[Ll]icence*|*[Ll]icense*|*SPDX*) echo "unverifiable manual-only-licence"; return ;; +verify_one() { # id class target expect verifier statement not_before + local class="$2" target="$3" expect="$4" verifier="$5" statement="$6" not_before="$7" + # Required fields — a claim missing any is unverifiable, never guessed. + if [ -z "$class" ] || [ -z "$target" ] || [ -z "$expect" ] || [ -z "$verifier" ]; then + echo "unverifiable missing-field"; return + fi + # Licence/SPDX claims are ALWAYS manual-only — scan class, target, expect AND + # statement so a licence claim phrased only in the statement cannot slip past. + case "$class $target $expect $statement" in + *[Ll]icence*|*[Ll]icense*|*SPDX*) echo "unverifiable manual-only"; return ;; esac + # not_before (stale-evidence guard): the reference verifier does not collect + # evidence timestamps, so a claim that pins freshness cannot be trusted here — + # fail safe rather than verify stale evidence as fresh. + if [ -n "$not_before" ]; then echo "unverifiable stale-evidence-unsupported"; return; fi if ! compatible "$verifier" "$class"; then echo "unverifiable incompatible-verifier"; return; fi case "$verifier" in git-diff) v_git_diff "$target" "$expect" ;; @@ -110,36 +188,51 @@ verify_one() { # id class target expect verifier } # --- parse + run ------------------------------------------------------------- -field() { sed -E "s/^$1 = \"//; s/\"$//"; } +# Whitespace-tolerant key/value extraction: accepts `key = "v"`, `key="v"`, and +# trailing inline whitespace. Sets KV_KEY and KV_VAL, or returns 1. +KV_KEY="" KV_VAL="" +kv() { # line + [[ "$1" =~ ^([A-Za-z_]+)[[:space:]]*=[[:space:]]*\"(.*)\"[[:space:]]*$ ]] || return 1 + KV_KEY="${BASH_REMATCH[1]}"; KV_VAL="${BASH_REMATCH[2]}"; return 0 +} -id="" class="" target="" expect="" verifier="" +id="" class="" target="" expect="" verifier="" statement="" not_before="" +in_claim=0 block_idx=0 n=0 confirmed=0 refuted=0 unver=0 declare -a rows=() emit() { - [ -z "$id" ] && return + [ "$in_claim" = 1 ] || return + block_idx=$((block_idx + 1)) + local cid="${id:-}" local res verdict reason - res="$(verify_one "$id" "$class" "$target" "$expect" "$verifier")" + res="$(verify_one "$cid" "$class" "$target" "$expect" "$verifier" "$statement" "$not_before")" verdict="${res%% *}"; reason="${res#* }" - n=$((n+1)) + # A block with no id is itself a defect: never confirm it, downgrade to unverifiable. + if [ -z "$id" ] && [ "$verdict" = "confirmed" ]; then verdict="unverifiable"; reason="missing-id"; fi + n=$((n + 1)) case "$verdict" in - confirmed) confirmed=$((confirmed+1)); rows+=(" ✅ $id confirmed [$class] $reason") ;; - refuted) refuted=$((refuted+1)); rows+=(" ❌ $id REFUTED [$class] $reason — statement: $statement") ;; - *) unver=$((unver+1)); rows+=(" ⚠️ $id unverifiable [$class] $reason") ;; + confirmed) confirmed=$((confirmed + 1)); rows+=(" ✅ $cid confirmed [$class] $reason") ;; + refuted) refuted=$((refuted + 1)); rows+=(" ❌ $cid REFUTED [${class:-?}] $reason — statement: $statement") ;; + *) unver=$((unver + 1)); rows+=(" ⚠️ $cid unverifiable [${class:-?}] $reason") ;; esac } -statement="" while IFS= read -r raw; do line="${raw#"${raw%%[![:space:]]*}"}" case "$line" in - '[[claim]]'*) emit; id=""; class=""; target=""; expect=""; verifier=""; statement="" ;; - 'id = "'*) id="$(printf '%s' "$line" | field id)" ;; - 'claim_class = "'*) class="$(printf '%s' "$line" | field claim_class)" ;; - 'target = "'*) target="$(printf '%s' "$line" | field target)" ;; - 'expect = "'*) expect="$(printf '%s' "$line" | field expect)" ;; - 'verifier = "'*) verifier="$(printf '%s' "$line" | field verifier)" ;; - 'statement = "'*) statement="$(printf '%s' "$line" | field statement)" ;; + '[[claim]]'*) emit; in_claim=1; id=""; class=""; target=""; expect=""; verifier=""; statement=""; not_before=""; continue ;; + esac + [ "$in_claim" = 1 ] || continue + kv "$line" || continue + case "$KV_KEY" in + id) id="$KV_VAL" ;; + claim_class) class="$KV_VAL" ;; + target) target="$KV_VAL" ;; + expect) expect="$KV_VAL" ;; + verifier) verifier="$KV_VAL" ;; + statement) statement="$KV_VAL" ;; + not_before) not_before="$KV_VAL" ;; esac done < "$CLAIMS" emit