Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f9ef8cb
feat: Add status list management and revocation for OpenID4VC issuanc…
sagarkhole4 Mar 26, 2026
3bb0645
feat: Add status list management and revocation for OpenID4VC issuanc…
sagarkhole4 Mar 26, 2026
dddb726
Merge branch 'feat/oidc-main-sync' into feat/sd-jwt-revocation-flow
sagarkhole4 Mar 26, 2026
7986f73
feat: Add property to credential offer types and issuance logic
sagarkhole4 Mar 26, 2026
fb0729d
making status list server URL mandatory
sagarkhole4 Mar 26, 2026
d3a02fd
feat: concurrency handled in the status list service.
sagarkhole4 Mar 30, 2026
730d3ab
Merge branch 'feat/oidc-main-sync' into feat/sd-jwt-revocation-flow
sagarkhole4 Mar 30, 2026
5e7263b
feat: update CLI configuration and refine status list service logic
sagarkhole4 Mar 30, 2026
b48e63f
feat: Add status list management and revocation for OpenID4VC issuanc…
sagarkhole4 Mar 26, 2026
2919807
feat: Add status list management and revocation for OpenID4VC issuanc…
sagarkhole4 Mar 26, 2026
738a540
feat: Add property to credential offer types and issuance logic
sagarkhole4 Mar 26, 2026
d7c6822
making status list server URL mandatory
sagarkhole4 Mar 26, 2026
02e80bb
feat: concurrency handled in the status list service.
sagarkhole4 Mar 30, 2026
1291984
feat: update CLI configuration and refine status list service logic
sagarkhole4 Mar 30, 2026
9cacb99
refactor:code review changes
sagarkhole4 Apr 7, 2026
df5c302
refactor:code review changes
sagarkhole4 Apr 7, 2026
53c330d
refactor:code review changes
sagarkhole4 Apr 7, 2026
ccd4574
feat: implement per-list concurrency locking and dynamic algorithm se…
sagarkhole4 Apr 13, 2026
9fcf4c0
update:Removed file server token from cli config for security
sagarkhole4 Apr 14, 2026
43b99c2
refactor: reduce cognitive complexity of createCredentialOffer flow
sagarkhole4 Apr 21, 2026
35e45d2
refactor: address code quality issues and cleanup x509 enums
sagarkhole4 Apr 21, 2026
c1ebf1a
Merge branch 'feat/oidc-main-sync' into feat/sd-jwt-revocation-flow
sagarkhole4 Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.demo
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ OID4VP_AUTH_REQUEST_PROOF_REQUEST_EXPIRY=3600
APP_JSON_BODY_SIZE=5mb
APP_URL_ENCODED_BODY_SIZE=5mb


API_KEY=supersecret-that-too-16chars
UPDATE_JWT_SECRET=false


STATUS_LIST_SERVER_URL=https://dev-status-list.sovio.id/
STATUS_LIST_API_KEY=test_key
STATUS_LIST_DEFAULT_SIZE=131072
BCOVRIN_TEST_GENESIS='{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"138.197.138.255","client_port":9702,"node_ip":"138.197.138.255","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"138.197.138.255","client_port":9704,"node_ip":"138.197.138.255","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"138.197.138.255","client_port":9706,"node_ip":"138.197.138.255","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"}
Expand Down
8 changes: 8 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ OID4VP_AUTH_REQUEST_PROOF_REQUEST_EXPIRY=3600
APP_JSON_BODY_SIZE=5mb
APP_URL_ENCODED_BODY_SIZE=5mb

# Security
API_KEY=supersecret-that-too-16chars
UPDATE_JWT_SECRET=false

