Skip to main content

Badge schema & verification

This describes the actual badge structure and the checks verify runs, taken directly from the verification logic (kept byte-for-byte identical between the CLI and the hosted API so a browser-minted badge and a CLI-minted badge are interchangeable).

Canonicalization

Every signature in a badge covers a canonical JSON encoding, not the badge's stored JSON formatting — object keys are sorted, arrays keep order, and the result is a single deterministic string:

function canonical(obj) {
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
if (Array.isArray(obj)) return '[' + obj.map(canonical).join(',') + ']';
const keys = Object.keys(obj).sort();
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonical(obj[k])).join(',') + '}';
}

Signatures use Ed25519 over sha256(canonical(...)) where noted below.

The inception object

The badge's founding event — everything else is derived from or bound to this:

FieldMeaning
schemaFormat version, e.g. "viaid.badge/0.1"
nameHuman-readable agent name
owner_idFree-text owner identifier (e.g. "web-visitor" for browser-minted badges)
owner_pubOwner's Ed25519 public key (base64 SPKI)
agent_pubAgent's current Ed25519 public key
voucher_pubThe VIA ID Witness's public key at mint time
next_key_commitmentsha256 of the next agent key, committed in advance (pre-rotation)
key_seqStarts at 0

agent_id is "via_" + sha256(canonical(inception)).slice(0, 32) — the agent's identity is literally the hash of its own founding event, which is what step 1 of verification re-checks.

The badge core

The object that gets signed (everything in the badge except .signatures):

{
schema, agent_id, inception,
keys: { owner_pub, agent_pub, voucher_pub },
assurance_tier, // e.g. "WITNESSED"
revocation_state, // "FRESH" or "REVOKED" (owner/org-set, distinct from computed freshness)
evidence, // null, or GraphSmith-evaluated capability evidence
log: [ ... ], // hash-chained entries, see below
created_note
}

Three signatures cover this core (canonicalized): owner_sig, agent_sig, and voucher_sig (the Witness's co-signature, added server-side during /api/mint).

Verification steps

Exactly what verify checks, in order:

  1. agent_id == hash(inception) — recomputes the hash and compares.
  2. owner/voucher keys bound to inceptionkeys.owner_pub and keys.voucher_pub must match inception.owner_pub / inception.voucher_pub.
  3. key rotation chain intact — walks every ROTATION / COMPROMISE_ROTATION log entry in order, checking each one's prev_key_seq, new_key_seq, prior_commitment, and that the revealed next key actually hashes to the previously-committed value — then confirms the chain's end state matches the badge's current key_seq, agent_pub, and next_key_commitment. This is what makes key rotation provable rather than just asserted.
  4. owner signature, agent signature, voucher signature — each verified against the canonicalized core.
  5. log hash-chain intact — walks the log from "GENESIS", recomputing each entry's hash from its body and the previous entry's hash, and comparing to the stored entry_hash. One altered byte anywhere in the log breaks every hash after it — this is the actual tamper-evidence mechanism.
  6. freshness / revocation stateREVOKED if revocation_state says so; UNKNOWN if issued_at or badge_ttl is missing/malformed (can't compute an age); STALE if the badge has outlived its TTL; otherwise FRESH.

Verdict

  • INVALID if the hash, any signature, or the log chain fails.
  • Otherwise, mirrors the freshness state: REVOKED, STALE, UNKNOWN, or VALID.

Rotation events

A ROTATION (routine) or COMPROMISE_ROTATION log entry carries a detail object with prev_key_seq, new_key_seq, prior_commitment, revealed_next_pub, new_next_key_commitment, and (for a compromise rotation) suspected_since, plus a voucher_attestation signature from the Witness. A compromise rotation adds a note to the verdict's scope_note flagging the suspected window — readers should discount log entries from that window themselves; verify does not silently discount them.

Short codes

The human-typeable short code (M16XMGN-style) is derived, not stored independently: sha256("shortcode:" + agent_id), mapped through Crockford Base32 (no I/L/O/U — avoids exactly the character confusion this scheme exists to prevent), 7 characters by default.