/tests/jest.config.ts',
diff --git a/lerna.json b/lerna.json
index 56d3a038a2..b8106b0a8a 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"packages": ["packages/*"],
- "version": "0.3.2",
+ "version": "0.3.3",
"useWorkspaces": true,
"npmClient": "yarn",
"command": {
diff --git a/package.json b/package.json
index 139ef64ae7..222fb1d497 100644
--- a/package.json
+++ b/package.json
@@ -23,48 +23,46 @@
"test": "jest",
"lint": "eslint --ignore-path .gitignore .",
"validate": "yarn lint && yarn check-types && yarn check-format",
- "prepare": "husky install",
"run-mediator": "ts-node ./samples/mediator.ts",
"next-version-bump": "ts-node ./scripts/get-next-bump.ts"
},
"devDependencies": {
"@types/cors": "^2.8.10",
- "@types/eslint": "^7.2.13",
+ "@types/eslint": "^8.21.2",
"@types/express": "^4.17.13",
- "@types/jest": "^26.0.23",
- "@types/node": "^15.14.4",
- "@types/uuid": "^8.3.1",
+ "@types/jest": "^29.5.0",
+ "@types/node": "^16.11.7",
+ "@types/uuid": "^9.0.1",
"@types/varint": "^6.0.0",
- "@types/ws": "^7.4.6",
- "@typescript-eslint/eslint-plugin": "^4.26.1",
- "@typescript-eslint/parser": "^4.26.1",
+ "@types/ws": "^8.5.4",
+ "@typescript-eslint/eslint-plugin": "^5.48.1",
+ "@typescript-eslint/parser": "^5.48.1",
"conventional-changelog-conventionalcommits": "^5.0.0",
"conventional-recommended-bump": "^6.1.0",
"cors": "^2.8.5",
- "dotenv": "^10.0.0",
- "eslint": "^7.28.0",
+ "eslint": "^8.36.0",
"eslint-config-prettier": "^8.3.0",
- "eslint-import-resolver-typescript": "^2.4.0",
+ "eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.23.4",
- "eslint-plugin-prettier": "^3.4.0",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-workspaces": "^0.8.0",
"express": "^4.17.1",
- "husky": "^7.0.1",
- "indy-sdk": "^1.16.0-dev-1636",
- "jest": "^27.0.4",
- "lerna": "^4.0.0",
+ "indy-sdk": "^1.16.0-dev-1655",
+ "jest": "^29.5.0",
+ "lerna": "^6.5.1",
"prettier": "^2.3.1",
- "rxjs": "^7.2.0",
- "ts-jest": "^27.0.3",
+ "rxjs": "^7.8.0",
+ "ts-jest": "^29.0.5",
"ts-node": "^10.0.0",
- "tsconfig-paths": "^3.9.0",
+ "tsconfig-paths": "^4.1.2",
"tsyringe": "^4.7.0",
- "typescript": "~4.3.0",
- "ws": "^7.4.6"
+ "typescript": "~4.9.5",
+ "ws": "^8.13.0"
},
"resolutions": {
- "@types/node": "^15.14.4"
+ "@types/node": "^16.11.7"
},
"engines": {
- "node": ">= 14"
+ "node": "^16 || ^18"
}
}
diff --git a/packages/action-menu/CHANGELOG.md b/packages/action-menu/CHANGELOG.md
index c3167eb95d..a18e55cd11 100644
--- a/packages/action-menu/CHANGELOG.md
+++ b/packages/action-menu/CHANGELOG.md
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [0.3.3](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.2...v0.3.3) (2023-01-18)
+
+### Bug Fixes
+
+- fix typing issues with typescript 4.9 ([#1214](https://github.com/hyperledger/aries-framework-javascript/issues/1214)) ([087980f](https://github.com/hyperledger/aries-framework-javascript/commit/087980f1adf3ee0bc434ca9782243a62c6124444))
+
+### Features
+
+- **indy-sdk:** add indy-sdk package ([#1200](https://github.com/hyperledger/aries-framework-javascript/issues/1200)) ([9933b35](https://github.com/hyperledger/aries-framework-javascript/commit/9933b35a6aa4524caef8a885e71b742cd0d7186b))
+
## [0.3.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.1...v0.3.2) (2023-01-04)
**Note:** Version bump only for package @aries-framework/action-menu
diff --git a/packages/action-menu/README.md b/packages/action-menu/README.md
index ffd98caf55..c47c6a4ac7 100644
--- a/packages/action-menu/README.md
+++ b/packages/action-menu/README.md
@@ -6,7 +6,7 @@
height="250px"
/>
-Aries Framework JavaScript Action Menu Plugin
+Aries Framework JavaScript Action Menu Module
-Action Menu plugin for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). Implements [Aries RFC 0509](https://github.com/hyperledger/aries-rfcs/blob/1795d5c2d36f664f88f5e8045042ace8e573808c/features/0509-action-menu/README.md).
+Action Menu module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). Implements [Aries RFC 0509](https://github.com/hyperledger/aries-rfcs/blob/1795d5c2d36f664f88f5e8045042ace8e573808c/features/0509-action-menu/README.md).
### Installation
@@ -38,7 +38,7 @@ Make sure you have set up the correct version of Aries Framework JavaScript acco
npm info "@aries-framework/action-menu" peerDependencies
```
-Then add the action-menu plugin to your project.
+Then add the action-menu module to your project.
```sh
yarn add @aries-framework/action-menu
@@ -46,7 +46,7 @@ yarn add @aries-framework/action-menu
### Quick start
-In order for this plugin to work, we have to inject it into the agent to access agent functionality. See the example for more information.
+In order for this module to work, we have to inject it into the agent to access agent functionality. See the example for more information.
### Example of usage
diff --git a/packages/action-menu/jest.config.ts b/packages/action-menu/jest.config.ts
index 55c67d70a6..93c0197296 100644
--- a/packages/action-menu/jest.config.ts
+++ b/packages/action-menu/jest.config.ts
@@ -6,7 +6,6 @@ import packageJson from './package.json'
const config: Config.InitialOptions = {
...base,
- name: packageJson.name,
displayName: packageJson.name,
setupFilesAfterEnv: ['./tests/setup.ts'],
}
diff --git a/packages/action-menu/package.json b/packages/action-menu/package.json
index 795c85f463..1a07638d9b 100644
--- a/packages/action-menu/package.json
+++ b/packages/action-menu/package.json
@@ -2,7 +2,7 @@
"name": "@aries-framework/action-menu",
"main": "build/index",
"types": "build/index",
- "version": "0.3.2",
+ "version": "0.3.3",
"files": [
"build"
],
@@ -18,24 +18,20 @@
},
"scripts": {
"build": "yarn run clean && yarn run compile",
- "clean": "rimraf -rf ./build",
+ "clean": "rimraf ./build",
"compile": "tsc -p tsconfig.build.json",
"prepublishOnly": "yarn run build",
"test": "jest"
},
"dependencies": {
+ "@aries-framework/core": "0.3.3",
"class-transformer": "0.5.1",
- "class-validator": "0.13.1",
+ "class-validator": "0.14.0",
"rxjs": "^7.2.0"
},
- "peerDependencies": {
- "@aries-framework/core": "0.2.5"
- },
"devDependencies": {
- "@aries-framework/core": "0.3.2",
- "@aries-framework/node": "0.3.2",
"reflect-metadata": "^0.1.13",
- "rimraf": "~3.0.2",
- "typescript": "~4.3.0"
+ "rimraf": "^4.4.0",
+ "typescript": "~4.9.5"
}
}
diff --git a/packages/action-menu/src/ActionMenuApi.ts b/packages/action-menu/src/ActionMenuApi.ts
index bb6f3cd4f3..6abe1b3fac 100644
--- a/packages/action-menu/src/ActionMenuApi.ts
+++ b/packages/action-menu/src/ActionMenuApi.ts
@@ -10,7 +10,6 @@ import {
AgentContext,
AriesFrameworkError,
ConnectionService,
- Dispatcher,
MessageSender,
OutboundMessageContext,
injectable,
@@ -36,7 +35,6 @@ export class ActionMenuApi {
private agentContext: AgentContext
public constructor(
- dispatcher: Dispatcher,
connectionService: ConnectionService,
messageSender: MessageSender,
actionMenuService: ActionMenuService,
@@ -46,7 +44,13 @@ export class ActionMenuApi {
this.messageSender = messageSender
this.actionMenuService = actionMenuService
this.agentContext = agentContext
- this.registerMessageHandlers(dispatcher)
+
+ this.agentContext.dependencyManager.registerMessageHandlers([
+ new ActionMenuProblemReportHandler(this.actionMenuService),
+ new MenuMessageHandler(this.actionMenuService),
+ new MenuRequestMessageHandler(this.actionMenuService),
+ new PerformMessageHandler(this.actionMenuService),
+ ])
}
/**
@@ -160,11 +164,4 @@ export class ActionMenuApi {
return actionMenuRecord ? await this.actionMenuService.clearMenu(this.agentContext, { actionMenuRecord }) : null
}
-
- private registerMessageHandlers(dispatcher: Dispatcher) {
- dispatcher.registerMessageHandler(new ActionMenuProblemReportHandler(this.actionMenuService))
- dispatcher.registerMessageHandler(new MenuMessageHandler(this.actionMenuService))
- dispatcher.registerMessageHandler(new MenuRequestMessageHandler(this.actionMenuService))
- dispatcher.registerMessageHandler(new PerformMessageHandler(this.actionMenuService))
- }
}
diff --git a/packages/action-menu/src/index.ts b/packages/action-menu/src/index.ts
index 204d9dc359..97c9a70ea7 100644
--- a/packages/action-menu/src/index.ts
+++ b/packages/action-menu/src/index.ts
@@ -5,4 +5,5 @@ export * from './ActionMenuEvents'
export * from './ActionMenuRole'
export * from './ActionMenuState'
export * from './models'
-export * from './repository/ActionMenuRecord'
+export * from './repository'
+export * from './messages'
diff --git a/packages/action-menu/src/services/ActionMenuService.ts b/packages/action-menu/src/services/ActionMenuService.ts
index 89c27f54a4..42da57e3ad 100644
--- a/packages/action-menu/src/services/ActionMenuService.ts
+++ b/packages/action-menu/src/services/ActionMenuService.ts
@@ -1,5 +1,3 @@
-import type { ActionMenuStateChangedEvent } from '../ActionMenuEvents'
-import type { ActionMenuProblemReportMessage } from '../messages'
import type {
ClearMenuOptions,
CreateMenuOptions,
@@ -7,6 +5,8 @@ import type {
CreateRequestOptions,
FindMenuOptions,
} from './ActionMenuServiceOptions'
+import type { ActionMenuStateChangedEvent } from '../ActionMenuEvents'
+import type { ActionMenuProblemReportMessage } from '../messages'
import type { AgentContext, InboundMessageContext, Logger, Query } from '@aries-framework/core'
import { AgentConfig, EventEmitter, AriesFrameworkError, injectable, JsonTransformer } from '@aries-framework/core'
@@ -63,7 +63,7 @@ export class ActionMenuService {
connectionId: options.connection.id,
role: ActionMenuRole.Requester,
state: ActionMenuState.AwaitingRootMenu,
- threadId: menuRequestMessage.id,
+ threadId: menuRequestMessage.threadId,
})
await this.actionMenuRepository.save(agentContext, actionMenuRecord)
@@ -102,7 +102,7 @@ export class ActionMenuService {
connectionId: connection.id,
role: ActionMenuRole.Responder,
state: ActionMenuState.PreparingRootMenu,
- threadId: menuRequestMessage.id,
+ threadId: menuRequestMessage.threadId,
})
await this.actionMenuRepository.save(agentContext, actionMenuRecord)
@@ -157,7 +157,7 @@ export class ActionMenuService {
role: ActionMenuRole.Responder,
state: ActionMenuState.AwaitingSelection,
menu: options.menu,
- threadId: menuMessage.id,
+ threadId: menuMessage.threadId,
})
await this.actionMenuRepository.save(agentContext, actionMenuRecord)
@@ -203,7 +203,7 @@ export class ActionMenuService {
connectionId: connection.id,
role: ActionMenuRole.Requester,
state: ActionMenuState.PreparingSelection,
- threadId: menuMessage.id,
+ threadId: menuMessage.threadId,
menu: new ActionMenu({
title: menuMessage.title,
description: menuMessage.description,
diff --git a/packages/action-menu/tests/action-menu.e2e.test.ts b/packages/action-menu/tests/action-menu.e2e.test.ts
index b15524fd93..a32b13df49 100644
--- a/packages/action-menu/tests/action-menu.e2e.test.ts
+++ b/packages/action-menu/tests/action-menu.e2e.test.ts
@@ -1,13 +1,9 @@
-import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport'
import type { ConnectionRecord } from '@aries-framework/core'
import { Agent } from '@aries-framework/core'
-import { Subject } from 'rxjs'
-import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport'
-import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport'
-import { getAgentOptions, makeConnection } from '../../core/tests/helpers'
-import testLogger from '../../core/tests/logger'
+import { getAgentOptions, makeConnection, testLogger, setupSubjectTransports, indySdk } from '../../core/tests'
+import { IndySdkModule } from '../../indy-sdk/src'
import { waitForActionMenuRecord } from './helpers'
@@ -19,14 +15,19 @@ import {
ActionMenuState,
} from '@aries-framework/action-menu'
+const modules = {
+ actionMenu: new ActionMenuModule(),
+ indySdk: new IndySdkModule({
+ indySdk,
+ }),
+}
+
const faberAgentOptions = getAgentOptions(
'Faber Action Menu',
{
endpoints: ['rxjs:faber'],
},
- {
- actionMenu: new ActionMenuModule(),
- }
+ modules
)
const aliceAgentOptions = getAgentOptions(
@@ -34,18 +35,12 @@ const aliceAgentOptions = getAgentOptions(
{
endpoints: ['rxjs:alice'],
},
- {
- actionMenu: new ActionMenuModule(),
- }
+ modules
)
describe('Action Menu', () => {
- let faberAgent: Agent<{
- actionMenu: ActionMenuModule
- }>
- let aliceAgent: Agent<{
- actionMenu: ActionMenuModule
- }>
+ let faberAgent: Agent
+ let aliceAgent: Agent
let faberConnection: ConnectionRecord
let aliceConnection: ConnectionRecord
@@ -84,21 +79,12 @@ describe('Action Menu', () => {
})
beforeEach(async () => {
- const faberMessages = new Subject()
- const aliceMessages = new Subject()
- const subjectMap = {
- 'rxjs:faber': faberMessages,
- 'rxjs:alice': aliceMessages,
- }
-
faberAgent = new Agent(faberAgentOptions)
- faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages))
- faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
- await faberAgent.initialize()
-
aliceAgent = new Agent(aliceAgentOptions)
- aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages))
- aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
+
+ setupSubjectTransports([faberAgent, aliceAgent])
+
+ await faberAgent.initialize()
await aliceAgent.initialize()
;[aliceConnection, faberConnection] = await makeConnection(aliceAgent, faberAgent)
})
diff --git a/packages/action-menu/tests/setup.ts b/packages/action-menu/tests/setup.ts
index 4955aeb601..78143033f2 100644
--- a/packages/action-menu/tests/setup.ts
+++ b/packages/action-menu/tests/setup.ts
@@ -1,3 +1,3 @@
import 'reflect-metadata'
-jest.setTimeout(20000)
+jest.setTimeout(120000)
diff --git a/packages/anoncreds-rs/README.md b/packages/anoncreds-rs/README.md
new file mode 100644
index 0000000000..87f28670e7
--- /dev/null
+++ b/packages/anoncreds-rs/README.md
@@ -0,0 +1,31 @@
+
+
+
+
+Aries Framework JavaScript AnonCreds RS Module
+
+
+
+
+
+
+
+
+AnonCreds RS module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git).
diff --git a/packages/didcomm-v2/jest.config.ts b/packages/anoncreds-rs/jest.config.ts
similarity index 91%
rename from packages/didcomm-v2/jest.config.ts
rename to packages/anoncreds-rs/jest.config.ts
index 55c67d70a6..93c0197296 100644
--- a/packages/didcomm-v2/jest.config.ts
+++ b/packages/anoncreds-rs/jest.config.ts
@@ -6,7 +6,6 @@ import packageJson from './package.json'
const config: Config.InitialOptions = {
...base,
- name: packageJson.name,
displayName: packageJson.name,
setupFilesAfterEnv: ['./tests/setup.ts'],
}
diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json
new file mode 100644
index 0000000000..633ea1f08e
--- /dev/null
+++ b/packages/anoncreds-rs/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@aries-framework/anoncreds-rs",
+ "main": "build/index",
+ "types": "build/index",
+ "version": "0.3.3",
+ "files": [
+ "build"
+ ],
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/anoncreds-rs",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/hyperledger/aries-framework-javascript",
+ "directory": "packages/anoncreds-rs"
+ },
+ "scripts": {
+ "build": "yarn run clean && yarn run compile",
+ "clean": "rimraf ./build",
+ "compile": "tsc -p tsconfig.build.json",
+ "prepublishOnly": "yarn run build",
+ "test": "jest"
+ },
+ "dependencies": {
+ "@aries-framework/core": "0.3.3",
+ "@aries-framework/anoncreds": "0.3.3",
+ "@hyperledger/anoncreds-shared": "^0.1.0-dev.15",
+ "class-transformer": "^0.5.1",
+ "class-validator": "0.14.0",
+ "rxjs": "^7.2.0",
+ "tsyringe": "^4.7.0"
+ },
+ "devDependencies": {
+ "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.15",
+ "reflect-metadata": "^0.1.13",
+ "rimraf": "^4.4.0",
+ "typescript": "~4.9.5"
+ }
+}
diff --git a/packages/anoncreds-rs/src/AnonCredsRsModule.ts b/packages/anoncreds-rs/src/AnonCredsRsModule.ts
new file mode 100644
index 0000000000..cca3f465fd
--- /dev/null
+++ b/packages/anoncreds-rs/src/AnonCredsRsModule.ts
@@ -0,0 +1,28 @@
+import type { AnonCredsRsModuleConfigOptions } from './AnonCredsRsModuleConfig'
+import type { DependencyManager, Module } from '@aries-framework/core'
+
+import {
+ AnonCredsHolderServiceSymbol,
+ AnonCredsIssuerServiceSymbol,
+ AnonCredsVerifierServiceSymbol,
+} from '@aries-framework/anoncreds'
+
+import { AnonCredsRsModuleConfig } from './AnonCredsRsModuleConfig'
+import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from './services'
+
+export class AnonCredsRsModule implements Module {
+ public readonly config: AnonCredsRsModuleConfig
+
+ public constructor(config: AnonCredsRsModuleConfigOptions) {
+ this.config = new AnonCredsRsModuleConfig(config)
+ }
+
+ public register(dependencyManager: DependencyManager) {
+ dependencyManager.registerInstance(AnonCredsRsModuleConfig, this.config)
+
+ // Register services
+ dependencyManager.registerSingleton(AnonCredsHolderServiceSymbol, AnonCredsRsHolderService)
+ dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, AnonCredsRsIssuerService)
+ dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, AnonCredsRsVerifierService)
+ }
+}
diff --git a/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts b/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts
new file mode 100644
index 0000000000..2d676b4d52
--- /dev/null
+++ b/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts
@@ -0,0 +1,58 @@
+import type { Anoncreds } from '@hyperledger/anoncreds-shared'
+
+/**
+ * @public
+ * AnonCredsRsModuleConfigOptions defines the interface for the options of the AnonCredsRsModuleConfig class.
+ */
+export interface AnonCredsRsModuleConfigOptions {
+ /**
+ *
+ * ## Node.JS
+ *
+ * ```ts
+ * import { anoncreds } from '@hyperledger/anoncreds-nodejs'
+ *
+ * const agent = new Agent({
+ * config: {},
+ * dependencies: agentDependencies,
+ * modules: {
+ * anoncredsRs: new AnoncredsRsModule({
+ * anoncreds,
+ * })
+ * }
+ * })
+ * ```
+ *
+ * ## React Native
+ *
+ * ```ts
+ * import { anoncreds } from '@hyperledger/anoncreds-react-native'
+ *
+ * const agent = new Agent({
+ * config: {},
+ * dependencies: agentDependencies,
+ * modules: {
+ * anoncredsRs: new AnoncredsRsModule({
+ * anoncreds,
+ * })
+ * }
+ * })
+ * ```
+ */
+ anoncreds: Anoncreds
+}
+
+/**
+ * @public
+ */
+export class AnonCredsRsModuleConfig {
+ private options: AnonCredsRsModuleConfigOptions
+
+ public constructor(options: AnonCredsRsModuleConfigOptions) {
+ this.options = options
+ }
+
+ public get anoncreds() {
+ return this.options.anoncreds
+ }
+}
diff --git a/packages/anoncreds-rs/src/errors/AnonCredsRsError.ts b/packages/anoncreds-rs/src/errors/AnonCredsRsError.ts
new file mode 100644
index 0000000000..e8cdf3023d
--- /dev/null
+++ b/packages/anoncreds-rs/src/errors/AnonCredsRsError.ts
@@ -0,0 +1,7 @@
+import { AriesFrameworkError } from '@aries-framework/core'
+
+export class AnonCredsRsError extends AriesFrameworkError {
+ public constructor(message: string, { cause }: { cause?: Error } = {}) {
+ super(message, { cause })
+ }
+}
diff --git a/packages/anoncreds-rs/src/index.ts b/packages/anoncreds-rs/src/index.ts
new file mode 100644
index 0000000000..5fdd9486c7
--- /dev/null
+++ b/packages/anoncreds-rs/src/index.ts
@@ -0,0 +1,5 @@
+// Services
+export * from './services'
+
+// Module
+export { AnonCredsRsModule } from './AnonCredsRsModule'
diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts
new file mode 100644
index 0000000000..20b9c5a74d
--- /dev/null
+++ b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts
@@ -0,0 +1,464 @@
+import type {
+ AnonCredsCredential,
+ AnonCredsCredentialInfo,
+ AnonCredsCredentialRequest,
+ AnonCredsCredentialRequestMetadata,
+ AnonCredsHolderService,
+ AnonCredsProof,
+ AnonCredsProofRequestRestriction,
+ AnonCredsRequestedAttributeMatch,
+ AnonCredsRequestedPredicateMatch,
+ CreateCredentialRequestOptions,
+ CreateCredentialRequestReturn,
+ CreateLinkSecretOptions,
+ CreateLinkSecretReturn,
+ CreateProofOptions,
+ GetCredentialOptions,
+ GetCredentialsForProofRequestOptions,
+ GetCredentialsForProofRequestReturn,
+ GetCredentialsOptions,
+ StoreCredentialOptions,
+} from '@aries-framework/anoncreds'
+import type { AgentContext, Query, SimpleQuery } from '@aries-framework/core'
+import type {
+ CredentialEntry,
+ CredentialProve,
+ CredentialRequestMetadata,
+ JsonObject,
+} from '@hyperledger/anoncreds-shared'
+
+import {
+ AnonCredsCredentialRecord,
+ AnonCredsCredentialRepository,
+ AnonCredsLinkSecretRepository,
+ AnonCredsRestrictionWrapper,
+ unqualifiedCredentialDefinitionIdRegex,
+ AnonCredsRegistryService,
+} from '@aries-framework/anoncreds'
+import { AriesFrameworkError, JsonTransformer, TypedArrayEncoder, injectable, utils } from '@aries-framework/core'
+import {
+ Credential,
+ CredentialRequest,
+ CredentialRevocationState,
+ LinkSecret,
+ Presentation,
+ RevocationRegistryDefinition,
+ RevocationStatusList,
+ anoncreds,
+} from '@hyperledger/anoncreds-shared'
+
+import { AnonCredsRsError } from '../errors/AnonCredsRsError'
+
+@injectable()
+export class AnonCredsRsHolderService implements AnonCredsHolderService {
+ public async createLinkSecret(
+ agentContext: AgentContext,
+ options?: CreateLinkSecretOptions
+ ): Promise {
+ return {
+ linkSecretId: options?.linkSecretId ?? utils.uuid(),
+ linkSecretValue: LinkSecret.create(),
+ }
+ }
+
+ public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise {
+ const { credentialDefinitions, proofRequest, selectedCredentials, schemas } = options
+
+ let presentation: Presentation | undefined
+ try {
+ const rsCredentialDefinitions: Record = {}
+ for (const credDefId in credentialDefinitions) {
+ rsCredentialDefinitions[credDefId] = credentialDefinitions[credDefId] as unknown as JsonObject
+ }
+
+ const rsSchemas: Record = {}
+ for (const schemaId in schemas) {
+ rsSchemas[schemaId] = schemas[schemaId] as unknown as JsonObject
+ }
+
+ const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository)
+
+ // Cache retrieved credentials in order to minimize storage calls
+ const retrievedCredentials = new Map()
+
+ const credentialEntryFromAttribute = async (
+ attribute: AnonCredsRequestedAttributeMatch | AnonCredsRequestedPredicateMatch
+ ): Promise<{ linkSecretId: string; credentialEntry: CredentialEntry }> => {
+ let credentialRecord = retrievedCredentials.get(attribute.credentialId)
+ if (!credentialRecord) {
+ credentialRecord = await credentialRepository.getByCredentialId(agentContext, attribute.credentialId)
+ retrievedCredentials.set(attribute.credentialId, credentialRecord)
+ }
+
+ const revocationRegistryDefinitionId = credentialRecord.credential.rev_reg_id
+ const revocationRegistryIndex = credentialRecord.credentialRevocationId
+
+ // TODO: Check if credential has a revocation registry id (check response from anoncreds-rs API, as it is
+ // sending back a mandatory string in Credential.revocationRegistryId)
+ const timestamp = attribute.timestamp
+
+ let revocationState: CredentialRevocationState | undefined
+ let revocationRegistryDefinition: RevocationRegistryDefinition | undefined
+ try {
+ if (timestamp && revocationRegistryIndex && revocationRegistryDefinitionId) {
+ if (!options.revocationRegistries[revocationRegistryDefinitionId]) {
+ throw new AnonCredsRsError(`Revocation Registry ${revocationRegistryDefinitionId} not found`)
+ }
+
+ const { definition, revocationStatusLists, tailsFilePath } =
+ options.revocationRegistries[revocationRegistryDefinitionId]
+
+ // Extract revocation status list for the given timestamp
+ const revocationStatusList = revocationStatusLists[timestamp]
+ if (!revocationStatusList) {
+ throw new AriesFrameworkError(
+ `Revocation status list for revocation registry ${revocationRegistryDefinitionId} and timestamp ${timestamp} not found in revocation status lists. All revocation status lists must be present.`
+ )
+ }
+
+ revocationRegistryDefinition = RevocationRegistryDefinition.fromJson(definition as unknown as JsonObject)
+ revocationState = CredentialRevocationState.create({
+ revocationRegistryIndex: Number(revocationRegistryIndex),
+ revocationRegistryDefinition,
+ tailsPath: tailsFilePath,
+ revocationStatusList: RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject),
+ })
+ }
+ return {
+ linkSecretId: credentialRecord.linkSecretId,
+ credentialEntry: {
+ credential: credentialRecord.credential as unknown as JsonObject,
+ revocationState: revocationState?.toJson(),
+ timestamp,
+ },
+ }
+ } finally {
+ revocationState?.handle.clear()
+ revocationRegistryDefinition?.handle.clear()
+ }
+ }
+
+ const credentialsProve: CredentialProve[] = []
+ const credentials: { linkSecretId: string; credentialEntry: CredentialEntry }[] = []
+
+ let entryIndex = 0
+ for (const referent in selectedCredentials.attributes) {
+ const attribute = selectedCredentials.attributes[referent]
+ credentials.push(await credentialEntryFromAttribute(attribute))
+ credentialsProve.push({ entryIndex, isPredicate: false, referent, reveal: attribute.revealed })
+ entryIndex = entryIndex + 1
+ }
+
+ for (const referent in selectedCredentials.predicates) {
+ const predicate = selectedCredentials.predicates[referent]
+ credentials.push(await credentialEntryFromAttribute(predicate))
+ credentialsProve.push({ entryIndex, isPredicate: true, referent, reveal: true })
+ entryIndex = entryIndex + 1
+ }
+
+ // Get all requested credentials and take linkSecret. If it's not the same for every credential, throw error
+ const linkSecretsMatch = credentials.every((item) => item.linkSecretId === credentials[0].linkSecretId)
+ if (!linkSecretsMatch) {
+ throw new AnonCredsRsError('All credentials in a Proof should have been issued using the same Link Secret')
+ }
+
+ const linkSecretRecord = await agentContext.dependencyManager
+ .resolve(AnonCredsLinkSecretRepository)
+ .getByLinkSecretId(agentContext, credentials[0].linkSecretId)
+
+ if (!linkSecretRecord.value) {
+ throw new AnonCredsRsError('Link Secret value not stored')
+ }
+
+ presentation = Presentation.create({
+ credentialDefinitions: rsCredentialDefinitions,
+ schemas: rsSchemas,
+ presentationRequest: proofRequest as unknown as JsonObject,
+ credentials: credentials.map((entry) => entry.credentialEntry),
+ credentialsProve,
+ selfAttest: selectedCredentials.selfAttestedAttributes,
+ linkSecret: linkSecretRecord.value,
+ })
+
+ return presentation.toJson() as unknown as AnonCredsProof
+ } finally {
+ presentation?.handle.clear()
+ }
+ }
+
+ public async createCredentialRequest(
+ agentContext: AgentContext,
+ options: CreateCredentialRequestOptions
+ ): Promise {
+ const { useLegacyProverDid, credentialDefinition, credentialOffer } = options
+ let createReturnObj:
+ | { credentialRequest: CredentialRequest; credentialRequestMetadata: CredentialRequestMetadata }
+ | undefined
+ try {
+ const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository)
+
+ // If a link secret is specified, use it. Otherwise, attempt to use default link secret
+ const linkSecretRecord = options.linkSecretId
+ ? await linkSecretRepository.getByLinkSecretId(agentContext, options.linkSecretId)
+ : await linkSecretRepository.findDefault(agentContext)
+
+ if (!linkSecretRecord) {
+ // No default link secret
+ throw new AnonCredsRsError(
+ 'No link secret provided to createCredentialRequest and no default link secret has been found'
+ )
+ }
+
+ if (!linkSecretRecord.value) {
+ throw new AnonCredsRsError('Link Secret value not stored')
+ }
+
+ const isLegacyIdentifier = credentialOffer.cred_def_id.match(unqualifiedCredentialDefinitionIdRegex)
+ if (!isLegacyIdentifier && useLegacyProverDid) {
+ throw new AriesFrameworkError('Cannot use legacy prover_did with non-legacy identifiers')
+ }
+ createReturnObj = CredentialRequest.create({
+ entropy: !useLegacyProverDid || !isLegacyIdentifier ? anoncreds.generateNonce() : undefined,
+ proverDid: useLegacyProverDid
+ ? TypedArrayEncoder.toBase58(TypedArrayEncoder.fromString(anoncreds.generateNonce().slice(0, 16)))
+ : undefined,
+ credentialDefinition: credentialDefinition as unknown as JsonObject,
+ credentialOffer: credentialOffer as unknown as JsonObject,
+ linkSecret: linkSecretRecord.value,
+ linkSecretId: linkSecretRecord.linkSecretId,
+ })
+
+ return {
+ credentialRequest: createReturnObj.credentialRequest.toJson() as unknown as AnonCredsCredentialRequest,
+ credentialRequestMetadata:
+ createReturnObj.credentialRequestMetadata.toJson() as unknown as AnonCredsCredentialRequestMetadata,
+ }
+ } finally {
+ createReturnObj?.credentialRequest.handle.clear()
+ createReturnObj?.credentialRequestMetadata.handle.clear()
+ }
+ }
+
+ public async storeCredential(agentContext: AgentContext, options: StoreCredentialOptions): Promise {
+ const { credential, credentialDefinition, credentialRequestMetadata, revocationRegistry, schema } = options
+
+ const linkSecretRecord = await agentContext.dependencyManager
+ .resolve(AnonCredsLinkSecretRepository)
+ .getByLinkSecretId(agentContext, credentialRequestMetadata.link_secret_name)
+
+ if (!linkSecretRecord.value) {
+ throw new AnonCredsRsError('Link Secret value not stored')
+ }
+
+ const revocationRegistryDefinition = revocationRegistry?.definition as unknown as JsonObject
+
+ const credentialId = options.credentialId ?? utils.uuid()
+
+ let credentialObj: Credential | undefined
+ let processedCredential: Credential | undefined
+ try {
+ credentialObj = Credential.fromJson(credential as unknown as JsonObject)
+ processedCredential = credentialObj.process({
+ credentialDefinition: credentialDefinition as unknown as JsonObject,
+ credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject,
+ linkSecret: linkSecretRecord.value,
+ revocationRegistryDefinition,
+ })
+
+ const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository)
+
+ const methodName = agentContext.dependencyManager
+ .resolve(AnonCredsRegistryService)
+ .getRegistryForIdentifier(agentContext, credential.cred_def_id).methodName
+
+ await credentialRepository.save(
+ agentContext,
+ new AnonCredsCredentialRecord({
+ credential: processedCredential.toJson() as unknown as AnonCredsCredential,
+ credentialId,
+ linkSecretId: linkSecretRecord.linkSecretId,
+ issuerId: options.credentialDefinition.issuerId,
+ schemaName: schema.name,
+ schemaIssuerId: schema.issuerId,
+ schemaVersion: schema.version,
+ credentialRevocationId: processedCredential.revocationRegistryIndex?.toString(),
+ methodName,
+ })
+ )
+
+ return credentialId
+ } finally {
+ credentialObj?.handle.clear()
+ processedCredential?.handle.clear()
+ }
+ }
+
+ public async getCredential(
+ agentContext: AgentContext,
+ options: GetCredentialOptions
+ ): Promise {
+ const credentialRecord = await agentContext.dependencyManager
+ .resolve(AnonCredsCredentialRepository)
+ .getByCredentialId(agentContext, options.credentialId)
+
+ const attributes: { [key: string]: string } = {}
+ for (const attribute in credentialRecord.credential.values) {
+ attributes[attribute] = credentialRecord.credential.values[attribute].raw
+ }
+ return {
+ attributes,
+ credentialDefinitionId: credentialRecord.credential.cred_def_id,
+ credentialId: credentialRecord.credentialId,
+ schemaId: credentialRecord.credential.schema_id,
+ credentialRevocationId: credentialRecord.credentialRevocationId,
+ revocationRegistryId: credentialRecord.credential.rev_reg_id,
+ methodName: credentialRecord.methodName,
+ }
+ }
+
+ public async getCredentials(
+ agentContext: AgentContext,
+ options: GetCredentialsOptions
+ ): Promise {
+ const credentialRecords = await agentContext.dependencyManager
+ .resolve(AnonCredsCredentialRepository)
+ .findByQuery(agentContext, {
+ credentialDefinitionId: options.credentialDefinitionId,
+ schemaId: options.schemaId,
+ issuerId: options.issuerId,
+ schemaName: options.schemaName,
+ schemaVersion: options.schemaVersion,
+ schemaIssuerId: options.schemaIssuerId,
+ methodName: options.methodName,
+ })
+
+ return credentialRecords.map((credentialRecord) => ({
+ attributes: Object.fromEntries(
+ Object.entries(credentialRecord.credential.values).map(([key, value]) => [key, value.raw])
+ ),
+ credentialDefinitionId: credentialRecord.credential.cred_def_id,
+ credentialId: credentialRecord.credentialId,
+ schemaId: credentialRecord.credential.schema_id,
+ credentialRevocationId: credentialRecord.credentialRevocationId,
+ revocationRegistryId: credentialRecord.credential.rev_reg_id,
+ methodName: credentialRecord.methodName,
+ }))
+ }
+
+ public async deleteCredential(agentContext: AgentContext, credentialId: string): Promise {
+ const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository)
+ const credentialRecord = await credentialRepository.getByCredentialId(agentContext, credentialId)
+ await credentialRepository.delete(agentContext, credentialRecord)
+ }
+
+ public async getCredentialsForProofRequest(
+ agentContext: AgentContext,
+ options: GetCredentialsForProofRequestOptions
+ ): Promise {
+ const proofRequest = options.proofRequest
+ const referent = options.attributeReferent
+
+ const requestedAttribute =
+ proofRequest.requested_attributes[referent] ?? proofRequest.requested_predicates[referent]
+
+ if (!requestedAttribute) {
+ throw new AnonCredsRsError(`Referent not found in proof request`)
+ }
+
+ const $and = []
+
+ // Make sure the attribute(s) that are requested are present using the marker tag
+ const attributes = requestedAttribute.names ?? [requestedAttribute.name]
+ const attributeQuery: SimpleQuery = {}
+ for (const attribute of attributes) {
+ attributeQuery[`attr::${attribute}::marker`] = true
+ }
+ $and.push(attributeQuery)
+
+ // Add query for proof request restrictions
+ if (requestedAttribute.restrictions) {
+ const restrictionQuery = this.queryFromRestrictions(requestedAttribute.restrictions)
+ $and.push(restrictionQuery)
+ }
+
+ // Add extra query
+ // TODO: we're not really typing the extraQuery, and it will work differently based on the anoncreds implmentation
+ // We should make the allowed properties more strict
+ if (options.extraQuery) {
+ $and.push(options.extraQuery)
+ }
+
+ const credentials = await agentContext.dependencyManager
+ .resolve(AnonCredsCredentialRepository)
+ .findByQuery(agentContext, {
+ $and,
+ })
+
+ return credentials.map((credentialRecord) => {
+ const attributes: { [key: string]: string } = {}
+ for (const attribute in credentialRecord.credential.values) {
+ attributes[attribute] = credentialRecord.credential.values[attribute].raw
+ }
+ return {
+ credentialInfo: {
+ attributes,
+ credentialDefinitionId: credentialRecord.credential.cred_def_id,
+ credentialId: credentialRecord.credentialId,
+ schemaId: credentialRecord.credential.schema_id,
+ credentialRevocationId: credentialRecord.credentialRevocationId,
+ revocationRegistryId: credentialRecord.credential.rev_reg_id,
+ methodName: credentialRecord.methodName,
+ },
+ interval: proofRequest.non_revoked,
+ }
+ })
+ }
+
+ private queryFromRestrictions(restrictions: AnonCredsProofRequestRestriction[]) {
+ const query: Query[] = []
+
+ const { restrictions: parsedRestrictions } = JsonTransformer.fromJSON({ restrictions }, AnonCredsRestrictionWrapper)
+
+ for (const restriction of parsedRestrictions) {
+ const queryElements: SimpleQuery = {}
+
+ if (restriction.credentialDefinitionId) {
+ queryElements.credentialDefinitionId = restriction.credentialDefinitionId
+ }
+
+ if (restriction.issuerId || restriction.issuerDid) {
+ queryElements.issuerId = restriction.issuerId ?? restriction.issuerDid
+ }
+
+ if (restriction.schemaId) {
+ queryElements.schemaId = restriction.schemaId
+ }
+
+ if (restriction.schemaIssuerId || restriction.schemaIssuerDid) {
+ queryElements.schemaIssuerId = restriction.schemaIssuerId ?? restriction.issuerDid
+ }
+
+ if (restriction.schemaName) {
+ queryElements.schemaName = restriction.schemaName
+ }
+
+ if (restriction.schemaVersion) {
+ queryElements.schemaVersion = restriction.schemaVersion
+ }
+
+ for (const [attributeName, attributeValue] of Object.entries(restriction.attributeValues)) {
+ queryElements[`attr::${attributeName}::value`] = attributeValue
+ }
+
+ for (const [attributeName, isAvailable] of Object.entries(restriction.attributeMarkers)) {
+ if (isAvailable) {
+ queryElements[`attr::${attributeName}::marker`] = isAvailable
+ }
+ }
+
+ query.push(queryElements)
+ }
+
+ return query.length === 1 ? query[0] : { $or: query }
+ }
+}
diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts
new file mode 100644
index 0000000000..383c6e94e7
--- /dev/null
+++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts
@@ -0,0 +1,193 @@
+import type {
+ AnonCredsIssuerService,
+ CreateCredentialDefinitionOptions,
+ CreateCredentialOfferOptions,
+ CreateCredentialOptions,
+ CreateCredentialReturn,
+ CreateSchemaOptions,
+ AnonCredsCredentialOffer,
+ AnonCredsSchema,
+ AnonCredsCredentialDefinition,
+ CreateCredentialDefinitionReturn,
+ AnonCredsCredential,
+} from '@aries-framework/anoncreds'
+import type { AgentContext } from '@aries-framework/core'
+import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared'
+
+import {
+ parseIndyDid,
+ getUnqualifiedSchemaId,
+ parseIndySchemaId,
+ isUnqualifiedCredentialDefinitionId,
+ AnonCredsKeyCorrectnessProofRepository,
+ AnonCredsCredentialDefinitionPrivateRepository,
+ AnonCredsCredentialDefinitionRepository,
+} from '@aries-framework/anoncreds'
+import { injectable, AriesFrameworkError } from '@aries-framework/core'
+import { Credential, CredentialDefinition, CredentialOffer, Schema } from '@hyperledger/anoncreds-shared'
+
+import { AnonCredsRsError } from '../errors/AnonCredsRsError'
+
+@injectable()
+export class AnonCredsRsIssuerService implements AnonCredsIssuerService {
+ public async createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise {
+ const { issuerId, name, version, attrNames: attributeNames } = options
+
+ let schema: Schema | undefined
+ try {
+ const schema = Schema.create({
+ issuerId,
+ name,
+ version,
+ attributeNames,
+ })
+
+ return schema.toJson() as unknown as AnonCredsSchema
+ } finally {
+ schema?.handle.clear()
+ }
+ }
+
+ public async createCredentialDefinition(
+ agentContext: AgentContext,
+ options: CreateCredentialDefinitionOptions
+ ): Promise {
+ const { tag, supportRevocation, schema, issuerId, schemaId } = options
+
+ let createReturnObj:
+ | {
+ credentialDefinition: CredentialDefinition
+ credentialDefinitionPrivate: CredentialDefinitionPrivate
+ keyCorrectnessProof: KeyCorrectnessProof
+ }
+ | undefined
+ try {
+ createReturnObj = CredentialDefinition.create({
+ schema: schema as unknown as JsonObject,
+ issuerId,
+ schemaId,
+ tag,
+ supportRevocation,
+ signatureType: 'CL',
+ })
+
+ return {
+ credentialDefinition: createReturnObj.credentialDefinition.toJson() as unknown as AnonCredsCredentialDefinition,
+ credentialDefinitionPrivate: createReturnObj.credentialDefinitionPrivate.toJson(),
+ keyCorrectnessProof: createReturnObj.keyCorrectnessProof.toJson(),
+ }
+ } finally {
+ createReturnObj?.credentialDefinition.handle.clear()
+ createReturnObj?.credentialDefinitionPrivate.handle.clear()
+ createReturnObj?.keyCorrectnessProof.handle.clear()
+ }
+ }
+
+ public async createCredentialOffer(
+ agentContext: AgentContext,
+ options: CreateCredentialOfferOptions
+ ): Promise {
+ const { credentialDefinitionId } = options
+
+ let credentialOffer: CredentialOffer | undefined
+ try {
+ // The getByCredentialDefinitionId supports both qualified and unqualified identifiers, even though the
+ // record is always stored using the qualified identifier.
+ const credentialDefinitionRecord = await agentContext.dependencyManager
+ .resolve(AnonCredsCredentialDefinitionRepository)
+ .getByCredentialDefinitionId(agentContext, options.credentialDefinitionId)
+
+ // We fetch the keyCorrectnessProof based on the credential definition record id, as the
+ // credential definition id passed to this module could be unqualified, and the key correctness
+ // proof is only stored using the qualified identifier.
+ const keyCorrectnessProofRecord = await agentContext.dependencyManager
+ .resolve(AnonCredsKeyCorrectnessProofRepository)
+ .getByCredentialDefinitionId(agentContext, credentialDefinitionRecord.credentialDefinitionId)
+
+ if (!credentialDefinitionRecord) {
+ throw new AnonCredsRsError(`Credential Definition ${credentialDefinitionId} not found`)
+ }
+
+ let schemaId = credentialDefinitionRecord.credentialDefinition.schemaId
+
+ // if the credentialDefinitionId is not qualified, we need to transform the schemaId to also be unqualified
+ if (isUnqualifiedCredentialDefinitionId(options.credentialDefinitionId)) {
+ const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(schemaId)
+ schemaId = getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion)
+ }
+
+ credentialOffer = CredentialOffer.create({
+ credentialDefinitionId,
+ keyCorrectnessProof: keyCorrectnessProofRecord?.value,
+ schemaId,
+ })
+
+ return credentialOffer.toJson() as unknown as AnonCredsCredentialOffer
+ } finally {
+ credentialOffer?.handle.clear()
+ }
+ }
+
+ public async createCredential(
+ agentContext: AgentContext,
+ options: CreateCredentialOptions
+ ): Promise {
+ const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryId } = options
+
+ let credential: Credential | undefined
+ try {
+ if (revocationRegistryId || tailsFilePath) {
+ throw new AriesFrameworkError('Revocation not supported yet')
+ }
+
+ const attributeRawValues: Record = {}
+ const attributeEncodedValues: Record = {}
+
+ Object.keys(credentialValues).forEach((key) => {
+ attributeRawValues[key] = credentialValues[key].raw
+ attributeEncodedValues[key] = credentialValues[key].encoded
+ })
+
+ const credentialDefinitionRecord = await agentContext.dependencyManager
+ .resolve(AnonCredsCredentialDefinitionRepository)
+ .getByCredentialDefinitionId(agentContext, options.credentialRequest.cred_def_id)
+
+ // We fetch the private record based on the cred def id from the cred def record, as the
+ // credential definition id passed to this module could be unqualified, and the private record
+ // is only stored using the qualified identifier.
+ const credentialDefinitionPrivateRecord = await agentContext.dependencyManager
+ .resolve(AnonCredsCredentialDefinitionPrivateRepository)
+ .getByCredentialDefinitionId(agentContext, credentialDefinitionRecord.credentialDefinitionId)
+
+ let credentialDefinition = credentialDefinitionRecord.credentialDefinition
+
+ if (isUnqualifiedCredentialDefinitionId(options.credentialRequest.cred_def_id)) {
+ const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(credentialDefinition.schemaId)
+ const { namespaceIdentifier: unqualifiedDid } = parseIndyDid(credentialDefinition.issuerId)
+ parseIndyDid
+ credentialDefinition = {
+ ...credentialDefinition,
+ schemaId: getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion),
+ issuerId: unqualifiedDid,
+ }
+ }
+
+ credential = Credential.create({
+ credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject,
+ credentialOffer: credentialOffer as unknown as JsonObject,
+ credentialRequest: credentialRequest as unknown as JsonObject,
+ revocationRegistryId,
+ attributeEncodedValues,
+ attributeRawValues,
+ credentialDefinitionPrivate: credentialDefinitionPrivateRecord.value,
+ })
+
+ return {
+ credential: credential.toJson() as unknown as AnonCredsCredential,
+ credentialRevocationId: credential.revocationRegistryIndex?.toString(),
+ }
+ } finally {
+ credential?.handle.clear()
+ }
+ }
+}
diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts
new file mode 100644
index 0000000000..26573309ff
--- /dev/null
+++ b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts
@@ -0,0 +1,49 @@
+import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds'
+import type { AgentContext } from '@aries-framework/core'
+import type { JsonObject } from '@hyperledger/anoncreds-shared'
+
+import { injectable } from '@aries-framework/core'
+import { Presentation } from '@hyperledger/anoncreds-shared'
+
+@injectable()
+export class AnonCredsRsVerifierService implements AnonCredsVerifierService {
+ public async verifyProof(agentContext: AgentContext, options: VerifyProofOptions): Promise {
+ const { credentialDefinitions, proof, proofRequest, revocationRegistries, schemas } = options
+
+ let presentation: Presentation | undefined
+ try {
+ presentation = Presentation.fromJson(proof as unknown as JsonObject)
+
+ const rsCredentialDefinitions: Record = {}
+ for (const credDefId in credentialDefinitions) {
+ rsCredentialDefinitions[credDefId] = credentialDefinitions[credDefId] as unknown as JsonObject
+ }
+
+ const rsSchemas: Record = {}
+ for (const schemaId in schemas) {
+ rsSchemas[schemaId] = schemas[schemaId] as unknown as JsonObject
+ }
+
+ const revocationRegistryDefinitions: Record = {}
+ const lists: JsonObject[] = []
+
+ for (const revocationRegistryDefinitionId in revocationRegistries) {
+ const { definition, revocationStatusLists } = options.revocationRegistries[revocationRegistryDefinitionId]
+
+ revocationRegistryDefinitions[revocationRegistryDefinitionId] = definition as unknown as JsonObject
+
+ lists.push(...(Object.values(revocationStatusLists) as unknown as Array))
+ }
+
+ return presentation.verify({
+ presentationRequest: proofRequest as unknown as JsonObject,
+ credentialDefinitions: rsCredentialDefinitions,
+ schemas: rsSchemas,
+ revocationRegistryDefinitions,
+ revocationStatusLists: lists,
+ })
+ } finally {
+ presentation?.handle.clear()
+ }
+ }
+}
diff --git a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts
new file mode 100644
index 0000000000..b7de80d8ae
--- /dev/null
+++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts
@@ -0,0 +1,624 @@
+import type {
+ AnonCredsCredentialDefinition,
+ AnonCredsProofRequest,
+ AnonCredsRevocationStatusList,
+ AnonCredsCredential,
+ AnonCredsSchema,
+ AnonCredsSelectedCredentials,
+ AnonCredsRevocationRegistryDefinition,
+ AnonCredsCredentialRequestMetadata,
+} from '@aries-framework/anoncreds'
+import type { JsonObject } from '@hyperledger/anoncreds-nodejs'
+
+import {
+ AnonCredsModuleConfig,
+ AnonCredsHolderServiceSymbol,
+ AnonCredsLinkSecretRecord,
+ AnonCredsCredentialRecord,
+} from '@aries-framework/anoncreds'
+import { anoncreds, RevocationRegistryDefinition } from '@hyperledger/anoncreds-nodejs'
+
+import { describeRunInNodeVersion } from '../../../../../tests/runInVersion'
+import { AnonCredsCredentialDefinitionRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialDefinitionRepository'
+import { AnonCredsCredentialRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialRepository'
+import { AnonCredsLinkSecretRepository } from '../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository'
+import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry'
+import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers'
+import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService'
+
+import {
+ createCredentialDefinition,
+ createCredentialForHolder,
+ createCredentialOffer,
+ createLinkSecret,
+} from './helpers'
+
+const agentConfig = getAgentConfig('AnonCredsRsHolderServiceTest')
+const anonCredsHolderService = new AnonCredsRsHolderService()
+
+jest.mock('../../../../anoncreds/src/repository/AnonCredsCredentialDefinitionRepository')
+const CredentialDefinitionRepositoryMock =
+ AnonCredsCredentialDefinitionRepository as jest.Mock
+const credentialDefinitionRepositoryMock = new CredentialDefinitionRepositoryMock()
+
+jest.mock('../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository')
+const AnonCredsLinkSecretRepositoryMock = AnonCredsLinkSecretRepository as jest.Mock
+const anoncredsLinkSecretRepositoryMock = new AnonCredsLinkSecretRepositoryMock()
+
+jest.mock('../../../../anoncreds/src/repository/AnonCredsCredentialRepository')
+const AnonCredsCredentialRepositoryMock = AnonCredsCredentialRepository as jest.Mock
+const anoncredsCredentialRepositoryMock = new AnonCredsCredentialRepositoryMock()
+
+const agentContext = getAgentContext({
+ registerInstances: [
+ [AnonCredsCredentialDefinitionRepository, credentialDefinitionRepositoryMock],
+ [AnonCredsLinkSecretRepository, anoncredsLinkSecretRepositoryMock],
+ [AnonCredsCredentialRepository, anoncredsCredentialRepositoryMock],
+ [AnonCredsHolderServiceSymbol, anonCredsHolderService],
+ [
+ AnonCredsModuleConfig,
+ new AnonCredsModuleConfig({
+ registries: [new InMemoryAnonCredsRegistry({})],
+ }),
+ ],
+ ],
+ agentConfig,
+})
+
+// FIXME: Re-include in tests when NodeJS wrapper performance is improved
+describeRunInNodeVersion([18], 'AnonCredsRsHolderService', () => {
+ const getByCredentialIdMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'getByCredentialId')
+ const findByQueryMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'findByQuery')
+
+ beforeEach(() => {
+ getByCredentialIdMock.mockClear()
+ })
+
+ test('createCredentialRequest', async () => {
+ mockFunction(anoncredsLinkSecretRepositoryMock.getByLinkSecretId).mockResolvedValue(
+ new AnonCredsLinkSecretRecord({ linkSecretId: 'linkSecretId', value: createLinkSecret() })
+ )
+
+ const { credentialDefinition, keyCorrectnessProof } = createCredentialDefinition({
+ attributeNames: ['phoneNumber'],
+ issuerId: 'issuer:uri',
+ })
+ const credentialOffer = createCredentialOffer(keyCorrectnessProof)
+
+ const { credentialRequest } = await anonCredsHolderService.createCredentialRequest(agentContext, {
+ credentialDefinition,
+ credentialOffer,
+ linkSecretId: 'linkSecretId',
+ })
+
+ expect(credentialRequest.cred_def_id).toBe('creddef:uri')
+ expect(credentialRequest.prover_did).toBeUndefined()
+ })
+
+ test('createLinkSecret', async () => {
+ let linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, {
+ linkSecretId: 'linkSecretId',
+ })
+
+ expect(linkSecret.linkSecretId).toBe('linkSecretId')
+ expect(linkSecret.linkSecretValue).toBeDefined()
+
+ linkSecret = await anonCredsHolderService.createLinkSecret(agentContext)
+
+ expect(linkSecret.linkSecretId).toBeDefined()
+ expect(linkSecret.linkSecretValue).toBeDefined()
+ })
+
+ test('createProof', async () => {
+ const proofRequest: AnonCredsProofRequest = {
+ nonce: anoncreds.generateNonce(),
+ name: 'pres_req_1',
+ version: '0.1',
+ requested_attributes: {
+ attr1_referent: {
+ name: 'name',
+ restrictions: [{ issuer_did: 'issuer:uri' }],
+ },
+ attr2_referent: {
+ name: 'phoneNumber',
+ },
+ attr3_referent: {
+ name: 'age',
+ },
+ attr4_referent: {
+ names: ['name', 'height'],
+ },
+ attr5_referent: {
+ name: 'favouriteSport',
+ },
+ },
+ requested_predicates: {
+ predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 },
+ },
+ //non_revoked: { from: 10, to: 200 },
+ }
+
+ const {
+ credentialDefinition: personCredentialDefinition,
+ credentialDefinitionPrivate: personCredentialDefinitionPrivate,
+ keyCorrectnessProof: personKeyCorrectnessProof,
+ } = createCredentialDefinition({
+ attributeNames: ['name', 'age', 'sex', 'height'],
+ issuerId: 'issuer:uri',
+ })
+
+ const {
+ credentialDefinition: phoneCredentialDefinition,
+ credentialDefinitionPrivate: phoneCredentialDefinitionPrivate,
+ keyCorrectnessProof: phoneKeyCorrectnessProof,
+ } = createCredentialDefinition({
+ attributeNames: ['phoneNumber'],
+ issuerId: 'issuer:uri',
+ })
+
+ const linkSecret = createLinkSecret()
+
+ mockFunction(anoncredsLinkSecretRepositoryMock.getByLinkSecretId).mockResolvedValue(
+ new AnonCredsLinkSecretRecord({ linkSecretId: 'linkSecretId', value: linkSecret })
+ )
+
+ const {
+ credential: personCredential,
+ credentialInfo: personCredentialInfo,
+ revocationRegistryDefinition: personRevRegDef,
+ tailsPath: personTailsPath,
+ } = createCredentialForHolder({
+ attributes: {
+ name: 'John',
+ sex: 'M',
+ height: '179',
+ age: '19',
+ },
+ credentialDefinition: personCredentialDefinition as unknown as JsonObject,
+ schemaId: 'personschema:uri',
+ credentialDefinitionId: 'personcreddef:uri',
+ credentialDefinitionPrivate: personCredentialDefinitionPrivate,
+ keyCorrectnessProof: personKeyCorrectnessProof,
+ linkSecret,
+ linkSecretId: 'linkSecretId',
+ credentialId: 'personCredId',
+ revocationRegistryDefinitionId: 'personrevregid:uri',
+ })
+
+ const {
+ credential: phoneCredential,
+ credentialInfo: phoneCredentialInfo,
+ revocationRegistryDefinition: phoneRevRegDef,
+ tailsPath: phoneTailsPath,
+ } = createCredentialForHolder({
+ attributes: {
+ phoneNumber: 'linkSecretId56',
+ },
+ credentialDefinition: phoneCredentialDefinition as unknown as JsonObject,
+ schemaId: 'phoneschema:uri',
+ credentialDefinitionId: 'phonecreddef:uri',
+ credentialDefinitionPrivate: phoneCredentialDefinitionPrivate,
+ keyCorrectnessProof: phoneKeyCorrectnessProof,
+ linkSecret,
+ linkSecretId: 'linkSecretId',
+ credentialId: 'phoneCredId',
+ revocationRegistryDefinitionId: 'phonerevregid:uri',
+ })
+
+ const selectedCredentials: AnonCredsSelectedCredentials = {
+ selfAttestedAttributes: { attr5_referent: 'football' },
+ attributes: {
+ attr1_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo, revealed: true },
+ attr2_referent: { credentialId: 'phoneCredId', credentialInfo: phoneCredentialInfo, revealed: true },
+ attr3_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo, revealed: true },
+ attr4_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo, revealed: true },
+ },
+ predicates: {
+ predicate1_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo },
+ },
+ }
+
+ getByCredentialIdMock.mockResolvedValueOnce(
+ new AnonCredsCredentialRecord({
+ credential: personCredential,
+ credentialId: 'personCredId',
+ linkSecretId: 'linkSecretId',
+ issuerId: 'issuerDid',
+ schemaIssuerId: 'schemaIssuerDid',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ methodName: 'inMemory',
+ })
+ )
+ getByCredentialIdMock.mockResolvedValueOnce(
+ new AnonCredsCredentialRecord({
+ credential: phoneCredential,
+ credentialId: 'phoneCredId',
+ linkSecretId: 'linkSecretId',
+ issuerId: 'issuerDid',
+ schemaIssuerId: 'schemaIssuerDid',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ methodName: 'inMemory',
+ })
+ )
+
+ const revocationRegistries = {
+ 'personrevregid:uri': {
+ tailsFilePath: personTailsPath,
+ definition: JSON.parse(anoncreds.getJson({ objectHandle: personRevRegDef })),
+ revocationStatusLists: { '1': {} as AnonCredsRevocationStatusList },
+ },
+ 'phonerevregid:uri': {
+ tailsFilePath: phoneTailsPath,
+ definition: JSON.parse(anoncreds.getJson({ objectHandle: phoneRevRegDef })),
+ revocationStatusLists: { '1': {} as AnonCredsRevocationStatusList },
+ },
+ }
+
+ await anonCredsHolderService.createProof(agentContext, {
+ credentialDefinitions: {
+ 'personcreddef:uri': personCredentialDefinition as AnonCredsCredentialDefinition,
+ 'phonecreddef:uri': phoneCredentialDefinition as AnonCredsCredentialDefinition,
+ },
+ proofRequest,
+ selectedCredentials,
+ schemas: {
+ 'phoneschema:uri': { attrNames: ['phoneNumber'], issuerId: 'issuer:uri', name: 'phoneschema', version: '1' },
+ 'personschema:uri': {
+ attrNames: ['name', 'sex', 'height', 'age'],
+ issuerId: 'issuer:uri',
+ name: 'personschema',
+ version: '1',
+ },
+ },
+ revocationRegistries,
+ })
+
+ expect(getByCredentialIdMock).toHaveBeenCalledTimes(2)
+ // TODO: check proof object
+ })
+
+ describe('getCredentialsForProofRequest', () => {
+ const findByQueryMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'findByQuery')
+
+ const proofRequest: AnonCredsProofRequest = {
+ nonce: anoncreds.generateNonce(),
+ name: 'pres_req_1',
+ version: '0.1',
+ requested_attributes: {
+ attr1_referent: {
+ name: 'name',
+ restrictions: [{ issuer_did: 'issuer:uri' }],
+ },
+ attr2_referent: {
+ name: 'phoneNumber',
+ },
+ attr3_referent: {
+ name: 'age',
+ restrictions: [{ schema_id: 'schemaid:uri', schema_name: 'schemaName' }, { schema_version: '1.0' }],
+ },
+ attr4_referent: {
+ names: ['name', 'height'],
+ restrictions: [{ cred_def_id: 'crededefid:uri', issuer_id: 'issuerid:uri' }],
+ },
+ attr5_referent: {
+ name: 'name',
+ restrictions: [{ 'attr::name::value': 'Alice', 'attr::name::marker': '1' }],
+ },
+ },
+ requested_predicates: {
+ predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 },
+ },
+ }
+
+ beforeEach(() => {
+ findByQueryMock.mockResolvedValue([])
+ })
+
+ afterEach(() => {
+ findByQueryMock.mockClear()
+ })
+
+ test('invalid referent', async () => {
+ await expect(
+ anonCredsHolderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent: 'name',
+ })
+ ).rejects.toThrowError()
+ })
+
+ test('referent with single restriction', async () => {
+ await anonCredsHolderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent: 'attr1_referent',
+ })
+
+ expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
+ $and: [
+ {
+ 'attr::name::marker': true,
+ },
+ {
+ issuerId: 'issuer:uri',
+ },
+ ],
+ })
+ })
+
+ test('referent without restrictions', async () => {
+ await anonCredsHolderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent: 'attr2_referent',
+ })
+
+ expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
+ $and: [
+ {
+ 'attr::phoneNumber::marker': true,
+ },
+ ],
+ })
+ })
+
+ test('referent with multiple, complex restrictions', async () => {
+ await anonCredsHolderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent: 'attr3_referent',
+ })
+
+ expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
+ $and: [
+ {
+ 'attr::age::marker': true,
+ },
+ {
+ $or: [{ schemaId: 'schemaid:uri', schemaName: 'schemaName' }, { schemaVersion: '1.0' }],
+ },
+ ],
+ })
+ })
+
+ test('referent with multiple names and restrictions', async () => {
+ await anonCredsHolderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent: 'attr4_referent',
+ })
+
+ expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
+ $and: [
+ {
+ 'attr::name::marker': true,
+ 'attr::height::marker': true,
+ },
+ {
+ credentialDefinitionId: 'crededefid:uri',
+ issuerId: 'issuerid:uri',
+ },
+ ],
+ })
+ })
+
+ test('referent with attribute values and marker restriction', async () => {
+ await anonCredsHolderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent: 'attr5_referent',
+ })
+
+ expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
+ $and: [
+ {
+ 'attr::name::marker': true,
+ },
+ {
+ 'attr::name::value': 'Alice',
+ 'attr::name::marker': true,
+ },
+ ],
+ })
+ })
+
+ test('predicate referent', async () => {
+ await anonCredsHolderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent: 'predicate1_referent',
+ })
+
+ expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
+ $and: [
+ {
+ 'attr::age::marker': true,
+ },
+ ],
+ })
+ })
+ })
+
+ test('deleteCredential', async () => {
+ getByCredentialIdMock.mockRejectedValueOnce(new Error())
+ getByCredentialIdMock.mockResolvedValueOnce(
+ new AnonCredsCredentialRecord({
+ credential: {} as AnonCredsCredential,
+ credentialId: 'personCredId',
+ linkSecretId: 'linkSecretId',
+ issuerId: 'issuerDid',
+ schemaIssuerId: 'schemaIssuerDid',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ methodName: 'inMemory',
+ })
+ )
+
+ expect(anonCredsHolderService.deleteCredential(agentContext, 'credentialId')).rejects.toThrowError()
+
+ await anonCredsHolderService.deleteCredential(agentContext, 'credentialId')
+
+ expect(getByCredentialIdMock).toHaveBeenCalledWith(agentContext, 'credentialId')
+ })
+
+ test('getCredential', async () => {
+ getByCredentialIdMock.mockRejectedValueOnce(new Error())
+
+ getByCredentialIdMock.mockResolvedValueOnce(
+ new AnonCredsCredentialRecord({
+ credential: {
+ cred_def_id: 'credDefId',
+ schema_id: 'schemaId',
+ signature: 'signature',
+ signature_correctness_proof: 'signatureCorrectnessProof',
+ values: { attr1: { raw: 'value1', encoded: 'encvalue1' }, attr2: { raw: 'value2', encoded: 'encvalue2' } },
+ rev_reg_id: 'revRegId',
+ } as AnonCredsCredential,
+ credentialId: 'myCredentialId',
+ credentialRevocationId: 'credentialRevocationId',
+ linkSecretId: 'linkSecretId',
+ issuerId: 'issuerDid',
+ schemaIssuerId: 'schemaIssuerDid',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ methodName: 'inMemory',
+ })
+ )
+ expect(
+ anonCredsHolderService.getCredential(agentContext, { credentialId: 'myCredentialId' })
+ ).rejects.toThrowError()
+
+ const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { credentialId: 'myCredentialId' })
+
+ expect(credentialInfo).toMatchObject({
+ attributes: { attr1: 'value1', attr2: 'value2' },
+ credentialDefinitionId: 'credDefId',
+ credentialId: 'myCredentialId',
+ revocationRegistryId: 'revRegId',
+ schemaId: 'schemaId',
+ credentialRevocationId: 'credentialRevocationId',
+ })
+ })
+
+ test('getCredentials', async () => {
+ findByQueryMock.mockResolvedValueOnce([
+ new AnonCredsCredentialRecord({
+ credential: {
+ cred_def_id: 'credDefId',
+ schema_id: 'schemaId',
+ signature: 'signature',
+ signature_correctness_proof: 'signatureCorrectnessProof',
+ values: { attr1: { raw: 'value1', encoded: 'encvalue1' }, attr2: { raw: 'value2', encoded: 'encvalue2' } },
+ rev_reg_id: 'revRegId',
+ } as AnonCredsCredential,
+ credentialId: 'myCredentialId',
+ credentialRevocationId: 'credentialRevocationId',
+ linkSecretId: 'linkSecretId',
+ issuerId: 'issuerDid',
+ schemaIssuerId: 'schemaIssuerDid',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ methodName: 'inMemory',
+ }),
+ ])
+
+ const credentialInfo = await anonCredsHolderService.getCredentials(agentContext, {
+ credentialDefinitionId: 'credDefId',
+ schemaId: 'schemaId',
+ schemaIssuerId: 'schemaIssuerDid',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ issuerId: 'issuerDid',
+ methodName: 'inMemory',
+ })
+
+ expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
+ credentialDefinitionId: 'credDefId',
+ schemaId: 'schemaId',
+ schemaIssuerId: 'schemaIssuerDid',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ issuerId: 'issuerDid',
+ methodName: 'inMemory',
+ })
+ expect(credentialInfo).toMatchObject([
+ {
+ attributes: { attr1: 'value1', attr2: 'value2' },
+ credentialDefinitionId: 'credDefId',
+ credentialId: 'myCredentialId',
+ revocationRegistryId: 'revRegId',
+ schemaId: 'schemaId',
+ credentialRevocationId: 'credentialRevocationId',
+ },
+ ])
+ })
+
+ test('storeCredential', async () => {
+ const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = createCredentialDefinition({
+ attributeNames: ['name', 'age', 'sex', 'height'],
+ issuerId: 'issuer:uri',
+ })
+
+ const linkSecret = createLinkSecret()
+
+ mockFunction(anoncredsLinkSecretRepositoryMock.getByLinkSecretId).mockResolvedValue(
+ new AnonCredsLinkSecretRecord({ linkSecretId: 'linkSecretId', value: linkSecret })
+ )
+
+ const schema: AnonCredsSchema = {
+ attrNames: ['name', 'sex', 'height', 'age'],
+ issuerId: 'issuerId',
+ name: 'schemaName',
+ version: '1',
+ }
+
+ const { credential, revocationRegistryDefinition, credentialRequestMetadata } = createCredentialForHolder({
+ attributes: {
+ name: 'John',
+ sex: 'M',
+ height: '179',
+ age: '19',
+ },
+ credentialDefinition: credentialDefinition as unknown as JsonObject,
+ schemaId: 'personschema:uri',
+ credentialDefinitionId: 'personcreddef:uri',
+ credentialDefinitionPrivate,
+ keyCorrectnessProof,
+ linkSecret,
+ linkSecretId: 'linkSecretId',
+ credentialId: 'personCredId',
+ revocationRegistryDefinitionId: 'personrevregid:uri',
+ })
+
+ const saveCredentialMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'save')
+
+ saveCredentialMock.mockResolvedValue()
+
+ const credentialId = await anonCredsHolderService.storeCredential(agentContext, {
+ credential,
+ credentialDefinition,
+ schema,
+ credentialDefinitionId: 'personcreddefid:uri',
+ credentialRequestMetadata: credentialRequestMetadata.toJson() as unknown as AnonCredsCredentialRequestMetadata,
+ credentialId: 'personCredId',
+ revocationRegistry: {
+ id: 'personrevregid:uri',
+ definition: new RevocationRegistryDefinition(
+ revocationRegistryDefinition.handle
+ ).toJson() as unknown as AnonCredsRevocationRegistryDefinition,
+ },
+ })
+
+ expect(credentialId).toBe('personCredId')
+ expect(saveCredentialMock).toHaveBeenCalledWith(
+ agentContext,
+ expect.objectContaining({
+ // The stored credential is different from the one received originally
+ credentialId: 'personCredId',
+ linkSecretId: 'linkSecretId',
+ _tags: expect.objectContaining({
+ issuerId: credentialDefinition.issuerId,
+ schemaName: 'schemaName',
+ schemaIssuerId: 'issuerId',
+ schemaVersion: '1',
+ }),
+ })
+ )
+ })
+})
diff --git a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts
new file mode 100644
index 0000000000..1b5d3db10d
--- /dev/null
+++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts
@@ -0,0 +1,451 @@
+import type { AnonCredsProofRequest } from '@aries-framework/anoncreds'
+
+import {
+ getUnqualifiedSchemaId,
+ parseIndySchemaId,
+ getUnqualifiedCredentialDefinitionId,
+ parseIndyCredentialDefinitionId,
+ AnonCredsModuleConfig,
+ AnonCredsHolderServiceSymbol,
+ AnonCredsIssuerServiceSymbol,
+ AnonCredsVerifierServiceSymbol,
+ AnonCredsSchemaRepository,
+ AnonCredsSchemaRecord,
+ AnonCredsCredentialDefinitionRecord,
+ AnonCredsCredentialDefinitionRepository,
+ AnonCredsCredentialDefinitionPrivateRecord,
+ AnonCredsCredentialDefinitionPrivateRepository,
+ AnonCredsKeyCorrectnessProofRepository,
+ AnonCredsKeyCorrectnessProofRecord,
+ AnonCredsLinkSecretRepository,
+ AnonCredsLinkSecretRecord,
+} from '@aries-framework/anoncreds'
+import { InjectionSymbols } from '@aries-framework/core'
+import { anoncreds } from '@hyperledger/anoncreds-nodejs'
+import { Subject } from 'rxjs'
+
+import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService'
+import { describeRunInNodeVersion } from '../../../../../tests/runInVersion'
+import { encodeCredentialValue } from '../../../../anoncreds/src/utils/credential'
+import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry'
+import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers'
+import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService'
+import { AnonCredsRsIssuerService } from '../AnonCredsRsIssuerService'
+import { AnonCredsRsVerifierService } from '../AnonCredsRsVerifierService'
+
+const agentConfig = getAgentConfig('AnonCredsCredentialFormatServiceTest')
+const anonCredsVerifierService = new AnonCredsRsVerifierService()
+const anonCredsHolderService = new AnonCredsRsHolderService()
+const anonCredsIssuerService = new AnonCredsRsIssuerService()
+const storageService = new InMemoryStorageService()
+const registry = new InMemoryAnonCredsRegistry()
+
+const agentContext = getAgentContext({
+ registerInstances: [
+ [InjectionSymbols.Stop$, new Subject()],
+ [InjectionSymbols.AgentDependencies, agentDependencies],
+ [InjectionSymbols.StorageService, storageService],
+ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService],
+ [AnonCredsHolderServiceSymbol, anonCredsHolderService],
+ [AnonCredsVerifierServiceSymbol, anonCredsVerifierService],
+ [
+ AnonCredsModuleConfig,
+ new AnonCredsModuleConfig({
+ registries: [registry],
+ }),
+ ],
+ ],
+ agentConfig,
+})
+
+// FIXME: Re-include in tests when NodeJS wrapper performance is improved
+describeRunInNodeVersion([18], 'AnonCredsRsServices', () => {
+ test('issuance flow without revocation', async () => {
+ const issuerId = 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt'
+
+ const schema = await anonCredsIssuerService.createSchema(agentContext, {
+ attrNames: ['name', 'age'],
+ issuerId,
+ name: 'Employee Credential',
+ version: '1.0.0',
+ })
+
+ const { schemaState } = await registry.registerSchema(agentContext, {
+ schema,
+ options: {},
+ })
+
+ const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } =
+ await anonCredsIssuerService.createCredentialDefinition(agentContext, {
+ issuerId,
+ schemaId: schemaState.schemaId as string,
+ schema,
+ tag: 'Employee Credential',
+ supportRevocation: false,
+ })
+
+ const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, {
+ credentialDefinition,
+ options: {},
+ })
+
+ if (
+ !credentialDefinitionState.credentialDefinition ||
+ !credentialDefinitionState.credentialDefinitionId ||
+ !schemaState.schema ||
+ !schemaState.schemaId
+ ) {
+ throw new Error('Failed to create schema or credential definition')
+ }
+
+ if (!credentialDefinitionPrivate || !keyCorrectnessProof) {
+ throw new Error('Failed to get private part of credential definition')
+ }
+
+ await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save(
+ agentContext,
+ new AnonCredsSchemaRecord({
+ schema: schemaState.schema,
+ schemaId: schemaState.schemaId,
+ methodName: 'inMemory',
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save(
+ agentContext,
+ new AnonCredsCredentialDefinitionRecord({
+ credentialDefinition: credentialDefinitionState.credentialDefinition,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ methodName: 'inMemory',
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save(
+ agentContext,
+ new AnonCredsCredentialDefinitionPrivateRecord({
+ value: credentialDefinitionPrivate,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save(
+ agentContext,
+ new AnonCredsKeyCorrectnessProofRecord({
+ value: keyCorrectnessProof,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+ )
+
+ const credentialOffer = await anonCredsIssuerService.createCredentialOffer(agentContext, {
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+
+ const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' })
+ expect(linkSecret.linkSecretId).toBe('linkSecretId')
+
+ await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save(
+ agentContext,
+ new AnonCredsLinkSecretRecord({
+ value: linkSecret.linkSecretValue,
+ linkSecretId: linkSecret.linkSecretId,
+ })
+ )
+
+ const credentialRequestState = await anonCredsHolderService.createCredentialRequest(agentContext, {
+ credentialDefinition: credentialDefinitionState.credentialDefinition,
+ credentialOffer,
+ linkSecretId: linkSecret.linkSecretId,
+ })
+
+ const { credential } = await anonCredsIssuerService.createCredential(agentContext, {
+ credentialOffer,
+ credentialRequest: credentialRequestState.credentialRequest,
+ credentialValues: {
+ name: { raw: 'John', encoded: encodeCredentialValue('John') },
+ age: { raw: '25', encoded: encodeCredentialValue('25') },
+ },
+ })
+
+ const credentialId = 'holderCredentialId'
+
+ const storedId = await anonCredsHolderService.storeCredential(agentContext, {
+ credential,
+ credentialDefinition,
+ schema,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ credentialRequestMetadata: credentialRequestState.credentialRequestMetadata,
+ credentialId,
+ })
+
+ expect(storedId).toEqual(credentialId)
+
+ const credentialInfo = await anonCredsHolderService.getCredential(agentContext, {
+ credentialId,
+ })
+
+ expect(credentialInfo).toEqual({
+ credentialId,
+ attributes: {
+ age: '25',
+ name: 'John',
+ },
+ schemaId: schemaState.schemaId,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ revocationRegistryId: null,
+ credentialRevocationId: undefined, // Should it be null in this case?
+ methodName: 'inMemory',
+ })
+
+ const proofRequest: AnonCredsProofRequest = {
+ nonce: anoncreds.generateNonce(),
+ name: 'pres_req_1',
+ version: '0.1',
+ requested_attributes: {
+ attr1_referent: {
+ name: 'name',
+ },
+ attr2_referent: {
+ name: 'age',
+ },
+ },
+ requested_predicates: {
+ predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 },
+ },
+ }
+
+ const proof = await anonCredsHolderService.createProof(agentContext, {
+ credentialDefinitions: { [credentialDefinitionState.credentialDefinitionId]: credentialDefinition },
+ proofRequest,
+ selectedCredentials: {
+ attributes: {
+ attr1_referent: { credentialId, credentialInfo, revealed: true },
+ attr2_referent: { credentialId, credentialInfo, revealed: true },
+ },
+ predicates: {
+ predicate1_referent: { credentialId, credentialInfo },
+ },
+ selfAttestedAttributes: {},
+ },
+ schemas: { [schemaState.schemaId]: schema },
+ revocationRegistries: {},
+ })
+
+ const verifiedProof = await anonCredsVerifierService.verifyProof(agentContext, {
+ credentialDefinitions: { [credentialDefinitionState.credentialDefinitionId]: credentialDefinition },
+ proof,
+ proofRequest,
+ schemas: { [schemaState.schemaId]: schema },
+ revocationRegistries: {},
+ })
+
+ expect(verifiedProof).toBeTruthy()
+ })
+
+ test('issuance flow with unqualified identifiers', async () => {
+ // Use qualified identifiers to create schema and credential definition (we only support qualified identifiers for these)
+ const issuerId = 'did:indy:pool:localtest:A4CYPASJYRZRt98YWrac3H'
+
+ const schema = await anonCredsIssuerService.createSchema(agentContext, {
+ attrNames: ['name', 'age'],
+ issuerId,
+ name: 'Employee Credential',
+ version: '1.0.0',
+ })
+
+ const { schemaState } = await registry.registerSchema(agentContext, {
+ schema,
+ options: {},
+ })
+
+ const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } =
+ await anonCredsIssuerService.createCredentialDefinition(agentContext, {
+ issuerId,
+ schemaId: schemaState.schemaId as string,
+ schema,
+ tag: 'Employee Credential',
+ supportRevocation: false,
+ })
+
+ const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, {
+ credentialDefinition,
+ options: {},
+ })
+
+ if (
+ !credentialDefinitionState.credentialDefinition ||
+ !credentialDefinitionState.credentialDefinitionId ||
+ !schemaState.schema ||
+ !schemaState.schemaId
+ ) {
+ throw new Error('Failed to create schema or credential definition')
+ }
+
+ if (!credentialDefinitionPrivate || !keyCorrectnessProof) {
+ throw new Error('Failed to get private part of credential definition')
+ }
+
+ await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save(
+ agentContext,
+ new AnonCredsSchemaRecord({
+ schema: schemaState.schema,
+ schemaId: schemaState.schemaId,
+ methodName: 'inMemory',
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save(
+ agentContext,
+ new AnonCredsCredentialDefinitionRecord({
+ credentialDefinition: credentialDefinitionState.credentialDefinition,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ methodName: 'inMemory',
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save(
+ agentContext,
+ new AnonCredsCredentialDefinitionPrivateRecord({
+ value: credentialDefinitionPrivate,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save(
+ agentContext,
+ new AnonCredsKeyCorrectnessProofRecord({
+ value: keyCorrectnessProof,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+ )
+
+ const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(
+ credentialDefinitionState.credentialDefinitionId
+ )
+ const unqualifiedCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(
+ namespaceIdentifier,
+ schemaSeqNo,
+ tag
+ )
+
+ const parsedSchema = parseIndySchemaId(schemaState.schemaId)
+ const unqualifiedSchemaId = getUnqualifiedSchemaId(
+ parsedSchema.namespaceIdentifier,
+ parsedSchema.schemaName,
+ parsedSchema.schemaVersion
+ )
+
+ // Create offer with unqualified credential definition id
+ const credentialOffer = await anonCredsIssuerService.createCredentialOffer(agentContext, {
+ credentialDefinitionId: unqualifiedCredentialDefinitionId,
+ })
+
+ const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'someLinkSecretId' })
+ expect(linkSecret.linkSecretId).toBe('someLinkSecretId')
+
+ await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save(
+ agentContext,
+ new AnonCredsLinkSecretRecord({
+ value: linkSecret.linkSecretValue,
+ linkSecretId: linkSecret.linkSecretId,
+ })
+ )
+
+ const unqualifiedCredentialDefinition = await registry.getCredentialDefinition(
+ agentContext,
+ credentialOffer.cred_def_id
+ )
+ const unqualifiedSchema = await registry.getSchema(agentContext, credentialOffer.schema_id)
+ if (!unqualifiedCredentialDefinition.credentialDefinition || !unqualifiedSchema.schema) {
+ throw new Error('unable to fetch credential definition or schema')
+ }
+
+ const credentialRequestState = await anonCredsHolderService.createCredentialRequest(agentContext, {
+ credentialDefinition: unqualifiedCredentialDefinition.credentialDefinition,
+ credentialOffer,
+ linkSecretId: linkSecret.linkSecretId,
+ })
+
+ const { credential } = await anonCredsIssuerService.createCredential(agentContext, {
+ credentialOffer,
+ credentialRequest: credentialRequestState.credentialRequest,
+ credentialValues: {
+ name: { raw: 'John', encoded: encodeCredentialValue('John') },
+ age: { raw: '25', encoded: encodeCredentialValue('25') },
+ },
+ })
+
+ const credentialId = 'holderCredentialId2'
+
+ const storedId = await anonCredsHolderService.storeCredential(agentContext, {
+ credential,
+ credentialDefinition: unqualifiedCredentialDefinition.credentialDefinition,
+ schema: unqualifiedSchema.schema,
+ credentialDefinitionId: credentialOffer.cred_def_id,
+ credentialRequestMetadata: credentialRequestState.credentialRequestMetadata,
+ credentialId,
+ })
+
+ expect(storedId).toEqual(credentialId)
+
+ const credentialInfo = await anonCredsHolderService.getCredential(agentContext, {
+ credentialId,
+ })
+
+ expect(credentialInfo).toEqual({
+ credentialId,
+ attributes: {
+ age: '25',
+ name: 'John',
+ },
+ schemaId: unqualifiedSchemaId,
+ credentialDefinitionId: unqualifiedCredentialDefinitionId,
+ revocationRegistryId: null,
+ credentialRevocationId: undefined, // Should it be null in this case?
+ methodName: 'inMemory',
+ })
+
+ const proofRequest: AnonCredsProofRequest = {
+ nonce: anoncreds.generateNonce(),
+ name: 'pres_req_1',
+ version: '0.1',
+ requested_attributes: {
+ attr1_referent: {
+ name: 'name',
+ },
+ attr2_referent: {
+ name: 'age',
+ },
+ },
+ requested_predicates: {
+ predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 },
+ },
+ }
+
+ const proof = await anonCredsHolderService.createProof(agentContext, {
+ credentialDefinitions: { [unqualifiedCredentialDefinitionId]: credentialDefinition },
+ proofRequest,
+ selectedCredentials: {
+ attributes: {
+ attr1_referent: { credentialId, credentialInfo, revealed: true },
+ attr2_referent: { credentialId, credentialInfo, revealed: true },
+ },
+ predicates: {
+ predicate1_referent: { credentialId, credentialInfo },
+ },
+ selfAttestedAttributes: {},
+ },
+ schemas: { [unqualifiedSchemaId]: schema },
+ revocationRegistries: {},
+ })
+
+ const verifiedProof = await anonCredsVerifierService.verifyProof(agentContext, {
+ credentialDefinitions: { [unqualifiedCredentialDefinitionId]: credentialDefinition },
+ proof,
+ proofRequest,
+ schemas: { [unqualifiedSchemaId]: schema },
+ revocationRegistries: {},
+ })
+
+ expect(verifiedProof).toBeTruthy()
+ })
+})
diff --git a/packages/anoncreds-rs/src/services/__tests__/helpers.ts b/packages/anoncreds-rs/src/services/__tests__/helpers.ts
new file mode 100644
index 0000000000..fefc63d9c1
--- /dev/null
+++ b/packages/anoncreds-rs/src/services/__tests__/helpers.ts
@@ -0,0 +1,198 @@
+import type {
+ AnonCredsCredential,
+ AnonCredsCredentialDefinition,
+ AnonCredsCredentialInfo,
+ AnonCredsCredentialOffer,
+} from '@aries-framework/anoncreds'
+import type { JsonObject } from '@hyperledger/anoncreds-nodejs'
+
+import {
+ anoncreds,
+ Credential,
+ CredentialDefinition,
+ CredentialOffer,
+ CredentialRequest,
+ CredentialRevocationConfig,
+ LinkSecret,
+ RevocationRegistryDefinition,
+ RevocationRegistryDefinitionPrivate,
+ RevocationStatusList,
+ Schema,
+} from '@hyperledger/anoncreds-shared'
+
+/**
+ * Creates a valid credential definition and returns its public and
+ * private part, including its key correctness proof
+ */
+export function createCredentialDefinition(options: { attributeNames: string[]; issuerId: string }) {
+ const { attributeNames, issuerId } = options
+
+ const schema = Schema.create({
+ issuerId,
+ attributeNames,
+ name: 'schema1',
+ version: '1',
+ })
+
+ const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = CredentialDefinition.create({
+ issuerId,
+ schema,
+ schemaId: 'schema:uri',
+ signatureType: 'CL',
+ supportRevocation: true, // FIXME: Revocation should not be mandatory but current anoncreds-rs is requiring it
+ tag: 'TAG',
+ })
+
+ const returnObj = {
+ credentialDefinition: credentialDefinition.toJson() as unknown as AnonCredsCredentialDefinition,
+ credentialDefinitionPrivate: credentialDefinitionPrivate.toJson() as unknown as JsonObject,
+ keyCorrectnessProof: keyCorrectnessProof.toJson() as unknown as JsonObject,
+ schema: schema.toJson() as unknown as Schema,
+ }
+
+ credentialDefinition.handle.clear()
+ credentialDefinitionPrivate.handle.clear()
+ keyCorrectnessProof.handle.clear()
+ schema.handle.clear()
+
+ return returnObj
+}
+
+/**
+ * Creates a valid credential offer and returns itsf
+ */
+export function createCredentialOffer(keyCorrectnessProof: Record) {
+ const credentialOffer = CredentialOffer.create({
+ credentialDefinitionId: 'creddef:uri',
+ keyCorrectnessProof,
+ schemaId: 'schema:uri',
+ })
+ const credentialOfferJson = credentialOffer.toJson() as unknown as AnonCredsCredentialOffer
+ credentialOffer.handle.clear()
+ return credentialOfferJson
+}
+
+/**
+ *
+ * @returns Creates a valid link secret value for anoncreds-rs
+ */
+export function createLinkSecret() {
+ return LinkSecret.create()
+}
+
+export function createCredentialForHolder(options: {
+ credentialDefinition: JsonObject
+ credentialDefinitionPrivate: JsonObject
+ keyCorrectnessProof: JsonObject
+ schemaId: string
+ credentialDefinitionId: string
+ attributes: Record
+ linkSecret: string
+ linkSecretId: string
+ credentialId: string
+ revocationRegistryDefinitionId: string
+}) {
+ const {
+ credentialDefinition,
+ credentialDefinitionPrivate,
+ keyCorrectnessProof,
+ schemaId,
+ credentialDefinitionId,
+ attributes,
+ linkSecret,
+ linkSecretId,
+ credentialId,
+ revocationRegistryDefinitionId,
+ } = options
+
+ const credentialOffer = CredentialOffer.create({
+ credentialDefinitionId,
+ keyCorrectnessProof,
+ schemaId,
+ })
+
+ const { credentialRequest, credentialRequestMetadata } = CredentialRequest.create({
+ entropy: 'some-entropy',
+ credentialDefinition,
+ credentialOffer,
+ linkSecret,
+ linkSecretId: linkSecretId,
+ })
+
+ const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate, tailsPath } =
+ createRevocationRegistryDefinition({
+ credentialDefinitionId,
+ credentialDefinition,
+ })
+
+ const timeCreateRevStatusList = 12
+ const revocationStatusList = RevocationStatusList.create({
+ issuerId: credentialDefinition.issuerId as string,
+ timestamp: timeCreateRevStatusList,
+ issuanceByDefault: true,
+ revocationRegistryDefinition: new RevocationRegistryDefinition(revocationRegistryDefinition.handle),
+ revocationRegistryDefinitionId: 'mock:uri',
+ })
+
+ const credentialObj = Credential.create({
+ credentialDefinition,
+ credentialDefinitionPrivate,
+ credentialOffer,
+ credentialRequest,
+ attributeRawValues: attributes,
+ revocationRegistryId: revocationRegistryDefinitionId,
+ revocationStatusList,
+ revocationConfiguration: new CredentialRevocationConfig({
+ registryDefinition: new RevocationRegistryDefinition(revocationRegistryDefinition.handle),
+ registryDefinitionPrivate: new RevocationRegistryDefinitionPrivate(revocationRegistryDefinitionPrivate.handle),
+ registryIndex: 9,
+ tailsPath,
+ }),
+ })
+
+ const credentialInfo: AnonCredsCredentialInfo = {
+ attributes,
+ credentialDefinitionId,
+ credentialId,
+ schemaId,
+ methodName: 'inMemory',
+ }
+ const returnObj = {
+ credential: credentialObj.toJson() as unknown as AnonCredsCredential,
+ credentialInfo,
+ revocationRegistryDefinition,
+ tailsPath,
+ credentialRequestMetadata,
+ }
+
+ credentialObj.handle.clear()
+ credentialOffer.handle.clear()
+ credentialRequest.handle.clear()
+ revocationRegistryDefinitionPrivate.clear()
+ revocationStatusList.handle.clear()
+
+ return returnObj
+}
+
+export function createRevocationRegistryDefinition(options: {
+ credentialDefinitionId: string
+ credentialDefinition: Record
+}) {
+ const { credentialDefinitionId, credentialDefinition } = options
+ const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } =
+ anoncreds.createRevocationRegistryDefinition({
+ credentialDefinitionId,
+ credentialDefinition: CredentialDefinition.fromJson(credentialDefinition).handle,
+ issuerId: credentialDefinition.issuerId as string,
+ tag: 'some_tag',
+ revocationRegistryType: 'CL_ACCUM',
+ maximumCredentialNumber: 10,
+ })
+
+ const tailsPath = anoncreds.revocationRegistryDefinitionGetAttribute({
+ objectHandle: revocationRegistryDefinition,
+ name: 'tails_location',
+ })
+
+ return { revocationRegistryDefinition, revocationRegistryDefinitionPrivate, tailsPath }
+}
diff --git a/packages/anoncreds-rs/src/services/index.ts b/packages/anoncreds-rs/src/services/index.ts
new file mode 100644
index 0000000000..b675ab0025
--- /dev/null
+++ b/packages/anoncreds-rs/src/services/index.ts
@@ -0,0 +1,3 @@
+export { AnonCredsRsHolderService } from './AnonCredsRsHolderService'
+export { AnonCredsRsIssuerService } from './AnonCredsRsIssuerService'
+export { AnonCredsRsVerifierService } from './AnonCredsRsVerifierService'
diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts
new file mode 100644
index 0000000000..b3465a543d
--- /dev/null
+++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts
@@ -0,0 +1,361 @@
+import type { AnonCredsCredentialRequest } from '@aries-framework/anoncreds'
+import type { Wallet } from '@aries-framework/core'
+
+import {
+ AnonCredsModuleConfig,
+ AnonCredsHolderServiceSymbol,
+ AnonCredsIssuerServiceSymbol,
+ AnonCredsVerifierServiceSymbol,
+ AnonCredsSchemaRecord,
+ AnonCredsSchemaRepository,
+ AnonCredsCredentialDefinitionRepository,
+ AnonCredsCredentialDefinitionRecord,
+ AnonCredsCredentialDefinitionPrivateRepository,
+ AnonCredsCredentialDefinitionPrivateRecord,
+ AnonCredsKeyCorrectnessProofRepository,
+ AnonCredsKeyCorrectnessProofRecord,
+ AnonCredsLinkSecretRepository,
+ AnonCredsLinkSecretRecord,
+ AnonCredsProofFormatService,
+ AnonCredsCredentialFormatService,
+} from '@aries-framework/anoncreds'
+import {
+ CredentialState,
+ CredentialExchangeRecord,
+ CredentialPreviewAttribute,
+ InjectionSymbols,
+ ProofState,
+ ProofExchangeRecord,
+} from '@aries-framework/core'
+import { Subject } from 'rxjs'
+
+import { InMemoryStorageService } from '../../../tests/InMemoryStorageService'
+import { describeRunInNodeVersion } from '../../../tests/runInVersion'
+import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService'
+import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry'
+import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers'
+import { AnonCredsRsHolderService } from '../src/services/AnonCredsRsHolderService'
+import { AnonCredsRsIssuerService } from '../src/services/AnonCredsRsIssuerService'
+import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierService'
+
+const registry = new InMemoryAnonCredsRegistry()
+const anonCredsModuleConfig = new AnonCredsModuleConfig({
+ registries: [registry],
+})
+
+const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs')
+const anonCredsVerifierService = new AnonCredsRsVerifierService()
+const anonCredsHolderService = new AnonCredsRsHolderService()
+const anonCredsIssuerService = new AnonCredsRsIssuerService()
+
+const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet
+
+const inMemoryStorageService = new InMemoryStorageService()
+const agentContext = getAgentContext({
+ registerInstances: [
+ [InjectionSymbols.Stop$, new Subject()],
+ [InjectionSymbols.AgentDependencies, agentDependencies],
+ [InjectionSymbols.StorageService, inMemoryStorageService],
+ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService],
+ [AnonCredsHolderServiceSymbol, anonCredsHolderService],
+ [AnonCredsVerifierServiceSymbol, anonCredsVerifierService],
+ [AnonCredsRegistryService, new AnonCredsRegistryService()],
+ [AnonCredsModuleConfig, anonCredsModuleConfig],
+ ],
+ agentConfig,
+ wallet,
+})
+
+const anoncredsCredentialFormatService = new AnonCredsCredentialFormatService()
+const anoncredsProofFormatService = new AnonCredsProofFormatService()
+
+const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL'
+
+// FIXME: Re-include in tests when NodeJS wrapper performance is improved
+describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', () => {
+ test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => {
+ const schema = await anonCredsIssuerService.createSchema(agentContext, {
+ attrNames: ['name', 'age'],
+ issuerId: indyDid,
+ name: 'Employee Credential',
+ version: '1.0.0',
+ })
+
+ const { schemaState } = await registry.registerSchema(agentContext, {
+ schema,
+ options: {},
+ })
+
+ const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } =
+ await anonCredsIssuerService.createCredentialDefinition(agentContext, {
+ issuerId: indyDid,
+ schemaId: schemaState.schemaId as string,
+ schema,
+ tag: 'Employee Credential',
+ supportRevocation: false,
+ })
+
+ const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, {
+ credentialDefinition,
+ options: {},
+ })
+
+ if (
+ !credentialDefinitionState.credentialDefinition ||
+ !credentialDefinitionState.credentialDefinitionId ||
+ !schemaState.schema ||
+ !schemaState.schemaId
+ ) {
+ throw new Error('Failed to create schema or credential definition')
+ }
+
+ if (
+ !credentialDefinitionState.credentialDefinition ||
+ !credentialDefinitionState.credentialDefinitionId ||
+ !schemaState.schema ||
+ !schemaState.schemaId
+ ) {
+ throw new Error('Failed to create schema or credential definition')
+ }
+
+ if (!credentialDefinitionPrivate || !keyCorrectnessProof) {
+ throw new Error('Failed to get private part of credential definition')
+ }
+
+ await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save(
+ agentContext,
+ new AnonCredsSchemaRecord({
+ schema: schemaState.schema,
+ schemaId: schemaState.schemaId,
+ methodName: 'inMemory',
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save(
+ agentContext,
+ new AnonCredsCredentialDefinitionRecord({
+ credentialDefinition: credentialDefinitionState.credentialDefinition,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ methodName: 'inMemory',
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save(
+ agentContext,
+ new AnonCredsCredentialDefinitionPrivateRecord({
+ value: credentialDefinitionPrivate,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save(
+ agentContext,
+ new AnonCredsKeyCorrectnessProofRecord({
+ value: keyCorrectnessProof,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+ )
+
+ const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' })
+ expect(linkSecret.linkSecretId).toBe('linkSecretId')
+
+ await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save(
+ agentContext,
+ new AnonCredsLinkSecretRecord({
+ value: linkSecret.linkSecretValue,
+ linkSecretId: linkSecret.linkSecretId,
+ })
+ )
+
+ const holderCredentialRecord = new CredentialExchangeRecord({
+ protocolVersion: 'v1',
+ state: CredentialState.ProposalSent,
+ threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa',
+ })
+
+ const issuerCredentialRecord = new CredentialExchangeRecord({
+ protocolVersion: 'v1',
+ state: CredentialState.ProposalReceived,
+ threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa',
+ })
+
+ const credentialAttributes = [
+ new CredentialPreviewAttribute({
+ name: 'name',
+ value: 'John',
+ }),
+ new CredentialPreviewAttribute({
+ name: 'age',
+ value: '25',
+ }),
+ ]
+
+ // Holder creates proposal
+ holderCredentialRecord.credentialAttributes = credentialAttributes
+ const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ credentialFormats: {
+ anoncreds: {
+ attributes: credentialAttributes,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ },
+ },
+ })
+
+ // Issuer processes and accepts proposal
+ await anoncredsCredentialFormatService.processProposal(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ attachment: proposalAttachment,
+ })
+ // Set attributes on the credential record, this is normally done by the protocol service
+ issuerCredentialRecord.credentialAttributes = credentialAttributes
+ const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ proposalAttachment: proposalAttachment,
+ })
+
+ // Holder processes and accepts offer
+ await anoncredsCredentialFormatService.processOffer(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ attachment: offerAttachment,
+ })
+ const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ offerAttachment,
+ credentialFormats: {
+ anoncreds: {
+ linkSecretId: linkSecret.linkSecretId,
+ },
+ },
+ })
+
+ // Make sure the request contains an entropy and does not contain a prover_did field
+ expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined()
+ expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined()
+
+ // Issuer processes and accepts request
+ await anoncredsCredentialFormatService.processRequest(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ attachment: requestAttachment,
+ })
+ const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ requestAttachment,
+ offerAttachment,
+ })
+
+ // Holder processes and accepts credential
+ await anoncredsCredentialFormatService.processCredential(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ attachment: credentialAttachment,
+ requestAttachment,
+ })
+
+ expect(holderCredentialRecord.credentials).toEqual([
+ { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) },
+ ])
+
+ const credentialId = holderCredentialRecord.credentials[0].credentialRecordId
+ const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, {
+ credentialId,
+ })
+
+ expect(anonCredsCredential).toEqual({
+ credentialId,
+ attributes: {
+ age: '25',
+ name: 'John',
+ },
+ schemaId: schemaState.schemaId,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ revocationRegistryId: null,
+ credentialRevocationId: undefined, // FIXME: should be null?
+ methodName: 'inMemory',
+ })
+
+ expect(holderCredentialRecord.metadata.data).toEqual({
+ '_anoncreds/credential': {
+ schemaId: schemaState.schemaId,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ },
+ '_anoncreds/credentialRequest': {
+ link_secret_blinding_data: expect.any(Object),
+ link_secret_name: expect.any(String),
+ nonce: expect.any(String),
+ },
+ })
+
+ expect(issuerCredentialRecord.metadata.data).toEqual({
+ '_anoncreds/credential': {
+ schemaId: schemaState.schemaId,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ },
+ })
+
+ const holderProofRecord = new ProofExchangeRecord({
+ protocolVersion: 'v1',
+ state: ProofState.ProposalSent,
+ threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38',
+ })
+ const verifierProofRecord = new ProofExchangeRecord({
+ protocolVersion: 'v1',
+ state: ProofState.ProposalReceived,
+ threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38',
+ })
+
+ const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, {
+ proofFormats: {
+ anoncreds: {
+ attributes: [
+ {
+ name: 'name',
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ value: 'John',
+ referent: '1',
+ },
+ ],
+ predicates: [
+ {
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ name: 'age',
+ predicate: '>=',
+ threshold: 18,
+ },
+ ],
+ name: 'Proof Request',
+ version: '1.0',
+ },
+ },
+ proofRecord: holderProofRecord,
+ })
+
+ await anoncredsProofFormatService.processProposal(agentContext, {
+ attachment: proofProposalAttachment,
+ proofRecord: verifierProofRecord,
+ })
+
+ const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, {
+ proofRecord: verifierProofRecord,
+ proposalAttachment: proofProposalAttachment,
+ })
+
+ await anoncredsProofFormatService.processRequest(agentContext, {
+ attachment: proofRequestAttachment,
+ proofRecord: holderProofRecord,
+ })
+
+ const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, {
+ proofRecord: holderProofRecord,
+ requestAttachment: proofRequestAttachment,
+ proposalAttachment: proofProposalAttachment,
+ })
+
+ const isValid = await anoncredsProofFormatService.processPresentation(agentContext, {
+ attachment: proofAttachment,
+ proofRecord: verifierProofRecord,
+ requestAttachment: proofRequestAttachment,
+ })
+
+ expect(isValid).toBe(true)
+ })
+})
diff --git a/packages/anoncreds-rs/tests/indy-flow.test.ts b/packages/anoncreds-rs/tests/indy-flow.test.ts
new file mode 100644
index 0000000000..e254ee7dc6
--- /dev/null
+++ b/packages/anoncreds-rs/tests/indy-flow.test.ts
@@ -0,0 +1,379 @@
+import type { AnonCredsCredentialRequest } from '@aries-framework/anoncreds'
+import type { Wallet } from '@aries-framework/core'
+
+import {
+ getUnqualifiedSchemaId,
+ parseIndySchemaId,
+ getUnqualifiedCredentialDefinitionId,
+ parseIndyCredentialDefinitionId,
+ AnonCredsModuleConfig,
+ LegacyIndyCredentialFormatService,
+ AnonCredsHolderServiceSymbol,
+ AnonCredsIssuerServiceSymbol,
+ AnonCredsVerifierServiceSymbol,
+ AnonCredsSchemaRecord,
+ AnonCredsSchemaRepository,
+ AnonCredsCredentialDefinitionRepository,
+ AnonCredsCredentialDefinitionRecord,
+ AnonCredsCredentialDefinitionPrivateRepository,
+ AnonCredsCredentialDefinitionPrivateRecord,
+ AnonCredsKeyCorrectnessProofRepository,
+ AnonCredsKeyCorrectnessProofRecord,
+ AnonCredsLinkSecretRepository,
+ AnonCredsLinkSecretRecord,
+ LegacyIndyProofFormatService,
+} from '@aries-framework/anoncreds'
+import {
+ CredentialState,
+ CredentialExchangeRecord,
+ CredentialPreviewAttribute,
+ InjectionSymbols,
+ ProofState,
+ ProofExchangeRecord,
+} from '@aries-framework/core'
+import { Subject } from 'rxjs'
+
+import { InMemoryStorageService } from '../../../tests/InMemoryStorageService'
+import { describeRunInNodeVersion } from '../../../tests/runInVersion'
+import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService'
+import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry'
+import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers'
+import { AnonCredsRsHolderService } from '../src/services/AnonCredsRsHolderService'
+import { AnonCredsRsIssuerService } from '../src/services/AnonCredsRsIssuerService'
+import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierService'
+
+const registry = new InMemoryAnonCredsRegistry()
+const anonCredsModuleConfig = new AnonCredsModuleConfig({
+ registries: [registry],
+})
+
+const agentConfig = getAgentConfig('LegacyIndyCredentialFormatService using anoncreds-rs')
+const anonCredsVerifierService = new AnonCredsRsVerifierService()
+const anonCredsHolderService = new AnonCredsRsHolderService()
+const anonCredsIssuerService = new AnonCredsRsIssuerService()
+
+const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet
+
+const inMemoryStorageService = new InMemoryStorageService()
+const agentContext = getAgentContext({
+ registerInstances: [
+ [InjectionSymbols.Stop$, new Subject()],
+ [InjectionSymbols.AgentDependencies, agentDependencies],
+ [InjectionSymbols.StorageService, inMemoryStorageService],
+ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService],
+ [AnonCredsHolderServiceSymbol, anonCredsHolderService],
+ [AnonCredsVerifierServiceSymbol, anonCredsVerifierService],
+ [AnonCredsRegistryService, new AnonCredsRegistryService()],
+ [AnonCredsModuleConfig, anonCredsModuleConfig],
+ ],
+ agentConfig,
+ wallet,
+})
+
+const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService()
+const legacyIndyProofFormatService = new LegacyIndyProofFormatService()
+
+// This is just so we don't have to register an actually indy did (as we don't have the indy did registrar configured)
+const indyDid = 'did:indy:bcovrin:test:LjgpST2rjsoxYegQDRm7EL'
+
+// FIXME: Re-include in tests when NodeJS wrapper performance is improved
+describeRunInNodeVersion([18], 'Legacy indy format services using anoncreds-rs', () => {
+ test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => {
+ const schema = await anonCredsIssuerService.createSchema(agentContext, {
+ attrNames: ['name', 'age'],
+ issuerId: indyDid,
+ name: 'Employee Credential',
+ version: '1.0.0',
+ })
+
+ const { schemaState } = await registry.registerSchema(agentContext, {
+ schema,
+ options: {},
+ })
+
+ const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } =
+ await anonCredsIssuerService.createCredentialDefinition(agentContext, {
+ issuerId: indyDid,
+ schemaId: schemaState.schemaId as string,
+ schema,
+ tag: 'Employee Credential',
+ supportRevocation: false,
+ })
+
+ const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, {
+ credentialDefinition,
+ options: {},
+ })
+
+ if (
+ !credentialDefinitionState.credentialDefinition ||
+ !credentialDefinitionState.credentialDefinitionId ||
+ !schemaState.schema ||
+ !schemaState.schemaId
+ ) {
+ throw new Error('Failed to create schema or credential definition')
+ }
+
+ if (
+ !credentialDefinitionState.credentialDefinition ||
+ !credentialDefinitionState.credentialDefinitionId ||
+ !schemaState.schema ||
+ !schemaState.schemaId
+ ) {
+ throw new Error('Failed to create schema or credential definition')
+ }
+
+ if (!credentialDefinitionPrivate || !keyCorrectnessProof) {
+ throw new Error('Failed to get private part of credential definition')
+ }
+
+ await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save(
+ agentContext,
+ new AnonCredsSchemaRecord({
+ schema: schemaState.schema,
+ schemaId: schemaState.schemaId,
+ methodName: 'inMemory',
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save(
+ agentContext,
+ new AnonCredsCredentialDefinitionRecord({
+ credentialDefinition: credentialDefinitionState.credentialDefinition,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ methodName: 'inMemory',
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save(
+ agentContext,
+ new AnonCredsCredentialDefinitionPrivateRecord({
+ value: credentialDefinitionPrivate,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+ )
+
+ await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save(
+ agentContext,
+ new AnonCredsKeyCorrectnessProofRecord({
+ value: keyCorrectnessProof,
+ credentialDefinitionId: credentialDefinitionState.credentialDefinitionId,
+ })
+ )
+
+ const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' })
+ expect(linkSecret.linkSecretId).toBe('linkSecretId')
+
+ await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save(
+ agentContext,
+ new AnonCredsLinkSecretRecord({
+ value: linkSecret.linkSecretValue,
+ linkSecretId: linkSecret.linkSecretId,
+ })
+ )
+
+ const holderCredentialRecord = new CredentialExchangeRecord({
+ protocolVersion: 'v1',
+ state: CredentialState.ProposalSent,
+ threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa',
+ })
+
+ const issuerCredentialRecord = new CredentialExchangeRecord({
+ protocolVersion: 'v1',
+ state: CredentialState.ProposalReceived,
+ threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa',
+ })
+
+ const credentialAttributes = [
+ new CredentialPreviewAttribute({
+ name: 'name',
+ value: 'John',
+ }),
+ new CredentialPreviewAttribute({
+ name: 'age',
+ value: '25',
+ }),
+ ]
+
+ const parsedCredentialDefinition = parseIndyCredentialDefinitionId(credentialDefinitionState.credentialDefinitionId)
+ const unqualifiedCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(
+ parsedCredentialDefinition.namespaceIdentifier,
+ parsedCredentialDefinition.schemaSeqNo,
+ parsedCredentialDefinition.tag
+ )
+
+ const parsedSchemaId = parseIndySchemaId(schemaState.schemaId)
+ const unqualifiedSchemaId = getUnqualifiedSchemaId(
+ parsedSchemaId.namespaceIdentifier,
+ parsedSchemaId.schemaName,
+ parsedSchemaId.schemaVersion
+ )
+
+ // Holder creates proposal
+ holderCredentialRecord.credentialAttributes = credentialAttributes
+ const { attachment: proposalAttachment } = await legacyIndyCredentialFormatService.createProposal(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ credentialFormats: {
+ indy: {
+ attributes: credentialAttributes,
+ credentialDefinitionId: unqualifiedCredentialDefinitionId,
+ },
+ },
+ })
+
+ // Issuer processes and accepts proposal
+ await legacyIndyCredentialFormatService.processProposal(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ attachment: proposalAttachment,
+ })
+ // Set attributes on the credential record, this is normally done by the protocol service
+ issuerCredentialRecord.credentialAttributes = credentialAttributes
+ const { attachment: offerAttachment } = await legacyIndyCredentialFormatService.acceptProposal(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ proposalAttachment: proposalAttachment,
+ })
+
+ // Holder processes and accepts offer
+ await legacyIndyCredentialFormatService.processOffer(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ attachment: offerAttachment,
+ })
+ const { attachment: requestAttachment } = await legacyIndyCredentialFormatService.acceptOffer(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ offerAttachment,
+ credentialFormats: {
+ indy: {
+ linkSecretId: linkSecret.linkSecretId,
+ },
+ },
+ })
+
+ // Make sure the request contains a prover_did field
+ expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeDefined()
+
+ // Issuer processes and accepts request
+ await legacyIndyCredentialFormatService.processRequest(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ attachment: requestAttachment,
+ })
+ const { attachment: credentialAttachment } = await legacyIndyCredentialFormatService.acceptRequest(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ requestAttachment,
+ offerAttachment,
+ })
+
+ // Holder processes and accepts credential
+ await legacyIndyCredentialFormatService.processCredential(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ attachment: credentialAttachment,
+ requestAttachment,
+ })
+
+ expect(holderCredentialRecord.credentials).toEqual([
+ { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) },
+ ])
+
+ const credentialId = holderCredentialRecord.credentials[0].credentialRecordId
+ const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, {
+ credentialId,
+ })
+
+ expect(anonCredsCredential).toEqual({
+ credentialId,
+ attributes: {
+ age: '25',
+ name: 'John',
+ },
+ schemaId: unqualifiedSchemaId,
+ credentialDefinitionId: unqualifiedCredentialDefinitionId,
+ revocationRegistryId: null,
+ credentialRevocationId: undefined, // FIXME: should be null?
+ methodName: 'inMemory',
+ })
+
+ expect(holderCredentialRecord.metadata.data).toEqual({
+ '_anoncreds/credential': {
+ schemaId: unqualifiedSchemaId,
+ credentialDefinitionId: unqualifiedCredentialDefinitionId,
+ },
+ '_anoncreds/credentialRequest': {
+ link_secret_blinding_data: expect.any(Object),
+ link_secret_name: expect.any(String),
+ nonce: expect.any(String),
+ },
+ })
+
+ expect(issuerCredentialRecord.metadata.data).toEqual({
+ '_anoncreds/credential': {
+ schemaId: unqualifiedSchemaId,
+ credentialDefinitionId: unqualifiedCredentialDefinitionId,
+ },
+ })
+
+ const holderProofRecord = new ProofExchangeRecord({
+ protocolVersion: 'v1',
+ state: ProofState.ProposalSent,
+ threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38',
+ })
+ const verifierProofRecord = new ProofExchangeRecord({
+ protocolVersion: 'v1',
+ state: ProofState.ProposalReceived,
+ threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38',
+ })
+
+ const { attachment: proofProposalAttachment } = await legacyIndyProofFormatService.createProposal(agentContext, {
+ proofFormats: {
+ indy: {
+ attributes: [
+ {
+ name: 'name',
+ credentialDefinitionId: unqualifiedCredentialDefinitionId,
+ value: 'John',
+ referent: '1',
+ },
+ ],
+ predicates: [
+ {
+ credentialDefinitionId: unqualifiedCredentialDefinitionId,
+ name: 'age',
+ predicate: '>=',
+ threshold: 18,
+ },
+ ],
+ name: 'Proof Request',
+ version: '1.0',
+ },
+ },
+ proofRecord: holderProofRecord,
+ })
+
+ await legacyIndyProofFormatService.processProposal(agentContext, {
+ attachment: proofProposalAttachment,
+ proofRecord: verifierProofRecord,
+ })
+
+ const { attachment: proofRequestAttachment } = await legacyIndyProofFormatService.acceptProposal(agentContext, {
+ proofRecord: verifierProofRecord,
+ proposalAttachment: proofProposalAttachment,
+ })
+
+ await legacyIndyProofFormatService.processRequest(agentContext, {
+ attachment: proofRequestAttachment,
+ proofRecord: holderProofRecord,
+ })
+
+ const { attachment: proofAttachment } = await legacyIndyProofFormatService.acceptRequest(agentContext, {
+ proofRecord: holderProofRecord,
+ requestAttachment: proofRequestAttachment,
+ proposalAttachment: proofProposalAttachment,
+ })
+
+ const isValid = await legacyIndyProofFormatService.processPresentation(agentContext, {
+ attachment: proofAttachment,
+ proofRecord: verifierProofRecord,
+ requestAttachment: proofRequestAttachment,
+ })
+
+ expect(isValid).toBe(true)
+ })
+})
diff --git a/packages/anoncreds-rs/tests/setup.ts b/packages/anoncreds-rs/tests/setup.ts
new file mode 100644
index 0000000000..4760c40357
--- /dev/null
+++ b/packages/anoncreds-rs/tests/setup.ts
@@ -0,0 +1,4 @@
+import '@hyperledger/anoncreds-nodejs'
+import 'reflect-metadata'
+
+jest.setTimeout(120000)
diff --git a/packages/didcomm-v2/tsconfig.build.json b/packages/anoncreds-rs/tsconfig.build.json
similarity index 98%
rename from packages/didcomm-v2/tsconfig.build.json
rename to packages/anoncreds-rs/tsconfig.build.json
index 9c30e30bd2..2b75d0adab 100644
--- a/packages/didcomm-v2/tsconfig.build.json
+++ b/packages/anoncreds-rs/tsconfig.build.json
@@ -1,9 +1,7 @@
{
"extends": "../../tsconfig.build.json",
-
"compilerOptions": {
"outDir": "./build"
},
-
"include": ["src/**/*"]
}
diff --git a/packages/didcomm-v2/tsconfig.json b/packages/anoncreds-rs/tsconfig.json
similarity index 100%
rename from packages/didcomm-v2/tsconfig.json
rename to packages/anoncreds-rs/tsconfig.json
diff --git a/packages/anoncreds/README.md b/packages/anoncreds/README.md
new file mode 100644
index 0000000000..5bf5e5fbb0
--- /dev/null
+++ b/packages/anoncreds/README.md
@@ -0,0 +1,35 @@
+
+
+
+
+Aries Framework JavaScript AnonCreds Interfaces
+
+
+
+
+
+
+
+
+### Installation
+
+### Quick start
+
+### Example of usage
diff --git a/packages/anoncreds/jest.config.ts b/packages/anoncreds/jest.config.ts
new file mode 100644
index 0000000000..93c0197296
--- /dev/null
+++ b/packages/anoncreds/jest.config.ts
@@ -0,0 +1,13 @@
+import type { Config } from '@jest/types'
+
+import base from '../../jest.config.base'
+
+import packageJson from './package.json'
+
+const config: Config.InitialOptions = {
+ ...base,
+ displayName: packageJson.name,
+ setupFilesAfterEnv: ['./tests/setup.ts'],
+}
+
+export default config
diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json
new file mode 100644
index 0000000000..fabc377594
--- /dev/null
+++ b/packages/anoncreds/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@aries-framework/anoncreds",
+ "main": "build/index",
+ "types": "build/index",
+ "version": "0.3.3",
+ "files": [
+ "build"
+ ],
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/anoncreds",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/hyperledger/aries-framework-javascript",
+ "directory": "packages/anoncreds"
+ },
+ "scripts": {
+ "build": "yarn run clean && yarn run compile",
+ "clean": "rimraf ./build",
+ "compile": "tsc -p tsconfig.build.json",
+ "prepublishOnly": "yarn run build",
+ "test": "jest"
+ },
+ "dependencies": {
+ "@aries-framework/core": "0.3.3",
+ "bn.js": "^5.2.1",
+ "class-transformer": "0.5.1",
+ "class-validator": "0.14.0",
+ "reflect-metadata": "^0.1.13"
+ },
+ "devDependencies": {
+ "@aries-framework/node": "0.3.3",
+ "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.15",
+ "indy-sdk": "^1.16.0-dev-1636",
+ "rimraf": "^4.4.0",
+ "rxjs": "^7.8.0",
+ "typescript": "~4.9.5"
+ }
+}
diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts
new file mode 100644
index 0000000000..ede2e691e3
--- /dev/null
+++ b/packages/anoncreds/src/AnonCredsApi.ts
@@ -0,0 +1,452 @@
+import type {
+ AnonCredsCreateLinkSecretOptions,
+ AnonCredsRegisterCredentialDefinitionOptions,
+} from './AnonCredsApiOptions'
+import type {
+ GetCredentialDefinitionReturn,
+ GetRevocationStatusListReturn,
+ GetRevocationRegistryDefinitionReturn,
+ GetSchemaReturn,
+ RegisterCredentialDefinitionReturn,
+ RegisterSchemaOptions,
+ RegisterSchemaReturn,
+ AnonCredsRegistry,
+ GetCredentialsOptions,
+} from './services'
+import type { Extensible } from './services/registry/base'
+import type { SimpleQuery } from '@aries-framework/core'
+
+import { AgentContext, inject, injectable } from '@aries-framework/core'
+
+import { AnonCredsModuleConfig } from './AnonCredsModuleConfig'
+import { AnonCredsStoreRecordError } from './error'
+import {
+ AnonCredsCredentialDefinitionPrivateRecord,
+ AnonCredsCredentialDefinitionPrivateRepository,
+ AnonCredsKeyCorrectnessProofRecord,
+ AnonCredsKeyCorrectnessProofRepository,
+ AnonCredsLinkSecretRecord,
+ AnonCredsLinkSecretRepository,
+} from './repository'
+import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord'
+import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository'
+import { AnonCredsSchemaRecord } from './repository/AnonCredsSchemaRecord'
+import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository'
+import { AnonCredsCredentialDefinitionRecordMetadataKeys } from './repository/anonCredsCredentialDefinitionRecordMetadataTypes'
+import {
+ AnonCredsHolderServiceSymbol,
+ AnonCredsIssuerServiceSymbol,
+ AnonCredsIssuerService,
+ AnonCredsHolderService,
+} from './services'
+import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService'
+
+@injectable()
+export class AnonCredsApi {
+ public config: AnonCredsModuleConfig
+
+ private agentContext: AgentContext
+ private anonCredsRegistryService: AnonCredsRegistryService
+ private anonCredsSchemaRepository: AnonCredsSchemaRepository
+ private anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository
+ private anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository
+ private anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository
+ private anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository
+ private anonCredsIssuerService: AnonCredsIssuerService
+ private anonCredsHolderService: AnonCredsHolderService
+
+ public constructor(
+ agentContext: AgentContext,
+ anonCredsRegistryService: AnonCredsRegistryService,
+ config: AnonCredsModuleConfig,
+ @inject(AnonCredsIssuerServiceSymbol) anonCredsIssuerService: AnonCredsIssuerService,
+ @inject(AnonCredsHolderServiceSymbol) anonCredsHolderService: AnonCredsHolderService,
+ anonCredsSchemaRepository: AnonCredsSchemaRepository,
+ anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository,
+ anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository,
+ anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository,
+ anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository
+ ) {
+ this.agentContext = agentContext
+ this.anonCredsRegistryService = anonCredsRegistryService
+ this.config = config
+ this.anonCredsIssuerService = anonCredsIssuerService
+ this.anonCredsHolderService = anonCredsHolderService
+ this.anonCredsSchemaRepository = anonCredsSchemaRepository
+ this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository
+ this.anonCredsCredentialDefinitionPrivateRepository = anonCredsCredentialDefinitionPrivateRepository
+ this.anonCredsKeyCorrectnessProofRepository = anonCredsKeyCorrectnessProofRepository
+ this.anonCredsLinkSecretRepository = anonCredsLinkSecretRepository
+ }
+
+ /**
+ * Create a Link Secret, optionally indicating its ID and if it will be the default one
+ * If there is no default Link Secret, this will be set as default (even if setAsDefault is true).
+ *
+ */
+ public async createLinkSecret(options?: AnonCredsCreateLinkSecretOptions) {
+ const { linkSecretId, linkSecretValue } = await this.anonCredsHolderService.createLinkSecret(this.agentContext, {
+ linkSecretId: options?.linkSecretId,
+ })
+
+ // In some cases we don't have the linkSecretValue. However we still want a record so we know which link secret ids are valid
+ const linkSecretRecord = new AnonCredsLinkSecretRecord({ linkSecretId, value: linkSecretValue })
+
+ // If it is the first link secret registered, set as default
+ const defaultLinkSecretRecord = await this.anonCredsLinkSecretRepository.findDefault(this.agentContext)
+ if (!defaultLinkSecretRecord || options?.setAsDefault) {
+ linkSecretRecord.setTag('isDefault', true)
+ }
+
+ // Set the current default link secret as not default
+ if (defaultLinkSecretRecord && options?.setAsDefault) {
+ defaultLinkSecretRecord.setTag('isDefault', false)
+ await this.anonCredsLinkSecretRepository.update(this.agentContext, defaultLinkSecretRecord)
+ }
+
+ await this.anonCredsLinkSecretRepository.save(this.agentContext, linkSecretRecord)
+ }
+
+ /**
+ * Get a list of ids for the created link secrets
+ */
+ public async getLinkSecretIds(): Promise {
+ const linkSecrets = await this.anonCredsLinkSecretRepository.getAll(this.agentContext)
+
+ return linkSecrets.map((linkSecret) => linkSecret.linkSecretId)
+ }
+
+ /**
+ * Retrieve a {@link AnonCredsSchema} from the registry associated
+ * with the {@link schemaId}
+ */
+ public async getSchema(schemaId: string): Promise {
+ const failedReturnBase = {
+ resolutionMetadata: {
+ error: 'error',
+ message: `Unable to resolve schema ${schemaId}`,
+ },
+ schemaId,
+ schemaMetadata: {},
+ }
+
+ const registry = this.findRegistryForIdentifier(schemaId)
+ if (!registry) {
+ failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod'
+ failedReturnBase.resolutionMetadata.message = `Unable to resolve schema ${schemaId}: No registry found for identifier ${schemaId}`
+ return failedReturnBase
+ }
+
+ try {
+ const result = await registry.getSchema(this.agentContext, schemaId)
+ return result
+ } catch (error) {
+ failedReturnBase.resolutionMetadata.message = `Unable to resolve schema ${schemaId}: ${error.message}`
+ return failedReturnBase
+ }
+ }
+
+ public async registerSchema(options: RegisterSchemaOptions): Promise {
+ const failedReturnBase = {
+ schemaState: {
+ state: 'failed' as const,
+ schema: options.schema,
+ reason: `Error registering schema for issuerId ${options.schema.issuerId}`,
+ },
+ registrationMetadata: {},
+ schemaMetadata: {},
+ }
+
+ const registry = this.findRegistryForIdentifier(options.schema.issuerId)
+ if (!registry) {
+ failedReturnBase.schemaState.reason = `Unable to register schema. No registry found for issuerId ${options.schema.issuerId}`
+ return failedReturnBase
+ }
+
+ try {
+ const result = await registry.registerSchema(this.agentContext, options)
+ await this.storeSchemaRecord(registry, result)
+
+ return result
+ } catch (error) {
+ // Storage failed
+ if (error instanceof AnonCredsStoreRecordError) {
+ failedReturnBase.schemaState.reason = `Error storing schema record: ${error.message}`
+ return failedReturnBase
+ }
+
+ // In theory registerSchema SHOULD NOT throw, but we can't know for sure
+ failedReturnBase.schemaState.reason = `Error registering schema: ${error.message}`
+ return failedReturnBase
+ }
+ }
+
+ public async getCreatedSchemas(query: SimpleQuery) {
+ return this.anonCredsSchemaRepository.findByQuery(this.agentContext, query)
+ }
+
+ /**
+ * Retrieve a {@link AnonCredsCredentialDefinition} from the registry associated
+ * with the {@link credentialDefinitionId}
+ */
+ public async getCredentialDefinition(credentialDefinitionId: string): Promise {
+ const failedReturnBase = {
+ resolutionMetadata: {
+ error: 'error',
+ message: `Unable to resolve credential definition ${credentialDefinitionId}`,
+ },
+ credentialDefinitionId,
+ credentialDefinitionMetadata: {},
+ }
+
+ const registry = this.findRegistryForIdentifier(credentialDefinitionId)
+ if (!registry) {
+ failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod'
+ failedReturnBase.resolutionMetadata.message = `Unable to resolve credential definition ${credentialDefinitionId}: No registry found for identifier ${credentialDefinitionId}`
+ return failedReturnBase
+ }
+
+ try {
+ const result = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId)
+ return result
+ } catch (error) {
+ failedReturnBase.resolutionMetadata.message = `Unable to resolve credential definition ${credentialDefinitionId}: ${error.message}`
+ return failedReturnBase
+ }
+ }
+
+ public async registerCredentialDefinition(options: {
+ credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions
+ // TODO: options should support supportsRevocation at some points
+ options: Extensible
+ }): Promise {
+ const failedReturnBase = {
+ credentialDefinitionState: {
+ state: 'failed' as const,
+ reason: `Error registering credential definition for issuerId ${options.credentialDefinition.issuerId}`,
+ },
+ registrationMetadata: {},
+ credentialDefinitionMetadata: {},
+ }
+
+ const registry = this.findRegistryForIdentifier(options.credentialDefinition.issuerId)
+ if (!registry) {
+ failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for issuerId ${options.credentialDefinition.issuerId}`
+ return failedReturnBase
+ }
+
+ const schemaRegistry = this.findRegistryForIdentifier(options.credentialDefinition.schemaId)
+ if (!schemaRegistry) {
+ failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for schemaId ${options.credentialDefinition.schemaId}`
+ return failedReturnBase
+ }
+
+ try {
+ const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId)
+
+ if (!schemaResult.schema) {
+ failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.credentialDefinition.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}`
+ return failedReturnBase
+ }
+
+ const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } =
+ await this.anonCredsIssuerService.createCredentialDefinition(
+ this.agentContext,
+ {
+ issuerId: options.credentialDefinition.issuerId,
+ schemaId: options.credentialDefinition.schemaId,
+ tag: options.credentialDefinition.tag,
+ supportRevocation: false,
+ schema: schemaResult.schema,
+ },
+ // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal.
+ {
+ indyLedgerSchemaSeqNo: schemaResult.schemaMetadata.indyLedgerSeqNo,
+ }
+ )
+
+ const result = await registry.registerCredentialDefinition(this.agentContext, {
+ credentialDefinition,
+ options: options.options,
+ })
+
+ await this.storeCredentialDefinitionRecord(registry, result, credentialDefinitionPrivate, keyCorrectnessProof)
+
+ return result
+ } catch (error) {
+ // Storage failed
+ if (error instanceof AnonCredsStoreRecordError) {
+ failedReturnBase.credentialDefinitionState.reason = `Error storing credential definition records: ${error.message}`
+ return failedReturnBase
+ }
+
+ // In theory registerCredentialDefinition SHOULD NOT throw, but we can't know for sure
+ failedReturnBase.credentialDefinitionState.reason = `Error registering credential definition: ${error.message}`
+ return failedReturnBase
+ }
+ }
+
+ public async getCreatedCredentialDefinitions(query: SimpleQuery) {
+ return this.anonCredsCredentialDefinitionRepository.findByQuery(this.agentContext, query)
+ }
+
+ /**
+ * Retrieve a {@link AnonCredsRevocationRegistryDefinition} from the registry associated
+ * with the {@link revocationRegistryDefinitionId}
+ */
+ public async getRevocationRegistryDefinition(
+ revocationRegistryDefinitionId: string
+ ): Promise {
+ const failedReturnBase = {
+ resolutionMetadata: {
+ error: 'error',
+ message: `Unable to resolve revocation registry ${revocationRegistryDefinitionId}`,
+ },
+ revocationRegistryDefinitionId,
+ revocationRegistryDefinitionMetadata: {},
+ }
+
+ const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId)
+ if (!registry) {
+ failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod'
+ failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}`
+ return failedReturnBase
+ }
+
+ try {
+ const result = await registry.getRevocationRegistryDefinition(this.agentContext, revocationRegistryDefinitionId)
+ return result
+ } catch (error) {
+ failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: ${error.message}`
+ return failedReturnBase
+ }
+ }
+
+ /**
+ * Retrieve the {@link AnonCredsRevocationStatusList} for the given {@link timestamp} from the registry associated
+ * with the {@link revocationRegistryDefinitionId}
+ */
+ public async getRevocationStatusList(
+ revocationRegistryDefinitionId: string,
+ timestamp: number
+ ): Promise {
+ const failedReturnBase = {
+ resolutionMetadata: {
+ error: 'error',
+ message: `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}`,
+ },
+ revocationStatusListMetadata: {},
+ }
+
+ const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId)
+ if (!registry) {
+ failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod'
+ failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}`
+ return failedReturnBase
+ }
+
+ try {
+ const result = await registry.getRevocationStatusList(
+ this.agentContext,
+ revocationRegistryDefinitionId,
+ timestamp
+ )
+ return result
+ } catch (error) {
+ failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}: ${error.message}`
+ return failedReturnBase
+ }
+ }
+
+ public async getCredential(credentialId: string) {
+ return this.anonCredsHolderService.getCredential(this.agentContext, { credentialId })
+ }
+
+ public async getCredentials(options: GetCredentialsOptions) {
+ return this.anonCredsHolderService.getCredentials(this.agentContext, options)
+ }
+
+ private async storeCredentialDefinitionRecord(
+ registry: AnonCredsRegistry,
+ result: RegisterCredentialDefinitionReturn,
+ credentialDefinitionPrivate?: Record,
+ keyCorrectnessProof?: Record
+ ): Promise {
+ try {
+ // If we have both the credentialDefinition and the credentialDefinitionId we will store a copy of the credential definition. We may need to handle an
+ // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel
+ if (
+ result.credentialDefinitionState.credentialDefinition &&
+ result.credentialDefinitionState.credentialDefinitionId
+ ) {
+ const credentialDefinitionRecord = new AnonCredsCredentialDefinitionRecord({
+ credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId,
+ credentialDefinition: result.credentialDefinitionState.credentialDefinition,
+ methodName: registry.methodName,
+ })
+
+ // TODO: do we need to store this metadata? For indy, the registration metadata contains e.g.
+ // the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions
+ // are stored in the metadata
+ credentialDefinitionRecord.metadata.set(
+ AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata,
+ result.credentialDefinitionMetadata
+ )
+ credentialDefinitionRecord.metadata.set(
+ AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata,
+ result.registrationMetadata
+ )
+
+ await this.anonCredsCredentialDefinitionRepository.save(this.agentContext, credentialDefinitionRecord)
+
+ // Store Credential Definition private data (if provided by issuer service)
+ if (credentialDefinitionPrivate) {
+ const credentialDefinitionPrivateRecord = new AnonCredsCredentialDefinitionPrivateRecord({
+ credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId,
+ value: credentialDefinitionPrivate,
+ })
+ await this.anonCredsCredentialDefinitionPrivateRepository.save(
+ this.agentContext,
+ credentialDefinitionPrivateRecord
+ )
+ }
+
+ if (keyCorrectnessProof) {
+ const keyCorrectnessProofRecord = new AnonCredsKeyCorrectnessProofRecord({
+ credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId,
+ value: keyCorrectnessProof,
+ })
+ await this.anonCredsKeyCorrectnessProofRepository.save(this.agentContext, keyCorrectnessProofRecord)
+ }
+ }
+ } catch (error) {
+ throw new AnonCredsStoreRecordError(`Error storing credential definition records`, { cause: error })
+ }
+ }
+
+ private async storeSchemaRecord(registry: AnonCredsRegistry, result: RegisterSchemaReturn): Promise {
+ try {
+ // If we have both the schema and the schemaId we will store a copy of the schema. We may need to handle an
+ // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel
+ if (result.schemaState.schema && result.schemaState.schemaId) {
+ const schemaRecord = new AnonCredsSchemaRecord({
+ schemaId: result.schemaState.schemaId,
+ schema: result.schemaState.schema,
+ methodName: registry.methodName,
+ })
+
+ await this.anonCredsSchemaRepository.save(this.agentContext, schemaRecord)
+ }
+ } catch (error) {
+ throw new AnonCredsStoreRecordError(`Error storing schema record`, { cause: error })
+ }
+ }
+
+ private findRegistryForIdentifier(identifier: string) {
+ try {
+ return this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, identifier)
+ } catch {
+ return null
+ }
+ }
+}
diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts
new file mode 100644
index 0000000000..860ea059df
--- /dev/null
+++ b/packages/anoncreds/src/AnonCredsApiOptions.ts
@@ -0,0 +1,8 @@
+import type { AnonCredsCredentialDefinition } from './models'
+
+export interface AnonCredsCreateLinkSecretOptions {
+ linkSecretId?: string
+ setAsDefault?: boolean
+}
+
+export type AnonCredsRegisterCredentialDefinitionOptions = Omit
diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts
new file mode 100644
index 0000000000..11b923e093
--- /dev/null
+++ b/packages/anoncreds/src/AnonCredsModule.ts
@@ -0,0 +1,48 @@
+import type { AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig'
+import type { DependencyManager, Module, Update } from '@aries-framework/core'
+
+import { AnonCredsApi } from './AnonCredsApi'
+import { AnonCredsModuleConfig } from './AnonCredsModuleConfig'
+import {
+ AnonCredsCredentialDefinitionPrivateRepository,
+ AnonCredsKeyCorrectnessProofRepository,
+ AnonCredsLinkSecretRepository,
+} from './repository'
+import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository'
+import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository'
+import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService'
+import { updateAnonCredsModuleV0_3_1ToV0_4 } from './updates/0.3.1-0.4'
+
+/**
+ * @public
+ */
+export class AnonCredsModule implements Module {
+ public readonly config: AnonCredsModuleConfig
+ public api = AnonCredsApi
+
+ public constructor(config: AnonCredsModuleConfigOptions) {
+ this.config = new AnonCredsModuleConfig(config)
+ }
+
+ public register(dependencyManager: DependencyManager) {
+ // Config
+ dependencyManager.registerInstance(AnonCredsModuleConfig, this.config)
+
+ dependencyManager.registerSingleton(AnonCredsRegistryService)
+
+ // Repositories
+ dependencyManager.registerSingleton(AnonCredsSchemaRepository)
+ dependencyManager.registerSingleton(AnonCredsCredentialDefinitionRepository)
+ dependencyManager.registerSingleton(AnonCredsCredentialDefinitionPrivateRepository)
+ dependencyManager.registerSingleton(AnonCredsKeyCorrectnessProofRepository)
+ dependencyManager.registerSingleton(AnonCredsLinkSecretRepository)
+ }
+
+ public updates: Update[] = [
+ {
+ fromVersion: '0.3.1',
+ toVersion: '0.4',
+ doUpdate: updateAnonCredsModuleV0_3_1ToV0_4,
+ },
+ ]
+}
diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts
new file mode 100644
index 0000000000..9f7b971aab
--- /dev/null
+++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts
@@ -0,0 +1,28 @@
+import type { AnonCredsRegistry } from './services'
+
+/**
+ * @public
+ * AnonCredsModuleConfigOptions defines the interface for the options of the AnonCredsModuleConfig class.
+ */
+export interface AnonCredsModuleConfigOptions {
+ /**
+ * A list of AnonCreds registries to make available to the AnonCreds module.
+ */
+ registries: [AnonCredsRegistry, ...AnonCredsRegistry[]]
+}
+
+/**
+ * @public
+ */
+export class AnonCredsModuleConfig {
+ private options: AnonCredsModuleConfigOptions
+
+ public constructor(options: AnonCredsModuleConfigOptions) {
+ this.options = options
+ }
+
+ /** See {@link AnonCredsModuleConfigOptions.registries} */
+ public get registries() {
+ return this.options.registries
+ }
+}
diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts
new file mode 100644
index 0000000000..f9c868c14c
--- /dev/null
+++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts
@@ -0,0 +1,40 @@
+import type { AnonCredsRegistry } from '../services'
+import type { DependencyManager } from '@aries-framework/core'
+
+import { AnonCredsModule } from '../AnonCredsModule'
+import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig'
+import {
+ AnonCredsSchemaRepository,
+ AnonCredsCredentialDefinitionRepository,
+ AnonCredsCredentialDefinitionPrivateRepository,
+ AnonCredsKeyCorrectnessProofRepository,
+ AnonCredsLinkSecretRepository,
+} from '../repository'
+import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService'
+
+const dependencyManager = {
+ registerInstance: jest.fn(),
+ registerSingleton: jest.fn(),
+} as unknown as DependencyManager
+
+const registry = {} as AnonCredsRegistry
+
+describe('AnonCredsModule', () => {
+ test('registers dependencies on the dependency manager', () => {
+ const anonCredsModule = new AnonCredsModule({
+ registries: [registry],
+ })
+ anonCredsModule.register(dependencyManager)
+
+ expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(6)
+ expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRegistryService)
+ expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsSchemaRepository)
+ expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionRepository)
+ expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionPrivateRepository)
+ expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsKeyCorrectnessProofRepository)
+ expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsLinkSecretRepository)
+
+ expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1)
+ expect(dependencyManager.registerInstance).toHaveBeenCalledWith(AnonCredsModuleConfig, anonCredsModule.config)
+ })
+})
diff --git a/packages/anoncreds/src/__tests__/AnonCredsModuleConfig.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModuleConfig.test.ts
new file mode 100644
index 0000000000..beaca8bf53
--- /dev/null
+++ b/packages/anoncreds/src/__tests__/AnonCredsModuleConfig.test.ts
@@ -0,0 +1,15 @@
+import type { AnonCredsRegistry } from '../services'
+
+import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig'
+
+describe('AnonCredsModuleConfig', () => {
+ test('sets values', () => {
+ const registry = {} as AnonCredsRegistry
+
+ const config = new AnonCredsModuleConfig({
+ registries: [registry],
+ })
+
+ expect(config.registries).toEqual([registry])
+ })
+})
diff --git a/packages/anoncreds/src/error/AnonCredsError.ts b/packages/anoncreds/src/error/AnonCredsError.ts
new file mode 100644
index 0000000000..eb6d250a4a
--- /dev/null
+++ b/packages/anoncreds/src/error/AnonCredsError.ts
@@ -0,0 +1,7 @@
+import { AriesFrameworkError } from '@aries-framework/core'
+
+export class AnonCredsError extends AriesFrameworkError {
+ public constructor(message: string, { cause }: { cause?: Error } = {}) {
+ super(message, { cause })
+ }
+}
diff --git a/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts b/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts
new file mode 100644
index 0000000000..11437d7b64
--- /dev/null
+++ b/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts
@@ -0,0 +1,7 @@
+import { AnonCredsError } from './AnonCredsError'
+
+export class AnonCredsStoreRecordError extends AnonCredsError {
+ public constructor(message: string, { cause }: { cause?: Error } = {}) {
+ super(message, { cause })
+ }
+}
diff --git a/packages/anoncreds/src/error/index.ts b/packages/anoncreds/src/error/index.ts
new file mode 100644
index 0000000000..6d25bc4dbb
--- /dev/null
+++ b/packages/anoncreds/src/error/index.ts
@@ -0,0 +1,2 @@
+export * from './AnonCredsError'
+export * from './AnonCredsStoreRecordError'
diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts
new file mode 100644
index 0000000000..dba5361a41
--- /dev/null
+++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts
@@ -0,0 +1,93 @@
+import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models'
+import type { CredentialPreviewAttributeOptions, CredentialFormat, LinkedAttachment } from '@aries-framework/core'
+
+export interface AnonCredsCredentialProposalFormat {
+ schema_issuer_id?: string
+ schema_name?: string
+ schema_version?: string
+ schema_id?: string
+
+ cred_def_id?: string
+ issuer_id?: string
+
+ // TODO: we don't necessarily need to include these in the AnonCreds Format RFC
+ // as it's a new one and we can just forbid the use of legacy properties
+ schema_issuer_did?: string
+ issuer_did?: string
+}
+
+/**
+ * This defines the module payload for calling CredentialsApi.createProposal
+ * or CredentialsApi.negotiateOffer
+ */
+export interface AnonCredsProposeCredentialFormat {
+ schemaIssuerId?: string
+ schemaId?: string
+ schemaName?: string
+ schemaVersion?: string
+
+ credentialDefinitionId?: string
+ issuerId?: string
+
+ attributes?: CredentialPreviewAttributeOptions[]
+ linkedAttachments?: LinkedAttachment[]
+
+ // Kept for backwards compatibility
+ schemaIssuerDid?: string
+ issuerDid?: string
+}
+
+/**
+ * This defines the module payload for calling CredentialsApi.acceptProposal
+ */
+export interface AnonCredsAcceptProposalFormat {
+ credentialDefinitionId?: string
+ attributes?: CredentialPreviewAttributeOptions[]
+ linkedAttachments?: LinkedAttachment[]
+}
+
+/**
+ * This defines the module payload for calling CredentialsApi.acceptOffer. No options are available for this
+ * method, so it's an empty object
+ */
+export interface AnonCredsAcceptOfferFormat {
+ linkSecretId?: string
+}
+
+/**
+ * This defines the module payload for calling CredentialsApi.offerCredential
+ * or CredentialsApi.negotiateProposal
+ */
+export interface AnonCredsOfferCredentialFormat {
+ credentialDefinitionId: string
+ attributes: CredentialPreviewAttributeOptions[]
+ linkedAttachments?: LinkedAttachment[]
+}
+
+/**
+ * This defines the module payload for calling CredentialsApi.acceptRequest. No options are available for this
+ * method, so it's an empty object
+ */
+export type AnonCredsAcceptRequestFormat = Record
+
+export interface AnonCredsCredentialFormat extends CredentialFormat {
+ formatKey: 'anoncreds'
+ credentialRecordType: 'anoncreds'
+ credentialFormats: {
+ createProposal: AnonCredsProposeCredentialFormat
+ acceptProposal: AnonCredsAcceptProposalFormat
+ createOffer: AnonCredsOfferCredentialFormat
+ acceptOffer: AnonCredsAcceptOfferFormat
+ createRequest: never // cannot start from createRequest
+ acceptRequest: AnonCredsAcceptRequestFormat
+ }
+ // TODO: update to new RFC once available
+ // Format data is based on RFC 0592
+ // https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments
+ formatData: {
+ proposal: AnonCredsCredentialProposalFormat
+ offer: AnonCredsCredentialOffer
+ request: AnonCredsCredentialRequest
+ credential: AnonCredsCredential
+ }
+}
diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts
new file mode 100644
index 0000000000..f9627cd0aa
--- /dev/null
+++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts
@@ -0,0 +1,636 @@
+import type { AnonCredsCredentialFormat, AnonCredsCredentialProposalFormat } from './AnonCredsCredentialFormat'
+import type {
+ AnonCredsCredential,
+ AnonCredsCredentialOffer,
+ AnonCredsCredentialRequest,
+ AnonCredsCredentialRequestMetadata,
+} from '../models'
+import type { AnonCredsIssuerService, AnonCredsHolderService, GetRevocationRegistryDefinitionReturn } from '../services'
+import type { AnonCredsCredentialMetadata } from '../utils/metadata'
+import type {
+ CredentialFormatService,
+ AgentContext,
+ CredentialFormatCreateProposalOptions,
+ CredentialFormatCreateProposalReturn,
+ CredentialFormatProcessOptions,
+ CredentialFormatAcceptProposalOptions,
+ CredentialFormatCreateOfferReturn,
+ CredentialFormatCreateOfferOptions,
+ CredentialFormatAcceptOfferOptions,
+ CredentialFormatCreateReturn,
+ CredentialFormatAcceptRequestOptions,
+ CredentialFormatProcessCredentialOptions,
+ CredentialFormatAutoRespondProposalOptions,
+ CredentialFormatAutoRespondOfferOptions,
+ CredentialFormatAutoRespondRequestOptions,
+ CredentialFormatAutoRespondCredentialOptions,
+ CredentialExchangeRecord,
+ CredentialPreviewAttributeOptions,
+ LinkedAttachment,
+} from '@aries-framework/core'
+
+import {
+ ProblemReportError,
+ MessageValidator,
+ CredentialFormatSpec,
+ AriesFrameworkError,
+ JsonEncoder,
+ utils,
+ CredentialProblemReportReason,
+ JsonTransformer,
+ V1Attachment,
+ V1AttachmentData,
+} from '@aries-framework/core'
+
+import { AnonCredsError } from '../error'
+import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal'
+import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services'
+import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService'
+import {
+ convertAttributesToCredentialValues,
+ assertCredentialValuesMatch,
+ checkCredentialValuesMatch,
+ assertAttributesMatch,
+ createAndLinkAttachmentsToPreview,
+} from '../utils/credential'
+import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata'
+
+const ANONCREDS_CREDENTIAL_OFFER = 'anoncreds/credential-offer@v1.0'
+const ANONCREDS_CREDENTIAL_REQUEST = 'anoncreds/credential-request@v1.0'
+const ANONCREDS_CREDENTIAL_FILTER = 'anoncreds/credential-filter@v1.0'
+const ANONCREDS_CREDENTIAL = 'anoncreds/credential@v1.0'
+
+export class AnonCredsCredentialFormatService implements CredentialFormatService {
+ /** formatKey is the key used when calling agent.credentials.xxx with credentialFormats.anoncreds */
+ public readonly formatKey = 'anoncreds' as const
+
+ /**
+ * credentialRecordType is the type of record that stores the credential. It is stored in the credential
+ * record binding in the credential exchange record.
+ */
+ public readonly credentialRecordType = 'anoncreds' as const
+
+ /**
+ * Create a {@link AttachmentFormats} object dependent on the message type.
+ *
+ * @param options The object containing all the options for the proposed credential
+ * @returns object containing associated attachment, format and optionally the credential preview
+ *
+ */
+ public async createProposal(
+ agentContext: AgentContext,
+ { credentialFormats, credentialRecord }: CredentialFormatCreateProposalOptions
+ ): Promise {
+ const format = new CredentialFormatSpec({
+ format: ANONCREDS_CREDENTIAL_FILTER,
+ })
+
+ const anoncredsFormat = credentialFormats.anoncreds
+
+ if (!anoncredsFormat) {
+ throw new AriesFrameworkError('Missing anoncreds payload in createProposal')
+ }
+
+ // We want all properties except for `attributes` and `linkedAttachments` attributes.
+ // The easiest way is to destructure and use the spread operator. But that leaves the other properties unused
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { attributes, linkedAttachments, ...anoncredsCredentialProposal } = anoncredsFormat
+ const proposal = new AnonCredsCredentialProposal(anoncredsCredentialProposal)
+
+ try {
+ MessageValidator.validateSync(proposal)
+ } catch (error) {
+ throw new AriesFrameworkError(
+ `Invalid proposal supplied: ${anoncredsCredentialProposal} in AnonCredsFormatService`
+ )
+ }
+
+ const attachment = this.getFormatData(JsonTransformer.toJSON(proposal), format.attachmentId)
+
+ const { previewAttributes } = this.getCredentialLinkedAttachments(
+ anoncredsFormat.attributes,
+ anoncredsFormat.linkedAttachments
+ )
+
+ // Set the metadata
+ credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, {
+ schemaId: proposal.schemaId,
+ credentialDefinitionId: proposal.credentialDefinitionId,
+ })
+
+ return { format, attachment, previewAttributes }
+ }
+
+ public async processProposal(
+ agentContext: AgentContext,
+ { attachment }: CredentialFormatProcessOptions
+ ): Promise {
+ const proposalJson = attachment.getDataAsJson()
+
+ JsonTransformer.fromJSON(proposalJson, AnonCredsCredentialProposal)
+ }
+
+ public async acceptProposal(
+ agentContext: AgentContext,
+ {
+ attachmentId,
+ credentialFormats,
+ credentialRecord,
+ proposalAttachment,
+ }: CredentialFormatAcceptProposalOptions
+ ): Promise {
+ const anoncredsFormat = credentialFormats?.anoncreds
+
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const credentialDefinitionId = anoncredsFormat?.credentialDefinitionId ?? proposalJson.cred_def_id
+
+ const attributes = anoncredsFormat?.attributes ?? credentialRecord.credentialAttributes
+
+ if (!credentialDefinitionId) {
+ throw new AriesFrameworkError(
+ 'No credential definition id in proposal or provided as input to accept proposal method.'
+ )
+ }
+
+ if (!attributes) {
+ throw new AriesFrameworkError('No attributes in proposal or provided as input to accept proposal method.')
+ }
+
+ const { format, attachment, previewAttributes } = await this.createAnonCredsOffer(agentContext, {
+ credentialRecord,
+ attachmentId,
+ attributes,
+ credentialDefinitionId,
+ linkedAttachments: anoncredsFormat?.linkedAttachments,
+ })
+
+ return { format, attachment, previewAttributes }
+ }
+
+ /**
+ * Create a credential attachment format for a credential request.
+ *
+ * @param options The object containing all the options for the credential offer
+ * @returns object containing associated attachment, formats and offersAttach elements
+ *
+ */
+ public async createOffer(
+ agentContext: AgentContext,
+ { credentialFormats, credentialRecord, attachmentId }: CredentialFormatCreateOfferOptions
+ ): Promise {
+ const anoncredsFormat = credentialFormats.anoncreds
+
+ if (!anoncredsFormat) {
+ throw new AriesFrameworkError('Missing anoncreds credential format data')
+ }
+
+ const { format, attachment, previewAttributes } = await this.createAnonCredsOffer(agentContext, {
+ credentialRecord,
+ attachmentId,
+ attributes: anoncredsFormat.attributes,
+ credentialDefinitionId: anoncredsFormat.credentialDefinitionId,
+ linkedAttachments: anoncredsFormat.linkedAttachments,
+ })
+
+ return { format, attachment, previewAttributes }
+ }
+
+ public async processOffer(
+ agentContext: AgentContext,
+ { attachment, credentialRecord }: CredentialFormatProcessOptions
+ ) {
+ agentContext.config.logger.debug(
+ `Processing anoncreds credential offer for credential record ${credentialRecord.id}`
+ )
+
+ const credOffer = attachment.getDataAsJson()
+
+ if (!credOffer.schema_id || !credOffer.cred_def_id) {
+ throw new ProblemReportError('Invalid credential offer', {
+ problemCode: CredentialProblemReportReason.IssuanceAbandoned,
+ })
+ }
+ }
+
+ public async acceptOffer(
+ agentContext: AgentContext,
+ {
+ credentialRecord,
+ attachmentId,
+ offerAttachment,
+ credentialFormats,
+ }: CredentialFormatAcceptOfferOptions
+ ): Promise {
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+ const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ const credentialOffer = offerAttachment.getDataAsJson()
+
+ // Get credential definition
+ const registry = registryService.getRegistryForIdentifier(agentContext, credentialOffer.cred_def_id)
+ const { credentialDefinition, resolutionMetadata } = await registry.getCredentialDefinition(
+ agentContext,
+ credentialOffer.cred_def_id
+ )
+
+ if (!credentialDefinition) {
+ throw new AnonCredsError(
+ `Unable to retrieve credential definition with id ${credentialOffer.cred_def_id}: ${resolutionMetadata.error} ${resolutionMetadata.message}`
+ )
+ }
+
+ const { credentialRequest, credentialRequestMetadata } = await holderService.createCredentialRequest(agentContext, {
+ credentialOffer,
+ credentialDefinition,
+ linkSecretId: credentialFormats?.anoncreds?.linkSecretId,
+ })
+
+ credentialRecord.metadata.set(
+ AnonCredsCredentialRequestMetadataKey,
+ credentialRequestMetadata
+ )
+ credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, {
+ credentialDefinitionId: credentialOffer.cred_def_id,
+ schemaId: credentialOffer.schema_id,
+ })
+
+ const format = new CredentialFormatSpec({
+ attachmentId,
+ format: ANONCREDS_CREDENTIAL_REQUEST,
+ })
+
+ const attachment = this.getFormatData(credentialRequest, format.attachmentId)
+ return { format, attachment }
+ }
+
+ /**
+ * Starting from a request is not supported for anoncreds credentials, this method only throws an error.
+ */
+ public async createRequest(): Promise {
+ throw new AriesFrameworkError('Starting from a request is not supported for anoncreds credentials')
+ }
+
+ /**
+ * We don't have any models to validate an anoncreds request object, for now this method does nothing
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public async processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise {
+ // not needed for anoncreds
+ }
+
+ public async acceptRequest(
+ agentContext: AgentContext,
+ {
+ credentialRecord,
+ attachmentId,
+ offerAttachment,
+ requestAttachment,
+ }: CredentialFormatAcceptRequestOptions
+ ): Promise {
+ // Assert credential attributes
+ const credentialAttributes = credentialRecord.credentialAttributes
+ if (!credentialAttributes) {
+ throw new AriesFrameworkError(
+ `Missing required credential attribute values on credential record with id ${credentialRecord.id}`
+ )
+ }
+
+ const anonCredsIssuerService =
+ agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol)
+
+ const credentialOffer = offerAttachment?.getDataAsJson()
+ if (!credentialOffer) throw new AriesFrameworkError('Missing anoncreds credential offer in createCredential')
+
+ const credentialRequest = requestAttachment.getDataAsJson()
+ if (!credentialRequest) throw new AriesFrameworkError('Missing anoncreds credential request in createCredential')
+
+ const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, {
+ credentialOffer,
+ credentialRequest,
+ credentialValues: convertAttributesToCredentialValues(credentialAttributes),
+ })
+
+ if (credential.rev_reg_id) {
+ credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, {
+ credentialRevocationId: credentialRevocationId,
+ revocationRegistryId: credential.rev_reg_id,
+ })
+ credentialRecord.setTags({
+ anonCredsRevocationRegistryId: credential.rev_reg_id,
+ anonCredsCredentialRevocationId: credentialRevocationId,
+ })
+ }
+
+ const format = new CredentialFormatSpec({
+ attachmentId,
+ format: ANONCREDS_CREDENTIAL,
+ })
+
+ const attachment = this.getFormatData(credential, format.attachmentId)
+ return { format, attachment }
+ }
+
+ /**
+ * Processes an incoming credential - retrieve metadata, retrieve payload and store it in wallet
+ * @param options the issue credential message wrapped inside this object
+ * @param credentialRecord the credential exchange record for this credential
+ */
+ public async processCredential(
+ agentContext: AgentContext,
+ { credentialRecord, attachment }: CredentialFormatProcessCredentialOptions
+ ): Promise {
+ const credentialRequestMetadata = credentialRecord.metadata.get(
+ AnonCredsCredentialRequestMetadataKey
+ )
+
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+ const anonCredsHolderService =
+ agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ if (!credentialRequestMetadata) {
+ throw new AriesFrameworkError(
+ `Missing required request metadata for credential exchange with thread id with id ${credentialRecord.id}`
+ )
+ }
+
+ if (!credentialRecord.credentialAttributes) {
+ throw new AriesFrameworkError(
+ 'Missing credential attributes on credential record. Unable to check credential attributes'
+ )
+ }
+
+ const anonCredsCredential = attachment.getDataAsJson()
+
+ const credentialDefinitionResult = await registryService
+ .getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id)
+ .getCredentialDefinition(agentContext, anonCredsCredential.cred_def_id)
+ if (!credentialDefinitionResult.credentialDefinition) {
+ throw new AriesFrameworkError(
+ `Unable to resolve credential definition ${anonCredsCredential.cred_def_id}: ${credentialDefinitionResult.resolutionMetadata.error} ${credentialDefinitionResult.resolutionMetadata.message}`
+ )
+ }
+
+ const schemaResult = await registryService
+ .getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id)
+ .getSchema(agentContext, anonCredsCredential.schema_id)
+ if (!schemaResult.schema) {
+ throw new AriesFrameworkError(
+ `Unable to resolve schema ${anonCredsCredential.schema_id}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}`
+ )
+ }
+
+ // Resolve revocation registry if credential is revocable
+ let revocationRegistryResult: null | GetRevocationRegistryDefinitionReturn = null
+ if (anonCredsCredential.rev_reg_id) {
+ revocationRegistryResult = await registryService
+ .getRegistryForIdentifier(agentContext, anonCredsCredential.rev_reg_id)
+ .getRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id)
+
+ if (!revocationRegistryResult.revocationRegistryDefinition) {
+ throw new AriesFrameworkError(
+ `Unable to resolve revocation registry definition ${anonCredsCredential.rev_reg_id}: ${revocationRegistryResult.resolutionMetadata.error} ${revocationRegistryResult.resolutionMetadata.message}`
+ )
+ }
+ }
+
+ // assert the credential values match the offer values
+ const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes)
+ assertCredentialValuesMatch(anonCredsCredential.values, recordCredentialValues)
+
+ const credentialId = await anonCredsHolderService.storeCredential(agentContext, {
+ credentialId: utils.uuid(),
+ credentialRequestMetadata,
+ credential: anonCredsCredential,
+ credentialDefinitionId: credentialDefinitionResult.credentialDefinitionId,
+ credentialDefinition: credentialDefinitionResult.credentialDefinition,
+ schema: schemaResult.schema,
+ revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition
+ ? {
+ definition: revocationRegistryResult.revocationRegistryDefinition,
+ id: revocationRegistryResult.revocationRegistryDefinitionId,
+ }
+ : undefined,
+ })
+
+ // If the credential is revocable, store the revocation identifiers in the credential record
+ if (anonCredsCredential.rev_reg_id) {
+ const credential = await anonCredsHolderService.getCredential(agentContext, { credentialId })
+
+ credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, {
+ credentialRevocationId: credential.credentialRevocationId,
+ revocationRegistryId: credential.revocationRegistryId,
+ })
+ credentialRecord.setTags({
+ anonCredsRevocationRegistryId: credential.revocationRegistryId,
+ anonCredsCredentialRevocationId: credential.credentialRevocationId,
+ })
+ }
+
+ credentialRecord.credentials.push({
+ credentialRecordType: this.credentialRecordType,
+ credentialRecordId: credentialId,
+ })
+ }
+
+ public supportsFormat(format: string): boolean {
+ const supportedFormats = [
+ ANONCREDS_CREDENTIAL_REQUEST,
+ ANONCREDS_CREDENTIAL_OFFER,
+ ANONCREDS_CREDENTIAL_FILTER,
+ ANONCREDS_CREDENTIAL,
+ ]
+
+ return supportedFormats.includes(format)
+ }
+
+ /**
+ * Gets the attachment object for a given attachmentId. We need to get out the correct attachmentId for
+ * anoncreds and then find the corresponding attachment (if there is one)
+ * @param formats the formats object containing the attachmentId
+ * @param messageAttachments the attachments containing the payload
+ * @returns The Attachment if found or undefined
+ *
+ */
+ public getAttachment(formats: CredentialFormatSpec[], messageAttachments: V1Attachment[]): V1Attachment | undefined {
+ const supportedAttachmentIds = formats.filter((f) => this.supportsFormat(f.format)).map((f) => f.attachmentId)
+ const supportedAttachment = messageAttachments.find((attachment) => supportedAttachmentIds.includes(attachment.id))
+
+ return supportedAttachment
+ }
+
+ public async deleteCredentialById(agentContext: AgentContext, credentialRecordId: string): Promise {
+ const anonCredsHolderService =
+ agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ await anonCredsHolderService.deleteCredential(agentContext, credentialRecordId)
+ }
+
+ public async shouldAutoRespondToProposal(
+ agentContext: AgentContext,
+ { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondProposalOptions
+ ) {
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const offerJson = offerAttachment.getDataAsJson()
+
+ // We want to make sure the credential definition matches.
+ // TODO: If no credential definition is present on the proposal, we could check whether the other fields
+ // of the proposal match with the credential definition id.
+ return proposalJson.cred_def_id === offerJson.cred_def_id
+ }
+
+ public async shouldAutoRespondToOffer(
+ agentContext: AgentContext,
+ { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondOfferOptions
+ ) {
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const offerJson = offerAttachment.getDataAsJson()
+
+ // We want to make sure the credential definition matches.
+ // TODO: If no credential definition is present on the proposal, we could check whether the other fields
+ // of the proposal match with the credential definition id.
+ return proposalJson.cred_def_id === offerJson.cred_def_id
+ }
+
+ public async shouldAutoRespondToRequest(
+ agentContext: AgentContext,
+ { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions
+ ) {
+ const credentialOfferJson = offerAttachment.getDataAsJson()
+ const credentialRequestJson = requestAttachment.getDataAsJson()
+
+ return credentialOfferJson.cred_def_id === credentialRequestJson.cred_def_id
+ }
+
+ public async shouldAutoRespondToCredential(
+ agentContext: AgentContext,
+ { credentialRecord, requestAttachment, credentialAttachment }: CredentialFormatAutoRespondCredentialOptions
+ ) {
+ const credentialJson = credentialAttachment.getDataAsJson()
+ const credentialRequestJson = requestAttachment.getDataAsJson()
+
+ // make sure the credential definition matches
+ if (credentialJson.cred_def_id !== credentialRequestJson.cred_def_id) return false
+
+ // If we don't have any attributes stored we can't compare so always return false.
+ if (!credentialRecord.credentialAttributes) return false
+ const attributeValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes)
+
+ // check whether the values match the values in the record
+ return checkCredentialValuesMatch(attributeValues, credentialJson.values)
+ }
+
+ private async createAnonCredsOffer(
+ agentContext: AgentContext,
+ {
+ credentialRecord,
+ attachmentId,
+ credentialDefinitionId,
+ attributes,
+ linkedAttachments,
+ }: {
+ credentialDefinitionId: string
+ credentialRecord: CredentialExchangeRecord
+ attachmentId?: string
+ attributes: CredentialPreviewAttributeOptions[]
+ linkedAttachments?: LinkedAttachment[]
+ }
+ ): Promise {
+ const anonCredsIssuerService =
+ agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol)
+
+ // if the proposal has an attachment Id use that, otherwise the generated id of the formats object
+ const format = new CredentialFormatSpec({
+ attachmentId: attachmentId,
+ format: ANONCREDS_CREDENTIAL,
+ })
+
+ const offer = await anonCredsIssuerService.createCredentialOffer(agentContext, {
+ credentialDefinitionId,
+ })
+
+ const { previewAttributes } = this.getCredentialLinkedAttachments(attributes, linkedAttachments)
+ if (!previewAttributes) {
+ throw new AriesFrameworkError('Missing required preview attributes for anoncreds offer')
+ }
+
+ await this.assertPreviewAttributesMatchSchemaAttributes(agentContext, offer, previewAttributes)
+
+ credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, {
+ schemaId: offer.schema_id,
+ credentialDefinitionId: offer.cred_def_id,
+ })
+
+ const attachment = this.getFormatData(offer, format.attachmentId)
+
+ return { format, attachment, previewAttributes }
+ }
+
+ private async assertPreviewAttributesMatchSchemaAttributes(
+ agentContext: AgentContext,
+ offer: AnonCredsCredentialOffer,
+ attributes: CredentialPreviewAttributeOptions[]
+ ): Promise {
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+ const registry = registryService.getRegistryForIdentifier(agentContext, offer.schema_id)
+
+ const schemaResult = await registry.getSchema(agentContext, offer.schema_id)
+
+ if (!schemaResult.schema) {
+ throw new AriesFrameworkError(
+ `Unable to resolve schema ${offer.schema_id} from registry: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}`
+ )
+ }
+
+ assertAttributesMatch(schemaResult.schema, attributes)
+ }
+
+ /**
+ * Get linked attachments for anoncreds format from a proposal message. This allows attachments
+ * to be copied across to old style credential records
+ *
+ * @param options ProposeCredentialOptions object containing (optionally) the linked attachments
+ * @return array of linked attachments or undefined if none present
+ */
+ private getCredentialLinkedAttachments(
+ attributes?: CredentialPreviewAttributeOptions[],
+ linkedAttachments?: LinkedAttachment[]
+ ): {
+ attachments?: V1Attachment[]
+ previewAttributes?: CredentialPreviewAttributeOptions[]
+ } {
+ if (!linkedAttachments && !attributes) {
+ return {}
+ }
+
+ let previewAttributes = attributes ?? []
+ let attachments: V1Attachment[] | undefined
+
+ if (linkedAttachments) {
+ // there are linked attachments so transform into the attribute field of the CredentialPreview object for
+ // this proposal
+ previewAttributes = createAndLinkAttachmentsToPreview(linkedAttachments, previewAttributes)
+ attachments = linkedAttachments.map((linkedAttachment) => linkedAttachment.attachment)
+ }
+
+ return { attachments, previewAttributes }
+ }
+
+ /**
+ * Returns an object of type {@link Attachment} for use in credential exchange messages.
+ * It looks up the correct format identifier and encodes the data as a base64 attachment.
+ *
+ * @param data The data to include in the attach object
+ * @param id the attach id from the formats component of the message
+ */
+ public getFormatData(data: unknown, id: string): V1Attachment {
+ const attachment = new V1Attachment({
+ id,
+ mimeType: 'application/json',
+ data: new V1AttachmentData({
+ base64: JsonEncoder.toBase64(data),
+ }),
+ })
+
+ return attachment
+ }
+}
diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormat.ts b/packages/anoncreds/src/formats/AnonCredsProofFormat.ts
new file mode 100644
index 0000000000..0c326943f8
--- /dev/null
+++ b/packages/anoncreds/src/formats/AnonCredsProofFormat.ts
@@ -0,0 +1,89 @@
+import type {
+ AnonCredsNonRevokedInterval,
+ AnonCredsPredicateType,
+ AnonCredsProof,
+ AnonCredsProofRequest,
+ AnonCredsRequestedAttribute,
+ AnonCredsRequestedAttributeMatch,
+ AnonCredsRequestedPredicate,
+ AnonCredsRequestedPredicateMatch,
+ AnonCredsSelectedCredentials,
+} from '../models'
+import type { ProofFormat } from '@aries-framework/core'
+
+export interface AnonCredsPresentationPreviewAttribute {
+ name: string
+ credentialDefinitionId?: string
+ mimeType?: string
+ value?: string
+ referent?: string
+}
+
+export interface AnonCredsPresentationPreviewPredicate {
+ name: string
+ credentialDefinitionId: string
+ predicate: AnonCredsPredicateType
+ threshold: number
+}
+
+/**
+ * Interface for creating an anoncreds proof proposal.
+ */
+export interface AnonCredsProposeProofFormat {
+ name?: string
+ version?: string
+ attributes?: AnonCredsPresentationPreviewAttribute[]
+ predicates?: AnonCredsPresentationPreviewPredicate[]
+}
+
+/**
+ * Interface for creating an anoncreds proof request.
+ */
+export interface AnonCredsRequestProofFormat {
+ name: string
+ version: string
+ non_revoked?: AnonCredsNonRevokedInterval
+ requested_attributes?: Record
+ requested_predicates?: Record
+}
+
+/**
+ * Interface for getting credentials for an indy proof request.
+ */
+export interface AnonCredsCredentialsForProofRequest {
+ attributes: Record
+ predicates: Record
+}
+
+export interface AnonCredsGetCredentialsForProofRequestOptions {
+ filterByNonRevocationRequirements?: boolean
+}
+
+export interface AnonCredsProofFormat extends ProofFormat {
+ formatKey: 'anoncreds'
+
+ proofFormats: {
+ createProposal: AnonCredsProposeProofFormat
+ acceptProposal: {
+ name?: string
+ version?: string
+ }
+ createRequest: AnonCredsRequestProofFormat
+ acceptRequest: AnonCredsSelectedCredentials
+
+ getCredentialsForRequest: {
+ input: AnonCredsGetCredentialsForProofRequestOptions
+ output: AnonCredsCredentialsForProofRequest
+ }
+ selectCredentialsForRequest: {
+ input: AnonCredsGetCredentialsForProofRequestOptions
+ output: AnonCredsSelectedCredentials
+ }
+ }
+
+ formatData: {
+ proposal: AnonCredsProofRequest
+ request: AnonCredsProofRequest
+ presentation: AnonCredsProof
+ }
+}
diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts
new file mode 100644
index 0000000000..138876195c
--- /dev/null
+++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts
@@ -0,0 +1,627 @@
+import type {
+ AnonCredsProofFormat,
+ AnonCredsCredentialsForProofRequest,
+ AnonCredsGetCredentialsForProofRequestOptions,
+} from './AnonCredsProofFormat'
+import type {
+ AnonCredsCredentialDefinition,
+ AnonCredsCredentialInfo,
+ AnonCredsProof,
+ AnonCredsRequestedAttribute,
+ AnonCredsRequestedPredicate,
+ AnonCredsSchema,
+ AnonCredsSelectedCredentials,
+ AnonCredsProofRequest,
+} from '../models'
+import type { AnonCredsHolderService, AnonCredsVerifierService, GetCredentialsForProofRequestReturn } from '../services'
+import type {
+ ProofFormatService,
+ AgentContext,
+ ProofFormatCreateReturn,
+ FormatCreateRequestOptions,
+ ProofFormatCreateProposalOptions,
+ ProofFormatProcessOptions,
+ ProofFormatAcceptProposalOptions,
+ ProofFormatAcceptRequestOptions,
+ ProofFormatProcessPresentationOptions,
+ ProofFormatGetCredentialsForRequestOptions,
+ ProofFormatGetCredentialsForRequestReturn,
+ ProofFormatSelectCredentialsForRequestOptions,
+ ProofFormatSelectCredentialsForRequestReturn,
+ ProofFormatAutoRespondProposalOptions,
+ ProofFormatAutoRespondRequestOptions,
+ ProofFormatAutoRespondPresentationOptions,
+} from '@aries-framework/core'
+
+import {
+ AriesFrameworkError,
+ V1Attachment,
+ V1AttachmentData,
+ JsonEncoder,
+ ProofFormatSpec,
+ JsonTransformer,
+} from '@aries-framework/core'
+
+import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest'
+import { AnonCredsVerifierServiceSymbol, AnonCredsHolderServiceSymbol } from '../services'
+import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService'
+import {
+ sortRequestedCredentialsMatches,
+ createRequestFromPreview,
+ areAnonCredsProofRequestsEqual,
+ assertBestPracticeRevocationInterval,
+ checkValidCredentialValueEncoding,
+ encodeCredentialValue,
+ assertNoDuplicateGroupsNamesInProofRequest,
+ getRevocationRegistriesForRequest,
+ getRevocationRegistriesForProof,
+} from '../utils'
+
+const ANONCREDS_PRESENTATION_PROPOSAL = 'anoncreds/proof-request@v1.0'
+const ANONCREDS_PRESENTATION_REQUEST = 'anoncreds/proof-request@v1.0'
+const ANONCREDS_PRESENTATION = 'anoncreds/proof@v1.0'
+
+export class AnonCredsProofFormatService implements ProofFormatService {
+ public readonly formatKey = 'anoncreds' as const
+
+ public async createProposal(
+ agentContext: AgentContext,
+ { attachmentId, proofFormats }: ProofFormatCreateProposalOptions
+ ): Promise {
+ const format = new ProofFormatSpec({
+ format: ANONCREDS_PRESENTATION_PROPOSAL,
+ attachmentId,
+ })
+
+ const anoncredsFormat = proofFormats.anoncreds
+ if (!anoncredsFormat) {
+ throw Error('Missing anoncreds format to create proposal attachment format')
+ }
+
+ const proofRequest = createRequestFromPreview({
+ attributes: anoncredsFormat.attributes ?? [],
+ predicates: anoncredsFormat.predicates ?? [],
+ name: anoncredsFormat.name ?? 'Proof request',
+ version: anoncredsFormat.version ?? '1.0',
+ nonce: await agentContext.wallet.generateNonce(),
+ })
+ const attachment = this.getFormatData(proofRequest, format.attachmentId)
+
+ return { attachment, format }
+ }
+
+ public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise {
+ const proposalJson = attachment.getDataAsJson()
+
+ // fromJson also validates
+ JsonTransformer.fromJSON(proposalJson, AnonCredsProofRequestClass)
+
+ // Assert attribute and predicate (group) names do not match
+ assertNoDuplicateGroupsNamesInProofRequest(proposalJson)
+ }
+
+ public async acceptProposal(
+ agentContext: AgentContext,
+ { proposalAttachment, attachmentId }: ProofFormatAcceptProposalOptions
+ ): Promise {
+ const format = new ProofFormatSpec({
+ format: ANONCREDS_PRESENTATION_REQUEST,
+ attachmentId,
+ })
+
+ const proposalJson = proposalAttachment.getDataAsJson()
+
+ const request = {
+ ...proposalJson,
+ // We never want to reuse the nonce from the proposal, as this will allow replay attacks
+ nonce: await agentContext.wallet.generateNonce(),
+ }
+
+ const attachment = this.getFormatData(request, format.attachmentId)
+
+ return { attachment, format }
+ }
+
+ public async createRequest(
+ agentContext: AgentContext,
+ { attachmentId, proofFormats }: FormatCreateRequestOptions
+ ): Promise {
+ const format = new ProofFormatSpec({
+ format: ANONCREDS_PRESENTATION_REQUEST,
+ attachmentId,
+ })
+
+ const anoncredsFormat = proofFormats.anoncreds
+ if (!anoncredsFormat) {
+ throw Error('Missing anoncreds format in create request attachment format')
+ }
+
+ const request = {
+ name: anoncredsFormat.name,
+ version: anoncredsFormat.version,
+ nonce: await agentContext.wallet.generateNonce(),
+ requested_attributes: anoncredsFormat.requested_attributes ?? {},
+ requested_predicates: anoncredsFormat.requested_predicates ?? {},
+ non_revoked: anoncredsFormat.non_revoked,
+ }
+
+ // Assert attribute and predicate (group) names do not match
+ assertNoDuplicateGroupsNamesInProofRequest(request)
+
+ const attachment = this.getFormatData(request, format.attachmentId)
+
+ return { attachment, format }
+ }
+
+ public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise {
+ const requestJson = attachment.getDataAsJson()
+
+ // fromJson also validates
+ JsonTransformer.fromJSON(requestJson, AnonCredsProofRequestClass)
+
+ // Assert attribute and predicate (group) names do not match
+ assertNoDuplicateGroupsNamesInProofRequest(requestJson)
+ }
+
+ public async acceptRequest(
+ agentContext: AgentContext,
+ { proofFormats, requestAttachment, attachmentId }: ProofFormatAcceptRequestOptions
+ ): Promise {
+ const format = new ProofFormatSpec({
+ format: ANONCREDS_PRESENTATION,
+ attachmentId,
+ })
+ const requestJson = requestAttachment.getDataAsJson()
+
+ const anoncredsFormat = proofFormats?.anoncreds
+
+ const selectedCredentials =
+ anoncredsFormat ??
+ (await this._selectCredentialsForRequest(agentContext, requestJson, {
+ filterByNonRevocationRequirements: true,
+ }))
+
+ const proof = await this.createProof(agentContext, requestJson, selectedCredentials)
+ const attachment = this.getFormatData(proof, format.attachmentId)
+
+ return {
+ attachment,
+ format,
+ }
+ }
+
+ public async processPresentation(
+ agentContext: AgentContext,
+ { requestAttachment, attachment }: ProofFormatProcessPresentationOptions
+ ): Promise {
+ const verifierService =
+ agentContext.dependencyManager.resolve(AnonCredsVerifierServiceSymbol)
+
+ const proofRequestJson = requestAttachment.getDataAsJson()
+
+ // NOTE: we don't do validation here, as this is handled by the AnonCreds implementation, however
+ // this can lead to confusing error messages. We should consider doing validation here as well.
+ // Defining a class-transformer/class-validator class seems a bit overkill, and the usage of interfaces
+ // for the anoncreds package keeps things simple. Maybe we can try to use something like zod to validate
+ const proofJson = attachment.getDataAsJson()
+
+ for (const [referent, attribute] of Object.entries(proofJson.requested_proof.revealed_attrs)) {
+ if (!checkValidCredentialValueEncoding(attribute.raw, attribute.encoded)) {
+ throw new AriesFrameworkError(
+ `The encoded value for '${referent}' is invalid. ` +
+ `Expected '${encodeCredentialValue(attribute.raw)}'. ` +
+ `Actual '${attribute.encoded}'`
+ )
+ }
+ }
+
+ for (const [, attributeGroup] of Object.entries(proofJson.requested_proof.revealed_attr_groups ?? {})) {
+ for (const [attributeName, attribute] of Object.entries(attributeGroup.values)) {
+ if (!checkValidCredentialValueEncoding(attribute.raw, attribute.encoded)) {
+ throw new AriesFrameworkError(
+ `The encoded value for '${attributeName}' is invalid. ` +
+ `Expected '${encodeCredentialValue(attribute.raw)}'. ` +
+ `Actual '${attribute.encoded}'`
+ )
+ }
+ }
+ }
+
+ const schemas = await this.getSchemas(agentContext, new Set(proofJson.identifiers.map((i) => i.schema_id)))
+ const credentialDefinitions = await this.getCredentialDefinitions(
+ agentContext,
+ new Set(proofJson.identifiers.map((i) => i.cred_def_id))
+ )
+
+ const revocationRegistries = await getRevocationRegistriesForProof(agentContext, proofJson)
+
+ return await verifierService.verifyProof(agentContext, {
+ proofRequest: proofRequestJson,
+ proof: proofJson,
+ schemas,
+ credentialDefinitions,
+ revocationRegistries,
+ })
+ }
+
+ public async getCredentialsForRequest(
+ agentContext: AgentContext,
+ { requestAttachment, proofFormats }: ProofFormatGetCredentialsForRequestOptions
+ ): Promise> {
+ const proofRequestJson = requestAttachment.getDataAsJson()
+
+ // Set default values
+ const { filterByNonRevocationRequirements = true } = proofFormats?.anoncreds ?? {}
+
+ const credentialsForRequest = await this._getCredentialsForRequest(agentContext, proofRequestJson, {
+ filterByNonRevocationRequirements,
+ })
+
+ return credentialsForRequest
+ }
+
+ public async selectCredentialsForRequest(
+ agentContext: AgentContext,
+ { requestAttachment, proofFormats }: ProofFormatSelectCredentialsForRequestOptions
+ ): Promise> {
+ const proofRequestJson = requestAttachment.getDataAsJson()
+
+ // Set default values
+ const { filterByNonRevocationRequirements = true } = proofFormats?.anoncreds ?? {}
+
+ const selectedCredentials = this._selectCredentialsForRequest(agentContext, proofRequestJson, {
+ filterByNonRevocationRequirements,
+ })
+
+ return selectedCredentials
+ }
+
+ public async shouldAutoRespondToProposal(
+ agentContext: AgentContext,
+ { proposalAttachment, requestAttachment }: ProofFormatAutoRespondProposalOptions
+ ): Promise {
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const requestJson = requestAttachment.getDataAsJson()
+
+ const areRequestsEqual = areAnonCredsProofRequestsEqual(proposalJson, requestJson)
+ agentContext.config.logger.debug(`AnonCreds request and proposal are are equal: ${areRequestsEqual}`, {
+ proposalJson,
+ requestJson,
+ })
+
+ return areRequestsEqual
+ }
+
+ public async shouldAutoRespondToRequest(
+ agentContext: AgentContext,
+ { proposalAttachment, requestAttachment }: ProofFormatAutoRespondRequestOptions
+ ): Promise {
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const requestJson = requestAttachment.getDataAsJson()
+
+ return areAnonCredsProofRequestsEqual(proposalJson, requestJson)
+ }
+
+ public async shouldAutoRespondToPresentation(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _agentContext: AgentContext,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _options: ProofFormatAutoRespondPresentationOptions
+ ): Promise {
+ // The presentation is already verified in processPresentation, so we can just return true here.
+ // It's only an ack, so it's just that we received the presentation.
+ return true
+ }
+
+ public supportsFormat(formatIdentifier: string): boolean {
+ const supportedFormats = [ANONCREDS_PRESENTATION_PROPOSAL, ANONCREDS_PRESENTATION_REQUEST, ANONCREDS_PRESENTATION]
+ return supportedFormats.includes(formatIdentifier)
+ }
+
+ private async _getCredentialsForRequest(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ options: AnonCredsGetCredentialsForProofRequestOptions
+ ): Promise {
+ const credentialsForProofRequest: AnonCredsCredentialsForProofRequest = {
+ attributes: {},
+ predicates: {},
+ }
+
+ for (const [referent, requestedAttribute] of Object.entries(proofRequest.requested_attributes)) {
+ const credentials = await this.getCredentialsForProofRequestReferent(agentContext, proofRequest, referent)
+
+ credentialsForProofRequest.attributes[referent] = sortRequestedCredentialsMatches(
+ await Promise.all(
+ credentials.map(async (credential) => {
+ const { isRevoked, timestamp } = await this.getRevocationStatus(
+ agentContext,
+ proofRequest,
+ requestedAttribute,
+ credential.credentialInfo
+ )
+
+ return {
+ credentialId: credential.credentialInfo.credentialId,
+ revealed: true,
+ credentialInfo: credential.credentialInfo,
+ timestamp,
+ revoked: isRevoked,
+ }
+ })
+ )
+ )
+
+ // We only attach revoked state if non-revocation is requested. So if revoked is true it means
+ // the credential is not applicable to the proof request
+ if (options.filterByNonRevocationRequirements) {
+ credentialsForProofRequest.attributes[referent] = credentialsForProofRequest.attributes[referent].filter(
+ (r) => !r.revoked
+ )
+ }
+ }
+
+ for (const [referent, requestedPredicate] of Object.entries(proofRequest.requested_predicates)) {
+ const credentials = await this.getCredentialsForProofRequestReferent(agentContext, proofRequest, referent)
+
+ credentialsForProofRequest.predicates[referent] = sortRequestedCredentialsMatches(
+ await Promise.all(
+ credentials.map(async (credential) => {
+ const { isRevoked, timestamp } = await this.getRevocationStatus(
+ agentContext,
+ proofRequest,
+ requestedPredicate,
+ credential.credentialInfo
+ )
+
+ return {
+ credentialId: credential.credentialInfo.credentialId,
+ credentialInfo: credential.credentialInfo,
+ timestamp,
+ revoked: isRevoked,
+ }
+ })
+ )
+ )
+
+ // We only attach revoked state if non-revocation is requested. So if revoked is true it means
+ // the credential is not applicable to the proof request
+ if (options.filterByNonRevocationRequirements) {
+ credentialsForProofRequest.predicates[referent] = credentialsForProofRequest.predicates[referent].filter(
+ (r) => !r.revoked
+ )
+ }
+ }
+
+ return credentialsForProofRequest
+ }
+
+ private async _selectCredentialsForRequest(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ options: AnonCredsGetCredentialsForProofRequestOptions
+ ): Promise {
+ const credentialsForRequest = await this._getCredentialsForRequest(agentContext, proofRequest, options)
+
+ const selectedCredentials: AnonCredsSelectedCredentials = {
+ attributes: {},
+ predicates: {},
+ selfAttestedAttributes: {},
+ }
+
+ Object.keys(credentialsForRequest.attributes).forEach((attributeName) => {
+ const attributeArray = credentialsForRequest.attributes[attributeName]
+
+ if (attributeArray.length === 0) {
+ throw new AriesFrameworkError('Unable to automatically select requested attributes.')
+ }
+
+ selectedCredentials.attributes[attributeName] = attributeArray[0]
+ })
+
+ Object.keys(credentialsForRequest.predicates).forEach((attributeName) => {
+ if (credentialsForRequest.predicates[attributeName].length === 0) {
+ throw new AriesFrameworkError('Unable to automatically select requested predicates.')
+ } else {
+ selectedCredentials.predicates[attributeName] = credentialsForRequest.predicates[attributeName][0]
+ }
+ })
+
+ return selectedCredentials
+ }
+
+ private async getCredentialsForProofRequestReferent(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ attributeReferent: string
+ ): Promise {
+ const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ const credentials = await holderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent,
+ })
+
+ return credentials
+ }
+
+ /**
+ * Build schemas object needed to create and verify proof objects.
+ *
+ * Creates object with `{ schemaId: AnonCredsSchema }` mapping
+ *
+ * @param schemaIds List of schema ids
+ * @returns Object containing schemas for specified schema ids
+ *
+ */
+ private async getSchemas(agentContext: AgentContext, schemaIds: Set) {
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+
+ const schemas: { [key: string]: AnonCredsSchema } = {}
+
+ for (const schemaId of schemaIds) {
+ const schemaRegistry = registryService.getRegistryForIdentifier(agentContext, schemaId)
+ const schemaResult = await schemaRegistry.getSchema(agentContext, schemaId)
+
+ if (!schemaResult.schema) {
+ throw new AriesFrameworkError(`Schema not found for id ${schemaId}: ${schemaResult.resolutionMetadata.message}`)
+ }
+
+ schemas[schemaId] = schemaResult.schema
+ }
+
+ return schemas
+ }
+
+ /**
+ * Build credential definitions object needed to create and verify proof objects.
+ *
+ * Creates object with `{ credentialDefinitionId: AnonCredsCredentialDefinition }` mapping
+ *
+ * @param credentialDefinitionIds List of credential definition ids
+ * @returns Object containing credential definitions for specified credential definition ids
+ *
+ */
+ private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) {
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+
+ const credentialDefinitions: { [key: string]: AnonCredsCredentialDefinition } = {}
+
+ for (const credentialDefinitionId of credentialDefinitionIds) {
+ const credentialDefinitionRegistry = registryService.getRegistryForIdentifier(
+ agentContext,
+ credentialDefinitionId
+ )
+
+ const credentialDefinitionResult = await credentialDefinitionRegistry.getCredentialDefinition(
+ agentContext,
+ credentialDefinitionId
+ )
+
+ if (!credentialDefinitionResult.credentialDefinition) {
+ throw new AriesFrameworkError(
+ `Credential definition not found for id ${credentialDefinitionId}: ${credentialDefinitionResult.resolutionMetadata.message}`
+ )
+ }
+
+ credentialDefinitions[credentialDefinitionId] = credentialDefinitionResult.credentialDefinition
+ }
+
+ return credentialDefinitions
+ }
+
+ private async getRevocationStatus(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ requestedItem: AnonCredsRequestedAttribute | AnonCredsRequestedPredicate,
+ credentialInfo: AnonCredsCredentialInfo
+ ) {
+ const requestNonRevoked = requestedItem.non_revoked ?? proofRequest.non_revoked
+ const credentialRevocationId = credentialInfo.credentialRevocationId
+ const revocationRegistryId = credentialInfo.revocationRegistryId
+
+ // If revocation interval is not present or the credential is not revocable then we
+ // don't need to fetch the revocation status
+ if (!requestNonRevoked || !credentialRevocationId || !revocationRegistryId) {
+ return { isRevoked: undefined, timestamp: undefined }
+ }
+
+ agentContext.config.logger.trace(
+ `Fetching credential revocation status for credential revocation id '${credentialRevocationId}' with revocation interval with from '${requestNonRevoked.from}' and to '${requestNonRevoked.to}'`
+ )
+
+ // Make sure the revocation interval follows best practices from Aries RFC 0441
+ assertBestPracticeRevocationInterval(requestNonRevoked)
+
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+ const registry = registryService.getRegistryForIdentifier(agentContext, revocationRegistryId)
+
+ const revocationStatusResult = await registry.getRevocationStatusList(
+ agentContext,
+ revocationRegistryId,
+ requestNonRevoked.to ?? Date.now()
+ )
+
+ if (!revocationStatusResult.revocationStatusList) {
+ throw new AriesFrameworkError(
+ `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${revocationStatusResult.resolutionMetadata.message}`
+ )
+ }
+
+ // Item is revoked when the value at the index is 1
+ const isRevoked = revocationStatusResult.revocationStatusList.revocationList[parseInt(credentialRevocationId)] === 1
+
+ agentContext.config.logger.trace(
+ `Credential with credential revocation index '${credentialRevocationId}' is ${
+ isRevoked ? '' : 'not '
+ }revoked with revocation interval with to '${requestNonRevoked.to}' & from '${requestNonRevoked.from}'`
+ )
+
+ return {
+ isRevoked,
+ timestamp: revocationStatusResult.revocationStatusList.timestamp,
+ }
+ }
+
+ /**
+ * Create anoncreds proof from a given proof request and requested credential object.
+ *
+ * @param proofRequest The proof request to create the proof for
+ * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof
+ * @returns anoncreds proof object
+ */
+ private async createProof(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ selectedCredentials: AnonCredsSelectedCredentials
+ ): Promise {
+ const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ const credentialObjects = await Promise.all(
+ [...Object.values(selectedCredentials.attributes), ...Object.values(selectedCredentials.predicates)].map(
+ async (c) => c.credentialInfo ?? holderService.getCredential(agentContext, { credentialId: c.credentialId })
+ )
+ )
+
+ const schemas = await this.getSchemas(agentContext, new Set(credentialObjects.map((c) => c.schemaId)))
+ const credentialDefinitions = await this.getCredentialDefinitions(
+ agentContext,
+ new Set(credentialObjects.map((c) => c.credentialDefinitionId))
+ )
+
+ // selectedCredentials are overridden with specified timestamps of the revocation status list that
+ // should be used for the selected credentials.
+ const { revocationRegistries, updatedSelectedCredentials } = await getRevocationRegistriesForRequest(
+ agentContext,
+ proofRequest,
+ selectedCredentials
+ )
+
+ return await holderService.createProof(agentContext, {
+ proofRequest,
+ selectedCredentials: updatedSelectedCredentials,
+ schemas,
+ credentialDefinitions,
+ revocationRegistries,
+ })
+ }
+
+ /**
+ * Returns an object of type {@link Attachment} for use in credential exchange messages.
+ * It looks up the correct format identifier and encodes the data as a base64 attachment.
+ *
+ * @param data The data to include in the attach object
+ * @param id the attach id from the formats component of the message
+ */
+ private getFormatData(data: unknown, id: string): V1Attachment {
+ const attachment = new V1Attachment({
+ id,
+ mimeType: 'application/json',
+ data: new V1AttachmentData({
+ base64: JsonEncoder.toBase64(data),
+ }),
+ })
+
+ return attachment
+ }
+}
diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts
new file mode 100644
index 0000000000..f4a6f2a0d2
--- /dev/null
+++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts
@@ -0,0 +1,56 @@
+import type {
+ AnonCredsAcceptOfferFormat,
+ AnonCredsAcceptProposalFormat,
+ AnonCredsAcceptRequestFormat,
+ AnonCredsCredentialProposalFormat,
+ AnonCredsOfferCredentialFormat,
+ AnonCredsProposeCredentialFormat,
+} from './AnonCredsCredentialFormat'
+import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models'
+import type { CredentialFormat } from '@aries-framework/core'
+
+// Legacy indy credential proposal doesn't support _id properties
+export type LegacyIndyCredentialProposalFormat = Omit<
+ AnonCredsCredentialProposalFormat,
+ 'schema_issuer_id' | 'issuer_id'
+>
+
+/**
+ * This defines the module payload for calling CredentialsApi.createProposal
+ * or CredentialsApi.negotiateOffer
+ *
+ * NOTE: This doesn't include the `issuerId` and `schemaIssuerId` properties that are present in the newer format.
+ */
+export type LegacyIndyProposeCredentialFormat = Omit
+
+export interface LegacyIndyCredentialRequest extends AnonCredsCredentialRequest {
+ // prover_did is optional in AnonCreds credential request, but required in legacy format
+ prover_did: string
+}
+
+export interface LegacyIndyCredentialFormat extends CredentialFormat {
+ formatKey: 'indy'
+
+ // The stored type is the same as the anoncreds credential service
+ credentialRecordType: 'anoncreds'
+
+ // credential formats are the same as the AnonCreds credential format
+ credentialFormats: {
+ // The createProposal interface is different between the interfaces
+ createProposal: LegacyIndyProposeCredentialFormat
+ acceptProposal: AnonCredsAcceptProposalFormat
+ createOffer: AnonCredsOfferCredentialFormat
+ acceptOffer: AnonCredsAcceptOfferFormat
+ createRequest: never // cannot start from createRequest
+ acceptRequest: AnonCredsAcceptRequestFormat
+ }
+
+ // Format data is based on RFC 0592
+ // https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments
+ formatData: {
+ proposal: LegacyIndyCredentialProposalFormat
+ offer: AnonCredsCredentialOffer
+ request: LegacyIndyCredentialRequest
+ credential: AnonCredsCredential
+ }
+}
diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts
new file mode 100644
index 0000000000..9bc9e2a000
--- /dev/null
+++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts
@@ -0,0 +1,649 @@
+import type { LegacyIndyCredentialFormat, LegacyIndyCredentialProposalFormat } from './LegacyIndyCredentialFormat'
+import type {
+ AnonCredsCredential,
+ AnonCredsCredentialOffer,
+ AnonCredsCredentialRequest,
+ AnonCredsCredentialRequestMetadata,
+} from '../models'
+import type { AnonCredsIssuerService, AnonCredsHolderService, GetRevocationRegistryDefinitionReturn } from '../services'
+import type { AnonCredsCredentialMetadata } from '../utils/metadata'
+import type {
+ CredentialFormatService,
+ AgentContext,
+ CredentialFormatCreateProposalOptions,
+ CredentialFormatCreateProposalReturn,
+ CredentialFormatProcessOptions,
+ CredentialFormatAcceptProposalOptions,
+ CredentialFormatCreateOfferReturn,
+ CredentialFormatCreateOfferOptions,
+ CredentialFormatAcceptOfferOptions,
+ CredentialFormatCreateReturn,
+ CredentialFormatAcceptRequestOptions,
+ CredentialFormatProcessCredentialOptions,
+ CredentialFormatAutoRespondProposalOptions,
+ CredentialFormatAutoRespondOfferOptions,
+ CredentialFormatAutoRespondRequestOptions,
+ CredentialFormatAutoRespondCredentialOptions,
+ CredentialExchangeRecord,
+ CredentialPreviewAttributeOptions,
+ LinkedAttachment,
+} from '@aries-framework/core'
+
+import {
+ ProblemReportError,
+ MessageValidator,
+ CredentialFormatSpec,
+ AriesFrameworkError,
+ JsonEncoder,
+ utils,
+ CredentialProblemReportReason,
+ JsonTransformer,
+ V1Attachment,
+ V1AttachmentData,
+} from '@aries-framework/core'
+
+import { AnonCredsError } from '../error'
+import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal'
+import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services'
+import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService'
+import {
+ convertAttributesToCredentialValues,
+ assertCredentialValuesMatch,
+ checkCredentialValuesMatch,
+ assertAttributesMatch,
+ createAndLinkAttachmentsToPreview,
+} from '../utils/credential'
+import { isUnqualifiedCredentialDefinitionId, isUnqualifiedSchemaId } from '../utils/indyIdentifiers'
+import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata'
+import { generateLegacyProverDidLikeString } from '../utils/proverDid'
+
+const INDY_CRED_ABSTRACT = 'hlindy/cred-abstract@v2.0'
+const INDY_CRED_REQUEST = 'hlindy/cred-req@v2.0'
+const INDY_CRED_FILTER = 'hlindy/cred-filter@v2.0'
+const INDY_CRED = 'hlindy/cred@v2.0'
+
+export class LegacyIndyCredentialFormatService implements CredentialFormatService {
+ /** formatKey is the key used when calling agent.credentials.xxx with credentialFormats.indy */
+ public readonly formatKey = 'indy' as const
+
+ /**
+ * credentialRecordType is the type of record that stores the credential. It is stored in the credential
+ * record binding in the credential exchange record.
+ */
+ public readonly credentialRecordType = 'anoncreds' as const
+
+ /**
+ * Create a {@link AttachmentFormats} object dependent on the message type.
+ *
+ * @param options The object containing all the options for the proposed credential
+ * @returns object containing associated attachment, format and optionally the credential preview
+ *
+ */
+ public async createProposal(
+ agentContext: AgentContext,
+ { credentialFormats, credentialRecord }: CredentialFormatCreateProposalOptions
+ ): Promise {
+ const format = new CredentialFormatSpec({
+ format: INDY_CRED_FILTER,
+ })
+
+ const indyFormat = credentialFormats.indy
+
+ if (!indyFormat) {
+ throw new AriesFrameworkError('Missing indy payload in createProposal')
+ }
+
+ // We want all properties except for `attributes` and `linkedAttachments` attributes.
+ // The easiest way is to destructure and use the spread operator. But that leaves the other properties unused
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { attributes, linkedAttachments, ...indyCredentialProposal } = indyFormat
+ const proposal = new AnonCredsCredentialProposal(indyCredentialProposal)
+
+ try {
+ MessageValidator.validateSync(proposal)
+ } catch (error) {
+ throw new AriesFrameworkError(`Invalid proposal supplied: ${indyCredentialProposal} in Indy Format Service`)
+ }
+
+ const attachment = this.getFormatData(JsonTransformer.toJSON(proposal), format.attachmentId)
+
+ const { previewAttributes } = this.getCredentialLinkedAttachments(
+ indyFormat.attributes,
+ indyFormat.linkedAttachments
+ )
+
+ // Set the metadata
+ credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, {
+ schemaId: proposal.schemaId,
+ credentialDefinitionId: proposal.credentialDefinitionId,
+ })
+
+ return { format, attachment, previewAttributes }
+ }
+
+ public async processProposal(
+ agentContext: AgentContext,
+ { attachment }: CredentialFormatProcessOptions
+ ): Promise {
+ const proposalJson = attachment.getDataAsJson()
+
+ JsonTransformer.fromJSON(proposalJson, AnonCredsCredentialProposal)
+ }
+
+ public async acceptProposal(
+ agentContext: AgentContext,
+ {
+ attachmentId,
+ credentialFormats,
+ credentialRecord,
+ proposalAttachment,
+ }: CredentialFormatAcceptProposalOptions
+ ): Promise {
+ const indyFormat = credentialFormats?.indy
+
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const credentialDefinitionId = indyFormat?.credentialDefinitionId ?? proposalJson.cred_def_id
+
+ const attributes = indyFormat?.attributes ?? credentialRecord.credentialAttributes
+
+ if (!credentialDefinitionId) {
+ throw new AriesFrameworkError(
+ 'No credential definition id in proposal or provided as input to accept proposal method.'
+ )
+ }
+
+ if (!isUnqualifiedCredentialDefinitionId(credentialDefinitionId)) {
+ throw new AriesFrameworkError(`${credentialDefinitionId} is not a valid legacy indy credential definition id`)
+ }
+
+ if (!attributes) {
+ throw new AriesFrameworkError('No attributes in proposal or provided as input to accept proposal method.')
+ }
+
+ const { format, attachment, previewAttributes } = await this.createIndyOffer(agentContext, {
+ credentialRecord,
+ attachmentId,
+ attributes,
+ credentialDefinitionId,
+ linkedAttachments: indyFormat?.linkedAttachments,
+ })
+
+ return { format, attachment, previewAttributes }
+ }
+
+ /**
+ * Create a credential attachment format for a credential request.
+ *
+ * @param options The object containing all the options for the credential offer
+ * @returns object containing associated attachment, formats and offersAttach elements
+ *
+ */
+ public async createOffer(
+ agentContext: AgentContext,
+ {
+ credentialFormats,
+ credentialRecord,
+ attachmentId,
+ }: CredentialFormatCreateOfferOptions
+ ): Promise {
+ const indyFormat = credentialFormats.indy
+
+ if (!indyFormat) {
+ throw new AriesFrameworkError('Missing indy credentialFormat data')
+ }
+
+ const { format, attachment, previewAttributes } = await this.createIndyOffer(agentContext, {
+ credentialRecord,
+ attachmentId,
+ attributes: indyFormat.attributes,
+ credentialDefinitionId: indyFormat.credentialDefinitionId,
+ linkedAttachments: indyFormat.linkedAttachments,
+ })
+
+ return { format, attachment, previewAttributes }
+ }
+
+ public async processOffer(
+ agentContext: AgentContext,
+ { attachment, credentialRecord }: CredentialFormatProcessOptions
+ ) {
+ agentContext.config.logger.debug(`Processing indy credential offer for credential record ${credentialRecord.id}`)
+
+ const credOffer = attachment.getDataAsJson()
+
+ if (!isUnqualifiedSchemaId(credOffer.schema_id) || !isUnqualifiedCredentialDefinitionId(credOffer.cred_def_id)) {
+ throw new ProblemReportError('Invalid credential offer', {
+ problemCode: CredentialProblemReportReason.IssuanceAbandoned,
+ })
+ }
+ }
+
+ public async acceptOffer(
+ agentContext: AgentContext,
+ {
+ credentialRecord,
+ attachmentId,
+ offerAttachment,
+ credentialFormats,
+ }: CredentialFormatAcceptOfferOptions
+ ): Promise {
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+ const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ const credentialOffer = offerAttachment.getDataAsJson()
+
+ if (!isUnqualifiedCredentialDefinitionId(credentialOffer.cred_def_id)) {
+ throw new AriesFrameworkError(
+ `${credentialOffer.cred_def_id} is not a valid legacy indy credential definition id`
+ )
+ }
+ // Get credential definition
+ const registry = registryService.getRegistryForIdentifier(agentContext, credentialOffer.cred_def_id)
+ const { credentialDefinition, resolutionMetadata } = await registry.getCredentialDefinition(
+ agentContext,
+ credentialOffer.cred_def_id
+ )
+
+ if (!credentialDefinition) {
+ throw new AnonCredsError(
+ `Unable to retrieve credential definition with id ${credentialOffer.cred_def_id}: ${resolutionMetadata.error} ${resolutionMetadata.message}`
+ )
+ }
+
+ const { credentialRequest, credentialRequestMetadata } = await holderService.createCredentialRequest(agentContext, {
+ credentialOffer,
+ credentialDefinition,
+ linkSecretId: credentialFormats?.indy?.linkSecretId,
+ useLegacyProverDid: true,
+ })
+
+ if (!credentialRequest.prover_did) {
+ // We just generate a prover did like string, as it's not used for anything and we don't need
+ // to prove ownership of the did. It's deprecated in AnonCreds v1, but kept for backwards compatibility
+ credentialRequest.prover_did = generateLegacyProverDidLikeString()
+ }
+
+ credentialRecord.metadata.set(
+ AnonCredsCredentialRequestMetadataKey,
+ credentialRequestMetadata
+ )
+ credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, {
+ credentialDefinitionId: credentialOffer.cred_def_id,
+ schemaId: credentialOffer.schema_id,
+ })
+
+ const format = new CredentialFormatSpec({
+ attachmentId,
+ format: INDY_CRED_REQUEST,
+ })
+
+ const attachment = this.getFormatData(credentialRequest, format.attachmentId)
+ return { format, attachment }
+ }
+
+ /**
+ * Starting from a request is not supported for indy credentials, this method only throws an error.
+ */
+ public async createRequest(): Promise {
+ throw new AriesFrameworkError('Starting from a request is not supported for indy credentials')
+ }
+
+ /**
+ * We don't have any models to validate an indy request object, for now this method does nothing
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public async processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise {
+ // not needed for Indy
+ }
+
+ public async acceptRequest(
+ agentContext: AgentContext,
+ {
+ credentialRecord,
+ attachmentId,
+ offerAttachment,
+ requestAttachment,
+ }: CredentialFormatAcceptRequestOptions
+ ): Promise {
+ // Assert credential attributes
+ const credentialAttributes = credentialRecord.credentialAttributes
+ if (!credentialAttributes) {
+ throw new AriesFrameworkError(
+ `Missing required credential attribute values on credential record with id ${credentialRecord.id}`
+ )
+ }
+
+ const anonCredsIssuerService =
+ agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol)
+
+ const credentialOffer = offerAttachment?.getDataAsJson()
+ if (!credentialOffer) throw new AriesFrameworkError('Missing indy credential offer in createCredential')
+
+ const credentialRequest = requestAttachment.getDataAsJson()
+ if (!credentialRequest) throw new AriesFrameworkError('Missing indy credential request in createCredential')
+
+ const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, {
+ credentialOffer,
+ credentialRequest,
+ credentialValues: convertAttributesToCredentialValues(credentialAttributes),
+ })
+
+ if (credential.rev_reg_id) {
+ credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, {
+ credentialRevocationId: credentialRevocationId,
+ revocationRegistryId: credential.rev_reg_id,
+ })
+ credentialRecord.setTags({
+ anonCredsRevocationRegistryId: credential.rev_reg_id,
+ anonCredsCredentialRevocationId: credentialRevocationId,
+ })
+ }
+
+ const format = new CredentialFormatSpec({
+ attachmentId,
+ format: INDY_CRED,
+ })
+
+ const attachment = this.getFormatData(credential, format.attachmentId)
+ return { format, attachment }
+ }
+
+ /**
+ * Processes an incoming credential - retrieve metadata, retrieve payload and store it in the Indy wallet
+ * @param options the issue credential message wrapped inside this object
+ * @param credentialRecord the credential exchange record for this credential
+ */
+ public async processCredential(
+ agentContext: AgentContext,
+ { credentialRecord, attachment }: CredentialFormatProcessCredentialOptions
+ ): Promise {
+ const credentialRequestMetadata = credentialRecord.metadata.get(
+ AnonCredsCredentialRequestMetadataKey
+ )
+
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+ const anonCredsHolderService =
+ agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ if (!credentialRequestMetadata) {
+ throw new AriesFrameworkError(
+ `Missing required request metadata for credential exchange with thread id with id ${credentialRecord.id}`
+ )
+ }
+
+ if (!credentialRecord.credentialAttributes) {
+ throw new AriesFrameworkError(
+ 'Missing credential attributes on credential record. Unable to check credential attributes'
+ )
+ }
+
+ const anonCredsCredential = attachment.getDataAsJson()
+
+ const credentialDefinitionResult = await registryService
+ .getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id)
+ .getCredentialDefinition(agentContext, anonCredsCredential.cred_def_id)
+ if (!credentialDefinitionResult.credentialDefinition) {
+ throw new AriesFrameworkError(
+ `Unable to resolve credential definition ${anonCredsCredential.cred_def_id}: ${credentialDefinitionResult.resolutionMetadata.error} ${credentialDefinitionResult.resolutionMetadata.message}`
+ )
+ }
+
+ const schemaResult = await registryService
+ .getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id)
+ .getSchema(agentContext, anonCredsCredential.schema_id)
+ if (!schemaResult.schema) {
+ throw new AriesFrameworkError(
+ `Unable to resolve schema ${anonCredsCredential.schema_id}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}`
+ )
+ }
+
+ // Resolve revocation registry if credential is revocable
+ let revocationRegistryResult: null | GetRevocationRegistryDefinitionReturn = null
+ if (anonCredsCredential.rev_reg_id) {
+ revocationRegistryResult = await registryService
+ .getRegistryForIdentifier(agentContext, anonCredsCredential.rev_reg_id)
+ .getRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id)
+
+ if (!revocationRegistryResult.revocationRegistryDefinition) {
+ throw new AriesFrameworkError(
+ `Unable to resolve revocation registry definition ${anonCredsCredential.rev_reg_id}: ${revocationRegistryResult.resolutionMetadata.error} ${revocationRegistryResult.resolutionMetadata.message}`
+ )
+ }
+ }
+
+ // assert the credential values match the offer values
+ const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes)
+ assertCredentialValuesMatch(anonCredsCredential.values, recordCredentialValues)
+
+ const credentialId = await anonCredsHolderService.storeCredential(agentContext, {
+ credentialId: utils.uuid(),
+ credentialRequestMetadata,
+ credential: anonCredsCredential,
+ credentialDefinitionId: credentialDefinitionResult.credentialDefinitionId,
+ credentialDefinition: credentialDefinitionResult.credentialDefinition,
+ schema: schemaResult.schema,
+ revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition
+ ? {
+ definition: revocationRegistryResult.revocationRegistryDefinition,
+ id: revocationRegistryResult.revocationRegistryDefinitionId,
+ }
+ : undefined,
+ })
+
+ // If the credential is revocable, store the revocation identifiers in the credential record
+ if (anonCredsCredential.rev_reg_id) {
+ const credential = await anonCredsHolderService.getCredential(agentContext, { credentialId })
+
+ credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, {
+ credentialRevocationId: credential.credentialRevocationId,
+ revocationRegistryId: credential.revocationRegistryId,
+ })
+ credentialRecord.setTags({
+ anonCredsRevocationRegistryId: credential.revocationRegistryId,
+ anonCredsCredentialRevocationId: credential.credentialRevocationId,
+ })
+ }
+
+ credentialRecord.credentials.push({
+ credentialRecordType: this.credentialRecordType,
+ credentialRecordId: credentialId,
+ })
+ }
+
+ public supportsFormat(format: string): boolean {
+ const supportedFormats = [INDY_CRED_ABSTRACT, INDY_CRED_REQUEST, INDY_CRED_FILTER, INDY_CRED]
+
+ return supportedFormats.includes(format)
+ }
+
+ /**
+ * Gets the attachment object for a given attachmentId. We need to get out the correct attachmentId for
+ * indy and then find the corresponding attachment (if there is one)
+ * @param formats the formats object containing the attachmentId
+ * @param messageAttachments the attachments containing the payload
+ * @returns The Attachment if found or undefined
+ *
+ */
+ public getAttachment(formats: CredentialFormatSpec[], messageAttachments: V1Attachment[]): V1Attachment | undefined {
+ const supportedAttachmentIds = formats.filter((f) => this.supportsFormat(f.format)).map((f) => f.attachmentId)
+ const supportedAttachment = messageAttachments.find((attachment) => supportedAttachmentIds.includes(attachment.id))
+
+ return supportedAttachment
+ }
+
+ public async deleteCredentialById(agentContext: AgentContext, credentialRecordId: string): Promise {
+ const anonCredsHolderService =
+ agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ await anonCredsHolderService.deleteCredential(agentContext, credentialRecordId)
+ }
+
+ public async shouldAutoRespondToProposal(
+ agentContext: AgentContext,
+ { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondProposalOptions
+ ) {
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const offerJson = offerAttachment.getDataAsJson()
+
+ // We want to make sure the credential definition matches.
+ // TODO: If no credential definition is present on the proposal, we could check whether the other fields
+ // of the proposal match with the credential definition id.
+ return proposalJson.cred_def_id === offerJson.cred_def_id
+ }
+
+ public async shouldAutoRespondToOffer(
+ agentContext: AgentContext,
+ { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondOfferOptions
+ ) {
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const offerJson = offerAttachment.getDataAsJson()
+
+ // We want to make sure the credential definition matches.
+ // TODO: If no credential definition is present on the proposal, we could check whether the other fields
+ // of the proposal match with the credential definition id.
+ return proposalJson.cred_def_id === offerJson.cred_def_id
+ }
+
+ public async shouldAutoRespondToRequest(
+ agentContext: AgentContext,
+ { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions
+ ) {
+ const credentialOfferJson = offerAttachment.getDataAsJson()
+ const credentialRequestJson = requestAttachment.getDataAsJson()
+
+ return credentialOfferJson.cred_def_id === credentialRequestJson.cred_def_id
+ }
+
+ public async shouldAutoRespondToCredential(
+ agentContext: AgentContext,
+ { credentialRecord, requestAttachment, credentialAttachment }: CredentialFormatAutoRespondCredentialOptions
+ ) {
+ const credentialJson = credentialAttachment.getDataAsJson()
+ const credentialRequestJson = requestAttachment.getDataAsJson()
+
+ // make sure the credential definition matches
+ if (credentialJson.cred_def_id !== credentialRequestJson.cred_def_id) return false
+
+ // If we don't have any attributes stored we can't compare so always return false.
+ if (!credentialRecord.credentialAttributes) return false
+ const attributeValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes)
+
+ // check whether the values match the values in the record
+ return checkCredentialValuesMatch(attributeValues, credentialJson.values)
+ }
+
+ private async createIndyOffer(
+ agentContext: AgentContext,
+ {
+ credentialRecord,
+ attachmentId,
+ credentialDefinitionId,
+ attributes,
+ linkedAttachments,
+ }: {
+ credentialDefinitionId: string
+ credentialRecord: CredentialExchangeRecord
+ attachmentId?: string
+ attributes: CredentialPreviewAttributeOptions[]
+ linkedAttachments?: LinkedAttachment[]
+ }
+ ): Promise {
+ const anonCredsIssuerService =
+ agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol)
+
+ // if the proposal has an attachment Id use that, otherwise the generated id of the formats object
+ const format = new CredentialFormatSpec({
+ attachmentId: attachmentId,
+ format: INDY_CRED_ABSTRACT,
+ })
+
+ const offer = await anonCredsIssuerService.createCredentialOffer(agentContext, {
+ credentialDefinitionId,
+ })
+
+ const { previewAttributes } = this.getCredentialLinkedAttachments(attributes, linkedAttachments)
+ if (!previewAttributes) {
+ throw new AriesFrameworkError('Missing required preview attributes for indy offer')
+ }
+
+ await this.assertPreviewAttributesMatchSchemaAttributes(agentContext, offer, previewAttributes)
+
+ credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, {
+ schemaId: offer.schema_id,
+ credentialDefinitionId: offer.cred_def_id,
+ })
+
+ const attachment = this.getFormatData(offer, format.attachmentId)
+
+ return { format, attachment, previewAttributes }
+ }
+
+ private async assertPreviewAttributesMatchSchemaAttributes(
+ agentContext: AgentContext,
+ offer: AnonCredsCredentialOffer,
+ attributes: CredentialPreviewAttributeOptions[]
+ ): Promise {
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+ const registry = registryService.getRegistryForIdentifier(agentContext, offer.schema_id)
+
+ const schemaResult = await registry.getSchema(agentContext, offer.schema_id)
+
+ if (!schemaResult.schema) {
+ throw new AriesFrameworkError(
+ `Unable to resolve schema ${offer.schema_id} from registry: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}`
+ )
+ }
+
+ assertAttributesMatch(schemaResult.schema, attributes)
+ }
+
+ /**
+ * Get linked attachments for indy format from a proposal message. This allows attachments
+ * to be copied across to old style credential records
+ *
+ * @param options ProposeCredentialOptions object containing (optionally) the linked attachments
+ * @return array of linked attachments or undefined if none present
+ */
+ private getCredentialLinkedAttachments(
+ attributes?: CredentialPreviewAttributeOptions[],
+ linkedAttachments?: LinkedAttachment[]
+ ): {
+ attachments?: V1Attachment[]
+ previewAttributes?: CredentialPreviewAttributeOptions[]
+ } {
+ if (!linkedAttachments && !attributes) {
+ return {}
+ }
+
+ let previewAttributes = attributes ?? []
+ let attachments: V1Attachment[] | undefined
+
+ if (linkedAttachments) {
+ // there are linked attachments so transform into the attribute field of the CredentialPreview object for
+ // this proposal
+ previewAttributes = createAndLinkAttachmentsToPreview(linkedAttachments, previewAttributes)
+ attachments = linkedAttachments.map((linkedAttachment) => linkedAttachment.attachment)
+ }
+
+ return { attachments, previewAttributes }
+ }
+
+ /**
+ * Returns an object of type {@link Attachment} for use in credential exchange messages.
+ * It looks up the correct format identifier and encodes the data as a base64 attachment.
+ *
+ * @param data The data to include in the attach object
+ * @param id the attach id from the formats component of the message
+ */
+ public getFormatData(data: unknown, id: string): V1Attachment {
+ const attachment = new V1Attachment({
+ id,
+ mimeType: 'application/json',
+ data: new V1AttachmentData({
+ base64: JsonEncoder.toBase64(data),
+ }),
+ })
+
+ return attachment
+ }
+}
diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormat.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormat.ts
new file mode 100644
index 0000000000..a586e77b10
--- /dev/null
+++ b/packages/anoncreds/src/formats/LegacyIndyProofFormat.ts
@@ -0,0 +1,40 @@
+import type {
+ AnonCredsProposeProofFormat,
+ AnonCredsRequestProofFormat,
+ AnonCredsGetCredentialsForProofRequestOptions,
+ AnonCredsCredentialsForProofRequest,
+} from './AnonCredsProofFormat'
+import type { AnonCredsProof, AnonCredsProofRequest, AnonCredsSelectedCredentials } from '../models'
+import type { ProofFormat } from '@aries-framework/core'
+
+// TODO: Custom restrictions to remove `_id` from restrictions?
+export type LegacyIndyProofRequest = AnonCredsProofRequest
+
+export interface LegacyIndyProofFormat extends ProofFormat {
+ formatKey: 'indy'
+
+ proofFormats: {
+ createProposal: AnonCredsProposeProofFormat
+ acceptProposal: {
+ name?: string
+ version?: string
+ }
+ createRequest: AnonCredsRequestProofFormat
+ acceptRequest: AnonCredsSelectedCredentials
+
+ getCredentialsForRequest: {
+ input: AnonCredsGetCredentialsForProofRequestOptions
+ output: AnonCredsCredentialsForProofRequest
+ }
+ selectCredentialsForRequest: {
+ input: AnonCredsGetCredentialsForProofRequestOptions
+ output: AnonCredsSelectedCredentials
+ }
+ }
+
+ formatData: {
+ proposal: LegacyIndyProofRequest
+ request: LegacyIndyProofRequest
+ presentation: AnonCredsProof
+ }
+}
diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts
new file mode 100644
index 0000000000..1890acc59e
--- /dev/null
+++ b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts
@@ -0,0 +1,640 @@
+import type {
+ AnonCredsCredentialsForProofRequest,
+ AnonCredsGetCredentialsForProofRequestOptions,
+} from './AnonCredsProofFormat'
+import type { LegacyIndyProofFormat } from './LegacyIndyProofFormat'
+import type {
+ AnonCredsCredentialDefinition,
+ AnonCredsCredentialInfo,
+ AnonCredsProof,
+ AnonCredsRequestedAttribute,
+ AnonCredsRequestedPredicate,
+ AnonCredsSchema,
+ AnonCredsSelectedCredentials,
+ AnonCredsProofRequest,
+} from '../models'
+import type { AnonCredsHolderService, AnonCredsVerifierService, GetCredentialsForProofRequestReturn } from '../services'
+import type {
+ ProofFormatService,
+ AgentContext,
+ ProofFormatCreateReturn,
+ FormatCreateRequestOptions,
+ ProofFormatCreateProposalOptions,
+ ProofFormatProcessOptions,
+ ProofFormatAcceptProposalOptions,
+ ProofFormatAcceptRequestOptions,
+ ProofFormatProcessPresentationOptions,
+ ProofFormatGetCredentialsForRequestOptions,
+ ProofFormatGetCredentialsForRequestReturn,
+ ProofFormatSelectCredentialsForRequestOptions,
+ ProofFormatSelectCredentialsForRequestReturn,
+ ProofFormatAutoRespondProposalOptions,
+ ProofFormatAutoRespondRequestOptions,
+ ProofFormatAutoRespondPresentationOptions,
+} from '@aries-framework/core'
+
+import {
+ AriesFrameworkError,
+ V1Attachment,
+ V1AttachmentData,
+ JsonEncoder,
+ ProofFormatSpec,
+ JsonTransformer,
+} from '@aries-framework/core'
+
+import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest'
+import { AnonCredsVerifierServiceSymbol, AnonCredsHolderServiceSymbol } from '../services'
+import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService'
+import {
+ sortRequestedCredentialsMatches,
+ createRequestFromPreview,
+ areAnonCredsProofRequestsEqual,
+ assertBestPracticeRevocationInterval,
+ checkValidCredentialValueEncoding,
+ encodeCredentialValue,
+ assertNoDuplicateGroupsNamesInProofRequest,
+ getRevocationRegistriesForRequest,
+ getRevocationRegistriesForProof,
+} from '../utils'
+import { isUnqualifiedCredentialDefinitionId, isUnqualifiedSchemaId } from '../utils/indyIdentifiers'
+
+const V2_INDY_PRESENTATION_PROPOSAL = 'hlindy/proof-req@v2.0'
+const V2_INDY_PRESENTATION_REQUEST = 'hlindy/proof-req@v2.0'
+const V2_INDY_PRESENTATION = 'hlindy/proof@v2.0'
+
+export class LegacyIndyProofFormatService implements ProofFormatService {
+ public readonly formatKey = 'indy' as const
+
+ public async createProposal(
+ agentContext: AgentContext,
+ { attachmentId, proofFormats }: ProofFormatCreateProposalOptions
+ ): Promise {
+ const format = new ProofFormatSpec({
+ format: V2_INDY_PRESENTATION_PROPOSAL,
+ attachmentId,
+ })
+
+ const indyFormat = proofFormats.indy
+ if (!indyFormat) {
+ throw Error('Missing indy format to create proposal attachment format')
+ }
+
+ const proofRequest = createRequestFromPreview({
+ attributes: indyFormat.attributes ?? [],
+ predicates: indyFormat.predicates ?? [],
+ name: indyFormat.name ?? 'Proof request',
+ version: indyFormat.version ?? '1.0',
+ nonce: await agentContext.wallet.generateNonce(),
+ })
+ const attachment = this.getFormatData(proofRequest, format.attachmentId)
+
+ return { attachment, format }
+ }
+
+ public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise {
+ const proposalJson = attachment.getDataAsJson()
+
+ // fromJson also validates
+ JsonTransformer.fromJSON(proposalJson, AnonCredsProofRequestClass)
+
+ // Assert attribute and predicate (group) names do not match
+ assertNoDuplicateGroupsNamesInProofRequest(proposalJson)
+ }
+
+ public async acceptProposal(
+ agentContext: AgentContext,
+ { proposalAttachment, attachmentId }: ProofFormatAcceptProposalOptions
+ ): Promise {
+ const format = new ProofFormatSpec({
+ format: V2_INDY_PRESENTATION_REQUEST,
+ attachmentId,
+ })
+
+ const proposalJson = proposalAttachment.getDataAsJson()
+
+ const request = {
+ ...proposalJson,
+ // We never want to reuse the nonce from the proposal, as this will allow replay attacks
+ nonce: await agentContext.wallet.generateNonce(),
+ }
+
+ const attachment = this.getFormatData(request, format.attachmentId)
+
+ return { attachment, format }
+ }
+
+ public async createRequest(
+ agentContext: AgentContext,
+ { attachmentId, proofFormats }: FormatCreateRequestOptions
+ ): Promise {
+ const format = new ProofFormatSpec({
+ format: V2_INDY_PRESENTATION_REQUEST,
+ attachmentId,
+ })
+
+ const indyFormat = proofFormats.indy
+ if (!indyFormat) {
+ throw Error('Missing indy format in create request attachment format')
+ }
+
+ const request = {
+ name: indyFormat.name,
+ version: indyFormat.version,
+ nonce: await agentContext.wallet.generateNonce(),
+ requested_attributes: indyFormat.requested_attributes ?? {},
+ requested_predicates: indyFormat.requested_predicates ?? {},
+ non_revoked: indyFormat.non_revoked,
+ }
+
+ // Assert attribute and predicate (group) names do not match
+ assertNoDuplicateGroupsNamesInProofRequest(request)
+
+ const attachment = this.getFormatData(request, format.attachmentId)
+
+ return { attachment, format }
+ }
+
+ public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise {
+ const requestJson = attachment.getDataAsJson()
+
+ // fromJson also validates
+ JsonTransformer.fromJSON(requestJson, AnonCredsProofRequestClass)
+
+ // Assert attribute and predicate (group) names do not match
+ assertNoDuplicateGroupsNamesInProofRequest(requestJson)
+ }
+
+ public async acceptRequest(
+ agentContext: AgentContext,
+ { proofFormats, requestAttachment, attachmentId }: ProofFormatAcceptRequestOptions
+ ): Promise {
+ const format = new ProofFormatSpec({
+ format: V2_INDY_PRESENTATION,
+ attachmentId,
+ })
+ const requestJson = requestAttachment.getDataAsJson()
+
+ const indyFormat = proofFormats?.indy
+
+ const selectedCredentials =
+ indyFormat ??
+ (await this._selectCredentialsForRequest(agentContext, requestJson, {
+ filterByNonRevocationRequirements: true,
+ }))
+
+ const proof = await this.createProof(agentContext, requestJson, selectedCredentials)
+ const attachment = this.getFormatData(proof, format.attachmentId)
+
+ return {
+ attachment,
+ format,
+ }
+ }
+
+ public async processPresentation(
+ agentContext: AgentContext,
+ { requestAttachment, attachment }: ProofFormatProcessPresentationOptions
+ ): Promise {
+ const verifierService =
+ agentContext.dependencyManager.resolve(AnonCredsVerifierServiceSymbol)
+
+ const proofRequestJson = requestAttachment.getDataAsJson()
+
+ // NOTE: we don't do validation here, as this is handled by the AnonCreds implementation, however
+ // this can lead to confusing error messages. We should consider doing validation here as well.
+ // Defining a class-transformer/class-validator class seems a bit overkill, and the usage of interfaces
+ // for the anoncreds package keeps things simple. Maybe we can try to use something like zod to validate
+ const proofJson = attachment.getDataAsJson()
+
+ for (const [referent, attribute] of Object.entries(proofJson.requested_proof.revealed_attrs)) {
+ if (!checkValidCredentialValueEncoding(attribute.raw, attribute.encoded)) {
+ throw new AriesFrameworkError(
+ `The encoded value for '${referent}' is invalid. ` +
+ `Expected '${encodeCredentialValue(attribute.raw)}'. ` +
+ `Actual '${attribute.encoded}'`
+ )
+ }
+ }
+
+ for (const [, attributeGroup] of Object.entries(proofJson.requested_proof.revealed_attr_groups ?? {})) {
+ for (const [attributeName, attribute] of Object.entries(attributeGroup.values)) {
+ if (!checkValidCredentialValueEncoding(attribute.raw, attribute.encoded)) {
+ throw new AriesFrameworkError(
+ `The encoded value for '${attributeName}' is invalid. ` +
+ `Expected '${encodeCredentialValue(attribute.raw)}'. ` +
+ `Actual '${attribute.encoded}'`
+ )
+ }
+ }
+ }
+
+ // TODO: pre verify proof json
+ // I'm not 100% sure how much indy does. Also if it checks whether the proof requests matches the proof
+ // @see https://github.com/hyperledger/aries-cloudagent-python/blob/master/aries_cloudagent/indy/sdk/verifier.py#L79-L164
+
+ const schemas = await this.getSchemas(agentContext, new Set(proofJson.identifiers.map((i) => i.schema_id)))
+ const credentialDefinitions = await this.getCredentialDefinitions(
+ agentContext,
+ new Set(proofJson.identifiers.map((i) => i.cred_def_id))
+ )
+
+ const revocationRegistries = await getRevocationRegistriesForProof(agentContext, proofJson)
+
+ return await verifierService.verifyProof(agentContext, {
+ proofRequest: proofRequestJson,
+ proof: proofJson,
+ schemas,
+ credentialDefinitions,
+ revocationRegistries,
+ })
+ }
+
+ public async getCredentialsForRequest(
+ agentContext: AgentContext,
+ { requestAttachment, proofFormats }: ProofFormatGetCredentialsForRequestOptions
+ ): Promise> {
+ const proofRequestJson = requestAttachment.getDataAsJson()
+
+ // Set default values
+ const { filterByNonRevocationRequirements = true } = proofFormats?.indy ?? {}
+
+ const credentialsForRequest = await this._getCredentialsForRequest(agentContext, proofRequestJson, {
+ filterByNonRevocationRequirements,
+ })
+
+ return credentialsForRequest
+ }
+
+ public async selectCredentialsForRequest(
+ agentContext: AgentContext,
+ { requestAttachment, proofFormats }: ProofFormatSelectCredentialsForRequestOptions
+ ): Promise> {
+ const proofRequestJson = requestAttachment.getDataAsJson()
+
+ // Set default values
+ const { filterByNonRevocationRequirements = true } = proofFormats?.indy ?? {}
+
+ const selectedCredentials = this._selectCredentialsForRequest(agentContext, proofRequestJson, {
+ filterByNonRevocationRequirements,
+ })
+
+ return selectedCredentials
+ }
+
+ public async shouldAutoRespondToProposal(
+ agentContext: AgentContext,
+ { proposalAttachment, requestAttachment }: ProofFormatAutoRespondProposalOptions
+ ): Promise {
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const requestJson = requestAttachment.getDataAsJson()
+
+ const areRequestsEqual = areAnonCredsProofRequestsEqual(proposalJson, requestJson)
+ agentContext.config.logger.debug(`AnonCreds request and proposal are are equal: ${areRequestsEqual}`, {
+ proposalJson,
+ requestJson,
+ })
+
+ return areRequestsEqual
+ }
+
+ public async shouldAutoRespondToRequest(
+ agentContext: AgentContext,
+ { proposalAttachment, requestAttachment }: ProofFormatAutoRespondRequestOptions
+ ): Promise {
+ const proposalJson = proposalAttachment.getDataAsJson()
+ const requestJson = requestAttachment.getDataAsJson()
+
+ return areAnonCredsProofRequestsEqual(proposalJson, requestJson)
+ }
+
+ public async shouldAutoRespondToPresentation(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _agentContext: AgentContext,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _options: ProofFormatAutoRespondPresentationOptions
+ ): Promise {
+ // The presentation is already verified in processPresentation, so we can just return true here.
+ // It's only an ack, so it's just that we received the presentation.
+ return true
+ }
+
+ public supportsFormat(formatIdentifier: string): boolean {
+ const supportedFormats = [V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST, V2_INDY_PRESENTATION]
+ return supportedFormats.includes(formatIdentifier)
+ }
+
+ private async _getCredentialsForRequest(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ options: AnonCredsGetCredentialsForProofRequestOptions
+ ): Promise {
+ const credentialsForProofRequest: AnonCredsCredentialsForProofRequest = {
+ attributes: {},
+ predicates: {},
+ }
+
+ for (const [referent, requestedAttribute] of Object.entries(proofRequest.requested_attributes)) {
+ const credentials = await this.getCredentialsForProofRequestReferent(agentContext, proofRequest, referent)
+
+ credentialsForProofRequest.attributes[referent] = sortRequestedCredentialsMatches(
+ await Promise.all(
+ credentials.map(async (credential) => {
+ const { isRevoked, timestamp } = await this.getRevocationStatus(
+ agentContext,
+ proofRequest,
+ requestedAttribute,
+ credential.credentialInfo
+ )
+
+ return {
+ credentialId: credential.credentialInfo.credentialId,
+ revealed: true,
+ credentialInfo: credential.credentialInfo,
+ timestamp,
+ revoked: isRevoked,
+ }
+ })
+ )
+ )
+
+ // We only attach revoked state if non-revocation is requested. So if revoked is true it means
+ // the credential is not applicable to the proof request
+ if (options.filterByNonRevocationRequirements) {
+ credentialsForProofRequest.attributes[referent] = credentialsForProofRequest.attributes[referent].filter(
+ (r) => !r.revoked
+ )
+ }
+ }
+
+ for (const [referent, requestedPredicate] of Object.entries(proofRequest.requested_predicates)) {
+ const credentials = await this.getCredentialsForProofRequestReferent(agentContext, proofRequest, referent)
+
+ credentialsForProofRequest.predicates[referent] = sortRequestedCredentialsMatches(
+ await Promise.all(
+ credentials.map(async (credential) => {
+ const { isRevoked, timestamp } = await this.getRevocationStatus(
+ agentContext,
+ proofRequest,
+ requestedPredicate,
+ credential.credentialInfo
+ )
+
+ return {
+ credentialId: credential.credentialInfo.credentialId,
+ credentialInfo: credential.credentialInfo,
+ timestamp,
+ revoked: isRevoked,
+ }
+ })
+ )
+ )
+
+ // We only attach revoked state if non-revocation is requested. So if revoked is true it means
+ // the credential is not applicable to the proof request
+ if (options.filterByNonRevocationRequirements) {
+ credentialsForProofRequest.predicates[referent] = credentialsForProofRequest.predicates[referent].filter(
+ (r) => !r.revoked
+ )
+ }
+ }
+
+ return credentialsForProofRequest
+ }
+
+ private async _selectCredentialsForRequest(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ options: AnonCredsGetCredentialsForProofRequestOptions
+ ): Promise {
+ const credentialsForRequest = await this._getCredentialsForRequest(agentContext, proofRequest, options)
+
+ const selectedCredentials: AnonCredsSelectedCredentials = {
+ attributes: {},
+ predicates: {},
+ selfAttestedAttributes: {},
+ }
+
+ Object.keys(credentialsForRequest.attributes).forEach((attributeName) => {
+ const attributeArray = credentialsForRequest.attributes[attributeName]
+
+ if (attributeArray.length === 0) {
+ throw new AriesFrameworkError('Unable to automatically select requested attributes.')
+ }
+
+ selectedCredentials.attributes[attributeName] = attributeArray[0]
+ })
+
+ Object.keys(credentialsForRequest.predicates).forEach((attributeName) => {
+ if (credentialsForRequest.predicates[attributeName].length === 0) {
+ throw new AriesFrameworkError('Unable to automatically select requested predicates.')
+ } else {
+ selectedCredentials.predicates[attributeName] = credentialsForRequest.predicates[attributeName][0]
+ }
+ })
+
+ return selectedCredentials
+ }
+
+ private async getCredentialsForProofRequestReferent(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ attributeReferent: string
+ ): Promise {
+ const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ const credentials = await holderService.getCredentialsForProofRequest(agentContext, {
+ proofRequest,
+ attributeReferent,
+ })
+
+ return credentials
+ }
+
+ /**
+ * Build schemas object needed to create and verify proof objects.
+ *
+ * Creates object with `{ schemaId: AnonCredsSchema }` mapping
+ *
+ * @param schemaIds List of schema ids
+ * @returns Object containing schemas for specified schema ids
+ *
+ */
+ private async getSchemas(agentContext: AgentContext, schemaIds: Set) {
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+
+ const schemas: { [key: string]: AnonCredsSchema } = {}
+
+ for (const schemaId of schemaIds) {
+ if (!isUnqualifiedSchemaId(schemaId)) {
+ throw new AriesFrameworkError(`${schemaId} is not a valid legacy indy schema id`)
+ }
+
+ const schemaRegistry = registryService.getRegistryForIdentifier(agentContext, schemaId)
+ const schemaResult = await schemaRegistry.getSchema(agentContext, schemaId)
+
+ if (!schemaResult.schema) {
+ throw new AriesFrameworkError(`Schema not found for id ${schemaId}: ${schemaResult.resolutionMetadata.message}`)
+ }
+
+ schemas[schemaId] = schemaResult.schema
+ }
+
+ return schemas
+ }
+
+ /**
+ * Build credential definitions object needed to create and verify proof objects.
+ *
+ * Creates object with `{ credentialDefinitionId: AnonCredsCredentialDefinition }` mapping
+ *
+ * @param credentialDefinitionIds List of credential definition ids
+ * @returns Object containing credential definitions for specified credential definition ids
+ *
+ */
+ private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) {
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+
+ const credentialDefinitions: { [key: string]: AnonCredsCredentialDefinition } = {}
+
+ for (const credentialDefinitionId of credentialDefinitionIds) {
+ if (!isUnqualifiedCredentialDefinitionId(credentialDefinitionId)) {
+ throw new AriesFrameworkError(`${credentialDefinitionId} is not a valid legacy indy credential definition id`)
+ }
+
+ const credentialDefinitionRegistry = registryService.getRegistryForIdentifier(
+ agentContext,
+ credentialDefinitionId
+ )
+
+ const credentialDefinitionResult = await credentialDefinitionRegistry.getCredentialDefinition(
+ agentContext,
+ credentialDefinitionId
+ )
+
+ if (!credentialDefinitionResult.credentialDefinition) {
+ throw new AriesFrameworkError(
+ `Credential definition not found for id ${credentialDefinitionId}: ${credentialDefinitionResult.resolutionMetadata.message}`
+ )
+ }
+
+ credentialDefinitions[credentialDefinitionId] = credentialDefinitionResult.credentialDefinition
+ }
+
+ return credentialDefinitions
+ }
+
+ private async getRevocationStatus(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ requestedItem: AnonCredsRequestedAttribute | AnonCredsRequestedPredicate,
+ credentialInfo: AnonCredsCredentialInfo
+ ) {
+ const requestNonRevoked = requestedItem.non_revoked ?? proofRequest.non_revoked
+ const credentialRevocationId = credentialInfo.credentialRevocationId
+ const revocationRegistryId = credentialInfo.revocationRegistryId
+
+ // If revocation interval is not present or the credential is not revocable then we
+ // don't need to fetch the revocation status
+ if (!requestNonRevoked || !credentialRevocationId || !revocationRegistryId) {
+ return { isRevoked: undefined, timestamp: undefined }
+ }
+
+ agentContext.config.logger.trace(
+ `Fetching credential revocation status for credential revocation id '${credentialRevocationId}' with revocation interval with from '${requestNonRevoked.from}' and to '${requestNonRevoked.to}'`
+ )
+
+ // Make sure the revocation interval follows best practices from Aries RFC 0441
+ assertBestPracticeRevocationInterval(requestNonRevoked)
+
+ const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService)
+ const registry = registryService.getRegistryForIdentifier(agentContext, revocationRegistryId)
+
+ const revocationStatusResult = await registry.getRevocationStatusList(
+ agentContext,
+ revocationRegistryId,
+ requestNonRevoked.to ?? Date.now()
+ )
+
+ if (!revocationStatusResult.revocationStatusList) {
+ throw new AriesFrameworkError(
+ `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${revocationStatusResult.resolutionMetadata.message}`
+ )
+ }
+
+ // Item is revoked when the value at the index is 1
+ const isRevoked = revocationStatusResult.revocationStatusList.revocationList[parseInt(credentialRevocationId)] === 1
+
+ agentContext.config.logger.trace(
+ `Credential with credential revocation index '${credentialRevocationId}' is ${
+ isRevoked ? '' : 'not '
+ }revoked with revocation interval with to '${requestNonRevoked.to}' & from '${requestNonRevoked.from}'`
+ )
+
+ return {
+ isRevoked,
+ timestamp: revocationStatusResult.revocationStatusList.timestamp,
+ }
+ }
+
+ /**
+ * Create indy proof from a given proof request and requested credential object.
+ *
+ * @param proofRequest The proof request to create the proof for
+ * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof
+ * @returns indy proof object
+ */
+ private async createProof(
+ agentContext: AgentContext,
+ proofRequest: AnonCredsProofRequest,
+ selectedCredentials: AnonCredsSelectedCredentials
+ ): Promise {
+ const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol)
+
+ const credentialObjects = await Promise.all(
+ [...Object.values(selectedCredentials.attributes), ...Object.values(selectedCredentials.predicates)].map(
+ async (c) => c.credentialInfo ?? holderService.getCredential(agentContext, { credentialId: c.credentialId })
+ )
+ )
+
+ const schemas = await this.getSchemas(agentContext, new Set(credentialObjects.map((c) => c.schemaId)))
+ const credentialDefinitions = await this.getCredentialDefinitions(
+ agentContext,
+ new Set(credentialObjects.map((c) => c.credentialDefinitionId))
+ )
+
+ // selectedCredentials are overridden with specified timestamps of the revocation status list that
+ // should be used for the selected credentials.
+ const { revocationRegistries, updatedSelectedCredentials } = await getRevocationRegistriesForRequest(
+ agentContext,
+ proofRequest,
+ selectedCredentials
+ )
+
+ return await holderService.createProof(agentContext, {
+ proofRequest,
+ selectedCredentials: updatedSelectedCredentials,
+ schemas,
+ credentialDefinitions,
+ revocationRegistries,
+ })
+ }
+
+ /**
+ * Returns an object of type {@link Attachment} for use in credential exchange messages.
+ * It looks up the correct format identifier and encodes the data as a base64 attachment.
+ *
+ * @param data The data to include in the attach object
+ * @param id the attach id from the formats component of the message
+ */
+ private getFormatData(data: unknown, id: string): V1Attachment {
+ const attachment = new V1Attachment({
+ id,
+ mimeType: 'application/json',
+ data: new V1AttachmentData({
+ base64: JsonEncoder.toBase64(data),
+ }),
+ })
+
+ return attachment
+ }
+}
diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts
new file mode 100644
index 0000000000..ebda7bf2ca
--- /dev/null
+++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts
@@ -0,0 +1,337 @@
+import type { AnonCredsCredentialRequest } from '../../models'
+
+import {
+ CredentialState,
+ CredentialExchangeRecord,
+ KeyProviderRegistry,
+ KeyType,
+ CredentialPreviewAttribute,
+ ProofExchangeRecord,
+ ProofState,
+ EventEmitter,
+} from '@aries-framework/core'
+import * as indySdk from 'indy-sdk'
+import { Subject } from 'rxjs'
+
+import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers'
+import {
+ IndySdkHolderService,
+ IndySdkIssuerService,
+ IndySdkStorageService,
+ IndySdkVerifierService,
+ IndySdkWallet,
+} from '../../../../indy-sdk/src'
+import { IndySdkRevocationService } from '../../../../indy-sdk/src/anoncreds/services/IndySdkRevocationService'
+import { legacyIndyDidFromPublicKeyBase58 } from '../../../../indy-sdk/src/utils/did'
+import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry'
+import { AnonCredsModuleConfig } from '../../AnonCredsModuleConfig'
+import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../../repository'
+import {
+ AnonCredsHolderServiceSymbol,
+ AnonCredsIssuerServiceSymbol,
+ AnonCredsVerifierServiceSymbol,
+} from '../../services'
+import { AnonCredsRegistryService } from '../../services/registry/AnonCredsRegistryService'
+import {
+ getUnqualifiedCredentialDefinitionId,
+ getUnqualifiedSchemaId,
+ parseIndyCredentialDefinitionId,
+ parseIndySchemaId,
+} from '../../utils/indyIdentifiers'
+import { LegacyIndyCredentialFormatService } from '../LegacyIndyCredentialFormatService'
+import { LegacyIndyProofFormatService } from '../LegacyIndyProofFormatService'
+
+const registry = new InMemoryAnonCredsRegistry()
+const anonCredsModuleConfig = new AnonCredsModuleConfig({
+ registries: [registry],
+})
+
+const agentConfig = getAgentConfig('LegacyIndyFormatServicesTest')
+const anonCredsRevocationService = new IndySdkRevocationService(indySdk)
+const anonCredsVerifierService = new IndySdkVerifierService(indySdk)
+const anonCredsHolderService = new IndySdkHolderService(anonCredsRevocationService, indySdk)
+const anonCredsIssuerService = new IndySdkIssuerService(indySdk)
+const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new KeyProviderRegistry([]))
+const storageService = new IndySdkStorageService(indySdk)
+const eventEmitter = new EventEmitter(agentDependencies, new Subject())
+const anonCredsLinkSecretRepository = new AnonCredsLinkSecretRepository(storageService, eventEmitter)
+const agentContext = getAgentContext({
+ registerInstances: [
+ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService],
+ [AnonCredsHolderServiceSymbol, anonCredsHolderService],
+ [AnonCredsVerifierServiceSymbol, anonCredsVerifierService],
+ [AnonCredsRegistryService, new AnonCredsRegistryService()],
+ [AnonCredsModuleConfig, anonCredsModuleConfig],
+ [AnonCredsLinkSecretRepository, anonCredsLinkSecretRepository],
+ ],
+ agentConfig,
+ wallet,
+})
+
+const indyCredentialFormatService = new LegacyIndyCredentialFormatService()
+const indyProofFormatService = new LegacyIndyProofFormatService()
+
+// We can split up these tests when we can use AnonCredsRS as a backend, but currently
+// we need to have the link secrets etc in the wallet which is not so easy to do with Indy
+describe('Legacy indy format services', () => {
+ beforeEach(async () => {
+ await wallet.createAndOpen(agentConfig.walletConfig)
+ })
+
+ afterEach(async () => {
+ await wallet.delete()
+ })
+
+ test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => {
+ // This is just so we don't have to register an actual indy did (as we don't have the indy did registrar configured)
+ const key = await wallet.createKey({ keyType: KeyType.Ed25519 })
+ const unqualifiedIndyDid = legacyIndyDidFromPublicKeyBase58(key.publicKeyBase58)
+ const indyDid = `did:indy:pool1:${unqualifiedIndyDid}`
+
+ // Create link secret
+ await anonCredsHolderService.createLinkSecret(agentContext, {
+ linkSecretId: 'link-secret-id',
+ })
+ const anonCredsLinkSecret = new AnonCredsLinkSecretRecord({
+ linkSecretId: 'link-secret-id',
+ })
+ anonCredsLinkSecret.setTag('isDefault', true)
+ await anonCredsLinkSecretRepository.save(agentContext, anonCredsLinkSecret)
+
+ const schema = await anonCredsIssuerService.createSchema(agentContext, {
+ attrNames: ['name', 'age'],
+ issuerId: indyDid,
+ name: 'Employee Credential',
+ version: '1.0.0',
+ })
+
+ const { schemaState, schemaMetadata } = await registry.registerSchema(agentContext, {
+ schema,
+ options: {},
+ })
+
+ const { credentialDefinition } = await anonCredsIssuerService.createCredentialDefinition(
+ agentContext,
+ {
+ issuerId: indyDid,
+ schemaId: schemaState.schemaId as string,
+ schema,
+ tag: 'Employee Credential',
+ supportRevocation: false,
+ },
+ {
+ // Need to pass this as the indy-sdk MUST have the seqNo
+ indyLedgerSchemaSeqNo: schemaMetadata.indyLedgerSeqNo as number,
+ }
+ )
+
+ const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, {
+ credentialDefinition,
+ options: {},
+ })
+
+ if (
+ !credentialDefinitionState.credentialDefinition ||
+ !credentialDefinitionState.credentialDefinitionId ||
+ !schemaState.schema ||
+ !schemaState.schemaId
+ ) {
+ throw new Error('Failed to create schema or credential definition')
+ }
+
+ const holderCredentialRecord = new CredentialExchangeRecord({
+ protocolVersion: 'v1',
+ state: CredentialState.ProposalSent,
+ threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa',
+ })
+
+ const issuerCredentialRecord = new CredentialExchangeRecord({
+ protocolVersion: 'v1',
+ state: CredentialState.ProposalReceived,
+ threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa',
+ })
+
+ const credentialAttributes = [
+ new CredentialPreviewAttribute({
+ name: 'name',
+ value: 'John',
+ }),
+ new CredentialPreviewAttribute({
+ name: 'age',
+ value: '25',
+ }),
+ ]
+
+ const cd = parseIndyCredentialDefinitionId(credentialDefinitionState.credentialDefinitionId)
+ const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(
+ cd.namespaceIdentifier,
+ cd.schemaSeqNo,
+ cd.tag
+ )
+
+ const s = parseIndySchemaId(schemaState.schemaId)
+ const legacySchemaId = getUnqualifiedSchemaId(s.namespaceIdentifier, s.schemaName, s.schemaVersion)
+
+ // Holder creates proposal
+ holderCredentialRecord.credentialAttributes = credentialAttributes
+ const { attachment: proposalAttachment } = await indyCredentialFormatService.createProposal(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ credentialFormats: {
+ indy: {
+ attributes: credentialAttributes,
+ credentialDefinitionId: legacyCredentialDefinitionId,
+ },
+ },
+ })
+
+ // Issuer processes and accepts proposal
+ await indyCredentialFormatService.processProposal(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ attachment: proposalAttachment,
+ })
+ // Set attributes on the credential record, this is normally done by the protocol service
+ issuerCredentialRecord.credentialAttributes = credentialAttributes
+ const { attachment: offerAttachment } = await indyCredentialFormatService.acceptProposal(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ proposalAttachment: proposalAttachment,
+ })
+
+ // Holder processes and accepts offer
+ await indyCredentialFormatService.processOffer(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ attachment: offerAttachment,
+ })
+ const { attachment: requestAttachment } = await indyCredentialFormatService.acceptOffer(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ offerAttachment,
+ })
+
+ // Make sure the request contains a prover_did field
+ expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeDefined()
+
+ // Issuer processes and accepts request
+ await indyCredentialFormatService.processRequest(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ attachment: requestAttachment,
+ })
+ const { attachment: credentialAttachment } = await indyCredentialFormatService.acceptRequest(agentContext, {
+ credentialRecord: issuerCredentialRecord,
+ requestAttachment,
+ offerAttachment,
+ })
+
+ // Holder processes and accepts credential
+ await indyCredentialFormatService.processCredential(agentContext, {
+ credentialRecord: holderCredentialRecord,
+ attachment: credentialAttachment,
+ requestAttachment,
+ })
+
+ expect(holderCredentialRecord.credentials).toEqual([
+ { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) },
+ ])
+
+ const credentialId = holderCredentialRecord.credentials[0].credentialRecordId
+ const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, {
+ credentialId,
+ })
+
+ expect(anonCredsCredential).toEqual({
+ credentialId,
+ attributes: {
+ age: '25',
+ name: 'John',
+ },
+ schemaId: legacySchemaId,
+ credentialDefinitionId: legacyCredentialDefinitionId,
+ revocationRegistryId: null,
+ credentialRevocationId: null,
+ methodName: 'indy',
+ })
+
+ expect(holderCredentialRecord.metadata.data).toEqual({
+ '_anoncreds/credential': {
+ schemaId: legacySchemaId,
+ credentialDefinitionId: legacyCredentialDefinitionId,
+ },
+ '_anoncreds/credentialRequest': {
+ link_secret_blinding_data: expect.any(Object),
+ link_secret_name: expect.any(String),
+ nonce: expect.any(String),
+ },
+ })
+
+ expect(issuerCredentialRecord.metadata.data).toEqual({
+ '_anoncreds/credential': {
+ schemaId: legacySchemaId,
+ credentialDefinitionId: legacyCredentialDefinitionId,
+ },
+ })
+
+ const holderProofRecord = new ProofExchangeRecord({
+ protocolVersion: 'v1',
+ state: ProofState.ProposalSent,
+ threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38',
+ })
+ const verifierProofRecord = new ProofExchangeRecord({
+ protocolVersion: 'v1',
+ state: ProofState.ProposalReceived,
+ threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38',
+ })
+
+ const { attachment: proofProposalAttachment } = await indyProofFormatService.createProposal(agentContext, {
+ proofFormats: {
+ indy: {
+ attributes: [
+ {
+ name: 'name',
+ credentialDefinitionId: legacyCredentialDefinitionId,
+ value: 'John',
+ referent: '1',
+ },
+ ],
+ predicates: [
+ {
+ credentialDefinitionId: legacyCredentialDefinitionId,
+ name: 'age',
+ predicate: '>=',
+ threshold: 18,
+ },
+ ],
+ name: 'Proof Request',
+ version: '1.0',
+ },
+ },
+ proofRecord: holderProofRecord,
+ })
+
+ await indyProofFormatService.processProposal(agentContext, {
+ attachment: proofProposalAttachment,
+ proofRecord: verifierProofRecord,
+ })
+
+ const { attachment: proofRequestAttachment } = await indyProofFormatService.acceptProposal(agentContext, {
+ proofRecord: verifierProofRecord,
+ proposalAttachment: proofProposalAttachment,
+ })
+
+ await indyProofFormatService.processRequest(agentContext, {
+ attachment: proofRequestAttachment,
+ proofRecord: holderProofRecord,
+ })
+
+ const { attachment: proofAttachment } = await indyProofFormatService.acceptRequest(agentContext, {
+ proofRecord: holderProofRecord,
+ requestAttachment: proofRequestAttachment,
+ proposalAttachment: proofProposalAttachment,
+ })
+
+ const isValid = await indyProofFormatService.processPresentation(agentContext, {
+ attachment: proofAttachment,
+ proofRecord: verifierProofRecord,
+ requestAttachment: proofRequestAttachment,
+ })
+
+ expect(isValid).toBe(true)
+ })
+})
diff --git a/packages/anoncreds/src/formats/index.ts b/packages/anoncreds/src/formats/index.ts
new file mode 100644
index 0000000000..07f76522ba
--- /dev/null
+++ b/packages/anoncreds/src/formats/index.ts
@@ -0,0 +1,9 @@
+export * from './AnonCredsCredentialFormat'
+export * from './LegacyIndyCredentialFormat'
+export { AnonCredsCredentialFormatService } from './AnonCredsCredentialFormatService'
+export { LegacyIndyCredentialFormatService } from './LegacyIndyCredentialFormatService'
+
+export * from './AnonCredsProofFormat'
+export * from './LegacyIndyProofFormat'
+export { AnonCredsProofFormatService } from './AnonCredsProofFormatService'
+export { LegacyIndyProofFormatService } from './LegacyIndyProofFormatService'
diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts
new file mode 100644
index 0000000000..edc9883578
--- /dev/null
+++ b/packages/anoncreds/src/index.ts
@@ -0,0 +1,16 @@
+import 'reflect-metadata'
+
+export * from './models'
+export * from './services'
+export * from './error'
+export * from './repository'
+export * from './formats'
+export * from './protocols'
+
+export { AnonCredsModule } from './AnonCredsModule'
+export { AnonCredsModuleConfig, AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig'
+export { AnonCredsApi } from './AnonCredsApi'
+export * from './AnonCredsApiOptions'
+export { generateLegacyProverDidLikeString } from './utils/proverDid'
+export * from './utils/indyIdentifiers'
+export { assertBestPracticeRevocationInterval } from './utils/revocationInterval'
diff --git a/packages/core/src/modules/credentials/formats/indy/models/IndyCredPropose.ts b/packages/anoncreds/src/models/AnonCredsCredentialProposal.ts
similarity index 58%
rename from packages/core/src/modules/credentials/formats/indy/models/IndyCredPropose.ts
rename to packages/anoncreds/src/models/AnonCredsCredentialProposal.ts
index 3ddfff7542..928c26b5d5 100644
--- a/packages/core/src/modules/credentials/formats/indy/models/IndyCredPropose.ts
+++ b/packages/anoncreds/src/models/AnonCredsCredentialProposal.ts
@@ -1,44 +1,62 @@
import { Expose } from 'class-transformer'
import { IsOptional, IsString } from 'class-validator'
-export interface IndyCredProposeOptions {
+export interface AnonCredsCredentialProposalOptions {
+ /**
+ * @deprecated Use `schemaIssuerId` instead. Only valid for legacy indy identifiers.
+ */
schemaIssuerDid?: string
+ schemaIssuerId?: string
+
schemaId?: string
schemaName?: string
schemaVersion?: string
credentialDefinitionId?: string
+
+ /**
+ * @deprecated Use `issuerId` instead. Only valid for legacy indy identifiers.
+ */
issuerDid?: string
+ issuerId?: string
}
/**
- * Class providing validation for the V2 credential proposal payload.
- *
- * The v1 message contains the properties directly in the message, which means they are
- * validated using the class validator decorators. In v2 the attachments content is not transformed
- * when transforming the message to a class instance so the content is not verified anymore, hence this
- * class.
- *
+ * Class representing an AnonCreds credential proposal as defined in Aries RFC 0592 (and soon the new AnonCreds RFC)
*/
-export class IndyCredPropose {
- public constructor(options: IndyCredProposeOptions) {
+export class AnonCredsCredentialProposal {
+ public constructor(options: AnonCredsCredentialProposalOptions) {
if (options) {
this.schemaIssuerDid = options.schemaIssuerDid
+ this.schemaIssuerId = options.schemaIssuerId
this.schemaId = options.schemaId
this.schemaName = options.schemaName
this.schemaVersion = options.schemaVersion
this.credentialDefinitionId = options.credentialDefinitionId
this.issuerDid = options.issuerDid
+ this.issuerId = options.issuerId
}
}
/**
* Filter to request credential based on a particular Schema issuer DID.
+ *
+ * May only be used with legacy indy identifiers
+ *
+ * @deprecated Use schemaIssuerId instead
*/
@Expose({ name: 'schema_issuer_did' })
@IsString()
@IsOptional()
public schemaIssuerDid?: string
+ /**
+ * Filter to request credential based on a particular Schema issuer DID.
+ */
+ @Expose({ name: 'schema_issuer_id' })
+ @IsString()
+ @IsOptional()
+ public schemaIssuerId?: string
+
/**
* Filter to request credential based on a particular Schema.
*/
@@ -73,9 +91,21 @@ export class IndyCredPropose {
/**
* Filter to request a credential issued by the owner of a particular DID.
+ *
+ * May only be used with legacy indy identifiers
+ *
+ * @deprecated Use issuerId instead
*/
@Expose({ name: 'issuer_did' })
@IsString()
@IsOptional()
public issuerDid?: string
+
+ /**
+ * Filter to request a credential issued by the owner of a particular DID.
+ */
+ @Expose({ name: 'issuer_id' })
+ @IsString()
+ @IsOptional()
+ public issuerId?: string
}
diff --git a/packages/anoncreds/src/models/AnonCredsProofRequest.ts b/packages/anoncreds/src/models/AnonCredsProofRequest.ts
new file mode 100644
index 0000000000..3448b71570
--- /dev/null
+++ b/packages/anoncreds/src/models/AnonCredsProofRequest.ts
@@ -0,0 +1,83 @@
+import type { AnonCredsRequestedAttributeOptions } from './AnonCredsRequestedAttribute'
+import type { AnonCredsRequestedPredicateOptions } from './AnonCredsRequestedPredicate'
+
+import { Expose, Type } from 'class-transformer'
+import { IsIn, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator'
+
+import { IsMap } from '../utils'
+
+import { AnonCredsRequestedAttribute } from './AnonCredsRequestedAttribute'
+import { AnonCredsRequestedPredicate } from './AnonCredsRequestedPredicate'
+import { AnonCredsRevocationInterval } from './AnonCredsRevocationInterval'
+
+export interface AnonCredsProofRequestOptions {
+ name: string
+ version: string
+ nonce: string
+ nonRevoked?: AnonCredsRevocationInterval
+ ver?: '1.0' | '2.0'
+ requestedAttributes?: Record
+ requestedPredicates?: Record
+}
+
+/**
+ * Proof Request for AnonCreds based proof format
+ */
+export class AnonCredsProofRequest {
+ public constructor(options: AnonCredsProofRequestOptions) {
+ if (options) {
+ this.name = options.name
+ this.version = options.version
+ this.nonce = options.nonce
+
+ this.requestedAttributes = new Map(
+ Object.entries(options.requestedAttributes ?? {}).map(([key, attribute]) => [
+ key,
+ new AnonCredsRequestedAttribute(attribute),
+ ])
+ )
+
+ this.requestedPredicates = new Map(
+ Object.entries(options.requestedPredicates ?? {}).map(([key, predicate]) => [
+ key,
+ new AnonCredsRequestedPredicate(predicate),
+ ])
+ )
+
+ this.nonRevoked = options.nonRevoked ? new AnonCredsRevocationInterval(options.nonRevoked) : undefined
+ this.ver = options.ver
+ }
+ }
+
+ @IsString()
+ public name!: string
+
+ @IsString()
+ public version!: string
+
+ @IsString()
+ public nonce!: string
+
+ @Expose({ name: 'requested_attributes' })
+ @IsMap()
+ @ValidateNested({ each: true })
+ @Type(() => AnonCredsRequestedAttribute)
+ public requestedAttributes!: Map
+
+ @Expose({ name: 'requested_predicates' })
+ @IsMap()
+ @ValidateNested({ each: true })
+ @Type(() => AnonCredsRequestedPredicate)
+ public requestedPredicates!: Map
+
+ @Expose({ name: 'non_revoked' })
+ @ValidateNested()
+ @Type(() => AnonCredsRevocationInterval)
+ @IsOptional()
+ @IsInstance(AnonCredsRevocationInterval)
+ public nonRevoked?: AnonCredsRevocationInterval
+
+ @IsIn(['1.0', '2.0'])
+ @IsOptional()
+ public ver?: '1.0' | '2.0'
+}
diff --git a/packages/anoncreds/src/models/AnonCredsRequestedAttribute.ts b/packages/anoncreds/src/models/AnonCredsRequestedAttribute.ts
new file mode 100644
index 0000000000..7e2df55c8f
--- /dev/null
+++ b/packages/anoncreds/src/models/AnonCredsRequestedAttribute.ts
@@ -0,0 +1,48 @@
+import type { AnonCredsRestrictionOptions } from './AnonCredsRestriction'
+
+import { Expose, Type } from 'class-transformer'
+import { ArrayNotEmpty, IsArray, IsInstance, IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator'
+
+import { AnonCredsRestriction, AnonCredsRestrictionTransformer } from './AnonCredsRestriction'
+import { AnonCredsRevocationInterval } from './AnonCredsRevocationInterval'
+
+export interface AnonCredsRequestedAttributeOptions {
+ name?: string
+ names?: string[]
+ nonRevoked?: AnonCredsRevocationInterval
+ restrictions?: AnonCredsRestrictionOptions[]
+}
+
+export class AnonCredsRequestedAttribute {
+ public constructor(options: AnonCredsRequestedAttributeOptions) {
+ if (options) {
+ this.name = options.name
+ this.names = options.names
+ this.nonRevoked = options.nonRevoked ? new AnonCredsRevocationInterval(options.nonRevoked) : undefined
+ this.restrictions = options.restrictions?.map((r) => new AnonCredsRestriction(r))
+ }
+ }
+
+ @IsString()
+ @ValidateIf((o: AnonCredsRequestedAttribute) => o.names === undefined)
+ public name?: string
+
+ @IsArray()
+ @IsString({ each: true })
+ @ValidateIf((o: AnonCredsRequestedAttribute) => o.name === undefined)
+ @ArrayNotEmpty()
+ public names?: string[]
+
+ @Expose({ name: 'non_revoked' })
+ @ValidateNested()
+ @IsInstance(AnonCredsRevocationInterval)
+ @Type(() => AnonCredsRevocationInterval)
+ @IsOptional()
+ public nonRevoked?: AnonCredsRevocationInterval
+
+ @ValidateNested({ each: true })
+ @Type(() => AnonCredsRestriction)
+ @IsOptional()
+ @AnonCredsRestrictionTransformer()
+ public restrictions?: AnonCredsRestriction[]
+}
diff --git a/packages/anoncreds/src/models/AnonCredsRequestedPredicate.ts b/packages/anoncreds/src/models/AnonCredsRequestedPredicate.ts
new file mode 100644
index 0000000000..9df0bcd698
--- /dev/null
+++ b/packages/anoncreds/src/models/AnonCredsRequestedPredicate.ts
@@ -0,0 +1,55 @@
+import type { AnonCredsRestrictionOptions } from './AnonCredsRestriction'
+
+import { Expose, Type } from 'class-transformer'
+import { IsArray, IsIn, IsInstance, IsInt, IsOptional, IsString, ValidateNested } from 'class-validator'
+
+import { AnonCredsPredicateType, anonCredsPredicateType } from '../models'
+
+import { AnonCredsRestriction, AnonCredsRestrictionTransformer } from './AnonCredsRestriction'
+import { AnonCredsRevocationInterval } from './AnonCredsRevocationInterval'
+
+export interface AnonCredsRequestedPredicateOptions {
+ name: string
+ // Also allow string value of the enum as input, to make it easier to use in the API
+ predicateType: AnonCredsPredicateType
+ predicateValue: number
+ nonRevoked?: AnonCredsRevocationInterval
+ restrictions?: AnonCredsRestrictionOptions[]
+}
+
+export class AnonCredsRequestedPredicate {
+ public constructor(options: AnonCredsRequestedPredicateOptions) {
+ if (options) {
+ this.name = options.name
+ this.nonRevoked = options.nonRevoked ? new AnonCredsRevocationInterval(options.nonRevoked) : undefined
+ this.restrictions = options.restrictions?.map((r) => new AnonCredsRestriction(r))
+ this.predicateType = options.predicateType as AnonCredsPredicateType
+ this.predicateValue = options.predicateValue
+ }
+ }
+
+ @IsString()
+ public name!: string
+
+ @Expose({ name: 'p_type' })
+ @IsIn(anonCredsPredicateType)
+ public predicateType!: AnonCredsPredicateType
+
+ @Expose({ name: 'p_value' })
+ @IsInt()
+ public predicateValue!: number
+
+ @Expose({ name: 'non_revoked' })
+ @ValidateNested()
+ @Type(() => AnonCredsRevocationInterval)
+ @IsOptional()
+ @IsInstance(AnonCredsRevocationInterval)
+ public nonRevoked?: AnonCredsRevocationInterval
+
+ @ValidateNested({ each: true })
+ @Type(() => AnonCredsRestriction)
+ @IsOptional()
+ @IsArray()
+ @AnonCredsRestrictionTransformer()
+ public restrictions?: AnonCredsRestriction[]
+}
diff --git a/packages/anoncreds/src/models/AnonCredsRestriction.ts b/packages/anoncreds/src/models/AnonCredsRestriction.ts
new file mode 100644
index 0000000000..c3f8ce843c
--- /dev/null
+++ b/packages/anoncreds/src/models/AnonCredsRestriction.ts
@@ -0,0 +1,152 @@
+import { Exclude, Expose, Transform, TransformationType } from 'class-transformer'
+import { IsOptional, IsString } from 'class-validator'
+
+export interface AnonCredsRestrictionOptions {
+ schemaId?: string
+ schemaIssuerDid?: string
+ schemaIssuerId?: string
+ schemaName?: string
+ schemaVersion?: string
+ issuerDid?: string
+ issuerId?: string
+ credentialDefinitionId?: string
+ attributeMarkers?: Record
+ attributeValues?: Record
+}
+
+export class AnonCredsRestriction {
+ public constructor(options: AnonCredsRestrictionOptions) {
+ if (options) {
+ this.schemaId = options.schemaId
+ this.schemaIssuerDid = options.schemaIssuerDid
+ this.schemaIssuerId = options.schemaIssuerId
+ this.schemaName = options.schemaName
+ this.schemaVersion = options.schemaVersion
+ this.issuerDid = options.issuerDid
+ this.issuerId = options.issuerId
+ this.credentialDefinitionId = options.credentialDefinitionId
+ this.attributeMarkers = options.attributeMarkers ?? {}
+ this.attributeValues = options.attributeValues ?? {}
+ }
+ }
+
+ @Expose({ name: 'schema_id' })
+ @IsOptional()
+ @IsString()
+ public schemaId?: string
+
+ @Expose({ name: 'schema_issuer_did' })
+ @IsOptional()
+ @IsString()
+ public schemaIssuerDid?: string
+
+ @Expose({ name: 'schema_issuer_id' })
+ @IsOptional()
+ @IsString()
+ public schemaIssuerId?: string
+
+ @Expose({ name: 'schema_name' })
+ @IsOptional()
+ @IsString()
+ public schemaName?: string
+
+ @Expose({ name: 'schema_version' })
+ @IsOptional()
+ @IsString()
+ public schemaVersion?: string
+
+ @Expose({ name: 'issuer_did' })
+ @IsOptional()
+ @IsString()
+ public issuerDid?: string
+
+ @Expose({ name: 'issuer_id' })
+ @IsOptional()
+ @IsString()
+ public issuerId?: string
+
+ @Expose({ name: 'cred_def_id' })
+ @IsOptional()
+ @IsString()
+ public credentialDefinitionId?: string
+
+ @Exclude()
+ public attributeMarkers: Record = {}
+
+ @Exclude()
+ public attributeValues: Record = {}
+}
+
+/**
+ * Decorator that transforms attribute values and attribute markers.
+ *
+ * It will transform between the following JSON structure:
+ * ```json
+ * {
+ * "attr::test_prop::value": "test_value"
+ * "attr::test_prop::marker": "1
+ * }
+ * ```
+ *
+ * And the following AnonCredsRestriction:
+ * ```json
+ * {
+ * "attributeValues": {
+ * "test_prop": "test_value"
+ * },
+ * "attributeMarkers": {
+ * "test_prop": true
+ * }
+ * }
+ * ```
+ *
+ * @example
+ * class Example {
+ * AttributeFilterTransformer()
+ * public restrictions!: AnonCredsRestriction[]
+ * }
+ */
+export function AnonCredsRestrictionTransformer() {
+ return Transform(({ value: restrictions, type }) => {
+ switch (type) {
+ case TransformationType.CLASS_TO_PLAIN:
+ if (restrictions && Array.isArray(restrictions)) {
+ for (const restriction of restrictions) {
+ const r = restriction as AnonCredsRestriction
+
+ for (const [attributeName, attributeValue] of Object.entries(r.attributeValues)) {
+ restriction[`attr::${attributeName}::value`] = attributeValue
+ }
+
+ for (const [attributeName] of Object.entries(r.attributeMarkers)) {
+ restriction[`attr::${attributeName}::marker`] = '1'
+ }
+ }
+ }
+
+ return restrictions
+
+ case TransformationType.PLAIN_TO_CLASS:
+ if (restrictions && Array.isArray(restrictions)) {
+ for (const restriction of restrictions) {
+ const r = restriction as AnonCredsRestriction
+
+ for (const [attributeName, attributeValue] of Object.entries(r)) {
+ const match = new RegExp('^attr::([^:]+)::(value|marker)$').exec(attributeName)
+
+ if (match && match[2] === 'marker' && attributeValue === '1') {
+ r.attributeMarkers[match[1]] = true
+ delete restriction[attributeName]
+ } else if (match && match[2] === 'value') {
+ r.attributeValues[match[1]] = attributeValue
+ delete restriction[attributeName]
+ }
+ }
+ }
+ }
+ return restrictions
+ default:
+ return restrictions
+ }
+ })
+}
diff --git a/packages/anoncreds/src/models/AnonCredsRestrictionWrapper.ts b/packages/anoncreds/src/models/AnonCredsRestrictionWrapper.ts
new file mode 100644
index 0000000000..a701c9e6ec
--- /dev/null
+++ b/packages/anoncreds/src/models/AnonCredsRestrictionWrapper.ts
@@ -0,0 +1,11 @@
+import { Type } from 'class-transformer'
+import { ValidateNested } from 'class-validator'
+
+import { AnonCredsRestrictionTransformer, AnonCredsRestriction } from './AnonCredsRestriction'
+
+export class AnonCredsRestrictionWrapper {
+ @ValidateNested({ each: true })
+ @Type(() => AnonCredsRestriction)
+ @AnonCredsRestrictionTransformer()
+ public restrictions!: AnonCredsRestriction[]
+}
diff --git a/packages/core/src/modules/credentials/formats/indy/models/IndyRevocationInterval.ts b/packages/anoncreds/src/models/AnonCredsRevocationInterval.ts
similarity index 69%
rename from packages/core/src/modules/credentials/formats/indy/models/IndyRevocationInterval.ts
rename to packages/anoncreds/src/models/AnonCredsRevocationInterval.ts
index 6057153aaa..0ae0160616 100644
--- a/packages/core/src/modules/credentials/formats/indy/models/IndyRevocationInterval.ts
+++ b/packages/anoncreds/src/models/AnonCredsRevocationInterval.ts
@@ -1,7 +1,7 @@
import { IsInt, IsOptional } from 'class-validator'
-export class IndyRevocationInterval {
- public constructor(options: { from?: number; to?: number }) {
+export class AnonCredsRevocationInterval {
+ public constructor(options: AnonCredsRevocationInterval) {
if (options) {
this.from = options.from
this.to = options.to
diff --git a/packages/anoncreds/src/models/__tests__/AnonCredsRestriction.test.ts b/packages/anoncreds/src/models/__tests__/AnonCredsRestriction.test.ts
new file mode 100644
index 0000000000..33884d09a8
--- /dev/null
+++ b/packages/anoncreds/src/models/__tests__/AnonCredsRestriction.test.ts
@@ -0,0 +1,145 @@
+import { JsonTransformer } from '@aries-framework/core'
+import { Type } from 'class-transformer'
+import { IsArray } from 'class-validator'
+
+import { AnonCredsRestriction, AnonCredsRestrictionTransformer } from '../AnonCredsRestriction'
+
+// We need to add the transformer class to the wrapper
+class Wrapper {
+ public constructor(options: Wrapper) {
+ if (options) {
+ this.restrictions = options.restrictions
+ }
+ }
+
+ @Type(() => AnonCredsRestriction)
+ @IsArray()
+ @AnonCredsRestrictionTransformer()
+ public restrictions!: AnonCredsRestriction[]
+}
+
+describe('AnonCredsRestriction', () => {
+ test('parses attribute values and markers', () => {
+ const anonCredsRestrictions = JsonTransformer.fromJSON(
+ {
+ restrictions: [
+ {
+ 'attr::test_prop::value': 'test_value',
+ 'attr::test_prop2::value': 'test_value2',
+ 'attr::test_prop::marker': '1',
+ 'attr::test_prop2::marker': '1',
+ },
+ ],
+ },
+ Wrapper
+ )
+
+ expect(anonCredsRestrictions).toEqual({
+ restrictions: [
+ {
+ attributeValues: {
+ test_prop: 'test_value',
+ test_prop2: 'test_value2',
+ },
+ attributeMarkers: {
+ test_prop: true,
+ test_prop2: true,
+ },
+ },
+ ],
+ })
+ })
+
+ test('transforms attributeValues and attributeMarkers to json', () => {
+ const restrictions = new Wrapper({
+ restrictions: [
+ new AnonCredsRestriction({
+ attributeMarkers: {
+ test_prop: true,
+ test_prop2: true,
+ },
+ attributeValues: {
+ test_prop: 'test_value',
+ test_prop2: 'test_value2',
+ },
+ }),
+ ],
+ })
+
+ expect(JsonTransformer.toJSON(restrictions)).toMatchObject({
+ restrictions: [
+ {
+ 'attr::test_prop::value': 'test_value',
+ 'attr::test_prop2::value': 'test_value2',
+ 'attr::test_prop::marker': '1',
+ 'attr::test_prop2::marker': '1',
+ },
+ ],
+ })
+ })
+
+ test('transforms properties from and to json with correct casing', () => {
+ const restrictions = new Wrapper({
+ restrictions: [
+ new AnonCredsRestriction({
+ credentialDefinitionId: 'credentialDefinitionId',
+ issuerDid: 'issuerDid',
+ issuerId: 'issuerId',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ schemaId: 'schemaId',
+ schemaIssuerDid: 'schemaIssuerDid',
+ schemaIssuerId: 'schemaIssuerId',
+ }),
+ ],
+ })
+
+ expect(JsonTransformer.toJSON(restrictions)).toMatchObject({
+ restrictions: [
+ {
+ cred_def_id: 'credentialDefinitionId',
+ issuer_did: 'issuerDid',
+ issuer_id: 'issuerId',
+ schema_name: 'schemaName',
+ schema_version: 'schemaVersion',
+ schema_id: 'schemaId',
+ schema_issuer_did: 'schemaIssuerDid',
+ schema_issuer_id: 'schemaIssuerId',
+ },
+ ],
+ })
+
+ expect(
+ JsonTransformer.fromJSON(
+ {
+ restrictions: [
+ {
+ cred_def_id: 'credentialDefinitionId',
+ issuer_did: 'issuerDid',
+ issuer_id: 'issuerId',
+ schema_name: 'schemaName',
+ schema_version: 'schemaVersion',
+ schema_id: 'schemaId',
+ schema_issuer_did: 'schemaIssuerDid',
+ schema_issuer_id: 'schemaIssuerId',
+ },
+ ],
+ },
+ Wrapper
+ )
+ ).toMatchObject({
+ restrictions: [
+ {
+ credentialDefinitionId: 'credentialDefinitionId',
+ issuerDid: 'issuerDid',
+ issuerId: 'issuerId',
+ schemaName: 'schemaName',
+ schemaVersion: 'schemaVersion',
+ schemaId: 'schemaId',
+ schemaIssuerDid: 'schemaIssuerDid',
+ schemaIssuerId: 'schemaIssuerId',
+ },
+ ],
+ })
+ })
+})
diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts
new file mode 100644
index 0000000000..0e0ae355c9
--- /dev/null
+++ b/packages/anoncreds/src/models/exchange.ts
@@ -0,0 +1,125 @@
+export const anonCredsPredicateType = ['>=', '>', '<=', '<'] as const
+export type AnonCredsPredicateType = (typeof anonCredsPredicateType)[number]
+
+export interface AnonCredsProofRequestRestriction {
+ schema_id?: string
+ schema_issuer_id?: string
+ schema_name?: string
+ schema_version?: string
+ issuer_id?: string
+ cred_def_id?: string
+ rev_reg_id?: string
+
+ // Deprecated, but kept for backwards compatibility with legacy indy anoncreds implementations
+ schema_issuer_did?: string
+ issuer_did?: string
+
+ // the following keys can be used for every `attribute name` in credential.
+ [key: `attr::${string}::marker`]: '1' | '0'
+ [key: `attr::${string}::value`]: string
+}
+
+export interface AnonCredsNonRevokedInterval {
+ from?: number
+ to?: number
+}
+
+export interface AnonCredsCredentialOffer {
+ schema_id: string
+ cred_def_id: string
+ nonce: string
+ key_correctness_proof: Record
+}
+
+export interface AnonCredsCredentialRequest {
+ // prover_did is deprecated, however it is kept for backwards compatibility with legacy anoncreds implementations
+ prover_did?: string
+ entropy?: string
+ cred_def_id: string
+ blinded_ms: Record
+ blinded_ms_correctness_proof: Record
+ nonce: string
+}
+
+export type AnonCredsCredentialValues = Record
+export interface AnonCredsCredentialValue {
+ raw: string
+ encoded: string // Raw value as number in string
+}
+
+export interface AnonCredsCredential {
+ schema_id: string
+ cred_def_id: string
+ rev_reg_id?: string
+ values: Record
+ signature: unknown
+ signature_correctness_proof: unknown
+}
+
+export interface AnonCredsProof {
+ requested_proof: {
+ revealed_attrs: Record<
+ string,
+ {
+ sub_proof_index: number
+ raw: string
+ encoded: string
+ }
+ >
+ // revealed_attr_groups is only defined if there's a requested attribute using `names`
+ revealed_attr_groups?: Record<
+ string,
+ {
+ sub_proof_index: number
+ values: {
+ [key: string]: {
+ raw: string
+ encoded: string
+ }
+ }
+ }
+ >
+ unrevealed_attrs: Record<
+ string,
+ {
+ sub_proof_index: number
+ }
+ >
+ self_attested_attrs: Record
+
+ requested_predicates: Record
+ }
+ // TODO: extend types for proof property
+ proof: any
+ identifiers: Array<{
+ schema_id: string
+ cred_def_id: string
+ rev_reg_id?: string
+ timestamp?: number
+ }>
+}
+
+export interface AnonCredsRequestedAttribute {
+ name?: string
+ names?: string[]
+ restrictions?: AnonCredsProofRequestRestriction[]
+ non_revoked?: AnonCredsNonRevokedInterval
+}
+
+export interface AnonCredsRequestedPredicate {
+ name: string
+ p_type: AnonCredsPredicateType
+ p_value: number
+ restrictions?: AnonCredsProofRequestRestriction[]
+ non_revoked?: AnonCredsNonRevokedInterval
+}
+
+export interface AnonCredsProofRequest {
+ name: string
+ version: string
+ nonce: string
+ requested_attributes: Record
+ requested_predicates: Record
+ non_revoked?: AnonCredsNonRevokedInterval
+ ver?: '1.0' | '2.0'
+}
diff --git a/packages/anoncreds/src/models/index.ts b/packages/anoncreds/src/models/index.ts
new file mode 100644
index 0000000000..3ad7724723
--- /dev/null
+++ b/packages/anoncreds/src/models/index.ts
@@ -0,0 +1,4 @@
+export * from './internal'
+export * from './exchange'
+export * from './registry'
+export * from './AnonCredsRestrictionWrapper'
diff --git a/packages/anoncreds/src/models/internal.ts b/packages/anoncreds/src/models/internal.ts
new file mode 100644
index 0000000000..8aacc72a52
--- /dev/null
+++ b/packages/anoncreds/src/models/internal.ts
@@ -0,0 +1,43 @@
+export interface AnonCredsCredentialInfo {
+ credentialId: string
+ attributes: {
+ [key: string]: string
+ }
+ schemaId: string
+ credentialDefinitionId: string
+ revocationRegistryId?: string | undefined
+ credentialRevocationId?: string | undefined
+ methodName: string
+}
+
+export interface AnonCredsRequestedAttributeMatch {
+ credentialId: string
+ timestamp?: number
+ revealed: boolean
+ credentialInfo: AnonCredsCredentialInfo
+ revoked?: boolean
+}
+
+export interface AnonCredsRequestedPredicateMatch {
+ credentialId: string
+ timestamp?: number
+ credentialInfo: AnonCredsCredentialInfo
+ revoked?: boolean
+}
+
+export interface AnonCredsSelectedCredentials {
+ attributes: Record
+ predicates: Record
+ selfAttestedAttributes: Record