Docs

Audit-challenge protocol

How a regulator issues a signed predicate-scoped challenge over an agent population and verifies the response — inclusion proofs against committed Signed Tree Heads anchored on the public Sigstore Rekor log, without trusting Iqrar's infrastructure.

The audit-challenge protocol is the regulator-facing surface of Iqrar's commitment ledger. It lets an authority — DFSA, ESMA, FCA — verify agent behaviour cryptographically, end-to-end, without trusting the operator running the agent or Iqrar itself.

What the regulator submits

A Challenge is a signed envelope { body, sig } where body carries:

FieldPurpose
idGlobally unique challenge id (UUID or hash-derived).
issuer_kidThe authority's kid in the foundation registry.
predicate.jurisdictionMatch agents whose declared jurisdiction equals this. The issuer's registered scope must encompass it.
predicate.capabilities[]Optional. Match agents declaring at least one of these capabilities.
predicate.time_window{ start, end } in ms — the inclusive ts window over the audit chain.
predicate.action_types[]Optional event-type filter (e.g. ["decision.made", "human_review.recorded"]).
sampling.method"all" or "random".
sampling.rate, sampling.seedRequired when method: "random". The seed drives a deterministic SHA-256 PRF the regulator can recompute.
bound_directive_idOptional. When set, the response also returns inclusion proofs for the directive's lifecycle audit-chain entries.
issued_atIssuance wall-clock time.

The regulator signs canonical(body) with their private key. The CLI helper does this for you:

DFSA issues a challenge

$ bun run challenge:issue
--issuer DFSA
--jurisdiction AE-DIFC
--since '24h ago'
--sample-rate 0.1
--bound-directive dir-3a91b...c4e
--publish

▸ Loaded DFSA private key from ~/.iqrar/keys/dfsa.json ▸ Built canonical body (4 predicates, sampling=random rate=0.1) ▸ Signed with Ed25519 ▸ POST https://api.iqrar.io/audit/challenge ▸ challenge_id: chl-7f3a... ▸ matched_agents: 12 ▸ proofs returned: 87 ▸ directive_audit.audit_chain_entries: 24

What the worker does

For every challenge the worker, atomically:

  1. Verifies the signature against the pinned foundation registry.
  2. Scope-checks the predicate's jurisdiction against authorities[issuer_kid].scope.jurisdictions. A regulator cannot challenge an agent outside their remit.
  3. Capability-checks that the issuer holds the audit_challenge capability.
  4. Resolves the matching agents by joining the predicate against the agents table (agent_id IN ..., org = ..., jurisdiction = ..., capability join via SQLite json_each).
  5. Loads matching audit_entries within the time window + action-type filter.
  6. Applies the deterministic samplesampleDeterministic(candidate_keys, rate, seed) — using SHA-256 as a PRF over (seed, agent_id, seq). The regulator can recompute the expected sample independently.
  7. Builds inclusion proofs for every sampled entry against the smallest STH that covers it (per agent), grouped per agent.
  8. Persists the challenge in audit_challenges with the result so the directive→challenge investigative chain is itself recorded.
  9. Returns { proofs[], directive_audit?, rekor_url }.

Every step is committed: a rejected challenge is recorded with status: 'rejected', a verified challenge with status: 'verified'. The challenge transcript is itself an auditable artifact.

What the response looks like

