Skip to content

Commit

Permalink
feat: Add support issue verify & revoke credentials [DEV-2669] (#255)
Browse files Browse the repository at this point in the history
* feat: Add support issue verify & revoke credentials

* feat: Refactor && add fetchStatusLists api

* feat: Update swagger schemas

* feat: Add credential suspension

* feat: Add reinstate credential api

* build: Upgrade did-provider-cheqd
  • Loading branch information
DaevMithran authored Jun 13, 2023
1 parent 67b167d commit c69df47
Show file tree
Hide file tree
Showing 13 changed files with 26,990 additions and 107 deletions.
26,171 changes: 26,151 additions & 20 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"README.md"
],
"dependencies": {
"@cheqd/did-provider-cheqd": "^3.3.0",
"@cheqd/did-provider-cheqd": "^3.3.1",
"@cosmjs/amino": "^0.30.1",
"@cosmjs/encoding": "^0.30.1",
"@logto/express": "^2.0.1",
Expand Down
15 changes: 11 additions & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@ import { IssuerController } from './controllers/issuer.js'
import { CustomerController } from './controllers/customer.js'
import { Authentication } from './middleware/authentication.js'
import { Connection } from './database/connection/connection.js'
import { RevocationController } from './controllers/revocation.js'
import { CORS_ERROR_MSG, configLogToExpress } from './types/constants.js'

import swaggerJSONDoc from '../swagger.json' assert { type: "json" }

import * as dotenv from 'dotenv'
dotenv.config()

import { UserInfo } from './controllers/user_info.js'
import path from 'path'

const swagger_options = {
customJs: '/static/custom_button.js',
}

dotenv.config()

class App {
public express: express.Application

Expand Down Expand Up @@ -77,8 +78,14 @@ class App {

// credentials
app.post(`/credential/issue`, CredentialController.issueValidator, new CredentialController().issue)
app.post(`/credential/verify`, CredentialController.verifyValidator, new CredentialController().verify)

app.post(`/credential/verify`, CredentialController.credentialValidator, new CredentialController().verify)
app.post(`/credential/revoke`, CredentialController.credentialValidator, new CredentialController().revoke)
app.post('/credential/suspend', new CredentialController().suspend)
app.post('/credential/reinstate', new CredentialController().reinstate)

//revocation
app.post('/revocation/statusList2021/create', RevocationController.didValidator, RevocationController.statusListValidator, new RevocationController().createStatusList)
app.get('/revocation/statusList2021/list', RevocationController.didValidator, new RevocationController().fetchStatusList)
// store
app.post(`/store`, new StoreController().set)
app.get(`/store/:id`, new StoreController().get)
Expand Down
63 changes: 58 additions & 5 deletions src/controllers/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { VerifiableCredential } from '@veramo/core'
import { check, validationResult } from 'express-validator'

import { Credentials } from '../services/credentials.js'
import { CustomerService } from '../services/customer.js'
import { Identity } from '../services/identity/index.js'

export class CredentialController {

Expand All @@ -22,8 +22,16 @@ export class CredentialController {
check('format').optional().isString().withMessage('Invalid credential format')
]

public static verifyValidator = [
public static credentialValidator = [
check('credential').exists().withMessage('W3c verifiable credential was not provided')
.custom((value) => {
if (typeof value === 'string' || typeof value === 'object') {
return true
}
return false
})
.withMessage('Entry must be a jwt string or an credential'),
check('publish').optional().isBoolean().withMessage('publish should be a boolean value')
]

public async issue(request: Request, response: Response) {
Expand All @@ -43,15 +51,60 @@ export class CredentialController {

public async verify(request: Request, response: Response) {
if (request?.headers && (!request.headers['content-type'] || request.headers['content-type'] != 'application/json')) {
return response.status(405).json({ error: 'Unsupported media type.' })
return response.status(405).json({ error: 'Unsupported media type.' })
}

const result = validationResult(request)
if (!result.isEmpty()) {
return response.status(400).json({ error: result.array()[0].msg })
return response.status(400).json({ error: result.array()[0].msg })
}
try {
return response.status(200).json(await Credentials.instance.verify_credentials(request.body.credential, request.body.statusOptions, response.locals.customerId))
} catch (error) {
return response.status(500).json({
error: `${error}`
})
}
}

public async revoke(request: Request, response: Response) {
const result = validationResult(request)
if (!result.isEmpty()) {
return response.status(400).json({ error: result.array()[0].msg })
}

try {
return response.status(200).json(await Identity.instance.revokeCredentials(request.body.credential, request.body.publish, response.locals.customerId))
} catch (error) {
return response.status(500).json({
error: `${error}`
})
}
}

public async suspend(request: Request, response: Response) {
const result = validationResult(request)
if (!result.isEmpty()) {
return response.status(400).json({ error: result.array()[0].msg })
}

try {
return response.status(200).json(await Identity.instance.suspendCredentials(request.body.credential, request.body.publish, response.locals.customerId))
} catch (error) {
return response.status(500).json({
error: `${error}`
})
}
}

public async reinstate(request: Request, response: Response) {
const result = validationResult(request)
if (!result.isEmpty()) {
return response.status(400).json({ error: result.array()[0].msg })
}

try {
return response.status(200).json(await Credentials.instance.verify_credentials(request.body.credential, response.locals.customerId))
return response.status(200).json(await Identity.instance.reinstateCredentials(request.body.credential, request.body.publish, response.locals.customerId))
} catch (error) {
return response.status(500).json({
error: `${error}`
Expand Down
5 changes: 2 additions & 3 deletions src/controllers/issuer.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type { Request, Response } from 'express'

import { check, param, validationResult } from 'express-validator'
import { fromString } from 'uint8arrays'
import { DIDDocument } from 'did-resolver'
import { v4 } from 'uuid'
import { MethodSpecificIdAlgo, VerificationMethods, CheqdNetwork } from '@cheqd/sdk'
import { MsgCreateResourcePayload } from '@cheqd/ts-proto/cheqd/resource/v2/index.js'

import { Identity } from '../services/identity/index.js'
import { generateDidDoc, validateSpecCompliantPayload } from '../helpers/helpers.js'
import { check, param, validationResult } from 'express-validator'
import { fromString } from 'uint8arrays'

export class IssuerController {

Expand Down
74 changes: 74 additions & 0 deletions src/controllers/revocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Request, Response } from 'express'
import { check, query, validationResult } from 'express-validator'
import { fromString } from 'uint8arrays'

import { Identity } from '../services/identity/index.js'
import { Veramo } from '../services/identity/agent.js'
import { ResourceMetadata } from '../types/types.js'

export class RevocationController {

static statusListValidator = [
check('length').isNumeric().withMessage('length should be a number'),
check('data').optional().isString().withMessage('data should be string'),
check('encoding').optional().isIn(['base64', 'base64url', 'hex']).withMessage('invalid encoding')
]

static didValidator = [
query('did').isString().withMessage('DID is required')
.contains('did:cheqd:').withMessage('Provide a valid cheqd DID')
]

async createStatusList(request: Request, response: Response) {
const result = validationResult(request)
if (!result.isEmpty()) {
return response.status(400).json({ error: result.array()[0].msg })
}

let { length, encoding } = request.body
let { data, name, type, alsoKnownAs, version, network } = request.body

const did = request.query.did as string
network = network || (did.split(':'))[2]
data = data ? fromString(data, 'base64') : undefined

try {
const result = await Identity.instance.createStatusList2021(did, network, { data, name, alsoKnownAs, version, resourceType: type }, { length, encoding }, response.locals.customerId)
return response.status(200).json({
success: result
})
} catch (error) {
return response.status(500).json({
error: `Internal error: ${error}`
})
}
}

async fetchStatusList(request: Request, response: Response) {
const result = validationResult(request)
if (!result.isEmpty()) {
return response.status(400).json({ error: result.array()[0].msg })
}

try {
let result = await Veramo.instance.resolve(`${request.query.did}?resourceType=StatusList2021&resourceMetadata=true`)
result = result.contentStream?.linkedResourceMetadata || []
const statusList = result
.filter((resource: ResourceMetadata)=>resource.mediaType=='application/octet-stream' || resource.mediaType=='application/gzip')
.map((resource: ResourceMetadata)=>{
return {
statusListName: resource.resourceName,
statusListVersion: resource.resourceVersion,
mediaType: resource.mediaType,
statusListId: resource.resourceId,
statusListNextVersion: resource.nextVersionId
}
})
return response.status(200).json(statusList)
} catch (error) {
return response.status(500).json({
error: `Internal error: ${error}`
})
}
}
}
10 changes: 6 additions & 4 deletions src/services/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
VC_CONTEXT,
VC_TYPE
} from '../types/constants.js'
import { CredentialRequest } from '../types/types.js'
import { CredentialRequest, StatusOptions, VerifyStatusOptions } from '../types/types.js'
import { Identity } from './identity/index.js'
import { VeridaService } from '../services/connectors/verida.js'
import { v4 } from 'uuid'
Expand Down Expand Up @@ -32,7 +32,9 @@ export class Credentials {
credential.expirationDate = request.expirationDate
}

let verifiable_credential = await Identity.instance.createCredential(credential, request.format, agentId)
const statusOptions = request.credentialStatus || null

let verifiable_credential = await Identity.instance.createCredential(credential, request.format, statusOptions, agentId)

if (ENABLE_VERIDA_CONNECTOR === 'true' && request.subjectDid.startsWith('did:vda')) {
await VeridaService.instance.sendCredential(
Expand All @@ -46,8 +48,8 @@ export class Credentials {
return verifiable_credential
}

async verify_credentials(credential: W3CVerifiableCredential | string, agentId: string): Promise<IVerifyResult> {
const result = await Identity.instance.verifyCredential(credential, agentId)
async verify_credentials(credential: W3CVerifiableCredential | string, statusOptions: VerifyStatusOptions | null, agentId: string): Promise<IVerifyResult> {
const result = await Identity.instance.verifyCredential(credential, statusOptions, agentId)
delete(result.payload)
return result
}
Expand Down
21 changes: 12 additions & 9 deletions src/services/identity/IIdentity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
CredentialPayload,
import type {
CredentialPayload,
DIDDocument,
DIDResolutionResult,
IIdentifier,
Expand All @@ -9,11 +9,10 @@ import {
VerifiableCredential,
VerifiablePresentation,
} from '@veramo/core'
import { AbstractPrivateKeyStore } from '@veramo/key-manager'
import { ResourcePayload } from '@cheqd/did-provider-cheqd'
import * as dotenv from 'dotenv'
import { CredentialRequest, VeramoAgent } from '../../types/types'
dotenv.config()
import type { AbstractPrivateKeyStore } from '@veramo/key-manager'
import type { ResourcePayload } from '@cheqd/did-provider-cheqd'
import type { RevocationResult, SuspensionResult, UnsuspensionResult } from '@cheqd/did-provider-cheqd/build/types/agent/ICheqd'
import type { CreateStatusListOptions, CredentialRequest, StatusOptions, VeramoAgent, VerifyStatusOptions } from '../../types/types'

export interface IIdentity {
agent?: TAgent<any>
Expand All @@ -28,7 +27,11 @@ export interface IIdentity {
getDid(did: string, agentId?: string): Promise<any>
importDid(did: string, privateKeyHex: string, publicKeyHex: string, agentId?: string): Promise<IIdentifier>
createResource(network: string, payload: ResourcePayload, agentId?: string): Promise<any>
createCredential(credential: CredentialPayload, format: CredentialRequest['format'], agentId?: string): Promise<VerifiableCredential>
verifyCredential(credential: VerifiableCredential | string, agentId?: string): Promise<IVerifyResult>
createCredential(credential: CredentialPayload, format: CredentialRequest['format'], statusListOptions: StatusOptions | null, agentId?: string): Promise<VerifiableCredential>
verifyCredential(credential: VerifiableCredential | string, statusOptions: VerifyStatusOptions | null, agentId?: string): Promise<IVerifyResult>
verifyPresentation(presentation: VerifiablePresentation | string, agentId?: string): Promise<IVerifyResult>
createStatusList2021(did: string, network: string, resourceOptions: ResourcePayload, statusOptions: CreateStatusListOptions, agentId: string): Promise<boolean>
revokeCredentials(credential: VerifiableCredential | VerifiableCredential[], publish: boolean, agentId?: string): Promise<RevocationResult| RevocationResult[]>
suspendCredentials(credential: VerifiableCredential | VerifiableCredential[], publish: boolean, agentId?: string): Promise<SuspensionResult| SuspensionResult[]>
reinstateCredentials(credential: VerifiableCredential | VerifiableCredential[], publish: boolean, agentId?: string): Promise<UnsuspensionResult| UnsuspensionResult[]>
}
Loading

0 comments on commit c69df47

Please sign in to comment.