@solidus-network/agent-identity-verify
v0.1.1
Published
Hot-path verifier for Solidus agent credentials — offline BBS+ selective-disclosure proof verification plus a cached, fail-closed OAuth Status List revocation check. Testnet-grade; external audit pending.
Maintainers
Readme
@solidus-network/agent-identity-verify
Hot-path verifier for Solidus agent credentials: offline BBS+ selective-disclosure proof verification plus a cached, fail-closed OAuth Status List revocation check. Built to sit in a gateway/middleware with minimal dependencies.
Status — read this first
- Solidus L1 is testnet-only.
did:solidusis submitted to the W3C DID Method Registry, under review — not yet registered. - The BBS+ implementation is testnet-grade; external audit pending (NLnet NGI Zero, H2 2026 target).
- Latency below is measured, not promised — run
scripts/bench.tson your own hardware.
Verify modes
import { createVerifier } from '@solidus-network/agent-identity-verify'
const verifier = createVerifier({
statusIssuerPublicKey: SOLIDUS_STATUS_KEY_HEX, // pin the issuer's Ed25519 key
refreshMs: 30_000,
solidusRpcUrl: 'https://rpc.solidus.network', // only needed for verifyLive()
})
// Cached/offline (default): no network on the proof path.
const result = await verifier.verify({
proof, publicKey, disclosed,
presentationHeader: myChallengeHex,
status: { uri, index },
})
// → { valid, proofValid, revoked, verifiedAsOf, staleMs, claims, mode: 'cached' }
// Live: one RPC to the Solidus L1 for the authoritative revocation state.
const live = await verifier.verifyLive({ credentialId, proof, publicKey, disclosed })The fail-closed staleness tradeoff — read before deploying
The cached mode is fail-closed with a last-known-good snapshot (design decision D7). Concretely:
- No snapshot available (first fetch failed, list unreachable, signature invalid) →
valid: false. A verifier that cannot check revocation never answers "authorized". - Snapshot exists but is stale (refetch failing) → the last-known-good snapshot is used and the verdict carries
verifiedAsOf+staleMs. A revocation issued afterverifiedAsOfis invisible to you. Decide your own staleness ceiling and reject verdicts older than it:
if (result.staleMs !== null && result.staleMs > 120_000) {
// treat as unverified — the revocation signal is too old for this endpoint
}- Spend-mandate checks must use
verifyLive()(or an aggressively low staleness ceiling). A stale "authorized" on a payment path is a financial liability, not a UX nit.
Revocation propagation on the cached path is bounded by refreshMs (default 30 s): a bit flipped by the issuer is visible within one refresh interval.
Measured performance (do not quote without the context)
scripts/bench.ts, 1000 sequential verify() calls after 100 warmup, status snapshot warm (1 fetch for the whole run):
| | p50 | p90 | p99 | mean |
|---|---|---|---|---|
| offline verify() | 225.60 ms | 320.16 ms | 496.41 ms | 241.59 ms |
Environment: Node v24.12.0, x64, 2012-class consumer hardware (macOS 12), single process, sequential. BLS12-381 pairing verification dominates. Your numbers will differ — especially on modern server CPUs — so run the benchmark yourself; this package never claims a latency it didn't measure on the machine making the claim.
Middleware — replace API keys with a verified agent identity
Entry points for the hot path, all over the same core (verifyRequest):
import { solidusAgentAuth } from '@solidus-network/agent-identity-verify/fastify' // preHandler
import { solidusAgentAuth } from '@solidus-network/agent-identity-verify/express' // middleware
import { createMcpToolGuard } from '@solidus-network/agent-identity-verify/mcp' // MCP tools/call guard
import { createWorkersGate } from '@solidus-network/agent-identity-verify/workers' // Cloudflare WorkersThe agent sends one self-contained X-Solidus-Agent header (base64url JSON envelope, ~1.7 kB); MCP callers put the same envelope in _meta["solidus/agent"]. Build it holder-side with buildAgentAuthHeader / buildMcpAgentMeta from @solidus-network/agent-identity.
Anti-replay model (know what you're getting): the proof is bound to <METHOD>:<path> (or mcp:<tool>) plus a timestamp. The verifier rejects timestamps outside maxSkewMs (default 60 s), rejects cross-endpoint replays cryptographically, and rejects identical-proof replays within the window via an in-memory cache (default on; per-process — use your own store behind authorize if you run many replicas). For strict one-time semantics issue your own challenge and call the base verifier directly.
Options: requireScopes (every listed scope must be disclosed), authorize(claims) policy hook, live: true to use verifyLive() per request.
Measured middleware overhead (scripts/bench-middleware.ts, same machine as above, 500 iterations): bare verify() p50 243.97 ms vs full middleware path p50 232.59 ms — the delta is inside run-to-run noise (±~12 ms). Envelope decode, binding checks and scope checks are unmeasurable next to the BLS12-381 pairing. Run it yourself.
What "offline" means here
The BBS+ proof check runs locally (@solidus-network/bbs). The only network activity is the periodic status-list refetch (one HTTP GET per refreshMs per list URI, amortized across all verifies) and nothing else. verifyLive() is explicitly online.
License
Apache-2.0
