From 25b2bcf81648100b572784e4489a288cc9da0557 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sat, 21 Jan 2023 03:20:49 +0800 Subject: [PATCH 01/20] feat(cache): add caching interface (#1229) Signed-off-by: Timo Glastra --- packages/core/src/agent/Agent.ts | 2 - packages/core/src/agent/AgentModules.ts | 2 + .../src/agent/__tests__/AgentModules.test.ts | 4 + packages/core/src/cache/CacheRecord.ts | 41 ----- packages/core/src/cache/CacheRepository.ts | 17 -- packages/core/src/cache/PersistedLruCache.ts | 75 --------- .../cache/__tests__/PersistedLruCache.test.ts | 73 -------- packages/core/src/cache/index.ts | 3 - packages/core/src/index.ts | 2 +- packages/core/src/modules/cache/Cache.ts | 7 + .../core/src/modules/cache/CacheModule.ts | 34 ++++ .../src/modules/cache/CacheModuleConfig.ts | 29 ++++ .../src/modules/cache/InMemoryLruCache.ts | 71 ++++++++ .../cache/__tests__/CacheModule.test.ts | 42 +++++ .../cache/__tests__/CacheModuleConfig.test.ts | 14 ++ .../cache/__tests__/InMemoryLruCache.test.ts | 43 +++++ packages/core/src/modules/cache/index.ts | 10 ++ .../SingleContextLruCacheRecord.ts | 41 +++++ .../SingleContextLruCacheRepository.ts | 17 ++ .../SingleContextStorageLruCache.ts | 158 ++++++++++++++++++ .../SingleContextStorageLruCache.test.ts | 91 ++++++++++ .../cache/singleContextLruCache/index.ts | 1 + .../ledger/__tests__/IndyPoolService.test.ts | 59 ++----- .../ledger/services/IndyPoolService.ts | 15 +- packages/indy-sdk/jest.config.ts | 2 +- .../indy-sdk/src/ledger/IndySdkPoolService.ts | 21 +-- .../__tests__/IndySdkPoolService.test.ts | 60 ++----- packages/indy-sdk/tests/setup.ts | 1 + 28 files changed, 607 insertions(+), 328 deletions(-) delete mode 100644 packages/core/src/cache/CacheRecord.ts delete mode 100644 packages/core/src/cache/CacheRepository.ts delete mode 100644 packages/core/src/cache/PersistedLruCache.ts delete mode 100644 packages/core/src/cache/__tests__/PersistedLruCache.test.ts delete mode 100644 packages/core/src/cache/index.ts create mode 100644 packages/core/src/modules/cache/Cache.ts create mode 100644 packages/core/src/modules/cache/CacheModule.ts create mode 100644 packages/core/src/modules/cache/CacheModuleConfig.ts create mode 100644 packages/core/src/modules/cache/InMemoryLruCache.ts create mode 100644 packages/core/src/modules/cache/__tests__/CacheModule.test.ts create mode 100644 packages/core/src/modules/cache/__tests__/CacheModuleConfig.test.ts create mode 100644 packages/core/src/modules/cache/__tests__/InMemoryLruCache.test.ts create mode 100644 packages/core/src/modules/cache/index.ts create mode 100644 packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRecord.ts create mode 100644 packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRepository.ts create mode 100644 packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts create mode 100644 packages/core/src/modules/cache/singleContextLruCache/__tests__/SingleContextStorageLruCache.test.ts create mode 100644 packages/core/src/modules/cache/singleContextLruCache/index.ts create mode 100644 packages/indy-sdk/tests/setup.ts diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index a3a6b11ab1..2909c3536d 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -9,7 +9,6 @@ import type { Subscription } from 'rxjs' import { Subject } from 'rxjs' import { concatMap, takeUntil } from 'rxjs/operators' -import { CacheRepository } from '../cache' import { InjectionSymbols } from '../constants' import { SigningProviderToken } from '../crypto' import { JwsService } from '../crypto/JwsService' @@ -59,7 +58,6 @@ export class Agent extends BaseAge dependencyManager.registerSingleton(EnvelopeService) dependencyManager.registerSingleton(FeatureRegistry) dependencyManager.registerSingleton(JwsService) - dependencyManager.registerSingleton(CacheRepository) dependencyManager.registerSingleton(DidCommMessageRepository) dependencyManager.registerSingleton(StorageVersionRepository) dependencyManager.registerSingleton(StorageUpdateService) diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index 4bb5cc4067..3f9512bdba 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -4,6 +4,7 @@ import type { IsAny } from '../types' import type { Constructor } from '../utils/mixins' import { BasicMessagesModule } from '../modules/basic-messages' +import { CacheModule } from '../modules/cache' import { ConnectionsModule } from '../modules/connections' import { CredentialsModule } from '../modules/credentials' import { DidsModule } from '../modules/dids' @@ -157,6 +158,7 @@ function getDefaultAgentModules(agentConfig: AgentConfig) { oob: () => new OutOfBandModule(), indy: () => new IndyModule(), w3cVc: () => new W3cVcModule(), + cache: () => new CacheModule(), } as const } diff --git a/packages/core/src/agent/__tests__/AgentModules.test.ts b/packages/core/src/agent/__tests__/AgentModules.test.ts index cfb88ab7b0..60755c487e 100644 --- a/packages/core/src/agent/__tests__/AgentModules.test.ts +++ b/packages/core/src/agent/__tests__/AgentModules.test.ts @@ -2,6 +2,7 @@ import type { Module } from '../../plugins' import { getAgentConfig } from '../../../tests/helpers' import { BasicMessagesModule } from '../../modules/basic-messages' +import { CacheModule } from '../../modules/cache' import { ConnectionsModule } from '../../modules/connections' import { CredentialsModule } from '../../modules/credentials' import { DidsModule } from '../../modules/dids' @@ -72,6 +73,7 @@ describe('AgentModules', () => { oob: expect.any(OutOfBandModule), indy: expect.any(IndyModule), w3cVc: expect.any(W3cVcModule), + cache: expect.any(CacheModule), }) }) @@ -96,6 +98,7 @@ describe('AgentModules', () => { oob: expect.any(OutOfBandModule), indy: expect.any(IndyModule), w3cVc: expect.any(W3cVcModule), + cache: expect.any(CacheModule), myModule, }) }) @@ -123,6 +126,7 @@ describe('AgentModules', () => { oob: expect.any(OutOfBandModule), indy: expect.any(IndyModule), w3cVc: expect.any(W3cVcModule), + cache: expect.any(CacheModule), myModule, }) }) diff --git a/packages/core/src/cache/CacheRecord.ts b/packages/core/src/cache/CacheRecord.ts deleted file mode 100644 index 26388d1706..0000000000 --- a/packages/core/src/cache/CacheRecord.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { RecordTags, TagsBase } from '../storage/BaseRecord' - -import { BaseRecord } from '../storage/BaseRecord' -import { uuid } from '../utils/uuid' - -export type CustomCacheTags = TagsBase -export type DefaultCacheTags = TagsBase - -export type CacheTags = RecordTags - -export interface CacheStorageProps { - id?: string - createdAt?: Date - tags?: CustomCacheTags - - entries: Array<{ key: string; value: unknown }> -} - -export class CacheRecord extends BaseRecord { - public entries!: Array<{ key: string; value: unknown }> - - public static readonly type = 'CacheRecord' - public readonly type = CacheRecord.type - - public constructor(props: CacheStorageProps) { - super() - - if (props) { - this.id = props.id ?? uuid() - this.createdAt = props.createdAt ?? new Date() - this.entries = props.entries - this._tags = props.tags ?? {} - } - } - - public getTags() { - return { - ...this._tags, - } - } -} diff --git a/packages/core/src/cache/CacheRepository.ts b/packages/core/src/cache/CacheRepository.ts deleted file mode 100644 index 3adb2e4fd2..0000000000 --- a/packages/core/src/cache/CacheRepository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { EventEmitter } from '../agent/EventEmitter' -import { InjectionSymbols } from '../constants' -import { inject, injectable } from '../plugins' -import { Repository } from '../storage/Repository' -import { StorageService } from '../storage/StorageService' - -import { CacheRecord } from './CacheRecord' - -@injectable() -export class CacheRepository extends Repository { - public constructor( - @inject(InjectionSymbols.StorageService) storageService: StorageService, - eventEmitter: EventEmitter - ) { - super(CacheRecord, storageService, eventEmitter) - } -} diff --git a/packages/core/src/cache/PersistedLruCache.ts b/packages/core/src/cache/PersistedLruCache.ts deleted file mode 100644 index bb94c41bee..0000000000 --- a/packages/core/src/cache/PersistedLruCache.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { CacheRepository } from './CacheRepository' -import type { AgentContext } from '../agent' - -import { LRUMap } from 'lru_map' - -import { CacheRecord } from './CacheRecord' - -export class PersistedLruCache { - private cacheId: string - private limit: number - private _cache?: LRUMap - private cacheRepository: CacheRepository - - public constructor(cacheId: string, limit: number, cacheRepository: CacheRepository) { - this.cacheId = cacheId - this.limit = limit - this.cacheRepository = cacheRepository - } - - public async get(agentContext: AgentContext, key: string) { - const cache = await this.getCache(agentContext) - - return cache.get(key) - } - - public async set(agentContext: AgentContext, key: string, value: CacheValue) { - const cache = await this.getCache(agentContext) - - cache.set(key, value) - await this.persistCache(agentContext) - } - - private async getCache(agentContext: AgentContext) { - if (!this._cache) { - const cacheRecord = await this.fetchCacheRecord(agentContext) - this._cache = this.lruFromRecord(cacheRecord) - } - - return this._cache - } - - private lruFromRecord(cacheRecord: CacheRecord) { - return new LRUMap( - this.limit, - cacheRecord.entries.map((e) => [e.key, e.value as CacheValue]) - ) - } - - private async fetchCacheRecord(agentContext: AgentContext) { - let cacheRecord = await this.cacheRepository.findById(agentContext, this.cacheId) - - if (!cacheRecord) { - cacheRecord = new CacheRecord({ - id: this.cacheId, - entries: [], - }) - - await this.cacheRepository.save(agentContext, cacheRecord) - } - - return cacheRecord - } - - private async persistCache(agentContext: AgentContext) { - const cache = await this.getCache(agentContext) - - await this.cacheRepository.update( - agentContext, - new CacheRecord({ - entries: cache.toJSON(), - id: this.cacheId, - }) - ) - } -} diff --git a/packages/core/src/cache/__tests__/PersistedLruCache.test.ts b/packages/core/src/cache/__tests__/PersistedLruCache.test.ts deleted file mode 100644 index c7b893108d..0000000000 --- a/packages/core/src/cache/__tests__/PersistedLruCache.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { getAgentContext, mockFunction } from '../../../tests/helpers' -import { CacheRecord } from '../CacheRecord' -import { CacheRepository } from '../CacheRepository' -import { PersistedLruCache } from '../PersistedLruCache' - -jest.mock('../CacheRepository') -const CacheRepositoryMock = CacheRepository as jest.Mock - -const agentContext = getAgentContext() - -describe('PersistedLruCache', () => { - let cacheRepository: CacheRepository - let cache: PersistedLruCache - - beforeEach(() => { - cacheRepository = new CacheRepositoryMock() - mockFunction(cacheRepository.findById).mockResolvedValue(null) - - cache = new PersistedLruCache('cacheId', 2, cacheRepository) - }) - - it('should return the value from the persisted record', async () => { - const findMock = mockFunction(cacheRepository.findById).mockResolvedValue( - new CacheRecord({ - id: 'cacheId', - entries: [ - { - key: 'test', - value: 'somevalue', - }, - ], - }) - ) - - expect(await cache.get(agentContext, 'doesnotexist')).toBeUndefined() - expect(await cache.get(agentContext, 'test')).toBe('somevalue') - expect(findMock).toHaveBeenCalledWith(agentContext, 'cacheId') - }) - - it('should set the value in the persisted record', async () => { - const updateMock = mockFunction(cacheRepository.update).mockResolvedValue() - - await cache.set(agentContext, 'test', 'somevalue') - const [[, cacheRecord]] = updateMock.mock.calls - - expect(cacheRecord.entries.length).toBe(1) - expect(cacheRecord.entries[0].key).toBe('test') - expect(cacheRecord.entries[0].value).toBe('somevalue') - - expect(await cache.get(agentContext, 'test')).toBe('somevalue') - }) - - it('should remove least recently used entries if entries are added that exceed the limit', async () => { - // Set first value in cache, resolves fine - await cache.set(agentContext, 'one', 'valueone') - expect(await cache.get(agentContext, 'one')).toBe('valueone') - - // Set two more entries in the cache. Third item - // exceeds limit, so first item gets removed - await cache.set(agentContext, 'two', 'valuetwo') - await cache.set(agentContext, 'three', 'valuethree') - expect(await cache.get(agentContext, 'one')).toBeUndefined() - expect(await cache.get(agentContext, 'two')).toBe('valuetwo') - expect(await cache.get(agentContext, 'three')).toBe('valuethree') - - // Get two from the cache, meaning three will be removed first now - // because it is not recently used - await cache.get(agentContext, 'two') - await cache.set(agentContext, 'four', 'valuefour') - expect(await cache.get(agentContext, 'three')).toBeUndefined() - expect(await cache.get(agentContext, 'two')).toBe('valuetwo') - }) -}) diff --git a/packages/core/src/cache/index.ts b/packages/core/src/cache/index.ts deleted file mode 100644 index dab23e81d6..0000000000 --- a/packages/core/src/cache/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './PersistedLruCache' -export * from './CacheRecord' -export * from './CacheRepository' diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5b2eaf1762..e2b1665a2a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -57,6 +57,7 @@ export * from './modules/routing' export * from './modules/oob' export * from './modules/dids' export * from './modules/vc' +export * from './modules/cache' export { JsonEncoder, JsonTransformer, isJsonObject, isValidJweStructure, TypedArrayEncoder, Buffer } from './utils' export * from './logger' export * from './error' @@ -65,7 +66,6 @@ export { parseMessageType, IsValidMessageType } from './utils/messageType' export type { Constructor } from './utils/mixins' export * from './agent/Events' export * from './crypto/' -export { PersistedLruCache, CacheRepository } from './cache' import { parseInvitationUrl } from './utils/parseInvitation' import { uuid } from './utils/uuid' diff --git a/packages/core/src/modules/cache/Cache.ts b/packages/core/src/modules/cache/Cache.ts new file mode 100644 index 0000000000..546e03925d --- /dev/null +++ b/packages/core/src/modules/cache/Cache.ts @@ -0,0 +1,7 @@ +import type { AgentContext } from '../../agent/context' + +export interface Cache { + get(agentContext: AgentContext, key: string): Promise + set(agentContext: AgentContext, key: string, value: CacheValue, expiresInSeconds?: number): Promise + remove(agentContext: AgentContext, key: string): Promise +} diff --git a/packages/core/src/modules/cache/CacheModule.ts b/packages/core/src/modules/cache/CacheModule.ts new file mode 100644 index 0000000000..c4d2ba0e5c --- /dev/null +++ b/packages/core/src/modules/cache/CacheModule.ts @@ -0,0 +1,34 @@ +import type { CacheModuleConfigOptions } from './CacheModuleConfig' +import type { DependencyManager, Module } from '../../plugins' +import type { Optional } from '../../utils' + +import { CacheModuleConfig } from './CacheModuleConfig' +import { SingleContextLruCacheRepository } from './singleContextLruCache/SingleContextLruCacheRepository' +import { SingleContextStorageLruCache } from './singleContextLruCache/SingleContextStorageLruCache' + +// CacheModuleOptions makes the credentialProtocols property optional from the config, as it will set it when not provided. +export type CacheModuleOptions = Optional + +export class CacheModule implements Module { + public readonly config: CacheModuleConfig + + public constructor(config?: CacheModuleOptions) { + this.config = new CacheModuleConfig({ + ...config, + cache: + config?.cache ?? + new SingleContextStorageLruCache({ + limit: 500, + }), + }) + } + + public register(dependencyManager: DependencyManager) { + dependencyManager.registerInstance(CacheModuleConfig, this.config) + + // Custom handling for when we're using the SingleContextStorageLruCache + if (this.config.cache instanceof SingleContextStorageLruCache) { + dependencyManager.registerSingleton(SingleContextLruCacheRepository) + } + } +} diff --git a/packages/core/src/modules/cache/CacheModuleConfig.ts b/packages/core/src/modules/cache/CacheModuleConfig.ts new file mode 100644 index 0000000000..a04b143dc8 --- /dev/null +++ b/packages/core/src/modules/cache/CacheModuleConfig.ts @@ -0,0 +1,29 @@ +import type { Cache } from './Cache' + +/** + * CacheModuleConfigOptions defines the interface for the options of the CacheModuleConfig class. + */ +export interface CacheModuleConfigOptions { + /** + * Implementation of the {@link Cache} interface. + * + * NOTE: Starting from AFJ 0.4.0 the default cache implementation will be {@link InMemoryLruCache} + * @default SingleContextStorageLruCache - with a limit of 500 + * + * + */ + cache: Cache +} + +export class CacheModuleConfig { + private options: CacheModuleConfigOptions + + public constructor(options: CacheModuleConfigOptions) { + this.options = options + } + + /** See {@link CacheModuleConfigOptions.cache} */ + public get cache() { + return this.options.cache + } +} diff --git a/packages/core/src/modules/cache/InMemoryLruCache.ts b/packages/core/src/modules/cache/InMemoryLruCache.ts new file mode 100644 index 0000000000..4a56cb97c5 --- /dev/null +++ b/packages/core/src/modules/cache/InMemoryLruCache.ts @@ -0,0 +1,71 @@ +import type { Cache } from './Cache' +import type { AgentContext } from '../../agent/context' + +import { LRUMap } from 'lru_map' + +export interface InMemoryLruCacheOptions { + /** The maximum number of entries allowed in the cache */ + limit: number +} + +/** + * In memory LRU cache. + * + * This cache can be used with multiple agent context instances, however all instances will share the same cache. + * If you need the cache to be isolated per agent context instance, make sure to use a different cache implementation. + */ +export class InMemoryLruCache implements Cache { + private readonly cache: LRUMap + + public constructor({ limit }: InMemoryLruCacheOptions) { + this.cache = new LRUMap(limit) + } + + public async get(agentContext: AgentContext, key: string) { + this.removeExpiredItems() + const item = this.cache.get(key) + + // Does not exist + if (!item) return null + + return item.value as CacheValue + } + + public async set( + agentContext: AgentContext, + key: string, + value: CacheValue, + expiresInSeconds?: number + ): Promise { + this.removeExpiredItems() + let expiresDate = undefined + + if (expiresInSeconds) { + expiresDate = new Date() + expiresDate.setSeconds(expiresDate.getSeconds() + expiresInSeconds) + } + + this.cache.set(key, { + expiresAt: expiresDate?.getTime(), + value, + }) + } + + public async remove(agentContext: AgentContext, key: string): Promise { + this.removeExpiredItems() + this.cache.delete(key) + } + + private removeExpiredItems() { + this.cache.forEach((value, key) => { + if (value.expiresAt && Date.now() > value.expiresAt) { + this.cache.delete(key) + } + }) + } +} + +interface CacheItem { + expiresAt?: number + value: unknown +} diff --git a/packages/core/src/modules/cache/__tests__/CacheModule.test.ts b/packages/core/src/modules/cache/__tests__/CacheModule.test.ts new file mode 100644 index 0000000000..fe38e2e139 --- /dev/null +++ b/packages/core/src/modules/cache/__tests__/CacheModule.test.ts @@ -0,0 +1,42 @@ +import { DependencyManager } from '../../../plugins/DependencyManager' +import { CacheModule } from '../CacheModule' +import { CacheModuleConfig } from '../CacheModuleConfig' +import { InMemoryLruCache } from '../InMemoryLruCache' +import { SingleContextStorageLruCache } from '../singleContextLruCache' +import { SingleContextLruCacheRepository } from '../singleContextLruCache/SingleContextLruCacheRepository' + +jest.mock('../../../plugins/DependencyManager') +const DependencyManagerMock = DependencyManager as jest.Mock + +const dependencyManager = new DependencyManagerMock() + +describe('CacheModule', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + test('registers dependencies on the dependency manager', () => { + const cacheModule = new CacheModule({ + cache: new InMemoryLruCache({ limit: 1 }), + }) + cacheModule.register(dependencyManager) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(CacheModuleConfig, cacheModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(0) + }) + + test('registers cache repository on the dependency manager if the SingleContextStorageLruCache is used', () => { + const cacheModule = new CacheModule({ + cache: new SingleContextStorageLruCache({ limit: 1 }), + }) + cacheModule.register(dependencyManager) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(CacheModuleConfig, cacheModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SingleContextLruCacheRepository) + }) +}) diff --git a/packages/core/src/modules/cache/__tests__/CacheModuleConfig.test.ts b/packages/core/src/modules/cache/__tests__/CacheModuleConfig.test.ts new file mode 100644 index 0000000000..9cbc267122 --- /dev/null +++ b/packages/core/src/modules/cache/__tests__/CacheModuleConfig.test.ts @@ -0,0 +1,14 @@ +import { CacheModuleConfig } from '../CacheModuleConfig' +import { InMemoryLruCache } from '../InMemoryLruCache' + +describe('CacheModuleConfig', () => { + test('sets values', () => { + const cache = new InMemoryLruCache({ limit: 1 }) + + const config = new CacheModuleConfig({ + cache, + }) + + expect(config.cache).toEqual(cache) + }) +}) diff --git a/packages/core/src/modules/cache/__tests__/InMemoryLruCache.test.ts b/packages/core/src/modules/cache/__tests__/InMemoryLruCache.test.ts new file mode 100644 index 0000000000..aa802575c4 --- /dev/null +++ b/packages/core/src/modules/cache/__tests__/InMemoryLruCache.test.ts @@ -0,0 +1,43 @@ +import { getAgentContext } from '../../../../tests/helpers' +import { InMemoryLruCache } from '../InMemoryLruCache' + +const agentContext = getAgentContext() + +describe('InMemoryLruCache', () => { + let cache: InMemoryLruCache + + beforeEach(() => { + cache = new InMemoryLruCache({ limit: 2 }) + }) + + it('should set, get and remove a value', async () => { + expect(await cache.get(agentContext, 'item')).toBeNull() + + await cache.set(agentContext, 'item', 'somevalue') + expect(await cache.get(agentContext, 'item')).toBe('somevalue') + + await cache.remove(agentContext, 'item') + expect(await cache.get(agentContext, 'item')).toBeNull() + }) + + it('should remove least recently used entries if entries are added that exceed the limit', async () => { + // Set first value in cache, resolves fine + await cache.set(agentContext, 'one', 'valueone') + expect(await cache.get(agentContext, 'one')).toBe('valueone') + + // Set two more entries in the cache. Third item + // exceeds limit, so first item gets removed + await cache.set(agentContext, 'two', 'valuetwo') + await cache.set(agentContext, 'three', 'valuethree') + expect(await cache.get(agentContext, 'one')).toBeNull() + expect(await cache.get(agentContext, 'two')).toBe('valuetwo') + expect(await cache.get(agentContext, 'three')).toBe('valuethree') + + // Get two from the cache, meaning three will be removed first now + // because it is not recently used + await cache.get(agentContext, 'two') + await cache.set(agentContext, 'four', 'valuefour') + expect(await cache.get(agentContext, 'three')).toBeNull() + expect(await cache.get(agentContext, 'two')).toBe('valuetwo') + }) +}) diff --git a/packages/core/src/modules/cache/index.ts b/packages/core/src/modules/cache/index.ts new file mode 100644 index 0000000000..5b5d932671 --- /dev/null +++ b/packages/core/src/modules/cache/index.ts @@ -0,0 +1,10 @@ +// Module +export { CacheModule, CacheModuleOptions } from './CacheModule' +export { CacheModuleConfig } from './CacheModuleConfig' + +// Cache +export { Cache } from './Cache' + +// Cache Implementations +export { InMemoryLruCache, InMemoryLruCacheOptions } from './InMemoryLruCache' +export { SingleContextStorageLruCache, SingleContextStorageLruCacheOptions } from './singleContextLruCache' diff --git a/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRecord.ts b/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRecord.ts new file mode 100644 index 0000000000..0016ed3d8e --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRecord.ts @@ -0,0 +1,41 @@ +import type { TagsBase } from '../../../storage/BaseRecord' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' + +export interface SingleContextLruCacheItem { + value: unknown + expiresAt?: number +} + +export interface SingleContextLruCacheProps { + id?: string + createdAt?: Date + tags?: TagsBase + + entries: Map +} + +export class SingleContextLruCacheRecord extends BaseRecord { + public entries!: Map + + public static readonly type = 'SingleContextLruCacheRecord' + public readonly type = SingleContextLruCacheRecord.type + + public constructor(props: SingleContextLruCacheProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.entries = props.entries + this._tags = props.tags ?? {} + } + } + + public getTags() { + return { + ...this._tags, + } + } +} diff --git a/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRepository.ts b/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRepository.ts new file mode 100644 index 0000000000..dab71b9761 --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/SingleContextLruCacheRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { SingleContextLruCacheRecord } from './SingleContextLruCacheRecord' + +@injectable() +export class SingleContextLruCacheRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(SingleContextLruCacheRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts b/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts new file mode 100644 index 0000000000..72498db91a --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts @@ -0,0 +1,158 @@ +import type { SingleContextLruCacheItem } from './SingleContextLruCacheRecord' +import type { AgentContext } from '../../../agent/context' +import type { Cache } from '../Cache' + +import { LRUMap } from 'lru_map' + +import { AriesFrameworkError } from '../../../error' + +import { SingleContextLruCacheRecord } from './SingleContextLruCacheRecord' +import { SingleContextLruCacheRepository } from './SingleContextLruCacheRepository' + +const CONTEXT_STORAGE_LRU_CACHE_ID = 'CONTEXT_STORAGE_LRU_CACHE_ID' + +export interface SingleContextStorageLruCacheOptions { + /** The maximum number of entries allowed in the cache */ + limit: number +} + +/** + * Cache that leverages the storage associated with the agent context to store cache records. + * It will keep an in-memory cache of the records to avoid hitting the storage on every read request. + * Therefor this cache is meant to be used with a single instance of the agent. + * + * Due to keeping an in-memory copy of the cache, it is also not meant to be used with multiple + * agent context instances (meaning multi-tenancy), as they will overwrite the in-memory cache. + * + * However, this means the cache is not meant for usage with multiple instances. + */ +export class SingleContextStorageLruCache implements Cache { + private limit: number + private _cache?: LRUMap + private _contextCorrelationId?: string + + public constructor({ limit }: SingleContextStorageLruCacheOptions) { + this.limit = limit + } + + public async get(agentContext: AgentContext, key: string) { + this.assertContextCorrelationId(agentContext) + + const cache = await this.getCache(agentContext) + this.removeExpiredItems(cache) + + const item = cache.get(key) + + // Does not exist + if (!item) return null + + // Expired + if (item.expiresAt && Date.now() > item.expiresAt) { + cache.delete(key) + await this.persistCache(agentContext) + return null + } + + return item.value as CacheValue + } + + public async set( + agentContext: AgentContext, + key: string, + value: CacheValue, + expiresInSeconds?: number + ): Promise { + this.assertContextCorrelationId(agentContext) + + let expiresDate = undefined + + if (expiresInSeconds) { + expiresDate = new Date() + expiresDate.setSeconds(expiresDate.getSeconds() + expiresInSeconds) + } + + const cache = await this.getCache(agentContext) + this.removeExpiredItems(cache) + + cache.set(key, { + expiresAt: expiresDate?.getTime(), + value, + }) + await this.persistCache(agentContext) + } + + public async remove(agentContext: AgentContext, key: string): Promise { + this.assertContextCorrelationId(agentContext) + + const cache = await this.getCache(agentContext) + this.removeExpiredItems(cache) + cache.delete(key) + + await this.persistCache(agentContext) + } + + private async getCache(agentContext: AgentContext) { + if (!this._cache) { + const cacheRecord = await this.fetchCacheRecord(agentContext) + this._cache = this.lruFromRecord(cacheRecord) + } + + return this._cache + } + + private lruFromRecord(cacheRecord: SingleContextLruCacheRecord) { + return new LRUMap(this.limit, cacheRecord.entries.entries()) + } + + private async fetchCacheRecord(agentContext: AgentContext) { + const cacheRepository = agentContext.dependencyManager.resolve(SingleContextLruCacheRepository) + let cacheRecord = await cacheRepository.findById(agentContext, CONTEXT_STORAGE_LRU_CACHE_ID) + + if (!cacheRecord) { + cacheRecord = new SingleContextLruCacheRecord({ + id: CONTEXT_STORAGE_LRU_CACHE_ID, + entries: new Map(), + }) + + await cacheRepository.save(agentContext, cacheRecord) + } + + return cacheRecord + } + + private removeExpiredItems(cache: LRUMap) { + cache.forEach((value, key) => { + if (value.expiresAt && Date.now() > value.expiresAt) { + cache.delete(key) + } + }) + } + + private async persistCache(agentContext: AgentContext) { + const cacheRepository = agentContext.dependencyManager.resolve(SingleContextLruCacheRepository) + const cache = await this.getCache(agentContext) + + await cacheRepository.update( + agentContext, + new SingleContextLruCacheRecord({ + entries: new Map(cache.toJSON().map(({ key, value }) => [key, value])), + id: CONTEXT_STORAGE_LRU_CACHE_ID, + }) + ) + } + + /** + * Asserts this class is not used with multiple agent context instances. + */ + private assertContextCorrelationId(agentContext: AgentContext) { + if (!this._contextCorrelationId) { + this._contextCorrelationId = agentContext.contextCorrelationId + } + + if (this._contextCorrelationId !== agentContext.contextCorrelationId) { + throw new AriesFrameworkError( + 'SingleContextStorageLruCache can not be used with multiple agent context instances. Register a custom cache implementation in the CacheModule.' + ) + } + } +} diff --git a/packages/core/src/modules/cache/singleContextLruCache/__tests__/SingleContextStorageLruCache.test.ts b/packages/core/src/modules/cache/singleContextLruCache/__tests__/SingleContextStorageLruCache.test.ts new file mode 100644 index 0000000000..2251b9b854 --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/__tests__/SingleContextStorageLruCache.test.ts @@ -0,0 +1,91 @@ +import { getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { SingleContextLruCacheRecord } from '../SingleContextLruCacheRecord' +import { SingleContextLruCacheRepository } from '../SingleContextLruCacheRepository' +import { SingleContextStorageLruCache } from '../SingleContextStorageLruCache' + +jest.mock('../SingleContextLruCacheRepository') +const SingleContextLruCacheRepositoryMock = + SingleContextLruCacheRepository as jest.Mock + +const cacheRepository = new SingleContextLruCacheRepositoryMock() +const agentContext = getAgentContext({ + registerInstances: [[SingleContextLruCacheRepository, cacheRepository]], +}) + +describe('SingleContextLruCache', () => { + let cache: SingleContextStorageLruCache + + beforeEach(() => { + mockFunction(cacheRepository.findById).mockResolvedValue(null) + cache = new SingleContextStorageLruCache({ limit: 2 }) + }) + + it('should return the value from the persisted record', async () => { + const findMock = mockFunction(cacheRepository.findById).mockResolvedValue( + new SingleContextLruCacheRecord({ + id: 'CONTEXT_STORAGE_LRU_CACHE_ID', + entries: new Map([ + [ + 'test', + { + value: 'somevalue', + }, + ], + ]), + }) + ) + + expect(await cache.get(agentContext, 'doesnotexist')).toBeNull() + expect(await cache.get(agentContext, 'test')).toBe('somevalue') + expect(findMock).toHaveBeenCalledWith(agentContext, 'CONTEXT_STORAGE_LRU_CACHE_ID') + }) + + it('should set the value in the persisted record', async () => { + const updateMock = mockFunction(cacheRepository.update).mockResolvedValue() + + await cache.set(agentContext, 'test', 'somevalue') + const [[, cacheRecord]] = updateMock.mock.calls + + expect(cacheRecord.entries.size).toBe(1) + + const [[key, item]] = cacheRecord.entries.entries() + expect(key).toBe('test') + expect(item.value).toBe('somevalue') + + expect(await cache.get(agentContext, 'test')).toBe('somevalue') + }) + + it('should remove least recently used entries if entries are added that exceed the limit', async () => { + // Set first value in cache, resolves fine + await cache.set(agentContext, 'one', 'valueone') + expect(await cache.get(agentContext, 'one')).toBe('valueone') + + // Set two more entries in the cache. Third item + // exceeds limit, so first item gets removed + await cache.set(agentContext, 'two', 'valuetwo') + await cache.set(agentContext, 'three', 'valuethree') + expect(await cache.get(agentContext, 'one')).toBeNull() + expect(await cache.get(agentContext, 'two')).toBe('valuetwo') + expect(await cache.get(agentContext, 'three')).toBe('valuethree') + + // Get two from the cache, meaning three will be removed first now + // because it is not recently used + await cache.get(agentContext, 'two') + await cache.set(agentContext, 'four', 'valuefour') + expect(await cache.get(agentContext, 'three')).toBeNull() + expect(await cache.get(agentContext, 'two')).toBe('valuetwo') + }) + + it('should throw an error if used with multiple context correlation ids', async () => { + // No issue, first call with an agentContext + await cache.get(agentContext, 'test') + + const secondAgentContext = getAgentContext({ + contextCorrelationId: 'another', + }) + + expect(cache.get(secondAgentContext, 'test')).rejects.toThrowError( + 'SingleContextStorageLruCache can not be used with multiple agent context instances. Register a custom cache implementation in the CacheModule.' + ) + }) +}) diff --git a/packages/core/src/modules/cache/singleContextLruCache/index.ts b/packages/core/src/modules/cache/singleContextLruCache/index.ts new file mode 100644 index 0000000000..4d01549062 --- /dev/null +++ b/packages/core/src/modules/cache/singleContextLruCache/index.ts @@ -0,0 +1 @@ +export { SingleContextStorageLruCache, SingleContextStorageLruCacheOptions } from './SingleContextStorageLruCache' diff --git a/packages/core/src/modules/ledger/__tests__/IndyPoolService.test.ts b/packages/core/src/modules/ledger/__tests__/IndyPoolService.test.ts index 073d08686f..b34d2b6fcf 100644 --- a/packages/core/src/modules/ledger/__tests__/IndyPoolService.test.ts +++ b/packages/core/src/modules/ledger/__tests__/IndyPoolService.test.ts @@ -1,26 +1,23 @@ import type { AgentContext } from '../../../agent' +import type { Cache } from '../../cache' import type { IndyPoolConfig } from '../IndyPool' import type { CachedDidResponse } from '../services/IndyPoolService' import { Subject } from 'rxjs' import { NodeFileSystem } from '../../../../../node/src/NodeFileSystem' -import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../../tests/helpers' -import { CacheRecord } from '../../../cache' -import { CacheRepository } from '../../../cache/CacheRepository' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../tests/helpers' import { SigningProviderRegistry } from '../../../crypto/signing-provider' import { AriesFrameworkError } from '../../../error/AriesFrameworkError' import { IndyWallet } from '../../../wallet/IndyWallet' +import { CacheModuleConfig, InMemoryLruCache } from '../../cache' import { LedgerError } from '../error/LedgerError' import { LedgerNotConfiguredError } from '../error/LedgerNotConfiguredError' import { LedgerNotFoundError } from '../error/LedgerNotFoundError' -import { DID_POOL_CACHE_ID, IndyPoolService } from '../services/IndyPoolService' +import { IndyPoolService } from '../services/IndyPoolService' import { getDidResponsesForDid } from './didResponses' -jest.mock('../../../cache/CacheRepository') -const CacheRepositoryMock = CacheRepository as jest.Mock - const pools: IndyPoolConfig[] = [ { id: 'sovrinMain', @@ -66,11 +63,11 @@ describe('IndyPoolService', () => { let agentContext: AgentContext let wallet: IndyWallet let poolService: IndyPoolService - let cacheRepository: CacheRepository + let cache: Cache beforeAll(async () => { wallet = new IndyWallet(config.agentDependencies, config.logger, new SigningProviderRegistry([])) - agentContext = getAgentContext() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await wallet.createAndOpen(config.walletConfig!) }) @@ -80,16 +77,11 @@ describe('IndyPoolService', () => { }) beforeEach(async () => { - cacheRepository = new CacheRepositoryMock() - mockFunction(cacheRepository.findById).mockResolvedValue(null) - - poolService = new IndyPoolService( - cacheRepository, - agentDependencies, - config.logger, - new Subject(), - new NodeFileSystem() - ) + cache = new InMemoryLruCache({ limit: 200 }) + agentContext = getAgentContext({ + registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]], + }) + poolService = new IndyPoolService(agentDependencies, config.logger, new Subject(), new NodeFileSystem()) poolService.setPools(pools) }) @@ -242,20 +234,7 @@ describe('IndyPoolService', () => { poolId: expectedPool.id, } - const cachedEntries = [ - { - key: did, - value: didResponse, - }, - ] - - mockFunction(cacheRepository.findById).mockResolvedValue( - new CacheRecord({ - id: DID_POOL_CACHE_ID, - entries: cachedEntries, - }) - ) - + await cache.set(agentContext, `IndySdkPoolService:${did}`, didResponse) const { pool } = await poolService.getPoolForDid(agentContext, did) expect(pool.config.id).toBe(pool.id) @@ -268,15 +247,6 @@ describe('IndyPoolService', () => { sovrinBuilder: '~M9kv2Ez61cur7X39DXWh8W', }) - mockFunction(cacheRepository.findById).mockResolvedValue( - new CacheRecord({ - id: DID_POOL_CACHE_ID, - entries: [], - }) - ) - - const spy = mockFunction(cacheRepository.update).mockResolvedValue() - poolService.pools.forEach((pool, index) => { const spy = jest.spyOn(pool, 'submitReadRequest') spy.mockImplementationOnce(responses[index]) @@ -287,10 +257,7 @@ describe('IndyPoolService', () => { expect(pool.config.id).toBe('sovrinBuilder') expect(pool.config.indyNamespace).toBe('sovrin:builder') - const cacheRecord = spy.mock.calls[0][1] - expect(cacheRecord.entries.length).toBe(1) - expect(cacheRecord.entries[0].key).toBe(did) - expect(cacheRecord.entries[0].value).toEqual({ + expect(await cache.get(agentContext, `IndySdkPoolService:${did}`)).toEqual({ nymResponse: { did, verkey: '~M9kv2Ez61cur7X39DXWh8W', diff --git a/packages/core/src/modules/ledger/services/IndyPoolService.ts b/packages/core/src/modules/ledger/services/IndyPoolService.ts index dd5095b08d..172d1febd1 100644 --- a/packages/core/src/modules/ledger/services/IndyPoolService.ts +++ b/packages/core/src/modules/ledger/services/IndyPoolService.ts @@ -5,7 +5,6 @@ import type { default as Indy, LedgerReadReplyResponse, LedgerRequest, LedgerWri import { Subject } from 'rxjs' import { AgentDependencies } from '../../../agent/AgentDependencies' -import { CacheRepository, PersistedLruCache } from '../../../cache' import { InjectionSymbols } from '../../../constants' import { IndySdkError } from '../../../error/IndySdkError' import { Logger } from '../../../logger/Logger' @@ -15,17 +14,17 @@ import { isSelfCertifiedDid } from '../../../utils/did' import { isIndyError } from '../../../utils/indyError' import { allSettled, onlyFulfilled, onlyRejected } from '../../../utils/promises' import { assertIndyWallet } from '../../../wallet/util/assertIndyWallet' +import { CacheModuleConfig } from '../../cache' import { IndyPool } from '../IndyPool' import { LedgerError } from '../error/LedgerError' import { LedgerNotConfiguredError } from '../error/LedgerNotConfiguredError' import { LedgerNotFoundError } from '../error/LedgerNotFoundError' -export const DID_POOL_CACHE_ID = 'DID_POOL_CACHE' -export const DID_POOL_CACHE_LIMIT = 500 export interface CachedDidResponse { nymResponse: Indy.GetNymResponse poolId: string } + @injectable() export class IndyPoolService { public pools: IndyPool[] = [] @@ -34,10 +33,8 @@ export class IndyPoolService { private agentDependencies: AgentDependencies private stop$: Subject private fileSystem: FileSystem - private didCache: PersistedLruCache public constructor( - cacheRepository: CacheRepository, @inject(InjectionSymbols.AgentDependencies) agentDependencies: AgentDependencies, @inject(InjectionSymbols.Logger) logger: Logger, @inject(InjectionSymbols.Stop$) stop$: Subject, @@ -48,8 +45,6 @@ export class IndyPoolService { this.agentDependencies = agentDependencies this.fileSystem = fileSystem this.stop$ = stop$ - - this.didCache = new PersistedLruCache(DID_POOL_CACHE_ID, DID_POOL_CACHE_LIMIT, cacheRepository) } public setPools(poolConfigs: IndyPoolConfig[]) { @@ -104,7 +99,9 @@ export class IndyPoolService { ) } - const cachedNymResponse = await this.didCache.get(agentContext, did) + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + + const cachedNymResponse = await cache.get(agentContext, `IndySdkPoolService:${did}`) const pool = this.pools.find((pool) => pool.id === cachedNymResponse?.poolId) // If we have the nym response with associated pool in the cache, we'll use that @@ -151,7 +148,7 @@ export class IndyPoolService { value = productionOrNonProduction[0].value } - await this.didCache.set(agentContext, did, { + await cache.set(agentContext, `IndySdkPoolService:${did}`, { nymResponse: value.did, poolId: value.pool.id, }) diff --git a/packages/indy-sdk/jest.config.ts b/packages/indy-sdk/jest.config.ts index c7c5196637..55c67d70a6 100644 --- a/packages/indy-sdk/jest.config.ts +++ b/packages/indy-sdk/jest.config.ts @@ -8,7 +8,7 @@ const config: Config.InitialOptions = { ...base, name: packageJson.name, displayName: packageJson.name, - // setupFilesAfterEnv: ['./tests/setup.ts'], + setupFilesAfterEnv: ['./tests/setup.ts'], } export default config diff --git a/packages/indy-sdk/src/ledger/IndySdkPoolService.ts b/packages/indy-sdk/src/ledger/IndySdkPoolService.ts index 773b7db2cc..9d237fd336 100644 --- a/packages/indy-sdk/src/ledger/IndySdkPoolService.ts +++ b/packages/indy-sdk/src/ledger/IndySdkPoolService.ts @@ -2,15 +2,7 @@ import type { AcceptanceMechanisms, AuthorAgreement, IndySdkPoolConfig } from '. import type { AgentContext } from '@aries-framework/core' import type { GetNymResponse, LedgerReadReplyResponse, LedgerRequest, LedgerWriteReplyResponse } from 'indy-sdk' -import { - InjectionSymbols, - Logger, - injectable, - inject, - FileSystem, - CacheRepository, - PersistedLruCache, -} from '@aries-framework/core' +import { CacheModuleConfig, InjectionSymbols, Logger, injectable, inject, FileSystem } from '@aries-framework/core' import { Subject } from 'rxjs' import { IndySdkError, isIndyError } from '../error' @@ -22,8 +14,6 @@ import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' import { IndySdkPool } from './IndySdkPool' import { IndySdkPoolError, IndySdkPoolNotConfiguredError, IndySdkPoolNotFoundError } from './error' -export const INDY_SDK_DID_POOL_CACHE_ID = 'INDY_SDK_DID_POOL_CACHE' -export const INDY_SDK_DID_POOL_CACHE_LIMIT = 500 export interface CachedDidResponse { nymResponse: GetNymResponse poolId: string @@ -36,10 +26,8 @@ export class IndySdkPoolService { private indySdk: IndySdk private stop$: Subject private fileSystem: FileSystem - private didCache: PersistedLruCache public constructor( - cacheRepository: CacheRepository, indySdk: IndySdk, @inject(InjectionSymbols.Logger) logger: Logger, @inject(InjectionSymbols.Stop$) stop$: Subject, @@ -49,8 +37,6 @@ export class IndySdkPoolService { this.indySdk = indySdk this.fileSystem = fileSystem this.stop$ = stop$ - - this.didCache = new PersistedLruCache(INDY_SDK_DID_POOL_CACHE_ID, INDY_SDK_DID_POOL_CACHE_LIMIT, cacheRepository) } public setPools(poolConfigs: IndySdkPoolConfig[]) { @@ -90,7 +76,8 @@ export class IndySdkPoolService { ) } - const cachedNymResponse = await this.didCache.get(agentContext, did) + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + const cachedNymResponse = await cache.get(agentContext, `IndySdkPoolService:${did}`) const pool = this.pools.find((pool) => pool.id === cachedNymResponse?.poolId) // If we have the nym response with associated pool in the cache, we'll use that @@ -137,7 +124,7 @@ export class IndySdkPoolService { value = productionOrNonProduction[0].value } - await this.didCache.set(agentContext, did, { + await cache.set(agentContext, `IndySdkPoolService:${did}`, { nymResponse: value.did, poolId: value.pool.id, }) diff --git a/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts b/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts index 74debe2656..2ef487e566 100644 --- a/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts +++ b/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts @@ -1,22 +1,22 @@ import type { IndySdkPoolConfig } from '../IndySdkPool' import type { CachedDidResponse } from '../IndySdkPoolService' -import type { AgentContext } from '@aries-framework/core' - -import { SigningProviderRegistry, AriesFrameworkError } from '@aries-framework/core' +import type { AgentContext, Cache } from '@aries-framework/core' + +import { + CacheModuleConfig, + InMemoryLruCache, + SigningProviderRegistry, + AriesFrameworkError, +} from '@aries-framework/core' import { Subject } from 'rxjs' -import { CacheRecord } from '../../../../core/src/cache' -import { CacheRepository } from '../../../../core/src/cache/CacheRepository' import { getDidResponsesForDid } from '../../../../core/src/modules/ledger/__tests__/didResponses' -import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' import { NodeFileSystem } from '../../../../node/src/NodeFileSystem' import { IndySdkWallet } from '../../wallet/IndySdkWallet' -import { INDY_SDK_DID_POOL_CACHE_ID, IndySdkPoolService } from '../IndySdkPoolService' +import { IndySdkPoolService } from '../IndySdkPoolService' import { IndySdkPoolError, IndySdkPoolNotConfiguredError, IndySdkPoolNotFoundError } from '../error' -jest.mock('../../../../core/src/cache/CacheRepository') -const CacheRepositoryMock = CacheRepository as jest.Mock - const pools: IndySdkPoolConfig[] = [ { id: 'sovrinMain', @@ -62,11 +62,10 @@ describe('IndySdkPoolService', () => { let agentContext: AgentContext let wallet: IndySdkWallet let poolService: IndySdkPoolService - let cacheRepository: CacheRepository + let cache: Cache beforeAll(async () => { wallet = new IndySdkWallet(config.agentDependencies.indy, config.logger, new SigningProviderRegistry([])) - agentContext = getAgentContext() // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await wallet.createAndOpen(config.walletConfig!) }) @@ -76,11 +75,11 @@ describe('IndySdkPoolService', () => { }) beforeEach(async () => { - cacheRepository = new CacheRepositoryMock() - mockFunction(cacheRepository.findById).mockResolvedValue(null) - + cache = new InMemoryLruCache({ limit: 200 }) + agentContext = getAgentContext({ + registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]], + }) poolService = new IndySdkPoolService( - cacheRepository, agentDependencies.indy, config.logger, new Subject(), @@ -226,20 +225,7 @@ describe('IndySdkPoolService', () => { poolId: expectedPool.id, } - const cachedEntries = [ - { - key: did, - value: didResponse, - }, - ] - - mockFunction(cacheRepository.findById).mockResolvedValue( - new CacheRecord({ - id: INDY_SDK_DID_POOL_CACHE_ID, - entries: cachedEntries, - }) - ) - + await cache.set(agentContext, `IndySdkPoolService:${did}`, didResponse) const { pool } = await poolService.getPoolForDid(agentContext, did) expect(pool.config.id).toBe(pool.id) @@ -252,15 +238,6 @@ describe('IndySdkPoolService', () => { sovrinBuilder: '~M9kv2Ez61cur7X39DXWh8W', }) - mockFunction(cacheRepository.findById).mockResolvedValue( - new CacheRecord({ - id: INDY_SDK_DID_POOL_CACHE_ID, - entries: [], - }) - ) - - const spy = mockFunction(cacheRepository.update).mockResolvedValue() - poolService.pools.forEach((pool, index) => { const spy = jest.spyOn(pool, 'submitReadRequest') spy.mockImplementationOnce(responses[index]) @@ -271,10 +248,7 @@ describe('IndySdkPoolService', () => { expect(pool.config.id).toBe('sovrinBuilder') expect(pool.config.indyNamespace).toBe('sovrin:builder') - const cacheRecord = spy.mock.calls[0][1] - expect(cacheRecord.entries.length).toBe(1) - expect(cacheRecord.entries[0].key).toBe(did) - expect(cacheRecord.entries[0].value).toEqual({ + expect(await cache.get(agentContext, `IndySdkPoolService:${did}`)).toEqual({ nymResponse: { did, verkey: '~M9kv2Ez61cur7X39DXWh8W', diff --git a/packages/indy-sdk/tests/setup.ts b/packages/indy-sdk/tests/setup.ts new file mode 100644 index 0000000000..719a473b6e --- /dev/null +++ b/packages/indy-sdk/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(10000) From 0f6d2312471efab20f560782c171434f907b6b9d Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sat, 21 Jan 2023 04:07:21 +0800 Subject: [PATCH 02/20] feat(proofs): sort credentials based on revocation (#1225) Signed-off-by: Timo Glastra --- .../formats/indy/IndyProofFormatService.ts | 63 ++++++++++--------- .../sortRequestedCredentials.test.ts | 48 ++++++++++++++ .../indy/util/sortRequestedCredentials.ts | 33 ++++++++++ 3 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/modules/proofs/formats/indy/util/__tests__/sortRequestedCredentials.test.ts create mode 100644 packages/core/src/modules/proofs/formats/indy/util/sortRequestedCredentials.ts diff --git a/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts b/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts index ecdb78f358..9730e6aa8d 100644 --- a/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts @@ -62,6 +62,7 @@ import { import { ProofRequest } from './models/ProofRequest' import { RequestedCredentials } from './models/RequestedCredentials' import { RetrievedCredentials } from './models/RetrievedCredentials' +import { sortRequestedCredentials } from './util/sortRequestedCredentials' @scoped(Lifecycle.ContainerScoped) export class IndyProofFormatService extends ProofFormatService { @@ -424,22 +425,24 @@ export class IndyProofFormatService extends ProofFormatService { }) } - retrievedCredentials.requestedAttributes[referent] = await Promise.all( - credentialMatch.map(async (credential: IndyCredential) => { - const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem(agentContext, { - proofRequest, - requestedItem: requestedAttribute, - credential, + retrievedCredentials.requestedAttributes[referent] = sortRequestedCredentials( + await Promise.all( + credentialMatch.map(async (credential: IndyCredential) => { + const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem(agentContext, { + proofRequest, + requestedItem: requestedAttribute, + credential, + }) + + return new RequestedAttribute({ + credentialId: credential.credentialInfo.referent, + revealed: true, + credentialInfo: credential.credentialInfo, + timestamp: deltaTimestamp, + revoked, + }) }) - - return new RequestedAttribute({ - credentialId: credential.credentialInfo.referent, - revealed: true, - credentialInfo: credential.credentialInfo, - timestamp: deltaTimestamp, - revoked, - }) - }) + ) ) // We only attach revoked state if non-revocation is requested. So if revoked is true it means @@ -454,21 +457,23 @@ export class IndyProofFormatService extends ProofFormatService { for (const [referent, requestedPredicate] of proofRequest.requestedPredicates.entries()) { const credentials = await this.getCredentialsForProofRequest(agentContext, proofRequest, referent) - retrievedCredentials.requestedPredicates[referent] = await Promise.all( - credentials.map(async (credential) => { - const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem(agentContext, { - proofRequest, - requestedItem: requestedPredicate, - credential, + retrievedCredentials.requestedPredicates[referent] = sortRequestedCredentials( + await Promise.all( + credentials.map(async (credential) => { + const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem(agentContext, { + proofRequest, + requestedItem: requestedPredicate, + credential, + }) + + return new RequestedPredicate({ + credentialId: credential.credentialInfo.referent, + credentialInfo: credential.credentialInfo, + timestamp: deltaTimestamp, + revoked, + }) }) - - return new RequestedPredicate({ - credentialId: credential.credentialInfo.referent, - credentialInfo: credential.credentialInfo, - timestamp: deltaTimestamp, - revoked, - }) - }) + ) ) // We only attach revoked state if non-revocation is requested. So if revoked is true it means diff --git a/packages/core/src/modules/proofs/formats/indy/util/__tests__/sortRequestedCredentials.test.ts b/packages/core/src/modules/proofs/formats/indy/util/__tests__/sortRequestedCredentials.test.ts new file mode 100644 index 0000000000..117fe2b898 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/indy/util/__tests__/sortRequestedCredentials.test.ts @@ -0,0 +1,48 @@ +import { RequestedAttribute } from '../../models' +import { sortRequestedCredentials } from '../sortRequestedCredentials' + +const credentials = [ + new RequestedAttribute({ + credentialId: '1', + revealed: true, + revoked: true, + }), + new RequestedAttribute({ + credentialId: '2', + revealed: true, + revoked: undefined, + }), + new RequestedAttribute({ + credentialId: '3', + revealed: true, + revoked: false, + }), + new RequestedAttribute({ + credentialId: '4', + revealed: true, + revoked: false, + }), + new RequestedAttribute({ + credentialId: '5', + revealed: true, + revoked: true, + }), + new RequestedAttribute({ + credentialId: '6', + revealed: true, + revoked: undefined, + }), +] + +describe('sortRequestedCredentials', () => { + test('sorts the credentials', () => { + expect(sortRequestedCredentials(credentials)).toEqual([ + credentials[1], + credentials[5], + credentials[2], + credentials[3], + credentials[0], + credentials[4], + ]) + }) +}) diff --git a/packages/core/src/modules/proofs/formats/indy/util/sortRequestedCredentials.ts b/packages/core/src/modules/proofs/formats/indy/util/sortRequestedCredentials.ts new file mode 100644 index 0000000000..2db1deb0b9 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/indy/util/sortRequestedCredentials.ts @@ -0,0 +1,33 @@ +import type { RequestedAttribute, RequestedPredicate } from '../models' + +/** + * Sort requested attributes and predicates by `revoked` status. The order is: + * - first credentials with `revoked` set to undefined, this means no revocation status is needed for the credentials + * - then credentials with `revoked` set to false, this means the credentials are not revoked + * - then credentials with `revoked` set to true, this means the credentials are revoked + */ +export function sortRequestedCredentials | Array>( + credentials: Requested +) { + const staySame = 0 + const credentialGoUp = -1 + const credentialGoDown = 1 + + // Clone as sort is in place + const credentialsClone = [...credentials] + + return credentialsClone.sort((credential, compareTo) => { + // Nothing needs to happen if values are the same + if (credential.revoked === compareTo.revoked) return staySame + + // Undefined always is at the top + if (credential.revoked === undefined) return credentialGoUp + if (compareTo.revoked === undefined) return credentialGoDown + + // Then revoked + if (credential.revoked === false) return credentialGoUp + + // It means that compareTo is false and credential is true + return credentialGoDown + }) +} From b6ae94825696034e51969e70d405513a9ffe84f7 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Thu, 26 Jan 2023 12:06:45 +0100 Subject: [PATCH 03/20] chore: deprecate injectionContainer on agent (#1241) Signed-off-by: Timo Glastra --- .../tests/v2.ldproof.credentials.propose-offerBbs.test.ts | 2 +- packages/core/src/agent/BaseAgent.ts | 3 +++ .../v2.ldproof.connectionless-credentials.test.ts | 2 +- .../__tests__/v2.ldproof.credentials-auto-accept.test.ts | 4 ++-- .../v2.ldproof.credentials.propose-offerED25519.test.ts | 3 +-- .../src/modules/dids/__tests__/dids-registrar.e2e.test.ts | 2 +- .../protocol/v1/__tests__/indy-proof-negotiation.test.ts | 8 ++++---- .../protocol/v1/__tests__/indy-proof-presentation.test.ts | 4 ++-- .../protocol/v1/__tests__/indy-proof-proposal.test.ts | 2 +- .../protocol/v1/__tests__/indy-proof-request.test.ts | 4 ++-- .../protocol/v2/__tests__/indy-proof-negotiation.test.ts | 8 ++++---- .../protocol/v2/__tests__/indy-proof-presentation.test.ts | 4 ++-- .../protocol/v2/__tests__/indy-proof-proposal.test.ts | 2 +- .../protocol/v2/__tests__/indy-proof-request.test.ts | 4 ++-- packages/core/src/storage/migration/__tests__/0.1.test.ts | 8 ++++---- packages/core/src/storage/migration/__tests__/0.2.test.ts | 6 +++--- packages/core/src/storage/migration/__tests__/0.3.test.ts | 2 +- .../core/src/storage/migration/__tests__/backup.test.ts | 4 ++-- packages/core/tests/v1-indy-proofs.test.ts | 6 +++--- packages/core/tests/v2-indy-proofs.test.ts | 2 +- packages/node/src/transport/WsInboundTransport.ts | 2 +- tests/transport/SubjectOutboundTransport.ts | 2 +- 22 files changed, 43 insertions(+), 41 deletions(-) diff --git a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts index 4569b44631..997c73c18b 100644 --- a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts +++ b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts @@ -35,7 +35,7 @@ describeSkipNode17And18('credentials, BBS+ signature', () => { 'Faber Agent Credentials LD BBS+', 'Alice Agent Credentials LD BBS+' )) - wallet = faberAgent.injectionContainer.resolve(InjectionSymbols.Wallet) + wallet = faberAgent.dependencyManager.resolve(InjectionSymbols.Wallet) await wallet.createKey({ keyType: KeyType.Ed25519, seed }) const key = await wallet.createKey({ keyType: KeyType.Bls12381g2, seed }) diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index 606b586a67..5d47157f59 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -194,6 +194,9 @@ export abstract class BaseAgent { aliceAgent.events .observable(CredentialEventTypes.CredentialStateChanged) .subscribe(aliceReplay) - wallet = faberAgent.injectionContainer.resolve(InjectionSymbols.Wallet) + wallet = faberAgent.dependencyManager.resolve(InjectionSymbols.Wallet) await wallet.createKey({ seed, keyType: KeyType.Ed25519 }) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts index a9bf76f0f5..ad08852a17 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts @@ -43,7 +43,7 @@ describe('credentials', () => { AutoAcceptCredential.Always )) - wallet = faberAgent.injectionContainer.resolve(InjectionSymbols.Wallet) + wallet = faberAgent.dependencyManager.resolve(InjectionSymbols.Wallet) await wallet.createKey({ seed, keyType: KeyType.Ed25519 }) signCredentialOptions = { credential: TEST_LD_DOCUMENT, @@ -142,7 +142,7 @@ describe('credentials', () => { 'alice agent: content-approved v2 jsonld', AutoAcceptCredential.ContentApproved )) - wallet = faberAgent.injectionContainer.resolve(InjectionSymbols.Wallet) + wallet = faberAgent.dependencyManager.resolve(InjectionSymbols.Wallet) await wallet.createKey({ seed, keyType: KeyType.Ed25519 }) signCredentialOptions = { credential: TEST_LD_DOCUMENT, diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts index 3d4d757554..c8f9a64d20 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts @@ -65,7 +65,7 @@ describe('credentials', () => { 'Faber Agent Credentials LD', 'Alice Agent Credentials LD' )) - wallet = faberAgent.injectionContainer.resolve(InjectionSymbols.Wallet) + wallet = faberAgent.dependencyManager.resolve(InjectionSymbols.Wallet) await wallet.createKey({ seed, keyType: KeyType.Ed25519 }) signCredentialOptions = { credential: inputDocAsJson, @@ -312,7 +312,6 @@ describe('credentials', () => { threadId: faberCredentialRecord.threadId, state: CredentialState.OfferReceived, }) - // didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const offerMessage = await didCommMessageRepository.findAgentMessage(faberAgent.context, { diff --git a/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts b/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts index f6d6763a4f..9599d06c92 100644 --- a/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts +++ b/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts @@ -177,7 +177,7 @@ describe('dids', () => { const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyEd25519) const indyDid = indyDidFromPublicKeyBase58(ed25519PublicKeyBase58) - const wallet = agent.injectionContainer.resolve(InjectionSymbols.Wallet) + const wallet = agent.dependencyManager.resolve(InjectionSymbols.Wallet) // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain, @typescript-eslint/no-non-null-assertion const submitterDid = `did:sov:${wallet.publicDid?.did!}` diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-negotiation.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-negotiation.test.ts index 19134854a8..ee7b481cbb 100644 --- a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-negotiation.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-negotiation.test.ts @@ -66,7 +66,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) let proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -159,7 +159,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) let request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -212,7 +212,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -266,7 +266,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts index 975a6aed43..8b32bbe14c 100644 --- a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts @@ -60,7 +60,7 @@ describe('Present Proof', () => { faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -118,7 +118,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts index e2b2df04ff..606f1e7ff8 100644 --- a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts @@ -58,7 +58,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts index 130f5cf04a..c3bccd9f9b 100644 --- a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts @@ -60,7 +60,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -120,7 +120,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-negotiation.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-negotiation.test.ts index 7ebe83cb32..a337eca475 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-negotiation.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-negotiation.test.ts @@ -69,7 +69,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) let proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -188,7 +188,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) let request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -241,7 +241,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -319,7 +319,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts index 08d5978d80..a8f2d6a531 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts @@ -66,7 +66,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberPresentationRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -118,7 +118,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await alicePresentationRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts index 0aed8af01c..39a29df125 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts @@ -59,7 +59,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberPresentationRecord = await faberPresentationRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberPresentationRecord.id, diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts index d30a0c02b9..47a697821f 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts @@ -62,7 +62,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberPresentationRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -114,7 +114,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for proof request from Faber') aliceProofExchangeRecord = await alicePresentationRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/core/src/storage/migration/__tests__/0.1.test.ts b/packages/core/src/storage/migration/__tests__/0.1.test.ts index 139b73024a..ad2dd0b837 100644 --- a/packages/core/src/storage/migration/__tests__/0.1.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.1.test.ts @@ -48,7 +48,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { dependencyManager ) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { @@ -110,7 +110,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { dependencyManager ) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { @@ -174,7 +174,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { dependencyManager ) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { @@ -242,7 +242,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { dependencyManager ) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { diff --git a/packages/core/src/storage/migration/__tests__/0.2.test.ts b/packages/core/src/storage/migration/__tests__/0.2.test.ts index b67e361855..08ed9dce64 100644 --- a/packages/core/src/storage/migration/__tests__/0.2.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.2.test.ts @@ -46,7 +46,7 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { dependencyManager ) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { @@ -119,7 +119,7 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { dependencyManager ) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) // We need to manually initialize the wallet as we're using the in memory wallet service // When we call agent.initialize() it will create the wallet and store the current framework @@ -170,7 +170,7 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { dependencyManager ) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) // We need to manually initialize the wallet as we're using the in memory wallet service // When we call agent.initialize() it will create the wallet and store the current framework diff --git a/packages/core/src/storage/migration/__tests__/0.3.test.ts b/packages/core/src/storage/migration/__tests__/0.3.test.ts index a7ec0f6adb..b797fc7c97 100644 --- a/packages/core/src/storage/migration/__tests__/0.3.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.3.test.ts @@ -43,7 +43,7 @@ describe('UpdateAssistant | v0.3 - v0.3.1', () => { dependencyManager ) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { diff --git a/packages/core/src/storage/migration/__tests__/backup.test.ts b/packages/core/src/storage/migration/__tests__/backup.test.ts index b1efe9b580..73aba5823f 100644 --- a/packages/core/src/storage/migration/__tests__/backup.test.ts +++ b/packages/core/src/storage/migration/__tests__/backup.test.ts @@ -81,7 +81,7 @@ describe('UpdateAssistant | Backup', () => { // Expect an update is needed expect(await updateAssistant.isUpToDate()).toBe(false) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) // Backup should not exist before update expect(await fileSystem.exists(backupPath)).toBe(false) @@ -128,7 +128,7 @@ describe('UpdateAssistant | Backup', () => { }, ]) - const fileSystem = agent.injectionContainer.resolve(InjectionSymbols.FileSystem) + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) // Backup should not exist before update expect(await fileSystem.exists(backupPath)).toBe(false) diff --git a/packages/core/tests/v1-indy-proofs.test.ts b/packages/core/tests/v1-indy-proofs.test.ts index 81b523d659..440da6a5d4 100644 --- a/packages/core/tests/v1-indy-proofs.test.ts +++ b/packages/core/tests/v1-indy-proofs.test.ts @@ -71,7 +71,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for a presentation proposal from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -390,7 +390,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, @@ -615,7 +615,7 @@ describe('Present Proof', () => { testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const request = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/core/tests/v2-indy-proofs.test.ts b/packages/core/tests/v2-indy-proofs.test.ts index 9be131c559..f54f6da15e 100644 --- a/packages/core/tests/v2-indy-proofs.test.ts +++ b/packages/core/tests/v2-indy-proofs.test.ts @@ -78,7 +78,7 @@ describe('Present Proof', () => { testLogger.test('Faber waits for a presentation proposal from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(faberAgent.context, { associatedRecordId: faberProofExchangeRecord.id, diff --git a/packages/node/src/transport/WsInboundTransport.ts b/packages/node/src/transport/WsInboundTransport.ts index 2fa23c6168..0ccda783ba 100644 --- a/packages/node/src/transport/WsInboundTransport.ts +++ b/packages/node/src/transport/WsInboundTransport.ts @@ -58,7 +58,7 @@ export class WsInboundTransport implements InboundTransport { } private listenOnWebSocketMessages(agent: Agent, socket: WebSocket, session: TransportSession) { - const messageReceiver = agent.injectionContainer.resolve(MessageReceiver) + const messageReceiver = agent.dependencyManager.resolve(MessageReceiver) // eslint-disable-next-line @typescript-eslint/no-explicit-any socket.addEventListener('message', async (event: any) => { diff --git a/tests/transport/SubjectOutboundTransport.ts b/tests/transport/SubjectOutboundTransport.ts index 16868df737..44f64555af 100644 --- a/tests/transport/SubjectOutboundTransport.ts +++ b/tests/transport/SubjectOutboundTransport.ts @@ -29,7 +29,7 @@ export class SubjectOutboundTransport implements OutboundTransport { } public async sendMessage(outboundPackage: OutboundPackage) { - const messageReceiver = this.agent.injectionContainer.resolve(MessageReceiver) + const messageReceiver = this.agent.dependencyManager.resolve(MessageReceiver) this.logger.debug(`Sending outbound message to endpoint ${outboundPackage.endpoint}`, { endpoint: outboundPackage.endpoint, }) From e8d6ac31a8e18847d99d7998bd7658439e48875b Mon Sep 17 00:00:00 2001 From: Victor Anene <62852943+Vickysomtee@users.noreply.github.com> Date: Thu, 26 Jan 2023 20:51:18 +0100 Subject: [PATCH 04/20] feat(indy-vdr): add indy-vdr package and indy vdr pool (#1160) work funded by the Government of Ontario Signed-off-by: Victor Anene --- packages/core/tests/helpers.ts | 3 + packages/indy-vdr/README.md | 35 +++ packages/indy-vdr/jest.config.ts | 14 ++ packages/indy-vdr/package.json | 36 +++ packages/indy-vdr/src/error/IndyVdrError.ts | 7 + .../src/error/IndyVdrNotConfiguredError.ts | 7 + .../indy-vdr/src/error/IndyVdrNotFound.ts | 7 + packages/indy-vdr/src/error/index.ts | 3 + packages/indy-vdr/src/index.ts | 6 + packages/indy-vdr/src/pool/IndyVdrPool.ts | 184 +++++++++++++++ .../indy-vdr/src/pool/IndyVdrPoolService.ts | 209 ++++++++++++++++ packages/indy-vdr/src/pool/index.ts | 1 + packages/indy-vdr/src/utils/did.ts | 61 +++++ packages/indy-vdr/src/utils/promises.ts | 44 ++++ .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 223 ++++++++++++++++++ packages/indy-vdr/tests/setup.ts | 4 + packages/indy-vdr/tsconfig.build.json | 7 + packages/indy-vdr/tsconfig.json | 6 + yarn.lock | 96 +++++++- 19 files changed, 952 insertions(+), 1 deletion(-) create mode 100644 packages/indy-vdr/README.md create mode 100644 packages/indy-vdr/jest.config.ts create mode 100644 packages/indy-vdr/package.json create mode 100644 packages/indy-vdr/src/error/IndyVdrError.ts create mode 100644 packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts create mode 100644 packages/indy-vdr/src/error/IndyVdrNotFound.ts create mode 100644 packages/indy-vdr/src/error/index.ts create mode 100644 packages/indy-vdr/src/index.ts create mode 100644 packages/indy-vdr/src/pool/IndyVdrPool.ts create mode 100644 packages/indy-vdr/src/pool/IndyVdrPoolService.ts create mode 100644 packages/indy-vdr/src/pool/index.ts create mode 100644 packages/indy-vdr/src/utils/did.ts create mode 100644 packages/indy-vdr/src/utils/promises.ts create mode 100644 packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts create mode 100644 packages/indy-vdr/tests/setup.ts create mode 100644 packages/indy-vdr/tsconfig.build.json create mode 100644 packages/indy-vdr/tsconfig.json diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 9e57c21943..9718a60975 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -23,6 +23,7 @@ import type { Awaited } from '../src/types' import type { CredDef, Schema } from 'indy-sdk' import type { Observable } from 'rxjs' +import { readFileSync } from 'fs' import path from 'path' import { firstValueFrom, ReplaySubject, Subject } from 'rxjs' import { catchError, filter, map, timeout } from 'rxjs/operators' @@ -83,6 +84,8 @@ export const genesisPath = process.env.GENESIS_TXN_PATH ? path.resolve(process.env.GENESIS_TXN_PATH) : path.join(__dirname, '../../../network/genesis/local-genesis.txn') +export const genesisTransactions = readFileSync(genesisPath).toString('utf-8') + export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' const taaVersion = (process.env.TEST_AGENT_TAA_VERSION ?? '1') as `${number}.${number}` | `${number}` const taaAcceptanceMechanism = process.env.TEST_AGENT_TAA_ACCEPTANCE_MECHANISM ?? 'accept' diff --git a/packages/indy-vdr/README.md b/packages/indy-vdr/README.md new file mode 100644 index 0000000000..310b38a4f9 --- /dev/null +++ b/packages/indy-vdr/README.md @@ -0,0 +1,35 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript - Indy Verifiable Data Registry (Indy-Vdr)

+

+ License + typescript + @aries-framework/anoncreds version + +

+
+ +### Installation + +### Quick start + +### Example of usage diff --git a/packages/indy-vdr/jest.config.ts b/packages/indy-vdr/jest.config.ts new file mode 100644 index 0000000000..55c67d70a6 --- /dev/null +++ b/packages/indy-vdr/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + name: packageJson.name, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json new file mode 100644 index 0000000000..32c8689d5d --- /dev/null +++ b/packages/indy-vdr/package.json @@ -0,0 +1,36 @@ +{ + "name": "@aries-framework/indy-vdr", + "main": "build/index", + "types": "build/index", + "version": "0.3.3", + "private": true, + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/indy-vdr", + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "packages/indy-vdr" + }, + "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", + "indy-vdr-test-shared": "^0.1.3" + }, + "devDependencies": { + "indy-vdr-test-nodejs": "^0.1.3", + "rimraf": "~4.0.7", + "typescript": "~4.9.4" + } +} diff --git a/packages/indy-vdr/src/error/IndyVdrError.ts b/packages/indy-vdr/src/error/IndyVdrError.ts new file mode 100644 index 0000000000..501f428640 --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrError.ts @@ -0,0 +1,7 @@ +import { AriesFrameworkError } from '@aries-framework/core' + +export class IndyVdrError extends AriesFrameworkError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts b/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts new file mode 100644 index 0000000000..75cf40c9f6 --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrNotConfiguredError.ts @@ -0,0 +1,7 @@ +import { IndyVdrError } from './IndyVdrError' + +export class IndyVdrNotConfiguredError extends IndyVdrError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/indy-vdr/src/error/IndyVdrNotFound.ts b/packages/indy-vdr/src/error/IndyVdrNotFound.ts new file mode 100644 index 0000000000..00b1b94c47 --- /dev/null +++ b/packages/indy-vdr/src/error/IndyVdrNotFound.ts @@ -0,0 +1,7 @@ +import { IndyVdrError } from './IndyVdrError' + +export class IndyVdrNotFoundError extends IndyVdrError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/indy-vdr/src/error/index.ts b/packages/indy-vdr/src/error/index.ts new file mode 100644 index 0000000000..f062bfbed0 --- /dev/null +++ b/packages/indy-vdr/src/error/index.ts @@ -0,0 +1,3 @@ +export * from './IndyVdrError' +export * from './IndyVdrNotFound' +export * from './IndyVdrNotConfiguredError' diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts new file mode 100644 index 0000000000..8a5ca6c21a --- /dev/null +++ b/packages/indy-vdr/src/index.ts @@ -0,0 +1,6 @@ +try { + // eslint-disable-next-line import/no-extraneous-dependencies + require('indy-vdr-test-nodejs') +} catch (error) { + throw new Error('Error registering nodejs bindings for Indy VDR') +} diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts new file mode 100644 index 0000000000..6958d882ab --- /dev/null +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -0,0 +1,184 @@ +import type { Logger, AgentContext, Key } from '@aries-framework/core' +import type { IndyVdrRequest, IndyVdrPool as indyVdrPool } from 'indy-vdr-test-shared' + +import { TypedArrayEncoder } from '@aries-framework/core' +import { + GetTransactionAuthorAgreementRequest, + GetAcceptanceMechanismsRequest, + PoolCreate, + indyVdr, +} from 'indy-vdr-test-shared' + +import { IndyVdrError } from '../error' + +export interface TransactionAuthorAgreement { + version?: `${number}.${number}` | `${number}` + acceptanceMechanism: string +} + +export interface AuthorAgreement { + digest: string + version: string + text: string + ratification_ts: number + acceptanceMechanisms: AcceptanceMechanisms +} + +export interface AcceptanceMechanisms { + aml: Record + amlContext: string + version: string +} + +export interface IndyVdrPoolConfig { + genesisTransactions: string + isProduction: boolean + indyNamespace: string + transactionAuthorAgreement?: TransactionAuthorAgreement +} + +export class IndyVdrPool { + private _pool?: indyVdrPool + private logger: Logger + private poolConfig: IndyVdrPoolConfig + public authorAgreement?: AuthorAgreement | null + + public constructor(poolConfig: IndyVdrPoolConfig, logger: Logger) { + this.logger = logger + this.poolConfig = poolConfig + } + + public get indyNamespace(): string { + return this.poolConfig.indyNamespace + } + + public get config() { + return this.poolConfig + } + + public async connect() { + this._pool = new PoolCreate({ + parameters: { + transactions: this.config.genesisTransactions, + }, + }) + + return this.pool.handle + } + + private get pool(): indyVdrPool { + if (!this._pool) { + throw new IndyVdrError('Pool is not connected. Make sure to call .connect() first') + } + + return this._pool + } + + public close() { + if (!this.pool) { + throw new IndyVdrError("Can't close pool. Pool is not connected") + } + + // FIXME: this method doesn't work?? + // this.pool.close() + } + + public async submitWriteRequest( + agentContext: AgentContext, + request: Request, + signingKey: Key + ) { + await this.appendTaa(request) + + const signature = await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(request.signatureInput), + key: signingKey, + }) + + request.setSignature({ + signature, + }) + + return await this.pool.submitRequest(request) + } + + public async submitReadRequest(request: Request) { + return await this.pool.submitRequest(request) + } + + private async appendTaa(request: IndyVdrRequest) { + const authorAgreement = await this.getTransactionAuthorAgreement() + const poolTaa = this.config.transactionAuthorAgreement + + // If ledger does not have TAA, we can just send request + if (authorAgreement == null) { + return request + } + + // Ledger has taa but user has not specified which one to use + if (!poolTaa) { + throw new IndyVdrError( + `Please, specify a transaction author agreement with version and acceptance mechanism. ${JSON.stringify( + authorAgreement + )}` + ) + } + + // Throw an error if the pool doesn't have the specified version and acceptance mechanism + if ( + authorAgreement.version !== poolTaa.version || + !authorAgreement.acceptanceMechanisms.aml[poolTaa.acceptanceMechanism] + ) { + // Throw an error with a helpful message + const errMessage = `Unable to satisfy matching TAA with mechanism ${JSON.stringify( + poolTaa.acceptanceMechanism + )} and version ${poolTaa.version} in pool.\n Found ${JSON.stringify( + authorAgreement.acceptanceMechanisms.aml + )} and version ${authorAgreement.version} in pool.` + throw new IndyVdrError(errMessage) + } + + const acceptance = indyVdr.prepareTxnAuthorAgreementAcceptance({ + text: authorAgreement.text, + version: authorAgreement.version, + taaDigest: authorAgreement.digest, + time: Math.floor(new Date().getTime() / 1000), + acceptanceMechanismType: poolTaa.acceptanceMechanism, + }) + + request.setTransactionAuthorAgreementAcceptance({ acceptance }) + } + + private async getTransactionAuthorAgreement(): Promise { + // TODO Replace this condition with memoization + if (this.authorAgreement !== undefined) { + return this.authorAgreement + } + + const taaRequest = new GetTransactionAuthorAgreementRequest({}) + const taaResponse = await this.submitReadRequest(taaRequest) + + const acceptanceMechanismRequest = new GetAcceptanceMechanismsRequest({}) + const acceptanceMechanismResponse = await this.submitReadRequest(acceptanceMechanismRequest) + + const taaData = taaResponse.result.data + + // TAA can be null + if (taaData == null) { + this.authorAgreement = null + return null + } + + // If TAA is not null, we can be sure AcceptanceMechanisms is also not null + const authorAgreement = taaData as Omit + + // FIME: remove cast when https://github.com/hyperledger/indy-vdr/pull/142 is released + const acceptanceMechanisms = acceptanceMechanismResponse.result.data as unknown as AcceptanceMechanisms + this.authorAgreement = { + ...authorAgreement, + acceptanceMechanisms, + } + + return this.authorAgreement + } +} diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts new file mode 100644 index 0000000000..2cc1b5a206 --- /dev/null +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -0,0 +1,209 @@ +import type { IndyVdrPoolConfig } from './IndyVdrPool' +import type { AgentContext } from '@aries-framework/core' +import type { GetNymResponse } from 'indy-vdr-test-shared' + +import { Logger, InjectionSymbols, injectable, inject, CacheModuleConfig } from '@aries-framework/core' +import { GetNymRequest } from 'indy-vdr-test-shared' + +import { IndyVdrError, IndyVdrNotFoundError, IndyVdrNotConfiguredError } from '../error' +import { isSelfCertifiedDid, DID_INDY_REGEX } from '../utils/did' +import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' + +import { IndyVdrPool } from './IndyVdrPool' + +export interface CachedDidResponse { + nymResponse: { + did: string + verkey: string + } + indyNamespace: string +} +@injectable() +export class IndyVdrPoolService { + public pools: IndyVdrPool[] = [] + private logger: Logger + + public constructor(@inject(InjectionSymbols.Logger) logger: Logger) { + this.logger = logger + } + + public setPools(poolConfigs: IndyVdrPoolConfig[]) { + this.pools = poolConfigs.map((poolConfig) => new IndyVdrPool(poolConfig, this.logger)) + } + + /** + * Create connections to all ledger pools + */ + public async connectToPools() { + const handleArray: number[] = [] + // Sequentially connect to pools so we don't use up too many resources connecting in parallel + for (const pool of this.pools) { + this.logger.debug(`Connecting to pool: ${pool.indyNamespace}`) + const poolHandle = await pool.connect() + this.logger.debug(`Finished connection to pool: ${pool.indyNamespace}`) + handleArray.push(poolHandle) + } + return handleArray + } + + /** + * Get the most appropriate pool for the given did. + * If the did is a qualified indy did, the pool will be determined based on the namespace. + * If it is a legacy unqualified indy did, the pool will be determined based on the algorithm as described in this document: + * https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA/edit + */ + public async getPoolForDid(agentContext: AgentContext, did: string): Promise { + // Check if the did starts with did:indy + const match = did.match(DID_INDY_REGEX) + + if (match) { + const [, namespace] = match + + const pool = this.getPoolForNamespace(namespace) + + if (pool) return pool + + throw new IndyVdrError(`Pool for indy namespace '${namespace}' not found`) + } else { + return await this.getPoolForLegacyDid(agentContext, did) + } + } + + private async getPoolForLegacyDid(agentContext: AgentContext, did: string): Promise { + const pools = this.pools + + if (pools.length === 0) { + throw new IndyVdrNotConfiguredError( + "No indy ledgers configured. Provide at least one pool configuration in the 'indyLedgers' agent configuration" + ) + } + + const didCache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + + const cachedNymResponse = await didCache.get(agentContext, `IndyVdrPoolService:${did}`) + const pool = this.pools.find((pool) => pool.indyNamespace === cachedNymResponse?.indyNamespace) + + // If we have the nym response with associated pool in the cache, we'll use that + if (cachedNymResponse && pool) { + this.logger.trace(`Found ledger id '${pool.indyNamespace}' for did '${did}' in cache`) + return pool + } + + const { successful, rejected } = await this.getSettledDidResponsesFromPools(did, pools) + + if (successful.length === 0) { + const allNotFound = rejected.every((e) => e.reason instanceof IndyVdrNotFoundError) + const rejectedOtherThanNotFound = rejected.filter((e) => !(e.reason instanceof IndyVdrNotFoundError)) + + // All ledgers returned response that the did was not found + if (allNotFound) { + throw new IndyVdrNotFoundError(`Did '${did}' not found on any of the ledgers (total ${this.pools.length}).`) + } + + // one or more of the ledgers returned an unknown error + throw new IndyVdrError( + `Unknown error retrieving did '${did}' from '${rejectedOtherThanNotFound.length}' of '${pools.length}' ledgers`, + { cause: rejectedOtherThanNotFound[0].reason } + ) + } + + // If there are self certified DIDs we always prefer it over non self certified DIDs + // We take the first self certifying DID as we take the order in the + // indyLedgers config as the order of preference of ledgers + let value = successful.find((response) => + isSelfCertifiedDid(response.value.did.nymResponse.did, response.value.did.nymResponse.verkey) + )?.value + + if (!value) { + // Split between production and nonProduction ledgers. If there is at least one + // successful response from a production ledger, only keep production ledgers + // otherwise we only keep the non production ledgers. + const production = successful.filter((s) => s.value.pool.config.isProduction) + const nonProduction = successful.filter((s) => !s.value.pool.config.isProduction) + const productionOrNonProduction = production.length >= 1 ? production : nonProduction + + // We take the first value as we take the order in the indyLedgers config as + // the order of preference of ledgers + value = productionOrNonProduction[0].value + } + + await didCache.set(agentContext, did, { + nymResponse: { + did: value.did.nymResponse.did, + verkey: value.did.nymResponse.verkey, + }, + indyNamespace: value.did.indyNamespace, + }) + return value.pool + } + + private async getSettledDidResponsesFromPools(did: string, pools: IndyVdrPool[]) { + this.logger.trace(`Retrieving did '${did}' from ${pools.length} ledgers`) + const didResponses = await allSettled(pools.map((pool) => this.getDidFromPool(did, pool))) + + const successful = onlyFulfilled(didResponses) + this.logger.trace(`Retrieved ${successful.length} responses from ledgers for did '${did}'`) + + const rejected = onlyRejected(didResponses) + + return { + rejected, + successful, + } + } + + /** + * Get the most appropriate pool for the given indyNamespace + */ + public getPoolForNamespace(indyNamespace: string) { + if (this.pools.length === 0) { + throw new IndyVdrNotConfiguredError( + "No indy ledgers configured. Provide at least one pool configuration in the 'indyLedgers' agent configuration" + ) + } + + const pool = this.pools.find((pool) => pool.indyNamespace === indyNamespace) + + if (!pool) { + throw new IndyVdrError(`No ledgers found for IndyNamespace '${indyNamespace}'.`) + } + + return pool + } + + private async getDidFromPool(did: string, pool: IndyVdrPool): Promise { + try { + this.logger.trace(`Get public did '${did}' from ledger '${pool.indyNamespace}'`) + const request = await new GetNymRequest({ dest: did }) + + this.logger.trace(`Submitting get did request for did '${did}' to ledger '${pool.indyNamespace}'`) + const response = await pool.submitReadRequest(request) + + if (!response.result.data) { + throw new IndyVdrNotFoundError(`Did ${did} not found on indy pool with namespace ${pool.indyNamespace}`) + } + + const result = JSON.parse(response.result.data) + + this.logger.trace(`Retrieved did '${did}' from ledger '${pool.indyNamespace}'`, result) + + return { + did: result, + pool, + response, + } + } catch (error) { + this.logger.trace(`Error retrieving did '${did}' from ledger '${pool.indyNamespace}'`, { + error, + did, + }) + throw error + } + } +} + +export interface PublicDidRequest { + did: CachedDidResponse + pool: IndyVdrPool + response: GetNymResponse +} diff --git a/packages/indy-vdr/src/pool/index.ts b/packages/indy-vdr/src/pool/index.ts new file mode 100644 index 0000000000..1e1f1b52f8 --- /dev/null +++ b/packages/indy-vdr/src/pool/index.ts @@ -0,0 +1 @@ +export * from './IndyVdrPool' diff --git a/packages/indy-vdr/src/utils/did.ts b/packages/indy-vdr/src/utils/did.ts new file mode 100644 index 0000000000..9cda8ee95d --- /dev/null +++ b/packages/indy-vdr/src/utils/did.ts @@ -0,0 +1,61 @@ +/** + * Based on DidUtils implementation in Aries Framework .NET + * @see: https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Utils/DidUtils.cs + * + * Some context about full verkeys versus abbreviated verkeys: + * A standard verkey is 32 bytes, and by default in Indy the DID is chosen as the first 16 bytes of that key, before base58 encoding. + * An abbreviated verkey replaces the first 16 bytes of the verkey with ~ when it matches the DID. + * + * When a full verkey is used to register on the ledger, this is stored as a full verkey on the ledger and also returned from the ledger as a full verkey. + * The same applies to an abbreviated verkey. If an abbreviated verkey is used to register on the ledger, this is stored as an abbreviated verkey on the ledger and also returned from the ledger as an abbreviated verkey. + * + * For this reason we need some methods to check whether verkeys are full or abbreviated, so we can align this with `indy.abbreviateVerkey` + * + * Aries Framework .NET also abbreviates verkey before sending to ledger: + * https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Ledger/DefaultLedgerService.cs#L139-L147 + */ + +import { TypedArrayEncoder } from '@aries-framework/core' + +export const DID_INDY_REGEX = /^did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)):([1-9A-HJ-NP-Za-km-z]{21,22})$/ +export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ + +/** + * Check whether the did is a self certifying did. If the verkey is abbreviated this method + * will always return true. Make sure that the verkey you pass in this method belongs to the + * did passed in + * + * @return Boolean indicating whether the did is self certifying + */ +export function isSelfCertifiedDid(did: string, verkey: string): boolean { + // If the verkey is Abbreviated, it means the full verkey + // is the did + the verkey + if (isAbbreviatedVerkey(verkey)) { + return true + } + + const didFromVerkey = indyDidFromPublicKeyBase58(verkey) + + if (didFromVerkey === did) { + return true + } + + return false +} + +export function indyDidFromPublicKeyBase58(publicKeyBase58: string): string { + const buffer = TypedArrayEncoder.fromBase58(publicKeyBase58) + + const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + + return did +} + +/** + * Check a base58 encoded string against a regex expression to determine if it is a valid abbreviated verkey + * @param verkey Base58 encoded string representation of an abbreviated verkey + * @returns Boolean indicating if the string is a valid abbreviated verkey + */ +export function isAbbreviatedVerkey(verkey: string): boolean { + return ABBREVIATED_VERKEY_REGEX.test(verkey) +} diff --git a/packages/indy-vdr/src/utils/promises.ts b/packages/indy-vdr/src/utils/promises.ts new file mode 100644 index 0000000000..0e843d73b5 --- /dev/null +++ b/packages/indy-vdr/src/utils/promises.ts @@ -0,0 +1,44 @@ +// This file polyfills the allSettled method introduced in ESNext + +export type AllSettledFulfilled = { + status: 'fulfilled' + value: T +} + +export type AllSettledRejected = { + status: 'rejected' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reason: any +} + +export function allSettled(promises: Promise[]) { + return Promise.all( + promises.map((p) => + p + .then( + (value) => + ({ + status: 'fulfilled', + value, + } as AllSettledFulfilled) + ) + .catch( + (reason) => + ({ + status: 'rejected', + reason, + } as AllSettledRejected) + ) + ) + ) +} + +export function onlyFulfilled(entries: Array | AllSettledRejected>) { + // We filter for only the rejected values, so we can safely cast the type + return entries.filter((e) => e.status === 'fulfilled') as AllSettledFulfilled[] +} + +export function onlyRejected(entries: Array | AllSettledRejected>) { + // We filter for only the rejected values, so we can safely cast the type + return entries.filter((e) => e.status === 'rejected') as AllSettledRejected[] +} diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts new file mode 100644 index 0000000000..5920344527 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -0,0 +1,223 @@ +import type { Key } from '@aries-framework/core' + +import { IndyWallet, KeyType, SigningProviderRegistry, TypedArrayEncoder } from '@aries-framework/core' +import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from 'indy-vdr-test-shared' + +import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { IndyVdrPool } from '../src/pool' +import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' + +const indyVdrPoolService = new IndyVdrPoolService(testLogger) +const wallet = new IndyWallet(agentDependencies, testLogger, new SigningProviderRegistry([])) +const agentConfig = getAgentConfig('IndyVdrPoolService') +const agentContext = getAgentContext({ wallet, agentConfig }) + +const config = { + isProduction: false, + genesisTransactions, + indyNamespace: `pool:localtest`, + transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, +} as const + +let signerKey: Key + +indyVdrPoolService.setPools([config]) + +describe('IndyVdrPoolService', () => { + beforeAll(async () => { + await indyVdrPoolService.connectToPools() + + if (agentConfig.walletConfig) { + await wallet.createAndOpen(agentConfig.walletConfig) + } + + signerKey = await wallet.createKey({ seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519 }) + }) + + afterAll(async () => { + for (const pool of indyVdrPoolService.pools) { + pool.close() + } + + await wallet.delete() + }) + + describe('DIDs', () => { + test('can get a pool based on the namespace', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + expect(pool).toBeInstanceOf(IndyVdrPool) + expect(pool.config).toEqual(config) + }) + + test('can resolve a did using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const request = new GetNymRequest({ + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + }) + + const response = await pool.submitReadRequest(request) + + expect(response).toMatchObject({ + op: 'REPLY', + result: { + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + type: '105', + data: expect.any(String), + identifier: 'LibindyDid111111111111', + reqId: expect.any(Number), + seqNo: expect.any(Number), + txnTime: expect.any(Number), + state_proof: expect.any(Object), + }, + }) + + expect(JSON.parse(response.result.data as string)).toMatchObject({ + dest: 'TL1EaPFCZ8Si5aUrqScBDt', + identifier: 'V4SGRU86Z58d6TV7PBUe6f', + role: '0', + seqNo: expect.any(Number), + txnTime: expect.any(Number), + verkey: '~43X4NhAFqREffK7eWdKgFH', + }) + }) + + test('can write a did using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + // prepare the DID we are going to write to the ledger + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const buffer = TypedArrayEncoder.fromBase58(key.publicKeyBase58) + const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + + const request = new NymRequest({ + dest: did, + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + verkey: key.publicKeyBase58, + }) + + const response = await pool.submitWriteRequest(agentContext, request, signerKey) + + expect(response).toMatchObject({ + op: 'REPLY', + result: { + txn: { + protocolVersion: 2, + metadata: expect.any(Object), + data: expect.any(Object), + type: '1', + }, + ver: '1', + rootHash: expect.any(String), + txnMetadata: expect.any(Object), + }, + }) + }) + }) + + describe('Schemas & credential Definition', () => { + test('can write a schema using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const dynamicVersion = `1.${Math.random() * 100}` + + const schemaRequest = new SchemaRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + schema: { + id: 'test-schema-id', + name: 'test-schema', + ver: '1.0', + version: dynamicVersion, + attrNames: ['first_name', 'last_name', 'age'], + }, + }) + + const schemaResponse = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) + + expect(schemaResponse).toMatchObject({ + op: 'REPLY', + result: { + ver: '1', + txn: { + metadata: expect.any(Object), + type: '101', + data: { + data: { + attr_names: expect.arrayContaining(['age', 'last_name', 'first_name']), + name: 'test-schema', + version: dynamicVersion, + }, + }, + }, + }, + }) + + const credentialDefinitionRequest = new CredentialDefinitionRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + credentialDefinition: { + ver: '1.0', + id: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.result.txnMetadata.seqNo}:TAG`, + // must be string version of the schema seqNo + schemaId: `${schemaResponse.result.txnMetadata.seqNo}`, + type: 'CL', + tag: 'TAG', + value: { + primary: { + n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', + s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', + r: { + master_secret: + '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', + last_name: + '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', + first_name: + '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', + age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', + }, + rctxt: + '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', + z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', + }, + }, + }, + }) + + const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, signerKey) + + expect(response).toMatchObject({ + op: 'REPLY', + result: { + ver: '1', + txn: { + metadata: expect.any(Object), + type: '102', + data: { + data: { + primary: { + r: { + last_name: + '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', + first_name: + '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', + age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', + master_secret: + '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', + }, + z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', + rctxt: + '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', + n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', + s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', + }, + }, + signature_type: 'CL', + ref: schemaResponse.result.txnMetadata.seqNo, + tag: 'TAG', + }, + }, + }, + }) + }) + }) +}) diff --git a/packages/indy-vdr/tests/setup.ts b/packages/indy-vdr/tests/setup.ts new file mode 100644 index 0000000000..ce7749d25e --- /dev/null +++ b/packages/indy-vdr/tests/setup.ts @@ -0,0 +1,4 @@ +// Needed to register indy-vdr node bindings +import '../src/index' + +jest.setTimeout(20000) diff --git a/packages/indy-vdr/tsconfig.build.json b/packages/indy-vdr/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/indy-vdr/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/indy-vdr/tsconfig.json b/packages/indy-vdr/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/indy-vdr/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/yarn.lock b/yarn.lock index c79dff1e1c..30954778a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1795,6 +1795,21 @@ semver "^7.3.5" tar "^6.1.11" +"@mapbox/node-pre-gyp@^1.0.10": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" + integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@mattrglobal/bbs-signatures@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@mattrglobal/bbs-signatures/-/bbs-signatures-1.0.0.tgz#8ff272c6d201aadab7e08bd84dbfd6e0d48ba12d" @@ -3087,6 +3102,14 @@ array-includes@^3.1.6: get-intrinsic "^1.1.3" is-string "^1.0.7" +array-index@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9" + integrity sha512-jesyNbBkLQgGZMSwA1FanaFjalb1mZUGxGeUEkSDidzgrbjBGhvizJkaItdhkt8eIHFOJC7nDsrXk+BaehTdRw== + dependencies: + debug "^2.2.0" + es6-symbol "^3.0.2" + array-map@~0.0.0: version "0.0.1" resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.1.tgz#d1bf3cc8813a7daaa335e5c8eb21d9d06230c1a7" @@ -4327,6 +4350,14 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -4779,6 +4810,32 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.0.2, es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -5138,6 +5195,13 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -6075,6 +6139,23 @@ indy-sdk@^1.16.0-dev-1636: nan "^2.11.1" node-gyp "^8.0.0" +indy-vdr-test-nodejs@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/indy-vdr-test-nodejs/-/indy-vdr-test-nodejs-0.1.3.tgz#97eaf38b1035bfabcd772a8399f23d766dfd493e" + integrity sha512-E6r86QGbswa+hBgMJKVWJycqvvmOgepFMDaAvuZQtxQK1Z2gghco6m/9EOAPYaJRs0MMEEhzUGhvtSpCzeZ6sg== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.10" + ffi-napi "^4.0.3" + indy-vdr-test-shared "0.1.3" + ref-array-di "^1.2.2" + ref-napi "^3.0.3" + ref-struct-di "^1.1.1" + +indy-vdr-test-shared@0.1.3, indy-vdr-test-shared@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/indy-vdr-test-shared/-/indy-vdr-test-shared-0.1.3.tgz#3b5ee9492ebc3367a027670aa9686c493de5929c" + integrity sha512-fdgV388zi3dglu49kqrV+i40w+18uJkv96Tk4nziLdP280SLnZKKnIRAiq11Hj8aHpnZmwMloyQCsIyQZDZk2g== + infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -8281,6 +8362,11 @@ neon-cli@0.8.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -9590,6 +9676,14 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== +ref-array-di@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ref-array-di/-/ref-array-di-1.2.2.tgz#ceee9d667d9c424b5a91bb813457cc916fb1f64d" + integrity sha512-jhCmhqWa7kvCVrWhR/d7RemkppqPUdxEil1CtTtm7FkZV8LcHHCK3Or9GinUiFP5WY3k0djUkMvhBhx49Jb2iA== + dependencies: + array-index "^1.0.0" + debug "^3.1.0" + "ref-napi@^2.0.1 || ^3.0.2", ref-napi@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/ref-napi/-/ref-napi-3.0.3.tgz#e259bfc2bbafb3e169e8cd9ba49037dd00396b22" @@ -9600,7 +9694,7 @@ reduce-flatten@^2.0.0: node-addon-api "^3.0.0" node-gyp-build "^4.2.1" -ref-struct-di@^1.1.0: +ref-struct-di@^1.1.0, ref-struct-di@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ref-struct-di/-/ref-struct-di-1.1.1.tgz#5827b1d3b32372058f177547093db1fe1602dc10" integrity sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g== From 13f374079262168f90ec7de7c3393beb9651295c Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 29 Jan 2023 16:32:55 +0100 Subject: [PATCH 05/20] feat(anoncreds): add legacy indy credential format (#1220) Signed-off-by: Timo Glastra --- packages/anoncreds/package.json | 5 +- .../src/formats/AnonCredsCredentialFormat.ts | 89 +++ .../src/formats/LegacyIndyCredentialFormat.ts | 67 ++ .../LegacyIndyCredentialFormatService.ts | 608 ++++++++++++++++++ .../LegacyIndyCredentialFormatService.test.ts | 224 +++++++ packages/anoncreds/src/models/exchange.ts | 41 +- packages/anoncreds/src/models/internal.ts | 27 +- .../src/services/AnonCredsHolderService.ts | 10 +- .../services/AnonCredsHolderServiceOptions.ts | 31 +- .../src/services/AnonCredsIssuerService.ts | 2 + .../services/AnonCredsIssuerServiceOptions.ts | 4 +- .../src/services/AnonCredsVerifierService.ts | 2 + .../registry/AnonCredsRegistryService.ts | 2 +- .../registry/CredentialDefinitionOptions.ts | 6 +- .../registry/RevocationListOptions.ts | 2 +- .../RevocationRegistryDefinitionOptions.ts | 2 +- .../src/services/registry/SchemaOptions.ts | 6 +- .../AnonCredsRegistryService.test.ts | 8 +- .../src/utils/__tests__/credential.test.ts | 225 +++++++ packages/anoncreds/src/utils/credential.ts | 200 ++++++ packages/anoncreds/src/utils/metadata.ts | 29 + .../tests/InMemoryAnonCredsRegistry.ts | 155 +++++ packages/core/src/index.ts | 4 + .../formats/indy/IndyCredentialFormat.ts | 5 - .../core/src/modules/credentials/index.ts | 1 + packages/core/src/storage/Metadata.ts | 14 +- packages/core/tests/helpers.ts | 9 +- .../services/IndySdkAnonCredsRegistry.ts | 56 +- .../services/IndySdkHolderService.ts | 35 +- .../services/IndySdkIssuerService.ts | 16 +- .../services/IndySdkRevocationService.ts | 23 +- .../services/IndySdkUtilitiesService.ts | 65 -- .../services/IndySdkVerifierService.ts | 4 +- .../indy-sdk/src/anoncreds/utils/proverDid.ts | 12 + .../indy-sdk/src/anoncreds/utils/tails.ts | 45 ++ yarn.lock | 2 +- 36 files changed, 1839 insertions(+), 197 deletions(-) create mode 100644 packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts create mode 100644 packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts create mode 100644 packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts create mode 100644 packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts create mode 100644 packages/anoncreds/src/utils/__tests__/credential.test.ts create mode 100644 packages/anoncreds/src/utils/credential.ts create mode 100644 packages/anoncreds/src/utils/metadata.ts create mode 100644 packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts delete mode 100644 packages/indy-sdk/src/anoncreds/services/IndySdkUtilitiesService.ts create mode 100644 packages/indy-sdk/src/anoncreds/utils/proverDid.ts create mode 100644 packages/indy-sdk/src/anoncreds/utils/tails.ts diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 7947fe5642..de4e294a54 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -25,9 +25,12 @@ "test": "jest" }, "dependencies": { - "@aries-framework/core": "0.3.3" + "@aries-framework/core": "0.3.3", + "@aries-framework/node": "0.3.3", + "bn.js": "^5.2.1" }, "devDependencies": { + "indy-sdk": "^1.16.0-dev-1636", "rimraf": "^4.0.7", "typescript": "~4.9.4" } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts new file mode 100644 index 0000000000..fd6ebf7fcb --- /dev/null +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts @@ -0,0 +1,89 @@ +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' +import type { CredentialPreviewAttributeOptions, CredentialFormat, LinkedAttachment } from '@aries-framework/core' + +/** + * 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 type AnonCredsAcceptOfferFormat = Record + +/** + * 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: { + 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 + } + offer: AnonCredsCredentialOffer + request: AnonCredsCredentialRequest + credential: AnonCredsCredential + } +} diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts new file mode 100644 index 0000000000..ce9be1e3eb --- /dev/null +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts @@ -0,0 +1,67 @@ +import type { + AnonCredsAcceptOfferFormat, + AnonCredsAcceptProposalFormat, + AnonCredsAcceptRequestFormat, + AnonCredsOfferCredentialFormat, +} from './AnonCredsCredentialFormat' +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' +import type { CredentialPreviewAttributeOptions, CredentialFormat, LinkedAttachment } from '@aries-framework/core' + +/** + * 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 interface LegacyIndyProposeCredentialFormat { + schemaIssuerDid?: string + schemaId?: string + schemaName?: string + schemaVersion?: string + + credentialDefinitionId?: string + issuerDid?: string + + attributes?: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] +} + +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: { + schema_name?: string + schema_issuer_did?: string + schema_version?: string + schema_id?: string + + cred_def_id?: string + issuer_did?: string + } + 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..e1fd945937 --- /dev/null +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -0,0 +1,608 @@ +import type { LegacyIndyCredentialFormat } 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, + FormatCreateProposalOptions, + FormatCreateProposalReturn, + FormatProcessOptions, + FormatAcceptProposalOptions, + FormatCreateOfferReturn, + FormatCreateOfferOptions, + FormatAcceptOfferOptions, + CredentialFormatCreateReturn, + FormatAcceptRequestOptions, + FormatProcessCredentialOptions, + FormatAutoRespondProposalOptions, + FormatAutoRespondOfferOptions, + FormatAutoRespondRequestOptions, + FormatAutoRespondCredentialOptions, + CredentialExchangeRecord, + CredentialPreviewAttributeOptions, + LinkedAttachment, +} from '@aries-framework/core' + +import { + CredentialFormatSpec, + AriesFrameworkError, + IndyCredPropose, + JsonTransformer, + Attachment, + CredentialPreviewAttribute, + AttachmentData, + JsonEncoder, + utils, + MessageValidator, + CredentialProblemReportError, + CredentialProblemReportReason, +} from '@aries-framework/core' + +import { AnonCredsError } from '../error' +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 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 }: FormatCreateProposalOptions + ): 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 IndyCredPropose(indyCredentialProposal) + + try { + MessageValidator.validateSync(proposal) + } catch (error) { + throw new AriesFrameworkError(`Invalid proposal supplied: ${indyCredentialProposal} in Indy Format Service`) + } + + const proposalJson = JsonTransformer.toJSON(proposal) + const attachment = this.getFormatData(proposalJson, format.attachId) + + 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 }: FormatProcessOptions): Promise { + const proposalJson = attachment.getDataAsJson() + + // fromJSON also validates + JsonTransformer.fromJSON(proposalJson, IndyCredPropose) + } + + public async acceptProposal( + agentContext: AgentContext, + { + attachId, + credentialFormats, + credentialRecord, + proposalAttachment, + }: FormatAcceptProposalOptions + ): Promise { + const indyFormat = credentialFormats?.indy + + const credentialProposal = JsonTransformer.fromJSON(proposalAttachment.getDataAsJson(), IndyCredPropose) + + const credentialDefinitionId = indyFormat?.credentialDefinitionId ?? credentialProposal.credentialDefinitionId + + const attributes = indyFormat?.attributes ?? credentialRecord.credentialAttributes + + if (!credentialDefinitionId) { + throw new AriesFrameworkError( + 'No credentialDefinitionId 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.createIndyOffer(agentContext, { + credentialRecord, + attachId, + 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, attachId }: FormatCreateOfferOptions + ): Promise { + const indyFormat = credentialFormats.indy + + if (!indyFormat) { + throw new AriesFrameworkError('Missing indy credentialFormat data') + } + + const { format, attachment, previewAttributes } = await this.createIndyOffer(agentContext, { + credentialRecord, + attachId, + attributes: indyFormat.attributes, + credentialDefinitionId: indyFormat.credentialDefinitionId, + linkedAttachments: indyFormat.linkedAttachments, + }) + + return { format, attachment, previewAttributes } + } + + public async processOffer(agentContext: AgentContext, { attachment, credentialRecord }: FormatProcessOptions) { + agentContext.config.logger.debug(`Processing indy credential offer for credential record ${credentialRecord.id}`) + + const credOffer = attachment.getDataAsJson() + + if (!credOffer.schema_id || !credOffer.cred_def_id) { + throw new CredentialProblemReportError('Invalid credential offer', { + problemCode: CredentialProblemReportReason.IssuanceAbandoned, + }) + } + } + + public async acceptOffer( + agentContext: AgentContext, + { credentialRecord, attachId, offerAttachment }: FormatAcceptOfferOptions + ): 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, + }) + + credentialRecord.metadata.set( + AnonCredsCredentialRequestMetadataKey, + credentialRequestMetadata + ) + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + credentialDefinitionId: credentialOffer.cred_def_id, + schemaId: credentialOffer.schema_id, + }) + + const format = new CredentialFormatSpec({ + attachId, + format: INDY_CRED_REQUEST, + }) + + const attachment = this.getFormatData(credentialRequest, format.attachId) + 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: FormatProcessOptions): Promise { + // not needed for Indy + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialRecord, + attachId, + offerAttachment, + requestAttachment, + }: FormatAcceptRequestOptions + ): Promise { + // Assert credential attributes + const credentialAttributes = credentialRecord.credentialAttributes + if (!credentialAttributes) { + throw new CredentialProblemReportError( + `Missing required credential attribute values on credential record with id ${credentialRecord.id}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + 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, + }) + } + + const format = new CredentialFormatSpec({ + attachId, + format: INDY_CRED, + }) + + const attachment = this.getFormatData(credential, format.attachId) + 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 }: FormatProcessCredentialOptions + ): Promise { + const credentialRequestMetadata = credentialRecord.metadata.get( + AnonCredsCredentialRequestMetadataKey + ) + + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + if (!credentialRequestMetadata) { + throw new CredentialProblemReportError( + `Missing required request metadata for credential with id ${credentialRecord.id}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + 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}` + ) + } + + // 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, + 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: anonCredsCredential.rev_reg_id, + }) + } + + 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 attachId. We need to get out the correct attachId for + * indy and then find the corresponding attachment (if there is one) + * @param formats the formats object containing the attachId + * @param messageAttachments the attachments containing the payload + * @returns The Attachment if found or undefined + * + */ + public getAttachment(formats: CredentialFormatSpec[], messageAttachments: Attachment[]): Attachment | undefined { + const supportedAttachmentIds = formats.filter((f) => this.supportsFormat(f.format)).map((f) => f.attachId) + 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 shouldAutoRespondToProposal( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: FormatAutoRespondProposalOptions + ) { + const credentialProposalJson = proposalAttachment.getDataAsJson() + const credentialProposal = JsonTransformer.fromJSON(credentialProposalJson, IndyCredPropose) + + const credentialOfferJson = 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 credentialProposal.credentialDefinitionId === credentialOfferJson.cred_def_id + } + + public shouldAutoRespondToOffer( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: FormatAutoRespondOfferOptions + ) { + const credentialProposalJson = proposalAttachment.getDataAsJson() + const credentialProposal = JsonTransformer.fromJSON(credentialProposalJson, IndyCredPropose) + + const credentialOfferJson = 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 credentialProposal.credentialDefinitionId === credentialOfferJson.cred_def_id + } + + public shouldAutoRespondToRequest( + agentContext: AgentContext, + { offerAttachment, requestAttachment }: FormatAutoRespondRequestOptions + ) { + const credentialOfferJson = offerAttachment.getDataAsJson() + const credentialRequestJson = requestAttachment.getDataAsJson() + + return credentialOfferJson.cred_def_id == credentialRequestJson.cred_def_id + } + + public shouldAutoRespondToCredential( + agentContext: AgentContext, + { credentialRecord, requestAttachment, credentialAttachment }: FormatAutoRespondCredentialOptions + ) { + 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, + attachId, + credentialDefinitionId, + attributes, + linkedAttachments, + }: { + credentialDefinitionId: string + credentialRecord: CredentialExchangeRecord + attachId?: 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({ + attachId: attachId, + 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.attachId) + + return { format, attachment, previewAttributes } + } + + private async assertPreviewAttributesMatchSchemaAttributes( + agentContext: AgentContext, + offer: AnonCredsCredentialOffer, + attributes: CredentialPreviewAttribute[] + ): 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?: Attachment[] + previewAttributes?: CredentialPreviewAttribute[] + } { + if (!linkedAttachments && !attributes) { + return {} + } + + let previewAttributes = attributes?.map((attribute) => new CredentialPreviewAttribute(attribute)) ?? [] + let attachments: Attachment[] | 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): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(data), + }), + }) + + return attachment + } +} diff --git a/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts b/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts new file mode 100644 index 0000000000..7e1e1909da --- /dev/null +++ b/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts @@ -0,0 +1,224 @@ +import { + CredentialState, + CredentialExchangeRecord, + SigningProviderRegistry, + KeyType, + CredentialPreviewAttribute, +} from '@aries-framework/core' +import * as indySdk from 'indy-sdk' + +import { getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { + IndySdkHolderService, + IndySdkIssuerService, + IndySdkVerifierService, + IndySdkWallet, +} from '../../../../indy-sdk/src' +import { IndySdkRevocationService } from '../../../../indy-sdk/src/anoncreds/services/IndySdkRevocationService' +import { indyDidFromPublicKeyBase58 } from '../../../../indy-sdk/src/utils/did' +import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' +import { AnonCredsModuleConfig } from '../../AnonCredsModuleConfig' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, +} from '../../services' +import { AnonCredsRegistryService } from '../../services/registry/AnonCredsRegistryService' +import { LegacyIndyCredentialFormatService } from '../LegacyIndyCredentialFormatService' + +const registry = new InMemoryAnonCredsRegistry() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], +}) + +const agentConfig = getAgentConfig('LegacyIndyCredentialFormatServiceTest') +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 SigningProviderRegistry([])) +const agentContext = getAgentContext({ + registerInstances: [ + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + ], + agentConfig, + wallet, +}) + +const indyCredentialFormatService = new LegacyIndyCredentialFormatService() + +describe('LegacyIndyCredentialFormatService', () => { + beforeEach(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + }) + + afterEach(async () => { + await wallet.delete() + }) + + test('issuance flow starting from proposal without negotiation and without revocation', async () => { + // 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 key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const indyDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + 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', + }), + ] + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await indyCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + indy: { + attributes: credentialAttributes, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }, + }) + + // 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, + }) + + // 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: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: null, + credentialRevocationId: null, + }) + + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anonCreds/anonCredsCredential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + '_anonCreds/anonCredsCredentialRequest': { + master_secret_blinding_data: expect.any(Object), + master_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anonCreds/anonCredsCredential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }) + }) +}) diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index bd30979a86..40713b227d 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -1,11 +1,22 @@ -// TODO: Maybe we can make this a bit more specific? -export type WalletQuery = Record +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 -export interface ReferentWalletQuery { - [key: string]: WalletQuery + // 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 NonRevokedInterval { +export interface AnonCredsNonRevokedInterval { from?: number to?: number } @@ -18,16 +29,16 @@ export interface AnonCredsCredentialOffer { } export interface AnonCredsCredentialRequest { - // TODO: Why is this needed? It is just used as context in Ursa, can be any string. Should we remove it? - // Should we not make it did related? - prover_did: string + // prover_did is deprecated, however it is kept for backwards compatibility with legacy anoncreds implementations + prover_did?: string cred_def_id: string blinded_ms: Record blinded_ms_correctness_proof: Record nonce: string } -export interface CredValue { +export type AnonCredsCredentialValues = Record +export interface AnonCredsCredentialValue { raw: string encoded: string // Raw value as number in string } @@ -36,7 +47,7 @@ export interface AnonCredsCredential { schema_id: string cred_def_id: string rev_reg_id?: string - values: Record + values: Record signature: unknown signature_correctness_proof: unknown } @@ -91,8 +102,8 @@ export interface AnonCredsProofRequest { { name?: string names?: string[] - restrictions?: WalletQuery[] - non_revoked?: NonRevokedInterval + restrictions?: AnonCredsProofRequestRestriction[] + non_revoked?: AnonCredsNonRevokedInterval } > requested_predicates: Record< @@ -101,10 +112,10 @@ export interface AnonCredsProofRequest { name: string p_type: '>=' | '>' | '<=' | '<' p_value: number - restrictions?: WalletQuery[] - non_revoked?: NonRevokedInterval + restrictions?: AnonCredsProofRequestRestriction[] + non_revoked?: AnonCredsNonRevokedInterval } > - non_revoked?: NonRevokedInterval + non_revoked?: AnonCredsNonRevokedInterval ver?: '1.0' | '2.0' } diff --git a/packages/anoncreds/src/models/internal.ts b/packages/anoncreds/src/models/internal.ts index c838dcf865..27d476ebb3 100644 --- a/packages/anoncreds/src/models/internal.ts +++ b/packages/anoncreds/src/models/internal.ts @@ -1,5 +1,5 @@ -export interface CredentialInfo { - referent: string +export interface AnonCredsCredentialInfo { + credentialId: string attributes: { [key: string]: string } @@ -9,23 +9,32 @@ export interface CredentialInfo { credentialRevocationId?: string | undefined } -export interface RequestedAttribute { +export interface AnonCredsRequestedAttribute { credentialId: string timestamp?: number revealed: boolean - credentialInfo: CredentialInfo + credentialInfo: AnonCredsCredentialInfo revoked?: boolean } -export interface RequestedPredicate { +export interface AnonCredsRequestedPredicate { credentialId: string timestamp?: number - credentialInfo: CredentialInfo + credentialInfo: AnonCredsCredentialInfo revoked?: boolean } -export interface RequestedCredentials { - requestedAttributes?: Record - requestedPredicates?: Record +export interface AnonCredsRequestedCredentials { + requestedAttributes?: Record + requestedPredicates?: Record selfAttestedAttributes: Record } + +export interface AnonCredsCredentialRequestMetadata { + master_secret_blinding_data: { + v_prime: string + vr_prime: string | null + } + master_secret_name: string + nonce: string +} diff --git a/packages/anoncreds/src/services/AnonCredsHolderService.ts b/packages/anoncreds/src/services/AnonCredsHolderService.ts index 4991dbca1f..a7c0dcb22e 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderService.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderService.ts @@ -7,10 +7,12 @@ import type { GetCredentialsForProofRequestOptions, GetCredentialsForProofRequestReturn, } from './AnonCredsHolderServiceOptions' -import type { CredentialInfo } from '../models' +import type { AnonCredsCredentialInfo } from '../models' import type { AnonCredsProof } from '../models/exchange' import type { AgentContext } from '@aries-framework/core' +export const AnonCredsHolderServiceSymbol = Symbol('AnonCredsHolderService') + export interface AnonCredsHolderService { createProof(agentContext: AgentContext, options: CreateProofOptions): Promise storeCredential( @@ -19,8 +21,10 @@ export interface AnonCredsHolderService { metadata?: Record ): Promise - // TODO: indy has different return types for the credential - getCredential(agentContext: AgentContext, options: GetCredentialOptions): Promise + // TODO: this doesn't actually return the credential, as the indy-sdk doesn't support that + // We could come up with a hack (as we've received the credential at one point), but for + // now I think it's not that much of an issue + getCredential(agentContext: AgentContext, options: GetCredentialOptions): Promise createCredentialRequest( agentContext: AgentContext, diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index 3de66df703..728482ff33 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -1,11 +1,14 @@ -import type { CredentialInfo, RequestedCredentials } from '../models' +import type { + AnonCredsCredentialInfo, + AnonCredsCredentialRequestMetadata, + AnonCredsRequestedCredentials, +} from '../models' import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest, AnonCredsProofRequest, - NonRevokedInterval, - ReferentWalletQuery, + AnonCredsNonRevokedInterval, } from '../models/exchange' import type { AnonCredsCredentialDefinition, @@ -14,14 +17,14 @@ import type { AnonCredsSchema, } from '../models/registry' -export interface AttributeInfo { +export interface AnonCredsAttributeInfo { name?: string names?: string[] } export interface CreateProofOptions { proofRequest: AnonCredsProofRequest - requestedCredentials: RequestedCredentials + requestedCredentials: AnonCredsRequestedCredentials schemas: { [schemaId: string]: AnonCredsSchema } @@ -41,8 +44,7 @@ export interface CreateProofOptions { } export interface StoreCredentialOptions { - // TODO: what is in credential request metadata? - credentialRequestMetadata: Record + credentialRequestMetadata: AnonCredsCredentialRequestMetadata credential: AnonCredsCredential credentialDefinition: AnonCredsCredentialDefinition credentialDefinitionId: string @@ -57,6 +59,12 @@ export interface GetCredentialOptions { credentialId: string } +// TODO: Maybe we can make this a bit more specific? +export type WalletQuery = Record +export interface ReferentWalletQuery { + [referent: string]: WalletQuery +} + export interface GetCredentialsForProofRequestOptions { proofRequest: AnonCredsProofRequest attributeReferent: string @@ -66,19 +74,16 @@ export interface GetCredentialsForProofRequestOptions { } export type GetCredentialsForProofRequestReturn = Array<{ - credentialInfo: CredentialInfo - interval?: NonRevokedInterval + credentialInfo: AnonCredsCredentialInfo + interval?: AnonCredsNonRevokedInterval }> export interface CreateCredentialRequestOptions { - // TODO: Why is this needed? It is just used as context in Ursa, can be any string. Should we remove it? - // Should we not make it did related? (related to comment in AnonCredsCredentialRequest) - holderDid: string credentialOffer: AnonCredsCredentialOffer credentialDefinition: AnonCredsCredentialDefinition } export interface CreateCredentialRequestReturn { credentialRequest: AnonCredsCredentialRequest - credentialRequestMetadata: Record + credentialRequestMetadata: AnonCredsCredentialRequestMetadata } diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts index 0f34d300ef..41cb4ebf9f 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerService.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -9,6 +9,8 @@ import type { AnonCredsCredentialOffer } from '../models/exchange' import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../models/registry' import type { AgentContext } from '@aries-framework/core' +export const AnonCredsIssuerServiceSymbol = Symbol('AnonCredsIssuerService') + export interface AnonCredsIssuerService { createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index e3bb8dcdfb..58d6cd9048 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -2,7 +2,7 @@ import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest, - CredValue, + AnonCredsCredentialValues, } from '../models/exchange' import type { AnonCredsSchema } from '../models/registry' @@ -29,7 +29,7 @@ export interface CreateCredentialOfferOptions { export interface CreateCredentialOptions { credentialOffer: AnonCredsCredentialOffer credentialRequest: AnonCredsCredentialRequest - credentialValues: Record + credentialValues: AnonCredsCredentialValues revocationRegistryId?: string // TODO: should this just be the tails file instead of a path? tailsFilePath?: string diff --git a/packages/anoncreds/src/services/AnonCredsVerifierService.ts b/packages/anoncreds/src/services/AnonCredsVerifierService.ts index ec68021817..00e2a5670d 100644 --- a/packages/anoncreds/src/services/AnonCredsVerifierService.ts +++ b/packages/anoncreds/src/services/AnonCredsVerifierService.ts @@ -1,5 +1,7 @@ import type { VerifyProofOptions } from './AnonCredsVerifierServiceOptions' +export const AnonCredsVerifierServiceSymbol = Symbol('AnonCredsVerifierService') + export interface AnonCredsVerifierService { // TODO: do we want to extend the return type with more info besides a boolean. // If the value is false it would be nice to have some extra contexts about why it failed diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts index 8ee8eb4b50..a860d1e8f5 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts @@ -13,7 +13,7 @@ import { AnonCredsError } from '../../error' */ @injectable() export class AnonCredsRegistryService { - public async getRegistryForIdentifier(agentContext: AgentContext, identifier: string): Promise { + public getRegistryForIdentifier(agentContext: AgentContext, identifier: string): AnonCredsRegistry { const registries = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).registries // TODO: should we check if multiple are registered? diff --git a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts index 142e784405..1bf5614720 100644 --- a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts @@ -8,7 +8,7 @@ import type { import type { AnonCredsCredentialDefinition } from '../../models/registry' export interface GetCredentialDefinitionReturn { - credentialDefinition: AnonCredsCredentialDefinition | null + credentialDefinition?: AnonCredsCredentialDefinition credentialDefinitionId: string resolutionMetadata: AnonCredsResolutionMetadata credentialDefinitionMetadata: Extensible @@ -20,7 +20,7 @@ export interface RegisterCredentialDefinitionOptions { } export interface RegisterCredentialDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { - credentialDefinition: AnonCredsCredentialDefinition + credentialDefinition?: AnonCredsCredentialDefinition credentialDefinitionId?: string } @@ -30,7 +30,7 @@ export interface RegisterCredentialDefinitionReturnStateFinished extends AnonCre } export interface RegisterCredentialDefinitionReturnState extends AnonCredsOperationState { - credentialDefinition: AnonCredsCredentialDefinition + credentialDefinition?: AnonCredsCredentialDefinition credentialDefinitionId?: string } diff --git a/packages/anoncreds/src/services/registry/RevocationListOptions.ts b/packages/anoncreds/src/services/registry/RevocationListOptions.ts index b6f0edea42..f3a07dc686 100644 --- a/packages/anoncreds/src/services/registry/RevocationListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationListOptions.ts @@ -2,7 +2,7 @@ import type { AnonCredsResolutionMetadata, Extensible } from './base' import type { AnonCredsRevocationList } from '../../models/registry' export interface GetRevocationListReturn { - revocationList: AnonCredsRevocationList | null + revocationList?: AnonCredsRevocationList resolutionMetadata: AnonCredsResolutionMetadata revocationListMetadata: Extensible } diff --git a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts index 6d45377114..6e9d1349fe 100644 --- a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts @@ -2,7 +2,7 @@ import type { AnonCredsResolutionMetadata, Extensible } from './base' import type { AnonCredsRevocationRegistryDefinition } from '../../models/registry' export interface GetRevocationRegistryDefinitionReturn { - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | null + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId: string resolutionMetadata: AnonCredsResolutionMetadata revocationRegistryDefinitionMetadata: Extensible diff --git a/packages/anoncreds/src/services/registry/SchemaOptions.ts b/packages/anoncreds/src/services/registry/SchemaOptions.ts index c436859060..9ff42c9bc4 100644 --- a/packages/anoncreds/src/services/registry/SchemaOptions.ts +++ b/packages/anoncreds/src/services/registry/SchemaOptions.ts @@ -9,7 +9,7 @@ import type { AnonCredsSchema } from '../../models/registry' // Get Schema export interface GetSchemaReturn { - schema: AnonCredsSchema | null + schema?: AnonCredsSchema schemaId: string // Can contain e.g. the ledger transaction request/response resolutionMetadata: AnonCredsResolutionMetadata @@ -24,7 +24,7 @@ export interface RegisterSchemaOptions { } export interface RegisterSchemaReturnStateFailed extends AnonCredsOperationStateFailed { - schema: AnonCredsSchema + schema?: AnonCredsSchema schemaId?: string } @@ -34,7 +34,7 @@ export interface RegisterSchemaReturnStateFinished extends AnonCredsOperationSta } export interface RegisterSchemaReturnState extends AnonCredsOperationState { - schema: AnonCredsSchema + schema?: AnonCredsSchema schemaId?: string } diff --git a/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts b/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts index 096626f805..553b9e626c 100644 --- a/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts +++ b/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts @@ -28,11 +28,11 @@ const anonCredsRegistryService = new AnonCredsRegistryService() describe('AnonCredsRegistryService', () => { test('returns the registry for an identifier based on the supportedMethods regex', async () => { - await expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'a')).resolves.toEqual(registryOne) - await expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'b')).resolves.toEqual(registryTwo) + expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'a')).toEqual(registryOne) + expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'b')).toEqual(registryTwo) }) - test('throws AnonCredsError if no registry is found for the given identifier', async () => { - await expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'c')).rejects.toThrow(AnonCredsError) + test('throws AnonCredsError if no registry is found for the given identifier', () => { + expect(() => anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'c')).toThrow(AnonCredsError) }) }) diff --git a/packages/anoncreds/src/utils/__tests__/credential.test.ts b/packages/anoncreds/src/utils/__tests__/credential.test.ts new file mode 100644 index 0000000000..0b81afe881 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/credential.test.ts @@ -0,0 +1,225 @@ +import { CredentialPreviewAttribute } from '@aries-framework/core' + +import { assertCredentialValuesMatch, checkValidEncoding, convertAttributesToCredentialValues } from '../credential' + +/** + * Sample test cases for encoding/decoding of verifiable credential claims - Aries RFCs 0036 and 0037 + * @see https://gist.github.com/swcurran/78e5a9e8d11236f003f6a6263c6619a6 + */ +const testEncodings: { [key: string]: { raw: string | number | boolean | null; encoded: string } } = { + address2: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + zip: { + raw: '87121', + encoded: '87121', + }, + city: { + raw: 'SLC', + encoded: '101327353979588246869873249766058188995681113722618593621043638294296500696424', + }, + address1: { + raw: '101 Tela Lane', + encoded: '63690509275174663089934667471948380740244018358024875547775652380902762701972', + }, + state: { + raw: 'UT', + encoded: '93856629670657830351991220989031130499313559332549427637940645777813964461231', + }, + Empty: { + raw: '', + encoded: '102987336249554097029535212322581322789799900648198034993379397001115665086549', + }, + Null: { + raw: null, + encoded: '99769404535520360775991420569103450442789945655240760487761322098828903685777', + }, + 'bool True': { + raw: true, + encoded: '1', + }, + 'bool False': { + raw: false, + encoded: '0', + }, + 'str True': { + raw: 'True', + encoded: '27471875274925838976481193902417661171675582237244292940724984695988062543640', + }, + 'str False': { + raw: 'False', + encoded: '43710460381310391454089928988014746602980337898724813422905404670995938820350', + }, + 'max i32': { + raw: 2147483647, + encoded: '2147483647', + }, + 'max i32 + 1': { + raw: 2147483648, + encoded: '26221484005389514539852548961319751347124425277437769688639924217837557266135', + }, + 'min i32': { + raw: -2147483648, + encoded: '-2147483648', + }, + 'min i32 - 1': { + raw: -2147483649, + encoded: '68956915425095939579909400566452872085353864667122112803508671228696852865689', + }, + 'float 0.1': { + raw: 0.1, + encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', + }, + 'str 0.1': { + raw: '0.1', + encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', + }, + 'str 1.0': { + raw: '1.0', + encoded: '94532235908853478633102631881008651863941875830027892478278578250784387892726', + }, + 'str 1': { + raw: '1', + encoded: '1', + }, + 'leading zero number string': { + raw: '012345', + encoded: '12345', + }, + 'chr 0': { + raw: String.fromCharCode(0), + encoded: '49846369543417741186729467304575255505141344055555831574636310663216789168157', + }, + 'chr 1': { + raw: String.fromCharCode(1), + encoded: '34356466678672179216206944866734405838331831190171667647615530531663699592602', + }, + 'chr 2': { + raw: String.fromCharCode(2), + encoded: '99398763056634537812744552006896172984671876672520535998211840060697129507206', + }, +} + +describe('Utils | Credentials', () => { + describe('convertAttributesToCredentialValues', () => { + test('returns object with raw and encoded attributes', () => { + const attributes = [ + new CredentialPreviewAttribute({ + name: 'name', + mimeType: 'text/plain', + value: '101 Wilson Lane', + }), + new CredentialPreviewAttribute({ + name: 'age', + mimeType: 'text/plain', + value: '1234', + }), + ] + + expect(convertAttributesToCredentialValues(attributes)).toEqual({ + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + }) + }) + }) + + describe('assertCredentialValuesMatch', () => { + test('does not throw if attributes match', () => { + const firstValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).not.toThrow() + }) + + test('throws if number of values in the entries do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + 'Number of values in first entry (1) does not match number of values in second entry (2)' + ) + }) + + test('throws if second value does not contain key from first value', () => { + const firstValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + anotherName: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Second cred values object has no value for key 'name'" + ) + }) + + test('throws if encoded values do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + age: { raw: '1234', encoded: '12345' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Encoded credential values for key 'age' do not match" + ) + }) + + test('throws if raw values do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + age: { raw: '12345', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Raw credential values for key 'age' do not match" + ) + }) + }) + + describe('checkValidEncoding', () => { + // Formatted for test.each + const testEntries = Object.entries(testEncodings).map( + ([name, { raw, encoded }]) => [name, raw, encoded] as [string, string | number | boolean | null, string] + ) + + test.each(testEntries)('returns true for valid encoding %s', (_, raw, encoded) => { + expect(checkValidEncoding(raw, encoded)).toEqual(true) + }) + }) +}) diff --git a/packages/anoncreds/src/utils/credential.ts b/packages/anoncreds/src/utils/credential.ts new file mode 100644 index 0000000000..6310270980 --- /dev/null +++ b/packages/anoncreds/src/utils/credential.ts @@ -0,0 +1,200 @@ +import type { AnonCredsSchema, AnonCredsCredentialValues } from '../models' +import type { CredentialPreviewAttributeOptions, LinkedAttachment } from '@aries-framework/core' + +import { CredentialPreviewAttribute, AriesFrameworkError, Hasher, encodeAttachment } from '@aries-framework/core' +import BigNumber from 'bn.js' + +const isString = (value: unknown): value is string => typeof value === 'string' +const isNumber = (value: unknown): value is number => typeof value === 'number' +const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean' +const isNumeric = (value: string) => /^-?\d+$/.test(value) + +const isInt32 = (number: number) => { + const minI32 = -2147483648 + const maxI32 = 2147483647 + + // Check if number is integer and in range of int32 + return Number.isInteger(number) && number >= minI32 && number <= maxI32 +} + +/** + * Converts int value to string + * Converts string value: + * - hash with sha256, + * - convert to byte array and reverse it + * - convert it to BigInteger and return as a string + * @param attributes + * + * @returns CredValues + */ +export function convertAttributesToCredentialValues( + attributes: CredentialPreviewAttributeOptions[] +): AnonCredsCredentialValues { + return attributes.reduce((credentialValues, attribute) => { + return { + [attribute.name]: { + raw: attribute.value, + encoded: encode(attribute.value), + }, + ...credentialValues, + } + }, {}) +} + +/** + * Check whether the values of two credentials match (using {@link assertCredentialValuesMatch}) + * + * @returns a boolean whether the values are equal + * + */ +export function checkCredentialValuesMatch( + firstValues: AnonCredsCredentialValues, + secondValues: AnonCredsCredentialValues +): boolean { + try { + assertCredentialValuesMatch(firstValues, secondValues) + return true + } catch { + return false + } +} + +/** + * Assert two credential values objects match. + * + * @param firstValues The first values object + * @param secondValues The second values object + * + * @throws If not all values match + */ +export function assertCredentialValuesMatch( + firstValues: AnonCredsCredentialValues, + secondValues: AnonCredsCredentialValues +) { + const firstValuesKeys = Object.keys(firstValues) + const secondValuesKeys = Object.keys(secondValues) + + if (firstValuesKeys.length !== secondValuesKeys.length) { + throw new Error( + `Number of values in first entry (${firstValuesKeys.length}) does not match number of values in second entry (${secondValuesKeys.length})` + ) + } + + for (const key of firstValuesKeys) { + const firstValue = firstValues[key] + const secondValue = secondValues[key] + + if (!secondValue) { + throw new Error(`Second cred values object has no value for key '${key}'`) + } + + if (firstValue.encoded !== secondValue.encoded) { + throw new Error(`Encoded credential values for key '${key}' do not match`) + } + + if (firstValue.raw !== secondValue.raw) { + throw new Error(`Raw credential values for key '${key}' do not match`) + } + } +} + +/** + * Check whether the raw value matches the encoded version according to the encoding format described in Aries RFC 0037 + * Use this method to ensure the received proof (over the encoded) value is the same as the raw value of the data. + * + * @param raw + * @param encoded + * @returns Whether raw and encoded value match + * + * @see https://github.com/hyperledger/aries-framework-dotnet/blob/a18bef91e5b9e4a1892818df7408e2383c642dfa/src/Hyperledger.Aries/Utils/CredentialUtils.cs#L78-L89 + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0037-present-proof/README.md#verifying-claims-of-indy-based-verifiable-credentials + */ +export function checkValidEncoding(raw: unknown, encoded: string) { + return encoded === encode(raw) +} + +/** + * Encode value according to the encoding format described in Aries RFC 0036/0037 + * + * @param value + * @returns Encoded version of value + * + * @see https://github.com/hyperledger/aries-cloudagent-python/blob/0000f924a50b6ac5e6342bff90e64864672ee935/aries_cloudagent/messaging/util.py#L106-L136 + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0037-present-proof/README.md#verifying-claims-of-indy-based-verifiable-credentials + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0036-issue-credential/README.md#encoding-claims-for-indy-based-verifiable-credentials + */ +export function encode(value: unknown) { + const isEmpty = (value: unknown) => isString(value) && value === '' + + // If bool return bool as number string + if (isBoolean(value)) { + return Number(value).toString() + } + + // If value is int32 return as number string + if (isNumber(value) && isInt32(value)) { + return value.toString() + } + + // If value is an int32 number string return as number string + if (isString(value) && !isEmpty(value) && !isNaN(Number(value)) && isNumeric(value) && isInt32(Number(value))) { + return Number(value).toString() + } + + if (isNumber(value)) { + value = value.toString() + } + + // If value is null we must use the string value 'None' + if (value === null || value === undefined) { + value = 'None' + } + + return new BigNumber(Hasher.hash(Buffer.from(value as string), 'sha2-256')).toString() +} + +export function assertAttributesMatch(schema: AnonCredsSchema, attributes: CredentialPreviewAttribute[]) { + const schemaAttributes = schema.attrNames + const credAttributes = attributes.map((a) => a.name) + + const difference = credAttributes + .filter((x) => !schemaAttributes.includes(x)) + .concat(schemaAttributes.filter((x) => !credAttributes.includes(x))) + + if (difference.length > 0) { + throw new AriesFrameworkError( + `The credential preview attributes do not match the schema attributes (difference is: ${difference}, needs: ${schemaAttributes})` + ) + } +} + +/** + * Adds attribute(s) to the credential preview that is linked to the given attachment(s) + * + * @param attachments a list of the attachments that need to be linked to a credential + * @param preview the credential previews where the new linked credential has to be appended to + * + * @returns a modified version of the credential preview with the linked credentials + * */ +export function createAndLinkAttachmentsToPreview( + attachments: LinkedAttachment[], + previewAttributes: CredentialPreviewAttribute[] +) { + const credentialPreviewAttributeNames = previewAttributes.map((attribute) => attribute.name) + const newPreviewAttributes = [...previewAttributes] + + attachments.forEach((linkedAttachment) => { + if (credentialPreviewAttributeNames.includes(linkedAttachment.attributeName)) { + throw new AriesFrameworkError(`linkedAttachment ${linkedAttachment.attributeName} already exists in the preview`) + } else { + const credentialPreviewAttribute = new CredentialPreviewAttribute({ + name: linkedAttachment.attributeName, + mimeType: linkedAttachment.attachment.mimeType, + value: encodeAttachment(linkedAttachment.attachment), + }) + newPreviewAttributes.push(credentialPreviewAttribute) + } + }) + + return newPreviewAttributes +} diff --git a/packages/anoncreds/src/utils/metadata.ts b/packages/anoncreds/src/utils/metadata.ts new file mode 100644 index 0000000000..1d8448ebfa --- /dev/null +++ b/packages/anoncreds/src/utils/metadata.ts @@ -0,0 +1,29 @@ +// TODO: we may want to already support multiple credentials in the metadata of a credential +// record, as that's what the RFCs support. We already need to write a migration script for modules + +/** + * Metadata key for strong metadata on an AnonCreds credential. + * + * MUST be used with {@link AnonCredsCredentialMetadata} + */ +export const AnonCredsCredentialMetadataKey = '_anonCreds/anonCredsCredential' + +/** + * Metadata key for strong metadata on an AnonCreds credential request. + * + * MUST be used with {@link AnonCredsCredentialRequestMetadata} + */ +export const AnonCredsCredentialRequestMetadataKey = '_anonCreds/anonCredsCredentialRequest' + +/** + * Metadata for an AnonCreds credential that will be stored + * in the credential record. + * + * MUST be used with {@link AnonCredsCredentialMetadataKey} + */ +export interface AnonCredsCredentialMetadata { + schemaId?: string + credentialDefinitionId?: string + revocationRegistryId?: string + credentialRevocationId?: string +} diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts new file mode 100644 index 0000000000..a1426fad46 --- /dev/null +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { + AnonCredsRegistry, + GetSchemaReturn, + RegisterSchemaOptions, + RegisterSchemaReturn, + GetCredentialDefinitionReturn, + RegisterCredentialDefinitionOptions, + RegisterCredentialDefinitionReturn, + GetRevocationRegistryDefinitionReturn, + GetRevocationListReturn, + AnonCredsSchema, + AnonCredsCredentialDefinition, +} from '../src' +import type { AgentContext } from '@aries-framework/core' + +import { Hasher, TypedArrayEncoder } from '@aries-framework/core' +import BigNumber from 'bn.js' + +/** + * In memory implementation of the {@link AnonCredsRegistry} interface. Useful for testing. + */ +export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { + // Roughly match that the identifier starts with an unqualified indy did. Once the + // anoncreds tests are not based on the indy-sdk anymore, we can use any identifier + // we want, but the indy-sdk is picky about the identifier format. + public readonly supportedIdentifier = /^[a-zA-Z0-9]{21,22}/ + + private schemas: Record = {} + private credentialDefinitions: Record = {} + + public async getSchema(agentContext: AgentContext, schemaId: string): Promise { + const schema = this.schemas[schemaId] + const indyLedgerSeqNo = getSeqNoFromSchemaId(schemaId) + + if (!schema) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Schema with id ${schemaId} not found in memory registry`, + }, + schemaId, + schemaMetadata: { + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo, + }, + } + } + + return { + resolutionMetadata: {}, + schema, + schemaId, + schemaMetadata: {}, + } + } + + public async registerSchema( + agentContext: AgentContext, + options: RegisterSchemaOptions + ): Promise { + const schemaId = `${options.schema.issuerId}:2:${options.schema.name}:${options.schema.version}` + const indyLedgerSeqNo = getSeqNoFromSchemaId(schemaId) + + this.schemas[schemaId] = options.schema + + return { + registrationMetadata: {}, + schemaMetadata: { + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo, + }, + schemaState: { + state: 'finished', + schema: options.schema, + schemaId, + }, + } + } + + public async getCredentialDefinition( + agentContext: AgentContext, + credentialDefinitionId: string + ): Promise { + const credentialDefinition = this.credentialDefinitions[credentialDefinitionId] + + if (!credentialDefinition) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Credential definition with id ${credentialDefinitionId} not found in memory registry`, + }, + credentialDefinitionId, + credentialDefinitionMetadata: {}, + } + } + + return { + resolutionMetadata: {}, + credentialDefinition, + credentialDefinitionId, + credentialDefinitionMetadata: {}, + } + } + + public async registerCredentialDefinition( + agentContext: AgentContext, + options: RegisterCredentialDefinitionOptions + ): Promise { + const indyLedgerSeqNo = getSeqNoFromSchemaId(options.credentialDefinition.schemaId) + const credentialDefinitionId = `${options.credentialDefinition.issuerId}:3:CL:${indyLedgerSeqNo}:${options.credentialDefinition.tag}` + + this.credentialDefinitions[credentialDefinitionId] = options.credentialDefinition + + return { + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + state: 'finished', + credentialDefinition: options.credentialDefinition, + credentialDefinitionId, + }, + } + } + + public getRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ): Promise { + throw new Error('Method not implemented.') + } + + public getRevocationList( + agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number + ): Promise { + throw new Error('Method not implemented.') + } +} + +/** + * Calculates a consistent sequence number for a given schema id. + * + * Does this by hashing the schema id, transforming the hash to a number and taking the first 6 digits. + */ +function getSeqNoFromSchemaId(schemaId: string) { + const seqNo = Number( + new BigNumber(Hasher.hash(TypedArrayEncoder.fromString(schemaId), 'sha2-256')).toString().slice(0, 5) + ) + + return seqNo +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e2b1665a2a..ee9c82dfa0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,6 +67,10 @@ export type { Constructor } from './utils/mixins' export * from './agent/Events' export * from './crypto/' +export { encodeAttachment } from './utils/attachment' +export { Hasher } from './utils/Hasher' +export { MessageValidator } from './utils/MessageValidator' +export { LinkedAttachment, LinkedAttachmentOptions } from './utils/LinkedAttachment' import { parseInvitationUrl } from './utils/parseInvitation' import { uuid } from './utils/uuid' diff --git a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormat.ts b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormat.ts index eeee56e5d9..73c8082372 100644 --- a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormat.ts +++ b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormat.ts @@ -36,11 +36,6 @@ export interface IndyOfferCredentialFormat { linkedAttachments?: LinkedAttachment[] } -export interface IndyIssueCredentialFormat { - credentialDefinitionId?: string - attributes?: CredentialPreviewAttributeOptions[] -} - export interface IndyCredentialFormat extends CredentialFormat { formatKey: 'indy' credentialRecordType: 'indy' diff --git a/packages/core/src/modules/credentials/index.ts b/packages/core/src/modules/credentials/index.ts index d34680afe1..286f34276d 100644 --- a/packages/core/src/modules/credentials/index.ts +++ b/packages/core/src/modules/credentials/index.ts @@ -7,3 +7,4 @@ export * from './formats' export * from './protocol' export * from './CredentialsModule' export * from './CredentialsModuleConfig' +export { CredentialProblemReportError, CredentialProblemReportReason } from './errors' diff --git a/packages/core/src/storage/Metadata.ts b/packages/core/src/storage/Metadata.ts index 87c3e0d298..c635c1c2c5 100644 --- a/packages/core/src/storage/Metadata.ts +++ b/packages/core/src/storage/Metadata.ts @@ -1,5 +1,9 @@ +// Any is used to prevent frustrating TS errors if we just want to store arbitrary json data +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type MetadataValue = Record + export type MetadataBase = { - [key: string]: Record + [key: string]: MetadataValue } /** @@ -31,7 +35,7 @@ export class Metadata { * @returns the value saved in the key value pair * @returns null when the key could not be found */ - public get, Key extends string = string>( + public get( key: Key ): (Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value) | null { return (this.data[key] as Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value) ?? null @@ -43,11 +47,11 @@ export class Metadata { * @param key the key to set the metadata by * @param value the value to set in the metadata */ - public set, Key extends string = string>( + public set( key: Key, value: Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value ): void { - this.data[key] = value as Record + this.data[key] = value as MetadataValue } /** @@ -56,7 +60,7 @@ export class Metadata { * @param key the key to add the metadata at * @param value the value to add in the metadata */ - public add, Key extends string = string>( + public add( key: Key, value: Partial ): void { diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 9718a60975..597cc6bda7 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -19,7 +19,7 @@ import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../ import type { IndyOfferCredentialFormat } from '../src/modules/credentials/formats/indy/IndyCredentialFormat' import type { ProofAttributeInfo, ProofPredicateInfo } from '../src/modules/proofs/formats/indy/models' import type { AutoAcceptProof } from '../src/modules/proofs/models/ProofAutoAcceptType' -import type { Awaited } from '../src/types' +import type { Awaited, WalletConfig } from '../src/types' import type { CredDef, Schema } from 'indy-sdk' import type { Observable } from 'rxjs' @@ -162,9 +162,12 @@ export function getPostgresAgentOptions(name: string, extraConfig: Partial = {}) { +export function getAgentConfig( + name: string, + extraConfig: Partial = {} +): AgentConfig & { walletConfig: WalletConfig } { const { config, dependencies } = getAgentOptions(name, extraConfig) - return new AgentConfig(config, dependencies) + return new AgentConfig(config, dependencies) as AgentConfig & { walletConfig: WalletConfig } } export function getAgentContext({ diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index 95b08fa88b..ffe975b7e1 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -75,13 +75,13 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { issuerId: issuerId, }, schemaId: schema.id, - resolutionMetadata: { + resolutionMetadata: {}, + schemaMetadata: { didIndyNamespace: pool.didIndyNamespace, // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. // For this reason we return it in the metadata. indyLedgerSeqNo: schema.seqNo, }, - schemaMetadata: {}, } } catch (error) { agentContext.config.logger.error(`Error retrieving schema '${schemaId}'`, { @@ -90,7 +90,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { }) return { - schema: null, schemaId, resolutionMetadata: { error: 'notFound', @@ -157,13 +156,13 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { }, schemaId: schema.id, }, - registrationMetadata: { + registrationMetadata: {}, + schemaMetadata: { // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. // For this reason we return it in the metadata. indyLedgerSeqNo: schema.seqNo, didIndyNamespace: pool.didIndyNamespace, }, - schemaMetadata: {}, } } catch (error) { agentContext.config.logger.error(`Error registering schema for did '${options.schema.issuerId}'`, { @@ -229,10 +228,10 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { type: 'CL', value: credentialDefinition.value, }, - credentialDefinitionMetadata: {}, - resolutionMetadata: { + credentialDefinitionMetadata: { didIndyNamespace: pool.didIndyNamespace, }, + resolutionMetadata: {}, } } catch (error) { agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`, { @@ -242,7 +241,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { return { credentialDefinitionId, - credentialDefinition: null, credentialDefinitionMetadata: {}, resolutionMetadata: { error: 'notFound', @@ -280,14 +278,17 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) // TODO: this will bypass caching if done on a higher level. - const { schema, resolutionMetadata } = await this.getSchema(agentContext, options.credentialDefinition.schemaId) + const { schema, schemaMetadata, resolutionMetadata } = await this.getSchema( + agentContext, + options.credentialDefinition.schemaId + ) - if (!schema || !resolutionMetadata.indyLedgerSeqNo || typeof resolutionMetadata.indyLedgerSeqNo !== 'number') { + if (!schema || !schemaMetadata.indyLedgerSeqNo || typeof schemaMetadata.indyLedgerSeqNo !== 'number') { return { - registrationMetadata: { + registrationMetadata: {}, + credentialDefinitionMetadata: { didIndyNamespace: pool.didIndyNamespace, }, - credentialDefinitionMetadata: {}, credentialDefinitionState: { credentialDefinition: options.credentialDefinition, state: 'failed', @@ -298,7 +299,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { const credentialDefinitionId = getLegacyCredentialDefinitionId( options.credentialDefinition.issuerId, - resolutionMetadata.indyLedgerSeqNo, + schemaMetadata.indyLedgerSeqNo, options.credentialDefinition.tag ) @@ -327,15 +328,15 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) return { - credentialDefinitionMetadata: {}, + credentialDefinitionMetadata: { + didIndyNamespace: pool.didIndyNamespace, + }, credentialDefinitionState: { credentialDefinition: options.credentialDefinition, credentialDefinitionId, state: 'finished', }, - registrationMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, + registrationMetadata: {}, } } catch (error) { agentContext.config.logger.error( @@ -388,13 +389,12 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) return { - resolutionMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, + resolutionMetadata: {}, revocationRegistryDefinition: anonCredsRevocationRegistryDefinitionFromIndySdk(revocationRegistryDefinition), revocationRegistryDefinitionId, revocationRegistryDefinitionMetadata: { issuanceType: revocationRegistryDefinition.value.issuanceType, + didIndyNamespace: pool.didIndyNamespace, }, } } catch (error) { @@ -411,7 +411,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { error: 'notFound', message: `unable to resolve revocation registry definition: ${error.message}`, }, - revocationRegistryDefinition: null, revocationRegistryDefinitionId, revocationRegistryDefinitionMetadata: {}, } @@ -469,20 +468,18 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) { return { resolutionMetadata: { - didIndyNamespace: pool.didIndyNamespace, error: `error resolving revocation registry definition with id ${revocationRegistryId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, }, - revocationListMetadata: {}, - revocationList: null, + revocationListMetadata: { + didIndyNamespace: pool.didIndyNamespace, + }, } } const isIssuanceByDefault = revocationRegistryDefinitionMetadata.issuanceType === 'ISSUANCE_BY_DEFAULT' return { - resolutionMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, + resolutionMetadata: {}, revocationList: anonCredsRevocationListFromIndySdk( revocationRegistryId, revocationRegistryDefinition, @@ -490,7 +487,9 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { deltaTimestamp, isIssuanceByDefault ), - revocationListMetadata: {}, + revocationListMetadata: { + didIndyNamespace: pool.didIndyNamespace, + }, } } catch (error) { agentContext.config.logger.error( @@ -506,7 +505,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { error: 'notFound', message: `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, }, - revocationList: null, revocationListMetadata: {}, } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts index 49b619332d..e472d1c1c4 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts @@ -4,12 +4,13 @@ import type { CreateCredentialRequestOptions, CreateCredentialRequestReturn, CreateProofOptions, - CredentialInfo, + AnonCredsCredentialInfo, GetCredentialOptions, StoreCredentialOptions, GetCredentialsForProofRequestOptions, GetCredentialsForProofRequestReturn, - RequestedCredentials, + AnonCredsRequestedCredentials, + AnonCredsCredentialRequestMetadata, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { @@ -18,6 +19,8 @@ import type { RevStates, Schemas, IndyCredential as IndySdkCredential, + CredReqMetadata, + IndyProofRequest, } from 'indy-sdk' import { inject } from '@aries-framework/core' @@ -26,6 +29,7 @@ import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' import { getIndySeqNoFromUnqualifiedCredentialDefinitionId } from '../utils/identifiers' +import { generateLegacyProverDidLikeString } from '../utils/proverDid' import { indySdkCredentialDefinitionFromAnonCreds, indySdkRevocationRegistryDefinitionFromAnonCreds, @@ -84,7 +88,7 @@ export class IndySdkHolderService implements AnonCredsHolderService { const indyProof = await this.indySdk.proverCreateProof( agentContext.wallet.handle, - proofRequest, + proofRequest as IndyProofRequest, this.parseRequestedCredentials(requestedCredentials), agentContext.wallet.masterSecretId, indySchemas, @@ -122,7 +126,8 @@ export class IndySdkHolderService implements AnonCredsHolderService { return await this.indySdk.proverStoreCredential( agentContext.wallet.handle, options.credentialId ?? null, - options.credentialRequestMetadata, + // The type is typed as a Record in the indy-sdk, but the anoncreds package contains the correct type + options.credentialRequestMetadata as unknown as CredReqMetadata, options.credential, indySdkCredentialDefinitionFromAnonCreds(options.credentialDefinitionId, options.credentialDefinition), indyRevocationRegistryDefinition @@ -136,7 +141,10 @@ export class IndySdkHolderService implements AnonCredsHolderService { } } - public async getCredential(agentContext: AgentContext, options: GetCredentialOptions): Promise { + public async getCredential( + agentContext: AgentContext, + options: GetCredentialOptions + ): Promise { assertIndySdkWallet(agentContext.wallet) try { @@ -145,7 +153,7 @@ export class IndySdkHolderService implements AnonCredsHolderService { return { credentialDefinitionId: result.cred_def_id, attributes: result.attrs, - referent: result.referent, + credentialId: result.referent, schemaId: result.schema_id, credentialRevocationId: result.cred_rev_id, revocationRegistryId: result.rev_reg_id, @@ -165,10 +173,14 @@ export class IndySdkHolderService implements AnonCredsHolderService { ): Promise { assertIndySdkWallet(agentContext.wallet) + // 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 + const proverDid = generateLegacyProverDidLikeString() + try { const result = await this.indySdk.proverCreateCredentialReq( agentContext.wallet.handle, - options.holderDid, + proverDid, options.credentialOffer, // NOTE: Is it safe to use the cred_def_id from the offer? I think so. You can't create a request // for a cred def that is not in the offer @@ -180,7 +192,8 @@ export class IndySdkHolderService implements AnonCredsHolderService { return { credentialRequest: result[0], - credentialRequestMetadata: result[1], + // The type is typed as a Record in the indy-sdk, but the anoncreds package contains the correct type + credentialRequestMetadata: result[1] as unknown as AnonCredsCredentialRequestMetadata, } } catch (error) { agentContext.config.logger.error(`Error creating Indy Credential Request`, { @@ -216,7 +229,7 @@ export class IndySdkHolderService implements AnonCredsHolderService { // Open indy credential search const searchHandle = await this.indySdk.proverSearchCredentialsForProofReq( agentContext.wallet.handle, - options.proofRequest, + options.proofRequest as IndyProofRequest, options.extraQuery ?? null ) @@ -240,7 +253,7 @@ export class IndySdkHolderService implements AnonCredsHolderService { return credentials.map((credential) => ({ credentialInfo: { credentialDefinitionId: credential.cred_info.cred_def_id, - referent: credential.cred_info.referent, + credentialId: credential.cred_info.referent, attributes: credential.cred_info.attrs, schemaId: credential.cred_info.schema_id, revocationRegistryId: credential.cred_info.rev_reg_id, @@ -299,7 +312,7 @@ export class IndySdkHolderService implements AnonCredsHolderService { /** * Converts a public api form of {@link RequestedCredentials} interface into a format {@link Indy.IndyRequestedCredentials} that Indy SDK expects. **/ - private parseRequestedCredentials(requestedCredentials: RequestedCredentials): IndyRequestedCredentials { + private parseRequestedCredentials(requestedCredentials: AnonCredsRequestedCredentials): IndyRequestedCredentials { const indyRequestedCredentials: IndyRequestedCredentials = { requested_attributes: {}, requested_predicates: {}, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index f877be4f75..96e9ef266a 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -1,5 +1,4 @@ import type { CreateCredentialDefinitionMetadata } from './IndySdkIssuerServiceMetadata' -import type { IndySdkUtilitiesService } from './IndySdkUtilitiesService' import type { AnonCredsIssuerService, CreateCredentialDefinitionOptions, @@ -18,15 +17,15 @@ import { AriesFrameworkError, inject } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' +import { generateLegacyProverDidLikeString } from '../utils/proverDid' +import { createTailsReader } from '../utils/tails' import { indySdkSchemaFromAnonCreds } from '../utils/transform' export class IndySdkIssuerService implements AnonCredsIssuerService { private indySdk: IndySdk - private IndySdkUtilitiesService: IndySdkUtilitiesService - public constructor(IndySdkUtilitiesService: IndySdkUtilitiesService, @inject(IndySdkSymbol) indySdk: IndySdk) { + public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { this.indySdk = indySdk - this.IndySdkUtilitiesService = IndySdkUtilitiesService } public async createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise { @@ -73,7 +72,7 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { return { issuerId, tag: credentialDefinition.tag, - schemaId: credentialDefinition.schemaId, + schemaId, type: 'CL', value: credentialDefinition.value, } @@ -103,16 +102,19 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { assertIndySdkWallet(agentContext.wallet) try { // Indy SDK requires tailsReaderHandle. Use null if no tailsFilePath is present - const tailsReaderHandle = tailsFilePath ? await this.IndySdkUtilitiesService.createTailsReader(tailsFilePath) : 0 + const tailsReaderHandle = tailsFilePath ? await createTailsReader(agentContext, tailsFilePath) : 0 if (revocationRegistryId || tailsFilePath) { throw new AriesFrameworkError('Revocation not supported yet') } + // prover_did is deprecated and thus if not provided we generate something on our side, as it's still required by the indy sdk + const proverDid = credentialRequest.prover_did ?? generateLegacyProverDidLikeString() + const [credential, credentialRevocationId] = await this.indySdk.issuerCreateCredential( agentContext.wallet.handle, credentialOffer, - credentialRequest, + { ...credentialRequest, prover_did: proverDid }, credentialValues, revocationRegistryId ?? null, tailsReaderHandle diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts index 0ed637a6ee..4f7eb6ef42 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts @@ -2,9 +2,9 @@ import type { AnonCredsRevocationRegistryDefinition, AnonCredsRevocationList, AnonCredsProofRequest, - RequestedCredentials, - CredentialInfo, - NonRevokedInterval, + AnonCredsRequestedCredentials, + AnonCredsCredentialInfo, + AnonCredsNonRevokedInterval, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { RevStates } from 'indy-sdk' @@ -13,13 +13,12 @@ import { AriesFrameworkError, inject, injectable } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' +import { createTailsReader } from '../utils/tails' import { indySdkRevocationDeltaFromAnonCreds, indySdkRevocationRegistryDefinitionFromAnonCreds, } from '../utils/transform' -import { IndySdkUtilitiesService } from './IndySdkUtilitiesService' - enum RequestReferentType { Attribute = 'attribute', Predicate = 'predicate', @@ -34,11 +33,9 @@ enum RequestReferentType { @injectable() export class IndySdkRevocationService { private indySdk: IndySdk - private indySdkUtilitiesService: IndySdkUtilitiesService - public constructor(indyUtilitiesService: IndySdkUtilitiesService, @inject(IndySdkSymbol) indySdk: IndySdk) { + public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { this.indySdk = indySdk - this.indySdkUtilitiesService = indyUtilitiesService } /** @@ -47,7 +44,7 @@ export class IndySdkRevocationService { public async createRevocationState( agentContext: AgentContext, proofRequest: AnonCredsProofRequest, - requestedCredentials: RequestedCredentials, + requestedCredentials: AnonCredsRequestedCredentials, revocationRegistries: { [revocationRegistryDefinitionId: string]: { // Tails is already downloaded @@ -68,8 +65,8 @@ export class IndySdkRevocationService { const referentCredentials: Array<{ type: RequestReferentType referent: string - credentialInfo: CredentialInfo - referentRevocationInterval: NonRevokedInterval | undefined + credentialInfo: AnonCredsCredentialInfo + referentRevocationInterval: AnonCredsNonRevokedInterval | undefined }> = [] //Retrieve information for referents and push to single array @@ -114,7 +111,7 @@ export class IndySdkRevocationService { // most accurate revocation list for a given timestamp. It doesn't have to be that the revocationList is from the `to` timestamp however. const revocationList = revocationLists[requestRevocationInterval.to] - const tails = await this.indySdkUtilitiesService.createTailsReader(tailsFilePath) + const tails = await createTailsReader(agentContext, tailsFilePath) const revocationState = await this.indySdk.createRevocationState( tails, @@ -152,7 +149,7 @@ export class IndySdkRevocationService { // TODO: we should do this verification on a higher level I think? // Check revocation interval in accordance with https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0441-present-proof-best-practices/README.md#semantics-of-non-revocation-interval-endpoints private assertRevocationInterval( - revocationInterval: NonRevokedInterval + revocationInterval: AnonCredsNonRevokedInterval ): asserts revocationInterval is BestPracticeNonRevokedInterval { if (!revocationInterval.to) { throw new AriesFrameworkError(`Presentation requests proof of non-revocation with no 'to' value specified`) diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkUtilitiesService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkUtilitiesService.ts deleted file mode 100644 index 1ac0dec33e..0000000000 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkUtilitiesService.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { BlobReaderHandle } from 'indy-sdk' - -import { - AriesFrameworkError, - FileSystem, - getDirFromFilePath, - IndySdkError, - InjectionSymbols, - Logger, -} from '@aries-framework/core' -import { inject, injectable } from 'tsyringe' - -import { isIndyError } from '../../error' -import { IndySdk, IndySdkSymbol } from '../../types' - -@injectable() -export class IndySdkUtilitiesService { - private indySdk: IndySdk - private logger: Logger - private fileSystem: FileSystem - - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - @inject(InjectionSymbols.FileSystem) fileSystem: FileSystem, - @inject(IndySdkSymbol) indySdk: IndySdk - ) { - this.indySdk = indySdk - this.logger = logger - this.fileSystem = fileSystem - } - - /** - * Get a handler for the blob storage tails file reader. - * - * @param tailsFilePath The path of the tails file - * @returns The blob storage reader handle - */ - public async createTailsReader(tailsFilePath: string): Promise { - try { - this.logger.debug(`Opening tails reader at path ${tailsFilePath}`) - const tailsFileExists = await this.fileSystem.exists(tailsFilePath) - - // Extract directory from path (should also work with windows paths) - const dirname = getDirFromFilePath(tailsFilePath) - - if (!tailsFileExists) { - throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) - } - - const tailsReaderConfig = { - base_dir: dirname, - } - - const tailsReader = await this.indySdk.openBlobStorageReader('default', tailsReaderConfig) - this.logger.debug(`Opened tails reader at path ${tailsFilePath}`) - return tailsReader - } catch (error) { - if (isIndyError(error)) { - throw new IndySdkError(error) - } - - throw error - } - } -} diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts index d302e66c97..d07a4ef1ef 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts @@ -1,5 +1,5 @@ import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' -import type { CredentialDefs, Schemas, RevocRegDefs, RevRegs } from 'indy-sdk' +import type { CredentialDefs, Schemas, RevocRegDefs, RevRegs, IndyProofRequest } from 'indy-sdk' import { inject } from '@aries-framework/core' @@ -72,7 +72,7 @@ export class IndySdkVerifierService implements AnonCredsVerifierService { } return await this.indySdk.verifierVerifyProof( - options.proofRequest, + options.proofRequest as IndyProofRequest, options.proof, indySchemas, indyCredentialDefinitions, diff --git a/packages/indy-sdk/src/anoncreds/utils/proverDid.ts b/packages/indy-sdk/src/anoncreds/utils/proverDid.ts new file mode 100644 index 0000000000..2d12648c70 --- /dev/null +++ b/packages/indy-sdk/src/anoncreds/utils/proverDid.ts @@ -0,0 +1,12 @@ +import { TypedArrayEncoder, utils } from '@aries-framework/core' + +/** + * generates a string that adheres to the format of a legacy indy did. + * + * This can be used for the `prover_did` property that is required in the legacy anoncreds credential + * request. This doesn't actually have to be a did, but some frameworks (like ACA-Py) require it to be + * an unqualified indy did. + */ +export function generateLegacyProverDidLikeString() { + return TypedArrayEncoder.toBase58(TypedArrayEncoder.fromString(utils.uuid()).slice(0, 16)) +} diff --git a/packages/indy-sdk/src/anoncreds/utils/tails.ts b/packages/indy-sdk/src/anoncreds/utils/tails.ts new file mode 100644 index 0000000000..f803ea5d78 --- /dev/null +++ b/packages/indy-sdk/src/anoncreds/utils/tails.ts @@ -0,0 +1,45 @@ +import type { IndySdk } from '../../types' +import type { AgentContext, FileSystem } from '@aries-framework/core' + +import { AriesFrameworkError, getDirFromFilePath, IndySdkError, InjectionSymbols } from '@aries-framework/core' + +import { isIndyError } from '../../error' +import { IndySdkSymbol } from '../../types' + +/** + * Get a handler for the blob storage tails file reader. + * + * @param agentContext The agent context + * @param tailsFilePath The path of the tails file + * @returns The blob storage reader handle + */ +export async function createTailsReader(agentContext: AgentContext, tailsFilePath: string) { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) + + try { + agentContext.config.logger.debug(`Opening tails reader at path ${tailsFilePath}`) + const tailsFileExists = await fileSystem.exists(tailsFilePath) + + // Extract directory from path (should also work with windows paths) + const dirname = getDirFromFilePath(tailsFilePath) + + if (!tailsFileExists) { + throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) + } + + const tailsReaderConfig = { + base_dir: dirname, + } + + const tailsReader = await indySdk.openBlobStorageReader('default', tailsReaderConfig) + agentContext.config.logger.debug(`Opened tails reader at path ${tailsFilePath}`) + return tailsReader + } catch (error) { + if (isIndyError(error)) { + throw new IndySdkError(error) + } + + throw error + } +} diff --git a/yarn.lock b/yarn.lock index 30954778a4..0f10727077 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3479,7 +3479,7 @@ bindings@^1.3.1: dependencies: file-uri-to-path "1.0.0" -bn.js@^5.2.0: +bn.js@^5.2.0, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== From b5eb08e99d7ea61adefb8c6c0c5c99c6c1ba1597 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 30 Jan 2023 10:44:17 -0300 Subject: [PATCH 06/20] feat(indy-vdr): did:sov resolver (#1247) Signed-off-by: Ariel Gentile --- .../src/dids/IndyVdrSovDidResolver.ts | 95 +++++++++ .../__tests__/IndyVdrSovDidResolver.test.ts | 127 ++++++++++++ .../didSovR1xKJw17sUoXhejEpugMYJ.json | 51 +++++ .../didSovWJz9mHyW9BZksioQnRsrAo.json | 49 +++++ packages/indy-vdr/src/dids/didSovUtil.ts | 166 ++++++++++++++++ packages/indy-vdr/src/dids/index.ts | 1 + packages/indy-vdr/src/index.ts | 2 + .../indy-vdr/src/pool/IndyVdrPoolService.ts | 2 +- packages/indy-vdr/src/pool/index.ts | 1 + packages/indy-vdr/tests/helpers.ts | 43 ++++ .../tests/indy-vdr-did-resolver.e2e.test.ts | 188 ++++++++++++++++++ packages/indy-vdr/tests/setup.ts | 2 +- 12 files changed, 725 insertions(+), 2 deletions(-) create mode 100644 packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts create mode 100644 packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json create mode 100644 packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json create mode 100644 packages/indy-vdr/src/dids/didSovUtil.ts create mode 100644 packages/indy-vdr/src/dids/index.ts create mode 100644 packages/indy-vdr/tests/helpers.ts create mode 100644 packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts new file mode 100644 index 0000000000..bb51d4aeaa --- /dev/null +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -0,0 +1,95 @@ +import type { GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' +import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@aries-framework/core' + +import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' + +import { IndyVdrError, IndyVdrNotFoundError } from '../error' +import { IndyVdrPoolService } from '../pool' + +import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' + +export class IndyVdrSovDidResolver implements DidResolver { + public readonly supportedMethods = ['sov'] + + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { + const didDocumentMetadata = {} + + try { + const nym = await this.getPublicDid(agentContext, parsed.id) + const endpoints = await this.getEndpointsForDid(agentContext, parsed.id) + + const keyAgreementId = `${parsed.did}#key-agreement-1` + const builder = sovDidDocumentFromDid(parsed.did, nym.verkey) + addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) + + return { + didDocument: builder.build(), + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } + + private async getPublicDid(agentContext: AgentContext, did: string) { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = await indyVdrPoolService.getPoolForDid(agentContext, did) + + const request = new GetNymRequest({ dest: did }) + + const didResponse = await pool.submitReadRequest(request) + + if (!didResponse.result.data) { + throw new IndyVdrNotFoundError(`DID ${did} not found`) + } + return JSON.parse(didResponse.result.data) as GetNymResponseData + } + + private async getEndpointsForDid(agentContext: AgentContext, did: string) { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = await indyVdrPoolService.getPoolForDid(agentContext, did) + + try { + agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.indyNamespace}'`) + + const request = new GetAttribRequest({ targetDid: did, raw: 'endpoint' }) + + agentContext.config.logger.debug( + `Submitting get endpoint ATTRIB request for did '${did}' to ledger '${pool.indyNamespace}'` + ) + const response = await pool.submitReadRequest(request) + + if (!response.result.data) return {} + + const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib + agentContext.config.logger.debug( + `Got endpoints '${JSON.stringify(endpoints)}' for did '${did}' from ledger '${pool.indyNamespace}'`, + { + response, + endpoints, + } + ) + + return endpoints ?? {} + } catch (error) { + agentContext.config.logger.error( + `Error retrieving endpoints for did '${did}' from ledger '${pool.indyNamespace}'`, + { + error, + } + ) + + throw new IndyVdrError(error) + } + } +} diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts new file mode 100644 index 0000000000..269aaa1a46 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrSovDidResolver.test.ts @@ -0,0 +1,127 @@ +import { JsonTransformer } from '@aries-framework/core' + +import { parseDid } from '../../../../core/src/modules/dids/domain/parse' +import { getAgentConfig, getAgentContext, mockProperty } from '../../../../core/tests/helpers' +import { IndyVdrPool, IndyVdrPoolService } from '../../pool' +import { IndyVdrSovDidResolver } from '../IndyVdrSovDidResolver' + +import didSovR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' +import didSovWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' + +jest.mock('../../pool/IndyVdrPoolService') +const IndyVdrPoolServiceMock = IndyVdrPoolService as jest.Mock +const poolServiceMock = new IndyVdrPoolServiceMock() + +jest.mock('../../pool/IndyVdrPool') +const IndyVdrPoolMock = IndyVdrPool as jest.Mock +const poolMock = new IndyVdrPoolMock() +mockProperty(poolMock, 'indyNamespace', 'local') +jest.spyOn(poolServiceMock, 'getPoolForDid').mockResolvedValue(poolMock) + +const agentConfig = getAgentConfig('IndyVdrSovDidResolver') + +const agentContext = getAgentContext({ + agentConfig, + registerInstances: [[IndyVdrPoolService, poolServiceMock]], +}) + +const resolver = new IndyVdrSovDidResolver() + +describe('DidResolver', () => { + describe('IndyVdrSovDidResolver', () => { + it('should correctly resolve a did:sov document', async () => { + const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'R1xKJw17sUoXhejEpugMYJ', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://ssi.com', + profile: 'https://profile.com', + hub: 'https://hub.com', + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovR1xKJw17sUoXhejEpugMYJFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:sov document with routingKeys and types entries in the attrib', async () => { + const did = 'did:sov:WJz9mHyW9BZksioQnRsrAo' + + const nymResponse = { + result: { + data: JSON.stringify({ + did: 'WJz9mHyW9BZksioQnRsrAo', + verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', + role: 'ENDORSER', + }), + }, + } + + const attribResponse = { + result: { + data: JSON.stringify({ + endpoint: { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + }), + }, + } + + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(nymResponse) + jest.spyOn(poolMock, 'submitReadRequest').mockResolvedValueOnce(attribResponse) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovWJz9mHyW9BZksioQnRsrAoFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { + const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' + + jest.spyOn(poolMock, 'submitReadRequest').mockRejectedValue(new Error('Error submitting read request')) + + const result = await resolver.resolve(agentContext, did, parseDid(did)) + + expect(result).toMatchObject({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:sov:R1xKJw17sUoXhejEpugMYJ': Error: Error submitting read request`, + }, + }) + }) + }) +}) diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..6a6e4ed706 --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,51 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-1", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1", + "publicKeyBase58": "Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt" + } + ], + "authentication": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], + "assertionMethod": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], + "keyAgreement": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "service": [ + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://ssi.com" + }, + { + "accept": ["didcomm/aip2;env=rfc19"], + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#did-communication", + "priority": 0, + "recipientKeys": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "routingKeys": [], + "serviceEndpoint": "https://ssi.com", + "type": "did-communication" + }, + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#profile", + "serviceEndpoint": "https://profile.com", + "type": "profile" + }, + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#hub", + "serviceEndpoint": "https://hub.com", + "type": "hub" + } + ] +} diff --git a/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json new file mode 100644 index 0000000000..7b74e0587f --- /dev/null +++ b/packages/indy-vdr/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1", + "https://didcomm.org/messaging/contexts/v2" + ], + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-1", + "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", + "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" + } + ], + "authentication": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], + "assertionMethod": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], + "keyAgreement": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "service": [ + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#did-communication", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "routingKeys": ["routingKey1", "routingKey2"], + "accept": ["didcomm/aip2;env=rfc19"], + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#didcomm-1", + "type": "DIDComm", + "serviceEndpoint": "https://agent.com", + "accept": ["didcomm/v2"], + "routingKeys": ["routingKey1", "routingKey2"] + } + ] +} diff --git a/packages/indy-vdr/src/dids/didSovUtil.ts b/packages/indy-vdr/src/dids/didSovUtil.ts new file mode 100644 index 0000000000..9fbe414b78 --- /dev/null +++ b/packages/indy-vdr/src/dids/didSovUtil.ts @@ -0,0 +1,166 @@ +import { + TypedArrayEncoder, + DidDocumentService, + DidDocumentBuilder, + DidCommV1Service, + DidCommV2Service, + convertPublicKeyToX25519, +} from '@aries-framework/core' + +export interface IndyEndpointAttrib { + endpoint?: string + types?: Array<'endpoint' | 'did-communication' | 'DIDComm'> + routingKeys?: string[] + [key: string]: unknown +} + +export interface GetNymResponseData { + did: string + verkey: string + role: string +} + +export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ + +/** + * Check a base58 encoded string against a regex expression to determine if it is a full valid verkey + * @param verkey Base58 encoded string representation of a verkey + * @return Boolean indicating if the string is a valid verkey + */ +export function isFullVerkey(verkey: string): boolean { + return FULL_VERKEY_REGEX.test(verkey) +} + +export function getFullVerkey(did: string, verkey: string) { + if (isFullVerkey(verkey)) return verkey + + // Did could have did:xxx prefix, only take the last item after : + const id = did.split(':').pop() ?? did + // Verkey is prefixed with ~ if abbreviated + const verkeyWithoutTilde = verkey.slice(1) + + // Create base58 encoded public key (32 bytes) + return TypedArrayEncoder.toBase58( + Buffer.concat([ + // Take did identifier (16 bytes) + TypedArrayEncoder.fromBase58(id), + // Concat the abbreviated verkey (16 bytes) + TypedArrayEncoder.fromBase58(verkeyWithoutTilde), + ]) + ) +} + +export function sovDidDocumentFromDid(fullDid: string, verkey: string) { + const verificationMethodId = `${fullDid}#key-1` + const keyAgreementId = `${fullDid}#key-agreement-1` + + const publicKeyBase58 = getFullVerkey(fullDid, verkey) + const publicKeyX25519 = TypedArrayEncoder.toBase58( + convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) + ) + + const builder = new DidDocumentBuilder(fullDid) + .addContext('https://w3id.org/security/suites/ed25519-2018/v1') + .addContext('https://w3id.org/security/suites/x25519-2019/v1') + .addVerificationMethod({ + controller: fullDid, + id: verificationMethodId, + publicKeyBase58: publicKeyBase58, + type: 'Ed25519VerificationKey2018', + }) + .addVerificationMethod({ + controller: fullDid, + id: keyAgreementId, + publicKeyBase58: publicKeyX25519, + type: 'X25519KeyAgreementKey2019', + }) + .addAuthentication(verificationMethodId) + .addAssertionMethod(verificationMethodId) + .addKeyAgreement(keyAgreementId) + + return builder +} + +// Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint +function processEndpointTypes(types?: string[]) { + const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] + const defaultTypes = ['endpoint', 'did-communication'] + + // Return default types if types "is NOT present [or] empty" + if (!types || types.length <= 0) { + return defaultTypes + } + + // Return default types if types "contain any other values" + for (const type of types) { + if (!expectedTypes.includes(type)) { + return defaultTypes + } + } + + // Return provided types + return types +} + +export function addServicesFromEndpointsAttrib( + builder: DidDocumentBuilder, + did: string, + endpoints: IndyEndpointAttrib, + keyAgreementId: string +) { + const { endpoint, routingKeys, types, ...otherEndpoints } = endpoints + + if (endpoint) { + const processedTypes = processEndpointTypes(types) + + // If 'endpoint' included in types, add id to the services array + if (processedTypes.includes('endpoint')) { + builder.addService( + new DidDocumentService({ + id: `${did}#endpoint`, + serviceEndpoint: endpoint, + type: 'endpoint', + }) + ) + } + + // If 'did-communication' included in types, add DIDComm v1 entry + if (processedTypes.includes('did-communication')) { + builder.addService( + new DidCommV1Service({ + id: `${did}#did-communication`, + serviceEndpoint: endpoint, + priority: 0, + routingKeys: routingKeys ?? [], + recipientKeys: [keyAgreementId], + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + + // If 'DIDComm' included in types, add DIDComm v2 entry + if (processedTypes.includes('DIDComm')) { + builder + .addService( + new DidCommV2Service({ + id: `${did}#didcomm-1`, + serviceEndpoint: endpoint, + routingKeys: routingKeys ?? [], + accept: ['didcomm/v2'], + }) + ) + .addContext('https://didcomm.org/messaging/contexts/v2') + } + } + } + + // Add other endpoint types + for (const [type, endpoint] of Object.entries(otherEndpoints)) { + builder.addService( + new DidDocumentService({ + id: `${did}#${type}`, + serviceEndpoint: endpoint as string, + type, + }) + ) + } +} diff --git a/packages/indy-vdr/src/dids/index.ts b/packages/indy-vdr/src/dids/index.ts new file mode 100644 index 0000000000..7f9973684d --- /dev/null +++ b/packages/indy-vdr/src/dids/index.ts @@ -0,0 +1 @@ +export { IndyVdrSovDidResolver } from './IndyVdrSovDidResolver' diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts index 8a5ca6c21a..ca0fe42285 100644 --- a/packages/indy-vdr/src/index.ts +++ b/packages/indy-vdr/src/index.ts @@ -1,3 +1,5 @@ +export { IndyVdrSovDidResolver } from './dids' + try { // eslint-disable-next-line import/no-extraneous-dependencies require('indy-vdr-test-nodejs') diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts index 2cc1b5a206..0ac5d9a1aa 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -188,7 +188,7 @@ export class IndyVdrPoolService { this.logger.trace(`Retrieved did '${did}' from ledger '${pool.indyNamespace}'`, result) return { - did: result, + did: { nymResponse: { did: result.dest, verkey: result.verkey }, indyNamespace: pool.indyNamespace }, pool, response, } diff --git a/packages/indy-vdr/src/pool/index.ts b/packages/indy-vdr/src/pool/index.ts index 1e1f1b52f8..ec4bc06677 100644 --- a/packages/indy-vdr/src/pool/index.ts +++ b/packages/indy-vdr/src/pool/index.ts @@ -1 +1,2 @@ export * from './IndyVdrPool' +export * from './IndyVdrPoolService' diff --git a/packages/indy-vdr/tests/helpers.ts b/packages/indy-vdr/tests/helpers.ts new file mode 100644 index 0000000000..7ea99d8263 --- /dev/null +++ b/packages/indy-vdr/tests/helpers.ts @@ -0,0 +1,43 @@ +import type { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' +import type { AgentContext, Key } from '@aries-framework/core' + +import { KeyType } from '@aries-framework/core' +import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' + +import { indyDidFromPublicKeyBase58 } from '../src/utils/did' + +export async function createDidOnLedger( + indyVdrPoolService: IndyVdrPoolService, + agentContext: AgentContext, + submitterDid: string, + signerKey: Key +) { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + + const key = await agentContext.wallet.createKey({ keyType: KeyType.Ed25519 }) + const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + const nymRequest = new NymRequest({ + dest: did, + submitterDid, + verkey: key.publicKeyBase58, + }) + + await pool.submitWriteRequest(agentContext, nymRequest, signerKey) + + const attribRequest = new AttribRequest({ + submitterDid: did, + targetDid: did, + raw: JSON.stringify({ + endpoint: { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + }), + }) + + await pool.submitWriteRequest(agentContext, attribRequest, key) + + return { did, key } +} diff --git a/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts new file mode 100644 index 0000000000..72a09afc83 --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-did-resolver.e2e.test.ts @@ -0,0 +1,188 @@ +import type { Key } from '@aries-framework/core' + +import { + CacheModuleConfig, + InMemoryLruCache, + JsonTransformer, + IndyWallet, + KeyType, + SigningProviderRegistry, +} from '@aries-framework/core' + +import { parseDid } from '../../core/src/modules/dids/domain/parse' +import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { IndyVdrSovDidResolver } from '../src/dids' +import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' +import { indyDidFromPublicKeyBase58 } from '../src/utils/did' + +import { createDidOnLedger } from './helpers' + +const logger = testLogger +const wallet = new IndyWallet(agentDependencies, logger, new SigningProviderRegistry([])) +const agentConfig = getAgentConfig('IndyVdrResolver E2E', { logger }) + +const cache = new InMemoryLruCache({ limit: 200 }) +const indyVdrSovDidResolver = new IndyVdrSovDidResolver() + +const config = { + isProduction: false, + genesisTransactions, + indyNamespace: `pool:localtest`, + transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, +} as const + +let signerKey: Key + +const agentContext = getAgentContext({ + wallet, + agentConfig, + registerInstances: [ + [IndyVdrPoolService, new IndyVdrPoolService(logger)], + [CacheModuleConfig, new CacheModuleConfig({ cache })], + ], +}) + +const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) +indyVdrPoolService.setPools([config]) + +describe('IndyVdrSov', () => { + beforeAll(async () => { + await indyVdrPoolService.connectToPools() + + if (agentConfig.walletConfig) { + await wallet.createAndOpen(agentConfig.walletConfig) + } + + signerKey = await wallet.createKey({ seed: '000000000000000000000000Trustee9', keyType: KeyType.Ed25519 }) + }) + + afterAll(async () => { + for (const pool of indyVdrPoolService.pools) { + pool.close() + } + + await wallet.delete() + }) + + describe('did:sov resolver', () => { + test('can resolve a did sov using the pool', async () => { + const did = 'did:sov:TL1EaPFCZ8Si5aUrqScBDt' + const didResult = await indyVdrSovDidResolver.resolve(agentContext, did, parseDid(did)) + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#key-1`, + publicKeyBase58: expect.any(String), + }, + { + controller: did, + type: 'X25519KeyAgreementKey2019', + id: `${did}#key-agreement-1`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#key-1`], + assertionMethod: [`${did}#key-1`], + keyAgreement: [`${did}#key-agreement-1`], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + test('resolve a did with endpoints', async () => { + // First we need to create a new DID and add ATTRIB endpoint to it + const { did } = await createDidOnLedger( + indyVdrPoolService, + agentContext, + indyDidFromPublicKeyBase58(signerKey.publicKeyBase58), + signerKey + ) + + // DID created. Now resolve it + + const fullyQualifiedDid = `did:sov:${did}` + const didResult = await indyVdrSovDidResolver.resolve( + agentContext, + fullyQualifiedDid, + parseDid(fullyQualifiedDid) + ) + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: fullyQualifiedDid, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: fullyQualifiedDid, + id: `${fullyQualifiedDid}#key-1`, + publicKeyBase58: expect.any(String), + }, + { + controller: fullyQualifiedDid, + type: 'X25519KeyAgreementKey2019', + id: `${fullyQualifiedDid}#key-agreement-1`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${fullyQualifiedDid}#key-1`], + assertionMethod: [`${fullyQualifiedDid}#key-1`], + keyAgreement: [`${fullyQualifiedDid}#key-agreement-1`], + service: [ + { + id: `${fullyQualifiedDid}#endpoint`, + type: 'endpoint', + serviceEndpoint: 'https://agent.com', + }, + { + id: `${fullyQualifiedDid}#did-communication`, + type: 'did-communication', + priority: 0, + recipientKeys: [`${fullyQualifiedDid}#key-agreement-1`], + routingKeys: ['routingKey1', 'routingKey2'], + accept: ['didcomm/aip2;env=rfc19'], + serviceEndpoint: 'https://agent.com', + }, + { + id: `${fullyQualifiedDid}#didcomm-1`, + type: 'DIDComm', + serviceEndpoint: 'https://agent.com', + accept: ['didcomm/v2'], + routingKeys: ['routingKey1', 'routingKey2'], + }, + ], + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + }) +}) diff --git a/packages/indy-vdr/tests/setup.ts b/packages/indy-vdr/tests/setup.ts index ce7749d25e..d69181fd10 100644 --- a/packages/indy-vdr/tests/setup.ts +++ b/packages/indy-vdr/tests/setup.ts @@ -1,4 +1,4 @@ // Needed to register indy-vdr node bindings import '../src/index' -jest.setTimeout(20000) +jest.setTimeout(60000) From acdb20a79d038fb4163d281ee8de0ccb649fdc32 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 2 Feb 2023 12:23:53 -0300 Subject: [PATCH 07/20] feat(indy-vdr): use @hyperledger packages (#1252) Signed-off-by: Ariel Gentile --- packages/indy-vdr/package.json | 6 +-- .../src/dids/IndyVdrSovDidResolver.ts | 2 +- packages/indy-vdr/src/index.ts | 2 +- packages/indy-vdr/src/pool/IndyVdrPool.ts | 11 ++--- .../indy-vdr/src/pool/IndyVdrPoolService.ts | 4 +- packages/indy-vdr/tests/helpers.ts | 2 +- .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 2 +- yarn.lock | 44 ++++++++++++------- 8 files changed, 42 insertions(+), 31 deletions(-) diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index 32c8689d5d..e12d0116de 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -26,11 +26,11 @@ }, "dependencies": { "@aries-framework/core": "0.3.3", - "indy-vdr-test-shared": "^0.1.3" + "@hyperledger/indy-vdr-shared": "^0.1.0-dev.4" }, "devDependencies": { - "indy-vdr-test-nodejs": "^0.1.3", - "rimraf": "~4.0.7", + "@hyperledger/indy-vdr-nodejs": "^0.1.0-dev.4", + "rimraf": "^4.0.7", "typescript": "~4.9.4" } } diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts index bb51d4aeaa..92fbefa20f 100644 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -1,7 +1,7 @@ import type { GetNymResponseData, IndyEndpointAttrib } from './didSovUtil' import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@aries-framework/core' -import { GetAttribRequest, GetNymRequest } from 'indy-vdr-test-shared' +import { GetAttribRequest, GetNymRequest } from '@hyperledger/indy-vdr-shared' import { IndyVdrError, IndyVdrNotFoundError } from '../error' import { IndyVdrPoolService } from '../pool' diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts index ca0fe42285..8278d55827 100644 --- a/packages/indy-vdr/src/index.ts +++ b/packages/indy-vdr/src/index.ts @@ -2,7 +2,7 @@ export { IndyVdrSovDidResolver } from './dids' try { // eslint-disable-next-line import/no-extraneous-dependencies - require('indy-vdr-test-nodejs') + require('@hyperledger/indy-vdr-nodejs') } catch (error) { throw new Error('Error registering nodejs bindings for Indy VDR') } diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts index 6958d882ab..99ba0d1b06 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPool.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -1,5 +1,5 @@ import type { Logger, AgentContext, Key } from '@aries-framework/core' -import type { IndyVdrRequest, IndyVdrPool as indyVdrPool } from 'indy-vdr-test-shared' +import type { IndyVdrRequest, IndyVdrPool as indyVdrPool } from '@hyperledger/indy-vdr-shared' import { TypedArrayEncoder } from '@aries-framework/core' import { @@ -7,7 +7,7 @@ import { GetAcceptanceMechanismsRequest, PoolCreate, indyVdr, -} from 'indy-vdr-test-shared' +} from '@hyperledger/indy-vdr-shared' import { IndyVdrError } from '../error' @@ -146,7 +146,9 @@ export class IndyVdrPool { acceptanceMechanismType: poolTaa.acceptanceMechanism, }) - request.setTransactionAuthorAgreementAcceptance({ acceptance }) + request.setTransactionAuthorAgreementAcceptance({ + acceptance: JSON.parse(acceptance), + }) } private async getTransactionAuthorAgreement(): Promise { @@ -172,8 +174,7 @@ export class IndyVdrPool { // If TAA is not null, we can be sure AcceptanceMechanisms is also not null const authorAgreement = taaData as Omit - // FIME: remove cast when https://github.com/hyperledger/indy-vdr/pull/142 is released - const acceptanceMechanisms = acceptanceMechanismResponse.result.data as unknown as AcceptanceMechanisms + const acceptanceMechanisms = acceptanceMechanismResponse.result.data as AcceptanceMechanisms this.authorAgreement = { ...authorAgreement, acceptanceMechanisms, diff --git a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts index 0ac5d9a1aa..b6a1a0f989 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPoolService.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPoolService.ts @@ -1,9 +1,9 @@ import type { IndyVdrPoolConfig } from './IndyVdrPool' import type { AgentContext } from '@aries-framework/core' -import type { GetNymResponse } from 'indy-vdr-test-shared' +import type { GetNymResponse } from '@hyperledger/indy-vdr-shared' import { Logger, InjectionSymbols, injectable, inject, CacheModuleConfig } from '@aries-framework/core' -import { GetNymRequest } from 'indy-vdr-test-shared' +import { GetNymRequest } from '@hyperledger/indy-vdr-shared' import { IndyVdrError, IndyVdrNotFoundError, IndyVdrNotConfiguredError } from '../error' import { isSelfCertifiedDid, DID_INDY_REGEX } from '../utils/did' diff --git a/packages/indy-vdr/tests/helpers.ts b/packages/indy-vdr/tests/helpers.ts index 7ea99d8263..2ac21a7711 100644 --- a/packages/indy-vdr/tests/helpers.ts +++ b/packages/indy-vdr/tests/helpers.ts @@ -2,7 +2,7 @@ import type { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import type { AgentContext, Key } from '@aries-framework/core' import { KeyType } from '@aries-framework/core' -import { AttribRequest, NymRequest } from 'indy-vdr-test-shared' +import { AttribRequest, NymRequest } from '@hyperledger/indy-vdr-shared' import { indyDidFromPublicKeyBase58 } from '../src/utils/did' diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index 5920344527..52bd467cd5 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -1,7 +1,7 @@ import type { Key } from '@aries-framework/core' import { IndyWallet, KeyType, SigningProviderRegistry, TypedArrayEncoder } from '@aries-framework/core' -import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from 'indy-vdr-test-shared' +import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from '@hyperledger/indy-vdr-shared' import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' diff --git a/yarn.lock b/yarn.lock index 0f10727077..945e21eff3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -858,6 +858,23 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@hyperledger/indy-vdr-nodejs@^0.1.0-dev.4": + version "0.1.0-dev.4" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.1.0-dev.4.tgz#b5d2090b30c4a51e4e4f15a024054aada0d3550e" + integrity sha512-SwvcoOONhxD9LaX7vunNi1KFKmDb8wmutkBI+Hl6JMX3R+0QgpyQx5M3cfp+V34fBS8pqzKbq9lQmo+pDu3IWg== + dependencies: + "@hyperledger/indy-vdr-shared" "0.1.0-dev.4" + "@mapbox/node-pre-gyp" "^1.0.10" + ffi-napi "^4.0.3" + ref-array-di "^1.2.2" + ref-napi "^3.0.3" + ref-struct-di "^1.1.1" + +"@hyperledger/indy-vdr-shared@0.1.0-dev.4", "@hyperledger/indy-vdr-shared@^0.1.0-dev.4": + version "0.1.0-dev.4" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-shared/-/indy-vdr-shared-0.1.0-dev.4.tgz#ad9ff18ea285cf3c8ba0b4a5bff03c02f57898e4" + integrity sha512-M6AnLQNryEqcWiH8oNNI/ovkFOykFg7zlO4oM+1xMbHbNzAe6ShBYQDB189zTQAG4RUkuA8yiLHt90g/q6N8dg== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -6139,23 +6156,6 @@ indy-sdk@^1.16.0-dev-1636: nan "^2.11.1" node-gyp "^8.0.0" -indy-vdr-test-nodejs@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/indy-vdr-test-nodejs/-/indy-vdr-test-nodejs-0.1.3.tgz#97eaf38b1035bfabcd772a8399f23d766dfd493e" - integrity sha512-E6r86QGbswa+hBgMJKVWJycqvvmOgepFMDaAvuZQtxQK1Z2gghco6m/9EOAPYaJRs0MMEEhzUGhvtSpCzeZ6sg== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.10" - ffi-napi "^4.0.3" - indy-vdr-test-shared "0.1.3" - ref-array-di "^1.2.2" - ref-napi "^3.0.3" - ref-struct-di "^1.1.1" - -indy-vdr-test-shared@0.1.3, indy-vdr-test-shared@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/indy-vdr-test-shared/-/indy-vdr-test-shared-0.1.3.tgz#3b5ee9492ebc3367a027670aa9686c493de5929c" - integrity sha512-fdgV388zi3dglu49kqrV+i40w+18uJkv96Tk4nziLdP280SLnZKKnIRAiq11Hj8aHpnZmwMloyQCsIyQZDZk2g== - infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -11035,6 +11035,16 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" From f1e493799f3b71942b2263010f2661f7839d8324 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 5 Feb 2023 17:00:27 +0100 Subject: [PATCH 08/20] ci: forceExit and bail tests (#1266) Signed-off-by: Timo Glastra --- .github/workflows/continuous-integration.yml | 2 +- packages/anoncreds/jest.config.ts | 2 +- packages/anoncreds/tests/setup.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 packages/anoncreds/tests/setup.ts diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 4134d8c5f1..6890536c12 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -108,7 +108,7 @@ jobs: run: yarn install - name: Run tests - run: TEST_AGENT_PUBLIC_DID_SEED=${TEST_AGENT_PUBLIC_DID_SEED} GENESIS_TXN_PATH=${GENESIS_TXN_PATH} yarn test --coverage + run: TEST_AGENT_PUBLIC_DID_SEED=${TEST_AGENT_PUBLIC_DID_SEED} GENESIS_TXN_PATH=${GENESIS_TXN_PATH} yarn test --coverage --forceExit --bail - uses: codecov/codecov-action@v1 if: always() diff --git a/packages/anoncreds/jest.config.ts b/packages/anoncreds/jest.config.ts index c7c5196637..55c67d70a6 100644 --- a/packages/anoncreds/jest.config.ts +++ b/packages/anoncreds/jest.config.ts @@ -8,7 +8,7 @@ const config: Config.InitialOptions = { ...base, name: packageJson.name, displayName: packageJson.name, - // setupFilesAfterEnv: ['./tests/setup.ts'], + setupFilesAfterEnv: ['./tests/setup.ts'], } export default config diff --git a/packages/anoncreds/tests/setup.ts b/packages/anoncreds/tests/setup.ts new file mode 100644 index 0000000000..719a473b6e --- /dev/null +++ b/packages/anoncreds/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(10000) From 3a4c5ecd940e49d4d192eef1d41f2aaedb34d85a Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 6 Feb 2023 21:49:12 +0100 Subject: [PATCH 09/20] feat(anoncreds): add anoncreds API (#1232) Signed-off-by: Timo Glastra --- packages/anoncreds/src/AnonCredsApi.ts | 428 ++++++++++++++++++ packages/anoncreds/src/AnonCredsApiOptions.ts | 4 + packages/anoncreds/src/AnonCredsModule.ts | 16 + .../src/__tests__/AnonCredsModule.test.ts | 14 +- .../src/error/AnonCredsStoreRecordError.ts | 7 + packages/anoncreds/src/error/index.ts | 1 + .../LegacyIndyCredentialFormatService.test.ts | 2 +- packages/anoncreds/src/index.ts | 4 + packages/anoncreds/src/models/exchange.ts | 2 +- packages/anoncreds/src/models/registry.ts | 2 +- ...nCredsCredentialDefinitionPrivateRecord.ts | 41 ++ ...dsCredentialDefinitionPrivateRepository.ts | 23 + .../AnonCredsCredentialDefinitionRecord.ts | 50 ++ ...AnonCredsCredentialDefinitionRepository.ts | 23 + .../AnonCredsKeyCorrectnessProofRecord.ts | 41 ++ .../AnonCredsKeyCorrectnessProofRepository.ts | 23 + .../repository/AnonCredsLinkSecretRecord.ts | 42 ++ .../AnonCredsLinkSecretRepository.ts | 31 ++ .../src/repository/AnonCredsSchemaRecord.ts | 50 ++ .../repository/AnonCredsSchemaRepository.ts | 23 + ...CredentialDefinitionRecordMetadataTypes.ts | 11 + .../anonCredsSchemaRecordMetadataTypes.ts | 11 + packages/anoncreds/src/repository/index.ts | 10 + .../src/services/AnonCredsHolderService.ts | 4 + .../services/AnonCredsHolderServiceOptions.ts | 16 +- .../src/services/AnonCredsIssuerService.ts | 5 +- .../services/AnonCredsIssuerServiceOptions.ts | 8 +- .../AnonCredsVerifierServiceOptions.ts | 6 +- .../services/registry/AnonCredsRegistry.ts | 7 +- .../registry/AnonCredsRegistryService.ts | 2 +- ...ions.ts => RevocationStatusListOptions.ts} | 8 +- .../AnonCredsRegistryService.test.ts | 2 +- .../anoncreds/src/services/registry/index.ts | 2 +- .../tests/InMemoryAnonCredsRegistry.ts | 83 +++- packages/anoncreds/tests/anoncreds.test.ts | 312 +++++++++++++ packages/anoncreds/tests/setup.ts | 2 +- packages/indy-sdk/src/IndySdkModule.ts | 18 + .../services/IndySdkAnonCredsRegistry.ts | 16 +- .../services/IndySdkHolderService.ts | 30 +- .../services/IndySdkIssuerService.ts | 19 +- .../services/IndySdkRevocationService.ts | 18 +- .../services/IndySdkVerifierService.ts | 11 +- .../utils/__tests__/transform.test.ts | 2 +- .../indy-sdk/src/anoncreds/utils/transform.ts | 20 +- 44 files changed, 1370 insertions(+), 80 deletions(-) create mode 100644 packages/anoncreds/src/AnonCredsApi.ts create mode 100644 packages/anoncreds/src/AnonCredsApiOptions.ts create mode 100644 packages/anoncreds/src/error/AnonCredsStoreRecordError.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts create mode 100644 packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts create mode 100644 packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts create mode 100644 packages/anoncreds/src/repository/index.ts rename packages/anoncreds/src/services/registry/{RevocationListOptions.ts => RevocationStatusListOptions.ts} (70%) create mode 100644 packages/anoncreds/tests/anoncreds.test.ts diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts new file mode 100644 index 0000000000..b52f4dbc0f --- /dev/null +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -0,0 +1,428 @@ +import type { AnonCredsCreateLinkSecretOptions } from './AnonCredsApiOptions' +import type { AnonCredsCredentialDefinition } from './models' +import type { + GetCredentialDefinitionReturn, + GetRevocationStatusListReturn, + GetRevocationRegistryDefinitionReturn, + GetSchemaReturn, + RegisterCredentialDefinitionReturn, + RegisterSchemaOptions, + RegisterSchemaReturn, +} from './services' +import type { Extensible } from './services/registry/base' + +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(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 + } + } + + /** + * 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: Omit + // 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(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 + } + } + + /** + * 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 + } + } + + private async storeCredentialDefinitionRecord( + 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, + }) + + // 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(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, + }) + + 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..78a8e77728 --- /dev/null +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -0,0 +1,4 @@ +export interface AnonCredsCreateLinkSecretOptions { + linkSecretId?: string + setAsDefault?: boolean +} diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index 0da6e242f7..3d6eff0b74 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -1,7 +1,15 @@ import type { AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' import type { DependencyManager, Module } 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' /** @@ -9,6 +17,7 @@ import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryS */ export class AnonCredsModule implements Module { public readonly config: AnonCredsModuleConfig + public api = AnonCredsApi public constructor(config: AnonCredsModuleConfigOptions) { this.config = new AnonCredsModuleConfig(config) @@ -19,5 +28,12 @@ export class AnonCredsModule implements Module { dependencyManager.registerInstance(AnonCredsModuleConfig, this.config) dependencyManager.registerSingleton(AnonCredsRegistryService) + + // Repositories + dependencyManager.registerSingleton(AnonCredsSchemaRepository) + dependencyManager.registerSingleton(AnonCredsCredentialDefinitionRepository) + dependencyManager.registerSingleton(AnonCredsCredentialDefinitionPrivateRepository) + dependencyManager.registerSingleton(AnonCredsKeyCorrectnessProofRepository) + dependencyManager.registerSingleton(AnonCredsLinkSecretRepository) } } diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts index 90aa51ce66..f9c868c14c 100644 --- a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -3,6 +3,13 @@ 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 = { @@ -19,8 +26,13 @@ describe('AnonCredsModule', () => { }) anonCredsModule.register(dependencyManager) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + 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/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 index d9786950bf..6d25bc4dbb 100644 --- a/packages/anoncreds/src/error/index.ts +++ b/packages/anoncreds/src/error/index.ts @@ -1 +1,2 @@ export * from './AnonCredsError' +export * from './AnonCredsStoreRecordError' diff --git a/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts b/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts index 7e1e1909da..2449c81124 100644 --- a/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts +++ b/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts @@ -77,7 +77,7 @@ describe('LegacyIndyCredentialFormatService', () => { options: {}, }) - const credentialDefinition = await anonCredsIssuerService.createCredentialDefinition( + const { credentialDefinition } = await anonCredsIssuerService.createCredentialDefinition( agentContext, { issuerId: indyDid, diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index 759e343c2c..9ef264f501 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -1,5 +1,9 @@ export * from './models' export * from './services' export * from './error' +export * from './repository' export { AnonCredsModule } from './AnonCredsModule' export { AnonCredsModuleConfig, AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' +export { AnonCredsApi } from './AnonCredsApi' +export { LegacyIndyCredentialFormatService } from './formats/LegacyIndyCredentialFormatService' +export { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index 40713b227d..b0e960afb8 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -1,4 +1,4 @@ -interface AnonCredsProofRequestRestriction { +export interface AnonCredsProofRequestRestriction { schema_id?: string schema_issuer_id?: string schema_name?: string diff --git a/packages/anoncreds/src/models/registry.ts b/packages/anoncreds/src/models/registry.ts index 1e5e6d7879..f4f3429ec2 100644 --- a/packages/anoncreds/src/models/registry.ts +++ b/packages/anoncreds/src/models/registry.ts @@ -32,7 +32,7 @@ export interface AnonCredsRevocationRegistryDefinition { tailsHash: string } -export interface AnonCredsRevocationList { +export interface AnonCredsRevocationStatusList { issuerId: string revRegId: string revocationList: number[] diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts new file mode 100644 index 0000000000..bc0c1c99ee --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts @@ -0,0 +1,41 @@ +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsCredentialDefinitionPrivateRecordProps { + id?: string + credentialDefinitionId: string + value: Record +} + +export type DefaultAnonCredsCredentialDefinitionPrivateTags = { + credentialDefinitionId: string +} + +export class AnonCredsCredentialDefinitionPrivateRecord extends BaseRecord< + DefaultAnonCredsCredentialDefinitionPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsCredentialDefinitionPrivateRecord' + public readonly type = AnonCredsCredentialDefinitionPrivateRecord.type + + public readonly credentialDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public constructor(props: AnonCredsCredentialDefinitionPrivateRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.value = props.value + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts new file mode 100644 index 0000000000..31c7737143 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsCredentialDefinitionPrivateRecord } from './AnonCredsCredentialDefinitionPrivateRecord' + +@injectable() +export class AnonCredsCredentialDefinitionPrivateRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsCredentialDefinitionPrivateRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts new file mode 100644 index 0000000000..f9c7df43f7 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts @@ -0,0 +1,50 @@ +import type { AnonCredsCredentialDefinitionRecordMetadata } from './anonCredsCredentialDefinitionRecordMetadataTypes' +import type { AnonCredsCredentialDefinition } from '../models' +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsCredentialDefinitionRecordProps { + id?: string + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition +} + +export type DefaultAnonCredsCredentialDefinitionTags = { + schemaId: string + credentialDefinitionId: string + issuerId: string + tag: string +} + +export class AnonCredsCredentialDefinitionRecord extends BaseRecord< + DefaultAnonCredsCredentialDefinitionTags, + TagsBase, + AnonCredsCredentialDefinitionRecordMetadata +> { + public static readonly type = 'AnonCredsCredentialDefinitionRecord' + public readonly type = AnonCredsCredentialDefinitionRecord.type + + public readonly credentialDefinitionId!: string + public readonly credentialDefinition!: AnonCredsCredentialDefinition + + public constructor(props: AnonCredsCredentialDefinitionRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.credentialDefinition = props.credentialDefinition + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + schemaId: this.credentialDefinition.schemaId, + issuerId: this.credentialDefinition.issuerId, + tag: this.credentialDefinition.tag, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts new file mode 100644 index 0000000000..7677dd76b8 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsCredentialDefinitionRecord } from './AnonCredsCredentialDefinitionRecord' + +@injectable() +export class AnonCredsCredentialDefinitionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsCredentialDefinitionRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts new file mode 100644 index 0000000000..cac331bd6c --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts @@ -0,0 +1,41 @@ +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsKeyCorrectnessProofRecordProps { + id?: string + credentialDefinitionId: string + value: Record +} + +export type DefaultAnonCredsKeyCorrectnessProofPrivateTags = { + credentialDefinitionId: string +} + +export class AnonCredsKeyCorrectnessProofRecord extends BaseRecord< + DefaultAnonCredsKeyCorrectnessProofPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsKeyCorrectnessProofRecord' + public readonly type = AnonCredsKeyCorrectnessProofRecord.type + + public readonly credentialDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public constructor(props: AnonCredsKeyCorrectnessProofRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.value = props.value + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts new file mode 100644 index 0000000000..959ba8b4a5 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsKeyCorrectnessProofRecord } from './AnonCredsKeyCorrectnessProofRecord' + +@injectable() +export class AnonCredsKeyCorrectnessProofRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsKeyCorrectnessProofRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts b/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts new file mode 100644 index 0000000000..ffb775526e --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts @@ -0,0 +1,42 @@ +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsLinkSecretRecordProps { + id?: string + linkSecretId: string + value?: string // If value is not provided, only reference to link secret is stored in regular storage +} + +export type DefaultAnonCredsLinkSecretTags = { + linkSecretId: string +} + +export type CustomAnonCredsLinkSecretTags = TagsBase & { + isDefault?: boolean +} + +export class AnonCredsLinkSecretRecord extends BaseRecord { + public static readonly type = 'AnonCredsLinkSecretRecord' + public readonly type = AnonCredsLinkSecretRecord.type + + public readonly linkSecretId!: string + public readonly value?: string + + public constructor(props: AnonCredsLinkSecretRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.linkSecretId = props.linkSecretId + this.value = props.value + } + } + + public getTags() { + return { + ...this._tags, + linkSecretId: this.linkSecretId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts b/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts new file mode 100644 index 0000000000..a4b69b08db --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsLinkSecretRecord } from './AnonCredsLinkSecretRecord' + +@injectable() +export class AnonCredsLinkSecretRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsLinkSecretRecord, storageService, eventEmitter) + } + + public async getDefault(agentContext: AgentContext) { + return this.getSingleByQuery(agentContext, { isDefault: true }) + } + + public async findDefault(agentContext: AgentContext) { + return this.findSingleByQuery(agentContext, { isDefault: true }) + } + + public async getByLinkSecretId(agentContext: AgentContext, linkSecretId: string) { + return this.getSingleByQuery(agentContext, { linkSecretId }) + } + + public async findByLinkSecretId(agentContext: AgentContext, linkSecretId: string) { + return this.findSingleByQuery(agentContext, { linkSecretId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts b/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts new file mode 100644 index 0000000000..13ad5d757c --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts @@ -0,0 +1,50 @@ +import type { AnonCredsSchemaRecordMetadata } from './anonCredsSchemaRecordMetadataTypes' +import type { AnonCredsSchema } from '../models' +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsSchemaRecordProps { + id?: string + schemaId: string + schema: AnonCredsSchema +} + +export type DefaultAnonCredsSchemaTags = { + schemaId: string + issuerId: string + schemaName: string + schemaVersion: string +} + +export class AnonCredsSchemaRecord extends BaseRecord< + DefaultAnonCredsSchemaTags, + TagsBase, + AnonCredsSchemaRecordMetadata +> { + public static readonly type = 'AnonCredsSchemaRecord' + public readonly type = AnonCredsSchemaRecord.type + + public readonly schemaId!: string + public readonly schema!: AnonCredsSchema + + public constructor(props: AnonCredsSchemaRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.schema = props.schema + this.schemaId = props.schemaId + } + } + + public getTags() { + return { + ...this._tags, + schemaId: this.schemaId, + issuerId: this.schema.issuerId, + schemaName: this.schema.name, + schemaVersion: this.schema.version, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts b/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts new file mode 100644 index 0000000000..0d0ab84b9f --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, inject, injectable } from '@aries-framework/core' + +import { AnonCredsSchemaRecord } from './AnonCredsSchemaRecord' + +@injectable() +export class AnonCredsSchemaRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsSchemaRecord, storageService, eventEmitter) + } + + public async getBySchemaId(agentContext: AgentContext, schemaId: string) { + return this.getSingleByQuery(agentContext, { schemaId: schemaId }) + } + + public async findBySchemaId(agentContext: AgentContext, schemaId: string) { + return await this.findSingleByQuery(agentContext, { schemaId: schemaId }) + } +} diff --git a/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts b/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts new file mode 100644 index 0000000000..05806802e4 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsCredentialDefinitionRecordMetadataKeys { + CredentialDefinitionRegistrationMetadata = '_internal/anonCredsCredentialDefinitionRegistrationMetadata', + CredentialDefinitionMetadata = '_internal/anonCredsCredentialDefinitionMetadata', +} + +export type AnonCredsCredentialDefinitionRecordMetadata = { + [AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata]: Extensible + [AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata]: Extensible +} diff --git a/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts b/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts new file mode 100644 index 0000000000..9880a50625 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsSchemaRecordMetadataKeys { + SchemaRegistrationMetadata = '_internal/anonCredsSchemaRegistrationMetadata', + SchemaMetadata = '_internal/anonCredsSchemaMetadata', +} + +export type AnonCredsSchemaRecordMetadata = { + [AnonCredsSchemaRecordMetadataKeys.SchemaRegistrationMetadata]: Extensible + [AnonCredsSchemaRecordMetadataKeys.SchemaMetadata]: Extensible +} diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts new file mode 100644 index 0000000000..5e17e19941 --- /dev/null +++ b/packages/anoncreds/src/repository/index.ts @@ -0,0 +1,10 @@ +export * from './AnonCredsCredentialDefinitionRecord' +export * from './AnonCredsCredentialDefinitionRepository' +export * from './AnonCredsCredentialDefinitionPrivateRecord' +export * from './AnonCredsCredentialDefinitionPrivateRepository' +export * from './AnonCredsKeyCorrectnessProofRecord' +export * from './AnonCredsKeyCorrectnessProofRepository' +export * from './AnonCredsLinkSecretRecord' +export * from './AnonCredsLinkSecretRepository' +export * from './AnonCredsSchemaRecord' +export * from './AnonCredsSchemaRepository' diff --git a/packages/anoncreds/src/services/AnonCredsHolderService.ts b/packages/anoncreds/src/services/AnonCredsHolderService.ts index a7c0dcb22e..85e51ce529 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderService.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderService.ts @@ -6,6 +6,8 @@ import type { StoreCredentialOptions, GetCredentialsForProofRequestOptions, GetCredentialsForProofRequestReturn, + CreateLinkSecretReturn, + CreateLinkSecretOptions, } from './AnonCredsHolderServiceOptions' import type { AnonCredsCredentialInfo } from '../models' import type { AnonCredsProof } from '../models/exchange' @@ -14,6 +16,8 @@ import type { AgentContext } from '@aries-framework/core' export const AnonCredsHolderServiceSymbol = Symbol('AnonCredsHolderService') export interface AnonCredsHolderService { + createLinkSecret(agentContext: AgentContext, options: CreateLinkSecretOptions): Promise + createProof(agentContext: AgentContext, options: CreateProofOptions): Promise storeCredential( agentContext: AgentContext, diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index 728482ff33..fcbc5e913c 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -12,7 +12,7 @@ import type { } from '../models/exchange' import type { AnonCredsCredentialDefinition, - AnonCredsRevocationList, + AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition, AnonCredsSchema, } from '../models/registry' @@ -36,8 +36,8 @@ export interface CreateProofOptions { // tails file MUST already be downloaded on a higher level and stored tailsFilePath: string definition: AnonCredsRevocationRegistryDefinition - revocationLists: { - [timestamp: string]: AnonCredsRevocationList + revocationStatusLists: { + [timestamp: string]: AnonCredsRevocationStatusList } } } @@ -81,9 +81,19 @@ export type GetCredentialsForProofRequestReturn = Array<{ export interface CreateCredentialRequestOptions { credentialOffer: AnonCredsCredentialOffer credentialDefinition: AnonCredsCredentialDefinition + linkSecretId?: string } export interface CreateCredentialRequestReturn { credentialRequest: AnonCredsCredentialRequest credentialRequestMetadata: AnonCredsCredentialRequestMetadata } + +export interface CreateLinkSecretOptions { + linkSecretId?: string +} + +export interface CreateLinkSecretReturn { + linkSecretId: string + linkSecretValue?: string +} diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts index 41cb4ebf9f..3090b1759b 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerService.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -4,9 +4,10 @@ import type { CreateCredentialOfferOptions, CreateCredentialReturn, CreateCredentialOptions, + CreateCredentialDefinitionReturn, } from './AnonCredsIssuerServiceOptions' import type { AnonCredsCredentialOffer } from '../models/exchange' -import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../models/registry' +import type { AnonCredsSchema } from '../models/registry' import type { AgentContext } from '@aries-framework/core' export const AnonCredsIssuerServiceSymbol = Symbol('AnonCredsIssuerService') @@ -20,7 +21,7 @@ export interface AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialDefinitionOptions, metadata?: Record - ): Promise + ): Promise createCredentialOffer( agentContext: AgentContext, diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index 58d6cd9048..c7da246b9b 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -4,7 +4,7 @@ import type { AnonCredsCredentialRequest, AnonCredsCredentialValues, } from '../models/exchange' -import type { AnonCredsSchema } from '../models/registry' +import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../models/registry' export interface CreateSchemaOptions { issuerId: string @@ -39,3 +39,9 @@ export interface CreateCredentialReturn { credential: AnonCredsCredential credentialRevocationId?: string } + +export interface CreateCredentialDefinitionReturn { + credentialDefinition: AnonCredsCredentialDefinition + credentialDefinitionPrivate?: Record + keyCorrectnessProof?: Record +} diff --git a/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts index f3ecb3b70c..85593764af 100644 --- a/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts @@ -1,7 +1,7 @@ import type { AnonCredsProof, AnonCredsProofRequest } from '../models/exchange' import type { AnonCredsCredentialDefinition, - AnonCredsRevocationList, + AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition, AnonCredsSchema, } from '../models/registry' @@ -23,8 +23,8 @@ export interface VerifyProofOptions { // as a verifier. This is just following the data models from the AnonCreds spec, but for e.g. indy // this means we need to retrieve _ALL_ deltas from the ledger to verify a proof. While currently we // only need to fetch the registry. - revocationLists: { - [timestamp: number]: AnonCredsRevocationList + revocationStatusLists: { + [timestamp: number]: AnonCredsRevocationStatusList } } } diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts index e3061043dd..870eb90571 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts @@ -3,8 +3,8 @@ import type { RegisterCredentialDefinitionOptions, RegisterCredentialDefinitionReturn, } from './CredentialDefinitionOptions' -import type { GetRevocationListReturn } from './RevocationListOptions' import type { GetRevocationRegistryDefinitionReturn } from './RevocationRegistryDefinitionOptions' +import type { GetRevocationStatusListReturn } from './RevocationStatusListOptions' import type { GetSchemaReturn, RegisterSchemaOptions, RegisterSchemaReturn } from './SchemaOptions' import type { AgentContext } from '@aries-framework/core' @@ -37,12 +37,11 @@ export interface AnonCredsRegistry { // options: RegisterRevocationRegistryDefinitionOptions // ): Promise - // TODO: The name of this data model is still tbd. - getRevocationList( + getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, timestamp: number - ): Promise + ): Promise // TODO: issuance of revocable credentials // registerRevocationList( diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts index a860d1e8f5..23c393bb38 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts @@ -20,7 +20,7 @@ export class AnonCredsRegistryService { const registry = registries.find((registry) => registry.supportedIdentifier.test(identifier)) if (!registry) { - throw new AnonCredsError(`No AnonCredsRegistry registered for identifier '${registry}'`) + throw new AnonCredsError(`No AnonCredsRegistry registered for identifier '${identifier}'`) } return registry diff --git a/packages/anoncreds/src/services/registry/RevocationListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts similarity index 70% rename from packages/anoncreds/src/services/registry/RevocationListOptions.ts rename to packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index f3a07dc686..6396fe6df0 100644 --- a/packages/anoncreds/src/services/registry/RevocationListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -1,10 +1,10 @@ import type { AnonCredsResolutionMetadata, Extensible } from './base' -import type { AnonCredsRevocationList } from '../../models/registry' +import type { AnonCredsRevocationStatusList } from '../../models/registry' -export interface GetRevocationListReturn { - revocationList?: AnonCredsRevocationList +export interface GetRevocationStatusListReturn { + revocationStatusList?: AnonCredsRevocationStatusList resolutionMetadata: AnonCredsResolutionMetadata - revocationListMetadata: Extensible + revocationStatusListMetadata: Extensible } // TODO: Support for issuance of revocable credentials diff --git a/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts b/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts index 553b9e626c..2cb39bc2e5 100644 --- a/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts +++ b/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts @@ -32,7 +32,7 @@ describe('AnonCredsRegistryService', () => { expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'b')).toEqual(registryTwo) }) - test('throws AnonCredsError if no registry is found for the given identifier', () => { + test('throws AnonCredsError if no registry is found for the given identifier', async () => { expect(() => anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'c')).toThrow(AnonCredsError) }) }) diff --git a/packages/anoncreds/src/services/registry/index.ts b/packages/anoncreds/src/services/registry/index.ts index 5d36ce3dd9..fd154074fd 100644 --- a/packages/anoncreds/src/services/registry/index.ts +++ b/packages/anoncreds/src/services/registry/index.ts @@ -2,5 +2,5 @@ export * from './AnonCredsRegistry' export * from './CredentialDefinitionOptions' export * from './SchemaOptions' export * from './RevocationRegistryDefinitionOptions' -export * from './RevocationListOptions' +export * from './RevocationStatusListOptions' export { AnonCredsResolutionMetadata } from './base' diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index a1426fad46..18bd9cfaab 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -8,7 +8,9 @@ import type { RegisterCredentialDefinitionOptions, RegisterCredentialDefinitionReturn, GetRevocationRegistryDefinitionReturn, - GetRevocationListReturn, + GetRevocationStatusListReturn, + AnonCredsRevocationStatusList, + AnonCredsRevocationRegistryDefinition, AnonCredsSchema, AnonCredsCredentialDefinition, } from '../src' @@ -26,8 +28,27 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { // we want, but the indy-sdk is picky about the identifier format. public readonly supportedIdentifier = /^[a-zA-Z0-9]{21,22}/ - private schemas: Record = {} - private credentialDefinitions: Record = {} + private schemas: Record + private credentialDefinitions: Record + private revocationRegistryDefinitions: Record + private revocationStatusLists: Record> + + public constructor({ + existingSchemas = {}, + existingCredentialDefinitions = {}, + existingRevocationRegistryDefinitions = {}, + existingRevocationStatusLists = {}, + }: { + existingSchemas?: Record + existingCredentialDefinitions?: Record + existingRevocationRegistryDefinitions?: Record + existingRevocationStatusLists?: Record> + } = {}) { + this.schemas = existingSchemas + this.credentialDefinitions = existingCredentialDefinitions + this.revocationRegistryDefinitions = existingRevocationRegistryDefinitions + this.revocationStatusLists = existingRevocationStatusLists + } public async getSchema(agentContext: AgentContext, schemaId: string): Promise { const schema = this.schemas[schemaId] @@ -40,11 +61,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { message: `Schema with id ${schemaId} not found in memory registry`, }, schemaId, - schemaMetadata: { - // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. - // For this reason we return it in the metadata. - indyLedgerSeqNo, - }, + schemaMetadata: {}, } } @@ -52,7 +69,11 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { resolutionMetadata: {}, schema, schemaId, - schemaMetadata: {}, + schemaMetadata: { + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo, + }, } } @@ -125,19 +146,53 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } - public getRevocationRegistryDefinition( + public async getRevocationRegistryDefinition( agentContext: AgentContext, revocationRegistryDefinitionId: string ): Promise { - throw new Error('Method not implemented.') + const revocationRegistryDefinition = this.revocationRegistryDefinitions[revocationRegistryDefinitionId] + + if (!revocationRegistryDefinition) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Revocation registry definition with id ${revocationRegistryDefinition} not found in memory registry`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + } + + return { + resolutionMetadata: {}, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } } - public getRevocationList( + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, timestamp: number - ): Promise { - throw new Error('Method not implemented.') + ): Promise { + const revocationStatusLists = this.revocationStatusLists[revocationRegistryId] + + if (!revocationStatusLists || !revocationStatusLists[timestamp]) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Revocation status list for revocation registry with id ${revocationRegistryId} not found in memory registry`, + }, + revocationStatusListMetadata: {}, + } + } + + return { + resolutionMetadata: {}, + revocationStatusList: revocationStatusLists[timestamp], + revocationStatusListMetadata: {}, + } } } diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts new file mode 100644 index 0000000000..e7abd466c4 --- /dev/null +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -0,0 +1,312 @@ +import { Agent, KeyDerivationMethod } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' + +import { IndySdkModule } from '../../indy-sdk/src/IndySdkModule' +import { AnonCredsCredentialDefinitionRepository, AnonCredsModule, AnonCredsSchemaRepository } from '../src' + +import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' + +const existingSchemas = { + '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0': { + attrNames: ['one', 'two'], + issuerId: '7Cd2Yj9yEZNcmNoH54tq9i', + name: 'Test Schema', + version: '1.0.0', + }, +} + +const existingCredentialDefinitions = { + 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG': { + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + r: { + one: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + two: '60366631925664005237432731340682977203246802182440530784833565276111958129922833461368205267143124766208499918438803966972947830682551774196763124331578934778868938718942789067536194229546670608604626738087066151521062180022991840618459591148096543440942293686250499935227881144460486543061212259250663566176469333982946568767707989969471450673037590849807300874360022327312564559087769485266016496010132793446151658150957771177955095876947792797176338483943233433284791481746843006255371654617950568875773118157773566188096075078351362095061968279597354733768049622048871890495958175847017320945873812850638157518451', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, +} as const + +const existingRevocationRegistryDefinitions = { + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG': { + credDefId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + maxCredNum: 100, + type: 'CL_ACCUM', + publicKeys: { + accumKey: { + z: 'ab81257c-be63-4051-9e21-c7d384412f64', + }, + }, + tag: 'TAG', + tailsHash: 'ab81257c-be63-4051-9e21-c7d384412f64', + tailsLocation: 'http://localhost:7200/tails', + }, +} as const + +const existingRevocationStatusLists = { + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG': { + 10123: { + currentAccumulator: 'ab81257c-be63-4051-9e21-c7d384412f64', + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + revocationList: [1, 0, 1], + revRegId: 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + timestamp: 10123, + }, + }, +} + +const agent = new Agent({ + config: { + label: '@aries-framework/anoncreds', + walletConfig: { + id: '@aries-framework/anoncreds', + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, + }, + }, + modules: { + indySdk: new IndySdkModule({ + indySdk: agentDependencies.indy, + }), + anoncreds: new AnonCredsModule({ + registries: [ + new InMemoryAnonCredsRegistry({ + existingSchemas, + existingCredentialDefinitions, + existingRevocationRegistryDefinitions, + existingRevocationStatusLists, + }), + ], + }), + }, + dependencies: agentDependencies, +}) + +describe('AnonCreds API', () => { + beforeEach(async () => { + await agent.initialize() + }) + + afterEach(async () => { + await agent.wallet.delete() + await agent.shutdown() + }) + + test('create and get link secret', async () => { + await agent.modules.anoncreds.createLinkSecret({ + linkSecretId: 'anoncreds-link-secret', + }) + + const linkSecretIds = await agent.modules.anoncreds.getLinkSecretIds() + + expect(linkSecretIds).toEqual(['anoncreds-link-secret']) + }) + + test('register a schema', async () => { + const schemaResult = await agent.modules.anoncreds.registerSchema({ + options: {}, + schema: { + attrNames: ['name', 'age'], + issuerId: '6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + }) + + expect(schemaResult).toEqual({ + registrationMetadata: {}, + schemaMetadata: { indyLedgerSeqNo: 16908 }, + schemaState: { + state: 'finished', + schema: { + attrNames: ['name', 'age'], + issuerId: '6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + }, + }) + + // Check if record was created + const anonCredsSchemaRepository = agent.dependencyManager.resolve(AnonCredsSchemaRepository) + const schemaRecord = await anonCredsSchemaRepository.getBySchemaId( + agent.context, + '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0' + ) + + expect(schemaRecord).toMatchObject({ + schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + schema: { + attrNames: ['name', 'age'], + issuerId: '6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + }) + + expect(schemaRecord.getTags()).toEqual({ + schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + issuerId: '6xDN7v3AiGgusRp4bqZACZ', + schemaName: 'Employee Credential', + schemaVersion: '1.0.0', + }) + }) + + test('resolve a schema', async () => { + const schemaResult = await agent.modules.anoncreds.getSchema('7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0') + + expect(schemaResult).toEqual({ + resolutionMetadata: {}, + schemaMetadata: { indyLedgerSeqNo: 75206 }, + schema: { + attrNames: ['one', 'two'], + issuerId: '7Cd2Yj9yEZNcmNoH54tq9i', + name: 'Test Schema', + version: '1.0.0', + }, + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + }) + }) + + test('register a credential definition', async () => { + // NOTE: the indy-sdk MUST have a did created, we can't just create a key + await agent.context.wallet.initPublicDid({ seed: '00000000000000000000000000000My1' }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const issuerId = agent.context.wallet.publicDid!.did + + const credentialDefinitionResult = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition: { + issuerId, + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + tag: 'TAG', + }, + options: {}, + }) + + expect(credentialDefinitionResult).toEqual({ + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + state: 'finished', + credentialDefinition: { + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: expect.any(String), + s: expect.any(String), + r: { + one: expect.any(String), + master_secret: expect.any(String), + two: expect.any(String), + }, + rctxt: expect.any(String), + z: expect.any(String), + }, + }, + }, + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + }, + }) + + // Check if record was created + const anonCredsCredentialDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsCredentialDefinitionRepository + ) + const credentialDefinitionRecord = await anonCredsCredentialDefinitionRepository.getByCredentialDefinitionId( + agent.context, + 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG' + ) + + expect(credentialDefinitionRecord).toMatchObject({ + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + credentialDefinition: { + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: expect.any(String), + s: expect.any(String), + r: { + one: expect.any(String), + master_secret: expect.any(String), + two: expect.any(String), + }, + rctxt: expect.any(String), + z: expect.any(String), + }, + }, + }, + }) + + expect(credentialDefinitionRecord.getTags()).toEqual({ + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + }) + }) + + test('resolve a credential definition', async () => { + const credentialDefinitionResult = await agent.modules.anoncreds.getCredentialDefinition( + 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG' + ) + + expect(credentialDefinitionResult).toEqual({ + resolutionMetadata: {}, + credentialDefinitionMetadata: {}, + credentialDefinition: existingCredentialDefinitions['VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG'], + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + }) + }) + + test('resolve a revocation regsitry definition', async () => { + const revocationRegistryDefinition = await agent.modules.anoncreds.getRevocationRegistryDefinition( + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ) + + expect(revocationRegistryDefinition).toEqual({ + revocationRegistryDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + revocationRegistryDefinition: + existingRevocationRegistryDefinitions[ + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ], + resolutionMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + }) + }) + + test('resolve a revocation status list', async () => { + const revocationStatusList = await agent.modules.anoncreds.getRevocationStatusList( + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + 10123 + ) + + expect(revocationStatusList).toEqual({ + revocationStatusList: + existingRevocationStatusLists[ + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ][10123], + resolutionMetadata: {}, + revocationStatusListMetadata: {}, + }) + }) +}) diff --git a/packages/anoncreds/tests/setup.ts b/packages/anoncreds/tests/setup.ts index 719a473b6e..b60b932be5 100644 --- a/packages/anoncreds/tests/setup.ts +++ b/packages/anoncreds/tests/setup.ts @@ -1 +1 @@ -jest.setTimeout(10000) +jest.setTimeout(25000) diff --git a/packages/indy-sdk/src/IndySdkModule.ts b/packages/indy-sdk/src/IndySdkModule.ts index ea3baa5a9a..20574f3d46 100644 --- a/packages/indy-sdk/src/IndySdkModule.ts +++ b/packages/indy-sdk/src/IndySdkModule.ts @@ -1,8 +1,18 @@ import type { IndySdkModuleConfigOptions } from './IndySdkModuleConfig' import type { DependencyManager, Module } from '@aries-framework/core' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, +} from '@aries-framework/anoncreds' +import { InjectionSymbols } from '@aries-framework/core' + import { IndySdkModuleConfig } from './IndySdkModuleConfig' +import { IndySdkHolderService, IndySdkIssuerService, IndySdkVerifierService } from './anoncreds' +import { IndySdkStorageService } from './storage' import { IndySdkSymbol } from './types' +import { IndySdkWallet } from './wallet' export class IndySdkModule implements Module { public readonly config: IndySdkModuleConfig @@ -13,5 +23,13 @@ export class IndySdkModule implements Module { public register(dependencyManager: DependencyManager) { dependencyManager.registerInstance(IndySdkSymbol, this.config.indySdk) + + // NOTE: for now we are registering the needed indy services. We may want to make this + // more explicit and require the user to register the services they need on the specific modules. + dependencyManager.registerSingleton(InjectionSymbols.Wallet, IndySdkWallet) + dependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) + dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, IndySdkIssuerService) + dependencyManager.registerSingleton(AnonCredsHolderServiceSymbol, IndySdkHolderService) + dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, IndySdkVerifierService) } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index ffe975b7e1..7ddb4a5db5 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -2,7 +2,7 @@ import type { IndySdk } from '../../types' import type { AnonCredsRegistry, GetCredentialDefinitionReturn, - GetRevocationListReturn, + GetRevocationStatusListReturn, GetRevocationRegistryDefinitionReturn, GetSchemaReturn, RegisterCredentialDefinitionOptions, @@ -25,7 +25,7 @@ import { indySdkAnonCredsRegistryIdentifierRegex, } from '../utils/identifiers' import { - anonCredsRevocationListFromIndySdk, + anonCredsRevocationStatusListFromIndySdk, anonCredsRevocationRegistryDefinitionFromIndySdk, } from '../utils/transform' @@ -417,11 +417,11 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } } - public async getRevocationList( + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, timestamp: number - ): Promise { + ): Promise { try { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) @@ -470,7 +470,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { resolutionMetadata: { error: `error resolving revocation registry definition with id ${revocationRegistryId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, }, - revocationListMetadata: { + revocationStatusListMetadata: { didIndyNamespace: pool.didIndyNamespace, }, } @@ -480,14 +480,14 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { return { resolutionMetadata: {}, - revocationList: anonCredsRevocationListFromIndySdk( + revocationStatusList: anonCredsRevocationStatusListFromIndySdk( revocationRegistryId, revocationRegistryDefinition, revocationRegistryDelta, deltaTimestamp, isIssuanceByDefault ), - revocationListMetadata: { + revocationStatusListMetadata: { didIndyNamespace: pool.didIndyNamespace, }, } @@ -505,7 +505,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { error: 'notFound', message: `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, }, - revocationListMetadata: {}, + revocationStatusListMetadata: {}, } } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts index e472d1c1c4..2e6e63ccc0 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts @@ -11,6 +11,8 @@ import type { GetCredentialsForProofRequestReturn, AnonCredsRequestedCredentials, AnonCredsCredentialRequestMetadata, + CreateLinkSecretOptions, + CreateLinkSecretReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { @@ -23,7 +25,7 @@ import type { IndyProofRequest, } from 'indy-sdk' -import { inject } from '@aries-framework/core' +import { injectable, inject, utils } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' @@ -38,6 +40,7 @@ import { import { IndySdkRevocationService } from './IndySdkRevocationService' +@injectable() export class IndySdkHolderService implements AnonCredsHolderService { private indySdk: IndySdk private indyRevocationService: IndySdkRevocationService @@ -47,6 +50,31 @@ export class IndySdkHolderService implements AnonCredsHolderService { this.indyRevocationService = indyRevocationService } + public async createLinkSecret( + agentContext: AgentContext, + options: CreateLinkSecretOptions + ): Promise { + assertIndySdkWallet(agentContext.wallet) + + const linkSecretId = options.linkSecretId ?? utils.uuid() + + try { + await this.indySdk.proverCreateMasterSecret(agentContext.wallet.handle, linkSecretId) + + // We don't have the value for the link secret when using the indy-sdk so we can't return it. + return { + linkSecretId, + } + } catch (error) { + agentContext.config.logger.error(`Error creating link secret`, { + error, + linkSecretId, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise { const { credentialDefinitions, proofRequest, requestedCredentials, schemas } = options diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index 96e9ef266a..ba6c2a1780 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -8,11 +8,11 @@ import type { CreateSchemaOptions, AnonCredsCredentialOffer, AnonCredsSchema, - AnonCredsCredentialDefinition, + CreateCredentialDefinitionReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' -import { AriesFrameworkError, inject } from '@aries-framework/core' +import { injectable, AriesFrameworkError, inject } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' @@ -21,6 +21,7 @@ import { generateLegacyProverDidLikeString } from '../utils/proverDid' import { createTailsReader } from '../utils/tails' import { indySdkSchemaFromAnonCreds } from '../utils/transform' +@injectable() export class IndySdkIssuerService implements AnonCredsIssuerService { private indySdk: IndySdk @@ -50,7 +51,7 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialDefinitionOptions, metadata?: CreateCredentialDefinitionMetadata - ): Promise { + ): Promise { const { tag, supportRevocation, schema, issuerId, schemaId } = options if (!metadata) @@ -70,11 +71,13 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { ) return { - issuerId, - tag: credentialDefinition.tag, - schemaId, - type: 'CL', - value: credentialDefinition.value, + credentialDefinition: { + issuerId, + tag: credentialDefinition.tag, + schemaId, + type: 'CL', + value: credentialDefinition.value, + }, } } catch (error) { throw isIndyError(error) ? new IndySdkError(error) : error diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts index 4f7eb6ef42..30f78bcbff 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts @@ -1,6 +1,6 @@ import type { AnonCredsRevocationRegistryDefinition, - AnonCredsRevocationList, + AnonCredsRevocationStatusList, AnonCredsProofRequest, AnonCredsRequestedCredentials, AnonCredsCredentialInfo, @@ -50,8 +50,8 @@ export class IndySdkRevocationService { // Tails is already downloaded tailsFilePath: string definition: AnonCredsRevocationRegistryDefinition - revocationLists: { - [timestamp: string]: AnonCredsRevocationList + revocationStatusLists: { + [timestamp: string]: AnonCredsRevocationStatusList } } } @@ -106,18 +106,18 @@ export class IndySdkRevocationService { this.assertRevocationInterval(requestRevocationInterval) - const { definition, revocationLists, tailsFilePath } = revocationRegistries[revocationRegistryId] - // NOTE: we assume that the revocationLists have been added based on timestamps of the `to` query. On a higher level it means we'll find the - // most accurate revocation list for a given timestamp. It doesn't have to be that the revocationList is from the `to` timestamp however. - const revocationList = revocationLists[requestRevocationInterval.to] + const { definition, revocationStatusLists, tailsFilePath } = revocationRegistries[revocationRegistryId] + // NOTE: we assume that the revocationStatusLists have been added based on timestamps of the `to` query. On a higher level it means we'll find the + // most accurate revocation list for a given timestamp. It doesn't have to be that the revocationStatusList is from the `to` timestamp however. + const revocationStatusList = revocationStatusLists[requestRevocationInterval.to] const tails = await createTailsReader(agentContext, tailsFilePath) const revocationState = await this.indySdk.createRevocationState( tails, indySdkRevocationRegistryDefinitionFromAnonCreds(revocationRegistryId, definition), - indySdkRevocationDeltaFromAnonCreds(revocationList), - revocationList.timestamp, + indySdkRevocationDeltaFromAnonCreds(revocationStatusList), + revocationStatusList.timestamp, credentialRevocationId ) const timestamp = revocationState.timestamp diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts index d07a4ef1ef..3e76fc6bc9 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts @@ -1,7 +1,7 @@ import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' import type { CredentialDefs, Schemas, RevocRegDefs, RevRegs, IndyProofRequest } from 'indy-sdk' -import { inject } from '@aries-framework/core' +import { inject, injectable } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' @@ -13,6 +13,7 @@ import { indySdkSchemaFromAnonCreds, } from '../utils/transform' +@injectable() export class IndySdkVerifierService implements AnonCredsVerifierService { private indySdk: IndySdk @@ -53,7 +54,7 @@ export class IndySdkVerifierService implements AnonCredsVerifierService { const indyRevocationRegistries: RevRegs = {} for (const revocationRegistryDefinitionId in options.revocationStates) { - const { definition, revocationLists } = options.revocationStates[revocationRegistryDefinitionId] + const { definition, revocationStatusLists } = options.revocationStates[revocationRegistryDefinitionId] indyRevocationDefinitions[revocationRegistryDefinitionId] = indySdkRevocationRegistryDefinitionFromAnonCreds( revocationRegistryDefinitionId, definition @@ -64,10 +65,10 @@ export class IndySdkVerifierService implements AnonCredsVerifierService { // Also transform the revocation lists for the specified timestamps into the revocation registry // format Indy expects - for (const timestamp in revocationLists) { - const revocationList = revocationLists[timestamp] + for (const timestamp in revocationStatusLists) { + const revocationStatusList = revocationStatusLists[timestamp] indyRevocationRegistries[revocationRegistryDefinitionId][timestamp] = - indySdkRevocationRegistryFromAnonCreds(revocationList) + indySdkRevocationRegistryFromAnonCreds(revocationStatusList) } } diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts index 20b16fa0ff..7930bfb2fb 100644 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts +++ b/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts @@ -108,7 +108,7 @@ describe('transform', () => { test.todo( 'indySdkRevocationRegistryDefinitionFromAnonCreds should return a valid indy sdk revocation registry definition' ) - test.todo('anonCredsRevocationListFromIndySdk should return a valid anoncreds revocation list') + test.todo('anonCredsRevocationStatusListFromIndySdk should return a valid anoncreds revocation list') test.todo('indySdkRevocationRegistryFromAnonCreds should return a valid indy sdk revocation registry') test.todo('indySdkRevocationDeltaFromAnonCreds should return a valid indy sdk revocation delta') }) diff --git a/packages/indy-sdk/src/anoncreds/utils/transform.ts b/packages/indy-sdk/src/anoncreds/utils/transform.ts index a5ad8afd60..6a91928f70 100644 --- a/packages/indy-sdk/src/anoncreds/utils/transform.ts +++ b/packages/indy-sdk/src/anoncreds/utils/transform.ts @@ -1,6 +1,6 @@ import type { AnonCredsCredentialDefinition, - AnonCredsRevocationList, + AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition, AnonCredsSchema, } from '@aries-framework/anoncreds' @@ -92,13 +92,13 @@ export function indySdkRevocationRegistryDefinitionFromAnonCreds( } } -export function anonCredsRevocationListFromIndySdk( +export function anonCredsRevocationStatusListFromIndySdk( revocationRegistryDefinitionId: string, revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, delta: RevocRegDelta, timestamp: number, isIssuanceByDefault: boolean -): AnonCredsRevocationList { +): AnonCredsRevocationStatusList { // 0 means unrevoked, 1 means revoked const defaultState = isIssuanceByDefault ? 0 : 1 @@ -124,25 +124,27 @@ export function anonCredsRevocationListFromIndySdk( } } -export function indySdkRevocationRegistryFromAnonCreds(revocationList: AnonCredsRevocationList): RevocReg { +export function indySdkRevocationRegistryFromAnonCreds(revocationStatusList: AnonCredsRevocationStatusList): RevocReg { return { ver: '1.0', value: { - accum: revocationList.currentAccumulator, + accum: revocationStatusList.currentAccumulator, }, } } -export function indySdkRevocationDeltaFromAnonCreds(revocationList: AnonCredsRevocationList): RevocRegDelta { - // Get all indices from the revocationList that are revoked (so have value '1') - const revokedIndices = revocationList.revocationList.reduce( +export function indySdkRevocationDeltaFromAnonCreds( + revocationStatusList: AnonCredsRevocationStatusList +): RevocRegDelta { + // Get all indices from the revocationStatusList that are revoked (so have value '1') + const revokedIndices = revocationStatusList.revocationList.reduce( (revoked, current, index) => (current === 1 ? [...revoked, index] : revoked), [] ) return { value: { - accum: revocationList.currentAccumulator, + accum: revocationStatusList.currentAccumulator, issued: [], revoked: revokedIndices, // NOTE: I don't think this is used? From 7f65ba999ad1f49065d24966a1d7f3b82264ea55 Mon Sep 17 00:00:00 2001 From: Jim Ezesinachi Date: Mon, 6 Feb 2023 22:27:03 +0100 Subject: [PATCH 10/20] feat: optional routing for legacy connectionless invitation (#1271) Signed-off-by: Jim Ezesinachi --- packages/core/src/modules/oob/OutOfBandApi.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts index aee58655da..5f1e0c00b7 100644 --- a/packages/core/src/modules/oob/OutOfBandApi.ts +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -247,9 +247,10 @@ export class OutOfBandApi { recordId: string message: Message domain: string + routing?: Routing }): Promise<{ message: Message; invitationUrl: string }> { // Create keys (and optionally register them at the mediator) - const routing = await this.routingService.getRouting(this.agentContext) + const routing = config.routing ?? (await this.routingService.getRouting(this.agentContext)) // Set the service on the message config.message.service = new ServiceDecorator({ From 3d86e78a4df87869aa5df4e28b79cd91787b61fb Mon Sep 17 00:00:00 2001 From: Karim Stekelenburg Date: Tue, 7 Feb 2023 00:09:24 +0100 Subject: [PATCH 11/20] feat(openid4vc-client): pre-authorized (#1243) This PR adds support for the `pre-authorized` OpenID for Verifiable Credentials issuance flow to the new `openid4vc-client` module. Here are some highlights of the work: - Allows the user to execute the entire `pre-authorized` flow by calling a single method. - Adds a happy-flow test - HTTP(S) requests and responses are mocked using a network mocking library called [nock](https://github.com/nock/nock) - Because the JSON-LD credential that is received is expanded by the `W3cCredentialService`, I've added a few new contexts to our test document loader. - Not-so-happy-flow tests will be added later on. If you have any suggestions for edge cases that deserve testing, feel free to drop a comment. - Modifies the `JwsService` - The `JwsService` was geared towards a very specific use case. I've generalized its API so it's usable for a wider range of applications. - All pre-existing tests and calls to the `JwsService` have been updated. It's worth noting that I have had to add some `@ts-ignore` statements here and there to get around some incomplete types in the `OpenID4VCI-Client` library we're using. Once these issues have been resolved in the client library, they will be removed. **Work funded by the government of Ontario** --------- Signed-off-by: Karim Stekelenburg Co-authored-by: Timo Glastra --- packages/core/src/crypto/JwkTypes.ts | 6 + packages/core/src/crypto/JwsService.ts | 111 +++-- packages/core/src/crypto/Key.ts | 23 +- .../src/crypto/__tests__/JwsService.test.ts | 30 +- packages/core/src/crypto/index.ts | 6 + packages/core/src/crypto/jwtUtils.ts | 13 + .../connections/DidExchangeProtocol.ts | 12 +- packages/core/src/modules/dids/DidsApi.ts | 4 +- .../modules/dids/repository/DidRepository.ts | 3 +- .../src/modules/vc/W3cCredentialService.ts | 11 +- .../contexts/mattr_vc_extension_v1.ts | 17 + .../vc/__tests__/contexts/purl_ob_v3po.ts | 438 ++++++++++++++++++ .../contexts/vc_revocation_list_2020.ts | 37 ++ .../vc/__tests__/dids/did_web_launchpad.ts | 24 + .../modules/vc/__tests__/documentLoader.ts | 9 + .../vc/models/credential/W3cCredential.ts | 4 +- .../vc/repository/W3cCredentialRecord.ts | 6 +- packages/core/src/modules/vc/validators.ts | 7 +- packages/openid4vc-client/README.md | 132 +++++- packages/openid4vc-client/package.json | 8 +- .../src/OpenId4VcClientApi.ts | 18 + .../src/OpenId4VcClientApiOptions.ts | 0 .../src/OpenId4VcClientService.ts | 230 ++++++++- packages/openid4vc-client/tests/fixtures.ts | 134 ++++++ .../tests/openid4vc-client.e2e.test.ts | 110 +++++ packages/openid4vc-client/tests/setup.ts | 2 - packages/openid4vc-client/tsconfig.build.json | 3 +- packages/openid4vc-client/tsconfig.json | 3 +- yarn.lock | 23 +- 29 files changed, 1352 insertions(+), 72 deletions(-) create mode 100644 packages/core/src/crypto/JwkTypes.ts create mode 100644 packages/core/src/crypto/jwtUtils.ts create mode 100644 packages/core/src/modules/vc/__tests__/contexts/mattr_vc_extension_v1.ts create mode 100644 packages/core/src/modules/vc/__tests__/contexts/purl_ob_v3po.ts create mode 100644 packages/core/src/modules/vc/__tests__/contexts/vc_revocation_list_2020.ts create mode 100644 packages/core/src/modules/vc/__tests__/dids/did_web_launchpad.ts delete mode 100644 packages/openid4vc-client/src/OpenId4VcClientApiOptions.ts create mode 100644 packages/openid4vc-client/tests/fixtures.ts create mode 100644 packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts diff --git a/packages/core/src/crypto/JwkTypes.ts b/packages/core/src/crypto/JwkTypes.ts new file mode 100644 index 0000000000..144e771f16 --- /dev/null +++ b/packages/core/src/crypto/JwkTypes.ts @@ -0,0 +1,6 @@ +export interface Jwk { + kty: 'EC' | 'OKP' + crv: 'Ed25519' | 'X25519' | 'P-256' | 'P-384' | 'secp256k1' + x: string + y?: string +} diff --git a/packages/core/src/crypto/JwsService.ts b/packages/core/src/crypto/JwsService.ts index ffad03c128..e81be473b8 100644 --- a/packages/core/src/crypto/JwsService.ts +++ b/packages/core/src/crypto/JwsService.ts @@ -1,3 +1,4 @@ +import type { Jwk } from './JwkTypes' import type { Jws, JwsGeneralFormat } from './JwsTypes' import type { AgentContext } from '../agent' import type { Buffer } from '../utils' @@ -17,25 +18,63 @@ const JWS_ALG = 'EdDSA' @injectable() export class JwsService { - public async createJws( - agentContext: AgentContext, - { payload, verkey, header }: CreateJwsOptions - ): Promise { - const base64Payload = TypedArrayEncoder.toBase64URL(payload) - const base64Protected = JsonEncoder.toBase64URL(this.buildProtected(verkey)) - const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + public static supportedKeyTypes = [KeyType.Ed25519] + + private async createJwsBase(agentContext: AgentContext, options: CreateJwsBaseOptions) { + if (!JwsService.supportedKeyTypes.includes(options.key.keyType)) { + throw new AriesFrameworkError( + `Only ${JwsService.supportedKeyTypes.join(',')} key type(s) supported for creating JWS` + ) + } + const base64Payload = TypedArrayEncoder.toBase64URL(options.payload) + const base64UrlProtectedHeader = JsonEncoder.toBase64URL(this.buildProtected(options.protectedHeaderOptions)) const signature = TypedArrayEncoder.toBase64URL( - await agentContext.wallet.sign({ data: TypedArrayEncoder.fromString(`${base64Protected}.${base64Payload}`), key }) + await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(`${base64UrlProtectedHeader}.${base64Payload}`), + key: options.key, + }) ) return { - protected: base64Protected, + base64Payload, + base64UrlProtectedHeader, + signature, + } + } + + public async createJws( + agentContext: AgentContext, + { payload, key, header, protectedHeaderOptions }: CreateJwsOptions + ): Promise { + const { base64UrlProtectedHeader, signature } = await this.createJwsBase(agentContext, { + payload, + key, + protectedHeaderOptions, + }) + + return { + protected: base64UrlProtectedHeader, signature, header, } } + /** + * @see {@link https://www.rfc-editor.org/rfc/rfc7515#section-3.1} + * */ + public async createJwsCompact( + agentContext: AgentContext, + { payload, key, protectedHeaderOptions }: CreateCompactJwsOptions + ): Promise { + const { base64Payload, base64UrlProtectedHeader, signature } = await this.createJwsBase(agentContext, { + payload, + key, + protectedHeaderOptions, + }) + return `${base64UrlProtectedHeader}.${base64Payload}.${signature}` + } + /** * Verify a JWS */ @@ -47,7 +86,7 @@ export class JwsService { throw new AriesFrameworkError('Unable to verify JWS: No entries in JWS signatures array.') } - const signerVerkeys = [] + const signerKeys: Key[] = [] for (const jws of signatures) { const protectedJson = JsonEncoder.fromBase64(jws.protected) @@ -62,9 +101,9 @@ export class JwsService { const data = TypedArrayEncoder.fromString(`${jws.protected}.${base64Payload}`) const signature = TypedArrayEncoder.fromBase64(jws.signature) - const verkey = TypedArrayEncoder.toBase58(TypedArrayEncoder.fromBase64(protectedJson?.jwk?.x)) - const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) - signerVerkeys.push(verkey) + const publicKey = TypedArrayEncoder.fromBase64(protectedJson?.jwk?.x) + const key = Key.fromPublicKey(publicKey, KeyType.Ed25519) + signerKeys.push(key) try { const isValid = await agentContext.wallet.verify({ key, data, signature }) @@ -72,7 +111,7 @@ export class JwsService { if (!isValid) { return { isValid: false, - signerVerkeys: [], + signerKeys: [], } } } catch (error) { @@ -81,7 +120,7 @@ export class JwsService { if (error instanceof WalletError) { return { isValid: false, - signerVerkeys: [], + signerKeys: [], } } @@ -89,31 +128,36 @@ export class JwsService { } } - return { isValid: true, signerVerkeys } + return { isValid: true, signerKeys: signerKeys } } - /** - * @todo This currently only work with a single alg, key type and curve - * This needs to be extended with other formats in the future - */ - private buildProtected(verkey: string) { + private buildProtected(options: ProtectedHeaderOptions) { + if (!options.jwk && !options.kid) { + throw new AriesFrameworkError('Both JWK and kid are undefined. Please provide one or the other.') + } + if (options.jwk && options.kid) { + throw new AriesFrameworkError('Both JWK and kid are provided. Please only provide one of the two.') + } + return { - alg: 'EdDSA', - jwk: { - kty: 'OKP', - crv: 'Ed25519', - x: TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromBase58(verkey)), - }, + alg: options.alg, + jwk: options.jwk, + kid: options.kid, } } } export interface CreateJwsOptions { - verkey: string + key: Key payload: Buffer header: Record + protectedHeaderOptions: ProtectedHeaderOptions } +type CreateJwsBaseOptions = Omit + +type CreateCompactJwsOptions = Omit + export interface VerifyJwsOptions { jws: Jws payload: Buffer @@ -121,5 +165,14 @@ export interface VerifyJwsOptions { export interface VerifyJwsResult { isValid: boolean - signerVerkeys: string[] + signerKeys: Key[] +} + +export type kid = string + +export interface ProtectedHeaderOptions { + alg: string + jwk?: Jwk + kid?: kid + [key: string]: any } diff --git a/packages/core/src/crypto/Key.ts b/packages/core/src/crypto/Key.ts index c5d03507f0..47576e3ffa 100644 --- a/packages/core/src/crypto/Key.ts +++ b/packages/core/src/crypto/Key.ts @@ -1,7 +1,9 @@ -import type { KeyType } from './KeyType' +import type { Jwk } from './JwkTypes' +import { AriesFrameworkError } from '../error' import { Buffer, MultiBaseEncoder, TypedArrayEncoder, VarintEncoder } from '../utils' +import { KeyType } from './KeyType' import { getKeyTypeByMultiCodecPrefix, getMultiCodecPrefixByKeytype } from './multiCodecKey' export class Key { @@ -50,4 +52,23 @@ export class Key { public get publicKeyBase58() { return TypedArrayEncoder.toBase58(this.publicKey) } + + public toJwk(): Jwk { + if (this.keyType !== KeyType.Ed25519) { + throw new AriesFrameworkError(`JWK creation is only supported for Ed25519 key types. Received ${this.keyType}`) + } + + return { + kty: 'OKP', + crv: 'Ed25519', + x: TypedArrayEncoder.toBase64URL(this.publicKey), + } + } + + public static fromJwk(jwk: Jwk) { + if (jwk.crv !== 'Ed25519') { + throw new AriesFrameworkError('Only JWKs with Ed25519 key type is supported.') + } + return Key.fromPublicKeyBase58(TypedArrayEncoder.toBase58(TypedArrayEncoder.fromBase64(jwk.x)), KeyType.Ed25519) + } } diff --git a/packages/core/src/crypto/__tests__/JwsService.test.ts b/packages/core/src/crypto/__tests__/JwsService.test.ts index b0311e396d..6b5d5fb258 100644 --- a/packages/core/src/crypto/__tests__/JwsService.test.ts +++ b/packages/core/src/crypto/__tests__/JwsService.test.ts @@ -1,5 +1,5 @@ import type { AgentContext } from '../../agent' -import type { Wallet } from '@aries-framework/core' +import type { Key, Wallet } from '@aries-framework/core' import { getAgentConfig, getAgentContext } from '../../../tests/helpers' import { DidKey } from '../../modules/dids' @@ -16,7 +16,8 @@ describe('JwsService', () => { let wallet: Wallet let agentContext: AgentContext let jwsService: JwsService - + let didJwsz6MkfKey: Key + let didJwsz6MkvKey: Key beforeAll(async () => { const config = getAgentConfig('JwsService') wallet = new IndyWallet(config.agentDependencies, config.logger, new SigningProviderRegistry([])) @@ -27,6 +28,8 @@ describe('JwsService', () => { await wallet.createAndOpen(config.walletConfig!) jwsService = new JwsService() + didJwsz6MkfKey = await wallet.createKey({ seed: didJwsz6Mkf.SEED, keyType: KeyType.Ed25519 }) + didJwsz6MkvKey = await wallet.createKey({ seed: didJwsz6Mkv.SEED, keyType: KeyType.Ed25519 }) }) afterAll(async () => { @@ -35,16 +38,17 @@ describe('JwsService', () => { describe('createJws', () => { it('creates a jws for the payload with the key associated with the verkey', async () => { - const key = await wallet.createKey({ seed: didJwsz6Mkf.SEED, keyType: KeyType.Ed25519 }) - const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON) - const kid = new DidKey(key).did + const kid = new DidKey(didJwsz6MkfKey).did const jws = await jwsService.createJws(agentContext, { payload, - // FIXME: update to use key instance instead of verkey - verkey: key.publicKeyBase58, + key: didJwsz6MkfKey, header: { kid }, + protectedHeaderOptions: { + alg: 'EdDSA', + jwk: didJwsz6MkfKey.toJwk(), + }, }) expect(jws).toEqual(didJwsz6Mkf.JWS_JSON) @@ -55,37 +59,37 @@ describe('JwsService', () => { it('returns true if the jws signature matches the payload', async () => { const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON) - const { isValid, signerVerkeys } = await jwsService.verifyJws(agentContext, { + const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, { payload, jws: didJwsz6Mkf.JWS_JSON, }) expect(isValid).toBe(true) - expect(signerVerkeys).toEqual([didJwsz6Mkf.VERKEY]) + expect(signerKeys).toEqual([didJwsz6MkfKey]) }) it('returns all verkeys that signed the jws', async () => { const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON) - const { isValid, signerVerkeys } = await jwsService.verifyJws(agentContext, { + const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, { payload, jws: { signatures: [didJwsz6Mkf.JWS_JSON, didJwsz6Mkv.JWS_JSON] }, }) expect(isValid).toBe(true) - expect(signerVerkeys).toEqual([didJwsz6Mkf.VERKEY, didJwsz6Mkv.VERKEY]) + expect(signerKeys).toEqual([didJwsz6MkfKey, didJwsz6MkvKey]) }) it('returns false if the jws signature does not match the payload', async () => { const payload = JsonEncoder.toBuffer({ ...didJwsz6Mkf.DATA_JSON, did: 'another_did' }) - const { isValid, signerVerkeys } = await jwsService.verifyJws(agentContext, { + const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, { payload, jws: didJwsz6Mkf.JWS_JSON, }) expect(isValid).toBe(false) - expect(signerVerkeys).toMatchObject([]) + expect(signerKeys).toMatchObject([]) }) it('throws an error if the jws signatures array does not contain a JWS', async () => { diff --git a/packages/core/src/crypto/index.ts b/packages/core/src/crypto/index.ts index 49b83878ad..449f3e537f 100644 --- a/packages/core/src/crypto/index.ts +++ b/packages/core/src/crypto/index.ts @@ -1,3 +1,9 @@ +export { Jwk } from './JwkTypes' +export { JwsService } from './JwsService' + +export * from './jwtUtils' + export { KeyType } from './KeyType' export { Key } from './Key' + export * from './signing-provider' diff --git a/packages/core/src/crypto/jwtUtils.ts b/packages/core/src/crypto/jwtUtils.ts new file mode 100644 index 0000000000..f60958fdf4 --- /dev/null +++ b/packages/core/src/crypto/jwtUtils.ts @@ -0,0 +1,13 @@ +export const jwtKeyAlgMapping = { + HMAC: ['HS256', 'HS384', 'HS512'], + RSA: ['RS256', 'RS384', 'RS512'], + ECDSA: ['ES256', 'ES384', 'ES512'], + 'RSA-PSS': ['PS256', 'PS384', 'PS512'], + EdDSA: ['Ed25519'], +} + +export type JwtAlgorithm = keyof typeof jwtKeyAlgMapping + +export function isJwtAlgorithm(value: string): value is JwtAlgorithm { + return Object.keys(jwtKeyAlgMapping).includes(value) +} diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts index c577a260f7..d337a818de 100644 --- a/packages/core/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -464,10 +464,14 @@ export class DidExchangeProtocol { const jws = await this.jwsService.createJws(agentContext, { payload, - verkey, + key, header: { kid, }, + protectedHeaderOptions: { + alg: 'EdDSA', + jwk: key.toJwk(), + }, }) didDocAttach.addJws(jws) }) @@ -510,7 +514,7 @@ export class DidExchangeProtocol { this.logger.trace('DidDocument JSON', json) const payload = JsonEncoder.toBuffer(json) - const { isValid, signerVerkeys } = await this.jwsService.verifyJws(agentContext, { jws, payload }) + const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { jws, payload }) const didDocument = JsonTransformer.fromJSON(json, DidDocument) const didDocumentKeysBase58 = didDocument.authentication @@ -525,9 +529,9 @@ export class DidExchangeProtocol { }) .concat(invitationKeysBase58) - this.logger.trace('JWS verification result', { isValid, signerVerkeys, didDocumentKeysBase58 }) + this.logger.trace('JWS verification result', { isValid, signerKeys, didDocumentKeysBase58 }) - if (!isValid || !signerVerkeys.every((verkey) => didDocumentKeysBase58?.includes(verkey))) { + if (!isValid || !signerKeys.every((key) => didDocumentKeysBase58?.includes(key.publicKeyBase58))) { const problemCode = message instanceof DidExchangeRequestMessage ? DidExchangeProblemReportReason.RequestNotAccepted diff --git a/packages/core/src/modules/dids/DidsApi.ts b/packages/core/src/modules/dids/DidsApi.ts index 59134e5f6d..49b997d8e5 100644 --- a/packages/core/src/modules/dids/DidsApi.ts +++ b/packages/core/src/modules/dids/DidsApi.ts @@ -96,7 +96,7 @@ export class DidsApi { * * You can call `${@link DidsModule.resolve} to resolve the did document based on the did itself. */ - public getCreatedDids({ method }: { method?: string } = {}) { - return this.didRepository.getCreatedDids(this.agentContext, { method }) + public getCreatedDids({ method, did }: { method?: string; did?: string } = {}) { + return this.didRepository.getCreatedDids(this.agentContext, { method, did }) } } diff --git a/packages/core/src/modules/dids/repository/DidRepository.ts b/packages/core/src/modules/dids/repository/DidRepository.ts index 80fad9cf52..538270eac5 100644 --- a/packages/core/src/modules/dids/repository/DidRepository.ts +++ b/packages/core/src/modules/dids/repository/DidRepository.ts @@ -57,10 +57,11 @@ export class DidRepository extends Repository { return this.findSingleByQuery(agentContext, { did: createdDid, role: DidDocumentRole.Created }) } - public getCreatedDids(agentContext: AgentContext, { method }: { method?: string }) { + public getCreatedDids(agentContext: AgentContext, { method, did }: { method?: string; did?: string }) { return this.findByQuery(agentContext, { role: DidDocumentRole.Created, method, + did, }) } } diff --git a/packages/core/src/modules/vc/W3cCredentialService.ts b/packages/core/src/modules/vc/W3cCredentialService.ts index e841a7162d..a1896abb26 100644 --- a/packages/core/src/modules/vc/W3cCredentialService.ts +++ b/packages/core/src/modules/vc/W3cCredentialService.ts @@ -103,7 +103,8 @@ export class W3cCredentialService { */ public async verifyCredential( agentContext: AgentContext, - options: VerifyCredentialOptions + options: VerifyCredentialOptions, + verifyRevocationState = true ): Promise { const suites = this.getSignatureSuitesForCredential(agentContext, options.credential) @@ -111,6 +112,14 @@ export class W3cCredentialService { credential: JsonTransformer.toJSON(options.credential), suite: suites, documentLoader: this.w3cVcModuleConfig.documentLoader(agentContext), + checkStatus: () => { + if (verifyRevocationState) { + throw new AriesFrameworkError('Revocation for W3C credentials is currently not supported') + } + return { + verified: true, + } + }, } // this is a hack because vcjs throws if purpose is passed as undefined or null diff --git a/packages/core/src/modules/vc/__tests__/contexts/mattr_vc_extension_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/mattr_vc_extension_v1.ts new file mode 100644 index 0000000000..aaadf21bb5 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/mattr_vc_extension_v1.ts @@ -0,0 +1,17 @@ +export const MATTR_VC_EXTENSION_V1 = { + '@context': { + '@version': 1.1, + '@protected': true, + VerifiableCredentialExtension: { + '@id': 'https://mattr.global/contexts/vc-extensions/v1#VerifiableCredentialExtension', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + name: 'https://mattr.global/contexts/vc-extensions/v1#name', + description: 'https://mattr.global/contexts/vc-extensions/v1#description', + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/purl_ob_v3po.ts b/packages/core/src/modules/vc/__tests__/contexts/purl_ob_v3po.ts new file mode 100644 index 0000000000..3b2ffe28f6 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/purl_ob_v3po.ts @@ -0,0 +1,438 @@ +export const PURL_OB_V3P0 = { + '@context': { + id: '@id', + type: '@type', + xsd: 'https://www.w3.org/2001/XMLSchema#', + OpenBadgeCredential: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#OpenBadgeCredential', + }, + Achievement: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Achievement', + '@context': { + achievementType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#achievementType', + '@type': 'xsd:string', + }, + alignment: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#alignment', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Alignment', + '@container': '@set', + }, + creator: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Profile', + }, + creditsAvailable: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#creditsAvailable', + '@type': 'xsd:float', + }, + criteria: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Criteria', + '@type': '@id', + }, + fieldOfStudy: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#fieldOfStudy', + '@type': 'xsd:string', + }, + humanCode: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#humanCode', + '@type': 'xsd:string', + }, + otherIdentifier: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#otherIdentifier', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentifierEntry', + '@container': '@set', + }, + related: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#related', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Related', + '@container': '@set', + }, + resultDescription: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#resultDescription', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#ResultDescription', + '@container': '@set', + }, + specialization: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#specialization', + '@type': 'xsd:string', + }, + tag: { + '@id': 'https://schema.org/keywords', + '@type': 'xsd:string', + '@container': '@set', + }, + version: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#version', + '@type': 'xsd:string', + }, + }, + }, + AchievementCredential: { + '@id': 'OpenBadgeCredential', + }, + AchievementSubject: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#AchievementSubject', + '@context': { + achievement: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Achievement', + }, + activityEndDate: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#activityEndDate', + '@type': 'xsd:date', + }, + activityStartDate: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#activityStartDate', + '@type': 'xsd:date', + }, + creditsEarned: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#creditsEarned', + '@type': 'xsd:float', + }, + identifier: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identifier', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentityObject', + '@container': '@set', + }, + licenseNumber: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#licenseNumber', + '@type': 'xsd:string', + }, + result: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#result', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Result', + '@container': '@set', + }, + role: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#role', + '@type': 'xsd:string', + }, + source: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#source', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Profile', + }, + term: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#term', + '@type': 'xsd:string', + }, + }, + }, + Address: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Address', + '@context': { + addressCountry: { + '@id': 'https://schema.org/addressCountry', + '@type': 'xsd:string', + }, + addressCountryCode: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#CountryCode', + '@type': 'xsd:string', + }, + addressLocality: { + '@id': 'https://schema.org/addressLocality', + '@type': 'xsd:string', + }, + addressRegion: { + '@id': 'https://schema.org/addressRegion', + '@type': 'xsd:string', + }, + geo: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#GeoCoordinates', + }, + postOfficeBoxNumber: { + '@id': 'https://schema.org/postOfficeBoxNumber', + '@type': 'xsd:string', + }, + postalCode: { + '@id': 'https://schema.org/postalCode', + '@type': 'xsd:string', + }, + streetAddress: { + '@id': 'https://schema.org/streetAddress', + '@type': 'xsd:string', + }, + }, + }, + Alignment: { + '@id': 'https://schema.org/Alignment', + '@context': { + targetCode: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#targetCode', + '@type': 'xsd:string', + }, + targetDescription: { + '@id': 'https://schema.org/targetDescription', + '@type': 'xsd:string', + }, + targetFramework: { + '@id': 'https://schema.org/targetFramework', + '@type': 'xsd:string', + }, + targetName: { + '@id': 'https://schema.org/targetName', + '@type': 'xsd:string', + }, + targetType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#targetType', + '@type': 'xsd:string', + }, + targetUrl: { + '@id': 'https://schema.org/targetUrl', + '@type': 'xsd:anyURI', + }, + }, + }, + Criteria: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Criteria', + }, + EndorsementCredential: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#EndorsementCredential', + }, + EndorsementSubject: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#EndorsementSubject', + '@context': { + endorsementComment: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#endorsementComment', + '@type': 'xsd:string', + }, + }, + }, + Evidence: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Evidence', + '@context': { + audience: { + '@id': 'https://schema.org/audience', + '@type': 'xsd:string', + }, + genre: { + '@id': 'https://schema.org/genre', + '@type': 'xsd:string', + }, + }, + }, + GeoCoordinates: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#GeoCoordinates', + '@context': { + latitude: { + '@id': 'https://schema.org/latitude', + '@type': 'xsd:string', + }, + longitude: { + '@id': 'https://schema.org/longitude', + '@type': 'xsd:string', + }, + }, + }, + IdentifierEntry: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentifierEntry', + '@context': { + identifier: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identifier', + '@type': 'xsd:string', + }, + identifierType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identifierType', + '@type': 'xsd:string', + }, + }, + }, + IdentityObject: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentityObject', + '@context': { + hashed: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#hashed', + '@type': 'xsd:boolean', + }, + identityHash: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identityHash', + '@type': 'xsd:string', + }, + identityType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#identityType', + '@type': 'xsd:string', + }, + salt: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#salt', + '@type': 'xsd:string', + }, + }, + }, + Image: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Image', + '@context': { + caption: { + '@id': 'https://schema.org/caption', + '@type': 'xsd:string', + }, + }, + }, + Profile: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Profile', + '@context': { + additionalName: { + '@id': 'https://schema.org/additionalName', + '@type': 'xsd:string', + }, + address: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Address', + }, + dateOfBirth: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#dateOfBirth', + '@type': 'xsd:date', + }, + email: { + '@id': 'https://schema.org/email', + '@type': 'xsd:string', + }, + familyName: { + '@id': 'https://schema.org/familyName', + '@type': 'xsd:string', + }, + familyNamePrefix: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#familyNamePrefix', + '@type': 'xsd:string', + }, + givenName: { + '@id': 'https://schema.org/givenName', + '@type': 'xsd:string', + }, + honorificPrefix: { + '@id': 'https://schema.org/honorificPrefix', + '@type': 'xsd:string', + }, + honorificSuffix: { + '@id': 'https://schema.org/honorificSuffix', + '@type': 'xsd:string', + }, + otherIdentifier: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#otherIdentifier', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#IdentifierEntry', + '@container': '@set', + }, + parentOrg: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#parentOrg', + '@type': 'xsd:string', + }, + patronymicName: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#patronymicName', + '@type': 'xsd:string', + }, + phone: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#PhoneNumber', + '@type': 'xsd:string', + }, + official: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#official', + '@type': 'xsd:string', + }, + }, + }, + Related: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Related', + '@context': { + version: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#version', + '@type': 'xsd:string', + }, + }, + }, + Result: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Result', + '@context': { + achievedLevel: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#achievedLevel', + '@type': 'xsd:anyURI', + }, + resultDescription: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#resultDescription', + '@type': 'xsd:anyURI', + }, + status: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#status', + '@type': 'xsd:string', + }, + value: { + '@id': 'https://schema.org/value', + '@type': 'xsd:string', + }, + }, + }, + ResultDescription: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#ResultDescription', + '@context': { + allowedValue: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#allowedValue', + '@type': 'xsd:string', + '@container': '@set', + }, + requiredLevel: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#requiredLevel', + '@type': 'xsd:anyURI', + }, + requiredValue: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#requiredValue', + '@type': 'xsd:string', + }, + resultType: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#resultType', + '@type': 'xsd:string', + }, + rubricCriterionLevel: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#rubricCriterionLevel', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#RubricCriterionLevel', + '@container': '@set', + }, + valueMax: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#valueMax', + '@type': 'xsd:string', + }, + valueMin: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#valueMin', + '@type': 'xsd:string', + }, + }, + }, + RubricCriterionLevel: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#RubricCriterionLevel', + '@context': { + level: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#level', + '@type': 'xsd:string', + }, + points: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#points', + '@type': 'xsd:string', + }, + }, + }, + alignment: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#alignment', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Alignment', + '@container': '@set', + }, + description: { + '@id': 'https://schema.org/description', + '@type': 'xsd:string', + }, + endorsement: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#endorsement', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#EndorsementCredential', + '@container': '@set', + }, + image: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#image', + '@type': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#Image', + }, + name: { + '@id': 'https://schema.org/name', + '@type': 'xsd:string', + }, + narrative: { + '@id': 'https://purl.imsglobal.org/spec/vc/ob/vocab.html#narrative', + '@type': 'xsd:string', + }, + url: { + '@id': 'https://schema.org/url', + '@type': 'xsd:anyURI', + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/vc_revocation_list_2020.ts b/packages/core/src/modules/vc/__tests__/contexts/vc_revocation_list_2020.ts new file mode 100644 index 0000000000..d0646eaa25 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/vc_revocation_list_2020.ts @@ -0,0 +1,37 @@ +export const VC_REVOCATION_LIST_2020 = { + '@context': { + '@protected': true, + RevocationList2020Credential: { + '@id': 'https://w3id.org/vc-revocation-list-2020#RevocationList2020Credential', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + name: 'http://schema.org/name', + }, + }, + RevocationList2020: { + '@id': 'https://w3id.org/vc-revocation-list-2020#RevocationList2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + encodedList: 'https://w3id.org/vc-revocation-list-2020#encodedList', + }, + }, + RevocationList2020Status: { + '@id': 'https://w3id.org/vc-revocation-list-2020#RevocationList2020Status', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + revocationListCredential: { + '@id': 'https://w3id.org/vc-revocation-list-2020#revocationListCredential', + '@type': '@id', + }, + revocationListIndex: 'https://w3id.org/vc-revocation-list-2020#revocationListIndex', + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_web_launchpad.ts b/packages/core/src/modules/vc/__tests__/dids/did_web_launchpad.ts new file mode 100644 index 0000000000..81c02d5555 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_web_launchpad.ts @@ -0,0 +1,24 @@ +export const DID_WEB_LAUNCHPAD = { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + '@context': ['https://w3.org/ns/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + verificationMethod: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', + type: 'Ed25519VerificationKey2018', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: '6BhFMCGTJg9DnpXZe7zbiTrtuwion5FVV6Z2NUpwDMVT', + }, + ], + keyAgreement: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#9eS8Tqsus1', + type: 'X25519KeyAgreementKey2019', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: '9eS8Tqsus1uJmQpf37S8CnEeBrEehsC3qz8RMq67KoLB', + }, + ], + authentication: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + assertionMethod: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + capabilityDelegation: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + capabilityInvocation: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], +} diff --git a/packages/core/src/modules/vc/__tests__/documentLoader.ts b/packages/core/src/modules/vc/__tests__/documentLoader.ts index 544ae02972..4d7aa89f0d 100644 --- a/packages/core/src/modules/vc/__tests__/documentLoader.ts +++ b/packages/core/src/modules/vc/__tests__/documentLoader.ts @@ -10,11 +10,15 @@ import { CITIZENSHIP_V1 } from './contexts/citizenship_v1' import { CREDENTIALS_V1 } from './contexts/credentials_v1' import { DID_V1 } from './contexts/did_v1' import { ED25519_V1 } from './contexts/ed25519_v1' +import { MATTR_VC_EXTENSION_V1 } from './contexts/mattr_vc_extension_v1' +import { PURL_OB_V3P0 } from './contexts/purl_ob_v3po' import { SECURITY_V1 } from './contexts/security_v1' import { SECURITY_V2 } from './contexts/security_v2' import { SECURITY_V3_UNSTABLE } from './contexts/security_v3_unstable' +import { VC_REVOCATION_LIST_2020 } from './contexts/vc_revocation_list_2020' import { DID_EXAMPLE_48939859 } from './dids/did_example_489398593' import { DID_SOV_QqEfJxe752NCmWqR5TssZ5 } from './dids/did_sov_QqEfJxe752NCmWqR5TssZ5' +import { DID_WEB_LAUNCHPAD } from './dids/did_web_launchpad' import { DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL } from './dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL' import { DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV } from './dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV' import { DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa } from './dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa' @@ -72,6 +76,7 @@ export const DOCUMENTS = { ]]: DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN, [DID_SOV_QqEfJxe752NCmWqR5TssZ5['id']]: DID_SOV_QqEfJxe752NCmWqR5TssZ5, + [DID_WEB_LAUNCHPAD['id']]: DID_WEB_LAUNCHPAD, SECURITY_CONTEXT_V1_URL: SECURITY_V1, SECURITY_CONTEXT_V2_URL: SECURITY_V2, SECURITY_CONTEXT_V3_URL: SECURITY_V3_UNSTABLE, @@ -88,10 +93,14 @@ export const DOCUMENTS = { 'https://www.w3.org/2018/credentials/v1': CREDENTIALS_V1, 'https://w3id.org/did/v1': DID_V1, 'https://www.w3.org/ns/did/v1': DID_V1, + 'https://w3.org/ns/did/v1': DID_V1, 'https://w3id.org/citizenship/v1': CITIZENSHIP_V1, 'https://www.w3.org/ns/odrl.jsonld': ODRL, 'http://schema.org/': SCHEMA_ORG, 'https://w3id.org/vaccination/v1': VACCINATION_V1, + 'https://mattr.global/contexts/vc-extensions/v1': MATTR_VC_EXTENSION_V1, + 'https://purl.imsglobal.org/spec/ob/v3p0/context.json': PURL_OB_V3P0, + 'https://w3c-ccg.github.io/vc-status-rl-2020/contexts/vc-revocation-list-2020/v1.jsonld': VC_REVOCATION_LIST_2020, } async function _customDocumentLoader(url: string): Promise { diff --git a/packages/core/src/modules/vc/models/credential/W3cCredential.ts b/packages/core/src/modules/vc/models/credential/W3cCredential.ts index ca5a1398bc..16f0b3f71c 100644 --- a/packages/core/src/modules/vc/models/credential/W3cCredential.ts +++ b/packages/core/src/modules/vc/models/credential/W3cCredential.ts @@ -40,7 +40,7 @@ export class W3cCredential { @Expose({ name: '@context' }) @IsJsonLdContext() - public context!: Array | JsonObject + public context!: Array | JsonObject @IsOptional() @IsUri() @@ -91,7 +91,7 @@ export class W3cCredential { return [this.credentialSubject.id] } - public get contexts(): Array { + public get contexts(): Array { if (Array.isArray(this.context)) { return this.context.filter((x) => typeof x === 'string') } diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts index 3c10234210..6ba94480be 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts @@ -44,12 +44,16 @@ export class W3cCredentialRecord extends BaseRecord typeof ctx === 'string') as string[] + return { ...this._tags, issuerId: this.credential.issuerId, subjectIds: this.credential.credentialSubjectIds, schemaIds: this.credential.credentialSchemaIds, - contexts: this.credential.contexts, + contexts: stringContexts, proofTypes: this.credential.proofTypes, givenId: this.credential.id, } diff --git a/packages/core/src/modules/vc/validators.ts b/packages/core/src/modules/vc/validators.ts index 0bce78fa79..317b286cf6 100644 --- a/packages/core/src/modules/vc/validators.ts +++ b/packages/core/src/modules/vc/validators.ts @@ -2,6 +2,8 @@ import type { ValidationOptions } from 'class-validator' import { buildMessage, isString, isURL, ValidateBy } from 'class-validator' +import { isJsonObject } from '../../utils/type' + import { CREDENTIALS_CONTEXT_V1_URL } from './constants' export function IsJsonLdContext(validationOptions?: ValidationOptions): PropertyDecorator { @@ -13,8 +15,11 @@ export function IsJsonLdContext(validationOptions?: ValidationOptions): Property // If value is an array, check if all items are strings, are URLs and that // the first entry is a verifiable credential context if (Array.isArray(value)) { - return value.every((v) => isString(v) && isURL(v)) && value[0] === CREDENTIALS_CONTEXT_V1_URL + return value.every( + (v) => (isString(v) && isURL(v)) || (isJsonObject(v) && value[0] === CREDENTIALS_CONTEXT_V1_URL) + ) } + // If value is not an array, check if it is an object (assuming it's a JSON-LD context definition) if (typeof value === 'object') { return true diff --git a/packages/openid4vc-client/README.md b/packages/openid4vc-client/README.md index e89f6cab7a..540339fef7 100644 --- a/packages/openid4vc-client/README.md +++ b/packages/openid4vc-client/README.md @@ -32,6 +32,136 @@ Open ID Connect For Verifiable Credentials Client Module for [Aries Framework Ja ### Installation +Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. + +```sh +yarn add @aries-framework/openid4vc-client +``` + ### Quick start -### Example of usage +#### Requirements + +Before a credential can be requested, you need the issuer URI. This URI starts with `openid-initiate-issuance://` and is provided by the issuer. The issuer URI is commonly acquired by scanning a QR code. + +#### Module registration + +In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. + +```ts +import { OpenId4VcClientModule } from '@aries-framework/openid4vc-client' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + openId4VcClient: new OpenId4VcClientModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() +``` + +How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcClient`. + +#### Preparing a DID + +In order to request a credential, you'll need to provide a DID that the issuer will use for setting the credential subject. In the following snippet we create one for the sake of the example, but this can be any DID that has a _authentication verification method_ with key type `Ed25519`. + +```ts +// first we create the DID +const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, +}) + +// next we do some assertions and extract the key identifier (kid) + +if ( + !did.didState.didDocument || + !did.didState.didDocument.authentication || + did.didState.didDocument.authentication.length === 0 +) { + throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") +} + +const [verificationMethod] = did.didState.didDocument.authentication +const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id +``` + +#### Requesting the credential (Pre-Authorized) + +Now a credential issuance can be requested as follows. + +```ts +const w3cCredentialRecord = await agent.modules.openId4VcClient.requestCredentialPreAuthorized({ + issuerUri, + kid, + checkRevocationState: false, +}) + +console.log(w3cCredentialRecord) +``` + +#### Full example + +```ts +import { OpenId4VcClientModule } from '@aries-framework/openid4vc-client' +import { agentDependencies } from '@aries-framework/node' // use @aries-framework/react-native for React Native +import { Agent, KeyDidCreateOptions } from '@aries-framework/core' + +const run = async () => { + const issuerUri = '' // The obtained issuer URI + + // Create the Agent + const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + openId4VcClient: new OpenId4VcClientModule(), + /* other custom modules */ + }, + }) + + // Initialize the Agent + await agent.initialize() + + // Create a DID + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + }) + + // Assert DIDDocument is valid + if ( + !did.didState.didDocument || + !did.didState.didDocument.authentication || + did.didState.didDocument.authentication.length === 0 + ) { + throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") + } + + // Extract key identified (kid) for authentication verification method + const [verificationMethod] = did.didState.didDocument.authentication + const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id + + // Request the credential + const w3cCredentialRecord = await agent.modules.openId4VcClient.requestCredentialPreAuthorized({ + issuerUri, + kid, + checkRevocationState: false, + }) + + // Log the received credential + console.log(w3cCredentialRecord) +} +``` diff --git a/packages/openid4vc-client/package.json b/packages/openid4vc-client/package.json index 3f7a671015..97d83446a2 100644 --- a/packages/openid4vc-client/package.json +++ b/packages/openid4vc-client/package.json @@ -6,7 +6,6 @@ "files": [ "build" ], - "private": true, "license": "Apache-2.0", "publishConfig": { "access": "public" @@ -26,14 +25,11 @@ }, "dependencies": { "@aries-framework/core": "0.3.3", - "@sphereon/openid4vci-client": "^0.3.6", - "class-transformer": "0.5.1", - "class-validator": "0.13.1" + "@sphereon/openid4vci-client": "^0.3.6" }, - "peerDependencies": {}, "devDependencies": { "@aries-framework/node": "0.3.3", - "reflect-metadata": "^0.1.13", + "nock": "^13.3.0", "rimraf": "^4.0.7", "typescript": "~4.9.4" } diff --git a/packages/openid4vc-client/src/OpenId4VcClientApi.ts b/packages/openid4vc-client/src/OpenId4VcClientApi.ts index ccf2cb84f3..ab671c46e1 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientApi.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientApi.ts @@ -1,7 +1,15 @@ +import type { W3cCredentialRecord } from '@aries-framework/core' + import { AgentContext, injectable } from '@aries-framework/core' import { OpenId4VcClientService } from './OpenId4VcClientService' +interface PreAuthorizedOptions { + issuerUri: string + kid: string + checkRevocationState?: boolean // default = true +} + /** * @public */ @@ -14,4 +22,14 @@ export class OpenId4VcClientApi { this.agentContext = agentContext this.openId4VcClientService = openId4VcClientService } + + public async requestCredentialPreAuthorized(options: PreAuthorizedOptions): Promise { + // set defaults + const checkRevocationState = options.checkRevocationState ?? true + + return this.openId4VcClientService.requestCredentialPreAuthorized(this.agentContext, { + ...options, + checkRevocationState: checkRevocationState, + }) + } } diff --git a/packages/openid4vc-client/src/OpenId4VcClientApiOptions.ts b/packages/openid4vc-client/src/OpenId4VcClientApiOptions.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/openid4vc-client/src/OpenId4VcClientService.ts b/packages/openid4vc-client/src/OpenId4VcClientService.ts index 9c54e9b81c..9193b9d219 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientService.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientService.ts @@ -1,10 +1,236 @@ -import { injectable, W3cCredentialService } from '@aries-framework/core' +import type { AgentContext, W3cCredentialRecord } from '@aries-framework/core' +import type { EndpointMetadata, Jwt } from '@sphereon/openid4vci-client' + +import { + inject, + InjectionSymbols, + isJwtAlgorithm, + Logger, + DidsApi, + getKeyDidMappingByVerificationMethod, + AriesFrameworkError, + injectable, + JsonEncoder, + JsonTransformer, + W3cCredentialService, + W3cVerifiableCredential, + JwsService, + jwtKeyAlgMapping, +} from '@aries-framework/core' +import { + Alg, + AuthzFlowType, + CredentialRequestClientBuilder, + OpenID4VCIClient, + ProofOfPossessionBuilder, +} from '@sphereon/openid4vci-client' + +export interface PreAuthorizedOptions { + issuerUri: string + kid: string + checkRevocationState: boolean +} @injectable() export class OpenId4VcClientService { + private logger: Logger private w3cCredentialService: W3cCredentialService + private jwsService: JwsService - public constructor(w3cCredentialService: W3cCredentialService) { + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + w3cCredentialService: W3cCredentialService, + jwsService: JwsService + ) { this.w3cCredentialService = w3cCredentialService + this.jwsService = jwsService + this.logger = logger + } + + private signCallback(agentContext: AgentContext) { + return async (jwt: Jwt, kid: string) => { + if (!jwt.header) { + throw new AriesFrameworkError('No header present on JWT') + } + + if (!jwt.payload) { + throw new AriesFrameworkError('No payload present on JWT') + } + if (!kid.startsWith('did:')) { + throw new AriesFrameworkError(`kid '${kid}' is not a valid did. Only dids are supported as kid.`) + } + + if (!kid.includes('#')) { + throw new AriesFrameworkError( + `kid '${kid}' does not include a reference to the verificationMethod. The kid must specify a specific verificationMethod within the did document .` + ) + } + + const did = kid.split('#')[0] + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const [didRecord] = await didsApi.getCreatedDids({ did }) + + if (!didRecord) { + throw new AriesFrameworkError(`No did record found for did ${did}. Is the did created by this agent?`) + } + + const didResult = await didsApi.resolve(did) + + if (!didResult.didDocument) { + throw new AriesFrameworkError( + `No did document found for did ${did}. ${didResult.didResolutionMetadata.error} - ${didResult.didResolutionMetadata.message}` + ) + } + + // TODO: which purposes are allowed? + const verificationMethod = didResult.didDocument.dereferenceKey(kid, ['authentication']) + const { getKeyFromVerificationMethod } = getKeyDidMappingByVerificationMethod(verificationMethod) + const key = getKeyFromVerificationMethod(verificationMethod) + + const payload = JsonEncoder.toBuffer(jwt.payload) + + if (!isJwtAlgorithm(jwt.header.alg)) { + throw new AriesFrameworkError(`Unknown JWT algorithm: ${jwt.header.alg}`) + } + + if (jwtKeyAlgMapping[jwt.header.alg].includes(key.keyType)) { + throw new AriesFrameworkError( + `The retreived key's type does't match the JWT algorithm. Key type: ${key.keyType}, JWT algorithm: ${jwt.header.alg}` + ) + } + + const jws = await this.jwsService.createJwsCompact(agentContext, { + key, + payload, + protectedHeaderOptions: { + alg: jwt.header.alg, + kid: jwt.header.kid, + }, + }) + + return jws + } + } + + private getSignCallback(agentContext: AgentContext) { + return { + signCallback: this.signCallback(agentContext), + } + } + + private assertCredentialHasFormat(format: string, scope: string, metadata: EndpointMetadata) { + if (!metadata.openid4vci_metadata) { + throw new AriesFrameworkError( + `Server metadata doesn't include OpenID4VCI metadata. Unable to verify if the issuer supports the requested credential format: ${format}` + ) + } + + const supportedFomats = Object.keys(metadata.openid4vci_metadata?.credentials_supported[scope].formats) + + if (!supportedFomats.includes(format)) { + throw new AriesFrameworkError( + `Issuer doesn't support the requested credential format '${format}'' for requested credential type '${scope}'. Supported formats are: ${supportedFomats}` + ) + } + } + + public async requestCredentialPreAuthorized( + agentContext: AgentContext, + options: PreAuthorizedOptions + ): Promise { + this.logger.debug('Running pre-authorized flow with options', options) + + // this value is hardcoded as it's the only supported format at this point + const credentialFormat = 'ldp_vc' + + const client = await OpenID4VCIClient.initiateFromURI({ + issuanceInitiationURI: options.issuerUri, + flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW, + kid: options.kid, + alg: Alg.EdDSA, + }) + + const accessToken = await client.acquireAccessToken({}) + + this.logger.info('Fetched server accessToken', accessToken) + + // We currently need the ts-ignore because the type + // inside of OpenID4VCIClient needs to be updated. + // @ts-ignore + if (!accessToken.scope) { + throw new AriesFrameworkError( + "Access token response doesn't contain a scope. Only scoped issuer URIs are supported at this time." + ) + } + + const serverMetadata = await client.retrieveServerMetadata() + + // @ts-ignore + this.assertCredentialHasFormat(credentialFormat, accessToken.scope, serverMetadata) + + this.logger.info('Fetched server metadata', { + issuer: serverMetadata.issuer, + credentialEndpoint: serverMetadata.credential_endpoint, + tokenEndpoint: serverMetadata.token_endpoint, + }) + + this.logger.debug('Full server metadata', serverMetadata) + + // proof of possession + const callbacks = this.getSignCallback(agentContext) + + const proofInput = await ProofOfPossessionBuilder.fromAccessTokenResponse({ + accessTokenResponse: accessToken, + callbacks: callbacks, + }) + .withEndpointMetadata(serverMetadata) + .withAlg(Alg.EdDSA) + .withKid(options.kid) + .build() + + this.logger.debug('Generated JWS', proofInput) + + const credentialRequestClient = CredentialRequestClientBuilder.fromIssuanceInitiationURI({ + uri: options.issuerUri, + metadata: serverMetadata, + }) + .withTokenFromResponse(accessToken) + .build() + + const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ + proofInput, + // @ts-ignore + credentialType: accessToken.scope, + format: 'ldp_vc', // Allows us to override the format + }) + + this.logger.debug('Credential request response', credentialResponse) + + if (!credentialResponse.successBody) { + throw new AriesFrameworkError('Did not receive a successful credential response') + } + + const credential = JsonTransformer.fromJSON(credentialResponse.successBody.credential, W3cVerifiableCredential) + + // verify the signature + const result = await this.w3cCredentialService.verifyCredential( + agentContext, + { credential }, + options.checkRevocationState + ) + + if (result && !result.verified) { + throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error}`) + } + + const storedCredential = await this.w3cCredentialService.storeCredential(agentContext, { + credential, + }) + + this.logger.info(`Stored credential with id: ${storedCredential.id}`) + this.logger.debug('Full credential', storedCredential) + + return storedCredential } } diff --git a/packages/openid4vc-client/tests/fixtures.ts b/packages/openid4vc-client/tests/fixtures.ts new file mode 100644 index 0000000000..44936b9169 --- /dev/null +++ b/packages/openid4vc-client/tests/fixtures.ts @@ -0,0 +1,134 @@ +export const getMetadataResponse = { + authorization_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize', + token_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token', + jwks_uri: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/jwks', + token_endpoint_auth_methods_supported: [ + 'none', + 'client_secret_basic', + 'client_secret_jwt', + 'client_secret_post', + 'private_key_jwt', + ], + code_challenge_methods_supported: ['S256'], + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + response_modes_supported: ['form_post', 'fragment', 'query'], + response_types_supported: ['code id_token', 'code', 'id_token', 'none'], + scopes_supported: ['OpenBadgeCredential', 'AcademicAward', 'LearnerProfile', 'PermanentResidentCard'], + token_endpoint_auth_signing_alg_values_supported: ['HS256', 'RS256', 'PS256', 'ES256', 'EdDSA'], + credential_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/credential', + credentials_supported: { + OpenBadgeCredential: { + formats: { + ldp_vc: { + name: 'JFF x vc-edu PlugFest 2', + description: "MATTR's submission for JFF Plugfest 2", + types: ['OpenBadgeCredential'], + binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + AcademicAward: { + formats: { + ldp_vc: { + name: 'Example Academic Award', + description: 'Microcredential from the MyCreds Network.', + types: ['AcademicAward'], + binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + LearnerProfile: { + formats: { + ldp_vc: { + name: 'Digitary Learner Profile', + description: 'Example', + types: ['LearnerProfile'], + binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + PermanentResidentCard: { + formats: { + ldp_vc: { + name: 'Permanent Resident Card', + description: 'Government of Kakapo', + types: ['PermanentResidentCard'], + binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + }, +} + +export const aquireAccessTokenResponse = { + access_token: '7nikUotMQefxn7oRX56R7MDNE7KJTGfwGjOkHzGaUIG', + expires_in: 3600, + scope: 'OpenBadgeCredential', + token_type: 'Bearer', +} + +export const credentialRequestResponse = { + format: 'w3cvc-jsonld', + credential: { + type: ['VerifiableCredential', 'VerifiableCredentialExtension', 'OpenBadgeCredential'], + issuer: { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + name: 'Jobs for the Future (JFF)', + iconUrl: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', + image: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', + }, + name: 'JFF x vc-edu PlugFest 2', + description: "MATTR's submission for JFF Plugfest 2", + credentialBranding: { + backgroundColor: '#464c49', + }, + issuanceDate: '2023-01-25T16:58:06.292Z', + credentialSubject: { + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + type: ['AchievementSubject'], + achievement: { + id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', + name: 'JFF x vc-edu PlugFest 2 Interoperability', + type: ['Achievement'], + image: { + id: 'https://w3c-ccg.github.io/vc-ed/plugfest-2-2022/images/JFF-VC-EDU-PLUGFEST2-badge-image.png', + type: 'Image', + }, + criteria: { + type: 'Criteria', + narrative: + 'Solutions providers earned this badge by demonstrating interoperability between multiple providers based on the OBv3 candidate final standard, with some additional required fields. Credential issuers earning this badge successfully issued a credential into at least two wallets. Wallet implementers earning this badge successfully displayed credentials issued by at least two different credential issuers.', + }, + description: + 'This credential solution supports the use of OBv3 and w3c Verifiable Credentials and is interoperable with at least two other solutions. This was demonstrated successfully during JFF x vc-edu PlugFest 2.', + }, + }, + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + { + '@vocab': 'https://w3id.org/security/undefinedTerm#', + }, + 'https://mattr.global/contexts/vc-extensions/v1', + 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', + 'https://w3c-ccg.github.io/vc-status-rl-2020/contexts/vc-revocation-list-2020/v1.jsonld', + ], + credentialStatus: { + id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/b4aa46a0-5539-4a6b-aa03-8f6791c22ce3#49', + type: 'RevocationList2020Status', + revocationListIndex: '49', + revocationListCredential: + 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/b4aa46a0-5539-4a6b-aa03-8f6791c22ce3', + }, + proof: { + type: 'Ed25519Signature2018', + created: '2023-01-25T16:58:07Z', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..PrpRKt60yXOzMNiQY5bELX40F6Svwm-FyQ-Jv02VJDfTTH8GPPByjtOb_n3YfWidQVgySfGQ_H7VmCGjvsU6Aw', + proofPurpose: 'assertionMethod', + verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', + }, + }, +} diff --git a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts new file mode 100644 index 0000000000..fe2d5b1dbc --- /dev/null +++ b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts @@ -0,0 +1,110 @@ +import type { KeyDidCreateOptions } from '@aries-framework/core' + +import { Agent, KeyType, LogLevel, W3cCredentialRecord, W3cVcModule } from '@aries-framework/core' +import nock, { cleanAll, enableNetConnect } from 'nock' + +import { didKeyToInstanceOfKey } from '../../core/src/modules/dids/helpers' +import { customDocumentLoader } from '../../core/src/modules/vc/__tests__/documentLoader' +import { getAgentOptions } from '../../core/tests/helpers' + +import { OpenId4VcClientModule } from '@aries-framework/openid4vc-client' + +import { TestLogger } from '../../core/tests/logger' + +import { aquireAccessTokenResponse, credentialRequestResponse, getMetadataResponse } from './fixtures' + +describe('OpenId4VcClient', () => { + let agent: Agent<{ + openId4VcClient: OpenId4VcClientModule + w3cVc: W3cVcModule + }> + + beforeEach(async () => { + const agentOptions = getAgentOptions( + 'OpenId4VcClient Agent', + { + logger: new TestLogger(LogLevel.test), + }, + { + openId4VcClient: new OpenId4VcClientModule(), + w3cVc: new W3cVcModule({ + documentLoader: customDocumentLoader, + }), + } + ) + + agent = new Agent(agentOptions) + await agent.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + describe('Pre-authorized flow', () => { + const issuerUri = + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-' + beforeAll(async () => { + /** + * Below we're setting up some mock HTTP responses. + * These responses are based on the openid-initiate-issuance URI above + * */ + + // setup temporary redirect mock + nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + + // setup server metadata response + const httpMock = nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/openid-credential-issuer') + .reply(200, getMetadataResponse) + + // setup access token response + httpMock.post('/oidc/v1/auth/token').reply(200, aquireAccessTokenResponse) + + // setup credential request response + httpMock.post('/oidc/v1/auth/credential').reply(200, credentialRequestResponse) + }) + + afterAll(async () => { + cleanAll() + enableNetConnect() + }) + + it('Should successfully execute the pre-authorized flow', async () => { + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + secret: { + seed: '96213c3d7fc8d4d6754c7a0fd969598e', + }, + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const keyInstance = didKeyToInstanceOfKey(did.didState.did!) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const kid = `${did.didState.did!}#${keyInstance.fingerprint}` + + const w3cCredentialRecord = await agent.modules.openId4VcClient.requestCredentialPreAuthorized({ + issuerUri, + kid, + checkRevocationState: false, + }) + + expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) + + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableCredentialExtension', + 'OpenBadgeCredential', + ]) + + // @ts-ignore + expect(w3cCredentialRecord.credential.credentialSubject.id).toEqual(did.didState.did) + }) + }) +}) diff --git a/packages/openid4vc-client/tests/setup.ts b/packages/openid4vc-client/tests/setup.ts index 4955aeb601..226f7031fa 100644 --- a/packages/openid4vc-client/tests/setup.ts +++ b/packages/openid4vc-client/tests/setup.ts @@ -1,3 +1 @@ -import 'reflect-metadata' - jest.setTimeout(20000) diff --git a/packages/openid4vc-client/tsconfig.build.json b/packages/openid4vc-client/tsconfig.build.json index 2b75d0adab..2b075bbd85 100644 --- a/packages/openid4vc-client/tsconfig.build.json +++ b/packages/openid4vc-client/tsconfig.build.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig.build.json", "compilerOptions": { - "outDir": "./build" + "outDir": "./build", + "skipLibCheck": true }, "include": ["src/**/*"] } diff --git a/packages/openid4vc-client/tsconfig.json b/packages/openid4vc-client/tsconfig.json index 46efe6f721..c1aca0e050 100644 --- a/packages/openid4vc-client/tsconfig.json +++ b/packages/openid4vc-client/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "types": ["jest"] + "types": ["jest"], + "skipLibCheck": true } } diff --git a/yarn.lock b/yarn.lock index 945e21eff3..4f5d4a3bbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2280,9 +2280,9 @@ uint8arrays "^3.1.1" "@sphereon/ssi-types@^0.8.1-next.123": - version "0.8.1-unstable.145" - resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.8.1-unstable.145.tgz#418cf00ebb077ccb9644e652bc4e7fb0eb23b645" - integrity sha512-ixT8z5bwDWKJaMQTsUeRs7vMg5fz68BRJhxn10Tkeg68nJUEUHck44QJOhog0MmjNJKw2k6U/IqIS0oOdxTSHQ== + version "0.8.1-unstable.179" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.8.1-unstable.179.tgz#9583ea0e1011d03876a9108eb863dce83502ada3" + integrity sha512-Se8n7sh3UEO+LGfUcO946TaQaGJf7ozY5tRo9V3Ssax0Rg5MMSOdlf+YE0tgZ7X84WZOrFTdzUVxpN2tpoYRlQ== dependencies: jwt-decode "^3.1.2" @@ -7574,7 +7574,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -8377,6 +8377,16 @@ nocache@^2.1.0: resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f" integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q== +nock@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.0.tgz#b13069c1a03f1ad63120f994b04bfd2556925768" + integrity sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash "^4.17.21" + propagate "^2.0.0" + node-addon-api@^3.0.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" @@ -9289,6 +9299,11 @@ prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.13.1" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" From 115d89736a8f529034ed0f64c655656bffbe6c9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 23:45:01 +0000 Subject: [PATCH 12/20] build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 (#1258) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4f5d4a3bbb..3fe3c222e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6007,9 +6007,9 @@ html-escaper@^2.0.0: integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== http-cache-semantics@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-errors@2.0.0: version "2.0.0" From f18d1890546f7d66571fe80f2f3fc1fead1cd4c3 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 6 Feb 2023 23:34:21 -0300 Subject: [PATCH 13/20] feat: add initial askar package (#1211) Signed-off-by: Ariel Gentile --- packages/askar/README.md | 31 + packages/askar/jest.config.ts | 14 + packages/askar/package.json | 41 + packages/askar/src/AskarModule.ts | 33 + packages/askar/src/AskarModuleConfig.ts | 44 ++ packages/askar/src/index.ts | 9 + .../askar/src/storage/AskarStorageService.ts | 177 +++++ .../__tests__/AskarStorageService.test.ts | 307 +++++++ packages/askar/src/storage/index.ts | 1 + packages/askar/src/storage/utils.ts | 110 +++ packages/askar/src/types.ts | 3 + packages/askar/src/utils/askarError.ts | 16 + packages/askar/src/utils/askarKeyTypes.ts | 6 + packages/askar/src/utils/askarWalletConfig.ts | 76 ++ packages/askar/src/utils/assertAskarWallet.ts | 13 + packages/askar/src/utils/index.ts | 3 + packages/askar/src/wallet/AskarWallet.ts | 748 ++++++++++++++++++ .../AskarWalletPostgresStorageConfig.ts | 22 + packages/askar/src/wallet/JweEnvelope.ts | 62 ++ .../src/wallet/__tests__/AskarWallet.test.ts | 252 ++++++ .../src/wallet/__tests__/packing.test.ts | 52 ++ packages/askar/src/wallet/index.ts | 2 + .../askar/tests/askar-postgres.e2e.test.ts | 102 +++ packages/askar/tests/helpers.ts | 49 ++ packages/askar/tests/setup.ts | 11 + packages/askar/tsconfig.build.json | 7 + packages/askar/tsconfig.json | 6 + packages/core/src/agent/Agent.ts | 6 +- packages/core/src/storage/FileSystem.ts | 1 + packages/core/src/types.ts | 10 +- packages/core/src/utils/TypedArrayEncoder.ts | 2 +- packages/node/src/NodeFileSystem.ts | 4 + .../react-native/src/ReactNativeFileSystem.ts | 4 + .../e2e-askar-indy-sdk-wallet-subject.test.ts | 135 ++++ yarn.lock | 32 + 35 files changed, 2383 insertions(+), 8 deletions(-) create mode 100644 packages/askar/README.md create mode 100644 packages/askar/jest.config.ts create mode 100644 packages/askar/package.json create mode 100644 packages/askar/src/AskarModule.ts create mode 100644 packages/askar/src/AskarModuleConfig.ts create mode 100644 packages/askar/src/index.ts create mode 100644 packages/askar/src/storage/AskarStorageService.ts create mode 100644 packages/askar/src/storage/__tests__/AskarStorageService.test.ts create mode 100644 packages/askar/src/storage/index.ts create mode 100644 packages/askar/src/storage/utils.ts create mode 100644 packages/askar/src/types.ts create mode 100644 packages/askar/src/utils/askarError.ts create mode 100644 packages/askar/src/utils/askarKeyTypes.ts create mode 100644 packages/askar/src/utils/askarWalletConfig.ts create mode 100644 packages/askar/src/utils/assertAskarWallet.ts create mode 100644 packages/askar/src/utils/index.ts create mode 100644 packages/askar/src/wallet/AskarWallet.ts create mode 100644 packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts create mode 100644 packages/askar/src/wallet/JweEnvelope.ts create mode 100644 packages/askar/src/wallet/__tests__/AskarWallet.test.ts create mode 100644 packages/askar/src/wallet/__tests__/packing.test.ts create mode 100644 packages/askar/src/wallet/index.ts create mode 100644 packages/askar/tests/askar-postgres.e2e.test.ts create mode 100644 packages/askar/tests/helpers.ts create mode 100644 packages/askar/tests/setup.ts create mode 100644 packages/askar/tsconfig.build.json create mode 100644 packages/askar/tsconfig.json create mode 100644 tests/e2e-askar-indy-sdk-wallet-subject.test.ts diff --git a/packages/askar/README.md b/packages/askar/README.md new file mode 100644 index 0000000000..5f68099a30 --- /dev/null +++ b/packages/askar/README.md @@ -0,0 +1,31 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript Askar Module

+

+ License + typescript + @aries-framework/askar version + +

+
+ +Askar module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). diff --git a/packages/askar/jest.config.ts b/packages/askar/jest.config.ts new file mode 100644 index 0000000000..55c67d70a6 --- /dev/null +++ b/packages/askar/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + name: packageJson.name, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/askar/package.json b/packages/askar/package.json new file mode 100644 index 0000000000..5ed1b8b150 --- /dev/null +++ b/packages/askar/package.json @@ -0,0 +1,41 @@ +{ + "name": "@aries-framework/askar", + "main": "build/index", + "types": "build/index", + "version": "0.3.3", + "private": true, + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/askar", + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "packages/askar" + }, + "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", + "@hyperledger/aries-askar-shared": "^0.1.0-dev.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "rxjs": "^7.2.0", + "tsyringe": "^4.7.0" + }, + "devDependencies": { + "@hyperledger/aries-askar-nodejs": "^0.1.0-dev.1", + "reflect-metadata": "^0.1.13", + "rimraf": "^4.0.7", + "typescript": "~4.9.4" + } +} diff --git a/packages/askar/src/AskarModule.ts b/packages/askar/src/AskarModule.ts new file mode 100644 index 0000000000..5eccb13b3d --- /dev/null +++ b/packages/askar/src/AskarModule.ts @@ -0,0 +1,33 @@ +import type { DependencyManager, Module } from '@aries-framework/core' + +import { AriesFrameworkError, InjectionSymbols } from '@aries-framework/core' + +import { AskarStorageService } from './storage' +import { AskarWallet } from './wallet' + +export class AskarModule implements Module { + public register(dependencyManager: DependencyManager) { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + require('@hyperledger/aries-askar-nodejs') + } catch (error) { + try { + require('@hyperledger/aries-askar-react-native') + } catch (error) { + throw new Error('Could not load aries-askar bindings') + } + } + + if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { + throw new AriesFrameworkError('There is an instance of Wallet already registered') + } else { + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) + } + + if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { + throw new AriesFrameworkError('There is an instance of StorageService already registered') + } else { + dependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + } + } +} diff --git a/packages/askar/src/AskarModuleConfig.ts b/packages/askar/src/AskarModuleConfig.ts new file mode 100644 index 0000000000..c2104eff8e --- /dev/null +++ b/packages/askar/src/AskarModuleConfig.ts @@ -0,0 +1,44 @@ +import type { AriesAskar } from './types' + +/** + * AskarModuleConfigOptions defines the interface for the options of the AskarModuleConfig class. + */ +export interface AskarModuleConfigOptions { + /** + * Implementation of the Askar interface according to aries-askar JavaScript wrapper. + * + * + * ## Node.JS + * + * ```ts + * import { NodeJSAriesAskar } from 'aries-askar-nodejs' + * + * const askarModule = new AskarModule({ + * askar: new NodeJSAriesAskar() + * }) + * ``` + * + * ## React Native + * + * ```ts + * import { ReactNativeAriesAskar } from 'aries-askar-react-native' + * + * const askarModule = new AskarModule({ + * askar: new ReactNativeAriesAskar() + * }) + * ``` + */ + askar: AriesAskar +} + +export class AskarModuleConfig { + private options: AskarModuleConfigOptions + + public constructor(options: AskarModuleConfigOptions) { + this.options = options + } + + public get askar() { + return this.options.askar + } +} diff --git a/packages/askar/src/index.ts b/packages/askar/src/index.ts new file mode 100644 index 0000000000..d7afa60eab --- /dev/null +++ b/packages/askar/src/index.ts @@ -0,0 +1,9 @@ +// Wallet +export { AskarWallet } from './wallet' + +// Storage +export { AskarStorageService } from './storage' + +// Module +export { AskarModule } from './AskarModule' +export { AskarModuleConfig } from './AskarModuleConfig' diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts new file mode 100644 index 0000000000..e7c96399c2 --- /dev/null +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -0,0 +1,177 @@ +import type { BaseRecordConstructor, AgentContext, BaseRecord, Query, StorageService } from '@aries-framework/core' + +import { + RecordDuplicateError, + WalletError, + RecordNotFoundError, + injectable, + JsonTransformer, +} from '@aries-framework/core' +import { Scan } from '@hyperledger/aries-askar-shared' + +import { askarErrors, isAskarError } from '../utils/askarError' +import { assertAskarWallet } from '../utils/assertAskarWallet' + +import { askarQueryFromSearchQuery, recordToInstance, transformFromRecordTagValues } from './utils' + +@injectable() +export class AskarStorageService implements StorageService { + /** @inheritDoc */ + public async save(agentContext: AgentContext, record: T) { + assertAskarWallet(agentContext.wallet) + const session = agentContext.wallet.session + + const value = JsonTransformer.serialize(record) + const tags = transformFromRecordTagValues(record.getTags()) as Record + + try { + await session.insert({ category: record.type, name: record.id, value, tags }) + } catch (error) { + if (isAskarError(error) && error.code === askarErrors.Duplicate) { + throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) + } + + throw new WalletError('Error saving record', { cause: error }) + } + } + + /** @inheritDoc */ + public async update(agentContext: AgentContext, record: T): Promise { + assertAskarWallet(agentContext.wallet) + const session = agentContext.wallet.session + + const value = JsonTransformer.serialize(record) + const tags = transformFromRecordTagValues(record.getTags()) as Record + + try { + await session.replace({ category: record.type, name: record.id, value, tags }) + } catch (error) { + if (isAskarError(error) && error.code === askarErrors.NotFound) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + cause: error, + }) + } + + throw new WalletError('Error updating record', { cause: error }) + } + } + + /** @inheritDoc */ + public async delete(agentContext: AgentContext, record: T) { + assertAskarWallet(agentContext.wallet) + const session = agentContext.wallet.session + + try { + await session.remove({ category: record.type, name: record.id }) + } catch (error) { + if (isAskarError(error) && error.code === askarErrors.NotFound) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + cause: error, + }) + } + throw new WalletError('Error deleting record', { cause: error }) + } + } + + /** @inheritDoc */ + public async deleteById( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + id: string + ): Promise { + assertAskarWallet(agentContext.wallet) + const session = agentContext.wallet.session + + try { + await session.remove({ category: recordClass.type, name: id }) + } catch (error) { + if (isAskarError(error) && error.code === askarErrors.NotFound) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + cause: error, + }) + } + throw new WalletError('Error deleting record', { cause: error }) + } + } + + /** @inheritDoc */ + public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { + assertAskarWallet(agentContext.wallet) + const session = agentContext.wallet.session + + try { + const record = await session.fetch({ category: recordClass.type, name: id }) + if (!record) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + }) + } + return recordToInstance(record, recordClass) + } catch (error) { + if ( + isAskarError(error) && + (error.code === askarErrors.NotFound || + // FIXME: this is current output from askar wrapper but does not describe specifically a not found scenario + error.message === 'Received null pointer. The native library could not find the value.') + ) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + cause: error, + }) + } + throw new WalletError(`Error getting record`, { cause: error }) + } + } + + /** @inheritDoc */ + public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { + assertAskarWallet(agentContext.wallet) + const session = agentContext.wallet.session + + const records = await session.fetchAll({ category: recordClass.type }) + + const instances = [] + for (const record of records) { + instances.push(recordToInstance(record, recordClass)) + } + return instances + } + + /** @inheritDoc */ + public async findByQuery( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + query: Query + ): Promise { + assertAskarWallet(agentContext.wallet) + const store = agentContext.wallet.store + + const askarQuery = askarQueryFromSearchQuery(query) + + const scan = new Scan({ + category: recordClass.type, + store, + tagFilter: askarQuery, + }) + + const instances = [] + try { + const records = await scan.fetchAll() + for (const record of records) { + instances.push(recordToInstance(record, recordClass)) + } + return instances + } catch (error) { + if ( + isAskarError(error) && // FIXME: this is current output from askar wrapper but does not describe specifically a 0 length scenario + error.message === 'Received null pointer. The native library could not find the value.' + ) { + return instances + } + throw new WalletError(`Error executing query`, { cause: error }) + } + } +} diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts new file mode 100644 index 0000000000..1ba1bf329f --- /dev/null +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -0,0 +1,307 @@ +import type { AgentContext, TagsBase } from '@aries-framework/core' + +import { + TypedArrayEncoder, + SigningProviderRegistry, + RecordDuplicateError, + RecordNotFoundError, +} from '@aries-framework/core' +import { ariesAskar } from '@hyperledger/aries-askar-shared' + +import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { AskarWallet } from '../../wallet/AskarWallet' +import { AskarStorageService } from '../AskarStorageService' +import { askarQueryFromSearchQuery } from '../utils' + +describe('AskarStorageService', () => { + let wallet: AskarWallet + let storageService: AskarStorageService + let agentContext: AgentContext + + beforeEach(async () => { + const agentConfig = getAgentConfig('AskarStorageServiceTest') + + wallet = new AskarWallet(agentConfig.logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + agentContext = getAgentContext({ + wallet, + agentConfig, + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(agentConfig.walletConfig!) + storageService = new AskarStorageService() + }) + + afterEach(async () => { + await wallet.delete() + }) + + const insertRecord = async ({ id, tags }: { id?: string; tags?: TagsBase }) => { + const props = { + id, + foo: 'bar', + tags: tags ?? { myTag: 'foobar' }, + } + const record = new TestRecord(props) + await storageService.save(agentContext, record) + return record + } + + describe('tag transformation', () => { + it('should correctly transform tag values to string before storing', async () => { + const record = await insertRecord({ + id: 'test-id', + tags: { + someBoolean: true, + someOtherBoolean: false, + someStringValue: 'string', + anArrayValue: ['foo', 'bar'], + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: '1', + anotherStringNumberValue: '0', + }, + }) + + const retrieveRecord = await ariesAskar.sessionFetch({ + category: record.type, + name: record.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sessionHandle: wallet.session.handle!, + forUpdate: false, + }) + + expect(JSON.parse(retrieveRecord.getTags(0))).toEqual({ + someBoolean: '1', + someOtherBoolean: '0', + someStringValue: 'string', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', + }) + }) + + it('should correctly transform tag values from string after retrieving', async () => { + await ariesAskar.sessionUpdate({ + category: TestRecord.type, + name: 'some-id', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sessionHandle: wallet.session.handle!, + value: TypedArrayEncoder.fromString('{}'), + tags: { + someBoolean: '1', + someOtherBoolean: '0', + someStringValue: 'string', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', + }, + operation: 0, // EntryOperation.Insert + }) + + const record = await storageService.getById(agentContext, TestRecord, 'some-id') + + expect(record.getTags()).toEqual({ + someBoolean: true, + someOtherBoolean: false, + someStringValue: 'string', + anArrayValue: expect.arrayContaining(['bar', 'foo']), + someStringNumberValue: '1', + anotherStringNumberValue: '0', + }) + }) + }) + + describe('save()', () => { + it('should throw RecordDuplicateError if a record with the id already exists', async () => { + const record = await insertRecord({ id: 'test-id' }) + + return expect(() => storageService.save(agentContext, record)).rejects.toThrowError(RecordDuplicateError) + }) + + it('should save the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + const found = await storageService.getById(agentContext, TestRecord, 'test-id') + + expect(record).toEqual(found) + }) + }) + + describe('getById()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + return expect(() => storageService.getById(agentContext, TestRecord, 'does-not-exist')).rejects.toThrowError( + RecordNotFoundError + ) + }) + + it('should return the record by id', async () => { + const record = await insertRecord({ id: 'test-id' }) + const found = await storageService.getById(agentContext, TestRecord, 'test-id') + + expect(found).toEqual(record) + }) + }) + + describe('update()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + const record = new TestRecord({ + id: 'test-id', + foo: 'test', + tags: { some: 'tag' }, + }) + + return expect(() => storageService.update(agentContext, record)).rejects.toThrowError(RecordNotFoundError) + }) + + it('should update the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + + record.replaceTags({ ...record.getTags(), foo: 'bar' }) + record.foo = 'foobaz' + await storageService.update(agentContext, record) + + const retrievedRecord = await storageService.getById(agentContext, TestRecord, record.id) + expect(retrievedRecord).toEqual(record) + }) + }) + + describe('delete()', () => { + it('should throw RecordNotFoundError if the record does not exist', async () => { + const record = new TestRecord({ + id: 'test-id', + foo: 'test', + tags: { some: 'tag' }, + }) + + return expect(() => storageService.delete(agentContext, record)).rejects.toThrowError(RecordNotFoundError) + }) + + it('should delete the record', async () => { + const record = await insertRecord({ id: 'test-id' }) + await storageService.delete(agentContext, record) + + return expect(() => storageService.getById(agentContext, TestRecord, record.id)).rejects.toThrowError( + RecordNotFoundError + ) + }) + }) + + describe('getAll()', () => { + it('should retrieve all records', async () => { + const createdRecords = await Promise.all( + Array(5) + .fill(undefined) + .map((_, index) => insertRecord({ id: `record-${index}` })) + ) + + const records = await storageService.getAll(agentContext, TestRecord) + + expect(records).toEqual(expect.arrayContaining(createdRecords)) + }) + }) + + describe('findByQuery()', () => { + it('should retrieve all records that match the query', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foobar' } }) + const expectedRecord2 = await insertRecord({ tags: { myTag: 'foobar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { myTag: 'foobar' }) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) + }) + + it('finds records using $and statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo', anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $and: [{ myTag: 'foo' }, { anotherTag: 'bar' }], + }) + + expect(records.length).toBe(1) + expect(records[0]).toEqual(expectedRecord) + }) + + it('finds records using $or statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) + const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $or: [{ myTag: 'foo' }, { anotherTag: 'bar' }], + }) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) + }) + + it('finds records using $not statements', async () => { + const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) + const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) + await insertRecord({ tags: { myTag: 'notfoobar' } }) + + const records = await storageService.findByQuery(agentContext, TestRecord, { + $not: { myTag: 'notfoobar' }, + }) + + expect(records.length).toBe(2) + expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) + }) + + it('correctly transforms an advanced query into a valid WQL query', async () => { + const expectedQuery = { + $and: [ + { + $and: undefined, + $not: undefined, + $or: [ + { myTag: '1', $and: undefined, $or: undefined, $not: undefined }, + { myTag: '0', $and: undefined, $or: undefined, $not: undefined }, + ], + }, + { + $or: undefined, + $not: undefined, + $and: [ + { theNumber: 'n__0', $and: undefined, $or: undefined, $not: undefined }, + { theNumber: 'n__1', $and: undefined, $or: undefined, $not: undefined }, + ], + }, + ], + $or: [ + { + 'aValue:foo': '1', + 'aValue:bar': '1', + $and: undefined, + $or: undefined, + $not: undefined, + }, + ], + $not: { myTag: 'notfoobar', $and: undefined, $or: undefined, $not: undefined }, + } + + expect( + askarQueryFromSearchQuery({ + $and: [ + { + $or: [{ myTag: true }, { myTag: false }], + }, + { + $and: [{ theNumber: '0' }, { theNumber: '1' }], + }, + ], + $or: [ + { + aValue: ['foo', 'bar'], + }, + ], + $not: { myTag: 'notfoobar' }, + }) + ).toEqual(expectedQuery) + }) + }) +}) diff --git a/packages/askar/src/storage/index.ts b/packages/askar/src/storage/index.ts new file mode 100644 index 0000000000..ac0265f1ea --- /dev/null +++ b/packages/askar/src/storage/index.ts @@ -0,0 +1 @@ +export * from './AskarStorageService' diff --git a/packages/askar/src/storage/utils.ts b/packages/askar/src/storage/utils.ts new file mode 100644 index 0000000000..381bd98dd7 --- /dev/null +++ b/packages/askar/src/storage/utils.ts @@ -0,0 +1,110 @@ +import type { BaseRecord, BaseRecordConstructor, Query, TagsBase } from '@aries-framework/core' +import type { EntryObject } from '@hyperledger/aries-askar-shared' + +import { JsonTransformer } from '@aries-framework/core' + +export function recordToInstance(record: EntryObject, recordClass: BaseRecordConstructor): T { + const instance = JsonTransformer.deserialize(record.value as string, recordClass) + instance.id = record.name + + const tags = record.tags ? transformToRecordTagValues(record.tags) : {} + instance.replaceTags(tags) + + return instance +} + +export function transformToRecordTagValues(tags: Record): TagsBase { + const transformedTags: TagsBase = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is a boolean string ('1' or '0') + // use the boolean val + if (value === '1' && key?.includes(':')) { + const [tagName, tagValue] = key.split(':') + + const transformedValue = transformedTags[tagName] + + if (Array.isArray(transformedValue)) { + transformedTags[tagName] = [...transformedValue, tagValue] + } else { + transformedTags[tagName] = [tagValue] + } + } + // Transform '1' and '0' to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = value === '1' + } + // If 1 or 0 is prefixed with 'n__' we need to remove it. This is to prevent + // casting the value to a boolean + else if (value === 'n__1' || value === 'n__0') { + transformedTags[key] = value === 'n__1' ? '1' : '0' + } + // Otherwise just use the value + else { + transformedTags[key] = value as string + } + } + + return transformedTags +} + +export function transformFromRecordTagValues(tags: TagsBase): { [key: string]: string | undefined } { + const transformedTags: { [key: string]: string | undefined } = {} + + for (const [key, value] of Object.entries(tags)) { + // If the value is of type null we use the value undefined + // Askar doesn't support null as a value + if (value === null) { + transformedTags[key] = undefined + } + // If the value is a boolean use the Askar + // '1' or '0' syntax + else if (typeof value === 'boolean') { + transformedTags[key] = value ? '1' : '0' + } + // If the value is 1 or 0, we need to add something to the value, otherwise + // the next time we deserialize the tag values it will be converted to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = `n__${value}` + } + // If the value is an array we create a tag for each array + // item ("tagName:arrayItem" = "1") + else if (Array.isArray(value)) { + value.forEach((item) => { + const tagName = `${key}:${item}` + transformedTags[tagName] = '1' + }) + } + // Otherwise just use the value + else { + transformedTags[key] = value + } + } + + return transformedTags +} + +/** + * Transforms the search query into a wallet query compatible with Askar WQL. + * + * The format used by AFJ is almost the same as the WQL query, with the exception of + * the encoding of values, however this is handled by the {@link AskarStorageServiceUtil.transformToRecordTagValues} + * method. + */ +export function askarQueryFromSearchQuery(query: Query): Record { + // eslint-disable-next-line prefer-const + let { $and, $or, $not, ...tags } = query + + $and = ($and as Query[] | undefined)?.map((q) => askarQueryFromSearchQuery(q)) + $or = ($or as Query[] | undefined)?.map((q) => askarQueryFromSearchQuery(q)) + $not = $not ? askarQueryFromSearchQuery($not as Query) : undefined + + const askarQuery = { + ...transformFromRecordTagValues(tags as unknown as TagsBase), + $and, + $or, + $not, + } + + return askarQuery +} diff --git a/packages/askar/src/types.ts b/packages/askar/src/types.ts new file mode 100644 index 0000000000..bc0baa2947 --- /dev/null +++ b/packages/askar/src/types.ts @@ -0,0 +1,3 @@ +import type { AriesAskar } from '@hyperledger/aries-askar-shared' + +export type { AriesAskar } diff --git a/packages/askar/src/utils/askarError.ts b/packages/askar/src/utils/askarError.ts new file mode 100644 index 0000000000..2cfcbd90cf --- /dev/null +++ b/packages/askar/src/utils/askarError.ts @@ -0,0 +1,16 @@ +import { AriesAskarError } from '@hyperledger/aries-askar-shared' + +export enum askarErrors { + Success = 0, + Backend = 1, + Busy = 2, + Duplicate = 3, + Encryption = 4, + Input = 5, + NotFound = 6, + Unexpected = 7, + Unsupported = 8, + Custom = 100, +} + +export const isAskarError = (error: Error) => error instanceof AriesAskarError diff --git a/packages/askar/src/utils/askarKeyTypes.ts b/packages/askar/src/utils/askarKeyTypes.ts new file mode 100644 index 0000000000..bb837f962e --- /dev/null +++ b/packages/askar/src/utils/askarKeyTypes.ts @@ -0,0 +1,6 @@ +import type { KeyType } from '@aries-framework/core' + +import { KeyAlgs } from '@hyperledger/aries-askar-shared' + +export const keyTypeSupportedByAskar = (keyType: KeyType) => + Object.entries(KeyAlgs).find(([, value]) => value === keyType.toString()) !== undefined diff --git a/packages/askar/src/utils/askarWalletConfig.ts b/packages/askar/src/utils/askarWalletConfig.ts new file mode 100644 index 0000000000..dcf1d15ab1 --- /dev/null +++ b/packages/askar/src/utils/askarWalletConfig.ts @@ -0,0 +1,76 @@ +import type { AskarWalletPostgresStorageConfig } from '../wallet/AskarWalletPostgresStorageConfig' +import type { WalletConfig } from '@aries-framework/core' + +import { KeyDerivationMethod, WalletError } from '@aries-framework/core' +import { StoreKeyMethod } from '@hyperledger/aries-askar-shared' + +export const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod?: KeyDerivationMethod) => { + if (!keyDerivationMethod) { + return undefined + } + + const correspondanceTable = { + [KeyDerivationMethod.Raw]: StoreKeyMethod.Raw, + [KeyDerivationMethod.Argon2IInt]: `${StoreKeyMethod.Kdf}:argon2i:int`, + [KeyDerivationMethod.Argon2IMod]: `${StoreKeyMethod.Kdf}:argon2i:mod`, + } + + return correspondanceTable[keyDerivationMethod] as StoreKeyMethod +} + +export const uriFromWalletConfig = (walletConfig: WalletConfig, basePath: string): { uri: string; path?: string } => { + let uri = '' + let path + + // By default use sqlite as database backend + if (!walletConfig.storage) { + walletConfig.storage = { type: 'sqlite' } + } + + if (walletConfig.storage.type === 'sqlite') { + if (walletConfig.storage.inMemory) { + uri = 'sqlite://:memory:' + } else { + path = `${(walletConfig.storage.path as string) ?? basePath + '/wallet'}/${walletConfig.id}/sqlite.db` + uri = `sqlite://${path}` + } + } else if (walletConfig.storage.type === 'postgres') { + const storageConfig = walletConfig.storage as unknown as AskarWalletPostgresStorageConfig + + if (!storageConfig.config || !storageConfig.credentials) { + throw new WalletError('Invalid storage configuration for postgres wallet') + } + + const urlParams = [] + if (storageConfig.config.connectTimeout !== undefined) { + urlParams.push(`connect_timeout=${encodeURIComponent(storageConfig.config.connectTimeout)}`) + } + if (storageConfig.config.idleTimeout !== undefined) { + urlParams.push(`idle_timeout=${encodeURIComponent(storageConfig.config.idleTimeout)}`) + } + if (storageConfig.config.maxConnections !== undefined) { + urlParams.push(`max_connections=${encodeURIComponent(storageConfig.config.maxConnections)}`) + } + if (storageConfig.config.minConnections !== undefined) { + urlParams.push(`min_connections=${encodeURIComponent(storageConfig.config.minConnections)}`) + } + if (storageConfig.credentials.adminAccount !== undefined) { + urlParams.push(`admin_account=${encodeURIComponent(storageConfig.credentials.adminAccount)}`) + } + if (storageConfig.credentials.adminPassword !== undefined) { + urlParams.push(`admin_password=${encodeURIComponent(storageConfig.credentials.adminPassword)}`) + } + + uri = `postgres://${encodeURIComponent(storageConfig.credentials.account)}:${encodeURIComponent( + storageConfig.credentials.password + )}@${storageConfig.config.host}/${encodeURIComponent(walletConfig.id)}` + + if (urlParams.length > 0) { + uri = `${uri}?${urlParams.join('&')}` + } + } else { + throw new WalletError(`Storage type not supported: ${walletConfig.storage.type}`) + } + + return { uri, path } +} diff --git a/packages/askar/src/utils/assertAskarWallet.ts b/packages/askar/src/utils/assertAskarWallet.ts new file mode 100644 index 0000000000..37213e3d28 --- /dev/null +++ b/packages/askar/src/utils/assertAskarWallet.ts @@ -0,0 +1,13 @@ +import type { Wallet } from '@aries-framework/core' + +import { AriesFrameworkError } from '@aries-framework/core' + +import { AskarWallet } from '../wallet/AskarWallet' + +export function assertAskarWallet(wallet: Wallet): asserts wallet is AskarWallet { + if (!(wallet instanceof AskarWallet)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const walletClassName = (wallet as any).constructor?.name ?? 'unknown' + throw new AriesFrameworkError(`Expected wallet to be instance of AskarWallet, found ${walletClassName}`) + } +} diff --git a/packages/askar/src/utils/index.ts b/packages/askar/src/utils/index.ts new file mode 100644 index 0000000000..b9f658de82 --- /dev/null +++ b/packages/askar/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './askarError' +export * from './askarKeyTypes' +export * from './askarWalletConfig' diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts new file mode 100644 index 0000000000..432e50cdda --- /dev/null +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -0,0 +1,748 @@ +import type { + EncryptedMessage, + WalletConfig, + WalletCreateKeyOptions, + DidConfig, + DidInfo, + WalletSignOptions, + UnpackedMessageContext, + WalletVerifyOptions, + Wallet, + WalletExportImportConfig, + WalletConfigRekey, + KeyPair, + KeyDerivationMethod, +} from '@aries-framework/core' +import type { Session } from '@hyperledger/aries-askar-shared' + +import { + JsonTransformer, + RecordNotFoundError, + RecordDuplicateError, + WalletInvalidKeyError, + WalletDuplicateError, + JsonEncoder, + KeyType, + Buffer, + AriesFrameworkError, + Logger, + WalletError, + InjectionSymbols, + Key, + SigningProviderRegistry, + TypedArrayEncoder, + FileSystem, + WalletNotFoundError, +} from '@aries-framework/core' +// eslint-disable-next-line import/order +import { + StoreKeyMethod, + KeyAlgs, + CryptoBox, + Store, + Key as AskarKey, + keyAlgFromString, +} from '@hyperledger/aries-askar-shared' + +const isError = (error: unknown): error is Error => error instanceof Error + +import { inject, injectable } from 'tsyringe' + +import { encodeToBase58, decodeFromBase58 } from '../../../core/src/utils/base58' +import { + askarErrors, + isAskarError, + keyDerivationMethodToStoreKeyMethod, + keyTypeSupportedByAskar, + uriFromWalletConfig, +} from '../utils' + +import { JweEnvelope, JweRecipient } from './JweEnvelope' + +@injectable() +export class AskarWallet implements Wallet { + private walletConfig?: WalletConfig + private _session?: Session + + private _store?: Store + + private logger: Logger + private fileSystem: FileSystem + + private signingKeyProviderRegistry: SigningProviderRegistry + private publicDidInfo: DidInfo | undefined + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + @inject(InjectionSymbols.FileSystem) fileSystem: FileSystem, + signingKeyProviderRegistry: SigningProviderRegistry + ) { + this.logger = logger + this.fileSystem = fileSystem + this.signingKeyProviderRegistry = signingKeyProviderRegistry + } + + public get isProvisioned() { + return this.walletConfig !== undefined + } + + public get isInitialized() { + return this._store !== undefined + } + + public get publicDid() { + return this.publicDidInfo + } + + public get store() { + if (!this._store) { + throw new AriesFrameworkError( + 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' + ) + } + + return this._store + } + + public get session() { + if (!this._session) { + throw new AriesFrameworkError('No Wallet Session is opened') + } + + return this._session + } + + public get masterSecretId() { + if (!this.isInitialized || !(this.walletConfig?.id || this.walletConfig?.masterSecretId)) { + throw new AriesFrameworkError( + 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' + ) + } + + return this.walletConfig?.masterSecretId ?? this.walletConfig.id + } + + /** + * Dispose method is called when an agent context is disposed. + */ + public async dispose() { + if (this.isInitialized) { + await this.close() + } + } + + /** + * @throws {WalletDuplicateError} if the wallet already exists + * @throws {WalletError} if another error occurs + */ + public async create(walletConfig: WalletConfig): Promise { + await this.createAndOpen(walletConfig) + await this.close() + } + + /** + * @throws {WalletDuplicateError} if the wallet already exists + * @throws {WalletError} if another error occurs + */ + public async createAndOpen(walletConfig: WalletConfig): Promise { + this.logger.debug(`Creating wallet '${walletConfig.id}`) + + const askarWalletConfig = await this.getAskarWalletConfig(walletConfig) + try { + this._store = await Store.provision({ + recreate: false, + uri: askarWalletConfig.uri, + profile: askarWalletConfig.profile, + keyMethod: askarWalletConfig.keyMethod, + passKey: askarWalletConfig.passKey, + }) + this.walletConfig = walletConfig + this._session = await this._store.openSession() + + // TODO: Master Secret creation (now part of IndyCredx/AnonCreds) + } catch (error) { + // FIXME: Askar should throw a Duplicate error code, but is currently returning Encryption + // And if we provide the very same wallet key, it will open it without any error + if (isAskarError(error) && (error.code === askarErrors.Encryption || error.code === askarErrors.Duplicate)) { + const errorMessage = `Wallet '${walletConfig.id}' already exists` + this.logger.debug(errorMessage) + + throw new WalletDuplicateError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) + } + + const errorMessage = `Error creating wallet '${walletConfig.id}'` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + + this.logger.debug(`Successfully created wallet '${walletConfig.id}'`) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async open(walletConfig: WalletConfig): Promise { + await this._open(walletConfig) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async rotateKey(walletConfig: WalletConfigRekey): Promise { + if (!walletConfig.rekey) { + throw new WalletError('Wallet rekey undefined!. Please specify the new wallet key') + } + await this._open( + { + id: walletConfig.id, + key: walletConfig.key, + keyDerivationMethod: walletConfig.keyDerivationMethod, + }, + walletConfig.rekey, + walletConfig.rekeyDerivationMethod + ) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + private async _open( + walletConfig: WalletConfig, + rekey?: string, + rekeyDerivation?: KeyDerivationMethod + ): Promise { + if (this._store) { + throw new WalletError( + 'Wallet instance already opened. Close the currently opened wallet before re-opening the wallet' + ) + } + + const askarWalletConfig = await this.getAskarWalletConfig(walletConfig) + + try { + this._store = await Store.open({ + uri: askarWalletConfig.uri, + keyMethod: askarWalletConfig.keyMethod, + passKey: askarWalletConfig.passKey, + }) + + if (rekey) { + await this._store.rekey({ + passKey: rekey, + keyMethod: keyDerivationMethodToStoreKeyMethod(rekeyDerivation) ?? StoreKeyMethod.Raw, + }) + } + this._session = await this._store.openSession() + + this.walletConfig = walletConfig + } catch (error) { + if (isAskarError(error) && error.code === askarErrors.NotFound) { + const errorMessage = `Wallet '${walletConfig.id}' not found` + this.logger.debug(errorMessage) + + throw new WalletNotFoundError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) + } else if (isAskarError(error) && error.code === askarErrors.Encryption) { + const errorMessage = `Incorrect key for wallet '${walletConfig.id}'` + this.logger.debug(errorMessage) + throw new WalletInvalidKeyError(errorMessage, { + walletType: 'AskarWallet', + cause: error, + }) + } + throw new WalletError( + `Error opening wallet ${walletConfig.id}. ERROR CODE ${error.code} MESSAGE ${error.message}`, + { cause: error } + ) + } + + this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this._store.handle.handle}'`) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async delete(): Promise { + if (!this.walletConfig) { + throw new WalletError( + 'Can not delete wallet that does not have wallet config set. Make sure to call create wallet before deleting the wallet' + ) + } + + this.logger.info(`Deleting wallet '${this.walletConfig.id}'`) + + if (this._store) { + await this.close() + } + + try { + const { uri } = uriFromWalletConfig(this.walletConfig, this.fileSystem.basePath) + await Store.remove(uri) + } catch (error) { + const errorMessage = `Error deleting wallet '${this.walletConfig.id}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async export(exportConfig: WalletExportImportConfig) { + // TODO + throw new WalletError('AskarWallet Export not yet implemented') + } + + public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig) { + // TODO + throw new WalletError('AskarWallet Import not yet implemented') + } + + /** + * @throws {WalletError} if the wallet is already closed or another error occurs + */ + public async close(): Promise { + this.logger.debug(`Closing wallet ${this.walletConfig?.id}`) + if (!this._store) { + throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no handle.') + } + + try { + await this.session.close() + await this.store.close() + this._session = undefined + this._store = undefined + this.publicDidInfo = undefined + } catch (error) { + const errorMessage = `Error closing wallet': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async initPublicDid(didConfig: DidConfig) { + // Not implemented, as it does not work with legacy Ledger module + } + + /** + * Create a key with an optional seed and keyType. + * The keypair is also automatically stored in the wallet afterwards + * + * @param seed string The seed for creating a key + * @param keyType KeyType the type of key that should be created + * + * @returns a Key instance with a publicKeyBase58 + * + * @throws {WalletError} When an unsupported keytype is requested + * @throws {WalletError} When the key could not be created + */ + public async createKey({ seed, keyType }: WalletCreateKeyOptions): Promise { + try { + if (keyTypeSupportedByAskar(keyType)) { + const algorithm = keyAlgFromString(keyType) + + // Create key from seed + const key = seed ? AskarKey.fromSeed({ seed: Buffer.from(seed), algorithm }) : AskarKey.generate(algorithm) + + // Store key + await this.session.insertKey({ key, name: encodeToBase58(key.publicBytes) }) + return Key.fromPublicKey(key.publicBytes, keyType) + } else { + // Check if there is a signing key provider for the specified key type. + if (this.signingKeyProviderRegistry.hasProviderForKeyType(keyType)) { + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(keyType) + + const keyPair = await signingKeyProvider.createKeyPair({ seed }) + await this.storeKeyPair(keyPair) + return Key.fromPublicKeyBase58(keyPair.publicKeyBase58, keyType) + } + throw new WalletError(`Unsupported key type: '${keyType}'`) + } + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) + } + } + + /** + * sign a Buffer with an instance of a Key class + * + * @param data Buffer The data that needs to be signed + * @param key Key The key that is used to sign the data + * + * @returns A signature for the data + */ + public async sign({ data, key }: WalletSignOptions): Promise { + try { + if (keyTypeSupportedByAskar(key.keyType)) { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signing of multiple messages`) + } + + const keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) + + if (!keyEntry) { + throw new WalletError('Key entry not found') + } + + const signed = keyEntry.key.signMessage({ message: data as Buffer }) + + return Buffer.from(signed) + } else { + // Check if there is a signing key provider for the specified key type. + if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) + + const keyPair = await this.retrieveKeyPair(key.publicKeyBase58) + const signed = await signingKeyProvider.sign({ + data, + privateKeyBase58: keyPair.privateKeyBase58, + publicKeyBase58: key.publicKeyBase58, + }) + + return signed + } + throw new WalletError(`Unsupported keyType: ${key.keyType}`) + } + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error signing data with verkey ${key.publicKeyBase58}`, { cause: error }) + } + } + + /** + * Verify the signature with the data and the used key + * + * @param data Buffer The data that has to be confirmed to be signed + * @param key Key The key that was used in the signing process + * @param signature Buffer The signature that was created by the signing process + * + * @returns A boolean whether the signature was created with the supplied data and key + * + * @throws {WalletError} When it could not do the verification + * @throws {WalletError} When an unsupported keytype is used + */ + public async verify({ data, key, signature }: WalletVerifyOptions): Promise { + try { + if (keyTypeSupportedByAskar(key.keyType)) { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting verification of multiple messages`) + } + + const askarKey = AskarKey.fromPublicBytes({ + algorithm: keyAlgFromString(key.keyType), + publicKey: key.publicKey, + }) + return askarKey.verifySignature({ message: data as Buffer, signature }) + } else { + // Check if there is a signing key provider for the specified key type. + if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) + + const signed = await signingKeyProvider.verify({ + data, + signature, + publicKeyBase58: key.publicKeyBase58, + }) + + return signed + } + throw new WalletError(`Unsupported keyType: ${key.keyType}`) + } + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error verifying signature of data signed with verkey ${key.publicKeyBase58}`, { + cause: error, + }) + } + } + + /** + * Pack a message using DIDComm V1 algorithm + * + * @param payload message to send + * @param recipientKeys array containing recipient keys in base58 + * @param senderVerkey sender key in base58 + * @returns JWE Envelope to send + */ + public async pack( + payload: Record, + recipientKeys: string[], + senderVerkey?: string // in base58 + ): Promise { + const cek = AskarKey.generate(KeyAlgs.Chacha20C20P) + + const senderKey = senderVerkey ? await this.session.fetchKey({ name: senderVerkey }) : undefined + + const senderExchangeKey = senderKey ? senderKey.key.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined + + const recipients: JweRecipient[] = [] + + for (const recipientKey of recipientKeys) { + const targetExchangeKey = AskarKey.fromPublicBytes({ + publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, + algorithm: KeyAlgs.Ed25519, + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + if (senderVerkey && senderExchangeKey) { + const encryptedSender = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: Buffer.from(senderVerkey), + }) + const nonce = CryptoBox.randomNonce() + const encryptedCek = CryptoBox.cryptoBox({ + recipientKey: targetExchangeKey, + senderKey: senderExchangeKey, + message: cek.secretBytes, + nonce, + }) + + recipients.push( + new JweRecipient({ + encryptedKey: encryptedCek, + header: { + kid: recipientKey, + sender: TypedArrayEncoder.toBase64URL(encryptedSender), + iv: TypedArrayEncoder.toBase64URL(nonce), + }, + }) + ) + } else { + const encryptedCek = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: cek.secretBytes, + }) + recipients.push( + new JweRecipient({ + encryptedKey: encryptedCek, + header: { + kid: recipientKey, + }, + }) + ) + } + } + + const protectedJson = { + enc: 'xchacha20poly1305_ietf', + typ: 'JWM/1.0', + alg: senderVerkey ? 'Authcrypt' : 'Anoncrypt', + recipients: recipients.map((item) => JsonTransformer.toJSON(item)), + } + + const { ciphertext, tag, nonce } = cek.aeadEncrypt({ + message: Buffer.from(JSON.stringify(payload)), + aad: Buffer.from(JsonEncoder.toBase64URL(protectedJson)), + }).parts + + const envelope = new JweEnvelope({ + ciphertext: TypedArrayEncoder.toBase64URL(ciphertext), + iv: TypedArrayEncoder.toBase64URL(nonce), + protected: JsonEncoder.toBase64URL(protectedJson), + tag: TypedArrayEncoder.toBase64URL(tag), + }).toJson() + + return envelope as EncryptedMessage + } + + /** + * Unpacks a JWE Envelope coded using DIDComm V1 algorithm + * + * @param messagePackage JWE Envelope + * @returns UnpackedMessageContext with plain text message, sender key and recipient key + */ + public async unpack(messagePackage: EncryptedMessage): Promise { + const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + + const alg = protectedJson.alg + const isAuthcrypt = alg === 'Authcrypt' + + if (!isAuthcrypt && alg != 'Anoncrypt') { + throw new WalletError(`Unsupported pack algorithm: ${alg}`) + } + + const recipients = [] + + for (const recip of protectedJson.recipients) { + const kid = recip.header.kid + if (!kid) { + throw new WalletError('Blank recipient key') + } + const sender = recip.header.sender ? TypedArrayEncoder.fromBase64(recip.header.sender) : undefined + const iv = recip.header.iv ? TypedArrayEncoder.fromBase64(recip.header.iv) : undefined + if (sender && !iv) { + throw new WalletError('Missing IV') + } else if (!sender && iv) { + throw new WalletError('Unexpected IV') + } + recipients.push({ + kid, + sender, + iv, + encrypted_key: TypedArrayEncoder.fromBase64(recip.encrypted_key), + }) + } + + let payloadKey, senderKey, recipientKey + + for (const recipient of recipients) { + let recipientKeyEntry + try { + recipientKeyEntry = await this.session.fetchKey({ name: recipient.kid }) + } catch (error) { + // TODO: Currently Askar wrapper throws error when key is not found + // In this case we don't need to throw any error because we should + // try with other recipient keys + continue + } + if (recipientKeyEntry) { + const recip_x = recipientKeyEntry.key.convertkey({ algorithm: KeyAlgs.X25519 }) + recipientKey = recipient.kid + + if (recipient.sender && recipient.iv) { + senderKey = TypedArrayEncoder.toUtf8String( + CryptoBox.sealOpen({ + recipientKey: recip_x, + ciphertext: recipient.sender, + }) + ) + const sender_x = AskarKey.fromPublicBytes({ + algorithm: KeyAlgs.Ed25519, + publicKey: decodeFromBase58(senderKey), + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + payloadKey = CryptoBox.open({ + recipientKey: recip_x, + senderKey: sender_x, + message: recipient.encrypted_key, + nonce: recipient.iv, + }) + } + break + } + } + if (!payloadKey) { + throw new WalletError('No corresponding recipient key found') + } + + if (!senderKey && isAuthcrypt) { + throw new WalletError('Sender public key not provided for Authcrypt') + } + + const cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) + const message = cek.aeadDecrypt({ + ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext as any), + nonce: TypedArrayEncoder.fromBase64(messagePackage.iv as any), + tag: TypedArrayEncoder.fromBase64(messagePackage.tag as any), + aad: TypedArrayEncoder.fromString(messagePackage.protected), + }) + return { + plaintextMessage: JsonEncoder.fromBuffer(message), + senderKey, + recipientKey, + } + } + + public async generateNonce(): Promise { + try { + return TypedArrayEncoder.toUtf8String(CryptoBox.randomNonce()) + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError('Error generating nonce', { cause: error }) + } + } + + public async generateWalletKey() { + try { + return Store.generateRawKey() + } catch (error) { + throw new WalletError('Error generating wallet key', { cause: error }) + } + } + + private async getAskarWalletConfig(walletConfig: WalletConfig) { + const { uri, path } = uriFromWalletConfig(walletConfig, this.fileSystem.basePath) + + // Make sure path exists before creating the wallet + if (path) { + await this.fileSystem.createDirectory(path) + } + + return { + uri, + profile: walletConfig.id, + // FIXME: Default derivation method should be set somewhere in either agent config or some constants + keyMethod: keyDerivationMethodToStoreKeyMethod(walletConfig.keyDerivationMethod) ?? StoreKeyMethod.None, + passKey: walletConfig.key, + } + } + + private async retrieveKeyPair(publicKeyBase58: string): Promise { + try { + const entryObject = await this.session.fetch({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` }) + + if (entryObject?.value) { + return JsonEncoder.fromString(entryObject?.value as string) as KeyPair + } else { + throw new WalletError(`No content found for record with public key: ${publicKeyBase58}`) + } + } catch (error) { + if ( + isAskarError(error) && + (error.code === askarErrors.NotFound || + // FIXME: this is current output from askar wrapper but does not describe specifically a not found scenario + error.message === 'Received null pointer. The native library could not find the value.') + ) { + throw new RecordNotFoundError(`KeyPairRecord not found for public key: ${publicKeyBase58}.`, { + recordType: 'KeyPairRecord', + cause: error, + }) + } + throw new WalletError('Error retrieving KeyPair record', { cause: error }) + } + } + + private async storeKeyPair(keyPair: KeyPair): Promise { + try { + await this.session.insert({ + category: 'KeyPairRecord', + name: `key-${keyPair.publicKeyBase58}`, + value: JSON.stringify(keyPair), + tags: { + keyType: keyPair.keyType, + }, + }) + } catch (error) { + if (isAskarError(error) && error.code === askarErrors.Duplicate) { + throw new RecordDuplicateError(`Record already exists`, { recordType: 'KeyPairRecord' }) + } + throw new WalletError('Error saving KeyPair record', { cause: error }) + } + } +} diff --git a/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts b/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts new file mode 100644 index 0000000000..a9a9aab91f --- /dev/null +++ b/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts @@ -0,0 +1,22 @@ +import type { WalletStorageConfig } from '../../../core/src/types' + +export interface AskarWalletPostgresConfig { + host: string + connectTimeout?: number + idleTimeout?: number + maxConnections?: number + minConnections?: number +} + +export interface AskarWalletPostgresCredentials { + account: string + password: string + adminAccount?: string + adminPassword?: string +} + +export interface AskarWalletPostgresStorageConfig extends WalletStorageConfig { + type: 'postgres' + config: AskarWalletPostgresConfig + credentials: AskarWalletPostgresCredentials +} diff --git a/packages/askar/src/wallet/JweEnvelope.ts b/packages/askar/src/wallet/JweEnvelope.ts new file mode 100644 index 0000000000..ac4d791f89 --- /dev/null +++ b/packages/askar/src/wallet/JweEnvelope.ts @@ -0,0 +1,62 @@ +import { JsonTransformer, TypedArrayEncoder } from '@aries-framework/core' +import { Expose, Type } from 'class-transformer' + +export class JweRecipient { + @Expose({ name: 'encrypted_key' }) + public encryptedKey!: string + public header?: Record + + public constructor(options: { encryptedKey: Uint8Array; header?: Record }) { + if (options) { + this.encryptedKey = TypedArrayEncoder.toBase64URL(options.encryptedKey) + + this.header = options.header + } + } +} + +export interface JweEnvelopeOptions { + protected: string + unprotected?: string + recipients?: JweRecipient[] + ciphertext: string + iv: string + tag: string + aad?: string + header?: string[] + encryptedKey?: string +} + +export class JweEnvelope { + public protected!: string + public unprotected?: string + + @Type(() => JweRecipient) + public recipients?: JweRecipient[] + public ciphertext!: string + public iv!: string + public tag!: string + public aad?: string + public header?: string[] + + @Expose({ name: 'encrypted_key' }) + public encryptedKey?: string + + public constructor(options: JweEnvelopeOptions) { + if (options) { + this.protected = options.protected + this.unprotected = options.unprotected + this.recipients = options.recipients + this.ciphertext = options.ciphertext + this.iv = options.iv + this.tag = options.tag + this.aad = options.aad + this.header = options.header + this.encryptedKey = options.encryptedKey + } + } + + public toJson() { + return JsonTransformer.toJSON(this) + } +} diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts new file mode 100644 index 0000000000..15bbf174cd --- /dev/null +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -0,0 +1,252 @@ +import type { + SigningProvider, + WalletConfig, + CreateKeyPairOptions, + KeyPair, + SignOptions, + VerifyOptions, +} from '@aries-framework/core' + +import { + WalletError, + WalletDuplicateError, + WalletNotFoundError, + WalletInvalidKeyError, + KeyType, + SigningProviderRegistry, + TypedArrayEncoder, + KeyDerivationMethod, + Buffer, +} from '@aries-framework/core' +import { Store } from '@hyperledger/aries-askar-shared' + +import { encodeToBase58 } from '../../../../core/src/utils/base58' +import { agentDependencies } from '../../../../core/tests/helpers' +import testLogger from '../../../../core/tests/logger' +import { AskarWallet } from '../AskarWallet' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const walletConfig: WalletConfig = { + id: 'Wallet: AskarWalletTest', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describe('AskarWallet basic operations', () => { + let askarWallet: AskarWallet + + const seed = 'sample-seed' + const message = TypedArrayEncoder.fromString('sample-message') + + beforeEach(async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('Get the Master Secret', () => { + expect(askarWallet.masterSecretId).toEqual('Wallet: AskarWalletTest') + }) + + test('Get the wallet store', () => { + expect(askarWallet.store).toEqual(expect.any(Store)) + }) + + test('Generate Nonce', async () => { + await expect(askarWallet.generateNonce()).resolves.toEqual(expect.any(String)) + }) + + test('Create ed25519 keypair', async () => { + await expect( + askarWallet.createKey({ seed: '2103de41b4ae37e8e28586d84a342b67', keyType: KeyType.Ed25519 }) + ).resolves.toMatchObject({ + keyType: KeyType.Ed25519, + }) + }) + + test('Create x25519 keypair', async () => { + await expect(askarWallet.createKey({ seed, keyType: KeyType.X25519 })).resolves.toMatchObject({ + keyType: KeyType.X25519, + }) + }) + + describe.skip('Currently, all KeyTypes are supported by Askar natively', () => { + test('Fail to create a Bls12381g1g2 keypair', async () => { + await expect(askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 })).rejects.toThrowError(WalletError) + }) + }) + + test('Create a signature with a ed25519 keypair', async () => { + const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const signature = await askarWallet.sign({ + data: message, + key: ed25519Key, + }) + expect(signature.length).toStrictEqual(64) + }) + + test('Verify a signed message with a ed25519 publicKey', async () => { + const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const signature = await askarWallet.sign({ + data: message, + key: ed25519Key, + }) + await expect(askarWallet.verify({ key: ed25519Key, data: message, signature })).resolves.toStrictEqual(true) + }) + + test('masterSecretId is equal to wallet ID by default', async () => { + expect(askarWallet.masterSecretId).toEqual(walletConfig.id) + }) +}) + +describe.skip('Currently, all KeyTypes are supported by Askar natively', () => { + describe('AskarWallet with custom signing provider', () => { + let askarWallet: AskarWallet + + const seed = 'sample-seed' + const message = TypedArrayEncoder.fromString('sample-message') + + class DummySigningProvider implements SigningProvider { + public keyType: KeyType = KeyType.Bls12381g1g2 + + public async createKeyPair(options: CreateKeyPairOptions): Promise { + return { + publicKeyBase58: encodeToBase58(Buffer.from(options.seed || 'publicKeyBase58')), + privateKeyBase58: 'privateKeyBase58', + keyType: KeyType.Bls12381g1g2, + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async sign(options: SignOptions): Promise { + return new Buffer('signed') + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async verify(options: VerifyOptions): Promise { + return true + } + } + + beforeEach(async () => { + askarWallet = new AskarWallet( + testLogger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([new DummySigningProvider()]) + ) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('Create custom keypair and use it for signing', async () => { + const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) + + const signature = await askarWallet.sign({ + data: message, + key, + }) + + expect(signature).toBeInstanceOf(Buffer) + }) + + test('Create custom keypair and use it for verifying', async () => { + const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) + + const signature = await askarWallet.verify({ + data: message, + signature: new Buffer('signature'), + key, + }) + + expect(signature).toBeTruthy() + }) + + test('Attempt to create the same custom keypair twice', async () => { + await askarWallet.createKey({ seed: 'keybase58', keyType: KeyType.Bls12381g1g2 }) + + await expect(askarWallet.createKey({ seed: 'keybase58', keyType: KeyType.Bls12381g1g2 })).rejects.toThrow( + WalletError + ) + }) + }) +}) + +describe('AskarWallet management', () => { + let askarWallet: AskarWallet + + afterEach(async () => { + if (askarWallet) { + await askarWallet.delete() + } + }) + + test('Create', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + const anotherKey = Store.generateRawKey() + + // Create and open wallet + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Create', key: initialKey }) + + // Close and try to re-create it + await askarWallet.close() + await expect( + askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Create', key: anotherKey }) + ).rejects.toThrowError(WalletDuplicateError) + }) + + test('Open', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + const wrongKey = Store.generateRawKey() + + // Create and open wallet + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Open', key: initialKey }) + + // Close and try to re-opening it with a wrong key + await askarWallet.close() + await expect(askarWallet.open({ ...walletConfig, id: 'AskarWallet Open', key: wrongKey })).rejects.toThrowError( + WalletInvalidKeyError + ) + + // Try to open a non existent wallet + await expect( + askarWallet.open({ ...walletConfig, id: 'AskarWallet Open - Non existent', key: initialKey }) + ).rejects.toThrowError(WalletNotFoundError) + }) + + test('Rotate key', async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + + const initialKey = Store.generateRawKey() + await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey }) + + await askarWallet.close() + + const newKey = Store.generateRawKey() + await askarWallet.rotateKey({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey, rekey: newKey }) + + await askarWallet.close() + + await expect( + askarWallet.open({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey }) + ).rejects.toThrowError(WalletInvalidKeyError) + + await askarWallet.open({ ...walletConfig, id: 'AskarWallet Key Rotation', key: newKey }) + + await askarWallet.close() + }) +}) diff --git a/packages/askar/src/wallet/__tests__/packing.test.ts b/packages/askar/src/wallet/__tests__/packing.test.ts new file mode 100644 index 0000000000..2a27e18678 --- /dev/null +++ b/packages/askar/src/wallet/__tests__/packing.test.ts @@ -0,0 +1,52 @@ +import type { WalletConfig } from '@aries-framework/core' + +import { + JsonTransformer, + BasicMessage, + KeyType, + SigningProviderRegistry, + KeyDerivationMethod, +} from '@aries-framework/core' + +import { agentDependencies } from '../../../../core/tests/helpers' +import testLogger from '../../../../core/tests/logger' +import { AskarWallet } from '../AskarWallet' + +// use raw key derivation method to speed up wallet creating / opening / closing between tests +const walletConfig: WalletConfig = { + id: 'Askar Wallet Packing', + // generated using indy.generateWalletKey + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, +} + +describe('askarWallet packing', () => { + let askarWallet: AskarWallet + + beforeEach(async () => { + askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) + await askarWallet.createAndOpen(walletConfig) + }) + + afterEach(async () => { + await askarWallet.delete() + }) + + test('DIDComm V1 packing and unpacking', async () => { + // Create both sender and recipient keys + const senderKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + const recipientKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) + + const message = new BasicMessage({ content: 'hello' }) + + const encryptedMessage = await askarWallet.pack( + message.toJSON(), + [recipientKey.publicKeyBase58], + senderKey.publicKeyBase58 + ) + + const plainTextMessage = await askarWallet.unpack(encryptedMessage) + + expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) + }) +}) diff --git a/packages/askar/src/wallet/index.ts b/packages/askar/src/wallet/index.ts new file mode 100644 index 0000000000..8d569fdf4c --- /dev/null +++ b/packages/askar/src/wallet/index.ts @@ -0,0 +1,2 @@ +export { AskarWallet } from './AskarWallet' +export * from './AskarWalletPostgresStorageConfig' diff --git a/packages/askar/tests/askar-postgres.e2e.test.ts b/packages/askar/tests/askar-postgres.e2e.test.ts new file mode 100644 index 0000000000..dfbc6db600 --- /dev/null +++ b/packages/askar/tests/askar-postgres.e2e.test.ts @@ -0,0 +1,102 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { AskarWalletPostgresStorageConfig } from '../src/wallet' +import type { ConnectionRecord } from '@aries-framework/core' + +import { Agent, HandshakeProtocol } from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { waitForBasicMessage } from '../../core/tests/helpers' + +import { getPostgresAgentOptions } from './helpers' + +const storageConfig: AskarWalletPostgresStorageConfig = { + type: 'postgres', + config: { + host: 'localhost:5432', + }, + credentials: { + account: 'postgres', + password: 'postgres', + }, +} + +const alicePostgresAgentOptions = getPostgresAgentOptions('AgentsAlice', storageConfig, { + endpoints: ['rxjs:alice'], +}) +const bobPostgresAgentOptions = getPostgresAgentOptions('AgentsBob', storageConfig, { + endpoints: ['rxjs:bob'], +}) + +// FIXME: Re-include in tests when Askar NodeJS wrapper performance is improved +describe.skip('Askar Postgres agents', () => { + let aliceAgent: Agent + let bobAgent: Agent + let aliceConnection: ConnectionRecord + let bobConnection: ConnectionRecord + + afterAll(async () => { + if (bobAgent) { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + } + + if (aliceAgent) { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + } + }) + + test('make a connection between postgres agents', async () => { + const aliceMessages = new Subject() + const bobMessages = new Subject() + + const subjectMap = { + 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, + } + + aliceAgent = new Agent(alicePostgresAgentOptions) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + bobAgent = new Agent(bobPostgresAgentOptions) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + + const aliceBobOutOfBandRecord = await aliceAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const { connectionRecord: bobConnectionAtBobAlice } = await bobAgent.oob.receiveInvitation( + aliceBobOutOfBandRecord.outOfBandInvitation + ) + bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnectionAtBobAlice!.id) + + const [aliceConnectionAtAliceBob] = await aliceAgent.connections.findAllByOutOfBandId(aliceBobOutOfBandRecord.id) + aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionAtAliceBob!.id) + }) + + test('send a message to connection', async () => { + const message = 'hello, world' + await aliceAgent.basicMessages.sendMessage(aliceConnection.id, message) + + const basicMessage = await waitForBasicMessage(bobAgent, { + content: message, + }) + + expect(basicMessage.content).toBe(message) + }) + + test('can shutdown and re-initialize the same postgres agent', async () => { + expect(aliceAgent.isInitialized).toBe(true) + await aliceAgent.shutdown() + expect(aliceAgent.isInitialized).toBe(false) + await aliceAgent.initialize() + expect(aliceAgent.isInitialized).toBe(true) + }) +}) diff --git a/packages/askar/tests/helpers.ts b/packages/askar/tests/helpers.ts new file mode 100644 index 0000000000..17a521a1af --- /dev/null +++ b/packages/askar/tests/helpers.ts @@ -0,0 +1,49 @@ +import type { AskarWalletPostgresStorageConfig } from '../src/wallet' +import type { InitConfig } from '@aries-framework/core' + +import { LogLevel } from '@aries-framework/core' +import path from 'path' + +import { TestLogger } from '../../core/tests/logger' +import { agentDependencies } from '../../node/src' +import { AskarModule } from '../src/AskarModule' + +export const genesisPath = process.env.GENESIS_TXN_PATH + ? path.resolve(process.env.GENESIS_TXN_PATH) + : path.join(__dirname, '../../../../network/genesis/local-genesis.txn') + +export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' + +export function getPostgresAgentOptions( + name: string, + storageConfig: AskarWalletPostgresStorageConfig, + extraConfig: Partial = {} +) { + const config: InitConfig = { + label: `Agent: ${name}`, + walletConfig: { + id: `Wallet${name}`, + key: `Key${name}`, + storage: storageConfig, + }, + connectToIndyLedgersOnStartup: false, + publicDidSeed, + autoAcceptConnections: true, + autoUpdateStorageOnStartup: false, + indyLedgers: [ + { + id: `pool-${name}`, + indyNamespace: `pool:localtest`, + isProduction: false, + genesisPath, + }, + ], + logger: new TestLogger(LogLevel.off, name), + ...extraConfig, + } + return { + config, + dependencies: agentDependencies, + modules: { askar: new AskarModule() }, + } as const +} diff --git a/packages/askar/tests/setup.ts b/packages/askar/tests/setup.ts new file mode 100644 index 0000000000..a09e05318c --- /dev/null +++ b/packages/askar/tests/setup.ts @@ -0,0 +1,11 @@ +import 'reflect-metadata' + +try { + // eslint-disable-next-line import/no-extraneous-dependencies + require('@hyperledger/aries-askar-nodejs') +} catch (error) { + throw new Error('Could not load aries-askar bindings') +} + +// FIXME: Remove when Askar JS Wrapper performance issues are solved +jest.setTimeout(180000) diff --git a/packages/askar/tsconfig.build.json b/packages/askar/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/askar/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/askar/tsconfig.json b/packages/askar/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/askar/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index 2909c3536d..3785d00f4a 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -74,6 +74,9 @@ export class Agent extends BaseAge dependencyManager.registerInstance(InjectionSymbols.Stop$, new Subject()) dependencyManager.registerInstance(InjectionSymbols.FileSystem, new agentConfig.agentDependencies.FileSystem()) + // Register all modules. This will also include the default modules + dependencyManager.registerModules(modulesWithDefaultModules) + // Register possibly already defined services if (!dependencyManager.isRegistered(InjectionSymbols.Wallet)) { dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndyWallet) @@ -88,9 +91,6 @@ export class Agent extends BaseAge dependencyManager.registerSingleton(InjectionSymbols.MessageRepository, InMemoryMessageRepository) } - // Register all modules. This will also include the default modules - dependencyManager.registerModules(modulesWithDefaultModules) - // TODO: contextCorrelationId for base wallet // Bind the default agent context to the container for use in modules etc. dependencyManager.registerInstance( diff --git a/packages/core/src/storage/FileSystem.ts b/packages/core/src/storage/FileSystem.ts index 6673bc333c..b724e68158 100644 --- a/packages/core/src/storage/FileSystem.ts +++ b/packages/core/src/storage/FileSystem.ts @@ -2,6 +2,7 @@ export interface FileSystem { readonly basePath: string exists(path: string): Promise + createDirectory(path: string): Promise write(path: string, data: string): Promise read(path: string): Promise downloadToFile(url: string, path: string): Promise diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d2f5a21c8f..b454c7963e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -13,14 +13,16 @@ export enum KeyDerivationMethod { Raw = 'RAW', } +export interface WalletStorageConfig { + type: string + [key: string]: unknown +} + export interface WalletConfig { id: string key: string keyDerivationMethod?: KeyDerivationMethod - storage?: { - type: string - [key: string]: unknown - } + storage?: WalletStorageConfig masterSecretId?: string } diff --git a/packages/core/src/utils/TypedArrayEncoder.ts b/packages/core/src/utils/TypedArrayEncoder.ts index 685eac485c..83ee5d89ca 100644 --- a/packages/core/src/utils/TypedArrayEncoder.ts +++ b/packages/core/src/utils/TypedArrayEncoder.ts @@ -17,7 +17,7 @@ export class TypedArrayEncoder { * * @param buffer the buffer to encode into base64url string */ - public static toBase64URL(buffer: Buffer) { + public static toBase64URL(buffer: Buffer | Uint8Array) { return base64ToBase64URL(TypedArrayEncoder.toBase64(buffer)) } diff --git a/packages/node/src/NodeFileSystem.ts b/packages/node/src/NodeFileSystem.ts index f739c40814..240440d64c 100644 --- a/packages/node/src/NodeFileSystem.ts +++ b/packages/node/src/NodeFileSystem.ts @@ -29,6 +29,10 @@ export class NodeFileSystem implements FileSystem { } } + public async createDirectory(path: string): Promise { + await promises.mkdir(dirname(path), { recursive: true }) + } + public async write(path: string, data: string): Promise { // Make sure parent directories exist await promises.mkdir(dirname(path), { recursive: true }) diff --git a/packages/react-native/src/ReactNativeFileSystem.ts b/packages/react-native/src/ReactNativeFileSystem.ts index 331fa11a54..0eaab55429 100644 --- a/packages/react-native/src/ReactNativeFileSystem.ts +++ b/packages/react-native/src/ReactNativeFileSystem.ts @@ -21,6 +21,10 @@ export class ReactNativeFileSystem implements FileSystem { return RNFS.exists(path) } + public async createDirectory(path: string): Promise { + await RNFS.mkdir(getDirFromFilePath(path)) + } + public async write(path: string, data: string): Promise { // Make sure parent directories exist await RNFS.mkdir(getDirFromFilePath(path)) diff --git a/tests/e2e-askar-indy-sdk-wallet-subject.test.ts b/tests/e2e-askar-indy-sdk-wallet-subject.test.ts new file mode 100644 index 0000000000..b7d4233738 --- /dev/null +++ b/tests/e2e-askar-indy-sdk-wallet-subject.test.ts @@ -0,0 +1,135 @@ +import type { SubjectMessage } from './transport/SubjectInboundTransport' + +import { Subject } from 'rxjs' + +import { getAgentOptions, makeConnection, waitForBasicMessage } from '../packages/core/tests/helpers' + +import { AskarModule } from '@aries-framework/askar' +import { Agent, DependencyManager, InjectionSymbols } from '@aries-framework/core' +import { IndySdkModule, IndySdkStorageService, IndySdkWallet } from '@aries-framework/indy-sdk' + +import { SubjectInboundTransport } from './transport/SubjectInboundTransport' + +import { agentDependencies } from '@aries-framework/node' + +import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' + +// FIXME: Re-include in tests when Askar NodeJS wrapper performance is improved +describe.skip('E2E Askar-Indy SDK Wallet Subject tests', () => { + let recipientAgent: Agent + let senderAgent: Agent + + afterEach(async () => { + if (recipientAgent) { + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + } + + if (senderAgent) { + await senderAgent.shutdown() + await senderAgent.wallet.delete() + } + }) + + test('Wallet Subject flow - Indy Sender / Askar Receiver ', async () => { + // Sender is an Agent using Indy SDK Wallet + const senderDependencyManager = new DependencyManager() + senderDependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) + senderDependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) + senderAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Sender Indy', { endpoints: ['rxjs:sender'] }), + modules: { indySdk: new IndySdkModule({ indySdk: agentDependencies.indy }) }, + }, + senderDependencyManager + ) + + // Recipient is an Agent using Askar Wallet + recipientAgent = new Agent({ + ...getAgentOptions('E2E Wallet Subject Recipient Askar', { endpoints: ['rxjs:recipient'] }), + modules: { askar: new AskarModule() }, + }) + + await e2eWalletTest(senderAgent, recipientAgent) + }) + + test('Wallet Subject flow - Askar Sender / Askar Recipient ', async () => { + // Sender is an Agent using Askar Wallet + senderAgent = new Agent({ + ...getAgentOptions('E2E Wallet Subject Sender Askar', { endpoints: ['rxjs:sender'] }), + modules: { askar: new AskarModule() }, + }) + + // Recipient is an Agent using Askar Wallet + recipientAgent = new Agent({ + ...getAgentOptions('E2E Wallet Subject Recipient Askar', { endpoints: ['rxjs:recipient'] }), + modules: { askar: new AskarModule() }, + }) + + await e2eWalletTest(senderAgent, recipientAgent) + }) + + test('Wallet Subject flow - Indy Sender / Indy Recipient ', async () => { + // Sender is an Agent using Indy SDK Wallet + const senderDependencyManager = new DependencyManager() + senderDependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) + senderDependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) + senderAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Sender Indy', { endpoints: ['rxjs:sender'] }), + modules: { indySdk: new IndySdkModule({ indySdk: agentDependencies.indy }) }, + }, + senderDependencyManager + ) + + // Recipient is an Agent using Indy Wallet + const recipientDependencyManager = new DependencyManager() + recipientDependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) + recipientDependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) + recipientAgent = new Agent( + { + ...getAgentOptions('E2E Wallet Subject Recipient Indy', { endpoints: ['rxjs:recipient'] }), + modules: { indySdk: new IndySdkModule({ indySdk: agentDependencies.indy }) }, + }, + recipientDependencyManager + ) + + await e2eWalletTest(senderAgent, recipientAgent) + }) +}) + +export async function e2eWalletTest(senderAgent: Agent, recipientAgent: Agent) { + const recipientMessages = new Subject() + const senderMessages = new Subject() + + const subjectMap = { + 'rxjs:recipient': recipientMessages, + 'rxjs:sender': senderMessages, + } + + // Recipient Setup + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) + await recipientAgent.initialize() + + // Sender Setup + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) + await senderAgent.initialize() + + // Make connection between sender and recipient + const [recipientSenderConnection, senderRecipientConnection] = await makeConnection(recipientAgent, senderAgent) + expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) + + // Sender sends a basic message and Recipient waits for it + await senderAgent.basicMessages.sendMessage(senderRecipientConnection.id, 'Hello') + await waitForBasicMessage(recipientAgent, { + content: 'Hello', + }) + + // Recipient sends a basic message and Sender waits for it + await recipientAgent.basicMessages.sendMessage(recipientSenderConnection.id, 'How are you?') + await waitForBasicMessage(senderAgent, { + content: 'How are you?', + }) +} diff --git a/yarn.lock b/yarn.lock index 3fe3c222e0..4a8447c5fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -858,6 +858,26 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@hyperledger/aries-askar-nodejs@^0.1.0-dev.1": + version "0.1.0-dev.1" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.1.0-dev.1.tgz#b384d422de48f0ce5918e1612d2ca32ebd160520" + integrity sha512-XrRskQ0PaNAerItvfxKkS8YaVg+iuImguoqfyQ4ZSaePKZQnTqZpkxo6faKS+GlsaubRXz/6yz3YndVRIxPO+w== + dependencies: + "@hyperledger/aries-askar-shared" "0.1.0-dev.1" + "@mapbox/node-pre-gyp" "^1.0.10" + ffi-napi "^4.0.3" + node-cache "^5.1.2" + ref-array-di "^1.2.2" + ref-napi "^3.0.3" + ref-struct-di "^1.1.1" + +"@hyperledger/aries-askar-shared@0.1.0-dev.1", "@hyperledger/aries-askar-shared@^0.1.0-dev.1": + version "0.1.0-dev.1" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-shared/-/aries-askar-shared-0.1.0-dev.1.tgz#4e4e494c3a44c7c82f7b95ad4f06149f2a3a9b6c" + integrity sha512-Pt525M6CvnE3N6jxMpSqLy7RpOsc4oqa2Q+hc2UdCHuSYwmM/aeqt6wiA5dpghvl8g/78lCi1Dz74pzp7Dmm3w== + dependencies: + fast-text-encoding "^1.0.3" + "@hyperledger/indy-vdr-nodejs@^0.1.0-dev.4": version "0.1.0-dev.4" resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.1.0-dev.4.tgz#b5d2090b30c4a51e4e4f15a024054aada0d3550e" @@ -3917,6 +3937,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clone@2.x: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -8392,6 +8417,13 @@ node-addon-api@^3.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-cache@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-dir@^0.1.17: version "0.1.17" resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" From d056316712b5ee5c42a159816b5dda0b05ad84a8 Mon Sep 17 00:00:00 2001 From: Victor Anene <62852943+Vickysomtee@users.noreply.github.com> Date: Fri, 10 Feb 2023 01:31:43 +0100 Subject: [PATCH 14/20] feat(indy-vdr): add IndyVdrAnonCredsRegistry (#1270) Signed-off-by: Timo Glastra --- packages/indy-sdk/tests/setup.ts | 2 +- packages/indy-vdr/package.json | 1 + .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 431 ++++++++++++++++++ .../utils/_tests_/identifier.test.ts | 52 +++ .../src/anoncreds/utils/identifiers.ts | 42 ++ .../indy-vdr-anoncreds-registry.e2e.test.ts | 190 ++++++++ .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 168 ++++--- 7 files changed, 800 insertions(+), 86 deletions(-) create mode 100644 packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts create mode 100644 packages/indy-vdr/src/anoncreds/utils/_tests_/identifier.test.ts create mode 100644 packages/indy-vdr/src/anoncreds/utils/identifiers.ts create mode 100644 packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts diff --git a/packages/indy-sdk/tests/setup.ts b/packages/indy-sdk/tests/setup.ts index 719a473b6e..b60b932be5 100644 --- a/packages/indy-sdk/tests/setup.ts +++ b/packages/indy-sdk/tests/setup.ts @@ -1 +1 @@ -jest.setTimeout(10000) +jest.setTimeout(25000) diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index e12d0116de..e73cfd7a83 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -25,6 +25,7 @@ "test": "jest" }, "dependencies": { + "@aries-framework/anoncreds": "0.3.3", "@aries-framework/core": "0.3.3", "@hyperledger/indy-vdr-shared": "^0.1.0-dev.4" }, diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts new file mode 100644 index 0000000000..1106b498cd --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -0,0 +1,431 @@ +import type { + AnonCredsRegistry, + GetCredentialDefinitionReturn, + GetSchemaReturn, + RegisterSchemaOptions, + RegisterCredentialDefinitionOptions, + RegisterSchemaReturn, + RegisterCredentialDefinitionReturn, + GetRevocationStatusListReturn, + GetRevocationRegistryDefinitionReturn, +} from '@aries-framework/anoncreds' +import type { AgentContext } from '@aries-framework/core' + +import { DidsApi, getKeyDidMappingByVerificationMethod } from '@aries-framework/core' +import { + GetSchemaRequest, + SchemaRequest, + GetCredentialDefinitionRequest, + CredentialDefinitionRequest, +} from '@hyperledger/indy-vdr-shared' + +import { IndyVdrPoolService } from '../pool' + +import { + didFromSchemaId, + didFromCredentialDefinitionId, + getLegacySchemaId, + getLegacyCredentialDefinitionId, + indyVdrAnonCredsRegistryIdentifierRegex, +} from './utils/identifiers' + +export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { + public readonly supportedIdentifier = indyVdrAnonCredsRegistryIdentifierRegex + + public async getSchema(agentContext: AgentContext, schemaId: string): Promise { + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const did = didFromSchemaId(schemaId) + + const pool = await indyVdrPoolService.getPoolForDid(agentContext, did) + + agentContext.config.logger.debug(`Getting schema '${schemaId}' from ledger '${pool.indyNamespace}'`) + const request = new GetSchemaRequest({ submitterDid: did, schemaId }) + + agentContext.config.logger.trace( + `Submitting get schema request for schema '${schemaId}' to ledger '${pool.indyNamespace}'` + ) + const response = await pool.submitReadRequest(request) + + agentContext.config.logger.trace(`Got un-parsed schema '${schemaId}' from ledger '${pool.indyNamespace}'`, { + response, + }) + + const issuerId = didFromSchemaId(schemaId) + + if ('attr_names' in response.result.data) { + return { + schema: { + attrNames: response.result.data.attr_names, + name: response.result.data.name, + version: response.result.data.version, + issuerId, + }, + schemaId: schemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: pool.indyNamespace, + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo: response.result.seqNo, + }, + } + } + + agentContext.config.logger.error(`Error retrieving schema '${schemaId}'`) + + return { + schemaId, + resolutionMetadata: { + error: 'notFound', + message: `unable to find schema with id ${schemaId}`, + }, + schemaMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error(`Error retrieving schema '${schemaId}'`, { + error, + schemaId, + }) + + return { + schemaId, + resolutionMetadata: { + error: 'notFound', + }, + schemaMetadata: {}, + } + } + } + + public async registerSchema( + agentContext: AgentContext, + options: IndyVdrRegisterSchemaOptions + ): Promise { + if (!options.options.didIndyNamespace) { + return { + schemaMetadata: {}, + registrationMetadata: {}, + schemaState: { + reason: 'no didIndyNamespace defined in the options. didIndyNamespace is required when using the Indy VDR', + schema: options.schema, + state: 'failed', + }, + } + } + + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const schemaRequest = new SchemaRequest({ + submitterDid: options.schema.issuerId, + schema: { + id: getLegacySchemaId(options.schema.issuerId, options.schema.name, options.schema.version), + name: options.schema.name, + ver: '1.0', + version: options.schema.version, + attrNames: options.schema.attrNames, + }, + }) + + const pool = indyVdrPoolService.getPoolForNamespace(options.options.didIndyNamespace) + + // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did + // from the ledger to know which key is associated with the did + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didResult = await didsApi.resolve(`did:sov:${options.schema.issuerId}`) + + if (!didResult.didDocument) { + return { + schemaMetadata: {}, + registrationMetadata: {}, + schemaState: { + schema: options.schema, + state: 'failed', + reason: `didNotFound: unable to resolve did did:sov:${options.schema.issuerId}: ${didResult.didResolutionMetadata.message}`, + }, + } + } + + const verificationMethod = didResult.didDocument.dereferenceKey(`did:sov:${options.schema.issuerId}#key-1`) + const { getKeyFromVerificationMethod } = getKeyDidMappingByVerificationMethod(verificationMethod) + const key = getKeyFromVerificationMethod(verificationMethod) + + const response = await pool.submitWriteRequest(agentContext, schemaRequest, key) + + return { + schemaState: { + state: 'finished', + schema: { + attrNames: options.schema.attrNames, + issuerId: options.schema.issuerId, + name: options.schema.name, + version: options.schema.version, + }, + schemaId: getLegacySchemaId(options.schema.issuerId, options.schema.name, options.schema.version), + }, + registrationMetadata: {}, + schemaMetadata: { + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo: response.result.txnMetadata.seqNo, + didIndyNamespace: pool.indyNamespace, + }, + } + } catch (error) { + agentContext.config.logger.error(`Error registering schema for did '${options.schema.issuerId}'`, { + error, + did: options.schema.issuerId, + schema: options.schema, + }) + + return { + schemaMetadata: {}, + registrationMetadata: {}, + schemaState: { + state: 'failed', + schema: options.schema, + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async getCredentialDefinition( + agentContext: AgentContext, + credentialDefinitionId: string + ): Promise { + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const did = didFromCredentialDefinitionId(credentialDefinitionId) + + const pool = await indyVdrPoolService.getPoolForDid(agentContext, did) + + agentContext.config.logger.debug( + `Getting credential definition '${credentialDefinitionId}' from ledger '${pool.indyNamespace}'` + ) + + const request = new GetCredentialDefinitionRequest({ + submitterDid: did, + credentialDefinitionId, + }) + + agentContext.config.logger.trace( + `Submitting get credential definition request for credential definition '${credentialDefinitionId}' to ledger '${pool.indyNamespace}'` + ) + + const response = await pool.submitReadRequest(request) + + if (response.result.data) { + return { + credentialDefinitionId: credentialDefinitionId, + credentialDefinition: { + issuerId: didFromCredentialDefinitionId(credentialDefinitionId), + schemaId: response.result.ref.toString(), + tag: response.result.tag, + type: 'CL', + value: response.result.data, + }, + credentialDefinitionMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + resolutionMetadata: {}, + } + } + + agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`) + + return { + credentialDefinitionId, + credentialDefinitionMetadata: {}, + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve credential definition with id ${credentialDefinitionId}`, + }, + } + } catch (error) { + agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`, { + error, + credentialDefinitionId, + }) + + return { + credentialDefinitionId, + credentialDefinitionMetadata: {}, + resolutionMetadata: { + error: 'notFound', + message: `unable to resolve credential definition: ${error.message}`, + }, + } + } + } + + public async registerCredentialDefinition( + agentContext: AgentContext, + options: IndyVdrRegisterCredentialDefinitionOptions + ): Promise { + // Make sure didIndyNamespace is passed + if (!options.options.didIndyNamespace) { + return { + credentialDefinitionMetadata: {}, + registrationMetadata: {}, + credentialDefinitionState: { + reason: 'no didIndyNamespace defined in the options. didIndyNamespace is required when using the Indy SDK', + credentialDefinition: options.credentialDefinition, + state: 'failed', + }, + } + } + + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = indyVdrPoolService.getPoolForNamespace(options.options.didIndyNamespace) + + const { schema, schemaMetadata, resolutionMetadata } = await this.getSchema( + agentContext, + options.credentialDefinition.schemaId + ) + + if (!schema || !schemaMetadata.indyLedgerSeqNo || typeof schemaMetadata.indyLedgerSeqNo !== 'number') { + return { + registrationMetadata: {}, + credentialDefinitionMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + credentialDefinitionState: { + credentialDefinition: options.credentialDefinition, + state: 'failed', + reason: `error resolving schema with id ${options.credentialDefinition.schemaId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, + }, + } + } + + const credentialDefinitionId = getLegacyCredentialDefinitionId( + options.credentialDefinition.issuerId, + schemaMetadata.indyLedgerSeqNo, + options.credentialDefinition.tag + ) + + const credentialDefinitionRequest = new CredentialDefinitionRequest({ + submitterDid: options.credentialDefinition.issuerId, + credentialDefinition: { + ver: '1.0', + id: credentialDefinitionId, + schemaId: `${schemaMetadata.indyLedgerSeqNo}`, + type: 'CL', + tag: options.credentialDefinition.tag, + value: { + primary: options.credentialDefinition.value, + }, + }, + }) + + // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did + // from the ledger to know which key is associated with the did + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didResult = await didsApi.resolve(`did:sov:${options.credentialDefinition.issuerId}`) + + if (!didResult.didDocument) { + return { + credentialDefinitionMetadata: {}, + registrationMetadata: {}, + credentialDefinitionState: { + credentialDefinition: options.credentialDefinition, + state: 'failed', + reason: `didNotFound: unable to resolve did did:sov${options.credentialDefinition.issuerId}: ${didResult.didResolutionMetadata.message}`, + }, + } + } + + const verificationMethod = didResult.didDocument.dereferenceKey( + `did:sov:${options.credentialDefinition.issuerId}#key-1` + ) + const { getKeyFromVerificationMethod } = getKeyDidMappingByVerificationMethod(verificationMethod) + const key = getKeyFromVerificationMethod(verificationMethod) + + const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, key) + + agentContext.config.logger.debug( + `Registered credential definition '${credentialDefinitionId}' on ledger '${pool.indyNamespace}'`, + { + response, + credentialDefinition: options.credentialDefinition, + } + ) + + return { + credentialDefinitionMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + credentialDefinitionState: { + credentialDefinition: options.credentialDefinition, + credentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering credential definition for schema '${options.credentialDefinition.schemaId}'`, + { + error, + did: options.credentialDefinition.issuerId, + credentialDefinition: options.credentialDefinition, + } + ) + + return { + credentialDefinitionMetadata: {}, + registrationMetadata: {}, + credentialDefinitionState: { + credentialDefinition: options.credentialDefinition, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async getRevocationStatusList( + agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number + ): Promise { + return { + resolutionMetadata: { + error: 'Not Implemented', + message: `Revocation list not yet implemented `, + }, + revocationStatusListMetadata: {}, + } + } + + public async getRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ): Promise { + return { + resolutionMetadata: { + error: 'Not Implemented', + message: `Revocation registry definition not yet implemented`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + } +} + +export interface IndyVdrRegisterSchemaOptions extends RegisterSchemaOptions { + options: { + didIndyNamespace: string + } +} + +export interface IndyVdrRegisterCredentialDefinitionOptions extends RegisterCredentialDefinitionOptions { + options: { + didIndyNamespace: string + } +} diff --git a/packages/indy-vdr/src/anoncreds/utils/_tests_/identifier.test.ts b/packages/indy-vdr/src/anoncreds/utils/_tests_/identifier.test.ts new file mode 100644 index 0000000000..62528a0075 --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/utils/_tests_/identifier.test.ts @@ -0,0 +1,52 @@ +import { + getLegacySchemaId, + getLegacyCredentialDefinitionId, + didFromSchemaId, + didFromCredentialDefinitionId, + indyVdrAnonCredsRegistryIdentifierRegex, +} from '../identifiers' + +describe('identifiers', () => { + it('matches against a legacy indy did, schema id, credential definition id and revocation registry id', () => { + const did = '7Tqg6BwSSWapxgUDm9KKgg' + const schemaId = 'BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0' + const credentialDefinitionId = 'N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID' + const revocationRegistryId = + 'N7baRMcyvPwWc8v85CtZ6e:4:N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID:CL_ACCUM:1-1024' + + const anotherId = 'some:id' + + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(did)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(schemaId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(credentialDefinitionId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(revocationRegistryId)).toEqual(true) + expect(indyVdrAnonCredsRegistryIdentifierRegex.test(anotherId)).toEqual(false) + }) + + it('getLegacySchemaId should return a valid schema Id', () => { + const did = '29347' + const name = 'starlinks' + const version = '321' + + expect(getLegacySchemaId(did, name, version)).toEqual(`29347:2:starlinks:321`) + }) + + it('getLegacyCredentialDefinition should return a valid Credential Id', () => { + const did = '15565' + const seqNo = 323 + const tag = 'indyTag' + expect(getLegacyCredentialDefinitionId(did, seqNo, tag)).toEqual('15565:3:CL:323:indyTag') + }) + + it('didFromSchemaId should return the valid did from the schema', () => { + const schemaId = '29347:2:starlinks:321' + + expect(didFromSchemaId(schemaId)).toEqual('29347') + }) + + it('didFromCredentialId should return the valid did from the schema', () => { + const credentialDefinitionId = '15565:3:CL:323:indyTag' + + expect(didFromCredentialDefinitionId(credentialDefinitionId)).toEqual('15565') + }) +}) diff --git a/packages/indy-vdr/src/anoncreds/utils/identifiers.ts b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts new file mode 100644 index 0000000000..d242ca3461 --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts @@ -0,0 +1,42 @@ +export const legacyIndyVdrIssuerIdRegex = /^[a-zA-Z0-9]{21,22}$/ +export const legacyIndyVdrSchemaIdRegex = /^[a-zA-Z0-9]{21,22}:2:.+:[0-9.]+$/ +export const legacyIndyVdrCredentialDefinitionIdRegex = + /^[a-zA-Z0-9]{21,22}:3:CL:(([1-9][0-9]*)|([a-zA-Z0-9]{21,22}:2:.+:[0-9.]+)):(.+)?$/ +export const legacyIndyVdrRevocationRegistryIdRegex = + /^[a-zA-Z0-9]{21,22}:4:[a-zA-Z0-9]{21,22}:3:CL:(([1-9][0-9]*)|([a-zA-Z0-9]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)/ + +export const indyVdrAnonCredsRegistryIdentifierRegex = new RegExp( + `${legacyIndyVdrIssuerIdRegex.source}|${legacyIndyVdrSchemaIdRegex.source}|${legacyIndyVdrCredentialDefinitionIdRegex.source}|${legacyIndyVdrRevocationRegistryIdRegex.source}` +) + +export function getLegacySchemaId(unqualifiedDid: string, name: string, version: string) { + return `${unqualifiedDid}:2:${name}:${version}` +} + +export function getLegacyCredentialDefinitionId(unqualifiedDid: string, seqNo: number, tag: string) { + return `${unqualifiedDid}:3:CL:${seqNo}:${tag}` +} + +/** + * Extract did from schema id + */ +export function didFromSchemaId(schemaId: string) { + const [did] = schemaId.split(':') + + return did +} + +/** + * Extract did from credential definition id + */ +export function didFromCredentialDefinitionId(credentialDefinitionId: string) { + const [did] = credentialDefinitionId.split(':') + + return did +} + +export function didFromRevocationRegistryDefinitionId(revocationRegistryId: string) { + const [did] = revocationRegistryId.split(':') + + return did +} diff --git a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts new file mode 100644 index 0000000000..9d214ea43d --- /dev/null +++ b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts @@ -0,0 +1,190 @@ +import { Agent } from '@aries-framework/core' + +import { agentDependencies, genesisTransactions, getAgentConfig } from '../../core/tests/helpers' +import { IndyVdrAnonCredsRegistry } from '../src/anoncreds/IndyVdrAnonCredsRegistry' +import { IndyVdrPoolService } from '../src/pool' + +const agentConfig = getAgentConfig('IndyVdrAnonCredsRegistry') + +// TODO: update to module once available +const indyVdrPoolService = new IndyVdrPoolService(agentConfig.logger) +indyVdrPoolService.setPools([ + { + genesisTransactions, + indyNamespace: 'local:test', + isProduction: false, + transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, + }, +]) + +const indyVdrAnonCredsRegistry = new IndyVdrAnonCredsRegistry() + +const agent = new Agent({ + config: agentConfig, + dependencies: agentDependencies, +}) + +agent.dependencyManager.registerInstance(IndyVdrPoolService, indyVdrPoolService) + +describe('IndyVdrAnonCredsRegistry', () => { + beforeAll(async () => { + await agent.initialize() + await indyVdrPoolService.connectToPools() + }) + + afterAll(async () => { + for (const pool of indyVdrPoolService.pools) { + pool.close() + } + + await agent.shutdown() + await agent.wallet.delete() + }) + + // One test as the credential definition depends on the schema + test('register and resolve a schema and credential definition', async () => { + const dynamicVersion = `1.${Math.random() * 100}` + + const schemaResult = await indyVdrAnonCredsRegistry.registerSchema(agent.context, { + options: { + didIndyNamespace: 'local:test', + }, + schema: { + attrNames: ['age'], + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', + name: 'test', + version: dynamicVersion, + }, + }) + + expect(schemaResult).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['age'], + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', + name: 'test', + version: dynamicVersion, + }, + schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}`, + }, + registrationMetadata: {}, + schemaMetadata: { + indyLedgerSeqNo: expect.any(Number), + didIndyNamespace: 'local:test', + }, + }) + + const schemaResponse = await indyVdrAnonCredsRegistry.getSchema( + agent.context, + schemaResult.schemaState.schemaId as string + ) + expect(schemaResponse).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', + }, + schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}`, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'local:test', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + credentialDefinition: { + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', + tag: 'TAG', + schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}`, + type: 'CL', + value: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + r: { + age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, + options: { + didIndyNamespace: 'local:test', + }, + }) + + expect(credentialDefinitionResult).toMatchObject({ + credentialDefinitionMetadata: { + didIndyNamespace: 'local:test', + }, + credentialDefinitionState: { + credentialDefinition: { + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', + tag: 'TAG', + schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}`, + type: 'CL', + value: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + r: { + age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, + credentialDefinitionId: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.schemaMetadata.indyLedgerSeqNo}:TAG`, + state: 'finished', + }, + registrationMetadata: {}, + }) + + const credentialDefinitionResponse = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId as string + ) + + expect(credentialDefinitionResponse).toMatchObject({ + credentialDefinitionId: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.schemaMetadata.indyLedgerSeqNo}:TAG`, + credentialDefinition: { + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', + // FIXME: this will change when https://github.com/hyperledger/aries-framework-javascript/issues/1259 is merged + schemaId: `${schemaResponse.schemaMetadata.indyLedgerSeqNo}`, + tag: 'TAG', + type: 'CL', + value: { + primary: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + r: { + age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'local:test', + }, + resolutionMetadata: {}, + }) + }) +}) diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index 52bd467cd5..95a3882ff1 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -1,12 +1,13 @@ import type { Key } from '@aries-framework/core' -import { IndyWallet, KeyType, SigningProviderRegistry, TypedArrayEncoder } from '@aries-framework/core' +import { IndyWallet, KeyType, SigningProviderRegistry } from '@aries-framework/core' import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from '@hyperledger/indy-vdr-shared' import { agentDependencies, genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' import { IndyVdrPool } from '../src/pool' import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' +import { indyDidFromPublicKeyBase58 } from '../src/utils/did' const indyVdrPoolService = new IndyVdrPoolService(testLogger) const wallet = new IndyWallet(agentDependencies, testLogger, new SigningProviderRegistry([])) @@ -88,8 +89,7 @@ describe('IndyVdrPoolService', () => { // prepare the DID we are going to write to the ledger const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const buffer = TypedArrayEncoder.fromBase58(key.publicKeyBase58) - const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) const request = new NymRequest({ dest: did, @@ -116,108 +116,106 @@ describe('IndyVdrPoolService', () => { }) }) - describe('Schemas & credential Definition', () => { - test('can write a schema using the pool', async () => { - const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') + test('can write a schema and credential definition using the pool', async () => { + const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') - const dynamicVersion = `1.${Math.random() * 100}` + const dynamicVersion = `1.${Math.random() * 100}` - const schemaRequest = new SchemaRequest({ - submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', - schema: { - id: 'test-schema-id', - name: 'test-schema', - ver: '1.0', - version: dynamicVersion, - attrNames: ['first_name', 'last_name', 'age'], - }, - }) + const schemaRequest = new SchemaRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + schema: { + id: 'test-schema-id', + name: 'test-schema', + ver: '1.0', + version: dynamicVersion, + attrNames: ['first_name', 'last_name', 'age'], + }, + }) - const schemaResponse = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) + const schemaResponse = await pool.submitWriteRequest(agentContext, schemaRequest, signerKey) - expect(schemaResponse).toMatchObject({ - op: 'REPLY', - result: { - ver: '1', - txn: { - metadata: expect.any(Object), - type: '101', + expect(schemaResponse).toMatchObject({ + op: 'REPLY', + result: { + ver: '1', + txn: { + metadata: expect.any(Object), + type: '101', + data: { data: { - data: { - attr_names: expect.arrayContaining(['age', 'last_name', 'first_name']), - name: 'test-schema', - version: dynamicVersion, - }, + attr_names: expect.arrayContaining(['age', 'last_name', 'first_name']), + name: 'test-schema', + version: dynamicVersion, }, }, }, - }) + }, + }) - const credentialDefinitionRequest = new CredentialDefinitionRequest({ - submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', - credentialDefinition: { - ver: '1.0', - id: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.result.txnMetadata.seqNo}:TAG`, - // must be string version of the schema seqNo - schemaId: `${schemaResponse.result.txnMetadata.seqNo}`, - type: 'CL', - tag: 'TAG', - value: { - primary: { - n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', - s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', - r: { - master_secret: - '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', - last_name: - '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', - first_name: - '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', - age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', - }, - rctxt: - '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', - z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', + const credentialDefinitionRequest = new CredentialDefinitionRequest({ + submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + credentialDefinition: { + ver: '1.0', + id: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.result.txnMetadata.seqNo}:TAG`, + // must be string version of the schema seqNo + schemaId: `${schemaResponse.result.txnMetadata.seqNo}`, + type: 'CL', + tag: 'TAG', + value: { + primary: { + n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', + s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', + r: { + master_secret: + '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', + last_name: + '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', + first_name: + '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', + age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', }, + rctxt: + '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', + z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', }, }, - }) + }, + }) - const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, signerKey) + const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, signerKey) - expect(response).toMatchObject({ - op: 'REPLY', - result: { - ver: '1', - txn: { - metadata: expect.any(Object), - type: '102', + expect(response).toMatchObject({ + op: 'REPLY', + result: { + ver: '1', + txn: { + metadata: expect.any(Object), + type: '102', + data: { data: { - data: { - primary: { - r: { - last_name: - '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', - first_name: - '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', - age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', - master_secret: - '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', - }, - z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', - rctxt: - '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', - n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', - s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', + primary: { + r: { + last_name: + '35864556460959997092903171610228165251001245539613587319116151716453114432309327039517115215674024166920383445379522674504803469517283236033110568676156285676664363558333716898161685255450536856645604857714925836474250821415182026707218622134953915013803750771185050002646661004119778318524426368842019753903741998256374803456282688037624993010626333853831264356355867746685055670790915539230702546586615988121383960277550317876816983602795121749533628953449405383896799464872758725899520173321672584180060465965090049734285011738428381648013150818429882628144544132356242262467090140003979917439514443707537952643217', + first_name: + '26405366527417391838431479783966663952336302347775179063968690502492620867161212873635806190080000833725932174641667734138216137047349915190546601368424742647800764149890590518336588437317392528514313749533980651547425554257026971104775208127915118918084350210726664749850578299247705298976657301433446491575776774836993110356033664644761593799921221474617858131678955318702706530853801195330271860527250931569815553226145458665481867408279941785848264018364216087471931232367137301987457054918438087686484522112532447779498424748261678616461026788516567300969886029412198319909977473167405879110243445062391837349387', + age: '19865805272519696320755573045337531955436490760876870776207490804137339344112305203631892390827288264857621916650098902064979838987400911652887344763586495880167030031364467726355103327059673023946234460960685398768709062405377107912774045508870580108596597470880834205563197111550140867466625683117333370595295321833757429488192170551320637065066368716366317421169802474954914904380304190861641082310805418122837214965865969459724848071006870574514215255412289237027267424055400593307112849859757094597401668252862525566316402695830217450073667487951799749275437192883439584518905943435472478496028380016245355151988', + master_secret: + '51468326064458249697956272807708948542001661888325200180968238787091473418947480867518174106588127385097619219536294589148765074804124925845579871788369264160902401097166484002617399484700234182426993061977152961670486891123188739266793651668791365808983166555735631354925174224786218771453042042304773095663181121735652667614424198057134974727791329623974680096491276337756445057223988781749506082654194307092164895251308088903000573135447235553684949564809677864522417041639512933806794232354223826262154508950271949764583849083972967642587778197779127063591201123312548182885603427440981731822883101260509710567731', }, + z: '57056568014385132434061065334124327103768023932445648883765905576432733866307137325457775876741578717650388638737098805750938053855430851133826479968450532729423746605371536096355616166421996729493639634413002114547787617999178137950004782677177313856876420539744625174205603354705595789330008560775613287118432593300023801651460885523314713996258581986238928077688246511704050386525431448517516821261983193275502089060128363906909778842476516981025598807378338053788433033754999771876361716562378445777250912525673660842724168260417083076824975992327559199634032439358787956784395443246565622469187082767614421691234', + rctxt: + '17146114573198643698878017247599007910707723139165264508694101989891626297408755744139587708989465136799243292477223763665064840330721616213638280284119891715514951989022398510785960099708705561761504012512387129498731093386014964896897751536856287377064154297370092339714578039195258061017640952790913108285519632654466006255438773382930416822756630391947263044087385305540191237328903426888518439803354213792647775798033294505898635058814132665832000734168261793545453678083703704122695006541391598116359796491845268631009298069826949515604008666680160398698425061157356267086946953480945396595351944425658076127674', + n: '95671911213029889766246243339609567053285242961853979532076192834533577534909796042025401129640348836502648821408485216223269830089771714177855160978214805993386076928594836829216646288195127289421136294309746871614765411402917891972999085287429566166932354413679994469616357622976775651506242447852304853465380257226445481515631782793575184420720296120464167257703633829902427169144462981949944348928086406211627174233811365419264314148304536534528344413738913277713548403058098093453580992173145127632199215550027527631259565822872315784889212327945030315062879193999012349220118290071491899498795367403447663354833', + s: '1573939820553851804028472930351082111827449763317396231059458630252708273163050576299697385049087601314071156646675105028237105229428440185022593174121924731226634356276616495327358864865629675802738680754755949997611920669823449540027707876555408118172529688443208301403297680159171306000341239398135896274940688268460793682007115152428685521865921925309154307574955324973580144009271977076586453011938089159885164705002797196738438392179082905738155386545935208094240038135576042886730802817809757582039362798495805441520744154270346780731494125065136433163757697326955962282840631850597919384092584727207908978907', }, - signature_type: 'CL', - ref: schemaResponse.result.txnMetadata.seqNo, - tag: 'TAG', }, + signature_type: 'CL', + ref: schemaResponse.result.txnMetadata.seqNo, + tag: 'TAG', }, }, - }) + }, }) }) }) From 86cb9d088693182a2a08f26645b00204bd7d2adc Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 10 Feb 2023 11:13:44 -0300 Subject: [PATCH 15/20] ci: increase maximum heap memory for node (#1280) Signed-off-by: Ariel Gentile --- .github/workflows/continuous-integration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6890536c12..44820700fe 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,6 +12,7 @@ env: TEST_AGENT_PUBLIC_DID_SEED: 000000000000000000000000Trustee9 GENESIS_TXN_PATH: network/genesis/local-genesis.txn LIB_INDY_STRG_POSTGRES: /home/runner/work/aries-framework-javascript/indy-sdk/experimental/plugins/postgres_storage/target/release # for Linux + NODE_OPTIONS: --max_old_space_size=4096 # Make sure we're not running multiple release steps at the same time as this can give issues with determining the next npm version to release. # Ideally we only add this to the 'release' job so it doesn't limit PR runs, but github can't guarantee the job order in that case: From 1d487b1a7e11b3f18b5229ba580bd035a7f564a0 Mon Sep 17 00:00:00 2001 From: Jim Ezesinachi Date: Fri, 10 Feb 2023 20:21:20 +0100 Subject: [PATCH 16/20] feat: added endpoint setter to agent InitConfig (#1278) Signed-off-by: Jim Ezesinachi --- packages/core/src/agent/AgentConfig.ts | 10 ++++++++-- .../core/src/agent/__tests__/AgentConfig.test.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agent/AgentConfig.ts b/packages/core/src/agent/AgentConfig.ts index 28ad67488a..a2df97a94c 100644 --- a/packages/core/src/agent/AgentConfig.ts +++ b/packages/core/src/agent/AgentConfig.ts @@ -11,12 +11,14 @@ import { DidCommMimeType } from '../types' export class AgentConfig { private initConfig: InitConfig + private _endpoints: string[] | undefined public label: string public logger: Logger public readonly agentDependencies: AgentDependencies public constructor(initConfig: InitConfig, agentDependencies: AgentDependencies) { this.initConfig = initConfig + this._endpoints = initConfig.endpoints this.label = initConfig.label this.logger = initConfig.logger ?? new ConsoleLogger(LogLevel.off) this.agentDependencies = agentDependencies @@ -134,11 +136,15 @@ export class AgentConfig { public get endpoints(): [string, ...string[]] { // if endpoints is not set, return queue endpoint // https://github.com/hyperledger/aries-rfcs/issues/405#issuecomment-582612875 - if (!this.initConfig.endpoints || this.initConfig.endpoints.length === 0) { + if (!this._endpoints || this._endpoints.length === 0) { return [DID_COMM_TRANSPORT_QUEUE] } - return this.initConfig.endpoints as [string, ...string[]] + return this._endpoints as [string, ...string[]] + } + + public set endpoints(endpoints: string[]) { + this._endpoints = endpoints } /** diff --git a/packages/core/src/agent/__tests__/AgentConfig.test.ts b/packages/core/src/agent/__tests__/AgentConfig.test.ts index 559a9880a3..43549b1e87 100644 --- a/packages/core/src/agent/__tests__/AgentConfig.test.ts +++ b/packages/core/src/agent/__tests__/AgentConfig.test.ts @@ -18,6 +18,18 @@ describe('AgentConfig', () => { expect(agentConfig.endpoints).toStrictEqual(['didcomm:transport/queue']) }) + + it('should return the new config endpoint after setter is called', () => { + const endpoint = 'https://local-url.com' + const newEndpoint = 'https://new-local-url.com' + + const agentConfig = getAgentConfig('AgentConfig Test', { + endpoints: [endpoint], + }) + + agentConfig.endpoints = [newEndpoint] + expect(agentConfig.endpoints).toEqual([newEndpoint]) + }) }) describe('label', () => { From 2669d7dd3d7c0ddfd1108dfd65e6115dd3418500 Mon Sep 17 00:00:00 2001 From: KolbyRKunz Date: Fri, 10 Feb 2023 14:14:59 -0700 Subject: [PATCH 17/20] fix: set updateAt on records when updating a record (#1272) Signed-off-by: KolbyRKunz --- .../askar/src/storage/AskarStorageService.ts | 4 + .../__tests__/AskarStorageService.test.ts | 12 +++ packages/core/jest.config.ts | 2 + .../core/src/storage/IndyStorageService.ts | 4 + .../__tests__/IndyStorageService.test.ts | 21 +++++ .../__tests__/__snapshots__/0.1.test.ts.snap | 82 +++++++++++++++++++ .../__tests__/__snapshots__/0.2.test.ts.snap | 11 +++ .../__tests__/__snapshots__/0.3.test.ts.snap | 9 ++ .../__snapshots__/backup.test.ts.snap | 4 + tests/InMemoryStorageService.ts | 2 + 10 files changed, 151 insertions(+) diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index e7c96399c2..cdf537745d 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -21,6 +21,8 @@ export class AskarStorageService implements StorageService assertAskarWallet(agentContext.wallet) const session = agentContext.wallet.session + record.updatedAt = new Date() + const value = JsonTransformer.serialize(record) const tags = transformFromRecordTagValues(record.getTags()) as Record @@ -40,6 +42,8 @@ export class AskarStorageService implements StorageService assertAskarWallet(agentContext.wallet) const session = agentContext.wallet.session + record.updatedAt = new Date() + const value = JsonTransformer.serialize(record) const tags = transformFromRecordTagValues(record.getTags()) as Record diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts index 1ba1bf329f..2208cde944 100644 --- a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -14,6 +14,8 @@ import { AskarWallet } from '../../wallet/AskarWallet' import { AskarStorageService } from '../AskarStorageService' import { askarQueryFromSearchQuery } from '../utils' +const startDate = Date.now() + describe('AskarStorageService', () => { let wallet: AskarWallet let storageService: AskarStorageService @@ -127,6 +129,11 @@ describe('AskarStorageService', () => { expect(record).toEqual(found) }) + + it('updatedAt should have a new value after a save', async () => { + const record = await insertRecord({ id: 'test-id' }) + expect(record.updatedAt?.getTime()).toBeGreaterThan(startDate) + }) }) describe('getById()', () => { @@ -165,6 +172,11 @@ describe('AskarStorageService', () => { const retrievedRecord = await storageService.getById(agentContext, TestRecord, record.id) expect(retrievedRecord).toEqual(record) }) + + it('updatedAt should have a new value after an update', async () => { + const record = await insertRecord({ id: 'test-id' }) + expect(record.updatedAt?.getTime()).toBeGreaterThan(startDate) + }) }) describe('delete()', () => { diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts index 55c67d70a6..22e2708f18 100644 --- a/packages/core/jest.config.ts +++ b/packages/core/jest.config.ts @@ -4,6 +4,8 @@ import base from '../../jest.config.base' import packageJson from './package.json' +process.env.TZ = 'GMT' + const config: Config.InitialOptions = { ...base, name: packageJson.name, diff --git a/packages/core/src/storage/IndyStorageService.ts b/packages/core/src/storage/IndyStorageService.ts index 452ef555c1..bd71d1701f 100644 --- a/packages/core/src/storage/IndyStorageService.ts +++ b/packages/core/src/storage/IndyStorageService.ts @@ -138,6 +138,8 @@ export class IndyStorageService> implements public async save(agentContext: AgentContext, record: T) { assertIndyWallet(agentContext.wallet) + record.updatedAt = new Date() + const value = JsonTransformer.serialize(record) const tags = this.transformFromRecordTagValues(record.getTags()) as Record @@ -157,6 +159,8 @@ export class IndyStorageService> implements public async update(agentContext: AgentContext, record: T): Promise { assertIndyWallet(agentContext.wallet) + record.updatedAt = new Date() + const value = JsonTransformer.serialize(record) const tags = this.transformFromRecordTagValues(record.getTags()) as Record diff --git a/packages/core/src/storage/__tests__/IndyStorageService.test.ts b/packages/core/src/storage/__tests__/IndyStorageService.test.ts index bd61553b08..b517641408 100644 --- a/packages/core/src/storage/__tests__/IndyStorageService.test.ts +++ b/packages/core/src/storage/__tests__/IndyStorageService.test.ts @@ -5,11 +5,14 @@ import type * as Indy from 'indy-sdk' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../tests/helpers' import { SigningProviderRegistry } from '../../crypto/signing-provider' import { RecordDuplicateError, RecordNotFoundError } from '../../error' +import { sleep } from '../../utils/sleep' import { IndyWallet } from '../../wallet/IndyWallet' import { IndyStorageService } from '../IndyStorageService' import { TestRecord } from './TestRecord' +const startDate = Date.now() + describe('IndyStorageService', () => { let wallet: IndyWallet let indy: typeof Indy @@ -113,6 +116,12 @@ describe('IndyStorageService', () => { expect(record).toEqual(found) }) + + it('After a save the record should have update the updatedAt property', async () => { + const time = startDate + const record = await insertRecord({ id: 'test-updatedAt' }) + expect(record.updatedAt?.getTime()).toBeGreaterThan(time) + }) }) describe('getById()', () => { @@ -151,6 +160,18 @@ describe('IndyStorageService', () => { const retrievedRecord = await storageService.getById(agentContext, TestRecord, record.id) expect(retrievedRecord).toEqual(record) }) + + it('After a record has been updated it should have updated the updatedAT property', async () => { + const time = startDate + const record = await insertRecord({ id: 'test-id' }) + + record.replaceTags({ ...record.getTags(), foo: 'bar' }) + record.foo = 'foobaz' + await storageService.update(agentContext, record) + + const retrievedRecord = await storageService.getById(agentContext, TestRecord, record.id) + expect(retrievedRecord.createdAt.getTime()).toBeGreaterThan(time) + }) }) describe('delete()', () => { diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap index 241611490a..8fe31caafb 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap @@ -56,6 +56,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "10-4e4f-41d9-94c4-f49351b811f1": Object { @@ -112,6 +113,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "11-4e4f-41d9-94c4-f49351b811f1": Object { @@ -151,6 +153,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "12-4e4f-41d9-94c4-f49351b811f1": Object { @@ -191,6 +194,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "2-4e4f-41d9-94c4-f49351b811f1": Object { @@ -230,6 +234,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "3-4e4f-41d9-94c4-f49351b811f1": Object { @@ -270,6 +275,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "4-4e4f-41d9-94c4-f49351b811f1": Object { @@ -326,6 +332,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "5-4e4f-41d9-94c4-f49351b811f1": Object { @@ -365,6 +372,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a": Object { @@ -410,6 +418,7 @@ Object { "protocolVersion": "v1", "state": "done", "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a": Object { @@ -471,6 +480,7 @@ Object { "protocolVersion": "v1", "state": "done", "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "6-4e4f-41d9-94c4-f49351b811f1": Object { @@ -511,6 +521,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7-4e4f-41d9-94c4-f49351b811f1": Object { @@ -567,6 +578,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "8-4e4f-41d9-94c4-f49351b811f1": Object { @@ -606,6 +618,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "9-4e4f-41d9-94c4-f49351b811f1": Object { @@ -646,6 +659,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -657,6 +671,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7": Object { @@ -702,6 +717,7 @@ Object { "protocolVersion": "v1", "state": "done", "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c": Object { @@ -763,6 +779,7 @@ Object { "protocolVersion": "v1", "state": "done", "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } @@ -832,6 +849,7 @@ Object { "reuseConnectionId": undefined, "role": "receiver", "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "2-4e4f-41d9-94c4-f49351b811f1": Object { @@ -896,6 +914,7 @@ Object { "reuseConnectionId": undefined, "role": "sender", "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "3-4e4f-41d9-94c4-f49351b811f1": Object { @@ -960,6 +979,7 @@ Object { "reuseConnectionId": undefined, "role": "sender", "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "4-4e4f-41d9-94c4-f49351b811f1": Object { @@ -1024,6 +1044,7 @@ Object { "reuseConnectionId": undefined, "role": "receiver", "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "5-4e4f-41d9-94c4-f49351b811f1": Object { @@ -1088,6 +1109,7 @@ Object { "reuseConnectionId": undefined, "role": "receiver", "state": "done", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "6-4e4f-41d9-94c4-f49351b811f1": Object { @@ -1147,6 +1169,7 @@ Object { "reuseConnectionId": undefined, "role": "sender", "state": "await-response", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7-4e4f-41d9-94c4-f49351b811f1": Object { @@ -1211,6 +1234,7 @@ Object { "reuseConnectionId": undefined, "role": "sender", "state": "await-response", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7781341d-be29-441b-9b79-4a957d8c6d37": Object { @@ -1244,6 +1268,7 @@ Object { "theirDid": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", "theirLabel": "Agent: PopulateWallet2", "threadId": "a0c0e4d2-1501-42a2-a09b-7d5adc90b353", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "8f4908ee-15ad-4058-9106-eda26eae735c": Object { @@ -1277,6 +1302,7 @@ Object { "theirDid": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", "theirLabel": "Agent: PopulateWallet2", "threadId": "fe287ec6-711b-4582-bb2b-d155aee86e61", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "9383d8e5-c002-4aae-8300-4a21384c919e": Object { @@ -1309,6 +1335,7 @@ Object { "theirDid": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", "theirLabel": "Agent: PopulateWallet2", "threadId": "0b2f1133-ced9-49f1-83a1-eb6ba1c24cdf", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -1320,6 +1347,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "b65c2ccd-277c-4140-9d87-c8dd30e7a98c": Object { @@ -1349,6 +1377,7 @@ Object { "outOfBandId": "7-4e4f-41d9-94c4-f49351b811f1", "role": "responder", "state": "invitation-sent", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "da518433-0e55-4b74-a05b-aa75c1095a99": Object { @@ -1382,6 +1411,7 @@ Object { "theirDid": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", "theirLabel": "Agent: PopulateWallet2", "threadId": "6eeb6a80-cd75-491d-b2e0-7bae65ced1c3", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT": Object { @@ -1455,6 +1485,7 @@ Object { }, }, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56": Object { @@ -1528,6 +1559,7 @@ Object { }, }, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ": Object { @@ -1601,6 +1633,7 @@ Object { }, }, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb": Object { @@ -1674,6 +1707,7 @@ Object { }, }, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU": Object { @@ -1747,6 +1781,7 @@ Object { }, }, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy": Object { @@ -1820,6 +1855,7 @@ Object { }, }, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf": Object { @@ -1893,6 +1929,7 @@ Object { }, }, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga": Object { @@ -1966,6 +2003,7 @@ Object { }, }, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ": Object { @@ -2039,6 +2077,7 @@ Object { }, }, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui": Object { @@ -2112,6 +2151,7 @@ Object { }, }, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw": Object { @@ -2185,6 +2225,7 @@ Object { }, }, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX": Object { @@ -2258,6 +2299,7 @@ Object { }, }, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv": Object { @@ -2331,6 +2373,7 @@ Object { }, }, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "e3f9bc2b-f0a1-4a2c-ab81-2f0a3488c199": Object { @@ -2364,6 +2407,7 @@ Object { "theirDid": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", "theirLabel": "Agent: PopulateWallet2", "threadId": "daf3372c-1ee2-4246-a1f4-f62f54f7d68b", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "ee88e2e1-e27e-46a6-a910-f87690109e32": Object { @@ -2393,6 +2437,7 @@ Object { "role": "requester", "state": "request-sent", "theirLabel": "Agent: PopulateWallet2", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } @@ -2454,6 +2499,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "10-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2510,6 +2556,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "11-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2549,6 +2596,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "12-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2589,6 +2637,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "2-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2628,6 +2677,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "3-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2668,6 +2718,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "4-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2724,6 +2775,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "5-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2763,6 +2815,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a": Object { @@ -2808,6 +2861,7 @@ Object { "protocolVersion": "v1", "state": "done", "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a": Object { @@ -2869,6 +2923,7 @@ Object { "protocolVersion": "v1", "state": "done", "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "6-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2909,6 +2964,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7-4e4f-41d9-94c4-f49351b811f1": Object { @@ -2965,6 +3021,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "8-4e4f-41d9-94c4-f49351b811f1": Object { @@ -3004,6 +3061,7 @@ Object { }, "metadata": Object {}, "role": "sender", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "9-4e4f-41d9-94c4-f49351b811f1": Object { @@ -3044,6 +3102,7 @@ Object { }, "metadata": Object {}, "role": "receiver", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -3055,6 +3114,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7": Object { @@ -3100,6 +3160,7 @@ Object { "protocolVersion": "v1", "state": "done", "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c": Object { @@ -3161,6 +3222,7 @@ Object { "protocolVersion": "v1", "state": "done", "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } @@ -3191,6 +3253,7 @@ Object { ], "state": "granted", "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7f14c1ec-514c-49b2-a00b-04af7e600060": Object { @@ -3216,6 +3279,7 @@ Object { ], "state": "granted", "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "802ef124-36b7-490f-b152-e9d090ddf073": Object { @@ -3238,6 +3302,7 @@ Object { "routingKeys": Array [], "state": "granted", "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -3249,6 +3314,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": Object { @@ -3271,6 +3337,7 @@ Object { "routingKeys": Array [], "state": "granted", "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } @@ -3301,6 +3368,7 @@ Object { ], "state": "granted", "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7f14c1ec-514c-49b2-a00b-04af7e600060": Object { @@ -3326,6 +3394,7 @@ Object { ], "state": "granted", "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "802ef124-36b7-490f-b152-e9d090ddf073": Object { @@ -3348,6 +3417,7 @@ Object { "routingKeys": Array [], "state": "granted", "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -3359,6 +3429,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": Object { @@ -3381,6 +3452,7 @@ Object { "routingKeys": Array [], "state": "granted", "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } @@ -3411,6 +3483,7 @@ Object { ], "state": "granted", "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7f14c1ec-514c-49b2-a00b-04af7e600060": Object { @@ -3436,6 +3509,7 @@ Object { ], "state": "granted", "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "802ef124-36b7-490f-b152-e9d090ddf073": Object { @@ -3458,6 +3532,7 @@ Object { "routingKeys": Array [], "state": "granted", "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -3469,6 +3544,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": Object { @@ -3491,6 +3567,7 @@ Object { "routingKeys": Array [], "state": "granted", "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } @@ -3521,6 +3598,7 @@ Object { ], "state": "granted", "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7f14c1ec-514c-49b2-a00b-04af7e600060": Object { @@ -3546,6 +3624,7 @@ Object { ], "state": "granted", "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "802ef124-36b7-490f-b152-e9d090ddf073": Object { @@ -3568,6 +3647,7 @@ Object { "routingKeys": Array [], "state": "granted", "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -3579,6 +3659,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.2", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": Object { @@ -3601,6 +3682,7 @@ Object { "routingKeys": Array [], "state": "granted", "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap index a56e8065c1..8d76122ef4 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap @@ -122,6 +122,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.3.1", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "ea840186-3c77-45f4-a2e6-349811ad8994": Object { @@ -302,6 +303,7 @@ Object { "id": "1-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "2-4e4f-41d9-94c4-f49351b811f1": Object { @@ -364,6 +366,7 @@ Object { "id": "2-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "3-4e4f-41d9-94c4-f49351b811f1": Object { @@ -426,6 +429,7 @@ Object { "id": "3-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "4-4e4f-41d9-94c4-f49351b811f1": Object { @@ -490,6 +494,7 @@ Object { "id": "4-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "5-4e4f-41d9-94c4-f49351b811f1": Object { @@ -552,6 +557,7 @@ Object { "id": "5-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "6-4e4f-41d9-94c4-f49351b811f1": Object { @@ -614,6 +620,7 @@ Object { "id": "6-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7-4e4f-41d9-94c4-f49351b811f1": Object { @@ -676,6 +683,7 @@ Object { "id": "7-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "8-4e4f-41d9-94c4-f49351b811f1": Object { @@ -738,6 +746,7 @@ Object { "id": "8-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -749,6 +758,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.3.1", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } @@ -876,6 +886,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.3.1", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "ea840186-3c77-45f4-a2e6-349811ad8994": Object { diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap index 8169373e57..d75c5d4c22 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap @@ -64,6 +64,7 @@ Object { "id": "1-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "2-4e4f-41d9-94c4-f49351b811f1": Object { @@ -126,6 +127,7 @@ Object { "id": "2-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "3-4e4f-41d9-94c4-f49351b811f1": Object { @@ -188,6 +190,7 @@ Object { "id": "3-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "4-4e4f-41d9-94c4-f49351b811f1": Object { @@ -252,6 +255,7 @@ Object { "id": "4-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "5-4e4f-41d9-94c4-f49351b811f1": Object { @@ -314,6 +318,7 @@ Object { "id": "5-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "6-4e4f-41d9-94c4-f49351b811f1": Object { @@ -376,6 +381,7 @@ Object { "id": "6-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "7-4e4f-41d9-94c4-f49351b811f1": Object { @@ -438,6 +444,7 @@ Object { "id": "7-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "received", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "8-4e4f-41d9-94c4-f49351b811f1": Object { @@ -500,6 +507,7 @@ Object { "id": "8-4e4f-41d9-94c4-f49351b811f1", "metadata": Object {}, "role": "created", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": Object { @@ -511,6 +519,7 @@ Object { "id": "STORAGE_VERSION_RECORD_ID", "metadata": Object {}, "storageVersion": "0.3.1", + "updatedAt": "2022-01-21T22:50:20.522Z", }, }, } diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap index 04765793f5..676480ae59 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap @@ -39,6 +39,7 @@ Array [ "protocolVersion": "v1", "state": "done", "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-03-21T22:50:20.522Z", }, Object { "_tags": Object { @@ -94,6 +95,7 @@ Array [ "protocolVersion": "v1", "state": "done", "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "updatedAt": "2022-03-21T22:50:20.522Z", }, Object { "_tags": Object { @@ -132,6 +134,7 @@ Array [ "protocolVersion": "v1", "state": "done", "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-03-21T22:50:20.522Z", }, Object { "_tags": Object { @@ -187,6 +190,7 @@ Array [ "protocolVersion": "v1", "state": "done", "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "updatedAt": "2022-03-21T22:50:20.522Z", }, ] `; diff --git a/tests/InMemoryStorageService.ts b/tests/InMemoryStorageService.ts index cd4415a2e5..0b2a73ebb4 100644 --- a/tests/InMemoryStorageService.ts +++ b/tests/InMemoryStorageService.ts @@ -35,6 +35,7 @@ export class InMemoryStorageService implement /** @inheritDoc */ public async save(agentContext: AgentContext, record: T) { + record.updatedAt = new Date() const value = JsonTransformer.toJSON(record) if (this.records[record.id]) { @@ -51,6 +52,7 @@ export class InMemoryStorageService implement /** @inheritDoc */ public async update(agentContext: AgentContext, record: T): Promise { + record.updatedAt = new Date() const value = JsonTransformer.toJSON(record) delete value._tags From efe0271198f21f1307df0f934c380f7a5c720b06 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 10 Feb 2023 19:15:36 -0300 Subject: [PATCH 18/20] feat: add anoncreds-rs package (#1275) Signed-off-by: Ariel Gentile --- .github/actions/setup-libssl/action.yml | 22 + .../setup-postgres-wallet-plugin/action.yml | 2 +- .github/workflows/continuous-integration.yml | 17 +- Dockerfile | 12 +- packages/anoncreds-rs/README.md | 31 ++ packages/anoncreds-rs/jest.config.ts | 14 + packages/anoncreds-rs/package.json | 41 ++ .../anoncreds-rs/src/AnonCredsRsModule.ts | 29 + .../src/errors/AnonCredsRsError.ts | 7 + packages/anoncreds-rs/src/index.ts | 5 + .../src/services/AnonCredsRsHolderService.ts | 374 +++++++++++++ .../src/services/AnonCredsRsIssuerService.ts | 158 ++++++ .../services/AnonCredsRsVerifierService.ts | 66 +++ .../AnonCredsRsHolderService.test.ts | 501 ++++++++++++++++++ .../__tests__/AnonCredsRsServices.test.ts | 224 ++++++++ .../src/services/__tests__/helpers.ts | 173 ++++++ packages/anoncreds-rs/src/services/index.ts | 3 + packages/anoncreds-rs/src/types.ts | 4 + packages/anoncreds-rs/tests/indy-flow.test.ts | 277 ++++++++++ packages/anoncreds-rs/tests/setup.ts | 3 + packages/anoncreds-rs/tsconfig.build.json | 7 + packages/anoncreds-rs/tsconfig.json | 6 + .../src/formats/AnonCredsCredentialFormat.ts | 4 +- .../LegacyIndyCredentialFormatService.ts | 18 +- packages/anoncreds/src/models/registry.ts | 16 +- .../repository/AnonCredsCredentialRecord.ts | 76 +++ .../AnonCredsCredentialRepository.ts | 31 ++ packages/anoncreds/src/repository/index.ts | 2 + .../services/AnonCredsHolderServiceOptions.ts | 1 + packages/anoncreds/tests/anoncreds.test.ts | 16 +- packages/core/src/index.ts | 2 +- .../indy-sdk/src/anoncreds/utils/transform.ts | 24 +- tests/InMemoryStorageService.ts | 1 + yarn.lock | 28 +- 34 files changed, 2155 insertions(+), 40 deletions(-) create mode 100644 .github/actions/setup-libssl/action.yml create mode 100644 packages/anoncreds-rs/README.md create mode 100644 packages/anoncreds-rs/jest.config.ts create mode 100644 packages/anoncreds-rs/package.json create mode 100644 packages/anoncreds-rs/src/AnonCredsRsModule.ts create mode 100644 packages/anoncreds-rs/src/errors/AnonCredsRsError.ts create mode 100644 packages/anoncreds-rs/src/index.ts create mode 100644 packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts create mode 100644 packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts create mode 100644 packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts create mode 100644 packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts create mode 100644 packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts create mode 100644 packages/anoncreds-rs/src/services/__tests__/helpers.ts create mode 100644 packages/anoncreds-rs/src/services/index.ts create mode 100644 packages/anoncreds-rs/src/types.ts create mode 100644 packages/anoncreds-rs/tests/indy-flow.test.ts create mode 100644 packages/anoncreds-rs/tests/setup.ts create mode 100644 packages/anoncreds-rs/tsconfig.build.json create mode 100644 packages/anoncreds-rs/tsconfig.json create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialRepository.ts diff --git a/.github/actions/setup-libssl/action.yml b/.github/actions/setup-libssl/action.yml new file mode 100644 index 0000000000..9710ea6e88 --- /dev/null +++ b/.github/actions/setup-libssl/action.yml @@ -0,0 +1,22 @@ +name: Setup libSSL +description: Install libssl and libssl-dev 1.1 +author: 'gentilester@gmail.com' + +runs: + using: composite + steps: + - name: Install libssl1.1 + run: | + curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1-1ubuntu2.1~18.04.21_amd64.deb -o libssl1.1.deb + sudo dpkg -i libssl1.1.deb + shell: bash + + - name: Instal libssl-dev.1.1 + run: | + curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl-dev_1.1.1-1ubuntu2.1~18.04.21_amd64.deb -o libssl-dev1.1.deb + sudo dpkg -i libssl-dev1.1.deb + shell: bash + +branding: + icon: scissors + color: purple diff --git a/.github/actions/setup-postgres-wallet-plugin/action.yml b/.github/actions/setup-postgres-wallet-plugin/action.yml index a03b2f3fde..81f41d3578 100644 --- a/.github/actions/setup-postgres-wallet-plugin/action.yml +++ b/.github/actions/setup-postgres-wallet-plugin/action.yml @@ -10,7 +10,7 @@ runs: # so pointing rust version to 1.63.0 - name: Setup Postgres wallet plugin run: | - sudo apt-get install -y libzmq3-dev libsodium-dev pkg-config libssl-dev + sudo apt-get install -y libzmq3-dev libsodium-dev pkg-config curl https://sh.rustup.rs -sSf | bash -s -- -y export PATH="/root/.cargo/bin:${PATH}" rustup default 1.63.0 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 44820700fe..0ad780e636 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -26,7 +26,7 @@ jobs: # validation scripts. To still be able to run the CI we can manually trigger it by adding the 'ci-test' # label to the pull request ci-trigger: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 outputs: triggered: ${{ steps.check.outputs.triggered }} steps: @@ -45,13 +45,16 @@ jobs: echo "::set-output name=triggered::${SHOULD_RUN}" validate: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 name: Validate steps: - name: Checkout aries-framework-javascript uses: actions/checkout@v2 # setup dependencies + - name: Setup Libssl + uses: ./.github/actions/setup-libssl + - name: Setup Libindy uses: ./.github/actions/setup-libindy @@ -76,7 +79,7 @@ jobs: run: yarn build integration-test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 name: Integration Tests strategy: @@ -88,6 +91,9 @@ jobs: uses: actions/checkout@v2 # setup dependencies + - name: Setup Libssl + uses: ./.github/actions/setup-libssl + - name: Setup Libindy uses: ./.github/actions/setup-libindy - name: Setup Indy Pool @@ -115,7 +121,7 @@ jobs: if: always() version-stable: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 name: Release stable needs: [integration-test, validate] if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' @@ -127,6 +133,9 @@ jobs: fetch-depth: 0 # setup dependencies + - name: Setup Libssl + uses: ./.github/actions/setup-libssl + - name: Setup Libindy uses: ./.github/actions/setup-libindy diff --git a/Dockerfile b/Dockerfile index 91ccda0363..7f55d81dfe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:18.04 as base +FROM ubuntu:22.04 as base ENV DEBIAN_FRONTEND noninteractive @@ -9,7 +9,15 @@ RUN apt-get update -y && apt-get install -y \ # Only needed to build indy-sdk build-essential \ git \ - libzmq3-dev libsodium-dev pkg-config libssl-dev + libzmq3-dev libsodium-dev pkg-config + +# libssl1.1 (required by libindy) +RUN curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1-1ubuntu2.1~18.04.21_amd64.deb -o libssl1.1.deb +RUN dpkg -i libssl1.1.deb + +# libssl-dev1.1 (required to compile libindy with posgres plugin) +RUN curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl-dev_1.1.1-1ubuntu2.1~18.04.21_amd64.deb -o libssl-dev1.1.deb +RUN dpkg -i libssl-dev1.1.deb # libindy RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 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 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript AnonCreds RS Module

+

+ License + typescript + @aries-framework/anoncreds-rs version + +

+
+ +AnonCreds RS module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). diff --git a/packages/anoncreds-rs/jest.config.ts b/packages/anoncreds-rs/jest.config.ts new file mode 100644 index 0000000000..55c67d70a6 --- /dev/null +++ b/packages/anoncreds-rs/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + name: packageJson.name, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json new file mode 100644 index 0000000000..d60aa4f4ca --- /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", + "private": true, + "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.5", + "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.5", + "rimraf": "^4.0.7", + "typescript": "~4.9.4" + } +} diff --git a/packages/anoncreds-rs/src/AnonCredsRsModule.ts b/packages/anoncreds-rs/src/AnonCredsRsModule.ts new file mode 100644 index 0000000000..4ceb7b8304 --- /dev/null +++ b/packages/anoncreds-rs/src/AnonCredsRsModule.ts @@ -0,0 +1,29 @@ +import type { DependencyManager, Module } from '@aries-framework/core' + +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, +} from '@aries-framework/anoncreds' + +import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from './services' + +export class AnonCredsRsModule implements Module { + public register(dependencyManager: DependencyManager) { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + require('@hyperledger/anoncreds-nodejs') + } catch (error) { + try { + require('@hyperledger/anoncreds-react-native') + } catch (error) { + throw new Error('Could not load anoncreds bindings') + } + } + + // Register services + dependencyManager.registerSingleton(AnonCredsHolderServiceSymbol, AnonCredsRsHolderService) + dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, AnonCredsRsIssuerService) + dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, AnonCredsRsVerifierService) + } +} 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..e0c84fd7b1 --- /dev/null +++ b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts @@ -0,0 +1,374 @@ +import type { + AnonCredsHolderService, + AnonCredsProof, + CreateCredentialRequestOptions, + CreateCredentialRequestReturn, + CreateProofOptions, + GetCredentialOptions, + StoreCredentialOptions, + GetCredentialsForProofRequestOptions, + GetCredentialsForProofRequestReturn, + AnonCredsCredentialInfo, + CreateLinkSecretOptions, + CreateLinkSecretReturn, + AnonCredsProofRequestRestriction, + AnonCredsRequestedAttribute, + AnonCredsRequestedPredicate, + AnonCredsCredential, +} from '@aries-framework/anoncreds' +import type { AgentContext, Query, SimpleQuery } from '@aries-framework/core' +import type { CredentialEntry, CredentialProve } from '@hyperledger/anoncreds-shared' + +import { + AnonCredsCredentialRecord, + AnonCredsLinkSecretRepository, + AnonCredsCredentialRepository, +} from '@aries-framework/anoncreds' +import { injectable } from '@aries-framework/core' +import { + CredentialRequestMetadata, + Credential, + CredentialDefinition, + CredentialOffer, + CredentialRequest, + CredentialRevocationState, + MasterSecret, + Presentation, + PresentationRequest, + RevocationRegistryDefinition, + RevocationStatusList, + Schema, +} from '@hyperledger/anoncreds-shared' + +import { uuid } from '../../../core/src/utils/uuid' +import { AnonCredsRsError } from '../errors/AnonCredsRsError' + +@injectable() +export class AnonCredsRsHolderService implements AnonCredsHolderService { + public async createLinkSecret( + agentContext: AgentContext, + options?: CreateLinkSecretOptions + ): Promise { + try { + return { + linkSecretId: options?.linkSecretId ?? uuid(), + linkSecretValue: JSON.parse(MasterSecret.create().toJson()).value.ms, + } + } catch (error) { + agentContext.config.logger.error(`Error creating Link Secret`, { + error, + }) + throw new AnonCredsRsError('Error creating Link Secret', { cause: error }) + } + } + + public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise { + const { credentialDefinitions, proofRequest, requestedCredentials, schemas } = options + + try { + const rsCredentialDefinitions: Record = {} + for (const credDefId in credentialDefinitions) { + rsCredentialDefinitions[credDefId] = CredentialDefinition.load(JSON.stringify(credentialDefinitions[credDefId])) + } + + const rsSchemas: Record = {} + for (const schemaId in schemas) { + rsSchemas[schemaId] = Schema.load(JSON.stringify(schemas[schemaId])) + } + + const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + + // Cache retrieved credentials in order to minimize storage calls + const retrievedCredentials = new Map() + + const credentialEntryFromAttribute = async ( + attribute: AnonCredsRequestedAttribute | AnonCredsRequestedPredicate + ): 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 credential = Credential.load(JSON.stringify(credentialRecord.credential)) + + const revocationRegistryDefinitionId = credential.revocationRegistryId + const revocationRegistryIndex = credential.revocationRegistryIndex + + // 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 + if (timestamp) { + if (revocationRegistryIndex) { + if (!options.revocationRegistries[revocationRegistryDefinitionId]) { + throw new AnonCredsRsError(`Revocation Registry ${revocationRegistryDefinitionId} not found`) + } + + const { definition, tailsFilePath } = options.revocationRegistries[revocationRegistryDefinitionId] + + const revocationRegistryDefinition = RevocationRegistryDefinition.load(JSON.stringify(definition)) + revocationState = CredentialRevocationState.create({ + revocationRegistryIndex, + revocationRegistryDefinition, + tailsPath: tailsFilePath, + revocationStatusList: RevocationStatusList.create({ + issuanceByDefault: true, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + timestamp, + }), + }) + } + } + return { + linkSecretId: credentialRecord.linkSecretId, + credentialEntry: { + credential, + revocationState, + timestamp, + }, + } + } + + const credentialsProve: CredentialProve[] = [] + const credentials: { linkSecretId: string; credentialEntry: CredentialEntry }[] = [] + + let entryIndex = 0 + for (const referent in requestedCredentials.requestedAttributes) { + const attribute = requestedCredentials.requestedAttributes[referent] + credentials.push(await credentialEntryFromAttribute(attribute)) + credentialsProve.push({ entryIndex, isPredicate: false, referent, reveal: attribute.revealed }) + entryIndex = entryIndex + 1 + } + + for (const referent in requestedCredentials.requestedPredicates) { + const predicate = requestedCredentials.requestedPredicates[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') + } + + const presentation = Presentation.create({ + credentialDefinitions: rsCredentialDefinitions, + schemas: rsSchemas, + presentationRequest: PresentationRequest.load(JSON.stringify(proofRequest)), + credentials: credentials.map((entry) => entry.credentialEntry), + credentialsProve, + selfAttest: requestedCredentials.selfAttestedAttributes, + masterSecret: MasterSecret.load(JSON.stringify({ value: { ms: linkSecretRecord.value } })), + }) + + return JSON.parse(presentation.toJson()) + } catch (error) { + agentContext.config.logger.error(`Error creating AnonCreds Proof`, { + error, + proofRequest, + requestedCredentials, + }) + throw new AnonCredsRsError(`Error creating proof: ${error}`, { cause: error }) + } + } + + public async createCredentialRequest( + agentContext: AgentContext, + options: CreateCredentialRequestOptions + ): Promise { + const { credentialDefinition, credentialOffer } = options + 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 default link secret has been found') + } + + const { credentialRequest, credentialRequestMetadata } = CredentialRequest.create({ + credentialDefinition: CredentialDefinition.load(JSON.stringify(credentialDefinition)), + credentialOffer: CredentialOffer.load(JSON.stringify(credentialOffer)), + masterSecret: MasterSecret.load(JSON.stringify({ value: { ms: linkSecretRecord.value } })), + masterSecretId: linkSecretRecord.linkSecretId, + }) + + return { + credentialRequest: JSON.parse(credentialRequest.toJson()), + credentialRequestMetadata: JSON.parse(credentialRequestMetadata.toJson()), + } + } catch (error) { + throw new AnonCredsRsError(`Error creating credential request: ${error}`, { cause: error }) + } + } + + 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.master_secret_name) + + const revocationRegistryDefinition = revocationRegistry?.definition + ? RevocationRegistryDefinition.load(JSON.stringify(revocationRegistry.definition)) + : undefined + + const credentialId = options.credentialId ?? uuid() + const processedCredential = Credential.load(JSON.stringify(credential)).process({ + credentialDefinition: CredentialDefinition.load(JSON.stringify(credentialDefinition)), + credentialRequestMetadata: CredentialRequestMetadata.load(JSON.stringify(credentialRequestMetadata)), + masterSecret: MasterSecret.load(JSON.stringify({ value: { ms: linkSecretRecord.value } })), + revocationRegistryDefinition, + }) + + const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + + await credentialRepository.save( + agentContext, + new AnonCredsCredentialRecord({ + credential: JSON.parse(processedCredential.toJson()) as AnonCredsCredential, + credentialId, + linkSecretId: linkSecretRecord.linkSecretId, + issuerId: options.credentialDefinition.issuerId, + schemaName: schema.name, + schemaIssuerId: schema.issuerId, + schemaVersion: schema.version, + }) + ) + + return credentialId + } + + 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, + } + } + + 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 attributes = requestedAttribute.name ? [requestedAttribute.name] : requestedAttribute.names + + const restrictionQuery = requestedAttribute.restrictions + ? this.queryFromRestrictions(requestedAttribute.restrictions) + : undefined + + const query: Query = { + attributes, + ...restrictionQuery, + ...options.extraQuery, + } + + const credentials = await agentContext.dependencyManager + .resolve(AnonCredsCredentialRepository) + .findByQuery(agentContext, query) + + 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, + }, + interval: proofRequest.non_revoked, + } + }) + } + + private queryFromRestrictions(restrictions: AnonCredsProofRequestRestriction[]) { + const query: Query[] = [] + + for (const restriction of restrictions) { + const queryElements: SimpleQuery = {} + + if (restriction.cred_def_id) { + queryElements.credentialDefinitionId = restriction.cred_def_id + } + + if (restriction.issuer_id || restriction.issuer_did) { + queryElements.issuerId = restriction.issuer_id ?? restriction.issuer_did + } + + if (restriction.rev_reg_id) { + queryElements.revocationRegistryId = restriction.rev_reg_id + } + + if (restriction.schema_id) { + queryElements.schemaId = restriction.schema_id + } + + if (restriction.schema_issuer_id || restriction.schema_issuer_did) { + queryElements.schemaIssuerId = restriction.schema_issuer_id ?? restriction.schema_issuer_did + } + + if (restriction.schema_name) { + queryElements.schemaName = restriction.schema_name + } + + if (restriction.schema_version) { + queryElements.schemaVersion = restriction.schema_version + } + + 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..17b3c91d91 --- /dev/null +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -0,0 +1,158 @@ +import type { + AnonCredsIssuerService, + CreateCredentialDefinitionOptions, + CreateCredentialOfferOptions, + CreateCredentialOptions, + CreateCredentialReturn, + CreateSchemaOptions, + AnonCredsCredentialOffer, + AnonCredsSchema, + AnonCredsCredentialDefinition, + CreateCredentialDefinitionReturn, +} from '@aries-framework/anoncreds' +import type { AgentContext } from '@aries-framework/core' + +import { + AnonCredsKeyCorrectnessProofRepository, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionRepository, +} from '@aries-framework/anoncreds' +import { injectable, AriesFrameworkError } from '@aries-framework/core' +import { + Credential, + CredentialDefinition, + CredentialDefinitionPrivate, + CredentialRequest, + CredentialOffer, + KeyCorrectnessProof, + 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 + + try { + const schema = Schema.create({ + issuerId, + name, + version, + attributeNames, + }) + + return JSON.parse(schema.toJson()) as AnonCredsSchema + } catch (error) { + throw new AnonCredsRsError('Error creating schema', { cause: error }) + } + } + + public async createCredentialDefinition( + agentContext: AgentContext, + options: CreateCredentialDefinitionOptions + ): Promise { + const { tag, supportRevocation, schema, issuerId, schemaId } = options + + try { + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = CredentialDefinition.create({ + schema: Schema.load(JSON.stringify(schema)), + issuerId, + schemaId, + tag, + supportRevocation, + signatureType: 'CL', + }) + + return { + credentialDefinition: JSON.parse(credentialDefinition.toJson()) as AnonCredsCredentialDefinition, + credentialDefinitionPrivate: JSON.parse(credentialDefinitionPrivate.toJson()), + keyCorrectnessProof: JSON.parse(keyCorrectnessProof.toJson()), + } + } catch (error) { + throw new AnonCredsRsError('Error creating credential definition', { cause: error }) + } + } + + public async createCredentialOffer( + agentContext: AgentContext, + options: CreateCredentialOfferOptions + ): Promise { + const { credentialDefinitionId } = options + + try { + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, options.credentialDefinitionId) + + const keyCorrectnessProofRecord = await agentContext.dependencyManager + .resolve(AnonCredsKeyCorrectnessProofRepository) + .getByCredentialDefinitionId(agentContext, options.credentialDefinitionId) + + if (!credentialDefinitionRecord) { + throw new AnonCredsRsError(`Credential Definition ${credentialDefinitionId} not found`) + } + + const credentialOffer = CredentialOffer.create({ + credentialDefinitionId, + keyCorrectnessProof: KeyCorrectnessProof.load(JSON.stringify(keyCorrectnessProofRecord?.value)), + schemaId: credentialDefinitionRecord.credentialDefinition.schemaId, + }) + + return JSON.parse(credentialOffer.toJson()) as AnonCredsCredentialOffer + } catch (error) { + throw new AnonCredsRsError(`Error creating credential offer: ${error}`, { cause: error }) + } + } + + public async createCredential( + agentContext: AgentContext, + options: CreateCredentialOptions + ): Promise { + const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryId } = options + + 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) + + const credentialDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionPrivateRepository) + .getByCredentialDefinitionId(agentContext, options.credentialRequest.cred_def_id) + + const credential = Credential.create({ + credentialDefinition: CredentialDefinition.load( + JSON.stringify(credentialDefinitionRecord.credentialDefinition) + ), + credentialOffer: CredentialOffer.load(JSON.stringify(credentialOffer)), + credentialRequest: CredentialRequest.load(JSON.stringify(credentialRequest)), + revocationRegistryId, + attributeEncodedValues, + attributeRawValues, + credentialDefinitionPrivate: CredentialDefinitionPrivate.load( + JSON.stringify(credentialDefinitionPrivateRecord.value) + ), + }) + + return { + credential: JSON.parse(credential.toJson()), + credentialRevocationId: credential.revocationRegistryIndex?.toString(), + } + } catch (error) { + throw new AnonCredsRsError(`Error creating credential: ${error}`, { cause: error }) + } + } +} diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts new file mode 100644 index 0000000000..96030d44ba --- /dev/null +++ b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts @@ -0,0 +1,66 @@ +import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' + +import { injectable } from '@aries-framework/core' +import { + CredentialDefinition, + Presentation, + PresentationRequest, + RevocationRegistryDefinition, + RevocationStatusList, + Schema, +} from '@hyperledger/anoncreds-shared' + +import { AnonCredsRsError } from '../errors/AnonCredsRsError' + +@injectable() +export class AnonCredsRsVerifierService implements AnonCredsVerifierService { + public async verifyProof(options: VerifyProofOptions): Promise { + const { credentialDefinitions, proof, proofRequest, revocationStates, schemas } = options + + try { + const presentation = Presentation.load(JSON.stringify(proof)) + + const rsCredentialDefinitions: Record = {} + for (const credDefId in credentialDefinitions) { + rsCredentialDefinitions[credDefId] = CredentialDefinition.load(JSON.stringify(credentialDefinitions[credDefId])) + } + + const rsSchemas: Record = {} + for (const schemaId in schemas) { + rsSchemas[schemaId] = Schema.load(JSON.stringify(schemas[schemaId])) + } + + const revocationRegistryDefinitions: Record = {} + const lists = [] + + for (const revocationRegistryDefinitionId in revocationStates) { + const { definition, revocationStatusLists } = options.revocationStates[revocationRegistryDefinitionId] + + revocationRegistryDefinitions[revocationRegistryDefinitionId] = RevocationRegistryDefinition.load( + JSON.stringify(definition) + ) + + for (const timestamp in revocationStatusLists) { + lists.push( + RevocationStatusList.create({ + issuanceByDefault: true, + revocationRegistryDefinition: revocationRegistryDefinitions[revocationRegistryDefinitionId], + revocationRegistryDefinitionId, + timestamp: Number(timestamp), + }) + ) + } + } + + return presentation.verify({ + presentationRequest: PresentationRequest.load(JSON.stringify(proofRequest)), + credentialDefinitions: rsCredentialDefinitions, + schemas: rsSchemas, + revocationRegistryDefinitions, + revocationStatusLists: lists, + }) + } catch (error) { + throw new AnonCredsRsError('Error verifying proof', { cause: error }) + } + } +} 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..f0585f6ffb --- /dev/null +++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts @@ -0,0 +1,501 @@ +import type { + AnonCredsCredentialDefinition, + AnonCredsProofRequest, + AnonCredsRequestedCredentials, + AnonCredsRevocationStatusList, + AnonCredsCredential, + AnonCredsSchema, +} from '@aries-framework/anoncreds' + +import { + AnonCredsHolderServiceSymbol, + AnonCredsLinkSecretRecord, + AnonCredsCredentialRecord, +} from '@aries-framework/anoncreds' +import { anoncreds, RevocationRegistryDefinition } from '@hyperledger/anoncreds-nodejs' + +import { AnonCredsCredentialDefinitionRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialDefinitionRepository' +import { AnonCredsCredentialRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialRepository' +import { AnonCredsLinkSecretRepository } from '../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository' +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], + ], + agentConfig, +}) + +describe('AnonCredsRsHolderService', () => { + const getByCredentialIdMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'getByCredentialId') + + afterEach(() => { + 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, + 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, + schemaId: 'phoneschema:uri', + credentialDefinitionId: 'phonecreddef:uri', + credentialDefinitionPrivate: phoneCredentialDefinitionPrivate, + keyCorrectnessProof: phoneKeyCorrectnessProof, + linkSecret, + linkSecretId: 'linkSecretId', + credentialId: 'phoneCredId', + revocationRegistryDefinitionId: 'phonerevregid:uri', + }) + + const requestedCredentials: AnonCredsRequestedCredentials = { + selfAttestedAttributes: { attr5_referent: 'football' }, + requestedAttributes: { + 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 }, + }, + requestedPredicates: { + predicate1_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo }, + }, + } + + getByCredentialIdMock.mockResolvedValueOnce( + new AnonCredsCredentialRecord({ + credential: personCredential, + credentialId: 'personCredId', + linkSecretId: 'linkSecretId', + issuerId: 'issuerDid', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + }) + ) + getByCredentialIdMock.mockResolvedValueOnce( + new AnonCredsCredentialRecord({ + credential: phoneCredential, + credentialId: 'phoneCredId', + linkSecretId: 'linkSecretId', + issuerId: 'issuerDid', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + }) + ) + + 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 }, + }, + } + + const proof = await anonCredsHolderService.createProof(agentContext, { + credentialDefinitions: { + 'personcreddef:uri': personCredentialDefinition as AnonCredsCredentialDefinition, + 'phonecreddef:uri': phoneCredentialDefinition as AnonCredsCredentialDefinition, + }, + proofRequest, + requestedCredentials, + 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' }], + }, + }, + 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, { + attributes: ['name'], + issuerId: 'issuer:uri', + }) + }) + + test('referent without restrictions', async () => { + await anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'attr2_referent', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + attributes: ['phoneNumber'], + }) + }) + + test('referent with multiple, complex restrictions', async () => { + await anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'attr3_referent', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + attributes: ['age'], + $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, { + attributes: ['name', 'height'], + credentialDefinitionId: 'crededefid:uri', + issuerId: 'issuerid:uri', + }) + }) + + test('predicate referent', async () => { + await anonCredsHolderService.getCredentialsForProofRequest(agentContext, { + proofRequest, + attributeReferent: 'predicate1_referent', + }) + + expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { + attributes: ['age'], + }) + }) + }) + + 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', + }) + ) + + 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', + }) + ) + 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('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, + 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: JSON.parse(credentialRequestMetadata.toJson()), + credentialId: 'personCredId', + revocationRegistry: { + id: 'personrevregid:uri', + definition: JSON.parse(new RevocationRegistryDefinition(revocationRegistryDefinition.handle).toJson()), + }, + }) + + 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..3e23f27eb0 --- /dev/null +++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts @@ -0,0 +1,224 @@ +import type { AnonCredsProofRequest } from '@aries-framework/anoncreds' + +import { + 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 { encode } 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], + ], + agentConfig, +}) + +describe('AnonCredsRsServices', () => { + test('issuance flow without revocation', async () => { + const issuerId = 'issuer:uri' + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState, schemaMetadata } = 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, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + 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: encode('John') }, age: { raw: '25', encoded: encode('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? + }) + + 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, + requestedCredentials: { + requestedAttributes: { + attr1_referent: { credentialId, credentialInfo, revealed: true }, + attr2_referent: { credentialId, credentialInfo, revealed: true }, + }, + requestedPredicates: { + predicate1_referent: { credentialId, credentialInfo }, + }, + selfAttestedAttributes: {}, + }, + schemas: { [schemaState.schemaId]: schema }, + revocationRegistries: {}, + }) + + const verifiedProof = await anonCredsVerifierService.verifyProof({ + credentialDefinitions: { [credentialDefinitionState.credentialDefinitionId]: credentialDefinition }, + proof, + proofRequest, + schemas: { [schemaState.schemaId]: schema }, + revocationStates: {}, + }) + + 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..07d5b09f49 --- /dev/null +++ b/packages/anoncreds-rs/src/services/__tests__/helpers.ts @@ -0,0 +1,173 @@ +import type { AnonCredsCredentialInfo } from '@aries-framework/anoncreds' + +import { + anoncreds, + CredentialDefinition, + CredentialDefinitionPrivate, + CredentialOffer, + CredentialRequest, + KeyCorrectnessProof, + MasterSecret, + 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', + }) + + return { + credentialDefinition: JSON.parse(credentialDefinition.toJson()), + credentialDefinitionPrivate: JSON.parse(credentialDefinitionPrivate.toJson()), + keyCorrectnessProof: JSON.parse(keyCorrectnessProof.toJson()), + schema: JSON.parse(schema.toJson()), + } +} + +/** + * Creates a valid credential offer and returns itsf + */ +export function createCredentialOffer(kcp: Record) { + const credentialOffer = CredentialOffer.create({ + credentialDefinitionId: 'creddef:uri', + keyCorrectnessProof: KeyCorrectnessProof.load(JSON.stringify(kcp)), + schemaId: 'schema:uri', + }) + return JSON.parse(credentialOffer.toJson()) +} + +/** + * + * @returns Creates a valid link secret value for anoncreds-rs + */ +export function createLinkSecret() { + return JSON.parse(MasterSecret.create().toJson()).value.ms as string +} + +export function createCredentialForHolder(options: { + credentialDefinition: Record + credentialDefinitionPrivate: Record + keyCorrectnessProof: Record + 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: KeyCorrectnessProof.load(JSON.stringify(keyCorrectnessProof)), + schemaId, + }) + + const { credentialRequest, credentialRequestMetadata } = CredentialRequest.create({ + credentialDefinition: CredentialDefinition.load(JSON.stringify(credentialDefinition)), + credentialOffer, + masterSecret: MasterSecret.load(JSON.stringify({ value: { ms: linkSecret } })), + masterSecretId: linkSecretId, + }) + + // FIXME: Revocation config should not be mandatory but current anoncreds-rs is requiring it + + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate, tailsPath } = + createRevocationRegistryDefinition({ + credentialDefinitionId, + credentialDefinition, + }) + + const timeCreateRevStatusList = 12 + const revocationStatusList = anoncreds.createRevocationStatusList({ + timestamp: timeCreateRevStatusList, + issuanceByDefault: true, + revocationRegistryDefinition, + revocationRegistryDefinitionId: revocationRegistryDefinitionId, + }) + + // TODO: Use Credential.create (needs to update the paramters in anoncreds-rs) + const credentialObj = anoncreds.createCredential({ + credentialDefinition: CredentialDefinition.load(JSON.stringify(credentialDefinition)).handle, + credentialDefinitionPrivate: CredentialDefinitionPrivate.load(JSON.stringify(credentialDefinitionPrivate)).handle, + credentialOffer: credentialOffer.handle, + credentialRequest: credentialRequest.handle, + attributeRawValues: attributes, + revocationRegistryId: revocationRegistryDefinitionId, + revocationStatusList, + revocationConfiguration: { + registryIndex: 9, + revocationRegistryDefinition, + revocationRegistryDefinitionPrivate, + tailsPath, + }, + }) + const credential = anoncreds.getJson({ objectHandle: credentialObj }) + + const credentialInfo: AnonCredsCredentialInfo = { + attributes, + credentialDefinitionId, + credentialId, + schemaId, + } + return { + credential: JSON.parse(credential), + credentialInfo, + revocationRegistryDefinition, + tailsPath, + credentialRequestMetadata, + } +} + +export function createRevocationRegistryDefinition(options: { + credentialDefinitionId: string + credentialDefinition: Record +}) { + const { credentialDefinitionId, credentialDefinition } = options + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + anoncreds.createRevocationRegistryDefinition({ + credentialDefinitionId, + credentialDefinition: CredentialDefinition.load(JSON.stringify(credentialDefinition)).handle, + issuerId: 'mock:uri', + 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/src/types.ts b/packages/anoncreds-rs/src/types.ts new file mode 100644 index 0000000000..2694976be7 --- /dev/null +++ b/packages/anoncreds-rs/src/types.ts @@ -0,0 +1,4 @@ +import type { Anoncreds } from '@hyperledger/anoncreds-shared' + +export const AnonCredsRsSymbol = Symbol('AnonCredsRs') +export type { Anoncreds } 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..fc2ce9ec87 --- /dev/null +++ b/packages/anoncreds-rs/tests/indy-flow.test.ts @@ -0,0 +1,277 @@ +import { + AnonCredsModuleConfig, + LegacyIndyCredentialFormatService, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, + AnonCredsRegistryService, + AnonCredsSchemaRecord, + AnonCredsSchemaRepository, + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsLinkSecretRepository, + AnonCredsLinkSecretRecord, +} from '@aries-framework/anoncreds' +import { + CredentialState, + CredentialExchangeRecord, + CredentialPreviewAttribute, + InjectionSymbols, +} from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +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') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +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, +}) + +const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() + +describe('LegacyIndyCredentialFormatService using anoncreds-rs', () => { + test('issuance flow starting from proposal without negotiation and without revocation', async () => { + // 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 = 'TL1EaPFCZ8Si5aUrqScBDt' + + 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, 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, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + 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 legacyIndyCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + indy: { + attributes: credentialAttributes, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }, + }) + + // 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, + }, + }, + }) + + // 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: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: null, + credentialRevocationId: undefined, // FIXME: should be null? + }) + + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anonCreds/anonCredsCredential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + '_anonCreds/anonCredsCredentialRequest': { + master_secret_blinding_data: expect.any(Object), + master_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anonCreds/anonCredsCredential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }) + }) +}) diff --git a/packages/anoncreds-rs/tests/setup.ts b/packages/anoncreds-rs/tests/setup.ts new file mode 100644 index 0000000000..a5fef0aec8 --- /dev/null +++ b/packages/anoncreds-rs/tests/setup.ts @@ -0,0 +1,3 @@ +import '@hyperledger/anoncreds-nodejs' + +jest.setTimeout(60000) diff --git a/packages/anoncreds-rs/tsconfig.build.json b/packages/anoncreds-rs/tsconfig.build.json new file mode 100644 index 0000000000..2b75d0adab --- /dev/null +++ b/packages/anoncreds-rs/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build" + }, + "include": ["src/**/*"] +} diff --git a/packages/anoncreds-rs/tsconfig.json b/packages/anoncreds-rs/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/packages/anoncreds-rs/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts index fd6ebf7fcb..e08109f56f 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts @@ -35,7 +35,9 @@ export interface AnonCredsAcceptProposalFormat { * This defines the module payload for calling CredentialsApi.acceptOffer. No options are available for this * method, so it's an empty object */ -export type AnonCredsAcceptOfferFormat = Record +export interface AnonCredsAcceptOfferFormat { + linkSecretId?: string +} /** * This defines the module payload for calling CredentialsApi.offerCredential diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts index e1fd945937..6be55555a4 100644 --- a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -209,7 +209,12 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic public async acceptOffer( agentContext: AgentContext, - { credentialRecord, attachId, offerAttachment }: FormatAcceptOfferOptions + { + credentialRecord, + attachId, + offerAttachment, + credentialFormats, + }: FormatAcceptOfferOptions ): Promise { const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) @@ -232,6 +237,7 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic const { credentialRequest, credentialRequestMetadata } = await holderService.createCredentialRequest(agentContext, { credentialOffer, credentialDefinition, + linkSecretId: credentialFormats?.indy?.linkSecretId, }) credentialRecord.metadata.set( @@ -357,6 +363,15 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic ) } + 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) { @@ -381,6 +396,7 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic credential: anonCredsCredential, credentialDefinitionId: credentialDefinitionResult.credentialDefinitionId, credentialDefinition: credentialDefinitionResult.credentialDefinition, + schema: schemaResult.schema, revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition ? { definition: revocationRegistryResult.revocationRegistryDefinition, diff --git a/packages/anoncreds/src/models/registry.ts b/packages/anoncreds/src/models/registry.ts index f4f3429ec2..31314ada51 100644 --- a/packages/anoncreds/src/models/registry.ts +++ b/packages/anoncreds/src/models/registry.ts @@ -19,17 +19,19 @@ export interface AnonCredsCredentialDefinition { export interface AnonCredsRevocationRegistryDefinition { issuerId: string - type: 'CL_ACCUM' + revocDefType: 'CL_ACCUM' credDefId: string tag: string - publicKeys: { - accumKey: { - z: string + value: { + publicKeys: { + accumKey: { + z: string + } } + maxCredNum: number + tailsLocation: string + tailsHash: string } - maxCredNum: number - tailsLocation: string - tailsHash: string } export interface AnonCredsRevocationStatusList { diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialRecord.ts new file mode 100644 index 0000000000..3d4d0958b7 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialRecord.ts @@ -0,0 +1,76 @@ +import type { AnonCredsCredential } from '../models' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsCredentialRecordProps { + id?: string + credential: AnonCredsCredential + credentialId: string + credentialRevocationId?: string + linkSecretId: string + schemaName: string + schemaVersion: string + schemaIssuerId: string + issuerId: string +} + +export type DefaultAnonCredsCredentialTags = { + credentialId: string + linkSecretId: string + credentialDefinitionId: string + credentialRevocationId?: string + revocationRegistryId?: string + schemaId: string + attributes: string[] +} + +export type CustomAnonCredsCredentialTags = { + schemaName: string + schemaVersion: string + schemaIssuerId: string + issuerId: string +} + +export class AnonCredsCredentialRecord extends BaseRecord< + DefaultAnonCredsCredentialTags, + CustomAnonCredsCredentialTags +> { + public static readonly type = 'AnonCredsCredentialRecord' + public readonly type = AnonCredsCredentialRecord.type + + public readonly credentialId!: string + public readonly credentialRevocationId?: string + public readonly linkSecretId!: string + public readonly credential!: AnonCredsCredential + + public constructor(props: AnonCredsCredentialRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialId = props.credentialId + this.credential = props.credential + this.credentialRevocationId = props.credentialRevocationId + this.linkSecretId = props.linkSecretId + this.setTags({ + issuerId: props.issuerId, + schemaIssuerId: props.schemaIssuerId, + schemaName: props.schemaName, + schemaVersion: props.schemaVersion, + }) + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credential.cred_def_id, + schemaId: this.credential.schema_id, + credentialId: this.credentialId, + credentialRevocationId: this.credentialRevocationId, + revocationRegistryId: this.credential.rev_reg_id, + linkSecretId: this.linkSecretId, + attributes: Object.keys(this.credential.values), + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialRepository.ts b/packages/anoncreds/src/repository/AnonCredsCredentialRepository.ts new file mode 100644 index 0000000000..fb02878439 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialRepository.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsCredentialRecord } from './AnonCredsCredentialRecord' + +@injectable() +export class AnonCredsCredentialRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsCredentialRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async getByCredentialId(agentContext: AgentContext, credentialId: string) { + return this.getSingleByQuery(agentContext, { credentialId }) + } + + public async findByCredentialId(agentContext: AgentContext, credentialId: string) { + return this.findSingleByQuery(agentContext, { credentialId }) + } +} diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts index 5e17e19941..c4fb3bbe80 100644 --- a/packages/anoncreds/src/repository/index.ts +++ b/packages/anoncreds/src/repository/index.ts @@ -1,3 +1,5 @@ +export * from './AnonCredsCredentialRecord' +export * from './AnonCredsCredentialRepository' export * from './AnonCredsCredentialDefinitionRecord' export * from './AnonCredsCredentialDefinitionRepository' export * from './AnonCredsCredentialDefinitionPrivateRecord' diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index fcbc5e913c..747e3fcfed 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -47,6 +47,7 @@ export interface StoreCredentialOptions { credentialRequestMetadata: AnonCredsCredentialRequestMetadata credential: AnonCredsCredential credentialDefinition: AnonCredsCredentialDefinition + schema: AnonCredsSchema credentialDefinitionId: string credentialId?: string revocationRegistry?: { diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index e7abd466c4..f905c92db9 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -43,16 +43,18 @@ const existingRevocationRegistryDefinitions = { 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG': { credDefId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', issuerId: 'VsKV7grR1BUE29mG2Fm2kX', - maxCredNum: 100, - type: 'CL_ACCUM', - publicKeys: { - accumKey: { - z: 'ab81257c-be63-4051-9e21-c7d384412f64', + revocDefType: 'CL_ACCUM', + value: { + publicKeys: { + accumKey: { + z: 'ab81257c-be63-4051-9e21-c7d384412f64', + }, }, + maxCredNum: 100, + tailsHash: 'ab81257c-be63-4051-9e21-c7d384412f64', + tailsLocation: 'http://localhost:7200/tails', }, tag: 'TAG', - tailsHash: 'ab81257c-be63-4051-9e21-c7d384412f64', - tailsLocation: 'http://localhost:7200/tails', }, } as const diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ee9c82dfa0..91d22659c4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -33,7 +33,7 @@ export * from './storage/BaseRecord' export { InMemoryMessageRepository } from './storage/InMemoryMessageRepository' export { Repository } from './storage/Repository' export * from './storage/RepositoryEvents' -export { StorageService, Query, BaseRecordConstructor } from './storage/StorageService' +export { StorageService, Query, SimpleQuery, BaseRecordConstructor } from './storage/StorageService' export * from './storage/migration' export { getDirFromFilePath } from './utils/path' export { InjectionSymbols } from './constants' diff --git a/packages/indy-sdk/src/anoncreds/utils/transform.ts b/packages/indy-sdk/src/anoncreds/utils/transform.ts index 6a91928f70..e976d514e4 100644 --- a/packages/indy-sdk/src/anoncreds/utils/transform.ts +++ b/packages/indy-sdk/src/anoncreds/utils/transform.ts @@ -63,12 +63,14 @@ export function anonCredsRevocationRegistryDefinitionFromIndySdk( return { issuerId, credDefId: revocationRegistryDefinition.credDefId, - maxCredNum: revocationRegistryDefinition.value.maxCredNum, - publicKeys: revocationRegistryDefinition.value.publicKeys, + value: { + maxCredNum: revocationRegistryDefinition.value.maxCredNum, + publicKeys: revocationRegistryDefinition.value.publicKeys, + tailsHash: revocationRegistryDefinition.value.tailsHash, + tailsLocation: revocationRegistryDefinition.value.tailsLocation, + }, tag: revocationRegistryDefinition.tag, - tailsHash: revocationRegistryDefinition.value.tailsHash, - tailsLocation: revocationRegistryDefinition.value.tailsLocation, - type: 'CL_ACCUM', + revocDefType: 'CL_ACCUM', } } @@ -79,14 +81,14 @@ export function indySdkRevocationRegistryDefinitionFromAnonCreds( return { id: revocationRegistryDefinitionId, credDefId: revocationRegistryDefinition.credDefId, - revocDefType: revocationRegistryDefinition.type, + revocDefType: revocationRegistryDefinition.revocDefType, tag: revocationRegistryDefinition.tag, value: { issuanceType: 'ISSUANCE_BY_DEFAULT', // NOTE: we always use ISSUANCE_BY_DEFAULT when passing to the indy-sdk. It doesn't matter, as we have the revocation List with the full state - maxCredNum: revocationRegistryDefinition.maxCredNum, - publicKeys: revocationRegistryDefinition.publicKeys, - tailsHash: revocationRegistryDefinition.tailsHash, - tailsLocation: revocationRegistryDefinition.tailsLocation, + maxCredNum: revocationRegistryDefinition.value.maxCredNum, + publicKeys: revocationRegistryDefinition.value.publicKeys, + tailsHash: revocationRegistryDefinition.value.tailsHash, + tailsLocation: revocationRegistryDefinition.value.tailsLocation, }, ver: '1.0', } @@ -103,7 +105,7 @@ export function anonCredsRevocationStatusListFromIndySdk( const defaultState = isIssuanceByDefault ? 0 : 1 // Fill with default value - const revocationList = new Array(revocationRegistryDefinition.maxCredNum).fill(defaultState) + const revocationList = new Array(revocationRegistryDefinition.value.maxCredNum).fill(defaultState) // Set all `issuer` indexes to 0 (not revoked) for (const issued of delta.value.issued ?? []) { diff --git a/tests/InMemoryStorageService.ts b/tests/InMemoryStorageService.ts index 0b2a73ebb4..e1fc3f2f60 100644 --- a/tests/InMemoryStorageService.ts +++ b/tests/InMemoryStorageService.ts @@ -131,6 +131,7 @@ export class InMemoryStorageService implement } const records = Object.values(this.records) + .filter((record) => record.type === recordClass.type) .filter((record) => { const tags = record.tags as TagsBase diff --git a/yarn.lock b/yarn.lock index 4a8447c5fe..0f3abfb2f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -858,6 +858,24 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@hyperledger/anoncreds-nodejs@^0.1.0-dev.5": + version "0.1.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.1.0-dev.5.tgz#71b6dbcfab72f826bcead2b79dafe47fc8f1567c" + integrity sha512-BX/OxQjTMoCAJP4fgJEcGct1ZnNYgybO+VLD5LyzHW4nmTFOJo3TXy5IYHAJv61b/uNUQ/2GMYmPKLSLOVExNw== + dependencies: + "@hyperledger/anoncreds-shared" "0.1.0-dev.5" + "@mapbox/node-pre-gyp" "^1.0.10" + ffi-napi "4.0.3" + node-cache "5.1.2" + ref-array-di "1.2.2" + ref-napi "3.0.3" + ref-struct-di "1.1.1" + +"@hyperledger/anoncreds-shared@0.1.0-dev.5", "@hyperledger/anoncreds-shared@^0.1.0-dev.5": + version "0.1.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.1.0-dev.5.tgz#653a8ec1ac83eae3af8aabb7fa5609bb0c3453b2" + integrity sha512-NPbjZd7WJN/eKtHtYcOy+E9Ebh0YkZ7bre59zWD3w66aiehZrSLbL5+pjY9shrSIN1h05t0XnvT1JZKTtXgqcQ== + "@hyperledger/aries-askar-nodejs@^0.1.0-dev.1": version "0.1.0-dev.1" resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.1.0-dev.1.tgz#b384d422de48f0ce5918e1612d2ca32ebd160520" @@ -5357,7 +5375,7 @@ fetch-blob@^2.1.1: resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c" integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow== -ffi-napi@^4.0.3: +ffi-napi@4.0.3, ffi-napi@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/ffi-napi/-/ffi-napi-4.0.3.tgz#27a8d42a8ea938457154895c59761fbf1a10f441" integrity sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg== @@ -8417,7 +8435,7 @@ node-addon-api@^3.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-cache@^5.1.2: +node-cache@5.1.2, node-cache@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== @@ -9723,7 +9741,7 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== -ref-array-di@^1.2.2: +ref-array-di@1.2.2, ref-array-di@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ref-array-di/-/ref-array-di-1.2.2.tgz#ceee9d667d9c424b5a91bb813457cc916fb1f64d" integrity sha512-jhCmhqWa7kvCVrWhR/d7RemkppqPUdxEil1CtTtm7FkZV8LcHHCK3Or9GinUiFP5WY3k0djUkMvhBhx49Jb2iA== @@ -9731,7 +9749,7 @@ ref-array-di@^1.2.2: array-index "^1.0.0" debug "^3.1.0" -"ref-napi@^2.0.1 || ^3.0.2", ref-napi@^3.0.3: +ref-napi@3.0.3, "ref-napi@^2.0.1 || ^3.0.2", ref-napi@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/ref-napi/-/ref-napi-3.0.3.tgz#e259bfc2bbafb3e169e8cd9ba49037dd00396b22" integrity sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA== @@ -9741,7 +9759,7 @@ ref-array-di@^1.2.2: node-addon-api "^3.0.0" node-gyp-build "^4.2.1" -ref-struct-di@^1.1.0, ref-struct-di@^1.1.1: +ref-struct-di@1.1.1, ref-struct-di@^1.1.0, ref-struct-di@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ref-struct-di/-/ref-struct-di-1.1.1.tgz#5827b1d3b32372058f177547093db1fe1602dc10" integrity sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g== From 30857b92702f422db808f372b2416998cd0365ed Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht <61358536+blu3beri@users.noreply.github.com> Date: Sat, 11 Feb 2023 22:50:10 +0100 Subject: [PATCH 19/20] fix(transport)!: added docs moved connection to connectionId (#1222) Signed-off-by: blu3beri --- packages/core/src/agent/MessageReceiver.ts | 2 +- packages/core/src/agent/TransportService.ts | 27 ++++++++++++++++--- .../agent/__tests__/TransportService.test.ts | 2 +- packages/core/src/agent/__tests__/stubs.ts | 3 +-- packages/core/src/foobarbaz | 0 5 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/foobarbaz diff --git a/packages/core/src/agent/MessageReceiver.ts b/packages/core/src/agent/MessageReceiver.ts index befabec616..55d1368fc0 100644 --- a/packages/core/src/agent/MessageReceiver.ts +++ b/packages/core/src/agent/MessageReceiver.ts @@ -147,7 +147,7 @@ export class MessageReceiver { // We allow unready connections to be attached to the session as we want to be able to // use return routing to make connections. This is especially useful for creating connections // with mediators when you don't have a public endpoint yet. - session.connection = connection ?? undefined + session.connectionId = connection?.id messageContext.sessionId = session.id this.transportService.saveSession(session) } else if (session) { diff --git a/packages/core/src/agent/TransportService.ts b/packages/core/src/agent/TransportService.ts index 0eda25500c..9455a045bb 100644 --- a/packages/core/src/agent/TransportService.ts +++ b/packages/core/src/agent/TransportService.ts @@ -1,6 +1,5 @@ import type { AgentMessage } from './AgentMessage' import type { EnvelopeKeys } from './EnvelopeService' -import type { ConnectionRecord } from '../modules/connections/repository' import type { DidDocument } from '../modules/dids' import type { EncryptedMessage } from '../types' @@ -16,7 +15,7 @@ export class TransportService { } public findSessionByConnectionId(connectionId: string) { - return Object.values(this.transportSessionTable).find((session) => session?.connection?.id === connectionId) + return Object.values(this.transportSessionTable).find((session) => session?.connectionId === connectionId) } public hasInboundEndpoint(didDocument: DidDocument): boolean { @@ -36,12 +35,34 @@ interface TransportSessionTable { [sessionId: string]: TransportSession | undefined } +// In the framework Transport sessions are used for communication. A session is +// associated with a connection and it can be reused when we want to respond to +// a message. If the message, for example, does not contain any way to reply to +// this message, the session should be closed. When a new sequence of messages +// starts it can be used again. A session will be deleted when a WebSocket +// closes, for the WsTransportSession that is. export interface TransportSession { + // unique identifier for a transport session. This can a uuid, or anything else, as long + // as it uniquely identifies a transport. id: string + + // The type is something that explicitly defines the transport type. For WebSocket it would + // be "WebSocket" and for HTTP it would be "HTTP". type: string + + // The enveloping keys that can be used during the transport. This is used so the framework + // does not have to look up the associated keys for sending a message. keys?: EnvelopeKeys + + // A received message that will be used to check whether it has any return routing. inboundMessage?: AgentMessage - connection?: ConnectionRecord + + // A stored connection id used to find this session via the `TransportService` for a specific connection + connectionId?: string + + // Send an encrypted message send(encryptedMessage: EncryptedMessage): Promise + + // Close the session to prevent dangling sessions. close(): Promise } diff --git a/packages/core/src/agent/__tests__/TransportService.test.ts b/packages/core/src/agent/__tests__/TransportService.test.ts index c16d00478b..f46707fa3d 100644 --- a/packages/core/src/agent/__tests__/TransportService.test.ts +++ b/packages/core/src/agent/__tests__/TransportService.test.ts @@ -15,7 +15,7 @@ describe('TransportService', () => { test(`remove session saved for a given connection`, () => { const connection = getMockConnection({ id: 'test-123', role: DidExchangeRole.Responder }) const session = new DummyTransportSession('dummy-session-123') - session.connection = connection + session.connectionId = connection.id transportService.saveSession(session) expect(transportService.findSessionByConnectionId(connection.id)).toEqual(session) diff --git a/packages/core/src/agent/__tests__/stubs.ts b/packages/core/src/agent/__tests__/stubs.ts index 49fcaae660..afd7ec4aaa 100644 --- a/packages/core/src/agent/__tests__/stubs.ts +++ b/packages/core/src/agent/__tests__/stubs.ts @@ -1,4 +1,3 @@ -import type { ConnectionRecord } from '../../modules/connections' import type { AgentMessage } from '../AgentMessage' import type { EnvelopeKeys } from '../EnvelopeService' import type { TransportSession } from '../TransportService' @@ -8,7 +7,7 @@ export class DummyTransportSession implements TransportSession { public readonly type = 'http' public keys?: EnvelopeKeys public inboundMessage?: AgentMessage - public connection?: ConnectionRecord + public connectionId?: string public constructor(id: string) { this.id = id diff --git a/packages/core/src/foobarbaz b/packages/core/src/foobarbaz new file mode 100644 index 0000000000..e69de29bb2 From 60bde6d2a042fc13ae561360978c87b47a5a1b4e Mon Sep 17 00:00:00 2001 From: blu3beri Date: Sun, 12 Feb 2023 21:01:12 +0100 Subject: [PATCH 20/20] chore(core): remove useless file Signed-off-by: blu3beri --- packages/core/src/foobarbaz | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/core/src/foobarbaz diff --git a/packages/core/src/foobarbaz b/packages/core/src/foobarbaz deleted file mode 100644 index e69de29bb2..0000000000