Skip to main content

Credential issuance and verification infrastructure with on-chain attestation anchoring

Project description

Verified Credentials Issuance & Verification SDK

verified-credentials

Overview

Verified Credentials (vercre) is a credential issuance and verification infrastructure SDK for W3C verifiable credentials with on-chain attestation anchoring. It ships with KYC, AML, and accredited investor credential types as built-in examples.

The system demonstrates how trust platforms can issue cryptographically signed credentials as JWTs, anchor their SHA-256 hashes on-chain for portable verification, and enable any downstream platform to verify a user's attestation status without re-running checks.

Privacy by design: Only credential hashes are stored on-chain — never raw credential data. The smart contract serves as a tamper-proof registry of attestation status, while the credential itself remains off-chain in the holder's control.

Composable credentials: Verifiers pick which credential types they need (e.g. KYC + AML for a DeFi protocol, KYC + AccreditedInvestor for a tokenized securities platform). Each credential is independently issued, anchored, and revocable.

Monetization layer: The SDK includes usage-based billing powered by Stripe with six plans (three issuer tiers, three verifier tiers). Free tiers provide full SDK functionality with file-backed attestation. Paid tiers unlock on-chain attestation anchoring on the production AttestationRegistry contract — making credentials verifiable by any third party on BaseScan. The production contract is owned by Vector Guard Labs; only the hosted API at https://vercre.vectorguardlabs.com can authorize issuers on-chain. Self-hosters run the SDK freely for internal use. See Deployment Modes & Monetization for details.

Custodial key management: Private keys are stored server-side and encrypted at rest with Fernet (derived from VERCRE_KEY_ENCRYPTION_SECRET via HKDF-SHA256 with application-specific salt). Keys never leave the server — onboarding returns only the public key and DID, and credential issuance references the issuer by DID rather than passing key material. Key rotation is supported: old keys are preserved (for verifying existing credentials) while new issuance uses the latest active key. Private key material uses mutable bytearray with explicit zeroing after use to minimize memory exposure.

API-first: The full trust network is accessible via a REST API with API key authentication (CLIENT/ADMIN/PLATFORM_ADMIN roles), so integrators can verify credentials, manage issuers, query fees, and monitor usage with curl — no CLI dependency required. Self-serve onboarding lets new issuers generate a key pair (stored server-side), DID, trust registry entry, and admin API key in a single unauthenticated call, with rate limiting (3 requests per hour per IP) to prevent abuse. Trust decisions (tier changes, issuer approval, staking, slashing) are restricted to platform admin keys — issuers cannot upgrade themselves. Verification requests declare a target tier and are validated against per-tier schemas with cumulative required fields — approval re-validates stored data against the approved tier, preventing under-documented issuers from reaching higher trust levels. API keys can optionally be bound to a DID to enable dispute filing authorization.

Webhook notifications: Integrators can register webhook endpoints to receive real-time HTTP POST notifications when events occur — verification results, credential revocations, and dispute openings. Webhooks are scoped to the registering issuer's DID (issuers only see their own events). URLs must use HTTPS and are validated against private/reserved IP ranges to prevent SSRF. Each issuer is limited to 10 webhooks. Dispatches are fire-and-forget so they never block API responses.


Core Architecture

The system consists of a Python SDK (src/vercre/), a Solidity smart contract (contracts/AttestationRegistry.sol), a REST API server, and a CLI tool for key management and credential operations.

Credential Lifecycle:

Issue → Sign as JWT → Anchor hash on-chain → Verify signature + on-chain status → Revoke

Key architectural elements include:

  • W3C Verifiable Credential Data Model v2.0 compliance with Pydantic validation
  • Multi-algorithm key support (Ed25519 for speed, secp256k1 for Ethereum compatibility)
  • Three DID methods (did:key, did:ethr, did:web) with pluggable resolver registry
  • Deterministic SHA-256 hashing via canonical JSON for reproducible credential fingerprints
  • Abstract attestation protocol with in-memory (dev), file-backed (CLI), and EVM (production) implementations
  • Tiered credential schemas (KYC Basic/Standard/Enhanced built-in) for privacy-appropriate verification
  • camelCase claim keys following W3C convention with Pydantic alias support
  • Mandatory expiration enforcement for built-in credential types
  • Optional credentialStatus, issuerMetadata, and schemaVersion fields on VCs
  • Issuer authorization enforcement at both SDK and smart contract layers
  • Self-issuance rejection — issuer and subject DIDs must be different entities
  • Only the original issuer can revoke — enforced on-chain with IssuerMismatch revert
  • Custodial key management: private keys encrypted at rest with Fernet, never exposed in API responses, looked up by DID for credential issuance, key rotation with version tracking
  • REST API with API key authentication (CLIENT/ADMIN/PLATFORM_ADMIN roles), SHA-256 hashed key storage, usage tracking, webhook event notifications, programmatic credential issuance, self-serve API key creation with optional DID binding, one-command onboarding with rate limiting (3 req/hour per IP), and issuer verification request/approval flow
  • Four-tier issuer trust registry (UNVERIFIED → VERIFIED → REGULATED → INSTITUTIONAL) with separate operational status (ACTIVE/SUSPENDED/REVOKED)
  • Economic staking with configurable minimum stakes per tier and automatic suspension on slash below minimum
  • W3C StatusList2021 credential status list publishing with GZIP+base64url encoded bitstrings for portable revocation checking
  • Prometheus metrics instrumentation with Grafana dashboards
  • Redis-backed rate limiting with in-memory fallback
  • Reputation engine with weighted event scoring (credential issuance, revocations, disputes, verifier feedback)
  • Dispute governance with lifecycle tracking (open → evidence → resolve as cleared or penalized)
  • Verifier policy engine with tier requirements, minimum reputation scores, and confidence scoring
  • Fee tracking with configurable schedules and treasury accumulation
  • DID method governance with lifecycle management (active/deprecated/suspended/deactivated), policy enforcement, deactivation/recovery, and audit logging
  • Policy-driven key rotation procedures with expiration enforcement, compromise response, and rotation audit trail
  • STRIDE-based threat modeling with asset inventory, threat catalog, severity tracking, and mitigation status
  • OWASP-aligned penetration testing framework with attack definitions, result tracking, and risk assessment
  • SD-JWT (Selective Disclosure JWT) with holder key binding — issuers create JWTs with selectively disclosable claims, holders reveal only what verifiers need, KB-JWT proves possession
  • Credential freshness heuristic — verifier responses include freshness level (fresh/aging/stale/critical/expired) and days-until-expiry for renewal decisions
  • Credential renewal endpoint — revoke-and-reissue in a single API call with new expiration and status list allocation
  • Circuit breaker on EVM integration — failure threshold with automatic open/half-open/closed state transitions to prevent cascading failures
  • Webhook retry with exponential backoff — 3 attempts at 1s/5s/30s delays before giving up (replaces fire-and-forget)
  • Per-API-key rate limiting — 60 req/min on sensitive endpoints (issue, verify, revoke) in addition to per-IP onboarding limits
  • JWT kid header matching — verifier matches the JWT's kid claim against DID Document verification methods instead of blindly using the first key

Core Components

Component Responsibility
Cryptography (crypto/) Ed25519 and secp256k1 key generation, JWT signing/verification, deterministic hashing, Fernet key-at-rest encryption
Key Store (key_store.py) Custodial key storage with key rotation: encrypted private keys, versioned keys per DID, lookup by DID, in-memory/file-backed/YugabyteDB backends
DID Resolution (did/) did:key (offline), did:ethr (Ethereum), did:web (HTTPS) resolution
Attestation Protocol (chain/) On-chain anchoring, revocation, status queries, and file-backed persistence
Credential Models (models/) W3C VC/VP data models, DID documents, tiered KYC/AML schemas
Issuer (issuer.py) Credential creation, signing, anchoring, revocation, and optional fee charging
Verifier (verifier.py) JWT signature verification, on-chain status checks, trust enrichment, and policy evaluation
Trust Registry (trust.py) Issuer registration, tier management, status tracking, staking, and slashing
Reputation (reputation.py) Weighted event-based reputation scoring with history tracking
Disputes (dispute.py) Dispute lifecycle governance (open, evidence, resolve)
Fees (fees.py) Fee scheduling, charging, and treasury tracking
Policy (policy.py) Verifier policy evaluation and confidence scoring
API Keys (api_keys.py) API key creation, validation, revocation, and usage tracking
Webhooks (webhook.py) Webhook registration, removal, listing, and async event dispatch
Status List (status_list.py) W3C StatusList2021 bitstring management, index allocation, revocation tracking, and credential publishing
Secrets (secrets.py) sops + age encrypted secret loading at startup with env var injection and dev-mode bypass
API (api.py) REST API server with three-tier role-based authentication and 53 endpoints
Metrics (metrics.py) Prometheus instrumentation: request counts/durations, credential operations, verification results, key rotations, rate limit hits
Rate Limiting (rate_limit.py) Redis-backed sliding window rate limiter with in-memory fallback
CLI (cli.py) Key generation, issuance, verification, revocation, DID resolution, trust, reputation, dispute, fee, and API key commands
Smart Contract (contracts/) On-chain attestation registry with access control
DID Governance (did/governance.py) DID method lifecycle management, policy enforcement, deactivation/recovery, and audit logging
Key Rotation Procedures (did/rotation.py) Policy-driven key rotation scheduling, expiration enforcement, compromise response, and rotation audit trail
Threat Model (security/threat_model.py) STRIDE-based threat modeling with asset inventory, threat catalog, severity tracking, and mitigation status
Penetration Testing (security/pentest.py) Security test case framework with OWASP-aligned attack definitions, result tracking, and risk assessment
SD-JWT (crypto/sd_jwt.py) Selective Disclosure JWT creation, holder presentation with claim selection, verifier reconstruction, and key binding JWT (KB-JWT) verification
Circuit Breaker (chain/circuit_breaker.py) Fault tolerance for EVM integration with failure threshold, automatic open/half-open/closed state transitions
Anchor Gateway (chain/anchor_gateway.py) Billing-gated attestation protocol — routes paid issuers to EVM (on-chain), free issuers to file-backed fallback
Credential Store (credential_store.py) Credential indexing by ID, hash, and subject DID with in-memory, file-backed, and YugabyteDB backends
Audit Logger (audit.py) Tamper-evident HMAC-chained audit logging with SIEM export (syslog RFC 5424 + JSON-lines), chain verification, and query filtering
Secret Rotation (secret_rotation.py) Re-encrypt stored keys on secret rotation, generate cryptographic secrets, zero-downtime infrastructure credential rotation
Lightweight EVM (chain/evm_lite.py) Lightweight EVM attestation protocol using httpx + cryptography (no web3 dependency) for Vercel/serverless deployments