{
  "ok": true,
  "challenge_id": "chl-7f3a...",
  "matched_agents": ["acme-bot-1", "acme-bot-7", "..."],
  "matched_entries": 871,
  "sampled": 87,
  "proofs": [
    {
      "agent_id": "acme-bot-1",
      "seq": 412,
      "entry": {
        "agent_id": "acme-bot-1",
        "seq": 412,
        "entry_hash": "8a2f...c1e9",
        "ts": 1746478291000,
        "event_type": "decision.made",
        "event_canonical": "{\"agent_id\":\"acme-bot-1\",...}"
      },
      "sth": {
        "tree_size": 500,
        "root_hash": "1f9c...7e2a",
        "signed_at": 1746478320000,
        "signer_kid": "foundation:root-1",
        "signature": "BASE64...",
        "rekor_log_id": "...",
        "rekor_index": 12345678,
        "rekor_uuid": "abcd..."
      },
      "proof": ["...", "...", "..."],
      "leaf_hash": "f8e1...0a7d",
      "rekor_url": "https://rekor.sigstore.dev/api/v1/log/entries/abcd..."
    }
  ],
  "directive_audit": {
    "directive_id": "dir-3a91b...c4e",
    "applied_at": 1746391891000,
    "expired_at": 1746478291000,
    "agents": ["acme-bot-1", "acme-bot-7"],
    "audit_chain_entries": [
      {
        "agent_id": "acme-bot-1",
        "seq": 311,
        "entry": { "event_type": "directive.applied", "ts": 1746391891000, "..." },
        "sth": { "tree_size": 500, "root_hash": "1f9c...7e2a", "..." },
        "proof": ["...", "..."],
        "leaf_hash": "...",
        "rekor_url": "https://rekor.sigstore.dev/api/v1/log/entries/..."
      }
    ]
  }
}

How the regulator verifies — locally

The regulator does not need to trust Iqrar's response. Every proof is verifiable against the public Rekor log.

import { verifyInclusionProof, leafHash } from "@iqrar/rules/merkle";

for (const item of response.proofs) {
  // 1. Recompute the leaf hash from the entry's entry_hash.
  const expected = await leafHash(item.entry.entry_hash);
  if (expected !== item.leaf_hash) throw new Error("leaf mismatch");

  // 2. Verify the inclusion proof reproduces the STH root.
  const ok = await verifyInclusionProof(
    item.leaf_hash,
    item.seq,
    item.sth.tree_size,
    item.proof,
    item.sth.root_hash,
  );
  if (!ok) throw new Error("inclusion proof invalid");

  // 3. Confirm the STH was actually published to Rekor.
  const rekorEntry = await fetch(item.rekor_url).then((r) => r.json());
  // Verify the Rekor entry's body matches our STH root + signer key.
  // (See @iqrar/rules/rekor for the canonical verification helper.)
}

If any step fails, the operator's claim that the agent recorded a particular action at a particular time is not credible — even if Iqrar's response was 200 OK.

The directive↔challenge binding

When a challenge carries bound_directive_id, the response's directive_audit section contains inclusion proofs for the directive's own lifecycle audit-chain entries — directive.received, directive.applied, directive.skipped, directive.rejected — across every targeted agent.

This closes the §8.3 sub-claim: the investigative chain is itself committed and verifiable. A tamper that excised a directive-application record from a regulator's view of the chain would invalidate the inclusion proof against the same STH that anchors the action records.

The regulator can therefore verify, in a single pass:

  1. The directive was issued.
  2. Every targeted agent received it.
  3. The agents that matched applied it (with the recorded window).
  4. The action records produced under the directive's window are committed under the same STHs.
  5. Nothing has been retroactively altered.

That property is the regulator's primary audit guarantee.

Capability gate

To issue an audit challenge, an authority must have may_issue: ["audit_challenge"] in the registry. By convention, rule_bundle and directive are the publication capabilities; audit_challenge is the inquiry capability. The same authority typically holds all three — but the registry can grant them separately, e.g., for a regulator that delegates inquiry to a dedicated supervisory unit.

See also

  • Concepts overview — what the audit chain, Merkle commitment, and Rekor anchoring do —
  • Foundation — why the registry's signing keys are governed by a structurally separate entity —
  • Sigstore Rekor — the public transparency log Iqrar anchors STHs to — rekor.sigstore.dev
© 2026 Cortex Innovations (Pty) Ltd. Iqrar is a working name pending trademark clearance.Powered by Stratafy