Three records, one of them needs us

Verify

Delegation tokens verify offline with a pinned service key. Exported envelopes and checkpoints verify on either side with the signer's public key. The settlement chain verifies against the hosted ledger. Each check below names what it does not prove.

1. Delegation tokens, offline

A token from POST /v1/delegations carries chain, chain_pubkey, binding, payload, signature and scheme. Fetch the service key once, pin it, and run the two checks below with the network off. A reordered chain fails the first check; an edited payload fails the second.

curl -sS "https://api.afaprotocol.com/v1/delegations/service-pubkey" -H "X-API-Key: afa-beta-EXAMPLE-e4qs"
import base64
import json

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey


def b64url_decode(value: str) -> bytes:
    return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))


def canonical(payload) -> bytes:
    return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")


def verify_delegation_offline(token: dict, service_pubkey_b64: str) -> dict:
    """Two checks, both required. Needs the token and the pinned service key only."""
    service_pub = Ed25519PublicKey.from_public_bytes(b64url_decode(service_pubkey_b64))
    chain_pub = Ed25519PublicKey.from_public_bytes(b64url_decode(token["chain_pubkey"]))
    binding = b64url_decode(token["binding"].split(":", 1)[1])
    signature = b64url_decode(token["signature"].split(":", 1)[1])

    # 1. Binding: the service attested THIS public key to THIS chain, in this order.
    binding_msg = canonical({"chain": list(token["chain"]),
                             "chain_pubkey": token["chain_pubkey"],
                             "scheme": token["scheme"]})
    try:
        service_pub.verify(binding, binding_msg)
    except InvalidSignature:
        return {"valid": False, "reason": "chain_order_mismatch_or_binding_invalid"}

    # 2. Payload: the token signature verifies under the order-bound key.
    try:
        chain_pub.verify(signature, canonical(token["payload"]))
    except InvalidSignature:
        return {"valid": False, "reason": "payload_tampered"}
    return {"valid": True, "reason": "ok"}


# service_pubkey_b64 comes from GET /v1/delegations/service-pubkey, fetched once and pinned.
# Swap two names in token["chain"] and the binding check fails: order is part of what was signed.

Check: run the function on the same token with the network off and with it on; the answers match, and POST /v1/delegations/verify gives the same reason. A changed service key means a changed service identity and voids the pin.

Not proved: that the delegation should have happened, or that the service's key custody is sound.

2. Exported envelopes and checkpoints, either side

POST /v1/verify/envelope takes an exported intent envelope and the signer's PEM public key; POST /v1/verify/checkpoint takes a human-pause checkpoint and the responder's raw public key. Both are stateless: nothing is stored or read. The same arithmetic runs offline, so run it on your side and compare the reasons.

CheckWhat passes itWhat it does not prove
envelope signatureCanonical JSON without signature and signer_id, scope sorted, Ed25519.That the plan was wise.
checkpoint signatureCanonical JSON without signature, Ed25519 under the responder key.That the human read carefully.
responder bindingresponder_id equals b64url of the first 16 bytes of sha256(public key).Who the person was.
satisfiesenvelope_id and operator_index both match the named pause.That approval was appropriate.

3. The settlement chain, against the hosted ledger

POST /v1/chain/checkpoint folds unsettled events into a block whose merkle root is the SHA-256 over the sorted payload hashes, each followed by a newline, and whose prev_block_hash links it to the previous block. POST /v1/chain/verify recomputes every block hash and every link and names the first block that fails.

import hashlib

def merkle_root_over(payload_hashes):
    if not payload_hashes:
        return "sha256:" + hashlib.sha256(b"").hexdigest()
    h = hashlib.sha256()
    for ph in sorted(payload_hashes):
        h.update(ph.encode("utf-8"))
        h.update(b"\n")
    return "sha256:" + h.hexdigest()

Check: take the events a block covers from POST /v1/events/search, run the function on their payload hashes, and compare with the block's merkle_root.

Not proved: that the events describe what really happened on the machine. The chain shows the record is the record that was written, in that order.

4. A single event's place

GET /v1/dedup/membership/{event_id} returns a merkle inclusion proof for an event under its parent scope, plus whether a client-signed settlement block covers it. An unparented event has no enclosing scope and the response says so.