Smart Contract

AttestationRegistry.sol (Solidity ^0.8.24)

The contract stores attestation status as a nested mapping: subject → credentialHash → Attestation.

Key methods:

Method Access Description
anchorAttestation() Authorized issuer Store credential hash with optional expiration
revokeAttestation() Original issuer (must be authorized) Mark attestation as revoked
getAttestationStatus() Public (view) Returns status: 0=not_found, 1=active, 2=revoked, 3=expired
authorizeIssuer() Owner Grant anchoring permission to an address
deauthorizeIssuer() Owner Revoke anchoring permission
transferOwnership() Owner Start two-step ownership transfer
acceptOwnership() Pending owner Accept ownership transfer

Events emitted: AttestationAnchored, AttestationRevoked, IssuerAuthorized, IssuerDeauthorized, OwnershipTransferStarted, OwnershipTransferred


Credential Types

The SDK implements a tiered credential schema system where each credential type maps to a distinct set of validated claims. Claim keys use camelCase (W3C convention).

KYC Credentials (Tiered)

Type KYC Level Fields Use Case
KYCBasicCredential basic kycLevel, countryOfResidence, verificationMethod, verifiedAt Lightweight identity check (country + method only)
KYCStandardCredential standard kycLevel, fullName, dateOfBirth, countryOfResidence, documentType, documentCountry, verifiedAt Document-verified identity
KYCEnhancedCredential enhanced kycLevel, fullName, dateOfBirth, countryOfResidence, sourceOfFunds, riskScore (0-100), pepStatus, sanctionsCheck (clear/flagged/failed), verifiedAt Full due diligence with risk assessment

All KYC credential types require an expiration date at issuance time. Country codes are auto-uppercased. The kycLevel field uses a Literal type to enforce the correct value per tier.

Other Credentials

Type Fields Use Case
AMLScreeningCredential sanctionsStatus (clear/flagged/blocked), pepStatus, adverseMedia, riskScore (0-100), screenedAt Anti-money laundering screening
AccreditedInvestorCredential accredited, jurisdiction, verificationMethod, verifiedAt SEC accredited investor verification

Trust & Monetization Layer

The SDK includes a monetization layer that governs issuer trust, economic incentives, reputation, dispute resolution, verification policies, and fee collection.

Issuer Trust Tiers

Issuers are classified into four tiers that control which credential types they may issue:

Tier Allowed Credential Types Minimum Stake
INSTITUTIONAL All 5 types 1000.0
REGULATED All 5 types 500.0
VERIFIED KYCBasic, KYCStandard, AMLScreening 100.0
UNVERIFIED KYCBasic only 0.0

Tiers are separate from operational status. An issuer has both a tier (what they can issue) and a status (whether they are allowed to issue right now):

Status Effect
ACTIVE Normal operation — issuer can issue credentials per their tier
SUSPENDED Blocked from issuing — triggered automatically when stake falls below tier minimum, or set manually
REVOKED Permanently blocked from issuing

Staking

Issuers stake tokens to back their tier. Staking is cumulative — multiple stake_issuer() calls add to the total. If an issuer's stake is slashed below their tier's minimum via slash_stake(), they are automatically suspended.

Reputation

Every issuer starts with a base score of 50 (range 0-100). Events adjust the score:

Event Score Delta
Credential issued +1
Credential revoked -3
Dispute won +5
Dispute lost -10
Verifier positive feedback +2
Verifier negative feedback -5

Reputation maps to descriptive labels: 0-29 = untrusted, 30-59 = basic, 60-84 = trusted, 85-100 = institutional.

Disputes

Disputes follow a lifecycle: OPENEVIDENCE_SUBMITTEDRESOLVED_CLEARED or RESOLVED_PENALIZED. Evidence can be submitted to open or evidence-submitted disputes. Resolved disputes cannot be modified.

Verifier Policy

Verifiers can enforce minimum requirements on credential issuers:

  • Minimum tier — e.g. require REGULATED or higher
  • Minimum reputation score — e.g. require 60+
  • Non-revoked status — block suspended or revoked issuers

The policy engine also computes a confidence score (0.0-1.0) weighted across three dimensions: credential validity (40%), issuer tier (30%), and issuer reputation (30%).

Fees

The fee system tracks charges across four fee types with configurable amounts:

Fee Type Default Amount
REGISTRATION 10.0
ISSUANCE 1.0
VERIFICATION 0.5
TIER_UPGRADE 50.0

When a FeeManager is attached to the issuer, an ISSUANCE fee is automatically charged on each successful credential issuance.

Webhooks

Integrators register webhook endpoints to receive HTTP POST notifications when events occur. Each webhook subscribes to one or more event types:

Event Trigger
verification.passed POST /v1/verify returns valid: true
verification.failed POST /v1/verify returns valid: false
credential.revoked POST /v1/credentials/revoke succeeds
dispute.opened POST /v1/disputes creates a dispute

Webhook payloads are sent as JSON with event, timestamp, and data fields. Dispatch is fire-and-forget via asyncio.create_task() — HTTP errors on individual webhooks are logged but never block the API response or affect other webhooks.

Usage Visibility

The GET /v1/usage endpoint returns aggregate counts so integrators can monitor their consumption:

{
  "verifications": 1240,
  "issuers": 3,
  "disputes": 2
}

The verification counter increments on every call to POST /v1/verify. Issuer and dispute counts are computed from the trust registry and dispute manager at query time.


REST API

The SDK includes a FastAPI server that exposes the full trust network over HTTP. Most endpoints require an API key in the X-API-Key header. Four endpoints are unauthenticated: health check, self-serve CLIENT API key creation, platform admin bootstrap (one-time), and onboarding.

API Key Authentication

Keys use the format vercre_client_<32-hex>, vercre_admin_<32-hex>, or vercre_platform_<32-hex>. The raw secret is shown once at creation and stored as a SHA-256 hash. Each authenticated request increments a usage counter.

Role Access DID Binding
CLIENT Verify credentials, issuer lookup (including can-issue and whitelisted checks), credential status, list disputes. Dispute filing requires DID binding. Optional (required for disputes)
ADMIN All CLIENT endpoints plus credential issuance, revocation, issuer list, verification request submission, reputation (including history), evidence submission, webhook management, fee schedule/history, and usage stats Required (automatically bound to issuer DID during onboarding)
PLATFORM_ADMIN All ADMIN endpoints plus issuer registration, removal, tier changes, status changes, staking, slashing, issuer approval, dispute resolution, and fee charging Not bound (operates on all issuers)

DID Binding: CLIENT keys can optionally be bound to a DID when created, enabling dispute filing on behalf of that DID. ADMIN keys are automatically bound to the issuer DID during onboarding to prevent cross-issuer credential operations. See docs/AUTHENTICATION.md for details.

Onboarding creates ADMIN keys — issuers can issue credentials and manage webhooks, but cannot change their own tier, status, or stake. Trust decisions are reserved for PLATFORM_ADMIN keys, which are created via the bootstrap endpoint (one-time, when no platform admin exists), by an existing platform admin via API, or via CLI.

Starting the Server

# Start the server (binds to 0.0.0.0:8000)
vercre-server

# Option 1: One-command onboarding (generates key pair stored server-side, DID, registers issuer, creates ADMIN API key)
curl -X POST http://localhost:8000/v1/onboard \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Corp"}'

# Option 2: Generate an API key via CLI
vercre apikey create --name "My Admin Key" --role admin
# Copy the Secret value — it is shown only once

# Option 3: Create a CLIENT API key via API (no auth required)
curl -X POST http://localhost:8000/v1/api-keys \
  -H "Content-Type: application/json" \
  -d '{"name":"My App"}'

Rate Limiting:

  • The onboarding endpoint is rate limited to 3 requests per hour per IP address
  • Uses Redis-backed sliding window algorithm (survives restarts, works across instances)
  • Falls back to in-memory if Redis is unavailable
  • When exceeded, returns 429 Too Many Requests with Retry-After: 3600 header
  • Error response: {"detail": "Rate limit exceeded. Try again in 3600 seconds."}
To create the first platform admin key, use the one-time bootstrap endpoint (no auth required):

