Credential issuance and verification infrastructure with on-chain attestation anchoring
Project description
Verified Credentials Issuance & Verification SDK
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, andschemaVersionfields 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
IssuerMismatchrevert - 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
kidheader matching — verifier matches the JWT'skidclaim 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: OPEN → EVIDENCE_SUBMITTED → RESOLVED_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 RequestswithRetry-After: 3600header - 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
UNVERIFIEDtier is rejected (422) - Approving to
UNVERIFIEDtier 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
complianceOfficerEmailmust 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:
ed25519produces smaller keys (32 bytes), fixed-size signatures (64 bytes), and deterministic signing (no random nonce). Works withdid:keyanddid:web.secp256k1is the curve used by Ethereum and Bitcoin. Required fordid:ethrbecause Ethereum addresses are derived from secp256k1 public keys via Keccak-256 hashing. Works withdid:keyanddid: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 fromfrontend/ - 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
AsyncConnectionPoolfrompsycopg_poolwith 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
activeand used for new credential issuance - Old keys remain in storage with
rotated_attimestamps - 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af6a35600f233ec11a52c552a2c3d91b1972b1a389963357870eda3971aa0310
|
|
| MD5 |
ebd3b19a72e6a24d8d7ca77f5127cb12
|
|
| BLAKE2b-256 |
40b416724e8334a3b35994ef96b05a857842c6a2cd2e46c7fe22bcdf560a1642
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c16079a898ec04e12b8636bc103dba4540c1d6b5bdc95c8ed89c7067ebc35f9
|
|
| MD5 |
fd318901544d812f020afc140a4a6902
|
|
| BLAKE2b-256 |
ee1d0750ad7ab51ba1ad5f831df320208d6cb20f88bfd5f27394a5754cf58f7d
|