Skip to content

Commit

Permalink
Merge branch 'feat/add-revocation-registry' into feat/create-revocati…
Browse files Browse the repository at this point in the history
…on-nestjs-client
  • Loading branch information
lotharking committed Dec 17, 2024
2 parents 92e3c39 + 3c975ef commit d268354
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 104 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ but likely needed for production and test deployments.
| POSTGRES_ADMIN_PASSWORD | PosgreSQL database admin password | None |
| REDIS_HOST | Redis database host user. This system will only function if this variable is defined. (Recommended for production mode) | None |
| REDIS_PASSWORD | Redis database password | None |
| SUPPORTING_REVOCATION | Enables support for revocation features (true/false) | false |


> **Note**: While not mandatory, it is recommended to set an agent public DID matching external hostname (e.g. if your Service Agent instance is accessable in `https://myagent.com:3000` you must set AGENT_PUBLIC_DID to `did:web:myagent.com%3A3000`), which will make possible for the agent to create its own creadential types and therefore issue credentials. Note that you'll need HTTPS in order to fully support did:web specification.
Expand Down
9 changes: 4 additions & 5 deletions doc/service-agent-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,29 +154,28 @@ Parameters:
- (optional) Revocation Index
- (optional) Claims

> **Note:** When using revocation parameters (`revocationDefinitionId` and `revocationRegistryIndex`), it is essential to ensure the `id` was preserved from the time it was generated with the credential. The `revocationRegistryIndex` serves as a reference to the specific credential in the revocation registry.
> **Note:** When using revocation parameters (`revocationRegistryDefinitionId` and `revocationRegistryIndex`), it is essential to ensure the `id` was preserved from the time it was generated with the credential. The `revocationRegistryIndex` serves as a reference to the specific credential in the revocation registry.
```json
{
...
"type": "credential-issuance",
"credentialDefinitionId": "id",
"revocationDefinitionId": "id",
"revocationRegistryDefinitionId": "id",
"revocationRegistryIndex": 1,
"claims": [{ "name": "claim-name", "mimeType": "mime-type", "value": "claim-value" }, ...]
}
```
#### Credential Revocation

By sending this message, a Verifiable Credential is effectively revocated and sent to the destination connection.
By sending this message, a Verifiable Credential is effectively revoked and a notification is sent to the DIDComm connection it has been issued to.

This message could be sent as a response to a Credential issuance. In such case, `connectionId` and `revocationDefinitionId` is used to identify credential details.
This message could be sent as a credential revocation notification. In such case, `threadId` is used to identify credential details.

```json
{
...
"type": "credential-revocation",
"revocationDefinitionId": "id",
}
```

Expand Down
5 changes: 0 additions & 5 deletions examples/chatbot/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ export const rootContextMenu = {
title: 'Issue credential',
id: 'issue',
},
{
title: 'Revoke credential',
id: 'revoke',
},
{
title: 'Request proof',
id: 'proof',
Expand All @@ -44,7 +40,6 @@ export const rootMenuAsQA = {
{ id: 'poll', text: '⚽ World Cup poll' },
{ id: 'rocky', text: '💪 Rocky quotes' },
{ id: 'issue', text: 'Issue credential' },
{ id: 'revoke', text: 'Revoke credential' },
{ id: 'proof', text: 'Request proof' },
{ id: 'help', text: '🆘 Help' },
],
Expand Down
45 changes: 26 additions & 19 deletions examples/chatbot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
VerifiableCredentialSubmittedProofItem,
MediaMessage,
MrtdSubmitState,
CredentialReceptionMessage,
} from '@2060.io/service-agent-model'
import cors from 'cors'
import { randomUUID } from 'crypto'
Expand Down Expand Up @@ -99,9 +100,9 @@ const server = app.listen(PORT, async () => {
phoneNumberCredentialDefinitionId =
phoneNumberCredentialType?.id ?? credentialDefinition.id
phoneNumberRevocationDefinitionId =
phoneNumberCredentialType?.revocationId ?? credentialDefinition.revocationId
phoneNumberCredentialType?.revocationId ? phoneNumberCredentialType?.revocationId?.[0] : credentialDefinition.revocationId?.[0]
logger.info(`phoneNumberCredentialDefinitionId: ${phoneNumberCredentialDefinitionId}`)
logger.info(`phoneNumberRevocationDefinitionId: ${phoneNumberRevocationDefinitionId}`)
logger.info(`phoneNumberRevocationDefinitionId: ${credentialDefinition.revocationId}`)
} catch (error) {
logger.error(`Could not create or retrieve phone number credential type: ${error}`)
}
Expand Down Expand Up @@ -186,29 +187,13 @@ const handleMenuSelection = async (options: { connectionId: string; item: string
value: '+5712345678',
},
],
revocationDefinitionId: phoneNumberRevocationDefinitionId,
revocationRegistryDefinitionId: phoneNumberRevocationDefinitionId,
revocationRegistryIndex: phoneNumberRevocationCount += 1,
})
await apiClient.messages.send(body)
}
}

