Metadata-Version: 2.4
Name: vercre
Version: 0.1.0
Summary: Credential issuance and verification infrastructure with on-chain attestation anchoring
Project-URL: Homepage, https://vercre.vectorguardlabs.com
Project-URL: Documentation, https://vercre.vectorguardlabs.com/docs
Project-URL: Repository, https://github.com/pavondunbar/Verified-Credentials-SDK
Project-URL: Issues, https://github.com/pavondunbar/Verified-Credentials-SDK/issues
Project-URL: Changelog, https://github.com/pavondunbar/Verified-Credentials-SDK/releases
Author-email: Pavon Dunbar <pavon@vectorguardlabs.com>
License-Expression: MIT
License-File: LICENSE
Keywords: aml,attestation,blockchain,credential-issuance,credential-verification,decentralized-identity,did,jwt,kyc,sd-jwt,selective-disclosure,trust-registry,verifiable-credentials,w3c
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: click==8.3.3
Requires-Dist: cryptography==47.0.0
Requires-Dist: ecdsa==0.19.2
Requires-Dist: fastapi==0.136.1
Requires-Dist: httpx==0.28.1
Requires-Dist: multiformats==0.3.1.post4
Requires-Dist: prometheus-client==0.22.1
Requires-Dist: pycryptodomex==3.23.0
Requires-Dist: pydantic==2.13.3
Requires-Dist: pyjwt[crypto]==2.12.1
Requires-Dist: redis[hiredis]==5.2.1
Requires-Dist: stripe==11.4.1
Requires-Dist: uvicorn==0.46.0
Provides-Extra: dev
Requires-Dist: hypothesis==6.155.1; extra == 'dev'
Requires-Dist: mutmut==3.5.0; extra == 'dev'
Requires-Dist: pip-audit==2.10.1; extra == 'dev'
Requires-Dist: pytest-asyncio==1.3.0; extra == 'dev'
Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
Requires-Dist: pytest==8.3.5; extra == 'dev'
Requires-Dist: ruff==0.15.12; extra == 'dev'
Requires-Dist: ty==0.0.49; extra == 'dev'
Provides-Extra: evm
Requires-Dist: web3==7.6.0; extra == 'evm'
Provides-Extra: loadtest
Requires-Dist: locust==2.32.4; extra == 'loadtest'
Provides-Extra: pg
Requires-Dist: alembic==1.15.2; extra == 'pg'
Requires-Dist: psycopg[binary,pool]==3.2.6; extra == 'pg'
Requires-Dist: sqlalchemy==2.0.41; extra == 'pg'
Provides-Extra: tracing
Requires-Dist: opentelemetry-api==1.33.0; extra == 'tracing'
Requires-Dist: opentelemetry-exporter-otlp==1.33.0; extra == 'tracing'
Requires-Dist: opentelemetry-instrumentation-fastapi==0.54b0; extra == 'tracing'
Requires-Dist: opentelemetry-instrumentation-httpx==0.54b0; extra == 'tracing'
Requires-Dist: opentelemetry-sdk==1.33.0; extra == 'tracing'
Description-Content-Type: text/markdown

# Verified Credentials Issuance & Verification SDK

<img width="1672" height="941" alt="verified-credentials" src="https://github.com/user-attachments/assets/419fb2fe-39b6-49a5-9578-c794284b2536" />

## 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](#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: **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:

```json
{
  "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`](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

```bash
# 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."}`

```bash

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:

```bash
# 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.

```bash
# 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):

```bash
# 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

```bash
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

```bash
# 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:

```bash
pip install "vercre[evm]"
```

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

```bash
pip install "vercre[pg]"
```

### CLI Usage

```bash
# 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.

```bash
# 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:

```bash
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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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:

```bash
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.

```bash
# 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.

```bash
# 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`](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:

```bash
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)

```bash
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:

```bash
# 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`](docs/BILLING.md) for full billing documentation including plan details, overage pricing, and Stripe setup.

---

## Testing

```bash
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)

```bash
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`](docs/AUDIT_LOGGING.md) for tamper-evident audit trail details, [`docs/DATABASE_HARDENING.md`](docs/DATABASE_HARDENING.md) for database hardening, [`docs/REDIS_HARDENING.md`](docs/REDIS_HARDENING.md) for Redis security configuration, and [`docs/SECRET_ROTATION.md`](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](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:**
```bash
# 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`:
```python
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`](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.

```bash
# 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:

```bash
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:

```python
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:**
```bash
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](#running-with-docker) for the full monitoring stack.

### Load Testing

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

```bash
# 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`](docs/LOAD_TESTING.md) for detailed load testing documentation.

---

## Troubleshooting

### Common Errors

#### Rate Limiting (429 Too Many Requests)

**Error:**
```json
{
  "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:**
```json
{
  "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:
```bash
curl -X POST http://localhost:8000/v1/api-keys \
  -H "Content-Type: application/json" \
  -d '{"name":"Dispute Reporter","did":"did:key:z6MkYourDID"}'
```

**Error:**
```json
{
  "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:**
```json
{
  "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](LICENSE) file for details.

---

*Built with ♥️ by [Pavon Dunbar](https://linktr.ee/pavondunbar)*
