Skip to content

Verification

Verification walks the delegation chain from invoker back to root, checking every signature and every caveat. It is fail-closed - any failure at any step rejects the entire invocation. No partial passes.

The 8-Step Process

When you call verify_invocation, here is exactly what happens:

Step 1: Verify Invocation Signature

The invocation's proof (a signed envelope) is verified against the invoker's public key. This proves the invoker actually created this invocation - it was not forged by a third party.

Step 2: Check Invoker-Delegation Binding

The invocation's invoker_did must match the delegation's delegate_did. This ensures the invoker is actually the intended recipient of the delegation, not a different agent trying to use someone else's delegation.

Step 3: Verify Delegation Signatures (Chain Walk)

Starting from the innermost delegation, walk the chain toward the root. At each link:

  • Reconstruct the issuer's identity from the embedded issuer_public_key bytes
  • Re-derive the DID from those bytes and confirm it matches issuer_did
  • Verify the delegation's proof signature against the reconstructed public key

This is the most important step. It ensures every link in the chain was created by the agent it claims - no forgery, no substitution.

Step 4: Check Chain Linkage

At each link, verify that the issuer is the delegate_did of its parent. If Agent B delegates to Agent C, then B must have been the delegate of the parent delegation. This prevents agents from injecting themselves into someone else's chain.

Step 5: Verify Embedded Public Keys Match DIDs

For each delegation, the public key embedded in issuer_public_key is used to compute a DID. If the computed DID does not match the claimed issuer_did, verification fails. This catches attempts to substitute a different public key while keeping the original DID.

Step 6: Check Revocation

Each delegation's content hash is passed to the is_revoked callback. If the callback returns true for any delegation in the chain, the entire invocation is rejected. See Revocation for implementation details.

Step 7: Verify Chain Terminates at Root

The chain must terminate at the expected root authority. The final delegation's issuer_did must match the root identity, and its embedded public key must match the root's known public key bytes. If the chain terminates at an unknown issuer, or at a different root than expected, verification fails.

Step 8: Check All Caveats

All caveats collected from the signed payloads (not from the outer caveats field) are checked against the invocation's action and args. Every caveat must pass. See Caveats for details on each type.

Verification in Code

rust
use kanoniv_agent_auth::{verify_invocation, verify_invocation_with_revocation};

// Basic verification (no revocation check)
let result = verify_invocation(
    &invocation,
    &invoker_identity,
    &root_identity,
)?;

println!("Invoker: {}", result.invoker_did);
println!("Root: {}", result.root_did);
println!("Chain: {:?}", result.chain);   // [invoker, ..., root]
println!("Depth: {}", result.depth);

// With revocation check
let result = verify_invocation_with_revocation(
    &invocation,
    &invoker_identity,
    &root_identity,
    |hash| revocation_set.contains(hash),
)?;
typescript
import { verifyInvocation, verifyInvocationWithRevocation } from "@kanoniv/agent-auth";

// Basic verification
const result = verifyInvocation(invocation, invokerIdentity, rootIdentity);

console.log(result.invoker_did);
console.log(result.root_did);
console.log(result.chain);
console.log(result.depth);

// With revocation check
const result = verifyInvocationWithRevocation(
  invocation, invokerIdentity, rootIdentity,
  (hash) => revokedSet.has(hash),
);
python
from kanoniv_agent_auth import verify_invocation

# Returns a tuple: (invoker_did, root_did, chain, depth)
invoker_did, root_did, chain, depth = verify_invocation(
    invocation,
    invoker_identity,
    root_identity,
)

print(f"Invoker: {invoker_did}")
print(f"Root: {root_did}")
print(f"Chain: {chain}")
print(f"Depth: {depth}")

Chain-Only Verification

You can verify a delegation chain without an invocation. This is useful for validating a delegation before storing or forwarding it.

rust
use kanoniv_agent_auth::verify_delegation_chain;

let chain = verify_delegation_chain(&delegation, &root_identity)?;
// chain is a Vec<String> of DIDs from delegate to root
typescript
// Chain verification is done implicitly during invocation verification.
// For standalone chain validation, walk the chain manually using
// verifyInvocation with a synthetic invocation.
python
# Chain verification happens as part of verify_invocation.
# The Python SDK does not expose a standalone chain verification function.

Fail-Closed Semantics

