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_keybytes - Re-derive the DID from those bytes and confirm it matches
issuer_did - Verify the delegation's
proofsignature 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
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),
)?;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),
);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.
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// Chain verification is done implicitly during invocation verification.
// For standalone chain validation, walk the chain manually using
// verifyInvocation with a synthetic invocation.# 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_costcaveat is present but the invocation args do not contain acostfield, that is a failure - not a pass. - DB errors in revocation callbacks should fail. If your
is_revokedcallback cannot reach the revocation store, it should returntrue(revoked) or propagate the error. Never silently returnfalsewhen 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 failedDelegationChainBroken
The chain structure is invalid. Common causes:
| Error Message | Cause |
|---|---|
issuer is not the delegate of parent delegation | Agent 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 mismatch | Chain reaches a DID matching root, but the public key is different |
chain depth exceeds maximum of 32 | Too 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 Message | Cause |
|---|---|
action 'merge' not in allowed scope ["resolve"] | Agent tried an action it was not granted |
delegation expired at 2026-03-01T00:00:00.000Z | Delegation has expired |
cost 10 exceeds max 5 | Invocation 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 args | Required 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.
// 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).