if (selectedItem === 'revoke' || selectedItem === 'Revoke credential') {
if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' ||
!phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') {
await sendTextMessage({
connectionId,
content: 'Service not available',
})
} else {
const body = new CredentialRevocationMessage({
connectionId,
revocationDefinitionId: phoneNumberRevocationDefinitionId,
})
await apiClient.messages.send(body)
}
}

// Proof
if (selectedItem === 'proof' || selectedItem === 'Request proof') {
if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '') {
Expand Down Expand Up @@ -437,6 +422,22 @@ expressHandler.messageReceived(async (req, res) => {
connectionId,
})
await apiClient.messages.send(body)
} else if (content.startsWith('/revoke')) {
const parsedContents = content.split(' ')
let threadId = parsedContents[1]
if (!phoneNumberCredentialDefinitionId || phoneNumberCredentialDefinitionId === '' ||
!phoneNumberRevocationDefinitionId || phoneNumberRevocationDefinitionId === '') {
await sendTextMessage({
connectionId,
content: 'Service not available',
})
} else {
const body = new CredentialRevocationMessage({
connectionId,
threadId,
})
await apiClient.messages.send(body)
}
} else if (content.startsWith('/proof')) {
const body = new IdentityProofRequestMessage({
connectionId,
Expand Down Expand Up @@ -520,6 +521,12 @@ expressHandler.messageReceived(async (req, res) => {
content: `Problem: emrtd ${obj.state}`,
})
}
} else if (obj.type === CredentialReceptionMessage.type) {
await submitMessageReceipt(obj, 'viewed')
obj.state === 'done' && await sendTextMessage({
connectionId: obj.connectionId,
content: `For revocation, please provide the thread ID: ${obj.threadId}`,
})
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ import { CreateCredentialTypeDto } from './CredentialTypeDto'
})
export class CredentialTypesController {
private readonly logger = new Logger(CredentialTypesController.name)
private readonly supportRevocation: boolean

constructor(private readonly agentService: AgentService) {}
constructor(private readonly agentService: AgentService) {
this.supportRevocation = process.env.SUPPORTING_REVOCATION === 'true'
}

/**
* Get all created credential types
Expand All @@ -57,13 +60,14 @@ export class CredentialTypesController {
const schemaResult = await agent.modules.anoncreds.getSchema(record.credentialDefinition.schemaId)

const schema = schemaResult.schema
const revocationRegistryIds = record.getTag('revocationRegistryDefinitionId') as string

return {
id: record.credentialDefinitionId,
name: (record.getTag('name') as string) ?? schema?.name,
version: (record.getTag('version') as string) ?? schema?.version,
attributes: schema?.attrNames || [],
revocationId: record.getTag('revocationDefinitionId') as string,
revocationId: revocationRegistryIds ? revocationRegistryIds.split('::') : undefined,
}
}),
)
Expand Down Expand Up @@ -133,7 +137,7 @@ export class CredentialTypesController {

const registrationResult = await agent.modules.anoncreds.registerCredentialDefinition({
credentialDefinition: { issuerId, schemaId, tag: `${options.name}.${options.version}` },
options: { supportRevocation: true },
options: { supportRevocation: this.supportRevocation },
})

const credentialDefinitionId = registrationResult.credentialDefinitionState.credentialDefinitionId
Expand All @@ -147,37 +151,53 @@ export class CredentialTypesController {
)
}

const revocationResult = await agent.modules.anoncreds.registerRevocationRegistryDefinition({
revocationRegistryDefinition: {
credentialDefinitionId,
tag: 'default',
maximumCredentialNumber: 1000,
issuerId,
},
options: {},
})
const revocationDefinitionId =
revocationResult.revocationRegistryDefinitionState.revocationRegistryDefinitionId
this.logger.debug!(
`revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`,
)
let revocationRegistryDefinitionId
if (this.supportRevocation) {
const revocationResult = await agent.modules.anoncreds.registerRevocationRegistryDefinition({
revocationRegistryDefinition: {
credentialDefinitionId,
tag: 'default',
maximumCredentialNumber: 1000,
issuerId,
},
options: {},
})
revocationRegistryDefinitionId =
revocationResult.revocationRegistryDefinitionState.revocationRegistryDefinitionId
this.logger.debug!(
`revocationRegistryDefinitionState: ${JSON.stringify(revocationResult.revocationRegistryDefinitionState)}`,
)

if (!revocationDefinitionId) {
throw new Error(
`Cannot create credential revocations: ${JSON.stringify(registrationResult.registrationMetadata)}`,
if (!revocationRegistryDefinitionId) {
throw new Error(
`Cannot create credential revocations: ${JSON.stringify(registrationResult.registrationMetadata)}`,
)
}

const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({
revocationStatusList: {
issuerId,
revocationRegistryDefinitionId: revocationRegistryDefinitionId,
},
options: {},
})
const revocationDefinitionRepository = agent.dependencyManager.resolve(
AnonCredsRevocationRegistryDefinitionRepository,
)
const revocationDefinitionRecord =
await revocationDefinitionRepository.getByRevocationRegistryDefinitionId(
agent.context,
revocationRegistryDefinitionId,
)
revocationDefinitionRecord.metadata.set(
'revStatusList',
revStatusListResult.revocationStatusListState.revocationStatusList!,
)
await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord)
}

const revStatusListResult = await agent.modules.anoncreds.registerRevocationStatusList({
revocationStatusList: {
issuerId,
revocationRegistryDefinitionId: revocationDefinitionId,
},
options: {},
})

this.logger.log(`Credential Definition Id: ${credentialDefinitionId}`)
this.logger.log(`Revocation Definition Id: ${revocationDefinitionId}`)
this.logger.log(`Revocation Registry Definition Id: ${revocationRegistryDefinitionId}`)

// Apply name and version as tags
const credentialDefinitionRepository = agent.dependencyManager.resolve(
Expand All @@ -187,32 +207,19 @@ export class CredentialTypesController {
agent.context,
credentialDefinitionId,
)
const revocationDefinitionRepository = agent.dependencyManager.resolve(
AnonCredsRevocationRegistryDefinitionRepository,
)
const revocationDefinitionRecord =
await revocationDefinitionRepository.getByRevocationRegistryDefinitionId(
agent.context,
revocationDefinitionId,
)
credentialDefinitionRecord.setTag('name', options.name)
credentialDefinitionRecord.setTag('version', options.version)
credentialDefinitionRecord.setTag('revocationDefinitionId', revocationDefinitionId)
credentialDefinitionRecord.setTag('revocationRegistryDefinitionId', revocationRegistryDefinitionId)

await credentialDefinitionRepository.update(agent.context, credentialDefinitionRecord)
revocationDefinitionRecord.metadata.set(
'revStatusList',
revStatusListResult.revocationStatusListState.revocationStatusList!,
)
await revocationDefinitionRepository.update(agent.context, revocationDefinitionRecord)

return {
id: credentialDefinitionId,
attributes: schema.attrNames,
name: options.name,
version: options.version,
schemaId,
revocationId: revocationDefinitionId,
revocationId: revocationRegistryDefinitionId?.split('::'),
}
} catch (error) {
throw new HttpException(
Expand Down
38 changes: 26 additions & 12 deletions packages/main/src/controllers/message/MessageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class MessageService {
return { name: item.name, mimeType: item.mimeType, value: item.value }
}),
credentialDefinitionId: msg.credentialDefinitionId,
revocationRegistryDefinitionId: msg.revocationDefinitionId,
revocationRegistryDefinitionId: msg.revocationRegistryDefinitionId,
revocationRegistryIndex: msg.revocationRegistryIndex,
},
},
Expand All @@ -241,17 +241,31 @@ export class MessageService {
} else if (messageType === CredentialRevocationMessage.type) {
const msg = JsonTransformer.fromJSON(message, CredentialRevocationMessage)

const credential = (await agent.credentials.getAll()).find(
item =>
item.getTag('anonCredsRevocationRegistryId') === msg.revocationDefinitionId &&
item.connectionId === msg.connectionId,
)
if (credential) {
await agent.credentials.sendRevocationNotification({
credentialRecordId: credential.id,
revocationFormat: 'anoncreds',
revocationId: `${credential.getTag('anonCredsRevocationRegistryId')}::${credential.getTag('anonCredsCredentialRevocationId')}`,
})
const credentials = await agent.credentials.findAllByQuery({ threadId: msg.threadId })
if (credentials && credentials.length > 0) {
for (const credential of credentials) {
const isRevoke = Boolean(
credential.getTag('anonCredsRevocationRegistryId') &&
credential.getTag('anonCredsCredentialRevocationId'),
)
isRevoke &&
(await agent.modules.anoncreds.updateRevocationStatusList({
revocationStatusList: {
revocationRegistryDefinitionId: credential.getTag(
'anonCredsRevocationRegistryId',
) as string,
revokedCredentialIndexes: [Number(credential.getTag('anonCredsCredentialRevocationId'))],
},
options: {},
}))

isRevoke &&
(await agent.credentials.sendRevocationNotification({
credentialRecordId: credential.id,
revocationFormat: 'anoncreds',
revocationId: `${credential.getTag('anonCredsRevocationRegistryId')}::${credential.getTag('anonCredsCredentialRevocationId')}`,
}))
}
} else {
throw new Error(`No credentials were found for connection: ${msg.connectionId}.`)
}
Expand Down
10 changes: 3 additions & 7 deletions packages/model/src/messages/CredentialIssuanceMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class Claim {

export interface CredentialIssuanceMessageOptions extends BaseMessageOptions {
credentialDefinitionId: string
revocationDefinitionId?: string
revocationRegistryDefinitionId?: string
revocationRegistryIndex?: number
claims?: Claim[]
}
Expand All @@ -52,7 +52,7 @@ export class CredentialIssuanceMessage extends BaseMessage {
this.timestamp = options.timestamp ?? new Date()
this.connectionId = options.connectionId
this.credentialDefinitionId = options.credentialDefinitionId
this.revocationDefinitionId = options.revocationDefinitionId
this.revocationRegistryDefinitionId = options.revocationRegistryDefinitionId
this.revocationRegistryIndex = options.revocationRegistryIndex
this.claims = options.claims?.map(item => new Claim(item))
}
Expand All @@ -61,21 +61,17 @@ export class CredentialIssuanceMessage extends BaseMessage {
public readonly type = CredentialIssuanceMessage.type
public static readonly type = MessageType.CredentialIssuanceMessage

@Expose()
@IsString()
public credentialDefinitionId?: string

@Expose()
@IsString()
@IsOptional()
public revocationDefinitionId?: string
public revocationRegistryDefinitionId?: string

@Expose()
@IsNumber()
@IsOptional()
public revocationRegistryIndex?: number

@Expose()
@Type(() => Claim)
@IsArray()
@ValidateNested()
Expand Down
Loading

0 comments on commit d268354

Please sign in to comment.