#!/usr/bin/env bash
#
# verify-anchor.sh — check that one published audit anchor really was signed by Tellus.
#
#   bash verify-anchor.sh <anchor.json> <verification-key.pem>
#
# An anchor is a signed checkpoint of an audit log: "at this moment, this chain held
# this many events and ended on this hash". It is signed inside a vault by a key that
# cannot be exported, and published to write-once storage. If the signature holds, the
# checkpoint is genuine — nobody rewrote the history behind it and re-signed.
#
# You need exactly two things, and neither of them is Tellus:
#
#   * the anchor       — one JSON file, from your integrity export or the WORM bucket
#   * the public key   — https://<trust-portal>/verification-key, saved as a .pem
#
# and two standard tools: openssl and jq. Nothing here touches the network, so the
# check is worth something even if every Tellus server is lying to you.
#
# Each step prints what it did, so you can redo it by hand with openssl rather than
# trusting this script either. That is the intended way to read it.

set -euo pipefail

die() { printf 'ERROR: %s\n' "$*" >&2; exit 2; }

case ${1:-} in
    -h|--help|"") sed -n '3,21p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
esac

anchor_file=${1:?anchor json}
key_file=${2:?public key pem}

[[ -r $anchor_file ]] || die "cannot read anchor file: $anchor_file"
[[ -r $key_file ]] || die "cannot read key file: $key_file"
command -v jq >/dev/null || die "jq is not installed"
command -v openssl >/dev/null || die "openssl is not installed"

workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT

field() { jq -re --arg name "$1" '.[$name] // empty | tostring' "$anchor_file"; }

# ── 1. Take the bytes that were signed ────────────────────────────────────────
# The anchor publishes them verbatim, base64, in "signedPayload". That field exists
# so nobody has to reconstruct them: signatures are over bytes, and the readable
# fields further down are a rendering of those bytes, not a recipe for rebuilding
# them. Take the payload as the truth and read the values out of it.

signed_payload_b64=$(field signedPayload || true)

if [[ -n $signed_payload_b64 ]]; then
    printf '%s' "$signed_payload_b64" | openssl base64 -d -A > "$workdir/signed-bytes" \
        || die "signedPayload is not valid base64"
    source_note="published verbatim in the anchor"
