Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
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
11 changes: 10 additions & 1 deletion .env.demo
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,13 @@ OID4VCI_CNONCE_EXPIRY=3600
OID4VP_AUTH_REQUEST_PROOF_REQUEST_EXPIRY=3600

APP_JSON_BODY_SIZE=5mb
APP_URL_ENCODED_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
11 changes: 10 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,13 @@ OID4VCI_CNONCE_EXPIRY=3600
OID4VP_AUTH_REQUEST_PROOF_REQUEST_EXPIRY=3600

APP_JSON_BODY_SIZE=5mb
APP_URL_ENCODED_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
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,
} from '@credo-ts/core'
import {
DidCommHttpOutboundTransport,
Expand Down Expand Up @@ -252,6 +253,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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
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,7 +18,10 @@
if (!issuer) {
throw new NotFoundError(`Issuer with id ${publicIssuerId} not found`)
}
const mappedCredentials = credentials.map((cred) => {

const offerStatusInfo: any[] = []

const mappedCredentials = await Promise.all(credentials.map(async (cred) => {

Check failure on line 24 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 21 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=credebl_afj-controller&issues=AZ1oJqSxQGBs4swOfkD9&open=AZ1oJqSxQGBs4swOfkD9&pullRequest=357
const supported = issuer?.credentialConfigurationsSupported[cred.credentialSupportedId]
if (!supported) {
throw new Error(`CredentialSupportedId '${cred.credentialSupportedId}' is not supported by issuer`)
Expand Down Expand Up @@ -45,19 +51,69 @@
)
}

const currentVct = cred.payload && 'vct' in cred.payload ? cred.payload.vct : undefined
const effectiveIssuerDid = cred.signerOptions?.method === SignerMethod.Did ? cred.signerOptions.did : undefined
const effectiveStatusList = cred.statusListDetails || options.statusListDetails

let statusBlock = undefined

if (options.isRevocable) {
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}`

statusBlock = {
status_list: {
uri: listUri,
idx: effectiveStatusList.index
}
}

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

const currentVct = cred.payload && 'vct' in cred.payload ? (cred.payload as any).vct : undefined

Check warning on line 99 in src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=credebl_afj-controller&issues=AZ09pEfQsmaXyNe1z6ly&open=AZ09pEfQsmaXyNe1z6ly&pullRequest=357
return {
...cred,
payload: {
...cred.payload,
vct: currentVct ?? (typeof supported.vct === 'string' ? supported.vct : undefined),
...(statusBlock ? { status: statusBlock } : {})
},
Comment thread
sagarkhole4 marked this conversation as resolved.
}
})
}))

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 Down Expand Up @@ -144,6 +200,30 @@
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
19 changes: 19 additions & 0 deletions src/controllers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ export interface AgentToken {
token: string
}

export enum CredentialState {
ProposalSent = 'proposal-sent',
ProposalReceived = 'proposal-received',
OfferSent = 'offer-sent',
OfferReceived = 'offer-received',
Declined = 'declined',
RequestSent = 'request-sent',
RequestReceived = 'request-received',
CredentialIssued = 'credential-issued',
CredentialReceived = 'credential-received',
Done = 'done',
Abandoned = 'abandoned',
}

export enum CredentialRole {
Issuer = 'issuer',
Holder = 'holder',
}

export interface AgentMessageType {
'@id': string
'@type': string
Expand Down
23 changes: 22 additions & 1 deletion src/controllers/x509/x509.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import type { Curve } from '../types'
import type { X509ExtendedKeyUsage, X509KeyUsage } from '@credo-ts/core'

export enum X509KeyUsage {
DigitalSignature = 1,
NonRepudiation = 2,
KeyEncipherment = 4,
DataEncipherment = 8,
KeyAgreement = 16,
KeyCertSign = 32,
CrlSign = 64,
EncipherOnly = 128,
DecipherOnly = 256,
}

export enum X509ExtendedKeyUsage {
ServerAuth = '1.3.6.1.5.5.7.3.1',
ClientAuth = '1.3.6.1.5.5.7.3.2',
CodeSigning = '1.3.6.1.5.5.7.3.3',
EmailProtection = '1.3.6.1.5.5.7.3.4',
TimeStamping = '1.3.6.1.5.5.7.3.8',
OcspSigning = '1.3.6.1.5.5.7.3.9',
MdlDs = '1.0.18013.5.1.2',
}

// Enum remains the same
export enum GeneralNameType {
Expand Down
8 changes: 8 additions & 0 deletions src/enums/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,11 @@ export enum KeyAlgorithmCurve {
secp256k1 = 'secp256k1',
Bls12381G2 = 'bls12381g2',
}

export enum CredentialFormat {
VcSdJwt = 'vc+sd-jwt',
DcSdJwt = 'dc+sd-jwt',
JwtVcJson = 'jwt_vc_json',
JwtVcJsonLd = 'jwt_vc_json-ld',
LdpVc = 'ldp_vc',
}
Loading