You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Star0 (0)You must be signed in to star a repository
About
A complete and secure modern identity API for Web3 credentials management by impementing DID, VC, and ZKP. You can use this API for issuing, sharing, and verifying tamper-proof digital credentials — degrees, certificates, badges — where users control exactly what they share.
A complete and secure modern identity API for Web3 credentials management by implementing DID, VC, and ZKP. You can use this API for issuing, sharing, and verifying tamper-proof digital credentials — degrees, certificates, badges — where users control exactly what they share.
Built on W3C Open Standards
Unlike a proprietary credential system, this API is built entirely on open internet standards published by the World Wide Web Consortium (W3C) — the same body that standardises HTML and the web itself. That means credentials issued here are interoperable: any compliant system worldwide can read and verify them, with no vendor lock-in.
Defines how signatures are embedded in credentials, ensuring long-term verifiability without a proprietary format
Why this matters for developers: You get a credential system that speaks the same language as emerging digital identity wallets, government ID programmes, and university transcript platforms — without writing any cryptography yourself.
Overview
This API enables four actor roles to participate in a full decentralized identity ecosystem:
Role
Description
Subject
Holds credentials and creates selective-disclosure presentations
Issuer
Signs and issues Verifiable Credentials to subjects
Verifier
Verifies presented credentials and their proofs
Attester
Acts as a trust registry, vouching for issuer authority
Key Capabilities
Email/password accounts — sign up and log in; a did:key is auto-generated per account (v1.2)
Organizations — group accounts under a shared institutional DID (v1.2)
did:key — self-sovereign DID generation (Ed25519 + BLS12-381 G2 keys)
W3C VC Data Model 2.0 — DataIntegrityProof with JSON-LD, fully spec-compliant
DID Auth — challenge/response authentication using Ed25519 key signatures (alternative path)
Trust Registry — attesters issue attestation VCs to authorize issuers
OWASP API Security Top 10 — compliant security controls throughout
Identity Lifecycle
The full lifecycle spans five phases:
Account & role setup — sign up, get roles assigned
Organization setup — create an institutional identity, invite members
Trust setup — attester vouches for the issuer
Credential issuance — issuer signs a BBS+ VC for a subject
Selective-disclosure verification — subject reveals only chosen claims
End-to-End Sequence
sequenceDiagram
autonumber
actor AD as 🛡️ Admin
actor AT as 🔐 Attester
actor OW as 🏛️ Org Owner (Issuer)
actor SU as 👤 Subject
actor VE as 🔍 Verifier
participant API as DID + ZKP API
rect rgb(220, 235, 255)
Note over AD,API: Phase 1 — Account & Role Setup
OW->>API: POST /v1/auth/signup { email, password, name }
API-->>OW: { token, user: { id, did, role:"subject" } }
AT->>API: POST /v1/auth/signup { email, password, name }
API-->>AT: { token, user: { id, did, role:"subject" } }
SU->>API: POST /v1/auth/signup { email, password, name }
API-->>SU: { token, user: { id, did, role:"subject" } }
VE->>API: POST /v1/auth/signup { email, password, name }
API-->>VE: { token, user: { id, did, role:"subject" } }
Note over AD: Admin upgrades roles using X-Admin-Secret header
AD->>API: POST /v1/admin/users/{attesterUserId}/role { role:"attester" }
API-->>AD: { success: true }
AD->>API: POST /v1/admin/users/{ownerUserId}/role { role:"issuer" }
API-->>AD: { success: true }
AD->>API: POST /v1/admin/users/{verifierUserId}/role { role:"verifier" }
API-->>AD: { success: true }
Note over OW,VE: Everyone logs in again to get a fresh JWT reflecting the new role
OW->>API: POST /v1/auth/login { email, password }
API-->>OW: { token } ← JWT now carries role:"issuer"
AT->>API: POST /v1/auth/login { email, password }
API-->>AT: { token } ← JWT now carries role:"attester"
end
rect rgb(255, 243, 220)
Note over OW,API: Phase 2 — Organization Setup
OW->>API: POST /v1/organizations { name:"Test University", role:"issuer" }
Note over API: Creates org with its own did:key (role=issuer)<br/>Owner auto-added as member with role "owner"
API-->>OW: { id:orgId, name, slug, did:orgDid, memberRole:"owner" }
Note over OW: Invite existing users by email — added directly as members
OW->>API: POST /v1/organizations/{orgId}/invites { email, role:"admin" }
API-->>OW: { userId, memberRole:"admin" }
Note over OW: Invite someone who hasn't signed up yet
OW->>API: POST /v1/organizations/{orgId}/invites { email:"newmember@example.com", role:"member" }
API-->>OW: { inviteToken, expiresAt } ← 72-hour token to share with invitee
SU->>API: POST /v1/organizations/invites/{inviteToken}/accept {}
Note over API: Verifies caller email matches invite email (BOLA protection)
API-->>SU: { success: true }
end
rect rgb(220, 255, 230)
Note over AT,API: Phase 3 — Trust Setup (Attester authorises the Issuer)
Note over AT: Attester calls the trust endpoint using their personal issuer DID<br/>(the DID embedded in their JWT)
AT->>API: POST /v1/trust/attest { issuerDid: ownerDid }
Note over API: Records attestation VC linking attester → issuer<br/>Issuer is now in the trusted registry
API-->>AT: attestation record { id, issuerDid, attesterDid }
end
rect rgb(220, 255, 240)
Note over OW,SU: Phase 4 — Credential Issuance
OW->>API: POST /v1/credentials/issue<br/>{ subjectDid, credentialType:["UniversityDegree"], claims:{ name, degree, gpa } }
Note over API: ① verify Issuer has active trust attestation<br/>② sign all claims with BBS+ (DataIntegrityProof)<br/>③ persist VC with status = active
API-->>OW: signed Verifiable Credential { id:credId, proof, ... }
Note over OW,SU: Issuer shares credentialId with Subject out-of-band (QR code, secure message, etc.)
SU->>API: GET /v1/credentials/{credId}
API-->>SU: full VC record
end
rect rgb(250, 225, 255)
Note over SU,VE: Phase 5 — Selective Disclosure & Verification
Note over SU: Subject chooses which claims to reveal<br/>e.g. ["name","degree"] — GPA stays private
SU->>API: POST /v1/presentations/derive<br/>{ credentialId, revealedClaims:["name","degree"] }
Note over API: derive BBS+ proof over chosen subset only
API-->>SU: Verifiable Presentation (VP with ZK proof)
Note over SU,VE: Subject sends VP to Verifier (off-API channel or direct share)
VE->>API: POST /v1/presentations/verify { presentation: <VP> }
Note over API: ① verify BBS+ cryptographic proof<br/>② check Issuer is in trust registry<br/>③ check credential revocation/expiry status
API-->>VE: { valid:true, disclosedClaims:{ name, degree }, issuerTrusted:true, credentialStatus:"active" }
end
Loading
Credential & Presentation State Machine
stateDiagram-v2
direction TB
[*] --> Account_Created : POST /v1/auth/signup
state "Account Created<br>(role: subject)" as Account_Created
state "Role Upgraded<br>(issuer / attester / verifier)" as Role_Upgraded
state "Org Created<br>(org DID: issuer)" as Org_Created
state "Issuer Trusted" as Issuer_Trusted
state "VC Active" as VC_Active
state "VP Derived" as VP_Derived
Account_Created --> Role_Upgraded : POST /v1/admin/users/{id}/role<br>(requires X-Admin-Secret)
Account_Created --> Org_Created : POST /v1/organizations<br>(any authenticated user)
Role_Upgraded --> Issuer_Trusted : POST /v1/trust/attest<br>(attester vouches for issuer DID)
Issuer_Trusted --> VC_Active : POST /v1/credentials/issue<br>(issuer + active attestation)
VC_Active --> VP_Derived : POST /v1/presentations/derive<br>(subject · choose claims to reveal)
VC_Active --> VC_Revoked : POST /v1/credentials/{id}/revoke<br>(issuer only · irreversible)
VC_Active --> VC_Expired : expiresAt timestamp reached
VP_Derived --> VP_Verified_OK : POST /v1/presentations/verify<br>✅ proof valid · issuer trusted · VC active
VP_Derived --> VP_Verified_FAIL : POST /v1/presentations/verify<br>❌ invalid proof OR issuer untrusted OR VC revoked/expired
VC_Revoked --> [*]
VC_Expired --> [*]
Privacy guarantee: The Verifier only ever sees the claims the Subject explicitly included in revealedClaims. All other claims in the original credential are cryptographically hidden — the BBS+ proof is mathematically indistinguishable from a proof over the full credential.
All endpoints are prefixed with /v1. Protected routes require Authorization: Bearer <token>.
Email Auth (v1.2)
Method
Path
Auth
Description
POST
/v1/auth/signup
Public
Create account; auto-generates a did:key
POST
/v1/auth/login
Public
Exchange email + password for JWT
POST
/v1/auth/forgot-password
Public
Request a password reset token
POST
/v1/auth/reset-password
Public
Consume reset token, set new password
Users (v1.2)
Method
Path
Auth
Description
GET
/v1/users/me
Any role
Get your profile (includes org memberships)
PATCH
/v1/users/me
Any role
Update name or organizationName
Admin (v1.2)
Method
Path
Auth
Description
POST
/v1/admin/users/:id/role
X-Admin-Secret header
Upgrade a user's DID role
Organizations (v1.2)
Method
Path
Auth
Description
POST
/v1/organizations
Any role
Create an org with its own DID
GET
/v1/organizations/:id
Member
Get org details
DELETE
/v1/organizations/:id
Owner
Delete org and deactivate its DID
GET
/v1/organizations/:id/members
Member
List all org members
POST
/v1/organizations/:id/invites
Admin/Owner
Invite a user by email
POST
/v1/organizations/invites/:token/accept
Any role
Accept a pending invite
PATCH
/v1/organizations/:id/members/:userId
Admin/Owner
Change a member's org role
DELETE
/v1/organizations/:id/members/:userId
Admin/Owner or self
Remove a member (or leave)
DID Authentication (challenge-response path)
Method
Path
Auth
Description
POST
/v1/auth/challenge
Public
Request a one-time nonce for a DID
POST
/v1/auth/verify
Public
Submit signed nonce, receive JWT
DID Management
Method
Path
Auth
Description
POST
/v1/dids
Public
Create a raw did:key (private key returned once)
GET
/v1/dids/me
Any role
Get your own DID document
GET
/v1/dids/:did
Public
Resolve any DID document
DELETE
/v1/dids/:did
Owner
Deactivate a DID
Verifiable Credentials
Method
Path
Auth
Description
POST
/v1/credentials/issue
Issuer
Issue a BBS+-signed VC to a subject
GET
/v1/credentials
Issuer
List all issued VCs
GET
/v1/credentials/:id
Issuer / Subject
Fetch a VC by ID
POST
/v1/credentials/:id/revoke
Issuer
Revoke a credential
GET
/v1/credentials/:id/status
Public
Check credential status
Verifiable Presentations
Method
Path
Auth
Description
POST
/v1/presentations/derive
Subject
Create a VP with BBS+ selective disclosure
POST
/v1/presentations/verify
Verifier
Verify a VP (proof + trust chain + revocation)
GET
/v1/presentations/:id
Subject
Fetch a previously derived presentation
Trust Registry
Method
Path
Auth
Description
POST
/v1/trust/attest
Attester
Vouch for an issuer DID
GET
/v1/trust/issuers
Public
List all trusted issuers
GET
/v1/trust/issuers/:did
Public
Check if a DID is a trusted issuer
POST
/v1/trust/attest/:id/revoke
Attester
Revoke an issuer attestation
Core Flows
1. Account Setup & Role Assignment
# Create an account (did:key auto-generated, initial role = subject)
POST /v1/auth/signup
{ "email": "issuer@example.com", "password": "password123", "name": "Alice" }
→ { token, user: { id, did, role: "subject" } }
# Admin upgrades role (requires X-Admin-Secret header)
POST /v1/admin/users/<USER_ID>/role
{ "role": "issuer" }
→ { success: true }
# Log in again — JWT now carries role: "issuer"
POST /v1/auth/login
{ "email": "issuer@example.com", "password": "password123" }
→ { token }
2. Organization Setup
# Create an org with an institutional DID
POST /v1/organizations
{ "name": "Test University", "role": "issuer" }
→ { id, slug: "test-university", did: "did:key:z6Mk...", memberRole: "owner" }
# Invite an existing user to join the org (adds them immediately)
POST /v1/organizations/<ORG_ID>/invites
{ "email": "colleague@example.com", "role": "admin" }
→ { userId, memberRole: "admin" }
# Invite a new user (returns a token to share with them)
POST /v1/organizations/<ORG_ID>/invites
{ "email": "newperson@example.com", "role": "member" }
→ { inviteToken, expiresAt }
# New user accepts the invite after signing up
POST /v1/organizations/invites/<INVITE_TOKEN>/accept {}
→ { success: true }
3. Trust Setup
# Attester authorizes the issuer (must use attester JWT)
POST /v1/trust/attest
{ "issuerDid": "did:key:z6Mk..." }
→ { id, issuerDid, attesterDid, createdAt }
4. Issuing a Verifiable Credential
# Issuer signs a VC with BBS+ (DataIntegrityProof)
POST /v1/credentials/issue
{
"subjectDid": "did:key:...",
"credentialType": ["UniversityDegree"],
"claims": { "name": "Alice", "degree": "BSc Computer Science", "gpa": "3.9" }
}
→ signed VC with DataIntegrityProof { id, proof, ... }
5. Selective Disclosure Presentation
# Subject reveals only name and degree — GPA stays hidden
POST /v1/presentations/derive
{ "credentialId": "urn:uuid:...", "revealedClaims": ["name", "degree"] }
→ VP with BBS+ derived proof
# Verifier checks the proof, trust chain, and revocation status
POST /v1/presentations/verify { "presentation": <VP> }
→ { valid: true, disclosedClaims: { name, degree }, issuerTrusted: true, credentialStatus: "active" }
Security
OWASP API Security Top 10 Controls
#
Threat
Control
API1
Broken Object Level Authorization
Resource ownership enforced — callers can only access their own resources; org BOLA: invite email must match caller email
# Create test database
createdb did_zkp_test
# Unit tests
bun test:unit
# Integration tests
bun test:integration
# Security (OWASP) tests
bun test:security
# All tests
bun test
CLI Utilities
Signature Generator
For local development and testing, use the built-in CLI signature generator to create signatures for the DID challenge-response authentication. It retrieves the required nonce and private key from your local database, signs the challenge, and outputs the base64-encoded signature:
bun run generate-signature.ts <did><challengeId>
Example:
bun run generate-signature.ts "did:key:z6MknwjrNSCBRBYdCgSXPHFQXNyisvEWM3SuVYnT6d215XUC""af003fcc-5012-42f9-b508-4f7e4d88236a"
Environment Variables
Variable
Required
Description
DATABASE_URL
Yes
PostgreSQL connection string
TEST_DATABASE_URL
Tests
PostgreSQL connection string for test DB
KEY_ENCRYPTION_SECRET
Yes
64-char hex string (32 bytes) for AES-GCM key encryption
JWT_SECRET
Yes
Secret for signing session JWTs
ADMIN_SECRET
Yes
Secret for X-Admin-Secret header on admin endpoints
CORS_ORIGIN
No
Allowed CORS origin (default: http://localhost:3000)
PORT
No
HTTP port (default: 3000)
NODE_ENV
No
development or production
Database Schema
Eleven tables cover the full protocol lifecycle:
Account & Identity
users — email/password accounts, name, organizationName, FK → dids
password_reset_tokens — one-time reset tokens with expiry and used flag
dids — DID documents, Ed25519 + BLS12-381 encrypted key pairs, role
sessions — JWT session records (15-min TTL)
auth_challenges — single-use nonces for DID Auth (5-min TTL)
Organizations
organizations — org name, slug, DID, owner FK
org_members — user ↔ org membership with role (member, admin, owner)
org_invites — pending invites with email, token, expiry, and accepted flag
Credentials & Trust
credentials — signed VCs with BBS+ DataIntegrityProof
presentations — derived VPs with selective-disclosure proofs
trust_attestations — attestation VCs linking attesters to authorized issuers
audit_log — append-only event log (UPDATE/DELETE blocked by DB trigger)
Documentation
Design Spec — architecture, data model, flows, security rationale
A complete and secure modern identity API for Web3 credentials management by impementing DID, VC, and ZKP. You can use this API for issuing, sharing, and verifying tamper-proof digital credentials — degrees, certificates, badges — where users control exactly what they share.