# Status List
STATUS_LIST_SERVER_URL=https://dev-status-list.sovio.id/
STATUS_LIST_API_KEY=test_key
STATUS_LIST_DEFAULT_SIZE=131072
# Specify Bcovrin test genesis
BCOVRIN_TEST_GENESIS=`{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"138.197.138.255","client_port":9702,"node_ip":"138.197.138.255","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"138.197.138.255","client_port":9704,"node_ip":"138.197.138.255","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"}
Expand Down
18 changes: 10 additions & 8 deletions samples/cliConfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"label": "AFJ Rest Agent 1",
"walletId": "sample",
"walletKey": "sample",
"walletId": "sample10",
"walletKey": "sample10",
"walletType": "postgres",
"walletUrl": "localhost:5432",
"walletAccount": "postgres",
Expand All @@ -23,7 +23,9 @@
"indyNamespace": "bcovrin:testnet"
}
],
"endpoint": ["http://localhost:4002"],
"endpoint": [
"http://localhost:4002"
],
"autoAcceptConnections": true,
"autoAcceptCredentials": "always",
"autoAcceptProofs": "contentApproved",
Expand All @@ -34,15 +36,15 @@
"port": 4002
}
],
"outboundTransport": ["http"],
"outboundTransport": [
"http"
],
"adminPort": 4001,
"tenancy": true,
"schemaFileServerURL": "https://schema.credebl.id/schemas/",
"didRegistryContractAddress": "0xcB80F37eDD2bE3570c6C9D5B0888614E04E1e49E",
"schemaManagerContractAddress": "0x4742d43C2dFCa5a1d4238240Afa8547Daf87Ee7a",
"rpcUrl": "https://rpc-amoy.polygon.technology",
"fileServerUrl": "https://schema.credebl.id",
"fileServerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBeWFuV29ya3MiLCJpZCI6ImNhZDI3ZjhjLTMyNWYtNDRmZC04ZmZkLWExNGNhZTY3NTMyMSJ9.I3IR7abjWbfStnxzn1BhxhV0OEzt1x3mULjDdUcgWHk",
"apiKey": "supersecret-that-too-16chars",
"updateJwtSecret": false
}
"fileServerToken": ""
}
9 changes: 7 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type { AriesRestConfig } from './cliAgent.js'

import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import dotenv from 'dotenv'

dotenv.config()

import { runRestAgent } from './cliAgent.js'

Expand Down Expand Up @@ -151,7 +154,9 @@ async function parseArguments(): Promise<Parsed> {
.option('wallet-idle-timeout', { number: true })
.option('apiKey', {
string: true,
coerce: (input: string) => {
default: process.env.API_KEY,
coerce: (input: string | undefined) => {
if (!input) return input
input = input.trim()
if (input && input.length < 16) {
throw new Error('API key must be at least 16 characters long')
Expand All @@ -161,7 +166,7 @@ async function parseArguments(): Promise<Parsed> {
})
.option('updateJwtSecret', {
boolean: true,
default: false,
default: process.env.UPDATE_JWT_SECRET === 'true',
})
.config()
.env('AFJ_REST')
Expand Down
2 changes: 2 additions & 0 deletions src/cliAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
X509Module,
JwkDidRegistrar,
JwkDidResolver,
SdJwtVcModule,
PeerDidNumAlgo,
} from '@credo-ts/core'
import {
Expand Down Expand Up @@ -255,6 +256,7 @@ const getModules = (
rpcUrl: rpcUrl ? rpcUrl : (process.env.RPC_URL as string),
serverUrl: fileServerUrl ? fileServerUrl : (process.env.SERVER_URL as string),
}),
sdJwtVc: new SdJwtVcModule(),
openid4vc: new OpenId4VcModule({
app: expressApp,
issuer: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,19 @@ export class IssuanceSessionsController extends Controller {
throw ErrorHandlingService.handle(error)
}
}

/**
* Revoke credentials in an issuance session by session ID
*/
@Post('{issuanceSessionId}/revoke')
public async revokeSessionById(
@Request() request: Req,
@Path('issuanceSessionId') issuanceSessionId: string,
) {
try {
return await issuanceSessionService.revokeBySessionId(request, issuanceSessionId)
} catch (error) {
throw ErrorHandlingService.handle(error)
}
}
}
181 changes: 140 additions & 41 deletions src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import type { Request as Req } from 'express'
import { type OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc'
import { OpenId4VcIssuanceSessionRepository } from '@credo-ts/openid4vc'

import { SignerMethod } from '../../../enums/enum'
import { CredentialFormat, SignerMethod } from '../../../enums/enum'
import { BadRequestError, NotFoundError } from '../../../errors/errors'

import { checkAndCreateStatusList, getServerUrl, revokeCredentialInStatusList } from '../../../utils/statusListService'
import { STATUS_LISTS_PATH } from '../../../utils/constant'

class IssuanceSessionsService {
public async createCredentialOffer(options: OpenId4VcIssuanceSessionsCreateOffer, agentReq: Req) {
const { credentials, publicIssuerId } = options
Expand All @@ -15,49 +18,36 @@ class IssuanceSessionsService {
if (!issuer) {
throw new NotFoundError(`Issuer with id ${publicIssuerId} not found`)
}
const mappedCredentials = credentials.map((cred) => {
const supported = issuer?.credentialConfigurationsSupported[cred.credentialSupportedId]
if (!supported) {
throw new Error(`CredentialSupportedId '${cred.credentialSupportedId}' is not supported by issuer`)
}
if (supported.format !== cred.format) {
throw new Error(
`Format mismatch for '${cred.credentialSupportedId}': expected '${supported.format}', got '${cred.format}'`,
)
}

// must have signing options
if (!cred.signerOptions?.method) {
throw new BadRequestError(
`signerOptions must be provided and allowed methods are ${Object.values(SignerMethod).join(', ')}`,
)
}

if (cred.signerOptions.method == SignerMethod.Did && !cred.signerOptions.did) {
throw new BadRequestError(
`For ${cred.credentialSupportedId} : did must be present inside signerOptions if SignerMethod is 'did' `,
)
}

if (cred.signerOptions.method === SignerMethod.X5c && !cred.signerOptions.x5c) {
throw new BadRequestError(
`For ${cred.credentialSupportedId} : x5c must be present inside signerOptions if SignerMethod is 'x5c' `,
)
}

const currentVct = cred.payload && 'vct' in cred.payload ? cred.payload.vct : undefined
return {
...cred,
payload: {
...cred.payload,
vct: currentVct ?? (typeof supported.vct === 'string' ? supported.vct : undefined),
},
}
})

options.issuanceMetadata ||= {}
const offerStatusInfo: any[] = []

const mappedCredentials = await Promise.all(
credentials.map(async (cred) => {
const supported = issuer.credentialConfigurationsSupported[cred.credentialSupportedId]

this.validateCredentialConfig(cred, supported)

const statusBlock = await this.processStatusList(cred, options, agentReq, offerStatusInfo)

const currentVct = cred.payload && 'vct' in cred.payload ? cred.payload.vct : undefined
return {
...cred,
payload: {
...cred.payload,
vct: currentVct ?? (typeof supported.vct === 'string' ? supported.vct : undefined),
...(statusBlock ? { status: statusBlock } : {}),
},
}
}),
)

options.issuanceMetadata ||= {}
options.issuanceMetadata.credentials = mappedCredentials
options.issuanceMetadata.isRevocable = options.isRevocable

if (offerStatusInfo.length > 0) {
options.issuanceMetadata.StatusListInfo = offerStatusInfo
}

const issuerModule = agentReq.agent.modules.openid4vc.issuer

Expand All @@ -75,6 +65,91 @@ class IssuanceSessionsService {
return { credentialOffer, issuanceSession }
}

private validateCredentialConfig(cred: any, supported: any) {
if (!supported) {
throw new Error(`CredentialSupportedId '${cred.credentialSupportedId}' is not supported by issuer`)
}
if (supported.format !== cred.format) {
throw new Error(
`Format mismatch for '${cred.credentialSupportedId}': expected '${supported.format}', got '${cred.format}'`,
)
}

if (!cred.signerOptions?.method) {
throw new BadRequestError(
`signerOptions must be provided and allowed methods are ${Object.values(SignerMethod).join(', ')}`,
)
}

if (cred.signerOptions.method === SignerMethod.Did && !cred.signerOptions.did) {
throw new BadRequestError(
`For ${cred.credentialSupportedId} : did must be present inside signerOptions if SignerMethod is 'did' `,
)
}

if (cred.signerOptions.method === SignerMethod.X5c && !cred.signerOptions.x5c) {
throw new BadRequestError(
`For ${cred.credentialSupportedId} : x5c must be present inside signerOptions if SignerMethod is 'x5c' `,
)
}
}

private async processStatusList(
cred: any,
options: OpenId4VcIssuanceSessionsCreateOffer,
agentReq: Req,
offerStatusInfo: any[],
) {
if (!options.isRevocable) {
return undefined
}

const effectiveIssuerDid = cred.signerOptions?.method === SignerMethod.Did ? cred.signerOptions.did : undefined
const effectiveStatusList = cred.statusListDetails || options.statusListDetails

if (![CredentialFormat.VcSdJwt, CredentialFormat.DcSdJwt].includes(cred.format as unknown as CredentialFormat)) {
throw new BadRequestError(
`Revocation is only supported for SD-JWT formats (vc+sd-jwt, dc+sd-jwt), got '${cred.format}'`,
)
}

if (!process.env.STATUS_LIST_SERVER_URL) {
throw new BadRequestError('Cannot create revocable credentials: STATUS_LIST_SERVER_URL is not configured')
}

if (cred.signerOptions.method !== SignerMethod.Did || !effectiveIssuerDid) {
throw new BadRequestError(`Revocation is not supported without a DID signer (found ${cred.signerOptions.method})`)
}

if (!effectiveStatusList) {
throw new BadRequestError('Status list details must be provided for revocable credentials')
}

await checkAndCreateStatusList(
agentReq.agent as any,
effectiveStatusList.listId,
effectiveIssuerDid,
effectiveStatusList.listSize,
)

const listUri = `${getServerUrl()}/${STATUS_LISTS_PATH}/${effectiveStatusList.listId}`

offerStatusInfo.push({
credentialSupportedId: cred.credentialSupportedId,
listId: effectiveStatusList.listId,
index: effectiveStatusList.index,
issuerDid: effectiveIssuerDid,
})

return {
status_list: {
uri: listUri,
idx: effectiveStatusList.index,
},
}
}


public async getIssuanceSessionsById(agentReq: Req, sessionId: string) {
const issuer = agentReq.agent.modules.openid4vc.issuer
if (!issuer) {
Expand Down Expand Up @@ -144,6 +219,30 @@ class IssuanceSessionsService {
const issuanceSessionRepository = agentReq.agent.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository)
await issuanceSessionRepository.deleteById(agentReq.agent.context, sessionId)
}

public async revokeBySessionId(agentReq: Req, sessionId: string) {
const issuanceSessionRepository = agentReq.agent.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository)
const record = await issuanceSessionRepository.findById(agentReq.agent.context, sessionId)

if (!record) {
throw new NotFoundError(`Issuance session with id ${sessionId} not found`)
}

const statusInfo = record.issuanceMetadata?.StatusListInfo as any[]
if (!statusInfo || statusInfo.length === 0) {
throw new Error(`No status list information found for session ${sessionId}`)
}
Comment thread
sagarkhole4 marked this conversation as resolved.

if (!process.env.STATUS_LIST_SERVER_URL) {
throw new BadRequestError('Cannot execute revocation: STATUS_LIST_SERVER_URL is not configured')
}

for (const info of statusInfo) {
await revokeCredentialInStatusList(agentReq.agent as any, info.listId, info.index, info.issuerDid)
}

return { message: 'Credentials in session revoked successfully' }
}
}

export const issuanceSessionService = new IssuanceSessionsService()
16 changes: 12 additions & 4 deletions src/controllers/openid4vc/types/issuer.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import type { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc'
import { Kms } from '@credo-ts/core'
import { OpenId4VciCreateCredentialOfferOptions, OpenId4VciSignCredentials } from '@credo-ts/openid4vc'

export enum SignerMethod {
Did = 'did',
X5c = 'x5c',
}
import { SignerMethod } from '../../../enums/enum'

export interface OpenId4VciOfferCredentials {
credentialSupportedId: string
Expand All @@ -18,6 +15,11 @@ export interface OpenId4VciOfferCredentials {
x5c?: string[]
keyId?: string
}
statusListDetails?: {
listId: string
index: number
listSize?: number
}
}

export interface DisclosureFrameForOffer {
Expand Down Expand Up @@ -72,6 +74,12 @@ export interface OpenId4VcIssuanceSessionsCreateOffer {
authorizationServerUrl: string
}
issuanceMetadata?: Record<string, unknown>
statusListDetails?: {
listId: string
index: number
listSize?: number
}
isRevocable?: boolean
}

export interface X509GenericRecordContent {
Expand Down
Loading