else
    # Anchors published before the field was added. Rebuilding the bytes means
    # respelling the timestamp: Tellus signs it the way .NET writes a UTC instant in
    # "O" format — always seven decimals and a trailing Z —
    #
    #     2026-07-07T14:26:11.7626740Z
    #
    # while the JSON below carries the same instant in the shorter form its
    # serializer emits, trailing zeros dropped and UTC spelled as an offset:
    #
    #     2026-07-07T14:26:11.762674+00:00
    #
    # Same moment, different bytes. This is exactly the step the newer format
    # removes, and getting it wrong fails the signature on a perfectly good anchor.
    respell_timestamp_as_signed() {
        local value=$1 seconds fraction

        case $value in
            *+00:00) value=${value%+00:00} ;;
            *Z)      value=${value%Z} ;;
            *) die "anchoredAt '$value' is not UTC — refusing to guess how it was signed" ;;
        esac

        seconds=${value%%.*}              # 2026-07-07T14:26:11
        fraction=${value#"$seconds"}      # .762674  (empty on a whole second)
        fraction=${fraction#.}            # 762674
        [[ ${#fraction} -le 7 ]] || die "anchoredAt carries more than 7 decimals: $value"

        fraction=${fraction}0000000       # pad …
        fraction=${fraction:0:7}          # … to exactly seven

        printf '%s.%sZ' "$seconds" "$fraction"
    }

    # Five fields, each followed by one 0x1F byte (ASCII "unit separator", \037). The
    # separator is there so fields cannot be slid into one another: no value may
    # contain 0x1F, so one and only one set of five values yields these bytes.
    printf '%s\037%s\037%s\037%s\037%s\037' \
        "$(field chainKey)" \
        "$(field headEventHash)" \
        "$(field headIngestSeq)" \
        "$(field eventCount)" \
        "$(respell_timestamp_as_signed "$(field anchoredAt)")" \
        > "$workdir/signed-bytes"
    source_note="rebuilt from the readable fields (anchor predates the signedPayload field)"
fi

# ── 2. Read the checkpoint out of the signed bytes ────────────────────────────
# Not out of the readable fields — those are not what the signature covers. Splitting
# on the separator gives the five values in the order they were signed.

mapfile -t signed_fields < <(tr '\037' '\n' < "$workdir/signed-bytes")
[[ ${#signed_fields[@]} -ge 5 ]] || die "signed payload does not hold five fields"

chain_key=${signed_fields[0]}
head_event_hash=${signed_fields[1]}
head_ingest_seq=${signed_fields[2]}
event_count=${signed_fields[3]}
anchored_at=${signed_fields[4]}

# ── 3. Check the readable fields tell the same story ──────────────────────────
# A difference here means the file shows an auditor one thing while carrying a
# signature over another — worth reporting separately from a bad signature, because
# the signature can be perfectly valid while the document misrepresents it. (The
# timestamp is exempt: same instant, two spellings — see above.)

contradictions=()
for pair in "chainKey:$chain_key" "headEventHash:$head_event_hash" \
            "headIngestSeq:$head_ingest_seq" "eventCount:$event_count"; do
    name=${pair%%:*}
    signed=${pair#*:}
    rendered=$(field "$name" || true)
    [[ -z $rendered || $rendered == "$signed" ]] \
        || contradictions+=("$name: the file displays $rendered, the signature covers $signed")
done

# ── 4. Unwrap the signature ───────────────────────────────────────────────────
# Stored as hex, which decodes to text rather than to raw signature bytes:
# "vault:v1:<base64>". The prefix is deliberate — it names the key version that
# signed, which is what lets the key rotate without making old anchors unverifiable.

hex_to_text() { printf '%b' "$(printf '%s' "$1" | sed 's/../\\x&/g')"; }

signature_text=$(hex_to_text "$(field signature)")
case $signature_text in
    vault:v*:*) ;;
    *) die "signature is not in the expected vault:vN:<base64> form" ;;
esac

key_version=${signature_text#vault:v}
key_version=${key_version%%:*}
printf '%s' "${signature_text#*:*:}" | openssl base64 -d -A > "$workdir/signature.der"

# ── 5. Check the key you were handed ──────────────────────────────────────────
# This fingerprint is the one the trust portal prints beside the key. Compare them by
# eye — it ties the file on your disk to the key Tellus published, and it is the step
# that catches someone handing you a forged key with a forged anchor to match.

openssl pkey -pubin -in "$key_file" -noout 2>/dev/null || die "not a valid public key: $key_file"
fingerprint=$(openssl pkey -pubin -in "$key_file" -outform DER | openssl dgst -sha256 -r | cut -d' ' -f1)

# ── 6. Verify ─────────────────────────────────────────────────────────────────
# Plain ECDSA P-256 over SHA-256. No Tellus code takes part in the decision: openssl
# either verifies the signature against the published key, or it does not.

printf 'Anchor      %s\n' "$(field anchorId || echo '?')"
printf 'Chain       %s\n' "$chain_key"
printf 'Checkpoint  %s events, head %s…, at %s\n' "$event_count" "${head_event_hash:0:16}" "$anchored_at"
printf 'Signed with key version v%s\n' "$key_version"
printf 'Key used    %s\n' "$fingerprint"
printf '            (must match the fingerprint listed for v%s on the trust portal)\n' "$key_version"
printf 'Signed bytes %s\n\n' "$source_note"

if ! openssl dgst -sha256 -verify "$key_file" -signature "$workdir/signature.der" "$workdir/signed-bytes" >/dev/null 2>&1; then
    cat <<'FAILED'
NOT VERIFIED — the signature does not match this anchor.

  Do not read this as proof of tampering yet. In order of likelihood:

  1. Wrong key version. This anchor was signed with the version printed above; check
     that the .pem you passed is that version, and that its fingerprint matches the
     one the trust portal lists for it.
  2. The file changed in transit. Fetch the anchor again — an editor that rewrote
     line endings or re-encoded the file is enough to break the signature.
  3. Neither of those. Then the anchor is not authentic, and that is worth a phone
     call. Contact Tellus and keep the file exactly as you received it.
FAILED
    exit 1
fi

if [[ ${#contradictions[@]} -gt 0 ]]; then
    printf 'NOT VERIFIED — the anchor displays values it did not sign.\n\n'
    printf '  The signature itself is valid, so the signed content below is genuine.\n'
    printf '  But the readable fields in the file disagree with it:\n\n'
    printf '    %s\n' "${contradictions[@]}"
    cat <<'MISMATCH'

  Trust the signed content, not the display. A file that renders one thing and signs
  another is not something a correct system produces — keep it exactly as you
  received it and contact Tellus.
MISMATCH
    exit 1
fi

cat <<'VERIFIED'
VERIFIED — this checkpoint was signed by Tellus and has not been altered since.

  What that means: at the moment stamped above, the audit log really did hold that
  many events, ending on that head hash. Not even someone in full control of the
  Tellus database can produce a different history matching this signature.

  What it does not mean: it says nothing yet about the events you were handed. To
  prove those are the events behind this checkpoint, run the chain verification —
  it recomputes each event hash and must end on the head hash printed above.
VERIFIED
exit 0