Verification is fail-closed. This means:

  • Any error rejects the entire invocation. There is no "partial success" or "warning" mode.
  • Unknown caveat types fail verification. If a delegation contains a caveat type that the verifier does not understand, the invocation is rejected. This prevents downgrade attacks where an attacker crafts a caveat that older verifiers silently ignore.
  • Missing fields fail verification. If a max_cost caveat is present but the invocation args do not contain a cost field, that is a failure - not a pass.
  • DB errors in revocation callbacks should fail. If your is_revoked callback cannot reach the revocation store, it should return true (revoked) or propagate the error. Never silently return false when you cannot check.

Error Types

Verification failures return specific error types that tell you exactly what went wrong.

SignatureInvalid

The Ed25519 signature does not match the payload and public key. This means the signed data was tampered with, the wrong key was used, or the signature bytes are corrupted.

Error: Signature verification failed

DelegationChainBroken

The chain structure is invalid. Common causes:

Error MessageCause
issuer is not the delegate of parent delegationAgent tried to delegate using someone else's delegation
chain terminates at 'did:agent:X', expected root 'did:agent:Y'Chain does not reach the expected root
embedded public key produces DID 'X' but delegation claims 'Y'Public key does not match the claimed DID
root public key mismatchChain reaches a DID matching root, but the public key is different
chain depth exceeds maximum of 32Too many levels of delegation
Error: Delegation chain broken: chain terminates at
  'did:agent:a1b2c3...', expected root 'did:agent:d4e5f6...'

CaveatViolation

A caveat check failed. The error message includes the specific caveat that was violated.

Error MessageCause
action 'merge' not in allowed scope ["resolve"]Agent tried an action it was not granted
delegation expired at 2026-03-01T00:00:00.000ZDelegation has expired
cost 10 exceeds max 5Invocation cost exceeds the cap
resource 'entity:order:1' does not match pattern 'entity:customer:*'Resource does not match the allowed pattern
context 'task_id' expected 'task-abc', got 'task-xyz'Wrong context value
max_cost caveat requires 'cost' field in argsRequired field missing from invocation args
Error: Caveat violation: action 'merge' not in allowed scope ["resolve"]

DelegationRevoked

A delegation in the chain has been revoked. The error includes the content hash of the revoked delegation.

Error: Delegation revoked: a1b2c3d4e5f6...

Why Caveats Are Read from Signed Payloads

This is a critical security property. During verification, caveats are extracted from the proof.payload.caveats field inside each delegation's signed envelope - not from the outer delegation.caveats field.

Why? Because the outer field can be tampered with. An attacker could intercept a delegation, modify the outer caveats array to widen the authority (e.g., add "merge" to an action_scope that only allows "resolve"), and pass it along. The outer field is just a convenience for inspection - it is not authoritative.

The signed payload, however, is protected by the issuer's Ed25519 signature. Modifying anything inside it invalidates the signature. So even if the outer caveats field says ["resolve", "merge"], the signed payload says ["resolve"], and that is what verification enforces.

rust
// Demonstration: tampering with outer caveats is detected
let mut delegation = Delegation::create_root(
    &root,
    &agent.identity().did,
    vec![Caveat::ActionScope(vec!["resolve".into()])],
).unwrap();

// Attacker modifies outer caveats to add "merge"
delegation.caveats = vec![
    Caveat::ActionScope(vec!["resolve".into(), "merge".into()])
];

// Agent tries to merge using the tampered delegation
let inv = Invocation::create(
    &agent, "merge", serde_json::json!({}), delegation,
).unwrap();

let result = verify_invocation(&inv, &agent.identity(), &root.identity());
// FAILS: the signed payload still says ["resolve"] only
// Error: CaveatViolation("action 'merge' not in allowed scope [\"resolve\"]")

Performance

Verification is fast. The main costs are:

  • Ed25519 signature verification: ~100 microseconds per signature
  • Chain walk: One iteration per delegation level
  • Caveat checks: Negligible (string comparisons)

A depth-3 chain requires 4 signature verifications (3 delegation signatures + 1 invocation signature), taking roughly 400 microseconds total. No I/O, no network calls, no database lookups (unless your is_revoked callback hits an external store).

For high-throughput scenarios, you can cache verification results keyed by the invocation's content hash. The same invocation will always produce the same result (modulo time-based caveats like expires_at).

The identity and delegation layer for AI agents.