```bash
# Bootstrap first PLATFORM_ADMIN key (returns 403 if one already exists)
curl -X POST http://localhost:8000/v1/api-keys/platform-admin/bootstrap \
  -H "Content-Type: application/json" \
  -d '{"name":"Root Admin"}'

The server reads from the same ~/.vercre/ files as the CLI, so issuers registered via CLI are visible to the API and vice versa. Issuers can be registered directly through POST /v1/issuers (platform admin), via the CLI with vercre trust add, or through the POST /v1/onboard endpoint.

Issuer Verification Flow

Onboarding creates an issuer at the UNVERIFIED tier with an ADMIN API key. To move to a higher tier, the issuer submits a verification request declaring their target tier. The request is validated against per-tier required fields at submission time, and re-validated against the approved tier at approval time.

1. Issuer: POST /v1/onboard                          → UNVERIFIED + ADMIN key
2. Issuer: POST /v1/issuers/{did}/verify-request      → declares requestedTier + submits required fields
3. Platform admin: POST /v1/issuers/{did}/approve      → validates stored data against approved tier, sets tier

Issuers cannot change their own tier, status, or stake — these are platform-level trust decisions.

Per-tier verification requirements (cumulative — each tier includes all fields from the tier below):

Field VERIFIED REGULATED INSTITUTIONAL
entityType required required required
jurisdiction required required required
licenseId required required required
website required required required
regulatoryBody required required
licenseType required required
complianceOfficerName required required
complianceOfficerEmail required required
primaryRegulator required
institutionCharterNumber required
soc2Certified required
lastExaminationDate required

Validation rules:

  • Requesting UNVERIFIED tier is rejected (422)
  • Approving to UNVERIFIED tier is rejected (422)
  • Approval requires a stored verification request on file
  • Downgrade approval is allowed (e.g. submit INSTITUTIONAL data, approve as REGULATED)
  • Upgrade approval is rejected if stored data lacks required fields for the target tier
  • complianceOfficerEmail must contain a valid email format

Running with Docker

The full stack (API server + YugabyteDB + Redis + Prometheus + Grafana) runs with a single command:

# Option A: Quick start with .env (development)
cp .env.example .env
# Edit .env and set strong passwords/secrets for:
#   YSQL_PASSWORD, REDIS_PASSWORD, VERCRE_KEY_ENCRYPTION_SECRET, GF_SECURITY_ADMIN_PASSWORD

# Option B: Production with sops + age (recommended)
# See docs/SECRET_MANAGEMENT.md for full setup
make secrets-keygen          # one-time: generate age key
# Edit .sops.yaml with your age1... public key
cp secrets.json.example secrets.json
# Edit secrets.json with real values
make secrets-encrypt         # creates secrets.enc.json (safe to commit)

docker compose up -d

This starts six containers:

Service Image Port Description
db yugabytedb/yugabyte:2024.2.9.1-b8 127.0.0.1:5433 (YSQL), 7000 (Master UI), 9000 (TServer), 15433 (YSQL UI) YugabyteDB distributed database with persistent ybdata volume and YSQL auth enabled
redis redis:7-alpine 127.0.0.1:6379 Redis for rate limiting (ACL auth, TLS, persistent redisdata volume)
api Built from Dockerfile 8000 Vercre API server connected to YugabyteDB and Redis
caddy caddy:2-alpine 80, 443 Reverse proxy with automatic HTTPS, security headers (HSTS, X-Frame-Options, X-Content-Type-Options)
prometheus prom/prometheus:v2.53.0 9090 Metrics collection (scrapes API every 15s)
grafana grafana/grafana:11.1.0 3000 Dashboards (login required, anonymous access disabled)

The API container waits for the database and Redis health checks to pass before starting. Data is stored in YugabyteDB (via its PostgreSQL-compatible YSQL API on port 5433) instead of ~/.vercre/ files. All credentials are read from .env (never committed to source control). Database and Redis ports are bound to localhost only.

# Verify the stack is running
curl http://localhost:8000/v1/health
# {"status":"ok","version":"0.1.0"}

# Rebuild after code changes
docker compose build && docker compose up -d

# View logs
docker compose logs -f api

# Tear down (keeps data)
docker compose down

# Tear down and delete data
docker compose down -v

To connect to the database directly (debugging):

# YugabyteDB uses the YSQL interface on port 5433
psql postgresql://vercre:vercre@localhost:5433/vercre

YugabyteDB also exposes monitoring UIs: Master UI at http://localhost:7000 and YSQL UI at http://localhost:15433.

Endpoints

All request/response bodies use camelCase JSON.

Unauthenticated

Method Path Description
GET /v1/health Service health check (returns status and version)
POST /v1/api-keys Create a CLIENT API key (returns raw secret once)
POST /v1/api-keys/platform-admin/bootstrap Bootstrap the first PLATFORM_ADMIN key (one-time; returns 403 if one already exists)
POST /v1/onboard One-command onboarding: generates key pair (stored server-side), DID, registers issuer, creates admin API key. Returns public key only.
POST /v1/identities/subject Generate a subject DID:key identity (rate limited 5/hr per IP). No API key created.
GET /v1/status-lists/{issuer} Published StatusList2021Credential for portable revocation checking

Client or Admin

Method Path Description
POST /v1/verify Verify a credential with optional policy constraints
POST /v1/verify-presentation Verify a Verifiable Presentation and its embedded credentials
GET /v1/issuers/{did} Look up an issuer by DID
GET /v1/issuers/{did}/can-issue Check if an issuer can issue a credential type (?type= required)
GET /v1/issuers/{did}/whitelisted Check if an issuer is registered and active
GET /v1/credentials/{hash}/status Check on-chain attestation status
GET /v1/credentials/{id}/detail Look up a previously issued credential by ID
GET /v1/subjects/{did}/credentials List all credentials issued to a subject DID
POST /v1/disputes Open a new dispute
GET /v1/disputes List disputes (optional ?issuer= filter)
GET /v1/disputes/{id} Look up a single dispute by ID
GET /v1/disputes/{id}/evidence List evidence for a dispute

Admin (Issuer or Platform Admin)

Method Path Description
POST /v1/issue Issue a credential and anchor it on-chain (key looked up by issuer DID)
GET /v1/issuers List issuers (optional ?tier= filter)
POST /v1/issuers/{did}/verify-request Submit a verification request with requestedTier and per-tier required fields
GET /v1/issuers/{did}/verify-request Get stored verification request for an issuer
POST /v1/credentials/revoke Revoke an on-chain attestation
POST /v1/credentials/renew Renew a credential (revoke old + issue new with updated expiration)
GET /v1/reputation/{did} Get reputation score
GET /v1/reputation/{did}/history Get reputation event history (optional ?event_type= filter)
POST /v1/reputation/{did}/feedback Submit verifier feedback (rate limited 1/hr per issuer per key)
POST /v1/disputes/{id}/evidence Submit evidence
GET /v1/usage Aggregate usage counts (verifications, issuers, disputes)
POST /v1/webhooks Register a webhook endpoint (201)
GET /v1/webhooks List all registered webhooks
GET /v1/webhooks/{id} Look up a single webhook
DELETE /v1/webhooks/{id} Remove a webhook
GET /v1/fees/schedule View fee schedule
GET /v1/fees/history View fee history (optional ?payer= filter)
POST /v1/keys/{did}/rotate Rotate the signing key for a DID (old keys preserved for verification)
GET /v1/keys/{did}/versions List all key versions for a DID (active + rotated)
GET /v1/api-keys List API keys (admin sees own keys, platform admin sees all)
GET /v1/api-keys/{key_id} Get a single API key by ID
DELETE /v1/api-keys/{key_id} Delete an API key
POST /v1/chain/authorize Authorize issuer on production on-chain contract (requires paid plan)
GET /v1/chain/authorization/{did} Check on-chain authorization status (free read)

Platform Admin Only

Method Path Description
POST /v1/api-keys/platform-admin Create an additional PLATFORM_ADMIN API key (requires existing platform admin auth)
POST /v1/issuers Register a new issuer (201) or return existing (200)
DELETE /v1/issuers/{did}/remove Remove an issuer
PATCH /v1/issuers/{did}/tier Update issuer tier
PATCH /v1/issuers/{did}/status Update issuer status
POST /v1/issuers/{did}/stake Add stake
POST /v1/issuers/{did}/slash Slash stake
POST /v1/issuers/{did}/approve Approve issuer verification and set tier
PATCH /v1/disputes/{id}/resolve Resolve a dispute
POST /v1/fees/charge Charge a fee manually
GET /v1/fees/treasury Treasury balance
GET /v1/usage/detailed Per-issuer usage breakdown
GET /v1/audit/log Query tamper-evident audit log (optional ?since=, ?event_type=, ?actor=, ?limit= filters)
GET /v1/audit/verify Verify HMAC chain integrity of the audit log
DELETE /v1/chain/authorize Revoke on-chain authorization for an issuer

Example API Calls

ADMIN_KEY="vercre_admin_..."       # Issuer admin key (from onboarding)
PLATFORM_KEY="vercre_platform_..." # Platform admin key (from CLI or platform admin)

# Health (no auth) — returns status and version
curl http://localhost:8000/v1/health
# {"status":"ok","version":"0.1.0"}

# Create a CLIENT API key without DID binding (read-only: verify, status checks)
curl -X POST http://localhost:8000/v1/api-keys \
  -H "Content-Type: application/json" \
  -d '{"name":"My App"}'
# Returns: keyId, name, role, secret (shown once), createdAt

# Create a CLIENT API key with DID binding (enables dispute filing)
curl -X POST http://localhost:8000/v1/api-keys \
  -H "Content-Type: application/json" \
  -d '{"name":"Dispute Reporter","did":"did:key:z6MkReporter"}'
# Returns: keyId, name, role, secret (shown once), createdAt, issuerDid

# --- Onboarding and Issuer Verification Flow ---

# Step 1: One-command onboarding (no auth required)
# Generates key pair (stored server-side), DID, registers as UNVERIFIED issuer, creates ADMIN API key
curl -X POST http://localhost:8000/v1/onboard \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Corp"}'
# Returns: did, name, tier (UNVERIFIED), status, reputation, stake, publicKey, apiKey, apiKeyId
# NOTE: Only the public key is returned — private key is stored server-side, encrypted at rest

# Step 2: Submit verification request (issuer ADMIN key)
# Must include requestedTier and all fields required for that tier
curl -X POST http://localhost:8000/v1/issuers/did:key:z6MkIssuer/verify-request \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"requestedTier":"VERIFIED","entityType":"bank","jurisdiction":"US","licenseId":"FDIC-12345","website":"https://acmecorp.com"}'
# Returns: did, status (pending_review), verificationRequest

# For REGULATED tier, include additional regulatory fields:
curl -X POST http://localhost:8000/v1/issuers/did:key:z6MkIssuer/verify-request \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"requestedTier":"REGULATED","entityType":"broker","jurisdiction":"US","licenseId":"SEC-789","website":"https://acmecorp.com","regulatoryBody":"SEC","licenseType":"broker-dealer","complianceOfficerName":"Jane Doe","complianceOfficerEmail":"jane@acmecorp.com"}'

# Step 3: Platform admin approves and sets tier (PLATFORM_ADMIN key)
curl -X POST http://localhost:8000/v1/issuers/did:key:z6MkIssuer/approve \
  -H "X-API-Key: $PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tier":"VERIFIED"}'

# --- Issuer operations (ADMIN key) ---

# Issue a credential via API (key looked up server-side by DID)
curl -X POST http://localhost:8000/v1/issue \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "issuerDid": "did:key:z6MkIssuer",
    "subjectDid": "did:key:z6MkSubject",
    "credentialType": "KYCBasicCredential",
    "claims": {"kycLevel":"basic","countryOfResidence":"US","verificationMethod":"document","verifiedAt":"2025-01-01T00:00:00Z"},
    "expiration": "2026-01-01T00:00:00+00:00"
  }'

# List issuers
curl http://localhost:8000/v1/issuers \
  -H "X-API-Key: $ADMIN_KEY"

# Check if an issuer can issue a credential type
curl "http://localhost:8000/v1/issuers/did:key:z6MkTest/can-issue?type=KYCBasicCredential" \
  -H "X-API-Key: $ADMIN_KEY"

# Check if an issuer is registered and active
curl http://localhost:8000/v1/issuers/did:key:z6MkTest/whitelisted \
  -H "X-API-Key: $ADMIN_KEY"

# Verify a credential
curl -X POST http://localhost:8000/v1/verify \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"credential\": $(cat credential.json)}"

# Verify with policy
curl -X POST http://localhost:8000/v1/verify \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"credential\": $(cat credential.json), \"policy\": {\"minTier\": \"REGULATED\", \"minReputation\": 60}}"

# Revoke a credential
curl -X POST http://localhost:8000/v1/credentials/revoke \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"credential\": $(cat credential.json), \"issuerDid\": \"did:key:z6MkIssuer\"}"

**Authorization Requirements:**
- CLIENT keys must be bound to a DID matching `reporter_did`
- ADMIN keys are automatically bound to issuer DID during onboarding
- Unbound CLIENT keys will receive `403: API key must be bound to a DID to file disputes`
- Mismatched DIDs will receive `403: API key not authorized to file disputes for this DID`

# Open a dispute (requires DID-bound CLIENT or ADMIN key)
# The API key's issuer_did must match reporter_did
curl -X POST http://localhost:8000/v1/disputes \
  -H "X-API-Key: $BOUND_CLIENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"credentialId":"urn:uuid:abc","reporterDid":"did:key:z6MkR","issuerDid":"did:key:z6MkI","reason":"fraudulent"}'

# Look up a single dispute
curl http://localhost:8000/v1/disputes/dispute-xxx \
  -H "X-API-Key: $ADMIN_KEY"

# Get reputation event history (with optional event_type filter)
curl "http://localhost:8000/v1/reputation/did:key:z6MkTest/history?event_type=CREDENTIAL_ISSUED" \
  -H "X-API-Key: $ADMIN_KEY"

# Get fee schedule
curl http://localhost:8000/v1/fees/schedule \
  -H "X-API-Key: $ADMIN_KEY"

# Get usage stats
curl http://localhost:8000/v1/usage \
  -H "X-API-Key: $ADMIN_KEY"

# Register a webhook for verification and revocation events
curl -X POST http://localhost:8000/v1/webhooks \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hook","events":["verification.passed","verification.failed","credential.revoked"]}'

# List all webhooks
curl http://localhost:8000/v1/webhooks \
  -H "X-API-Key: $ADMIN_KEY"

# Look up a single webhook
curl http://localhost:8000/v1/webhooks/wh-xxx \
  -H "X-API-Key: $ADMIN_KEY"

# Delete a webhook
curl -X DELETE http://localhost:8000/v1/webhooks/wh-xxx \
  -H "X-API-Key: $ADMIN_KEY"

# --- Platform admin key creation ---

# Bootstrap the first PLATFORM_ADMIN key (only works when none exist, no auth required)
curl -X POST http://localhost:8000/v1/api-keys/platform-admin/bootstrap \
  -H "Content-Type: application/json" \
  -d '{"name":"Root Platform Admin"}'
# Returns: keyId, name, role, secret (shown once), createdAt

# Create an additional PLATFORM_ADMIN key (requires existing platform admin auth)
curl -X POST http://localhost:8000/v1/api-keys/platform-admin \
  -H "X-API-Key: $PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Second Platform Admin"}'

# --- Platform admin operations (PLATFORM_ADMIN key) ---

# Register a new issuer directly (creates if new, returns existing if already registered)
curl -X POST http://localhost:8000/v1/issuers \
  -H "X-API-Key: $PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"did":"did:key:z6MkTest","tier":"VERIFIED","name":"Acme Corp"}'

# Stake
curl -X POST http://localhost:8000/v1/issuers/did:key:z6MkTest/stake \
  -H "X-API-Key: $PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":1000.0}'

# Slash stake
curl -X POST http://localhost:8000/v1/issuers/did:key:z6MkTest/slash \
  -H "X-API-Key: $PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":200.0,"reason":"policy violation"}'

# Resolve a dispute
curl -X PATCH http://localhost:8000/v1/disputes/dispute-xxx/resolve \
  -H "X-API-Key: $PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"penalize":true,"penaltyAmount":100.0}'

# Charge a fee manually
curl -X POST http://localhost:8000/v1/fees/charge \
  -H "X-API-Key: $PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"feeType":"ISSUANCE","payerDid":"did:key:z6MkTest","description":"Manual issuance fee"}'

# --- Key rotation (ADMIN key) ---

# Rotate a signing key (generates new key pair, deactivates old one)
curl -X POST http://localhost:8000/v1/keys/did:key:z6MkIssuer/rotate \
  -H "X-API-Key: $ADMIN_KEY"
# Returns: message, did, newPublicKey, keyType

# List all key versions for a DID
curl http://localhost:8000/v1/keys/did:key:z6MkIssuer/versions \
  -H "X-API-Key: $ADMIN_KEY"
# Returns: [{version, active, publicKey, keyType, createdAt, rotatedAt}, ...]

DID Methods Supported

Method Format Resolution Best For
did:key did:key:z6Mk... Offline (key embedded in DID) Testing, self-certifying identities
did:ethr did:ethr:0x... Ethereum address derivation On-chain subjects, wallet-based identity
did:web did:web:example.com HTTPS fetch of /.well-known/did.json Organizations, institutional issuers

Key Types

The --type flag on vercre keygen accepts two values:

Type Algorithm JWT Algorithm Security Level Use When
ed25519 (default) EdDSA (Edwards-curve) EdDSA 128-bit General-purpose credentials, did:key, did:web
secp256k1 ECDSA (Koblitz curve) ES256K 128-bit Ethereum integration, did:ethr, wallet-based identity

Both types provide the same cryptographic strength. The difference is ecosystem compatibility:

  • ed25519 produces smaller keys (32 bytes), fixed-size signatures (64 bytes), and deterministic signing (no random nonce). Works with did:key and did:web.
  • secp256k1 is the curve used by Ethereum and Bitcoin. Required for did:ethr because Ethereum addresses are derived from secp256k1 public keys via Keccak-256 hashing. Works with did:key and did:ethr.

State Machine

Attestation Status: NOT_FOUND → ACTIVE → REVOKED or NOT_FOUND → ACTIVE → EXPIRED

Status is computed dynamically — expiration is checked at query time against block.timestamp, not stored as a separate state.


Running the SDK

Prerequisites

  • Python 3.13+
  • uv (recommended) or pip

Install

# From PyPI (recommended for integrators)
pip install vercre

# From source (for contributors)
git clone https://github.com/pavondunbar/Verified-Credentials-SDK.git
cd Verified-Credentials-SDK
uv pip install -e ".[dev]"

For EVM integration:

pip install "vercre[evm]"

For YugabyteDB backend (uses PostgreSQL-compatible YSQL interface):

pip install "vercre[pg]"

CLI Usage

# Generate issuer key pair and DID (private key stored server-side, output contains only public key + DID)
vercre keygen --type ed25519 --output issuer.json

# Generate subject key pair and DID
vercre keygen --type ed25519 --output subject.json

The issuer and subject must be different identities. Using the same DID for both --issuer-did and --subject will be rejected.

Issuing Credentials

All KYC types require --expiration. AML and AccreditedInvestor do not.

# Issue a KYC Basic credential
vercre issue --issuer-did did:key:z6MkIssuer --subject did:key:z6Mk... \
  --type KYCBasicCredential \
  --claims '{"kycLevel":"basic","countryOfResidence":"US","verificationMethod":"document","verifiedAt":"2025-01-01T00:00:00Z"}' \
  --expiration 2026-01-01T00:00:00+00:00 \
  --output credential.json

# Issue a KYC Standard credential
vercre issue --issuer-did did:key:z6MkIssuer --subject did:key:z6Mk... \
  --type KYCStandardCredential \
  --claims '{"kycLevel":"standard","fullName":"Jane Doe","dateOfBirth":"1990-05-15","countryOfResidence":"GB","documentType":"passport","documentCountry":"GB","verifiedAt":"2025-01-01T00:00:00Z"}' \
  --expiration 2026-01-01T00:00:00+00:00 \
  --output credential.json

# Issue a KYC Enhanced credential
vercre issue --issuer-did did:key:z6MkIssuer --subject did:key:z6Mk... \
  --type KYCEnhancedCredential \
  --claims '{"kycLevel":"enhanced","fullName":"John Smith","dateOfBirth":"1985-03-20","countryOfResidence":"DE","sourceOfFunds":"employment","riskScore":25,"pepStatus":false,"sanctionsCheck":"clear","verifiedAt":"2025-01-01T00:00:00Z"}' \
  --expiration 2026-01-01T00:00:00+00:00 \
  --output credential.json

# Issue an AML Screening credential (no expiration required)
vercre issue --issuer-did did:key:z6MkIssuer --subject did:key:z6Mk... \
  --type AMLScreeningCredential \
  --claims '{"sanctionsStatus":"clear","pepStatus":false,"adverseMedia":false,"riskScore":15,"screenedAt":"2025-01-01T00:00:00Z"}' \
  --output credential.json

# Issue an Accredited Investor credential (no expiration required)
vercre issue --issuer-did did:key:z6MkIssuer --subject did:key:z6Mk... \
  --type AccreditedInvestorCredential \
  --claims '{"accredited":true,"jurisdiction":"US-SEC","verificationMethod":"income","verifiedAt":"2025-01-01T00:00:00Z"}' \
  --output credential.json

Optional Issuer Metadata

Add issuer metadata and schema version to any credential:

vercre issue --issuer-did did:key:z6MkIssuer --subject did:key:z6Mk... \
  --type AMLScreeningCredential \
  --claims '{"sanctionsStatus":"clear","pepStatus":false,"adverseMedia":false,"riskScore":10,"screenedAt":"2025-01-01T00:00:00Z"}' \
  --schema-version 1.0.0 \
  --issuer-name "Acme Compliance" \
  --issuer-jurisdiction "US-SEC" \
  --issuer-license "LIC-001" \
  --output credential.json

When both --issuer-name and --issuer-jurisdiction are provided, the credential includes issuerMetadata and credentialStatus fields.

Verification, Status, Revocation, and DID Resolution

# Verify a credential (checks signature + on-chain status)
vercre verify --credential credential.json

# Skip on-chain check (signature only)
vercre verify --credential credential.json --no-chain

# Verify with policy constraints (minimum tier and reputation)
vercre verify --credential credential.json --min-tier REGULATED --min-reputation 60

# Check on-chain attestation status
vercre status --subject did:key:z6Mk... --credential credential.json

# Revoke an attestation (key looked up by DID from key store)
vercre revoke --issuer-did did:key:z6MkIssuer --credential credential.json

# Resolve a DID to its DID Document
vercre resolve did:key:z6Mk...

Attestation state is persisted to ~/.vercre/attestations.json, so verify, status, and revoke work across separate CLI invocations.

Trust Registry Management

# Register an issuer
vercre trust add --did did:key:z6Mk... --tier INSTITUTIONAL --name "Acme Compliance"

# Update an issuer's tier
vercre trust update --did did:key:z6Mk... --tier VERIFIED

# Update an issuer's operational status
vercre trust set-status --did did:key:z6Mk... --status SUSPENDED

# Stake tokens for an issuer
vercre trust stake --did did:key:z6Mk... --amount 1000

# Slash an issuer's stake (auto-suspends if below minimum)
vercre trust slash --did did:key:z6Mk... --amount 500 --reason "policy violation"

# List registered issuers (with status, stake, and reputation)
vercre trust list

# Check if an issuer can issue a specific credential type
vercre trust check --did did:key:z6Mk... --type KYCEnhancedCredential

# Remove an issuer
vercre trust remove --did did:key:z6Mk...

Reputation

# Check an issuer's reputation score
vercre reputation score --did did:key:z6Mk...

# View reputation event history
vercre reputation history --did did:key:z6Mk...

Disputes

# Open a dispute against a credential
vercre dispute open --credential-id urn:uuid:... --reporter did:key:z2 --issuer did:key:z1 --reason "fraudulent claims"

# Submit evidence to a dispute
vercre dispute evidence --id dispute-... --evidence "screenshot of discrepancy"

# Resolve a dispute (clear the issuer)
vercre dispute resolve --id dispute-... --clear

# Resolve a dispute (penalize the issuer)
vercre dispute resolve --id dispute-... --penalize --penalty-amount 50.0

# List all disputes (optionally filter by issuer)
vercre dispute list
vercre dispute list --issuer did:key:z1

Fees

# View the fee schedule
vercre fees schedule

# View fee history (optionally filter by payer)
vercre fees history
vercre fees history --payer did:key:z1

API Key Management

# Create a platform admin API key (trust decisions: tier, status, approve, stake, slash)
vercre apikey create --name "Platform Key" --role platform_admin

# Create an admin API key (issuer operations: issue, webhooks, verify-request)
vercre apikey create --name "My Admin Key" --role admin

# Create a client API key (verify, lookup, disputes only)
vercre apikey create --name "My Client Key" --role client

# List all keys (secrets are never shown again)
vercre apikey list

# Revoke a key
vercre apikey revoke --id key-a1b2c3d4...

Secret Rotation

# Generate a new random secret (does not apply it)
vercre rotate generate-secret

# Generate with custom length
vercre rotate generate-secret --length 128

# Rotate the encryption secret and re-encrypt all stored keys
vercre rotate encryption-secret --old-secret "current_secret_value" --new-secret "new_secret_value"

# Auto-generate new secret (printed to stdout)
vercre rotate encryption-secret --old-secret "current_secret_value"

# Also update the sops-encrypted secrets file
vercre rotate encryption-secret --old-secret "current_secret_value" --update-sops secrets.enc.json

Demo

A self-contained in-memory demo that showcases the full credential lifecycle without requiring a server, database, or external services:

python demo.py

The demo walks through a real-world scenario: a KYC provider onboards, issues a credential for a user, and three different platforms verify it instantly — demonstrating how one KYC check replaces three redundant ones.


Demo Frontend

A minimal browser-based console (frontend/) for testing the API interactively. No build step or dependencies — just HTML + vanilla JS.

# Start the API server with CORS enabled
VERCRE_CORS_ORIGINS="http://localhost:3001" vercre-server

# Serve the frontend
cd frontend && python3 -m http.server 3001

Open http://localhost:3001 to access tabs for onboarding, verification requests, credential issuance, verification, revocation, and status lookups. DID and API key are saved in sessionStorage after onboarding.

A quickstart.sh script is also included for automated end-to-end testing via curl.


Vercel Deployment

The project includes a vercel.json configuration for deploying the frontend as static files and the API as a Python serverless function:

  • Frontend routes (/) serve from frontend/
  • API routes (/v1/*, /docs, /openapi.json, /metrics) route to the Python backend

Required Environment Variables

Set these in the Vercel project dashboard (Settings → Environment Variables) to enable on-chain attestation anchoring:

Variable Value Purpose
VERCRE_EVM_RPC_URL https://sepolia.base.org Base Sepolia RPC endpoint
VERCRE_EVM_CONTRACT_ADDRESS 0x2c1982fa24ffc8f6d5e5f119171e4950400daec4 Deployed AttestationRegistry contract
VERCRE_EVM_PRIVATE_KEY (deployer private key hex) Signing key for on-chain transactions
VERCRE_KEY_ENCRYPTION_SECRET (generate with python -c "import secrets; print(secrets.token_urlsafe(32))") Fernet encryption for stored private keys

Without the EVM variables, the API falls back to file-backed attestation and on-chain proof (BaseScan links) will not appear in the demo.

# Or set via CLI:
vercel env add VERCRE_EVM_RPC_URL
vercel env add VERCRE_EVM_CONTRACT_ADDRESS
vercel env add VERCRE_EVM_PRIVATE_KEY
vercel env add VERCRE_KEY_ENCRYPTION_SECRET

API Documentation

A standalone API reference is available in API-Docs.md with detailed request/response examples for all endpoints, authentication flows, and credential type schemas.


Foundry Deployment

A deployment script is included at foundry/script/DeployAttestationRegistry.s.sol for deploying the AttestationRegistry contract:

cd foundry
forge script script/DeployAttestationRegistry.s.sol --rpc-url $RPC_URL --broadcast --private-key $PRIVATE_KEY

The script deploys the contract and logs the deployed address and owner.


Deployment Modes & Monetization

The Vercre SDK supports three deployment modes, with on-chain attestation anchoring as the monetization hook:

Self-Hosted (Free, Open Source)

pip install vercre
vercre-server  # Runs with file-backed attestation

Self-hosters get the full SDK: credential issuance, JWT signing, DID resolution, trust registry, revocation tracking, and cryptographic verification. All credentials are valid and cryptographically verifiable. Attestation is tracked locally (file-backed or YugabyteDB).

What you don't get: On-chain anchoring on the production AttestationRegistry contract. Self-hosted credentials are not verifiable by third parties via BaseScan.

Hosted API — Free Tier

The managed API at https://vercre.vectorguardlabs.com provides the same capabilities as self-hosted, plus managed infrastructure, monitoring, and a Stripe billing portal. Free-tier issuers (Issuer Starter plan) get:

  • 25 credential issuances/month
  • File-backed attestation (no on-chain proof)
  • 2 webhooks, 5 req/min rate limit

Hosted API — Paid Tier (On-Chain Anchoring)

Paid-tier issuers (Professional/Enterprise) unlock on-chain attestation anchoring on the production contract:

# 1. Onboard
curl -X POST https://vercre.vectorguardlabs.com/v1/onboard \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme Compliance"}'

# 2. Upgrade to paid plan
curl -X POST https://vercre.vectorguardlabs.com/v1/billing/subscribe \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plan": "issuer_professional"}'

# 3. Authorize on-chain (one-time, requires paid plan)
curl -X POST https://vercre.vectorguardlabs.com/v1/chain/authorize \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"issuerDid": "did:key:z6MkYourDID"}'

# 4. Issue credentials (now anchored on-chain automatically)
curl -X POST https://vercre.vectorguardlabs.com/v1/issue \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"issuerDid": "...", "subjectDid": "...", "credentialType": "KYCBasicCredential", ...}'
# Response includes transactionId: "0x..." (on-chain tx hash)

Why On-Chain Is the Monetization Hook

The production AttestationRegistry contract on Base is owned by Vector Guard Labs. Only the hosted API can call authorizeIssuer() on this contract. This means:

  • Self-hosters can issue valid credentials (JWTs with signatures) but cannot anchor them on the shared public registry
  • Paid hosted users get on-chain proof that any DeFi protocol, exchange, or compliance platform can verify by calling getAttestationStatus() on the contract
  • Verifiers can check attestation status for free (read-only on-chain call, no API key needed)

This creates natural demand: issuers who need their credentials to be verifiable by downstream consumers need the hosted API. Self-hosters still get a fully functional SDK for internal use, testing, and private deployments.

Comparison

Capability Self-Hosted Hosted (Free) Hosted (Paid)
Credential issuance (JWT) Yes Yes Yes
Cryptographic signature verification Yes Yes Yes
DID resolution (key, ethr, web) Yes Yes Yes
Trust registry Yes Yes Yes
File-backed / DB attestation Yes Yes Yes
Stripe billing & usage metering No Yes Yes
On-chain anchoring (production contract) No No Yes
BaseScan-verifiable proof No No Yes
Third-party on-chain verification No No Yes
SLA guarantee No No Yes (Enterprise)

On-Chain Authorization Endpoints

Method Path Auth Description
POST /v1/chain/authorize ADMIN / PLATFORM_ADMIN Authorize issuer on production contract (requires paid plan)
DELETE /v1/chain/authorize PLATFORM_ADMIN Revoke on-chain authorization
GET /v1/chain/authorization/{did} ADMIN+ Check on-chain auth status (free read)

See docs/BILLING.md for full billing documentation including plan details, overage pricing, and Stripe setup.


Testing

pytest -q

441 tests validate:

  • Key generation and serialization (Ed25519, secp256k1)
  • JWT signing and verification round-trips
  • Deterministic hashing (canonical JSON)
  • DID creation and resolution (all three methods)
  • Attestation protocol operations (anchor, revoke, status, expiration)
  • File-backed persistence (save/reload, cross-instance revocation, missing/empty files)
  • Schema validation (all 5 credential types, Literal enforcement, country uppercase, range checks)
  • Self-issuance rejection (issuer DID cannot equal subject DID)
  • Mandatory expiration enforcement for KYC types
  • Credential metadata pass-through (credentialStatus, issuerMetadata, schemaVersion)
  • Full lifecycle integration (issue → anchor → verify → revoke → verify-fails)
  • Composable credentials (KYC + AML + AccreditedInvestor on same subject)
  • CLI command parsing, file I/O, and error handling
  • Trust registry: tier/status management, staking, slashing, auto-suspension, file-backed persistence
  • Reputation engine: event recording, score computation, clamping, tier mapping, filtering, file persistence
  • Dispute manager: lifecycle transitions, evidence submission, resolution guards, file persistence
  • Fee manager: charging, treasury accumulation, history filtering, custom schedules, file persistence
  • Policy engine: tier ordering, confidence scoring, policy evaluation (tier/reputation/status checks)
  • API key manager: creation, validation, revocation, usage tracking, file-backed persistence
  • Webhook manager: registration, removal, listing, dispatch, file-backed persistence
  • REST API: auth enforcement (401/403), all 53 endpoints, three-tier role-based access control (CLIENT/ADMIN/PLATFORM_ADMIN), usage counter increments, self-serve API key creation, platform admin bootstrap/creation, onboarding flow, per-tier verification request validation and approval flow (VERIFIED/REGULATED/INSTITUTIONAL schemas, downgrade/upgrade validation, missing fields rejection, email format validation), platform admin route gating, webhook CRUD and dispatch
  • YugabyteDB (YSQL) backends: all 8 store implementations (trust, attestation, dispute, API keys, key store, reputation, fees, webhooks) with schema initialization and reset
  • Key rotation: store/rotate/get_all_keys lifecycle, multiple rotations, version tracking, active key selection, API endpoint auth enforcement
  • Rate limiting: Redis-backed sliding window with in-memory fallback, per-key independence, limit enforcement
  • Status list: bitstring operations (create/set/get/encode/decode), index allocation, revocation bit flipping, file-backed persistence, API endpoint serving, full issue→revoke→check lifecycle
  • Threat model: STRIDE threat catalog, asset inventory, severity filtering, mitigation status lifecycle, risk summaries, JSON persistence round-trip
  • Penetration testing: test case registration, result recording, category filtering, pass/fail summaries, default suite coverage across 7 OWASP categories
  • DID governance: method registration, duplicate rejection, status lifecycle (active/deprecated/suspended/deactivated), policy updates, DID validation, deactivation/recovery, recovery policy enforcement, audit log filtering
  • Key rotation procedures: policy-driven rotation, expiration checking, manual/emergency/scheduled rotation, rotation history, status reporting, multiple sequential rotations
  • Credential freshness: freshness heuristic computation (fresh/aging/stale/critical/expired/no_expiry), days-until-expiry calculation, API response inclusion
  • SD-JWT: selective disclosure creation, holder presentation with claim selection, verifier reconstruction, key binding JWT (KB-JWT) with audience/nonce validation, wrong-key rejection
  • Credential renewal: revoke-and-reissue lifecycle, new expiration, status list allocation, API endpoint auth

Smart Contract Tests (Foundry)

cd foundry && forge test

96 Foundry tests across 21 test suites validate the AttestationRegistry.sol contract:

Category Tests Description
Unit Test 14 Core functionality: anchor, revoke, status, ownership, events
Edge Case Test 12 Boundary conditions, unauthorized access, re-anchoring prevention
Stateless Fuzz Test 5 Random inputs for anchor, revoke, expiry, authorization
Stateful Fuzz Test 1 Multi-step sequences with state tracking
Property Test 4 Universal properties: issuer identity, timestamp ordering, irreversibility
Invariant Test 2 System-wide invariants: revoked ≤ anchored, owner ≠ zero
Assertion Test 4 Internal consistency of status returns
Static Analysis Test 4 Compile-time properties: view functions, public accessors, error selectors
Dynamic Analysis Test 5 Gas bounds, stress testing, reentrancy safety
Mutation Test 5 Boundary mutations that would break correctness
Formal Verification Test 4 Symbolic proofs: status range, zero-anchor semantics, auth requirement
Symbolic Execution Test 4 Path exploration: anchor→active, revoke→permanent, revoked beats expired
Economic Test 4 Gas cost bounds, griefing resistance, view call efficiency
Game Theory Test 4 Adversarial scenarios: cross-revocation, front-running, deauth timing
Advanced Fuzz Testing 2 Multi-issuer scenarios, ownership transfer sequences
Differential Fuzz Testing 1 Two-registry comparison for deterministic behavior
Composability Test 4 External verifier contracts, multi-registry verification
Integration Test 4 Full lifecycle, multi-credential, ownership continuity
EVM Opcode Test 5 SSTORE, TIMESTAMP, CALLER, REVERT, EXTCODESIZE
Calldata Test 4 Function selectors, low-level calls, empty calldata
Cross Chain Test 3 Dual-chain anchoring, independent revocation, hash consistency

Key Design Patterns

Tiered Privacy: KYC credentials come in three tiers. A DeFi protocol that only needs country verification requests KYCBasicCredential. A securities platform needing full identity requests KYCStandardCredential. Enhanced due diligence uses KYCEnhancedCredential. The subject never reveals more than the verifier requires.

Composable Verification: A verifier can require multiple credential types (e.g. KYC + AML + AccreditedInvestor) for a single subject. Each credential is independently issued, anchored, and revocable — revoking one doesn't affect others.

Deterministic Hashing: Credentials are hashed using sorted-key, no-whitespace JSON serialization. The same credential always produces the same hash regardless of field ordering.

Decoupled Verification: JWT signature verification and on-chain status checks are independent operations. A verifier can validate the cryptographic signature offline, then optionally check blockchain status for revocation/expiration.

Abstract Protocol Interface: The attestation protocol is defined as an async abstract base class. The in-memory implementation enables testing without infrastructure. The file-backed implementation persists attestation state to ~/.vercre/attestations.json for CLI use. The EVM implementation handles real blockchain transactions.

Issuer-Subject Separation: The SDK rejects self-issued credentials where the issuer and subject share the same DID. A credential must always be issued by one identity about a different identity. This prevents an entity from self-certifying its own compliance status.

Issuer-Only Revocation: Both the SDK and smart contract enforce that only the original issuing address can revoke an attestation — preventing unauthorized revocation attacks.

Economic Staking: Issuers back their tier with staked tokens. Slashing below the tier minimum automatically suspends the issuer, creating a direct economic incentive against issuing fraudulent credentials.

Reputation as Signal: Reputation scores aggregate weighted events over time — credential issuance, revocations, dispute outcomes, and verifier feedback. Verifiers can set minimum reputation thresholds as part of their policy, creating a feedback loop that rewards trustworthy behavior.

Policy-Based Verification: Verifiers define declarative policies (minimum tier, minimum reputation, non-revoked status) rather than writing custom verification logic. The policy engine evaluates these constraints and computes a confidence score that combines credential validity, issuer tier, and reputation into a single 0.0-1.0 metric.

Dispute Governance: Disputes follow a strict lifecycle with guarded state transitions. Evidence can only be submitted to unresolved disputes, and disputes can only be resolved once — preventing after-the-fact manipulation.

Custodial Key Management: Private keys never leave the server. On onboarding, the key pair is generated and stored server-side with the private key encrypted at rest using Fernet (AES-128-CBC with HMAC-SHA256). The encryption key is derived from the VERCRE_KEY_ENCRYPTION_SECRET environment variable via HKDF-SHA256 with an application-specific salt and info parameter. The system raises a hard error if this secret is unset (unless VERCRE_ENV=development is explicitly configured). API responses and CLI output include only the public key and DID. Credential issuance and revocation reference the issuer by DID — the server looks up the encrypted private key, decrypts it in memory using mutable bytearray (which can be explicitly zeroed after signing), signs the credential, and discards the plaintext. This eliminates the risk of private key exposure through API responses, log files, or client-side storage. Key rotation generates a new key pair while preserving old keys — existing credentials remain verifiable against the key that signed them, while new issuance uses the latest active key.

API Key Gating: Every non-health endpoint requires an API key in the X-API-Key header. Keys are SHA-256 hashed before storage so a database compromise does not leak raw secrets. Key validation uses constant-time comparison (hmac.compare_digest) to prevent timing attacks. Three roles (CLIENT, ADMIN, PLATFORM_ADMIN) form a hierarchy — clients can verify and look up issuers, admins can issue credentials and manage webhooks, and platform admins control trust decisions (tier changes, approval, staking, slashing). Onboarding creates ADMIN keys so issuers cannot self-upgrade. Usage counters on each key enable metering and billing — counters only increment after role authorization succeeds. Failed authentication attempts are rate-limited (10/minute per IP) to prevent brute-force attacks.


Real-World Example

Scenario: A compliance provider (Chainalysis, Securitize) issues tiered KYC credentials to institutional investors. Multiple DeFi protocols verify credentials at the tier they require without redundant KYC checks.

Step Actor Operation
1 Compliance provider Calls POST /v1/onboard — gets ADMIN key + public key + DID, registered as UNVERIFIED (private key stored server-side)
2 Compliance provider Upgrades to paid plan via POST /v1/billing/subscribe with plan: issuer_enterprise
3 Compliance provider Calls POST /v1/chain/authorize — authorizes DID on the production on-chain contract
4 Compliance provider Calls POST /v1/issuers/{did}/verify-request with requestedTier: INSTITUTIONAL and all required fields (FDIC license, jurisdiction, regulatory body, charter number, SOC2 status, etc.)
5 Platform admin Reviews verification request, calls POST /v1/issuers/{did}/approve with tier INSTITUTIONAL
6 Platform admin Stakes 1000 tokens for the provider via POST /v1/issuers/{did}/stake
7 Investor Generates did:key, submits KYC documents to provider
8 Compliance provider Issues KYCStandardCredential + AMLScreeningCredential via POST /v1/issue — hashes anchored on-chain (verifiable on BaseScan), usage metered via Stripe
9 Investor Presents credentials to DeFi protocol
10 DeFi protocol Calls POST /v1/verify with policy (min tier: REGULATED, min reputation: 60), checks confidence score
11 Compliance provider Detects sanctions match, revokes AML attestation on-chain
12 DeFi protocol Re-checks AML status = REVOKED, restricts access (KYC still valid)
13 Reporter Opens dispute against revoked credential, submits evidence
14 Platform admin Resolves dispute — penalizes issuer, slashes stake, reputation drops

Project Structure

VERIFIABLE-CREDENTIAL/
├── pyproject.toml
├── requirements.txt
├── Makefile
├── .sops.yaml
├── secrets.json.example
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── vercel.json
├── app.py
├── demo.py
├── API-Docs.md
├── .github/
│   └── workflows/
│       ├── container-scan.yml
│       └── dependency-scan.yml
├── config/
│   ├── caddy/
│   │   └── Caddyfile
│   └── redis/
│       ├── redis.conf
│       └── users.acl
├── contracts/
│   └── AttestationRegistry.sol
├── docs/
│   ├── AUDIT_LOGGING.md
│   ├── AUTHENTICATION.md
│   ├── DATABASE_HARDENING.md
│   ├── LOAD_TESTING.md
│   ├── RATE_LIMITING.md
│   ├── REDIS_HARDENING.md
│   ├── SECRET_MANAGEMENT.md
│   └── SECRET_ROTATION.md
├── frontend/
│   ├── index.html
│   ├── app.js
│   ├── quickstart.sh
│   ├── generate_sandbox_pdf.py
│   ├── vercre_sandbox_quickstart.pdf
│   └── README.md
├── foundry/
│   ├── foundry.toml
│   ├── src/
│   │   └── AttestationRegistry.sol
│   ├── script/
│   │   └── DeployAttestationRegistry.s.sol
│   └── test/
│       ├── AttestationRegistry.t.sol
│       ├── FuzzTests.t.sol
│       ├── InvariantTests.t.sol
│       ├── AdvancedTests.t.sol
│       ├── EconomicAndGameTests.t.sol
│       └── IntegrationAndLowLevel.t.sol
├── loadtests/
│   └── locustfile.py
├── monitoring/
│   ├── prometheus.yml
│   └── grafana/
│       └── provisioning/
│           ├── datasources/
│           │   └── prometheus.yml
│           └── dashboards/
│               ├── dashboard.yml
│               └── vercre.json
├── scripts/
│   ├── generate-db-certs.sh
│   ├── generate-redis-certs.sh
│   ├── yb-backup.sh
│   ├── yb-restore.sh
│   └── rotate-infra-secrets.sh
├── src/vercre/
│   ├── __init__.py
│   ├── issuer.py
│   ├── verifier.py
│   ├── api.py
│   ├── api_keys.py
│   ├── key_store.py
│   ├── metrics.py
│   ├── rate_limit.py
│   ├── webhook.py
│   ├── status_list.py
│   ├── secrets.py
│   ├── cli.py
│   ├── trust.py
│   ├── reputation.py
│   ├── dispute.py
│   ├── fees.py
│   ├── policy.py
│   ├── credential_store.py
│   ├── audit.py
│   ├── secret_rotation.py
│   ├── crypto/
│   │   ├── keys.py
│   │   ├── jwt.py
│   │   ├── hashing.py
│   │   ├── encryption.py
│   │   └── sd_jwt.py
│   ├── did/
│   │   ├── resolver.py
│   │   ├── did_key.py
│   │   ├── did_ethr.py
│   │   ├── did_web.py
│   │   ├── governance.py
│   │   └── rotation.py
│   ├── chain/
│   │   ├── protocol.py
│   │   ├── registry.py
│   │   ├── file_registry.py
│   │   ├── evm.py
│   │   ├── evm_lite.py
│   │   ├── circuit_breaker.py
│   │   └── anchor_gateway.py
│   ├── models/
│   │   ├── credential.py
│   │   ├── presentation.py
│   │   ├── did_document.py
│   │   ├── schemas.py
│   │   ├── status.py
│   │   ├── trust.py
│   │   ├── reputation.py
│   │   ├── dispute.py
│   │   ├── fee.py
│   │   ├── policy.py
│   │   ├── api.py
│   │   ├── api_key.py
│   │   ├── webhook.py
│   │   └── status_list.py
│   ├── security/
│   │   ├── threat_model.py
│   │   └── pentest.py
│   └── pg/
│       ├── pool.py
│       ├── trust.py
│       ├── attestation.py
│       ├── dispute.py
│       ├── api_keys.py
│       ├── key_store.py
│       ├── reputation.py
│       ├── fees.py
│       ├── webhook.py
│       └── credential_store.py
└── tests/
    ├── test_lifecycle.py
    ├── test_issuer.py
    ├── test_verifier.py
    ├── test_api.py
    ├── test_api_keys.py
    ├── test_cli.py
    ├── test_trust.py
    ├── test_reputation.py
    ├── test_dispute.py
    ├── test_fees.py
    ├── test_policy.py
    ├── test_key_rotation.py
    ├── test_status_list.py
    ├── crypto/
    ├── did/
    ├── chain/
    ├── models/
    ├── pg/
    └── security/

Dependencies

Package Version Purpose
pydantic 2.13.3 Data validation and serialization
PyJWT[crypto] 2.12.1 JWT signing and verification
cryptography 47.0.0 Ed25519 and secp256k1 key operations
click 8.3.3 CLI framework
httpx 0.28.1 Async HTTP client (did:web resolution)
multiformats 0.3.1.post4 Multibase/multicodec encoding (did:key)
fastapi 0.136.1 REST API framework
uvicorn 0.46.0 ASGI server for the API
redis[hiredis] 5.2.1 Redis-backed rate limiting
pycryptodomex 3.23.0 Additional cryptographic primitives (SD-JWT disclosure hashing)
ecdsa 0.19.2 ECDSA signing and verification (secp256k1 support)
prometheus_client 0.22.1 Prometheus metrics instrumentation
web3 7.6.0 (optional) EVM attestation protocol integration
psycopg[pool] 3.2.6 (optional) YugabyteDB (YSQL) backend for all stores
locust 2.32.4 (optional) Load testing framework

Security Hardening

The SDK implements defense-in-depth across all layers:

Cryptographic Security

Control Implementation
Key derivation HKDF-SHA256 with application-specific salt (not raw SHA-256)
Key-at-rest encryption Fernet (AES-128-CBC + HMAC-SHA256); hard failure if secret unset
Memory protection Private keys use mutable bytearray with explicit zero() after signing
JWT algorithm pinning Algorithm locked to key type — no caller-overridable algorithm list
API key comparison Constant-time hmac.compare_digest (prevents timing attacks)
Key serialization KeyPair.to_dict() excludes private key material

API Security

Control Implementation
Rate limiting Redis-backed sliding window on onboarding (3/hr), API key creation (5/hr), subject identity (5/hr), auth failures (10/min), reputation feedback (1/hr per issuer)
DID authorization ADMIN keys bound to issuer DID — cannot operate on other issuers' data
DID format validation Regex validation (did:<method>:<id>) on all DID input fields
Input length limits max_length constraints on all free-text request fields
Security headers X-Content-Type-Options, X-Frame-Options, Cache-Control, HSTS on all responses
Metrics protection /metrics requires bearer token or platform admin API key
Webhook SSRF prevention HTTPS-only, blocks RFC 1918/link-local/loopback/metadata IPs
Webhook scoping Webhooks bound to issuer DID — issuers only see their own events
Bootstrap race prevention asyncio.Lock serializes platform admin key creation
Proxy-aware IP extraction X-Forwarded-For parsing with configurable trusted proxy CIDRs (VERCRE_TRUSTED_PROXIES)

Smart Contract Security

Control Implementation
Ownership transfer Two-step transferOwnership + acceptOwnership (prevents accidental loss)
Revocation authorization onlyAuthorizedIssuer modifier — deauthorized issuers cannot revoke
Re-anchoring prevention Once anchored, a credential hash can never be re-anchored (even after revocation)

Infrastructure Security

Control Implementation
Secret management sops + age encryption; secrets decrypted at startup, never stored in plaintext on disk in production
No hardcoded secrets All credentials in secrets.enc.json (encrypted, safe to commit) or .env (gitignored)
Localhost-only ports YugabyteDB and Redis bound to 127.0.0.1
Redis authentication ACL-based auth with per-user permissions (users.acl); dangerous commands (FLUSHALL, FLUSHDB, DEBUG) disabled
Redis TLS Mutual TLS on port 6379 (plaintext port disabled); CA-signed certificates with optional client cert verification
YugabyteDB TLS SSL enforced via _enforce_ssl() in production — connection strings upgraded to sslmode=require at minimum; development mode allows sslmode=disable
Database backup/restore scripts/yb-backup.sh (ysql_dump export) and scripts/yb-restore.sh (ysqlsh import)
Infrastructure secret rotation scripts/rotate-infra-secrets.sh — zero-downtime YugabyteDB/Redis password rotation
Container image scanning Trivy CI pipeline (.github/workflows/container-scan.yml) blocks CRITICAL/HIGH CVEs; SARIF upload to GitHub Security
Dependency vulnerability scanning pip-audit + Trivy filesystem scan (.github/workflows/dependency-scan.yml) on push/PR/weekly schedule; blocks CRITICAL/HIGH CVEs
Container resource limits Memory and CPU caps on all containers (API: 512M/1CPU, DB: 256M/0.5CPU, Redis: 128M/0.25CPU)
Pinned dependencies All dependencies (including optional) pinned to exact versions
File permissions Key store file set to chmod 0o600 (owner-only read)

Business Logic Guards

Control Implementation
Negative amounts Fee charges and dispute penalties reject negative values
Evidence cap Maximum 20 evidence submissions per dispute
Webhook cap Maximum 10 webhooks per issuer
Atomic counters Verification count protected by asyncio.Lock
Usage tracking Counter increments only after role authorization succeeds
Credential scoping Credential lookups filtered by issuer DID (prevents IDOR)

See also: docs/AUDIT_LOGGING.md for tamper-evident audit trail details, docs/DATABASE_HARDENING.md for database hardening, docs/REDIS_HARDENING.md for Redis security configuration, and docs/SECRET_ROTATION.md for secret rotation policy.


Deployment Readiness

What's ready today

The VERCRE SDK, REST API, and demo are ready for evaluation, integration testing, and pilot programs. The following are fully implemented and tested:

Capability Status
W3C Verifiable Credentials issuance and verification Production-tested (441 unit/integration tests)
On-chain attestation anchoring (Base Sepolia) Deployed and operational (96 contract tests)
Trust registry with tiered issuer management Complete
DID method governance and key rotation Complete
W3C StatusList2021 revocation publishing Complete
Secret management (sops + age encryption) Complete (docs/SECRET_MANAGEMENT.md)
STRIDE threat model + penetration testing framework Complete
Rate limiting, audit logging, security hardening Complete

What's needed for regulated production deployment

Deploying VERCRE to issue legally binding compliance credentials in a regulated environment requires additional infrastructure that is standard for any financial services product:

  • Smart contract security audit — formal verification by a third-party auditor
  • Hardware Security Module (HSM) — tamper-resistant hardware key storage replacing the current software-only Fernet encryption
  • Licensed KYC/AML provider integration — Chainalysis, Securitize, Plaid, or equivalent
  • OFAC sanctions screening — real-time screening and ongoing monitoring
  • Regulatory registration — GDPR data controller obligations, state money transmitter licenses as applicable

These are implementation steps for production deployment, not limitations of the SDK architecture. The SDK is designed to integrate with HSMs, licensed providers, and regulatory frameworks.


Performance & Scalability

Async YugabyteDB Backend

The YugabyteDB (YSQL) backend is fully asynchronous, preventing event loop blocking in the FastAPI application:

  • Connection Pool: Uses AsyncConnectionPool from psycopg_pool with extended reconnect timeout (600s) to handle YugabyteDB tablet leader elections
  • Transient Retry Logic: Automatic retry with exponential backoff for distributed-system transient failures (serialization conflicts, tablet leader elections, connection interruptions)
  • All Operations Async: 45+ async methods across 8 store implementations
  • No Event Loop Blocking: Database I/O doesn't block concurrent request handling
  • Better Throughput: Improved performance under concurrent load with distributed consistency

What this means:

  • ✅ Faster response times when multiple requests arrive simultaneously
  • ✅ Better scalability for production deployments
  • ✅ No changes to API surface — fully backward compatible

Verify async performance:

# Test concurrent requests (no blocking)
ab -n 1000 -c 10 http://localhost:8000/v1/health

Rate Limiting

To prevent abuse and ensure fair resource allocation, rate limiting is applied to sensitive endpoints using Redis-backed sliding window algorithm:

Endpoint Limit Window Tracking
/v1/onboard 3 requests 1 hour Per IP address
/v1/identities/subject 5 requests 1 hour Per IP address

Rate Limit Algorithm:

  • Redis sorted sets — sliding window with atomic operations, survives restarts, works across multiple instances
  • IP-based — tracked by request.client.host
  • Automatic fallback — if Redis is unavailable, falls back to in-memory rate limiting (logs a warning once)

Configuration: Rate limits are configured in src/vercre/api.py:

app.state.rate_limiter = RateLimiter(
    redis_url=os.environ.get("VERCRE_REDIS_URL", "redis://localhost:6379"),
)
app.state.onboard_rate_limit_rule = RateLimitRule(
    max_requests=3,      # Change this
    window_seconds=3600  # Or this (seconds)
)

Environment variable: Set VERCRE_REDIS_URL to point to your Redis instance (default: redis://localhost:6379).

See also: docs/RATE_LIMITING.md for detailed rate limiting documentation.

Key Rotation

Signing keys can be rotated without invalidating existing credentials. Old keys are preserved so previously-signed credentials remain verifiable.

# Rotate a key (generates new key pair, deactivates old one)
curl -X POST http://localhost:8000/v1/keys/did:key:z6MkIssuer/rotate \
  -H "X-API-Key: $ADMIN_KEY"
# Returns: message, did, newPublicKey, keyType

# List all key versions
curl http://localhost:8000/v1/keys/did:key:z6MkIssuer/versions \
  -H "X-API-Key: $ADMIN_KEY"
# Returns: [{version, active, publicKey, keyType, createdAt, rotatedAt}, ...]

How it works:

  • Each DID has a list of versioned keys (v1, v2, v3...)
  • Only the latest key is marked active and used for new credential issuance
  • Old keys remain in storage with rotated_at timestamps
  • Verification can use any key version that was active when the credential was signed

Credential Freshness

The verification response includes a freshness heuristic to help integrators decide when to request credential renewal:

freshness daysUntilExpiry Meaning
fresh >90 No action needed
aging 30–90 Consider renewal
stale 7–30 Renewal recommended
critical 0–7 Renewal urgent
expired 0 Credential expired
no_expiry null No expiration set

Credential Renewal

Credentials can be renewed in a single API call — the old credential is revoked and a new one is issued with the same claims but a new expiration:

curl -X POST http://localhost:8000/v1/credentials/renew \
  -H "X-API-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"credential": {...}, "issuerDid": "did:key:z6MkIssuer", "newExpiration": "2027-06-01T00:00:00Z"}'

Circuit Breaker (EVM)

The EVM attestation protocol is protected by a circuit breaker that prevents cascading failures when the blockchain node is unavailable:

  • Closed (normal): requests pass through, failures counted
  • Open (after 5 failures): all requests immediately rejected with CircuitOpenError
  • Half-open (after 30s recovery timeout): one test request allowed through
  • Success in half-open state resets to closed; failure re-opens

Webhook Retry

Webhook dispatch uses exponential backoff instead of fire-and-forget:

Attempt Delay Action
1 0s Initial delivery
2 1s First retry (on 5xx or connection error)
3 5s Second retry
4 30s Final retry — gives up after this

HTTP 4xx responses are treated as terminal (no retry).

Per-API-Key Rate Limiting

Sensitive endpoints (issue, verify, revoke) enforce per-API-key rate limits in addition to per-IP onboarding limits:

Endpoint Limit Window Tracking
POST /v1/issue 60 requests 1 minute Per API key
POST /v1/verify 60 requests 1 minute Per API key
POST /v1/credentials/revoke 60 requests 1 minute Per API key
POST /v1/onboard 3 requests 1 hour Per IP address

Adaptive Rate Limiting

Rate limits automatically scale down when the server is under load, preventing cascading failures during traffic spikes:

  • Monitors p95 latency across a sliding window of recent requests
  • Scales down when p95 exceeds the high threshold (default: 1.0s) — effective limits are multiplied by a scale factor (default: 0.5×)
  • Restores when p95 drops below the low threshold (default: 0.3s)
  • Prometheus metrics track adaptive state (rate_limit_adaptive_scaled_down, rate_limit_p95_latency_seconds)

Configuration (environment variables):

Variable Default Description
VERCRE_ADAPTIVE_RATE_LIMIT 1 Set to 0 to disable adaptive scaling
VERCRE_ADAPTIVE_LATENCY_HIGH 1.0 p95 seconds threshold to trigger reduction
VERCRE_ADAPTIVE_LATENCY_LOW 0.3 p95 seconds threshold to restore limits
VERCRE_ADAPTIVE_SCALE_FACTOR 0.5 Multiplier applied to max_requests when under load
VERCRE_ADAPTIVE_WINDOW_SIZE 100 Number of latency samples for p95 calculation

All rate limit rules are also configurable via environment variables (e.g. VERCRE_RATE_LIMIT_ONBOARD_MAX, VERCRE_RATE_LIMIT_API_KEY_MAX).

SD-JWT (Selective Disclosure)

The SDK supports SD-JWT for privacy-preserving credential presentation:

from vercre.crypto.sd_jwt import create_sd_jwt, present_sd_jwt, verify_sd_jwt

# Issuer: create SD-JWT with selectively disclosable claims
sd_jwt, disclosures = create_sd_jwt(
    payload={"iss": issuer_did, "sub": holder_did, "name": "Alice", "age": 30, "country": "US"},
    key_pair=issuer_kp,
    sd_claims=["name", "age", "country"],  # These can be selectively revealed
    holder_did=holder_did,  # Enables key binding
)

# Holder: present only age (with key binding proof)
presentation = present_sd_jwt(
    sd_jwt, disclosures, ["age"],
    holder_key_pair=holder_kp,
    audience="verifier.example.com",
    nonce="challenge123",
)

# Verifier: verify signature + reconstruct only disclosed claims + check key binding
claims = verify_sd_jwt(
    presentation, issuer_kp.public_key_bytes, KeyType.ED25519,
    holder_public_key_bytes=holder_kp.public_key_bytes,
    holder_key_type=KeyType.ED25519,
    expected_audience="verifier.example.com",
    expected_nonce="challenge123",
)
# claims = {"iss": "did:key:...", "sub": "did:key:...", "age": 30}
# "name" and "country" are NOT revealed

Key Binding JWT (KB-JWT): When holder_key_pair is provided, the holder signs a short JWT proving possession of the private key referenced in the SD-JWT's cnf claim. This prevents replay attacks — a stolen SD-JWT cannot be presented without the holder's private key.

Monitoring (Prometheus + Grafana)

The API exposes Prometheus metrics at /metrics for observability:

Metric Type Description
http_requests_total Counter Total HTTP requests (labels: method, endpoint, status_code)
http_request_duration_seconds Histogram Request latency (labels: method, endpoint)
credentials_issued_total Counter Total credentials issued
credentials_revoked_total Counter Total credentials revoked
verifications_total Counter Total verifications (labels: result=passed|failed)
active_issuers_gauge Gauge Number of active issuers
key_rotations_total Counter Total key rotations performed
rate_limit_hits_total Counter Total rate limit rejections

Accessing metrics:

curl http://localhost:8000/metrics

Grafana dashboard is auto-provisioned at http://localhost:3000 (admin/admin) with panels for request rate, p95 latency, credential operations, verification results, rate limit hits, and key rotations.

See: Running with Docker for the full monitoring stack.

Load Testing

The project includes a Locust load test suite (loadtests/locustfile.py) that validates async database pool sizing, connection limits, and API throughput under concurrent load.

# Install load test dependencies
uv pip install -e ".[loadtest]"

# Quick headless run (50 users, 60s)
locust -f loadtests/locustfile.py --headless -u 50 -r 10 --run-time 60s

# Web UI (http://localhost:8089)
locust -f loadtests/locustfile.py

# Stress test pool limits (exceed max_size=10)
locust -f loadtests/locustfile.py --headless -u 100 -r 20 --run-time 120s

Environment variables:

  • VERCRE_BASE_URL: API base URL (default: http://localhost:8000)

The test simulates typical API consumers: onboarding, issuing credentials, verifying, checking status, and listing issuers. Each virtual user onboards to get an API key, then exercises authenticated endpoints.

See: docs/LOAD_TESTING.md for detailed load testing documentation.


Troubleshooting

Common Errors

Rate Limiting (429 Too Many Requests)

Error:

{
  "detail": "Rate limit exceeded. Try again in 3600 seconds."
}

Cause: Exceeded onboarding rate limit (3 requests per hour per IP)

Solutions:

  • Wait 1 hour before trying again from the same IP
  • Use a different IP address
  • Contact platform admin for manual issuer registration via POST /v1/issuers

Dispute Authorization (403 Forbidden)

Error:

{
  "detail": "API key must be bound to a DID to file disputes"
}

Cause: Attempting to file dispute with unbound CLIENT key

Solution: Create a new CLIENT key with DID binding:

curl -X POST http://localhost:8000/v1/api-keys \
  -H "Content-Type: application/json" \
  -d '{"name":"Dispute Reporter","did":"did:key:z6MkYourDID"}'

Error:

{
  "detail": "API key not authorized to file disputes for this DID"
}

Cause: API key's issuer_did doesn't match reporter_did in request

Solution: Ensure reporter_did matches the DID used when creating the CLIENT key

Credential Issuance (403 Forbidden)

Error:

{
  "detail": "API key is not authorized for this issuer DID"
}

Cause: Attempting to issue credentials with wrong ADMIN key

Solution: Use the ADMIN key created during onboarding for that specific issuer, or use a PLATFORM_ADMIN key


License

MIT License — See LICENSE file for details.


Built with ♥️ by Pavon Dunbar

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vercre-0.1.0.tar.gz (4.6 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vercre-0.1.0-py3-none-any.whl (209.2 kB view details)

Uploaded Python 3

File details

Details for the file vercre-0.1.0.tar.gz.

File metadata

  • Download URL: vercre-0.1.0.tar.gz
  • Upload date:
  • Size: 4.6 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for vercre-0.1.0.tar.gz
Algorithm Hash digest
SHA256 af6a35600f233ec11a52c552a2c3d91b1972b1a389963357870eda3971aa0310
MD5 ebd3b19a72e6a24d8d7ca77f5127cb12
BLAKE2b-256 40b416724e8334a3b35994ef96b05a857842c6a2cd2e46c7fe22bcdf560a1642

See more details on using hashes here.

File details

Details for the file vercre-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: vercre-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 209.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for vercre-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8c16079a898ec04e12b8636bc103dba4540c1d6b5bdc95c8ed89c7067ebc35f9
MD5 fd318901544d812f020afc140a4a6902
BLAKE2b-256 ee1d0750ad7ab51ba1ad5f831df320208d6cb20f88bfd5f27394a5754cf58f7d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page