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:
| Field | Meaning |
|---|---|
schema | Format version, e.g. "viaid.badge/0.1" |
name | Human-readable agent name |
owner_id | Free-text owner identifier (e.g. "web-visitor" for browser-minted badges) |
owner_pub | Owner's Ed25519 public key (base64 SPKI) |
agent_pub | Agent's current Ed25519 public key |
voucher_pub | The VIA ID Witness's public key at mint time |
next_key_commitment | sha256 of the next agent key, committed in advance (pre-rotation) |
key_seq | Starts 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:
agent_id == hash(inception)— recomputes the hash and compares.- owner/voucher keys bound to inception —
keys.owner_pubandkeys.voucher_pubmust matchinception.owner_pub/inception.voucher_pub. - key rotation chain intact — walks every
ROTATION/COMPROMISE_ROTATIONlog entry in order, checking each one'sprev_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 currentkey_seq,agent_pub, andnext_key_commitment. This is what makes key rotation provable rather than just asserted. - owner signature, agent signature, voucher signature — each verified against the canonicalized core.
- 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 storedentry_hash. One altered byte anywhere in the log breaks every hash after it — this is the actual tamper-evidence mechanism. - freshness / revocation state —
REVOKEDifrevocation_statesays so;UNKNOWNifissued_atorbadge_ttlis missing/malformed (can't compute an age);STALEif the badge has outlived its TTL; otherwiseFRESH.
Verdict
INVALIDif the hash, any signature, or the log chain fails.- Otherwise, mirrors the freshness state:
REVOKED,STALE,UNKNOWN, orVALID.
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.