From c228f88e5accc243478fc18d469a5c074c143d29 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 29 Mar 2023 09:24:16 -0300 Subject: [PATCH 01/27] feat: initial revocation registry definition implementation Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 111 +++++++++++- packages/anoncreds/src/AnonCredsApi.ts | 158 +++++++++++++++--- packages/anoncreds/src/AnonCredsApiOptions.ts | 16 +- packages/anoncreds/src/AnonCredsModule.ts | 4 + .../AnonCredsCredentialFormatService.ts | 3 + packages/anoncreds/src/models/registry.ts | 1 + ...vocationRegistryDefinitionPrivateRecord.ts | 44 +++++ ...tionRegistryDefinitionPrivateRepository.ts | 27 +++ ...CredsRevocationRegistryDefinitionRecord.ts | 45 +++++ ...sRevocationRegistryDefinitionRepository.ts | 27 +++ ...istryDefinitionRecordMetadataTypes copy.ts | 11 ++ packages/anoncreds/src/repository/index.ts | 4 + .../src/services/AnonCredsIssuerService.ts | 7 + .../services/AnonCredsIssuerServiceOptions.ts | 27 ++- .../services/registry/AnonCredsRegistry.ts | 15 +- .../RevocationRegistryDefinitionOptions.ts | 43 ++++- .../tests/InMemoryAnonCredsRegistry.ts | 61 +++++++ .../services/IndySdkAnonCredsRegistry.ts | 7 + .../services/IndySdkIssuerService.ts | 11 +- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 113 ++++++++++++- 20 files changed, 686 insertions(+), 49 deletions(-) create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts create mode 100644 packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes copy.ts diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index 91c439d4b1..ec1313d3de 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -10,17 +10,30 @@ import type { AnonCredsCredentialDefinition, CreateCredentialDefinitionReturn, AnonCredsCredential, + CreateRevocationRegistryDefinitionOptions, + CreateRevocationRegistryDefinitionReturn, + AnonCredsRevocationRegistryDefinition, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared' import { + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, AnonCredsCredentialDefinitionPrivateRepository, AnonCredsCredentialDefinitionRepository, } from '@aries-framework/anoncreds' import { injectable, AriesFrameworkError } from '@aries-framework/core' -import { Credential, CredentialDefinition, CredentialOffer, Schema } from '@hyperledger/anoncreds-shared' +import { + RevocationRegistryDefinitionPrivate, + RevocationRegistryDefinition, + CredentialRevocationConfig, + Credential, + CredentialDefinition, + CredentialOffer, + Schema, +} from '@hyperledger/anoncreds-shared' import { AnonCredsRsError } from '../errors/AnonCredsRsError' @@ -79,6 +92,42 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } } + public async createRevocationRegistryDefinition( + agentContext: AgentContext, + options: CreateRevocationRegistryDefinitionOptions + ): Promise { + const { tag, issuerId, credentialDefinition, credentialDefinitionId, maximumCredentialNumber, tailsDirectoryPath } = + options + + let createReturnObj: + | { + revocationRegistryDefinition: RevocationRegistryDefinition + revocationRegistryDefinitionPrivate: RevocationRegistryDefinitionPrivate + } + | undefined + try { + createReturnObj = RevocationRegistryDefinition.create({ + credentialDefinition: credentialDefinition as unknown as JsonObject, + credentialDefinitionId, + issuerId, + maximumCredentialNumber, + revocationRegistryType: 'CL_ACCUM', + tag, + tailsDirectoryPath, + }) + + return { + revocationRegistryDefinition: + createReturnObj.revocationRegistryDefinition.toJson() as unknown as AnonCredsRevocationRegistryDefinition, + tailsHash: createReturnObj.revocationRegistryDefinition.getTailsHash(), + revocationRegistryDefinitionPrivate: createReturnObj.revocationRegistryDefinitionPrivate.toJson(), + } + } finally { + createReturnObj?.revocationRegistryDefinition.handle.clear() + createReturnObj?.revocationRegistryDefinitionPrivate.handle.clear() + } + } + public async createCredentialOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions @@ -115,14 +164,24 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialOptions ): Promise { - const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryId } = options + const { + credentialOffer, + credentialRequest, + credentialValues, + revocationRegistryDefinitionId, + tailsFilePath, + revocationStatusList, + } = options + + const definedRevocationOptions = [revocationRegistryDefinitionId, tailsFilePath, revocationStatusList].filter( + (e) => e !== undefined + ) + if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 3) { + throw new AriesFrameworkError('Revocation requires all of revocationRegistryDefinitionId and tailsFilePath') + } let credential: Credential | undefined try { - if (revocationRegistryId || tailsFilePath) { - throw new AriesFrameworkError('Revocation not supported yet') - } - const attributeRawValues: Record = {} const attributeEncodedValues: Record = {} @@ -139,14 +198,52 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { .resolve(AnonCredsCredentialDefinitionPrivateRepository) .getByCredentialDefinitionId(agentContext, options.credentialRequest.cred_def_id) + let revocationConfiguration: CredentialRevocationConfig | undefined + if (options.revocationRegistryDefinitionId && options.tailsFilePath) { + const revocationRegistryDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionRepository) + .getByRevocationRegistryDefinitionId(agentContext, options.revocationRegistryDefinitionId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, options.revocationRegistryDefinitionId) + + const registryIndex = revocationRegistryDefinitionPrivateRecord.currentIndex + 1 + + if (registryIndex > revocationRegistryDefinitionRecord.revocationRegistryDefinition.value.maxCredNum) { + throw new AriesFrameworkError('Revocation registry has reached maximum credential number') + } + + // Update current registry index in storage + // Note: if an error is produced or the credential is not effectively sent, + // the previous index will be skipped + revocationRegistryDefinitionPrivateRecord.currentIndex = registryIndex + await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .update(agentContext, revocationRegistryDefinitionPrivateRecord) + + revocationConfiguration = new CredentialRevocationConfig({ + registryDefinition: RevocationRegistryDefinition.fromJson( + revocationRegistryDefinitionRecord.revocationRegistryDefinition as unknown as JsonObject + ), + registryDefinitionPrivate: RevocationRegistryDefinitionPrivate.fromJson( + revocationRegistryDefinitionPrivateRecord.value + ), + tailsPath: options.tailsFilePath, + registryIndex, + }) + } + credential = Credential.create({ credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, credentialOffer: credentialOffer as unknown as JsonObject, credentialRequest: credentialRequest as unknown as JsonObject, - revocationRegistryId, + revocationRegistryId: revocationRegistryDefinitionId, attributeEncodedValues, attributeRawValues, credentialDefinitionPrivate: credentialDefinitionPrivateRecord.value, + revocationConfiguration, + revocationStatusList: revocationStatusList ? (revocationStatusList as unknown as JsonObject) : undefined, }) return { diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 77333e1afd..d42f5c5a0c 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -1,6 +1,7 @@ import type { AnonCredsCreateLinkSecretOptions, AnonCredsRegisterCredentialDefinitionOptions, + AnonCredsRegisterRevocationRegistryDefinitionOptions, } from './AnonCredsApiOptions' import type { GetCredentialDefinitionReturn, @@ -11,26 +12,33 @@ import type { RegisterSchemaOptions, RegisterSchemaReturn, GetCredentialsOptions, + RegisterRevocationRegistryDefinitionReturn, } 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 { + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, AnonCredsCredentialDefinitionPrivateRecord, AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRecord, AnonCredsKeyCorrectnessProofRepository, AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionRecord, } 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 { + AnonCredsCredentialDefinitionRecordMetadataKeys, + AnonCredsCredentialDefinitionRecordMetadataKeys as AnonCredsRevocationRegistryDefinitionRecordMetadataKeys, +} from './repository/anonCredsCredentialDefinitionRecordMetadataTypes' import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, @@ -48,6 +56,8 @@ export class AnonCredsApi { private anonCredsSchemaRepository: AnonCredsSchemaRepository private anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository private anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository + private anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository + private anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository private anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository private anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository private anonCredsIssuerService: AnonCredsIssuerService @@ -60,6 +70,8 @@ export class AnonCredsApi { @inject(AnonCredsIssuerServiceSymbol) anonCredsIssuerService: AnonCredsIssuerService, @inject(AnonCredsHolderServiceSymbol) anonCredsHolderService: AnonCredsHolderService, anonCredsSchemaRepository: AnonCredsSchemaRepository, + anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository, + anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository, anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository, anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository, anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository, @@ -71,6 +83,8 @@ export class AnonCredsApi { this.anonCredsIssuerService = anonCredsIssuerService this.anonCredsHolderService = anonCredsHolderService this.anonCredsSchemaRepository = anonCredsSchemaRepository + this.anonCredsRevocationRegistryDefinitionRepository = anonCredsRevocationRegistryDefinitionRepository + this.anonCredsRevocationRegistryDefinitionPrivateRepository = anonCredsRevocationRegistryDefinitionPrivateRepository this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository this.anonCredsCredentialDefinitionPrivateRepository = anonCredsCredentialDefinitionPrivateRepository this.anonCredsKeyCorrectnessProofRepository = anonCredsKeyCorrectnessProofRepository @@ -209,37 +223,38 @@ export class AnonCredsApi { } } - public async registerCredentialDefinition(options: { - credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions - // TODO: options should support supportsRevocation at some points - options: Extensible - }): Promise { + // TODO: Shall we store in Credential Definition Record the currently used revocation registry id? This can be used when accepting credential request to determine + // Which one we'll need to use. It can be also a tag in RevocRegDef Record stating which one is the active one for a given CredDefId. + + public async registerCredentialDefinition( + options: AnonCredsRegisterCredentialDefinitionOptions + ): Promise { const failedReturnBase = { credentialDefinitionState: { state: 'failed' as const, - reason: `Error registering credential definition for issuerId ${options.credentialDefinition.issuerId}`, + reason: `Error registering credential definition for issuerId ${options.issuerId}`, }, registrationMetadata: {}, credentialDefinitionMetadata: {}, } - const registry = this.findRegistryForIdentifier(options.credentialDefinition.issuerId) + const registry = this.findRegistryForIdentifier(options.issuerId) if (!registry) { - failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for issuerId ${options.credentialDefinition.issuerId}` + failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for issuerId ${options.issuerId}` return failedReturnBase } - const schemaRegistry = this.findRegistryForIdentifier(options.credentialDefinition.schemaId) + const schemaRegistry = this.findRegistryForIdentifier(options.schemaId) if (!schemaRegistry) { - failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for schemaId ${options.credentialDefinition.schemaId}` + failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for schemaId ${options.schemaId}` return failedReturnBase } try { - const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId) + const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.schemaId) if (!schemaResult.schema) { - failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.credentialDefinition.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` + failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` return failedReturnBase } @@ -247,10 +262,10 @@ export class AnonCredsApi { await this.anonCredsIssuerService.createCredentialDefinition( this.agentContext, { - issuerId: options.credentialDefinition.issuerId, - schemaId: options.credentialDefinition.schemaId, - tag: options.credentialDefinition.tag, - supportRevocation: false, + issuerId: options.issuerId, + schemaId: options.schemaId, + tag: options.tag ?? 'default', + supportRevocation: options.supportRevocation ?? false, schema: schemaResult.schema, }, // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. @@ -261,7 +276,7 @@ export class AnonCredsApi { const result = await registry.registerCredentialDefinition(this.agentContext, { credentialDefinition, - options: options.options, + options: {}, }) await this.storeCredentialDefinitionRecord(result, credentialDefinitionPrivate, keyCorrectnessProof) @@ -280,6 +295,58 @@ export class AnonCredsApi { } } + public async registerRevocationRegistryDefinition( + options: AnonCredsRegisterRevocationRegistryDefinitionOptions + ): Promise { + const { issuerId, tag, credentialDefinitionId, credentialDefinition, tailsDirectoryPath, maximumCredentialNumber } = + options + + const failedReturnBase = { + revocationRegistryDefinitionState: { + state: 'failed' as const, + reason: `Error registering revocation registry definition for issuerId ${options.issuerId}`, + }, + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(options.issuerId) + if (!registry) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Unable to register revocation registry definition. No registry found for issuerId ${options.issuerId}` + return failedReturnBase + } + + try { + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate, tailsHash } = + await this.anonCredsIssuerService.createRevocationRegistryDefinition(this.agentContext, { + issuerId, + tag, + credentialDefinitionId, + credentialDefinition, + maximumCredentialNumber, + tailsDirectoryPath, + }) + + const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { + revocationRegistryDefinition, + options: {}, + }) + + await this.storeRevocationRegistryDefinitionRecord(result, tailsHash, revocationRegistryDefinitionPrivate) + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Error storing revocation registry definition records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationRegistryDefinitionState.reason = `Error registering revocation registry definition: ${error.message}` + return failedReturnBase + } + } + /** * Retrieve a {@link AnonCredsRevocationRegistryDefinition} from the registry associated * with the {@link revocationRegistryDefinitionId} @@ -356,6 +423,59 @@ export class AnonCredsApi { return this.anonCredsHolderService.getCredentials(this.agentContext, options) } + private async storeRevocationRegistryDefinitionRecord( + result: RegisterRevocationRegistryDefinitionReturn, + tailsHash: string, + revocationRegistryDefinitionPrivate?: Record + ): Promise { + try { + // If we have both the revocationRegistryDefinition and the revocationRegistryDefinitionId 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.revocationRegistryDefinitionState.revocationRegistryDefinition && + result.revocationRegistryDefinitionState.revocationRegistryDefinitionId + ) { + const revocationRegistryDefinitionRecord = new AnonCredsRevocationRegistryDefinitionRecord({ + revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + revocationRegistryDefinition: result.revocationRegistryDefinitionState.revocationRegistryDefinition, + tailsHash, + }) + + // 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 + revocationRegistryDefinitionRecord.metadata.set( + AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.CredentialDefinitionMetadata, + result.revocationRegistryDefinitionMetadata + ) + revocationRegistryDefinitionRecord.metadata.set( + AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata, + result.registrationMetadata + ) + + await this.anonCredsRevocationRegistryDefinitionRepository.save( + this.agentContext, + revocationRegistryDefinitionRecord + ) + + // Store Revocation Registry Definition private data (if provided by issuer service) + if (revocationRegistryDefinitionPrivate) { + const revocationRegistryDefinitionPrivateRecord = new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + value: revocationRegistryDefinitionPrivate, + }) + await this.anonCredsRevocationRegistryDefinitionPrivateRepository.save( + this.agentContext, + revocationRegistryDefinitionPrivateRecord + ) + } + } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing revocation registry definition records`, { cause: error }) + } + } + private async storeCredentialDefinitionRecord( result: RegisterCredentialDefinitionReturn, credentialDefinitionPrivate?: Record, diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index 860ea059df..76b8b52c6f 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -5,4 +5,18 @@ export interface AnonCredsCreateLinkSecretOptions { setAsDefault?: boolean } -export type AnonCredsRegisterCredentialDefinitionOptions = Omit +export interface AnonCredsRegisterCredentialDefinitionOptions { + issuerId: string + schemaId: string + tag?: string + supportRevocation?: boolean +} + +export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { + issuerId: string + tag: string + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition + tailsDirectoryPath: string + maximumCredentialNumber: number +} diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index 3d6eff0b74..7fd59de890 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -7,6 +7,8 @@ import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, } from './repository' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' @@ -35,5 +37,7 @@ export class AnonCredsModule implements Module { dependencyManager.registerSingleton(AnonCredsCredentialDefinitionPrivateRepository) dependencyManager.registerSingleton(AnonCredsKeyCorrectnessProofRepository) dependencyManager.registerSingleton(AnonCredsLinkSecretRepository) + dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionRepository) + dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionPrivateRepository) } } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 28d7d47185..d3305f2a74 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -303,6 +303,9 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const credentialRequest = requestAttachment.getDataAsJson() if (!credentialRequest) throw new AriesFrameworkError('Missing anoncreds credential request in createCredential') + // TODO: Check if credential definition supports revocation, fetch current revocation registry id and tailsFilePath, + // and current revocation status list + const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer, credentialRequest, diff --git a/packages/anoncreds/src/models/registry.ts b/packages/anoncreds/src/models/registry.ts index 31314ada51..968640b857 100644 --- a/packages/anoncreds/src/models/registry.ts +++ b/packages/anoncreds/src/models/registry.ts @@ -23,6 +23,7 @@ export interface AnonCredsRevocationRegistryDefinition { credDefId: string tag: string value: { + issuanceType: 'ISSUANCE_BY_DEFAULT' | 'ISSUANCE_ON_DEMAND' publicKeys: { accumKey: { z: string diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts new file mode 100644 index 0000000000..ad2ed6c525 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts @@ -0,0 +1,44 @@ +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsRevocationRegistryDefinitionPrivateRecordProps { + id?: string + revocationRegistryDefinitionId: string + value: Record + index?: number +} + +export type DefaultAnonCredsRevocationRegistryPrivateTags = { + revocationRegistryDefinitionId: string +} + +export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseRecord< + DefaultAnonCredsRevocationRegistryPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsRevocationRegistryDefinitionPrivateRecord' + public readonly type = AnonCredsRevocationRegistryDefinitionPrivateRecord.type + + public readonly revocationRegistryDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public currentIndex!: number + public constructor(props: AnonCredsRevocationRegistryDefinitionPrivateRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId + this.value = props.value + this.currentIndex = props.index ?? 0 + } + } + + public getTags() { + return { + ...this._tags, + revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts new file mode 100644 index 0000000000..3dfff98758 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts @@ -0,0 +1,27 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsRevocationRegistryDefinitionPrivateRecord } from './AnonCredsRevocationRegistryDefinitionPrivateRecord' + +@injectable() +export class AnonCredsRevocationRegistryDefinitionPrivateRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) + storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsRevocationRegistryDefinitionPrivateRecord, storageService, eventEmitter) + } + + public async getByRevocationRegistryDefinitionId(agentContext: AgentContext, revocationRegistryDefinitionId: string) { + return this.getSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findByRevocationRegistryDefinitionId( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ) { + return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts new file mode 100644 index 0000000000..1971661a0c --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts @@ -0,0 +1,45 @@ +import type { AnonCredsRevocationRegistryDefinition } from '../models' +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsRevocationRegistryDefinitionRecordProps { + id?: string + revocationRegistryDefinitionId: string + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + tailsHash: string +} + +export type DefaultAnonCredsRevocationRegistryDefinitionTags = { + revocationRegistryDefinitionId: string +} + +export class AnonCredsRevocationRegistryDefinitionRecord extends BaseRecord< + DefaultAnonCredsRevocationRegistryDefinitionTags, + TagsBase +> { + public static readonly type = 'AnonCredsRevocationRegistryDefinitionRecord' + public readonly type = AnonCredsRevocationRegistryDefinitionRecord.type + + public readonly revocationRegistryDefinitionId!: string + public readonly revocationRegistryDefinition!: AnonCredsRevocationRegistryDefinition + public readonly tailsHash!: string + + public constructor(props: AnonCredsRevocationRegistryDefinitionRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId + this.revocationRegistryDefinition = props.revocationRegistryDefinition + this.tailsHash = props.tailsHash + } + } + + public getTags() { + return { + ...this._tags, + revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts new file mode 100644 index 0000000000..78222f0e63 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts @@ -0,0 +1,27 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsRevocationRegistryDefinitionRecord } from './AnonCredsRevocationRegistryDefinitionRecord' + +@injectable() +export class AnonCredsRevocationRegistryDefinitionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) + storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsRevocationRegistryDefinitionRecord, storageService, eventEmitter) + } + + public async getByRevocationRegistryDefinitionId(agentContext: AgentContext, revocationRegistryDefinitionId: string) { + return this.getSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findByRevocationRegistryDefinitionId( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ) { + return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes copy.ts b/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes copy.ts new file mode 100644 index 0000000000..7a960c2af1 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes copy.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsRevocationRegistryDefinitionRecordMetadataKeys { + RevocationRegistryDefinitionRegistrationMetadata = '_internal/anonCredsRevocationRegistryDefinitionRegistrationMetadata', + RevocationRegistryDefinitionMetadata = '_internal/anonCredsRevocationRegistryDefinitionMetadata', +} + +export type AnonCredsRevocationRegistryDefinitionRecordMetadata = { + [AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionRegistrationMetadata]: Extensible + [AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionMetadata]: Extensible +} diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts index c4fb3bbe80..8772b528e7 100644 --- a/packages/anoncreds/src/repository/index.ts +++ b/packages/anoncreds/src/repository/index.ts @@ -8,5 +8,9 @@ export * from './AnonCredsKeyCorrectnessProofRecord' export * from './AnonCredsKeyCorrectnessProofRepository' export * from './AnonCredsLinkSecretRecord' export * from './AnonCredsLinkSecretRepository' +export * from './AnonCredsRevocationRegistryDefinitionRecord' +export * from './AnonCredsRevocationRegistryDefinitionRepository' +export * from './AnonCredsRevocationRegistryDefinitionPrivateRecord' +export * from './AnonCredsRevocationRegistryDefinitionPrivateRepository' export * from './AnonCredsSchemaRecord' export * from './AnonCredsSchemaRepository' diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts index 3090b1759b..6edd86f505 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerService.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -5,6 +5,8 @@ import type { CreateCredentialReturn, CreateCredentialOptions, CreateCredentialDefinitionReturn, + CreateRevocationRegistryDefinitionOptions, + CreateRevocationRegistryDefinitionReturn, } from './AnonCredsIssuerServiceOptions' import type { AnonCredsCredentialOffer } from '../models/exchange' import type { AnonCredsSchema } from '../models/registry' @@ -23,6 +25,11 @@ export interface AnonCredsIssuerService { metadata?: Record ): Promise + createRevocationRegistryDefinition( + agentContext: AgentContext, + options: CreateRevocationRegistryDefinitionOptions + ): Promise + createCredentialOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index c7da246b9b..d9c2cadac0 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -4,7 +4,12 @@ import type { AnonCredsCredentialRequest, AnonCredsCredentialValues, } from '../models/exchange' -import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../models/registry' +import type { + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, + AnonCredsSchema, +} from '../models/registry' export interface CreateSchemaOptions { issuerId: string @@ -17,11 +22,19 @@ export interface CreateCredentialDefinitionOptions { issuerId: string tag: string supportRevocation?: boolean - schemaId: string schema: AnonCredsSchema } +export interface CreateRevocationRegistryDefinitionOptions { + issuerId: string + tag: string + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition + maximumCredentialNumber: number + tailsDirectoryPath: string +} + export interface CreateCredentialOfferOptions { credentialDefinitionId: string } @@ -30,9 +43,9 @@ export interface CreateCredentialOptions { credentialOffer: AnonCredsCredentialOffer credentialRequest: AnonCredsCredentialRequest credentialValues: AnonCredsCredentialValues - revocationRegistryId?: string - // TODO: should this just be the tails file instead of a path? + revocationRegistryDefinitionId?: string tailsFilePath?: string + revocationStatusList?: AnonCredsRevocationStatusList } export interface CreateCredentialReturn { @@ -45,3 +58,9 @@ export interface CreateCredentialDefinitionReturn { credentialDefinitionPrivate?: Record keyCorrectnessProof?: Record } + +export interface CreateRevocationRegistryDefinitionReturn { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionPrivate?: Record + tailsHash: string +} diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts index 870eb90571..ce9ca19f76 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts @@ -3,7 +3,11 @@ import type { RegisterCredentialDefinitionOptions, RegisterCredentialDefinitionReturn, } from './CredentialDefinitionOptions' -import type { GetRevocationRegistryDefinitionReturn } from './RevocationRegistryDefinitionOptions' +import type { + GetRevocationRegistryDefinitionReturn, + RegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturn, +} from './RevocationRegistryDefinitionOptions' import type { GetRevocationStatusListReturn } from './RevocationStatusListOptions' import type { GetSchemaReturn, RegisterSchemaOptions, RegisterSchemaReturn } from './SchemaOptions' import type { AgentContext } from '@aries-framework/core' @@ -31,11 +35,10 @@ export interface AnonCredsRegistry { revocationRegistryDefinitionId: string ): Promise - // TODO: issuance of revocable credentials - // registerRevocationRegistryDefinition( - // agentContext: AgentContext, - // options: RegisterRevocationRegistryDefinitionOptions - // ): Promise + registerRevocationRegistryDefinition( + agentContext: AgentContext, + options: RegisterRevocationRegistryDefinitionOptions + ): Promise getRevocationStatusList( agentContext: AgentContext, diff --git a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts index 6e9d1349fe..caf50268b9 100644 --- a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts @@ -1,4 +1,10 @@ -import type { AnonCredsResolutionMetadata, Extensible } from './base' +import type { + AnonCredsOperationState, + AnonCredsOperationStateFailed, + AnonCredsOperationStateFinished, + AnonCredsResolutionMetadata, + Extensible, +} from './base' import type { AnonCredsRevocationRegistryDefinition } from '../../models/registry' export interface GetRevocationRegistryDefinitionReturn { @@ -8,11 +14,32 @@ export interface GetRevocationRegistryDefinitionReturn { revocationRegistryDefinitionMetadata: Extensible } -// TODO: Support for issuance of revocable credentials -// export interface RegisterRevocationRegistryDefinitionOptions { -// revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition -// } +export interface RegisterRevocationRegistryDefinitionOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: Extensible +} + +export interface RegisterRevocationRegistryDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string +} + +export interface RegisterRevocationRegistryDefinitionReturnStateFinished extends AnonCredsOperationStateFinished { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId: string +} + +export interface RegisterRevocationRegistryDefinitionReturnState extends AnonCredsOperationState { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string +} -// export interface RegisterRevocationRegistryDefinitionReturn { -// revocationRegistryDefinitionId: string -// } +export interface RegisterRevocationRegistryDefinitionReturn { + jobId?: string + revocationRegistryDefinitionState: + | RegisterRevocationRegistryDefinitionReturnStateFailed + | RegisterRevocationRegistryDefinitionReturnStateFinished + | RegisterRevocationRegistryDefinitionReturnState + revocationRegistryDefinitionMetadata: Extensible + registrationMetadata: Extensible +} diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index dc6a812b57..7e3df0ba97 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -13,6 +13,8 @@ import type { AnonCredsRevocationRegistryDefinition, AnonCredsSchema, AnonCredsCredentialDefinition, + RegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturn, } from '../src' import type { AgentContext } from '@aries-framework/core' @@ -20,10 +22,13 @@ import { Hasher, TypedArrayEncoder } from '@aries-framework/core' import BigNumber from 'bn.js' import { + getDidIndyRevocationRegistryId, getDidIndyCredentialDefinitionId, getDidIndySchemaId, + getLegacyRevocationRegistryId, getLegacyCredentialDefinitionId, getLegacySchemaId, + parseCredentialDefinitionId, parseSchemaId, } from '../../indy-sdk/src/anoncreds/utils/identifiers' import { parseIndyDid } from '../../indy-sdk/src/dids/didIndyUtil' @@ -236,6 +241,62 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationRegistryDefinition( + agentContext: AgentContext, + options: RegisterRevocationRegistryDefinitionOptions + ): Promise { + const parsedCredentialDefinition = parseCredentialDefinitionId(options.revocationRegistryDefinition.credDefId) + const legacyCredentialDefinitionId = getLegacyCredentialDefinitionId( + parsedCredentialDefinition.namespaceIdentifier, + parsedCredentialDefinition.schemaSeqNo, + parsedCredentialDefinition.tag + ) + const indyLedgerSeqNo = getSeqNoFromSchemaId(legacyCredentialDefinitionId) + + let legacyIssuerId + let didIndyRevocationRegistryDefinitionId = '' + if (this.useLegacyIdentifiers) { + legacyIssuerId = options.revocationRegistryDefinition.issuerId + } else { + const { namespace, namespaceIdentifier } = parseIndyDid(options.revocationRegistryDefinition.issuerId) + legacyIssuerId = namespaceIdentifier + didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryId( + namespace, + namespaceIdentifier, + indyLedgerSeqNo, + parsedCredentialDefinition.tag, + options.revocationRegistryDefinition.tag + ) + + this.revocationRegistryDefinitions[didIndyRevocationRegistryDefinitionId] = options.revocationRegistryDefinition + } + + const legacyRevocationRegistryDefinitionId = getLegacyRevocationRegistryId( + legacyIssuerId, + indyLedgerSeqNo, + parsedCredentialDefinition.tag, + options.revocationRegistryDefinition.tag + ) + + this.revocationRegistryDefinitions[legacyRevocationRegistryDefinitionId] = { + ...options.revocationRegistryDefinition, + issuerId: legacyIssuerId, + credDefId: legacyCredentialDefinitionId, + } + + return { + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + state: 'finished', + revocationRegistryDefinition: options.revocationRegistryDefinition, + revocationRegistryDefinitionId: this.useLegacyIdentifiers + ? legacyRevocationRegistryDefinitionId + : didIndyRevocationRegistryDefinitionId, + }, + } + } + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index 8c222b1a18..2338dfb305 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -10,6 +10,8 @@ import type { RegisterCredentialDefinitionReturn, RegisterSchemaOptions, RegisterSchemaReturn, + RegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { Schema as IndySdkSchema } from 'indy-sdk' @@ -428,6 +430,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { issuerId: did, credDefId: credentialDefinitionId, value: { + issuanceType: revocationRegistryDefinition.value.issuanceType, maxCredNum: revocationRegistryDefinition.value.maxCredNum, publicKeys: revocationRegistryDefinition.value.publicKeys, tailsHash: revocationRegistryDefinition.value.tailsHash, @@ -462,6 +465,10 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationRegistryDefinition(): Promise { + throw new Error('Method not implemented.') + } + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index 72abbbdea8..5c02cf2437 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -9,6 +9,7 @@ import type { AnonCredsCredentialOffer, AnonCredsSchema, CreateCredentialDefinitionReturn, + CreateRevocationRegistryDefinitionReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' @@ -30,6 +31,9 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { this.indySdk = indySdk } + public async createRevocationRegistryDefinition(): Promise { + throw new AriesFrameworkError('Method not implemented.') + } public async createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise { // We only support passing qualified did:indy issuer ids in the indy issuer service when creating objects @@ -111,14 +115,15 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialOptions ): Promise { - const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryId } = options + const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryDefinitionId } = + options assertIndySdkWallet(agentContext.wallet) try { // Indy SDK requires tailsReaderHandle. Use null if no tailsFilePath is present const tailsReaderHandle = tailsFilePath ? await createTailsReader(agentContext, tailsFilePath) : 0 - if (revocationRegistryId || tailsFilePath) { + if (revocationRegistryDefinitionId || tailsFilePath) { throw new AriesFrameworkError('Revocation not supported yet') } @@ -130,7 +135,7 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { credentialOffer, { ...credentialRequest, prover_did: proverDid }, credentialValues, - revocationRegistryId ?? null, + revocationRegistryDefinitionId ?? null, tailsReaderHandle ) diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index ca2c1149f0..f4010e08e0 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -9,6 +9,8 @@ import type { GetRevocationStatusListReturn, GetRevocationRegistryDefinitionReturn, AnonCredsRevocationRegistryDefinition, + RegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' @@ -20,6 +22,7 @@ import { GetTransactionRequest, GetRevocationRegistryDeltaRequest, GetRevocationRegistryDefinitionRequest, + RevocationRegistryDefinitionRequest, } from '@hyperledger/indy-vdr-shared' import { parseIndyDid, verificationKeyForIndyDid } from '../dids/didIndyUtil' @@ -35,6 +38,7 @@ import { getDidIndyCredentialDefinitionId, parseRevocationRegistryId, getLegacyRevocationRegistryId, + getDidIndyRevocationRegistryId, } from './utils/identifiers' import { anonCredsRevocationStatusListFromIndyVdr } from './utils/transform' @@ -270,7 +274,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { options: RegisterCredentialDefinitionOptions ): Promise { try { - // This will throw an error if trying to register a credential defintion with a legacy indy identifier. We only support did:indy + // This will throw an error if trying to register a credential definition with a legacy indy identifier. We only support did:indy // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. const { namespaceIdentifier, namespace } = parseIndyDid(options.credentialDefinition.issuerId) @@ -435,6 +439,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { issuerId: did, revocDefType: response.result.data.revocDefType, value: { + issuanceType: response.result.data.value.issuanceType, maxCredNum: response.result.data.value.maxCredNum, tailsHash: response.result.data.value.tailsHash, tailsLocation: response.result.data.value.tailsLocation, @@ -477,6 +482,112 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationRegistryDefinition( + agentContext: AgentContext, + options: RegisterRevocationRegistryDefinitionOptions + ): Promise { + try { + // This will throw an error if trying to register a revocation registry definition with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { namespaceIdentifier, namespace } = parseIndyDid(options.revocationRegistryDefinition.issuerId) + + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const pool = indyVdrPoolService.getPoolForNamespace(namespace) + agentContext.config.logger.debug( + `Registering revocation registry definition on ledger '${pool.indyNamespace}' with did '${options.revocationRegistryDefinition.issuerId}'`, + options.revocationRegistryDefinition + ) + + // TODO: this will bypass caching if done on a higher level. + const { credentialDefinition, credentialDefinitionMetadata, resolutionMetadata } = + await this.getCredentialDefinition(agentContext, options.revocationRegistryDefinition.credDefId) + + if ( + !credentialDefinition || + !credentialDefinitionMetadata.indyLedgerSeqNo || + typeof credentialDefinitionMetadata.indyLedgerSeqNo !== 'number' + ) { + return { + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + revocationRegistryDefinitionState: { + revocationRegistryDefinition: options.revocationRegistryDefinition, + state: 'failed', + reason: `error resolving credential definition with id ${options.revocationRegistryDefinition.credDefId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, + }, + } + } + + const legacyRevocationRegistryDefinitionId = getLegacyRevocationRegistryId( + options.revocationRegistryDefinition.issuerId, + credentialDefinitionMetadata.indyLedgerSeqNo, + credentialDefinition.tag, + options.revocationRegistryDefinition.tag + ) + const didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryId( + namespace, + namespaceIdentifier, + credentialDefinitionMetadata.indyLedgerSeqNo, + credentialDefinition.tag, + options.revocationRegistryDefinition.tag + ) + + const credentialDefinitionRequest = new RevocationRegistryDefinitionRequest({ + submitterDid: namespaceIdentifier, + revocationRegistryDefinitionV1: { + ver: '1.0', + id: legacyRevocationRegistryDefinitionId, + credDefId: `${credentialDefinitionMetadata.indyLedgerSeqNo}`, + revocDefType: 'CL_ACCUM', + tag: options.revocationRegistryDefinition.tag, + value: options.revocationRegistryDefinition.value, + }, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, options.revocationRegistryDefinition.issuerId) + const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, submitterKey) + agentContext.config.logger.debug( + `Registered revocation registry definition '${didIndyRevocationRegistryDefinitionId}' on ledger '${pool.indyNamespace}'`, + { + response, + credentialDefinition: options.revocationRegistryDefinition, + } + ) + + return { + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition: options.revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering credential definition for credential definition '${options.revocationRegistryDefinition.credDefId}'`, + { + error, + did: options.revocationRegistryDefinition.issuerId, + credentialDefinition: options.revocationRegistryDefinition, + } + ) + + return { + revocationRegistryDefinitionMetadata: {}, + registrationMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition: options.revocationRegistryDefinition, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, From cafe703d09678df687c1eaa8d6bb01c8d06ba440 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 30 Mar 2023 22:04:05 -0300 Subject: [PATCH 02/27] feat: register revocation status list Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 27 +++++++++++ packages/anoncreds/src/AnonCredsApi.ts | 30 ++++++------ .../src/services/AnonCredsIssuerService.ts | 7 +++ .../services/AnonCredsIssuerServiceOptions.ts | 11 +++++ .../registry/RevocationStatusListOptions.ts | 47 +++++++++++++++---- packages/anoncreds/tests/anoncreds.test.ts | 1 + .../services/IndySdkIssuerService.ts | 6 +++ 7 files changed, 105 insertions(+), 24 deletions(-) diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index ec1313d3de..b11c355142 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -13,6 +13,9 @@ import type { CreateRevocationRegistryDefinitionOptions, CreateRevocationRegistryDefinitionReturn, AnonCredsRevocationRegistryDefinition, + CreateRevocationStatusListOptions, + CreateRevocationStatusListReturn, + AnonCredsRevocationStatusList, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared' @@ -26,6 +29,7 @@ import { } from '@aries-framework/anoncreds' import { injectable, AriesFrameworkError } from '@aries-framework/core' import { + RevocationStatusList, RevocationRegistryDefinitionPrivate, RevocationRegistryDefinition, CredentialRevocationConfig, @@ -128,6 +132,29 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } } + public async createRevocationStatusList( + agentContext: AgentContext, + options: CreateRevocationStatusListOptions + ): Promise { + const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, issuanceByDefault } = options + + let revocationStatusList: RevocationStatusList | undefined + try { + revocationStatusList = RevocationStatusList.create({ + issuanceByDefault, + revocationRegistryDefinitionId, + revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, + issuerId, + }) + + return { + revocationStatusList: revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList, + } + } finally { + revocationStatusList?.handle.clear() + } + } + public async createCredentialOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index d42f5c5a0c..9dfcdb845d 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -14,6 +14,7 @@ import type { GetCredentialsOptions, RegisterRevocationRegistryDefinitionReturn, } from './services' +import type { Extensible } from './services/registry/base' import { AgentContext, inject, injectable } from '@aries-framework/core' @@ -226,35 +227,36 @@ export class AnonCredsApi { // TODO: Shall we store in Credential Definition Record the currently used revocation registry id? This can be used when accepting credential request to determine // Which one we'll need to use. It can be also a tag in RevocRegDef Record stating which one is the active one for a given CredDefId. - public async registerCredentialDefinition( - options: AnonCredsRegisterCredentialDefinitionOptions - ): Promise { + public async registerCredentialDefinition(options: { + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions + options: Extensible + }): Promise { const failedReturnBase = { credentialDefinitionState: { state: 'failed' as const, - reason: `Error registering credential definition for issuerId ${options.issuerId}`, + reason: `Error registering credential definition for issuerId ${options.credentialDefinition.issuerId}`, }, registrationMetadata: {}, credentialDefinitionMetadata: {}, } - const registry = this.findRegistryForIdentifier(options.issuerId) + const registry = this.findRegistryForIdentifier(options.credentialDefinition.issuerId) if (!registry) { - failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for issuerId ${options.issuerId}` + failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for issuerId ${options.credentialDefinition.issuerId}` return failedReturnBase } - const schemaRegistry = this.findRegistryForIdentifier(options.schemaId) + const schemaRegistry = this.findRegistryForIdentifier(options.credentialDefinition.schemaId) if (!schemaRegistry) { - failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for schemaId ${options.schemaId}` + 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.schemaId) + const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId) if (!schemaResult.schema) { - failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` + failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.credentialDefinition.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` return failedReturnBase } @@ -262,10 +264,10 @@ export class AnonCredsApi { await this.anonCredsIssuerService.createCredentialDefinition( this.agentContext, { - issuerId: options.issuerId, - schemaId: options.schemaId, - tag: options.tag ?? 'default', - supportRevocation: options.supportRevocation ?? false, + issuerId: options.credentialDefinition.issuerId, + schemaId: options.credentialDefinition.schemaId, + tag: options.credentialDefinition.tag ?? 'default', + supportRevocation: options.credentialDefinition.supportRevocation ?? false, schema: schemaResult.schema, }, // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts index 6edd86f505..8580ffd84f 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerService.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -7,6 +7,8 @@ import type { CreateCredentialDefinitionReturn, CreateRevocationRegistryDefinitionOptions, CreateRevocationRegistryDefinitionReturn, + CreateRevocationStatusListOptions, + CreateRevocationStatusListReturn, } from './AnonCredsIssuerServiceOptions' import type { AnonCredsCredentialOffer } from '../models/exchange' import type { AnonCredsSchema } from '../models/registry' @@ -30,6 +32,11 @@ export interface AnonCredsIssuerService { options: CreateRevocationRegistryDefinitionOptions ): Promise + createRevocationStatusList( + agentContext: AgentContext, + options: CreateRevocationStatusListOptions + ): Promise + createCredentialOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index d9c2cadac0..a589968c06 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -35,6 +35,13 @@ export interface CreateRevocationRegistryDefinitionOptions { tailsDirectoryPath: string } +export interface CreateRevocationStatusListOptions { + issuerId: string + issuanceByDefault: boolean + revocationRegistryDefinitionId: string + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +} + export interface CreateCredentialOfferOptions { credentialDefinitionId: string } @@ -64,3 +71,7 @@ export interface CreateRevocationRegistryDefinitionReturn { revocationRegistryDefinitionPrivate?: Record tailsHash: string } + +export interface CreateRevocationStatusListReturn { + revocationStatusList: AnonCredsRevocationStatusList +} diff --git a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index 6396fe6df0..3741949954 100644 --- a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -1,4 +1,10 @@ -import type { AnonCredsResolutionMetadata, Extensible } from './base' +import type { + AnonCredsOperationState, + AnonCredsOperationStateFailed, + AnonCredsOperationStateFinished, + AnonCredsResolutionMetadata, + Extensible, +} from './base' import type { AnonCredsRevocationStatusList } from '../../models/registry' export interface GetRevocationStatusListReturn { @@ -7,12 +13,33 @@ export interface GetRevocationStatusListReturn { revocationStatusListMetadata: Extensible } -// TODO: Support for issuance of revocable credentials -// export interface RegisterRevocationListOptions { -// // Timestamp is often calculated by the ledger, otherwise method should just take current time -// // Return type does include the timestamp. -// revocationList: Omit -// } -// export interface RegisterRevocationListReturn { -// timestamp: string -// } +export interface RegisterRevocationListOptions { + // Timestamp is often calculated by the ledger, otherwise method should just take current time + // Return type does include the timestamp. + revocationStatusList: Omit +} + +export interface RegisterRevocationStatusListReturnStateFailed extends AnonCredsOperationStateFailed { + revocationStatusList?: AnonCredsRevocationStatusList + timestamp?: string +} + +export interface RegisterRevocationStatusListReturnStateFinished extends AnonCredsOperationStateFinished { + revocationStatusList: AnonCredsRevocationStatusList + timestamp: string +} + +export interface RegisterRevocationStatusListReturnState extends AnonCredsOperationState { + revocationStatusList?: AnonCredsRevocationStatusList + timestamp?: string +} + +export interface RegisterRevocationStatusListReturn { + jobId?: string + revocationStatusListState: + | RegisterRevocationStatusListReturnStateFailed + | RegisterRevocationStatusListReturnStateFinished + | RegisterRevocationStatusListReturnState + revocationStatusListMetadata: Extensible + registrationMetadata: Extensible +} diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index 1f4c32f973..745ddf49d9 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -46,6 +46,7 @@ const existingRevocationRegistryDefinitions = { issuerId: 'VsKV7grR1BUE29mG2Fm2kX', revocDefType: 'CL_ACCUM', value: { + issuanceType: 'ISSUANCE_BY_DEFAULT', publicKeys: { accumKey: { z: 'ab81257c-be63-4051-9e21-c7d384412f64', diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index 5c02cf2437..d07e85df5d 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -10,6 +10,7 @@ import type { AnonCredsSchema, CreateCredentialDefinitionReturn, CreateRevocationRegistryDefinitionReturn, + CreateRevocationStatusListReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' @@ -31,6 +32,11 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { this.indySdk = indySdk } + + public async createRevocationStatusList(): Promise { + throw new AriesFrameworkError('Method not implemented.') + } + public async createRevocationRegistryDefinition(): Promise { throw new AriesFrameworkError('Method not implemented.') } From b70ba0a50eacb5bc7d84e3a001aac9173f7ab8df Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 31 Mar 2023 20:32:59 -0300 Subject: [PATCH 03/27] feat: add RevocationStatusList registration methods in API Signed-off-by: Ariel Gentile --- packages/anoncreds/src/AnonCredsApi.ts | 109 +++++++++++++++++- packages/anoncreds/src/AnonCredsApiOptions.ts | 9 +- .../anoncreds/src/AnonCredsModuleConfig.ts | 22 ++++ ...vocationRegistryDefinitionPrivateRecord.ts | 13 +++ ...tionRegistryDefinitionPrivateRepository.ts | 9 ++ ...CredsRevocationRegistryDefinitionRecord.ts | 6 +- ...sRevocationRegistryDefinitionRepository.ts | 4 + .../AnonCredsRevocationStatusListRecord.ts | 46 ++++++++ ...AnonCredsRevocationStatusListRepository.ts | 43 +++++++ ...nRegistryDefinitionRecordMetadataTypes.ts} | 0 packages/anoncreds/src/repository/index.ts | 2 + .../services/registry/AnonCredsRegistry.ts | 15 ++- .../registry/RevocationStatusListOptions.ts | 1 + .../tests/InMemoryAnonCredsRegistry.ts | 24 ++++ .../services/IndySdkAnonCredsRegistry.ts | 10 +- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 10 ++ 16 files changed, 307 insertions(+), 16 deletions(-) create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationStatusListRepository.ts rename packages/anoncreds/src/repository/{anonCredsRevocationRegistryDefinitionRecordMetadataTypes copy.ts => anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts} (100%) diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 3466714b71..cf4a60fd5d 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -2,6 +2,7 @@ import type { AnonCredsCreateLinkSecretOptions, AnonCredsRegisterCredentialDefinitionOptions, AnonCredsRegisterRevocationRegistryDefinitionOptions, + AnonCredsRegisterRevocationStatusListOptions, } from './AnonCredsApiOptions' import type { GetCredentialDefinitionReturn, @@ -14,6 +15,7 @@ import type { AnonCredsRegistry, GetCredentialsOptions, RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, } from './services' import type { Extensible } from './services/registry/base' import type { SimpleQuery } from '@aries-framework/core' @@ -33,15 +35,15 @@ import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository, AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationStatusListRecord, + AnonCredsRevocationStatusListRepository, } from './repository' import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRecord } from './repository/AnonCredsSchemaRecord' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' -import { - AnonCredsCredentialDefinitionRecordMetadataKeys, - AnonCredsCredentialDefinitionRecordMetadataKeys as AnonCredsRevocationRegistryDefinitionRecordMetadataKeys, -} from './repository/anonCredsCredentialDefinitionRecordMetadataTypes' +import { AnonCredsCredentialDefinitionRecordMetadataKeys } from './repository/anonCredsCredentialDefinitionRecordMetadataTypes' +import { AnonCredsRevocationRegistryDefinitionRecordMetadataKeys } from './repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes' import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, @@ -61,6 +63,7 @@ export class AnonCredsApi { private anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository private anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository private anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository + private anonCredsRevocationStatusListRepository: AnonCredsRevocationStatusListRepository private anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository private anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository private anonCredsIssuerService: AnonCredsIssuerService @@ -73,6 +76,7 @@ export class AnonCredsApi { @inject(AnonCredsIssuerServiceSymbol) anonCredsIssuerService: AnonCredsIssuerService, @inject(AnonCredsHolderServiceSymbol) anonCredsHolderService: AnonCredsHolderService, anonCredsSchemaRepository: AnonCredsSchemaRepository, + anonCredsRevocationStatusListRepository: AnonCredsRevocationStatusListRepository, anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository, anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository, anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository, @@ -88,6 +92,7 @@ export class AnonCredsApi { this.anonCredsSchemaRepository = anonCredsSchemaRepository this.anonCredsRevocationRegistryDefinitionRepository = anonCredsRevocationRegistryDefinitionRepository this.anonCredsRevocationRegistryDefinitionPrivateRepository = anonCredsRevocationRegistryDefinitionPrivateRepository + this.anonCredsRevocationStatusListRepository = anonCredsRevocationStatusListRepository this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository this.anonCredsCredentialDefinitionPrivateRepository = anonCredsCredentialDefinitionPrivateRepository this.anonCredsKeyCorrectnessProofRepository = anonCredsKeyCorrectnessProofRepository @@ -289,6 +294,32 @@ export class AnonCredsApi { await this.storeCredentialDefinitionRecord(registry, result, credentialDefinitionPrivate, keyCorrectnessProof) + // If it supports revocation, create the first revocation registry definition + if (options.credentialDefinition.supportRevocation && result.credentialDefinitionState.credentialDefinitionId) { + const { revocationRegistryDefinitionState } = await this.registerRevocationRegistryDefinition({ + credentialDefinition, + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + issuerId: options.credentialDefinition.issuerId, + maximumCredentialNumber: this.config.maximumCredentialNumberPerRevocationRegistry, + tailsDirectoryPath: + this.config.tailsDirectoryPath ?? new this.agentContext.config.agentDependencies.FileSystem().dataPath, + tag: '', // TODO: define tags + }) + + if ( + revocationRegistryDefinitionState.revocationRegistryDefinition && + revocationRegistryDefinitionState.revocationRegistryDefinitionId + ) { + // Create and register an initial revocation status list + await this.registerRevocationStatusList({ + issuanceByDefault: true, + issuerId: options.credentialDefinition.issuerId, + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + }) + } + } + return result } catch (error) { // Storage failed @@ -427,6 +458,54 @@ export class AnonCredsApi { } } + public async registerRevocationStatusList( + options: AnonCredsRegisterRevocationStatusListOptions + ): Promise { + const { issuanceByDefault, issuerId, revocationRegistryDefinition, revocationRegistryDefinitionId } = options + + const failedReturnBase = { + revocationStatusListState: { + state: 'failed' as const, + reason: `Error registering revocation status list for issuerId ${options.issuerId}`, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(options.issuerId) + if (!registry) { + failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No registry found for issuerId ${options.issuerId}` + return failedReturnBase + } + + try { + const { revocationStatusList } = await this.anonCredsIssuerService.createRevocationStatusList(this.agentContext, { + issuanceByDefault, + issuerId, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + }) + + const result = await registry.registerRevocationStatusList(this.agentContext, { + revocationStatusList, + options: {}, + }) + + await this.storeRevocationStatusListRecord(result, options.revocationRegistryDefinition.credDefId) + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationStatusListState.reason = `Error storing revocation status list records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationStatusListState.reason = `Error registering revocation status list: ${error.message}` + return failedReturnBase + } + } + public async getCredential(credentialId: string) { return this.anonCredsHolderService.getCredential(this.agentContext, { credentialId }) } @@ -458,11 +537,11 @@ export class AnonCredsApi { // the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions // are stored in the metadata revocationRegistryDefinitionRecord.metadata.set( - AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.CredentialDefinitionMetadata, + AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionMetadata, result.revocationRegistryDefinitionMetadata ) revocationRegistryDefinitionRecord.metadata.set( - AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata, + AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionRegistrationMetadata, result.registrationMetadata ) @@ -488,6 +567,24 @@ export class AnonCredsApi { } } + private async storeRevocationStatusListRecord( + result: RegisterRevocationStatusListReturn, + credentialDefinitionId: string + ): Promise { + try { + if (result.revocationStatusListState.revocationStatusList && result.revocationStatusListState.timestamp) { + const revocationStatusListRecord = new AnonCredsRevocationStatusListRecord({ + revocationStatusList: result.revocationStatusListState.revocationStatusList, + credentialDefinitionId, + }) + + await this.anonCredsRevocationStatusListRepository.save(this.agentContext, revocationStatusListRecord) + } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing revocation status list record`, { cause: error }) + } + } + private async storeCredentialDefinitionRecord( registry: AnonCredsRegistry, result: RegisterCredentialDefinitionReturn, diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index 76b8b52c6f..e665e754f0 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -1,4 +1,4 @@ -import type { AnonCredsCredentialDefinition } from './models' +import type { AnonCredsCredentialDefinition, AnonCredsRevocationRegistryDefinition } from './models' export interface AnonCredsCreateLinkSecretOptions { linkSecretId?: string @@ -20,3 +20,10 @@ export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { tailsDirectoryPath: string maximumCredentialNumber: number } + +export interface AnonCredsRegisterRevocationStatusListOptions { + issuerId: string + issuanceByDefault: true + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId: string +} diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts index 9f7b971aab..df3229d791 100644 --- a/packages/anoncreds/src/AnonCredsModuleConfig.ts +++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts @@ -9,6 +9,18 @@ export interface AnonCredsModuleConfigOptions { * A list of AnonCreds registries to make available to the AnonCreds module. */ registries: [AnonCredsRegistry, ...AnonCredsRegistry[]] + + /** + * Maximum credential number per revocation registry + * @default 1000 + */ + maximumCredentialNumberPerRevocationRegistry?: number + + /** + * Maximum credential number per revocation registry + * @default agent's data path + */ + tailsDirectoryPath?: string } /** @@ -25,4 +37,14 @@ export class AnonCredsModuleConfig { public get registries() { return this.options.registries } + + /** See {@link AnonCredsModuleConfigOptions.maximumCredentialNumberPerRevocationRegistry} */ + public get maximumCredentialNumberPerRevocationRegistry() { + return this.options.maximumCredentialNumberPerRevocationRegistry ?? 1000 + } + + /** See {@link AnonCredsModuleConfigOptions.tailsDirectoryPath} */ + public get tailsDirectoryPath() { + return this.options.tailsDirectoryPath + } } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts index ad2ed6c525..c1ff4a733b 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts @@ -2,6 +2,13 @@ import type { TagsBase } from '@aries-framework/core' import { BaseRecord, utils } from '@aries-framework/core' +export enum RevocationRegistryState { + Created = 'created', + Published = 'published', + Active = 'active', + Full = 'full', +} + export interface AnonCredsRevocationRegistryDefinitionPrivateRecordProps { id?: string revocationRegistryDefinitionId: string @@ -11,6 +18,7 @@ export interface AnonCredsRevocationRegistryDefinitionPrivateRecordProps { export type DefaultAnonCredsRevocationRegistryPrivateTags = { revocationRegistryDefinitionId: string + state: RevocationRegistryState } export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseRecord< @@ -24,6 +32,9 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco public readonly value!: Record // TODO: Define structure public currentIndex!: number + + public state!: RevocationRegistryState + public constructor(props: AnonCredsRevocationRegistryDefinitionPrivateRecordProps) { super() @@ -32,6 +43,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId this.value = props.value this.currentIndex = props.index ?? 0 + this.state = RevocationRegistryState.Created } } @@ -39,6 +51,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco return { ...this._tags, revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + state: this.state, } } } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts index 3dfff98758..6bb4523db9 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts @@ -1,3 +1,4 @@ +import type { RevocationRegistryState } from './AnonCredsRevocationRegistryDefinitionPrivateRecord' import type { AgentContext } from '@aries-framework/core' import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' @@ -24,4 +25,12 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRepository extends Repo ) { return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId }) } + + public async findAllByCredentialDefinitionIdAndState( + agentContext: AgentContext, + credentialDefinitionId: string, + state?: RevocationRegistryState + ) { + return this.findByQuery(agentContext, { credentialDefinitionId, state }) + } } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts index 1971661a0c..a99a5ef611 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts @@ -1,3 +1,4 @@ +import type { AnonCredsRevocationRegistryDefinitionRecordMetadata } from './anonCredsRevocationRegistryDefinitionRecordMetadataTypes' import type { AnonCredsRevocationRegistryDefinition } from '../models' import type { TagsBase } from '@aries-framework/core' @@ -12,11 +13,13 @@ export interface AnonCredsRevocationRegistryDefinitionRecordProps { export type DefaultAnonCredsRevocationRegistryDefinitionTags = { revocationRegistryDefinitionId: string + credentialDefinitionId: string } export class AnonCredsRevocationRegistryDefinitionRecord extends BaseRecord< DefaultAnonCredsRevocationRegistryDefinitionTags, - TagsBase + TagsBase, + AnonCredsRevocationRegistryDefinitionRecordMetadata > { public static readonly type = 'AnonCredsRevocationRegistryDefinitionRecord' public readonly type = AnonCredsRevocationRegistryDefinitionRecord.type @@ -40,6 +43,7 @@ export class AnonCredsRevocationRegistryDefinitionRecord extends BaseRecord< return { ...this._tags, revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + credentialDefinitionId: this.revocationRegistryDefinition.credDefId, } } } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts index 78222f0e63..4b1890b09b 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts @@ -24,4 +24,8 @@ export class AnonCredsRevocationRegistryDefinitionRepository extends Repository< ) { return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId }) } + + public async findAllByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findByQuery(agentContext, { credentialDefinitionId }) + } } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts new file mode 100644 index 0000000000..d0bb4727e4 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts @@ -0,0 +1,46 @@ +import type { AnonCredsRevocationStatusList } from '../models' +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsRevocationStatusListRecordProps { + id?: string + credentialDefinitionId: string + revocationStatusList: AnonCredsRevocationStatusList +} + +export type DefaultAnonCredsRevocationStatusListTags = { + revocationRegistryDefinitionId: string + credentialDefinitionId: string + timestamp: string +} + +export class AnonCredsRevocationStatusListRecord extends BaseRecord< + DefaultAnonCredsRevocationStatusListTags, + TagsBase +> { + public static readonly type = 'AnonCredsRevocationStatusListRecord' + public readonly type = AnonCredsRevocationStatusListRecord.type + + public readonly credentialDefinitionId!: string + public readonly revocationStatusList!: AnonCredsRevocationStatusList + + public constructor(props: AnonCredsRevocationStatusListRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.revocationStatusList = props.revocationStatusList + } + } + + public getTags() { + return { + ...this._tags, + revocationRegistryDefinitionId: this.revocationStatusList.revRegId, + credentialDefinitionId: this.credentialDefinitionId, + timestamp: this.revocationStatusList.timestamp.toString(), + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRepository.ts new file mode 100644 index 0000000000..010f059dd9 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRepository.ts @@ -0,0 +1,43 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsRevocationStatusListRecord } from './AnonCredsRevocationStatusListRecord' + +@injectable() +export class AnonCredsRevocationStatusListRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) + storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsRevocationStatusListRecord, storageService, eventEmitter) + } + + public async getByRevocationRegistryDefinitionIdAndTimestamp( + agentContext: AgentContext, + revocationRegistryDefinitionId: string, + timestamp: string + ) { + return this.getSingleByQuery(agentContext, { revocationRegistryDefinitionId, timestamp }) + } + + public async findByRevocationRegistryDefinitionIdAndTimestamp( + agentContext: AgentContext, + revocationRegistryDefinitionId: string, + timestamp: string + ) { + return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId, timestamp }) + } + + public async findAllByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findByQuery(agentContext, { credentialDefinitionId }) + } + + public async findAllByRevocationRegistryDefinitionId( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ) { + return this.findByQuery(agentContext, { revocationRegistryDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes copy.ts b/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts similarity index 100% rename from packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes copy.ts rename to packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts index 8772b528e7..a87ac8b2a9 100644 --- a/packages/anoncreds/src/repository/index.ts +++ b/packages/anoncreds/src/repository/index.ts @@ -12,5 +12,7 @@ export * from './AnonCredsRevocationRegistryDefinitionRecord' export * from './AnonCredsRevocationRegistryDefinitionRepository' export * from './AnonCredsRevocationRegistryDefinitionPrivateRecord' export * from './AnonCredsRevocationRegistryDefinitionPrivateRepository' +export * from './AnonCredsRevocationStatusListRecord' +export * from './AnonCredsRevocationStatusListRepository' export * from './AnonCredsSchemaRecord' export * from './AnonCredsSchemaRepository' diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts index 2731f3c889..8305af35b2 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts @@ -8,7 +8,11 @@ import type { RegisterRevocationRegistryDefinitionOptions, RegisterRevocationRegistryDefinitionReturn, } from './RevocationRegistryDefinitionOptions' -import type { GetRevocationStatusListReturn } from './RevocationStatusListOptions' +import type { + GetRevocationStatusListReturn, + RegisterRevocationListOptions as RegisterRevocationStatusListOptions, + RegisterRevocationStatusListReturn, +} from './RevocationStatusListOptions' import type { GetSchemaReturn, RegisterSchemaOptions, RegisterSchemaReturn } from './SchemaOptions' import type { AgentContext } from '@aries-framework/core' @@ -53,9 +57,8 @@ export interface AnonCredsRegistry { timestamp: number ): Promise - // TODO: issuance of revocable credentials - // registerRevocationList( - // agentContext: AgentContext, - // options: RegisterRevocationListOptions - // ): Promise + registerRevocationStatusList( + agentContext: AgentContext, + options: RegisterRevocationStatusListOptions + ): Promise } diff --git a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index 3741949954..c79201dbfd 100644 --- a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -17,6 +17,7 @@ export interface RegisterRevocationListOptions { // Timestamp is often calculated by the ledger, otherwise method should just take current time // Return type does include the timestamp. revocationStatusList: Omit + options: Extensible } export interface RegisterRevocationStatusListReturnStateFailed extends AnonCredsOperationStateFailed { diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 83c47e0bcc..0503c5ca72 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -15,6 +15,8 @@ import type { AnonCredsCredentialDefinition, RegisterRevocationRegistryDefinitionOptions, RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, + RegisterRevocationListOptions as RegisterRevocationStatusListOptions, } from '../src' import type { AgentContext } from '@aries-framework/core' @@ -322,6 +324,28 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { revocationStatusListMetadata: {}, } } + + public async registerRevocationStatusList( + agentContext: AgentContext, + options: RegisterRevocationStatusListOptions + ): Promise { + const timestamp = (options.options.timestamp as number) ?? new Date().getTime() + const revocationStatusList = { + ...options.revocationStatusList, + timestamp, + } satisfies AnonCredsRevocationStatusList + this.revocationStatusLists[options.revocationStatusList.revRegId][timestamp.toString()] = revocationStatusList + + return { + registrationMetadata: {}, + revocationStatusListMetadata: {}, + revocationStatusListState: { + state: 'finished', + revocationStatusList, + timestamp: timestamp.toString(), + }, + } + } } /** diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index 9a5d240805..9460488a1e 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -10,12 +10,14 @@ import type { RegisterCredentialDefinitionReturn, RegisterSchemaOptions, RegisterSchemaReturn, - RegisterRevocationRegistryDefinitionOptions, RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { Schema as IndySdkSchema } from 'indy-sdk' +import { AriesFrameworkError } from '@aries-framework/core' + import { parseIndyDid, verificationKeyForIndyDid } from '../../dids/didIndyUtil' import { IndySdkError, isIndyError } from '../../error' import { IndySdkPoolService } from '../../ledger' @@ -468,7 +470,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } public async registerRevocationRegistryDefinition(): Promise { - throw new Error('Method not implemented.') + throw new AriesFrameworkError('Not implemented!') } public async getRevocationStatusList( @@ -572,6 +574,10 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationStatusList(): Promise { + throw new AriesFrameworkError('Not implemented!') + } + private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, pool: IndySdkPool, seqNo: number) { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index af5734ac2e..8ad52fe058 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -11,9 +11,12 @@ import type { AnonCredsRevocationRegistryDefinition, RegisterRevocationRegistryDefinitionOptions, RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationListOptions, + RegisterRevocationStatusListReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' +import { AriesFrameworkError } from '@aries-framework/core' import { GetSchemaRequest, SchemaRequest, @@ -694,6 +697,13 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationStatusList( + agentContext: AgentContext, + options: RegisterRevocationListOptions + ): Promise { + throw new AriesFrameworkError('Not implemented!') + } + private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, seqNo: number, did: string) { const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) From ea889040802ea27667c93161bc87c6ccb782f4b4 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 31 Mar 2023 23:28:55 -0300 Subject: [PATCH 04/27] feat: add revocation parameters when issuing credential Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 11 ++-- packages/anoncreds/src/AnonCredsApi.ts | 13 +++- .../AnonCredsCredentialFormatService.ts | 60 ++++++++++++++++++- ...vocationRegistryDefinitionPrivateRecord.ts | 4 +- ...CredsRevocationRegistryDefinitionRecord.ts | 2 + 5 files changed, 81 insertions(+), 9 deletions(-) diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index b11c355142..81f2a65a45 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -1,4 +1,4 @@ -import type { +import { AnonCredsIssuerService, CreateCredentialDefinitionOptions, CreateCredentialOfferOptions, @@ -16,6 +16,7 @@ import type { CreateRevocationStatusListOptions, CreateRevocationStatusListReturn, AnonCredsRevocationStatusList, + RevocationRegistryState, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared' @@ -204,7 +205,9 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { (e) => e !== undefined ) if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 3) { - throw new AriesFrameworkError('Revocation requires all of revocationRegistryDefinitionId and tailsFilePath') + throw new AriesFrameworkError( + 'Revocation requires all of revocationRegistryDefinitionId, revocationStatusList and tailsFilePath' + ) } let credential: Credential | undefined @@ -237,8 +240,8 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { const registryIndex = revocationRegistryDefinitionPrivateRecord.currentIndex + 1 - if (registryIndex > revocationRegistryDefinitionRecord.revocationRegistryDefinition.value.maxCredNum) { - throw new AriesFrameworkError('Revocation registry has reached maximum credential number') + if (registryIndex >= revocationRegistryDefinitionRecord.revocationRegistryDefinition.value.maxCredNum) { + revocationRegistryDefinitionPrivateRecord.state = RevocationRegistryState.Full } // Update current registry index in storage diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index cf4a60fd5d..9dc3cc25af 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -37,6 +37,7 @@ import { AnonCredsRevocationRegistryDefinitionRecord, AnonCredsRevocationStatusListRecord, AnonCredsRevocationStatusListRepository, + RevocationRegistryState, } from './repository' import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' @@ -366,12 +367,19 @@ export class AnonCredsApi { tailsDirectoryPath, }) + const localTailsFilePath = `${tailsDirectoryPath}/${tailsHash}}` + const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { revocationRegistryDefinition, options: {}, }) - await this.storeRevocationRegistryDefinitionRecord(result, tailsHash, revocationRegistryDefinitionPrivate) + await this.storeRevocationRegistryDefinitionRecord( + result, + localTailsFilePath, + tailsHash, + revocationRegistryDefinitionPrivate + ) return result } catch (error) { @@ -516,6 +524,7 @@ export class AnonCredsApi { private async storeRevocationRegistryDefinitionRecord( result: RegisterRevocationRegistryDefinitionReturn, + localTailsFilePath: string, tailsHash: string, revocationRegistryDefinitionPrivate?: Record ): Promise { @@ -530,6 +539,7 @@ export class AnonCredsApi { const revocationRegistryDefinitionRecord = new AnonCredsRevocationRegistryDefinitionRecord({ revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, revocationRegistryDefinition: result.revocationRegistryDefinitionState.revocationRegistryDefinition, + localTailsFilePath, tailsHash, }) @@ -555,6 +565,7 @@ export class AnonCredsApi { const revocationRegistryDefinitionPrivateRecord = new AnonCredsRevocationRegistryDefinitionPrivateRecord({ revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, value: revocationRegistryDefinitionPrivate, + state: RevocationRegistryState.Active, }) await this.anonCredsRevocationRegistryDefinitionPrivateRepository.save( this.agentContext, diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index d3305f2a74..52c48055e2 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -53,6 +53,12 @@ import { createAndLinkAttachmentsToPreview, } from '../utils/credential' import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, + RevocationRegistryState, +} from '../repository' const ANONCREDS_CREDENTIAL_OFFER = 'anoncreds/credential-offer@v1.0' const ANONCREDS_CREDENTIAL_REQUEST = 'anoncreds/credential-request@v1.0' @@ -303,13 +309,63 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const credentialRequest = requestAttachment.getDataAsJson() if (!credentialRequest) throw new AriesFrameworkError('Missing anoncreds credential request in createCredential') - // TODO: Check if credential definition supports revocation, fetch current revocation registry id and tailsFilePath, - // and current revocation status list + // We check locally for credential definition info. If it supports revocation, we need to search locally for + // an active revocation registry + const credentialDefinition = ( + await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, credentialRequest.cred_def_id) + ).credentialDefinition.value + + let revocationRegistryDefinitionId + let revocationStatusList + let tailsFilePath + + if (credentialDefinition.revocation) { + const [revocationRegistryDefinitionPrivateRecord] = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .findAllByCredentialDefinitionIdAndState( + agentContext, + credentialRequest.cred_def_id, + RevocationRegistryState.Active + ) + + if (!revocationRegistryDefinitionPrivateRecord) { + throw new AriesFrameworkError(`No available revocation registry found for ${credentialRequest.cred_def_id}`) + } + + revocationRegistryDefinitionId = revocationRegistryDefinitionPrivateRecord.revocationRegistryDefinitionId + + // get current revocation status list + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + const registry = registryService.getRegistryForIdentifier(agentContext, credentialRequest.cred_def_id) + const result = await registry.getRevocationStatusList( + agentContext, + revocationRegistryDefinitionId, + new Date().getTime() + ) + + if (!result.revocationStatusList) { + throw new AriesFrameworkError( + `Could not get current revocation status list for ${revocationRegistryDefinitionId}` + ) + } + revocationStatusList = result.revocationStatusList + + const revocationRegistryDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + tailsFilePath = revocationRegistryDefinitionRecord.localTailsFilePath + } const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer, credentialRequest, credentialValues: convertAttributesToCredentialValues(credentialAttributes), + revocationRegistryDefinitionId, + revocationStatusList, + tailsFilePath, }) if (credential.rev_reg_id) { diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts index c1ff4a733b..7f9c7d194a 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts @@ -4,7 +4,6 @@ import { BaseRecord, utils } from '@aries-framework/core' export enum RevocationRegistryState { Created = 'created', - Published = 'published', Active = 'active', Full = 'full', } @@ -14,6 +13,7 @@ export interface AnonCredsRevocationRegistryDefinitionPrivateRecordProps { revocationRegistryDefinitionId: string value: Record index?: number + state?: RevocationRegistryState } export type DefaultAnonCredsRevocationRegistryPrivateTags = { @@ -43,7 +43,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId this.value = props.value this.currentIndex = props.index ?? 0 - this.state = RevocationRegistryState.Created + this.state = props.state ?? RevocationRegistryState.Created } } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts index a99a5ef611..231a3fb045 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts @@ -8,6 +8,7 @@ export interface AnonCredsRevocationRegistryDefinitionRecordProps { id?: string revocationRegistryDefinitionId: string revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + localTailsFilePath: string tailsHash: string } @@ -26,6 +27,7 @@ export class AnonCredsRevocationRegistryDefinitionRecord extends BaseRecord< public readonly revocationRegistryDefinitionId!: string public readonly revocationRegistryDefinition!: AnonCredsRevocationRegistryDefinition + public readonly localTailsFilePath!: string public readonly tailsHash!: string public constructor(props: AnonCredsRevocationRegistryDefinitionRecordProps) { From 1cba24c953b0e3b31dfaee04d6e6f1209d1d3da5 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 1 Apr 2023 10:38:32 -0300 Subject: [PATCH 05/27] test: add anoncreds revocation test Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 7 +- .../anoncreds-rs/tests/anoncreds-flow.test.ts | 359 +++++++++++++++++- packages/anoncreds/src/AnonCredsApi.ts | 2 +- .../src/services/AnonCredsIssuerService.ts | 5 +- .../services/AnonCredsIssuerServiceOptions.ts | 3 - 5 files changed, 360 insertions(+), 16 deletions(-) diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index 81f2a65a45..deee69a749 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -14,7 +14,6 @@ import { CreateRevocationRegistryDefinitionReturn, AnonCredsRevocationRegistryDefinition, CreateRevocationStatusListOptions, - CreateRevocationStatusListReturn, AnonCredsRevocationStatusList, RevocationRegistryState, } from '@aries-framework/anoncreds' @@ -136,7 +135,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { public async createRevocationStatusList( agentContext: AgentContext, options: CreateRevocationStatusListOptions - ): Promise { + ): Promise { const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, issuanceByDefault } = options let revocationStatusList: RevocationStatusList | undefined @@ -148,9 +147,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { issuerId, }) - return { - revocationStatusList: revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList, - } + return revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList } finally { revocationStatusList?.handle.clear() } diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 26620aa6d2..7cc45be398 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -1,5 +1,5 @@ -import type { AnonCredsCredentialRequest } from '@aries-framework/anoncreds' -import type { Wallet } from '@aries-framework/core' +import { AnonCredsCredentialRequest, AnonCredsRevocationRegistryDefinitionPrivateRecord, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryDefinitionRecord, AnonCredsRevocationRegistryDefinitionRepository, AnonCredsRevocationStatusListRecord, AnonCredsRevocationStatusListRepository, RevocationRegistryState } from '@aries-framework/anoncreds' +import { ConsoleLogger, LogLevel, Wallet } from '@aries-framework/core' import { AnonCredsModuleConfig, @@ -43,7 +43,7 @@ const anonCredsModuleConfig = new AnonCredsModuleConfig({ registries: [registry], }) -const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs', { logger: new ConsoleLogger(LogLevel.debug)}) const anonCredsVerifierService = new AnonCredsRsVerifierService() const anonCredsHolderService = new AnonCredsRsHolderService() const anonCredsIssuerService = new AnonCredsRsIssuerService() @@ -358,4 +358,355 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( expect(isValid).toBe(true) }) -}) + + test.only('issuance and verification flow starting from proposal without negotiation and with revocation', async () => { + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId: indyDid, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + if ( + !schemaState.schema || + !schemaState.schemaId + ) { + throw new Error('Failed to create schema') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', + }) + ) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId: indyDid, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: true, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if ( + !credentialDefinitionState.credentialDefinition || + !credentialDefinitionState.credentialDefinitionId + ) { + throw new Error('Failed to create credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + const { revocationRegistryDefinition, tailsHash, revocationRegistryDefinitionPrivate } = + await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { + issuerId: indyDid, + credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + maximumCredentialNumber: 100, + tailsDirectoryPath: new agentDependencies.FileSystem().dataPath, + tag: 'default' + }) + + const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { + revocationRegistryDefinition, + options: {}, + }) + + if ( + !revocationRegistryDefinitionState.revocationRegistryDefinition || + !revocationRegistryDefinitionState.revocationRegistryDefinitionId || + !revocationRegistryDefinitionPrivate + ) { + throw new Error('Failed to create revocation registry') + } + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionRecord({ + tailsHash, + localTailsFilePath: `${new agentDependencies.FileSystem().dataPath}/${tailsHash}`, + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + state: RevocationRegistryState.Active, + value: revocationRegistryDefinitionPrivate, + revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + }) + ) + + const revocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { + issuanceByDefault: true, + issuerId: indyDid, + revocationRegistryDefinition, + revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId + }) + + const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { + revocationStatusList, + options: {}, + }) + + if ( + !revocationStatusListState.revocationStatusList || + !revocationStatusListState.timestamp + ) { + throw new Error('Failed to create revocation status list') + } + + await agentContext.dependencyManager.resolve(AnonCredsRevocationStatusListRepository).save( + agentContext, + new AnonCredsRevocationStatusListRecord({ + credentialDefinitionId: revocationRegistryDefinition.credDefId, + revocationStatusList, + }) + ) + + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) + expect(linkSecret.linkSecretId).toBe('linkSecretId') + + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, + }) + ) + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ + name: 'name', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + value: '25', + }), + ] + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + anoncreds: { + attributes: credentialAttributes, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }, + }) + + // Issuer processes and accepts proposal + await anoncredsCredentialFormatService.processProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: proposalAttachment, + }) + // Set attributes on the credential record, this is normally done by the protocol service + issuerCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + proposalAttachment: proposalAttachment, + }) + + // Holder processes and accepts offer + await anoncredsCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + anoncreds: { + linkSecretId: linkSecret.linkSecretId, + }, + }, + }) + + // Make sure the request contains an entropy and does not contain a prover_did field + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() + + // Issuer processes and accepts request + await anoncredsCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + }) + + // Holder processes and accepts credential + await anoncredsCredentialFormatService.processCredential(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, + ]) + + const credentialId = holderCredentialRecord.credentials[0].credentialRecordId + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + credentialId, + }) + + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: '25', + name: 'John', + }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: null, + credentialRevocationId: undefined, // FIXME: should be null? + methodName: 'inMemory', + }) + + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + '_anoncreds/credentialRequest': { + master_secret_blinding_data: expect.any(Object), + master_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }) + + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalReceived, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + + const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { + proofFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + }, + }, + proofRecord: holderProofRecord, + }) + + await anoncredsProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) + + const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) + + await anoncredsProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) + + const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) + + const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, + }) + + expect(isValid).toBe(true) + }) +}) \ No newline at end of file diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 9dc3cc25af..26f684c134 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -487,7 +487,7 @@ export class AnonCredsApi { } try { - const { revocationStatusList } = await this.anonCredsIssuerService.createRevocationStatusList(this.agentContext, { + const revocationStatusList = await this.anonCredsIssuerService.createRevocationStatusList(this.agentContext, { issuanceByDefault, issuerId, revocationRegistryDefinition, diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts index 8580ffd84f..c7be1ec8b6 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerService.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -8,10 +8,9 @@ import type { CreateRevocationRegistryDefinitionOptions, CreateRevocationRegistryDefinitionReturn, CreateRevocationStatusListOptions, - CreateRevocationStatusListReturn, } from './AnonCredsIssuerServiceOptions' import type { AnonCredsCredentialOffer } from '../models/exchange' -import type { AnonCredsSchema } from '../models/registry' +import type { AnonCredsRevocationStatusList, AnonCredsSchema } from '../models/registry' import type { AgentContext } from '@aries-framework/core' export const AnonCredsIssuerServiceSymbol = Symbol('AnonCredsIssuerService') @@ -35,7 +34,7 @@ export interface AnonCredsIssuerService { createRevocationStatusList( agentContext: AgentContext, options: CreateRevocationStatusListOptions - ): Promise + ): Promise createCredentialOffer( agentContext: AgentContext, diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index a589968c06..6d8b3aa73b 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -72,6 +72,3 @@ export interface CreateRevocationRegistryDefinitionReturn { tailsHash: string } -export interface CreateRevocationStatusListReturn { - revocationStatusList: AnonCredsRevocationStatusList -} From 429da812dd1cd67b28079bd907c4d3935d5c4151 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sun, 2 Apr 2023 22:28:26 -0300 Subject: [PATCH 06/27] various fixes (WIP) Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 38 ++++++++++++++++++- .../anoncreds-rs/tests/anoncreds-flow.test.ts | 35 ++++++++++++----- packages/anoncreds/src/AnonCredsApi.ts | 11 +++--- .../AnonCredsCredentialFormatService.ts | 7 +++- .../src/formats/AnonCredsProofFormat.ts | 1 + .../formats/AnonCredsProofFormatService.ts | 1 + packages/anoncreds/src/models/registry.ts | 2 +- ...vocationRegistryDefinitionPrivateRecord.ts | 5 +++ ...CredsRevocationRegistryDefinitionRecord.ts | 5 --- .../services/AnonCredsIssuerServiceOptions.ts | 10 ++++- .../src/utils/createRequestFromPreview.ts | 9 ++++- .../tests/InMemoryAnonCredsRegistry.ts | 23 +++++++++-- 12 files changed, 117 insertions(+), 30 deletions(-) diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index deee69a749..1e1d9003cc 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -16,6 +16,7 @@ import { CreateRevocationStatusListOptions, AnonCredsRevocationStatusList, RevocationRegistryState, + UpdateRevocationStatusListOptions, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared' @@ -123,7 +124,6 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { return { revocationRegistryDefinition: createReturnObj.revocationRegistryDefinition.toJson() as unknown as AnonCredsRevocationRegistryDefinition, - tailsHash: createReturnObj.revocationRegistryDefinition.getTailsHash(), revocationRegistryDefinitionPrivate: createReturnObj.revocationRegistryDefinitionPrivate.toJson(), } } finally { @@ -138,6 +138,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { ): Promise { const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, issuanceByDefault } = options + console.log(`revocation list: ${options.revocationRegistryDefinition.value.tailsLocation}`) let revocationStatusList: RevocationStatusList | undefined try { revocationStatusList = RevocationStatusList.create({ @@ -145,6 +146,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { revocationRegistryDefinitionId, revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, issuerId, + timestamp: new Date().getTime() // TODO: fix optional field issue in anoncreds-rs }) return revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList @@ -153,6 +155,40 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } } + public async updateRevocationStatusList( + agentContext: AgentContext, + options: UpdateRevocationStatusListOptions + ): Promise { + const { revocationStatusList, revocationRegistryDefinition, issued, revoked, timestamp } = options + + let updatedRevocationStatusList: RevocationStatusList | undefined + let revocationRegistryDefinitionObj: RevocationRegistryDefinition | undefined + + try { + updatedRevocationStatusList = RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject) + + if (timestamp && !issued && !revoked) { + updatedRevocationStatusList.updateTimestamp({ + timestamp + }) + } else { + revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson(revocationRegistryDefinition as unknown as JsonObject) + updatedRevocationStatusList.update({ + // TODO: Fix parameters in anoncreds-rs + revocationRegstryDefinition: revocationRegistryDefinitionObj, + issued, + revoked, + timestamp + }) + } + + return updatedRevocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList + } finally { + updatedRevocationStatusList?.handle.clear() + revocationRegistryDefinitionObj?.handle.clear() + } + } + public async createCredentialOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 7cb7228c42..a437cb9dd0 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -1,5 +1,5 @@ import { AnonCredsCredentialRequest, AnonCredsRevocationRegistryDefinitionPrivateRecord, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryDefinitionRecord, AnonCredsRevocationRegistryDefinitionRepository, AnonCredsRevocationStatusListRecord, AnonCredsRevocationStatusListRepository, RevocationRegistryState } from '@aries-framework/anoncreds' -import { ConsoleLogger, LogLevel, Wallet } from '@aries-framework/core' +import { ConsoleLogger, DownloadToFileOptions, FileSystem, LogLevel, Wallet } from '@aries-framework/core' import { AnonCredsModuleConfig, @@ -55,6 +55,7 @@ const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], [InjectionSymbols.StorageService, inMemoryStorageService], [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], [AnonCredsHolderServiceSymbol, anonCredsHolderService], @@ -438,7 +439,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( }) ) - const { revocationRegistryDefinition, tailsHash, revocationRegistryDefinitionPrivate } = + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { issuerId: indyDid, credentialDefinition, @@ -448,6 +449,13 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( tag: 'default' }) + // At this moment, tails file should be published and a valid public URL will be received + const localTailsPath = revocationRegistryDefinition.value.tailsLocation + + // TOOD: addd test tails server and upload file + revocationRegistryDefinition.value.tailsLocation = + `http://${revocationRegistryDefinition.value.tailsLocation}` + const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { revocationRegistryDefinition, options: {}, @@ -464,8 +472,6 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionRepository).save( agentContext, new AnonCredsRevocationRegistryDefinitionRecord({ - tailsHash, - localTailsFilePath: `${new agentDependencies.FileSystem().dataPath}/${tailsHash}`, revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, }) @@ -476,6 +482,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( new AnonCredsRevocationRegistryDefinitionPrivateRecord({ state: RevocationRegistryState.Active, value: revocationRegistryDefinitionPrivate, + credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, }) ) @@ -483,8 +490,9 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( const revocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { issuanceByDefault: true, issuerId: indyDid, - revocationRegistryDefinition, - revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId + revocationRegistryDefinition: { ...revocationRegistryDefinition, value: { ...revocationRegistryDefinition.value, + tailsLocation: localTailsPath }}, + revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, }) const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { @@ -619,8 +627,8 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( }, schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: null, - credentialRevocationId: undefined, // FIXME: should be null? + revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialRevocationId: "1", methodName: 'inMemory', }) @@ -628,16 +636,20 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( '_anoncreds/credential': { schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialRevocationId: "1", }, '_anoncreds/credentialRequest': { - master_secret_blinding_data: expect.any(Object), - master_secret_name: expect.any(String), + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), nonce: expect.any(String), }, }) expect(issuerCredentialRecord.metadata.data).toEqual({ '_anoncreds/credential': { + revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialRevocationId: "1", schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, }, @@ -654,6 +666,8 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', }) + const nrpRequestedTime = new Date().getTime() + const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { proofFormats: { anoncreds: { @@ -675,6 +689,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( ], name: 'Proof Request', version: '1.0', + nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime } }, }, proofRecord: holderProofRecord, diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 26f684c134..81e14646dd 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -357,7 +357,7 @@ export class AnonCredsApi { } try { - const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate, tailsHash } = + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = await this.anonCredsIssuerService.createRevocationRegistryDefinition(this.agentContext, { issuerId, tag, @@ -367,7 +367,9 @@ export class AnonCredsApi { tailsDirectoryPath, }) - const localTailsFilePath = `${tailsDirectoryPath}/${tailsHash}}` + // TODO: Publish tails file and get public URL for it + const localTailsFilePath = + `${revocationRegistryDefinition.value.tailsLocation}/${revocationRegistryDefinition.value.tailsHash}}` const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { revocationRegistryDefinition, @@ -377,7 +379,6 @@ export class AnonCredsApi { await this.storeRevocationRegistryDefinitionRecord( result, localTailsFilePath, - tailsHash, revocationRegistryDefinitionPrivate ) @@ -525,7 +526,6 @@ export class AnonCredsApi { private async storeRevocationRegistryDefinitionRecord( result: RegisterRevocationRegistryDefinitionReturn, localTailsFilePath: string, - tailsHash: string, revocationRegistryDefinitionPrivate?: Record ): Promise { try { @@ -539,8 +539,6 @@ export class AnonCredsApi { const revocationRegistryDefinitionRecord = new AnonCredsRevocationRegistryDefinitionRecord({ revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, revocationRegistryDefinition: result.revocationRegistryDefinitionState.revocationRegistryDefinition, - localTailsFilePath, - tailsHash, }) // TODO: do we need to store this metadata? For indy, the registration metadata contains e.g. @@ -564,6 +562,7 @@ export class AnonCredsApi { if (revocationRegistryDefinitionPrivate) { const revocationRegistryDefinitionPrivateRecord = new AnonCredsRevocationRegistryDefinitionPrivateRecord({ revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, value: revocationRegistryDefinitionPrivate, state: RevocationRegistryState.Active, }) diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 52c48055e2..7d2efcf7e3 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -59,6 +59,7 @@ import { AnonCredsRevocationRegistryDefinitionRepository, RevocationRegistryState, } from '../repository' +import { downloadTailsFile } from '../utils' const ANONCREDS_CREDENTIAL_OFFER = 'anoncreds/credential-offer@v1.0' const ANONCREDS_CREDENTIAL_REQUEST = 'anoncreds/credential-request@v1.0' @@ -352,11 +353,13 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService } revocationStatusList = result.revocationStatusList - const revocationRegistryDefinitionRecord = await agentContext.dependencyManager + const { revocationRegistryDefinition } = await agentContext.dependencyManager .resolve(AnonCredsRevocationRegistryDefinitionRepository) .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) - tailsFilePath = revocationRegistryDefinitionRecord.localTailsFilePath + ;({ tailsFilePath } = await downloadTailsFile(agentContext, + revocationRegistryDefinition.value.tailsLocation, + revocationRegistryDefinition.value.tailsHash)) } const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormat.ts b/packages/anoncreds/src/formats/AnonCredsProofFormat.ts index 0c326943f8..c25dd17bc8 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormat.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormat.ts @@ -34,6 +34,7 @@ export interface AnonCredsProposeProofFormat { version?: string attributes?: AnonCredsPresentationPreviewAttribute[] predicates?: AnonCredsPresentationPreviewPredicate[] + nonRevokedInterval?: AnonCredsNonRevokedInterval } /** diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index 001aebb340..c1d0931cff 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -86,6 +86,7 @@ export class AnonCredsProofFormatService implements ProofFormatService index?: number state?: RevocationRegistryState @@ -18,6 +19,7 @@ export interface AnonCredsRevocationRegistryDefinitionPrivateRecordProps { export type DefaultAnonCredsRevocationRegistryPrivateTags = { revocationRegistryDefinitionId: string + credentialDefinitionId: string state: RevocationRegistryState } @@ -29,6 +31,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco public readonly type = AnonCredsRevocationRegistryDefinitionPrivateRecord.type public readonly revocationRegistryDefinitionId!: string + public readonly credentialDefinitionId!: string public readonly value!: Record // TODO: Define structure public currentIndex!: number @@ -41,6 +44,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco if (props) { this.id = props.id ?? utils.uuid() this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId + this.credentialDefinitionId = props.credentialDefinitionId this.value = props.value this.currentIndex = props.index ?? 0 this.state = props.state ?? RevocationRegistryState.Created @@ -51,6 +55,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco return { ...this._tags, revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + credentialDefinitionId: this.credentialDefinitionId, state: this.state, } } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts index 231a3fb045..8a41227574 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts @@ -8,8 +8,6 @@ export interface AnonCredsRevocationRegistryDefinitionRecordProps { id?: string revocationRegistryDefinitionId: string revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition - localTailsFilePath: string - tailsHash: string } export type DefaultAnonCredsRevocationRegistryDefinitionTags = { @@ -27,8 +25,6 @@ export class AnonCredsRevocationRegistryDefinitionRecord extends BaseRecord< public readonly revocationRegistryDefinitionId!: string public readonly revocationRegistryDefinition!: AnonCredsRevocationRegistryDefinition - public readonly localTailsFilePath!: string - public readonly tailsHash!: string public constructor(props: AnonCredsRevocationRegistryDefinitionRecordProps) { super() @@ -37,7 +33,6 @@ export class AnonCredsRevocationRegistryDefinitionRecord extends BaseRecord< this.id = props.id ?? utils.uuid() this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId this.revocationRegistryDefinition = props.revocationRegistryDefinition - this.tailsHash = props.tailsHash } } diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index 6d8b3aa73b..e309c9c4df 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -42,6 +42,15 @@ export interface CreateRevocationStatusListOptions { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } +export interface UpdateRevocationStatusListOptions { + revocationStatusList: AnonCredsRevocationStatusList + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revoked: number[] + issued: number[] + timestamp?: number +} + + export interface CreateCredentialOfferOptions { credentialDefinitionId: string } @@ -69,6 +78,5 @@ export interface CreateCredentialDefinitionReturn { export interface CreateRevocationRegistryDefinitionReturn { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionPrivate?: Record - tailsHash: string } diff --git a/packages/anoncreds/src/utils/createRequestFromPreview.ts b/packages/anoncreds/src/utils/createRequestFromPreview.ts index d1738d000e..5faa8f9516 100644 --- a/packages/anoncreds/src/utils/createRequestFromPreview.ts +++ b/packages/anoncreds/src/utils/createRequestFromPreview.ts @@ -2,7 +2,7 @@ import type { AnonCredsPresentationPreviewAttribute, AnonCredsPresentationPreviewPredicate, } from '../formats/AnonCredsProofFormat' -import type { AnonCredsProofRequest } from '../models' +import type { AnonCredsNonRevokedInterval, AnonCredsProofRequest } from '../models' import { utils } from '@aries-framework/core' @@ -12,12 +12,14 @@ export function createRequestFromPreview({ nonce, attributes, predicates, + nonRevokedInterval }: { name: string version: string nonce: string attributes: AnonCredsPresentationPreviewAttribute[] predicates: AnonCredsPresentationPreviewPredicate[] + nonRevokedInterval?: AnonCredsNonRevokedInterval }): AnonCredsProofRequest { const proofRequest: AnonCredsProofRequest = { name, @@ -85,5 +87,10 @@ export function createRequestFromPreview({ } } + // TODO: local non_revoked? + if (nonRevokedInterval) { + proofRequest.non_revoked = nonRevokedInterval + } + return proofRequest } diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 0503c5ca72..a99479b0b4 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -308,7 +308,8 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { ): Promise { const revocationStatusLists = this.revocationStatusLists[revocationRegistryId] - if (!revocationStatusLists || !revocationStatusLists[timestamp]) { + console.log(`revocationStatusLists: ${JSON.stringify(this.revocationStatusLists)}`) + if (!revocationStatusLists || Object.entries(revocationStatusLists).length === 0) { return { resolutionMetadata: { error: 'notFound', @@ -318,9 +319,22 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } + const previousTimestamps = Object.keys(revocationStatusLists).filter(ts => Number(ts) <= timestamp).sort() + + console.log(`previous timestamps: ${previousTimestamps}`) + if (!previousTimestamps) { + return { + resolutionMetadata: { + error: 'notFound', + message: `No active Revocation status list found at ${timestamp} for revocation registry with id ${revocationRegistryId}`, + }, + revocationStatusListMetadata: {}, + } + } + return { resolutionMetadata: {}, - revocationStatusList: revocationStatusLists[timestamp], + revocationStatusList: revocationStatusLists[previousTimestamps[previousTimestamps.length-1]], revocationStatusListMetadata: {}, } } @@ -334,7 +348,10 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { ...options.revocationStatusList, timestamp, } satisfies AnonCredsRevocationStatusList - this.revocationStatusLists[options.revocationStatusList.revRegId][timestamp.toString()] = revocationStatusList + if (!this.revocationStatusLists[options.revocationStatusList.revRegDefId]) { + this.revocationStatusLists[options.revocationStatusList.revRegDefId] = {} + } + this.revocationStatusLists[revocationStatusList.revRegDefId][timestamp.toString()] = revocationStatusList return { registrationMetadata: {}, From e635a49fe07482fbaab9d7ad8305856a4324f24c Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 4 Apr 2023 19:05:22 -0300 Subject: [PATCH 07/27] add test local tails server and some fixes to pass full flow test Signed-off-by: Ariel Gentile --- .../services/AnonCredsRsVerifierService.ts | 24 +- .../anoncreds-rs/tests/anoncreds-flow.test.ts | 82 +++-- packages/anoncreds/src/AnonCredsApi.ts | 9 +- .../AnonCredsRevocationStatusListRecord.ts | 2 +- .../tests/InMemoryAnonCredsRegistry.ts | 10 +- packages/anoncreds/tests/anoncreds.test.ts | 2 +- .../services/IndySdkIssuerService.ts | 4 +- .../indy-sdk/src/anoncreds/utils/transform.ts | 2 +- .../indy-vdr/src/anoncreds/utils/transform.ts | 2 +- samples/tails/.gitignore | 1 + samples/tails/README.md | 5 + samples/tails/package.json | 25 ++ samples/tails/server.ts | 132 +++++++ samples/tails/tsconfig.json | 6 + samples/tails/yarn.lock | 335 ++++++++++++++++++ yarn.lock | 57 ++- 16 files changed, 625 insertions(+), 73 deletions(-) create mode 100644 samples/tails/.gitignore create mode 100644 samples/tails/README.md create mode 100644 samples/tails/package.json create mode 100644 samples/tails/server.ts create mode 100644 samples/tails/tsconfig.json create mode 100644 samples/tails/yarn.lock diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts index 81edd11c56..bbed40a806 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts @@ -3,7 +3,7 @@ import type { AgentContext } from '@aries-framework/core' import type { JsonObject } from '@hyperledger/anoncreds-shared' import { injectable } from '@aries-framework/core' -import { Presentation, RevocationRegistryDefinition, RevocationStatusList } from '@hyperledger/anoncreds-shared' +import { Presentation } from '@hyperledger/anoncreds-shared' @injectable() export class AnonCredsRsVerifierService implements AnonCredsVerifierService { @@ -24,27 +24,15 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { rsSchemas[schemaId] = schemas[schemaId] as unknown as JsonObject } - const revocationRegistryDefinitions: Record = {} - const lists = [] + const revocationRegistryDefinitions: Record = {} + const lists: JsonObject[] = [] for (const revocationRegistryDefinitionId in revocationRegistries) { const { definition, revocationStatusLists } = options.revocationRegistries[revocationRegistryDefinitionId] - revocationRegistryDefinitions[revocationRegistryDefinitionId] = RevocationRegistryDefinition.fromJson( - definition as unknown as JsonObject - ) - - for (const timestamp in revocationStatusLists) { - lists.push( - RevocationStatusList.create({ - issuerId: definition.issuerId, - issuanceByDefault: true, - revocationRegistryDefinition: revocationRegistryDefinitions[revocationRegistryDefinitionId], - revocationRegistryDefinitionId, - timestamp: Number(timestamp), - }) - ) - } + revocationRegistryDefinitions[revocationRegistryDefinitionId] = definition as unknown as JsonObject + + lists.push(...Object.values(revocationStatusLists).map((item) => item as unknown as JsonObject)) } return presentation.verify({ diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index a437cb9dd0..1dc9829ce1 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -1,7 +1,14 @@ -import { AnonCredsCredentialRequest, AnonCredsRevocationRegistryDefinitionPrivateRecord, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryDefinitionRecord, AnonCredsRevocationRegistryDefinitionRepository, AnonCredsRevocationStatusListRecord, AnonCredsRevocationStatusListRepository, RevocationRegistryState } from '@aries-framework/anoncreds' -import { ConsoleLogger, DownloadToFileOptions, FileSystem, LogLevel, Wallet } from '@aries-framework/core' +import type { AnonCredsCredentialRequest } from '@aries-framework/anoncreds' +import type { Wallet } from '@aries-framework/core' import { + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationStatusListRecord, + AnonCredsRevocationStatusListRepository, + RevocationRegistryState, AnonCredsModuleConfig, AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, @@ -20,6 +27,9 @@ import { AnonCredsCredentialFormatService, } from '@aries-framework/anoncreds' import { + utils, + ConsoleLogger, + LogLevel, CredentialState, CredentialExchangeRecord, CredentialPreviewAttribute, @@ -27,6 +37,8 @@ import { ProofState, ProofExchangeRecord, } from '@aries-framework/core' +import FormData from 'form-data' +import fs from 'fs' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' @@ -43,7 +55,9 @@ const anonCredsModuleConfig = new AnonCredsModuleConfig({ registries: [registry], }) -const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs', { logger: new ConsoleLogger(LogLevel.debug)}) +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs', { + logger: new ConsoleLogger(LogLevel.debug), +}) const anonCredsVerifierService = new AnonCredsRsVerifierService() const anonCredsHolderService = new AnonCredsRsHolderService() const anonCredsIssuerService = new AnonCredsRsIssuerService() @@ -373,10 +387,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( options: {}, }) - if ( - !schemaState.schema || - !schemaState.schemaId - ) { + if (!schemaState.schema || !schemaState.schemaId) { throw new Error('Failed to create schema') } @@ -403,10 +414,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( options: {}, }) - if ( - !credentialDefinitionState.credentialDefinition || - !credentialDefinitionState.credentialDefinitionId - ) { + if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { throw new Error('Failed to create credential definition') } @@ -438,7 +446,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, }) ) - + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { issuerId: indyDid, @@ -446,21 +454,34 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, maximumCredentialNumber: 100, tailsDirectoryPath: new agentDependencies.FileSystem().dataPath, - tag: 'default' + tag: 'default', }) // At this moment, tails file should be published and a valid public URL will be received - const localTailsPath = revocationRegistryDefinition.value.tailsLocation - - // TOOD: addd test tails server and upload file - revocationRegistryDefinition.value.tailsLocation = - `http://${revocationRegistryDefinition.value.tailsLocation}` + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const tailsFileId = utils.uuid() + const data = new FormData() + const readStream = fs.createReadStream(localTailsFilePath) + data.append('file', readStream) + const response = await agentContext.config.agentDependencies.fetch( + `http://localhost:3001/${encodeURIComponent(tailsFileId)}`, + { + method: 'PUT', + body: data, + } + ) + if (response.status !== 200) { + throw new Error('Cannot upload tails file') + } + + revocationRegistryDefinition.value.tailsLocation = `http://localhost:3001/${encodeURIComponent(tailsFileId)}` const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { revocationRegistryDefinition, options: {}, }) - + if ( !revocationRegistryDefinitionState.revocationRegistryDefinition || !revocationRegistryDefinitionState.revocationRegistryDefinitionId || @@ -490,8 +511,10 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( const revocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { issuanceByDefault: true, issuerId: indyDid, - revocationRegistryDefinition: { ...revocationRegistryDefinition, value: { ...revocationRegistryDefinition.value, - tailsLocation: localTailsPath }}, + revocationRegistryDefinition: { + ...revocationRegistryDefinition, + value: { ...revocationRegistryDefinition.value, tailsLocation: localTailsFilePath }, + }, revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, }) @@ -500,10 +523,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( options: {}, }) - if ( - !revocationStatusListState.revocationStatusList || - !revocationStatusListState.timestamp - ) { + if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { throw new Error('Failed to create revocation status list') } @@ -628,7 +648,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - credentialRevocationId: "1", + credentialRevocationId: '1', methodName: 'inMemory', }) @@ -637,7 +657,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - credentialRevocationId: "1", + credentialRevocationId: '1', }, '_anoncreds/credentialRequest': { link_secret_blinding_data: expect.any(Object), @@ -649,7 +669,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( expect(issuerCredentialRecord.metadata.data).toEqual({ '_anoncreds/credential': { revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - credentialRevocationId: "1", + credentialRevocationId: '1', schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, }, @@ -666,7 +686,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', }) - const nrpRequestedTime = new Date().getTime() + const nrpRequestedTime = Math.floor(new Date().getTime() / 1000) const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { proofFormats: { @@ -689,7 +709,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( ], name: 'Proof Request', version: '1.0', - nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime } + nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, }, }, proofRecord: holderProofRecord, @@ -724,4 +744,4 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( expect(isValid).toBe(true) }) -}) \ No newline at end of file +}) diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 81e14646dd..388fd937a9 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -368,19 +368,13 @@ export class AnonCredsApi { }) // TODO: Publish tails file and get public URL for it - const localTailsFilePath = - `${revocationRegistryDefinition.value.tailsLocation}/${revocationRegistryDefinition.value.tailsHash}}` const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { revocationRegistryDefinition, options: {}, }) - await this.storeRevocationRegistryDefinitionRecord( - result, - localTailsFilePath, - revocationRegistryDefinitionPrivate - ) + await this.storeRevocationRegistryDefinitionRecord(result, revocationRegistryDefinitionPrivate) return result } catch (error) { @@ -525,7 +519,6 @@ export class AnonCredsApi { private async storeRevocationRegistryDefinitionRecord( result: RegisterRevocationRegistryDefinitionReturn, - localTailsFilePath: string, revocationRegistryDefinitionPrivate?: Record ): Promise { try { diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts index d0bb4727e4..234be32696 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts @@ -38,7 +38,7 @@ export class AnonCredsRevocationStatusListRecord extends BaseRecord< public getTags() { return { ...this._tags, - revocationRegistryDefinitionId: this.revocationStatusList.revRegId, + revocationRegistryDefinitionId: this.revocationStatusList.revRegDefId, credentialDefinitionId: this.credentialDefinitionId, timestamp: this.revocationStatusList.timestamp.toString(), } diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index a99479b0b4..b5996f9ed4 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -308,7 +308,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { ): Promise { const revocationStatusLists = this.revocationStatusLists[revocationRegistryId] - console.log(`revocationStatusLists: ${JSON.stringify(this.revocationStatusLists)}`) if (!revocationStatusLists || Object.entries(revocationStatusLists).length === 0) { return { resolutionMetadata: { @@ -319,9 +318,10 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } - const previousTimestamps = Object.keys(revocationStatusLists).filter(ts => Number(ts) <= timestamp).sort() + const previousTimestamps = Object.keys(revocationStatusLists) + .filter((ts) => Number(ts) <= timestamp) + .sort() - console.log(`previous timestamps: ${previousTimestamps}`) if (!previousTimestamps) { return { resolutionMetadata: { @@ -334,7 +334,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { return { resolutionMetadata: {}, - revocationStatusList: revocationStatusLists[previousTimestamps[previousTimestamps.length-1]], + revocationStatusList: revocationStatusLists[previousTimestamps[previousTimestamps.length - 1]], revocationStatusListMetadata: {}, } } @@ -343,7 +343,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { agentContext: AgentContext, options: RegisterRevocationStatusListOptions ): Promise { - const timestamp = (options.options.timestamp as number) ?? new Date().getTime() + const timestamp = (options.options.timestamp as number) ?? Math.floor(new Date().getTime() / 1000) const revocationStatusList = { ...options.revocationStatusList, timestamp, diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index ee01d9baac..1be988311e 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -66,7 +66,7 @@ const existingRevocationStatusLists = { currentAccumulator: 'ab81257c-be63-4051-9e21-c7d384412f64', issuerId: 'VsKV7grR1BUE29mG2Fm2kX', revocationList: [1, 0, 1], - revRegId: 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + revRegDefId: 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', timestamp: 10123, }, }, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index d07e85df5d..9eaef14dea 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -10,7 +10,7 @@ import type { AnonCredsSchema, CreateCredentialDefinitionReturn, CreateRevocationRegistryDefinitionReturn, - CreateRevocationStatusListReturn, + AnonCredsRevocationStatusList, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' @@ -33,7 +33,7 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { this.indySdk = indySdk } - public async createRevocationStatusList(): Promise { + public async createRevocationStatusList(): Promise { throw new AriesFrameworkError('Method not implemented.') } diff --git a/packages/indy-sdk/src/anoncreds/utils/transform.ts b/packages/indy-sdk/src/anoncreds/utils/transform.ts index e7eac06ecc..9ddc18f81d 100644 --- a/packages/indy-sdk/src/anoncreds/utils/transform.ts +++ b/packages/indy-sdk/src/anoncreds/utils/transform.ts @@ -103,7 +103,7 @@ export function anonCredsRevocationStatusListFromIndySdk( return { issuerId: revocationRegistryDefinition.issuerId, currentAccumulator: delta.value.accum, - revRegId: revocationRegistryDefinitionId, + revRegDefId: revocationRegistryDefinitionId, revocationList, timestamp, } diff --git a/packages/indy-vdr/src/anoncreds/utils/transform.ts b/packages/indy-vdr/src/anoncreds/utils/transform.ts index b0b0bd5fd6..36f4628bb4 100644 --- a/packages/indy-vdr/src/anoncreds/utils/transform.ts +++ b/packages/indy-vdr/src/anoncreds/utils/transform.ts @@ -26,7 +26,7 @@ export function anonCredsRevocationStatusListFromIndyVdr( return { issuerId: revocationRegistryDefinition.issuerId, currentAccumulator: delta.accum, - revRegId: revocationRegistryDefinitionId, + revRegDefId: revocationRegistryDefinitionId, revocationList, timestamp, } diff --git a/samples/tails/.gitignore b/samples/tails/.gitignore new file mode 100644 index 0000000000..6c4291916e --- /dev/null +++ b/samples/tails/.gitignore @@ -0,0 +1 @@ +tails \ No newline at end of file diff --git a/samples/tails/README.md b/samples/tails/README.md new file mode 100644 index 0000000000..62001cceaa --- /dev/null +++ b/samples/tails/README.md @@ -0,0 +1,5 @@ +

Test tails file server

+ +This is a very simple server that is used to host tails files during for tests suites where revocable credentials are issued and verified. + +It offers a single endpoint at the root that takes an URI-encoded `tailsFileId` as URL path and allows to upload (using PUT method and a through a multi-part encoded form) or retrieve a tails file (using GET method). \ No newline at end of file diff --git a/samples/tails/package.json b/samples/tails/package.json new file mode 100644 index 0000000000..8bc0e430ca --- /dev/null +++ b/samples/tails/package.json @@ -0,0 +1,25 @@ +{ + "name": "test-tails-file-server", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "samples/tails/" + }, + "license": "Apache-2.0", + "scripts": { + "start": "ts-node server.ts" + }, + "devDependencies": { + "ts-node": "^10.4.0" + }, + "dependencies": { + "@aries-framework/core": "^0.3.3", + "@types/express": "^4.17.13", + "@types/multer": "^1.4.7", + "@types/uuid": "^9.0.1", + "@types/ws": "^8.5.4", + "multer": "^1.4.5-lts.1" + } +} diff --git a/samples/tails/server.ts b/samples/tails/server.ts new file mode 100644 index 0000000000..b02d420d30 --- /dev/null +++ b/samples/tails/server.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ConsoleLogger, LogLevel } from '@aries-framework/core' +import { createHash } from 'crypto' +import express from 'express' +import fs from 'fs' +import multer, { diskStorage } from 'multer' + +const port = process.env.AGENT_PORT ? Number(process.env.AGENT_PORT) : 3001 +const app = express() + +const baseFilePath = './tails' +const indexFilePath = `./${baseFilePath}/index.json` + +if (!fs.existsSync(baseFilePath)) { + fs.mkdirSync(baseFilePath, { recursive: true }) +} +const tailsIndex = ( + fs.existsSync(indexFilePath) ? JSON.parse(fs.readFileSync(indexFilePath, { encoding: 'utf-8' })) : {} +) as Record + +const logger = new ConsoleLogger(LogLevel.debug) + +function fileHash(filePath: string, algorithm = 'sha256') { + return new Promise((resolve, reject) => { + const shasum = createHash(algorithm) + try { + const s = fs.createReadStream(filePath) + s.on('data', function (data) { + shasum.update(data) + }) + // making digest + s.on('end', function () { + const hash = shasum.digest('hex') + return resolve(hash) + }) + } catch (error) { + return reject('error in calculation') + } + }) +} + +const fileStorage = diskStorage({ + filename: (req: any, file: { originalname: string }, cb: (arg0: null, arg1: string) => void) => { + cb(null, file.originalname + '-' + new Date().toISOString()) + }, +}) + +// Allow to create invitation, no other way to ask for invitation yet +app.get('/:tailsFileId', async (req, res) => { + logger.debug(`requested file`) + + const tailsFileId = req.params.tailsFileId + if (!tailsFileId) { + res.status(409).end() + return + } + + const fileName = tailsIndex[tailsFileId] + + if (!fileName) { + logger.debug(`no entry found for tailsFileId: ${tailsFileId}`) + res.status(404).end() + return + } + + const path = `${baseFilePath}/${fileName}` + try { + logger.debug(`reading file: ${path}`) + + if (!fs.existsSync(path)) { + logger.debug(`file not found: ${path}`) + res.status(404).end() + return + } + + const file = fs.createReadStream(path) + res.setHeader('Content-Disposition', `attachment: filename="${fileName}"`) + file.pipe(res) + } catch (error) { + logger.debug(`error reading file: ${path}`) + res.status(500).end() + } +}) + +app.put('/:tailsFileId', multer({ storage: fileStorage }).single('file'), async (req, res) => { + logger.info(`tails file upload: ${req.params.tailsFileId}`) + + const file = req.file + + if (!file) { + logger.info(`No file found: ${JSON.stringify(req.headers)}`) + return res.status(400).send('No files were uploaded.') + } + + const tailsFileId = req.params.tailsFileId + if (!tailsFileId) { + // Clean up temporary file + fs.rmSync(file.path) + return res.status(409).send('Missing tailsFileId') + } + + const item = tailsIndex[tailsFileId] + + if (item) { + logger.debug(`there is already an entry for: ${tailsFileId}`) + res.status(409).end() + return + } + + const hash = await fileHash(file.path) + const destinationPath = `${baseFilePath}/${hash}` + + if (fs.existsSync(destinationPath)) { + logger.warn('tails file already exists') + } else { + fs.copyFileSync(file.path, destinationPath) + fs.rmSync(file.path) + } + + // Store filename in index + tailsIndex[tailsFileId] = hash + fs.writeFileSync(indexFilePath, JSON.stringify(tailsIndex)) + + res.status(200).end() +}) + +const run = async () => { + app.listen(port) + logger.info(`server started at port ${port}`) +} + +void run() diff --git a/samples/tails/tsconfig.json b/samples/tails/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/samples/tails/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/samples/tails/yarn.lock b/samples/tails/yarn.lock new file mode 100644 index 0000000000..bf3778cf7f --- /dev/null +++ b/samples/tails/yarn.lock @@ -0,0 +1,335 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + +"@types/node@*": + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/serve-static@*": + version "1.15.1" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d" + integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +"@types/uuid@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6" + integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA== + +"@types/ws@^8.5.4": + version "8.5.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" + integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== + dependencies: + "@types/node" "*" + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +multer@^1.4.5-lts.1: + version "1.4.5-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" + integrity sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +ts-node@^10.4.0: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +type-is@^1.6.4: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/yarn.lock b/yarn.lock index 93024b56e7..e7240d3fd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2209,7 +2209,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.13", "@types/express@^4.17.15": +"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.15": version "4.17.17" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== @@ -2312,6 +2312,13 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + "@types/node-fetch@^2.5.10": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" @@ -2730,6 +2737,11 @@ appdirsjs@^1.2.4: resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.7.tgz#50b4b7948a26ba6090d4aede2ae2dc2b051be3b3" integrity sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw== +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -3322,6 +3334,13 @@ builtins@^5.0.0: dependencies: semver "^7.0.0" +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + byte-size@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-7.0.0.tgz#36528cd1ca87d39bd9abd51f5715dc93b6ceb032" @@ -3807,6 +3826,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concat-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" @@ -7937,7 +7966,7 @@ mkdirp-infer-owner@^2.0.0: infer-owner "^1.0.4" mkdirp "^1.0.3" -mkdirp@^0.5.1, mkdirp@^0.5.5: +mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -7974,6 +8003,19 @@ msrcrypto@^1.5.6: resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c" integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q== +multer@^1.4.5-lts.1: + version "1.4.5-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" + integrity sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + multiformats@^9.4.2: version "9.9.0" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" @@ -9400,7 +9442,7 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.6, readable-stream@~2.3.6: +readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -10112,6 +10154,11 @@ str2buf@^1.3.0: resolved "https://registry.yarnpkg.com/str2buf/-/str2buf-1.3.0.tgz#a4172afff4310e67235178e738a2dbb573abead0" integrity sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA== +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" @@ -10659,7 +10706,7 @@ type-fest@^3.2.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.6.1.tgz#cf8025edeebfd6cf48de73573a5e1423350b9993" integrity sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA== -type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -11160,7 +11207,7 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== -xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== From 0cc2cb76552936cb4ee73b3cd63b7259791dee8b Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 10 Apr 2023 09:06:58 -0300 Subject: [PATCH 08/27] add revokeCredentials method and tests Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 25 +- .../tests/anoncreds-revocation.test.ts | 462 ++++++++++++ packages/anoncreds-rs/tests/anoncredsSetup.ts | 476 +++++++++++++ .../v2-credential-revocation.e2e.test.ts | 229 ++++++ .../tests/v2-credentials.e2e.test.ts | 672 ++++++++++++++++++ packages/anoncreds/src/AnonCredsApi.ts | 276 +++++-- packages/anoncreds/src/AnonCredsApiOptions.ts | 16 +- .../AnonCredsCredentialFormatService.ts | 11 +- .../formats/AnonCredsProofFormatService.ts | 2 +- .../src/services/AnonCredsIssuerService.ts | 6 + .../services/AnonCredsIssuerServiceOptions.ts | 8 +- .../src/services/TailsFileUploader.ts | 42 ++ .../src/utils/createRequestFromPreview.ts | 2 +- .../src/modules/credentials/CredentialsApi.ts | 4 + .../credentials/CredentialsApiOptions.ts | 7 + .../services/IndySdkIssuerService.ts | 4 + samples/tails/README.md | 2 +- 17 files changed, 2166 insertions(+), 78 deletions(-) create mode 100644 packages/anoncreds-rs/tests/anoncreds-revocation.test.ts create mode 100644 packages/anoncreds-rs/tests/anoncredsSetup.ts create mode 100644 packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts create mode 100644 packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts create mode 100644 packages/anoncreds/src/services/TailsFileUploader.ts diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index 1e1d9003cc..c567a338fe 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -136,17 +136,20 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateRevocationStatusListOptions ): Promise { - const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, issuanceByDefault } = options + const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, issuanceByDefault, tailsLocation } = + options - console.log(`revocation list: ${options.revocationRegistryDefinition.value.tailsLocation}`) let revocationStatusList: RevocationStatusList | undefined try { revocationStatusList = RevocationStatusList.create({ issuanceByDefault, revocationRegistryDefinitionId, - revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, + revocationRegistryDefinition: { + ...revocationRegistryDefinition, + value: { ...revocationRegistryDefinition.value, tailsLocation }, + } as unknown as JsonObject, issuerId, - timestamp: new Date().getTime() // TODO: fix optional field issue in anoncreds-rs + timestamp: Math.floor(new Date().getTime() / 1000), // TODO: fix optional field issue in anoncreds-rs }) return revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList @@ -159,7 +162,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: UpdateRevocationStatusListOptions ): Promise { - const { revocationStatusList, revocationRegistryDefinition, issued, revoked, timestamp } = options + const { revocationStatusList, revocationRegistryDefinition, issued, revoked, timestamp, tailsLocation } = options let updatedRevocationStatusList: RevocationStatusList | undefined let revocationRegistryDefinitionObj: RevocationRegistryDefinition | undefined @@ -169,16 +172,18 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { if (timestamp && !issued && !revoked) { updatedRevocationStatusList.updateTimestamp({ - timestamp + timestamp, }) } else { - revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson(revocationRegistryDefinition as unknown as JsonObject) + revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson( + { ...revocationRegistryDefinition, value: { ...revocationRegistryDefinition.value, tailsLocation }} as unknown as JsonObject + ) updatedRevocationStatusList.update({ // TODO: Fix parameters in anoncreds-rs revocationRegstryDefinition: revocationRegistryDefinitionObj, - issued, - revoked, - timestamp + issued: [3], + revoked: [2], + timestamp: timestamp ?? -1, }) } diff --git a/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts b/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts new file mode 100644 index 0000000000..0c595e8443 --- /dev/null +++ b/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts @@ -0,0 +1,462 @@ +import type { AnonCredsCredentialRequest } from '@aries-framework/anoncreds' +import type { Wallet } from '@aries-framework/core' + +import { + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationStatusListRecord, + AnonCredsRevocationStatusListRepository, + RevocationRegistryState, + AnonCredsModuleConfig, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, + AnonCredsSchemaRecord, + AnonCredsSchemaRepository, + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsLinkSecretRepository, + AnonCredsLinkSecretRecord, + AnonCredsProofFormatService, + AnonCredsCredentialFormatService, +} from '@aries-framework/anoncreds' +import { + utils, + ConsoleLogger, + LogLevel, + CredentialState, + CredentialExchangeRecord, + CredentialPreviewAttribute, + InjectionSymbols, + ProofState, + ProofExchangeRecord, +} from '@aries-framework/core' +import FormData from 'form-data' +import fs from 'fs' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { describeRunInNodeVersion } from '../../../tests/runInVersion' +import { AnonCredsRegistryService } from '@aries-framework/anoncreds/src/services/registry/AnonCredsRegistryService' +import { InMemoryAnonCredsRegistry } from '@aries-framework/anoncreds/tests/InMemoryAnonCredsRegistry' +import { agentDependencies, getAgentConfig, getAgentContext } from '@aries-framework/core/tests/helpers' +import { AnonCredsRsHolderService } from '../src/services/AnonCredsRsHolderService' +import { AnonCredsRsIssuerService } from '../src/services/AnonCredsRsIssuerService' +import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierService' +import { AnonCredsApi } from 'packages/anoncreds/build' + +const registry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], +}) + +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs', { + logger: new ConsoleLogger(LogLevel.debug), +}) +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet + +const inMemoryStorageService = new InMemoryStorageService() +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + ], + agentConfig, + wallet, +}) + +const anoncredsCredentialFormatService = new AnonCredsCredentialFormatService() +const anoncredsProofFormatService = new AnonCredsProofFormatService() + +const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + +// FIXME: Re-include in tests when NodeJS wrapper performance is improved +describeRunInNodeVersion([18], 'AnonCreds API using anoncreds-rs', () => { + test.only('issuance and revocation', async () => { + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId: indyDid, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + if (!schemaState.schema || !schemaState.schemaId) { + throw new Error('Failed to create schema') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', + }) + ) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId: indyDid, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: true, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { + throw new Error('Failed to create credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { + issuerId: indyDid, + credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + maximumCredentialNumber: 100, + tailsDirectoryPath: new agentDependencies.FileSystem().dataPath, + tag: 'default', + }) + + // At this moment, tails file should be published and a valid public URL will be received + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const tailsFileId = utils.uuid() + const data = new FormData() + const readStream = fs.createReadStream(localTailsFilePath) + data.append('file', readStream) + const response = await agentContext.config.agentDependencies.fetch( + `http://localhost:3001/${encodeURIComponent(tailsFileId)}`, + { + method: 'PUT', + body: data, + } + ) + if (response.status !== 200) { + throw new Error('Cannot upload tails file') + } + + revocationRegistryDefinition.value.tailsLocation = `http://localhost:3001/${encodeURIComponent(tailsFileId)}` + + const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { + revocationRegistryDefinition, + options: {}, + }) + + if ( + !revocationRegistryDefinitionState.revocationRegistryDefinition || + !revocationRegistryDefinitionState.revocationRegistryDefinitionId || + !revocationRegistryDefinitionPrivate + ) { + throw new Error('Failed to create revocation registry') + } + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionRecord({ + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + state: RevocationRegistryState.Active, + value: revocationRegistryDefinitionPrivate, + credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, + revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + }) + ) + + const revocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { + issuanceByDefault: true, + issuerId: indyDid, + revocationRegistryDefinition: { + ...revocationRegistryDefinition, + value: { ...revocationRegistryDefinition.value, tailsLocation: localTailsFilePath }, + }, + revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + }) + + const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { + revocationStatusList, + options: {}, + }) + + if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { + throw new Error('Failed to create revocation status list') + } + + await agentContext.dependencyManager.resolve(AnonCredsRevocationStatusListRepository).save( + agentContext, + new AnonCredsRevocationStatusListRecord({ + credentialDefinitionId: revocationRegistryDefinition.credDefId, + revocationStatusList, + }) + ) + + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) + expect(linkSecret.linkSecretId).toBe('linkSecretId') + + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, + }) + ) + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ + name: 'name', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + value: '25', + }), + ] + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + anoncreds: { + attributes: credentialAttributes, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }, + }) + + // Issuer processes and accepts proposal + await anoncredsCredentialFormatService.processProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: proposalAttachment, + }) + // Set attributes on the credential record, this is normally done by the protocol service + issuerCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + proposalAttachment: proposalAttachment, + }) + + // Holder processes and accepts offer + await anoncredsCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + anoncreds: { + linkSecretId: linkSecret.linkSecretId, + }, + }, + }) + + // Make sure the request contains an entropy and does not contain a prover_did field + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() + + // Issuer processes and accepts request + await anoncredsCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + }) + + // Holder processes and accepts credential + await anoncredsCredentialFormatService.processCredential(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, + ]) + + const credentialId = holderCredentialRecord.credentials[0].credentialRecordId + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + credentialId, + }) + + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: '25', + name: 'John', + }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialRevocationId: '1', + methodName: 'inMemory', + }) + + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialRevocationId: '1', + }, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': { + revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialRevocationId: '1', + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }) + + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalReceived, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + + const nrpRequestedTime = Math.floor(new Date().getTime() / 1000) + + const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { + proofFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, + }, + }, + proofRecord: holderProofRecord, + }) + + await anoncredsProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) + + const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) + + await anoncredsProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) + + const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) + + const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, + }) + + expect(isValid).toBe(true) + }) +}) diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts new file mode 100644 index 0000000000..d6c4e56b94 --- /dev/null +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -0,0 +1,476 @@ +import type { EventReplaySubject } from '../../core/tests' +import { + AnonCredsRegisterCredentialDefinitionOptions, + AnonCredsRequestedAttribute, + AnonCredsRequestedPredicate, + AnonCredsOfferCredentialFormat, + AnonCredsSchema, + RegisterCredentialDefinitionReturnStateFinished, + RegisterSchemaReturnStateFinished, + AnonCredsCredentialFormatService, + AnonCredsProofFormatService, + AnonCredsRegistry, +} from '../../anoncreds/src' +import type { AutoAcceptProof, ConnectionRecord } from '@aries-framework/core' + +import { + TypedArrayEncoder, + CacheModule, + InMemoryLruCache, + Agent, + AriesFrameworkError, + AutoAcceptCredential, + CredentialEventTypes, + CredentialsModule, + CredentialState, + ProofEventTypes, + ProofsModule, + ProofState, + V2CredentialProtocol, + V2ProofProtocol, + DidsModule, +} from '@aries-framework/core' +import { anoncreds } from '@hyperledger/anoncreds-nodejs' +import { randomUUID } from 'crypto' + +import { AnonCredsRsModule } from '../src' +import { AskarModule } from '../../askar/src' +import { askarModuleConfig } from '../../askar/tests/helpers' +import { sleep } from '../../core/src/utils/sleep' +import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' +import { + getAgentOptions, + importExistingIndyDidFromPrivateKey, + makeConnection, + publicDidSeed, + waitForCredentialRecordSubject, + waitForProofExchangeRecordSubject, +} from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { + IndyVdrAnonCredsRegistry, + IndyVdrSovDidResolver, + IndyVdrModule, + IndyVdrIndyDidResolver, + IndyVdrIndyDidRegistrar, +} from '../../indy-vdr/src' +import { indyVdrModuleConfig } from '../../indy-vdr/tests/helpers' +import { AnonCredsModule } from '../../anoncreds/src' + +// Helper type to get the type of the agents (with the custom modules) for the credential tests +export type AnonCredsTestsAgent = Agent< + ReturnType & { mediationRecipient?: any; mediator?: any } +> + +export const getAnonCredsModules = ({ + autoAcceptCredentials, + autoAcceptProofs, + registries, +}: { + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] +} = {}) => { + const anonCredsCredentialFormatService = new AnonCredsCredentialFormatService() + const anonCredsProofFormatService = new AnonCredsProofFormatService() + + const modules = { + credentials: new CredentialsModule({ + autoAcceptCredentials, + credentialProtocols: [ + new V2CredentialProtocol({ + credentialFormats: [anonCredsCredentialFormatService], + }), + ], + }), + proofs: new ProofsModule({ + autoAcceptProofs, + proofProtocols: [ + new V2ProofProtocol({ + proofFormats: [anonCredsProofFormatService], + }), + ], + }), + anoncreds: new AnonCredsModule({ + registries: registries ?? [new IndyVdrAnonCredsRegistry()], + }), + anoncredsRs: new AnonCredsRsModule({ + anoncreds, + }), + indyVdr: new IndyVdrModule(indyVdrModuleConfig), + dids: new DidsModule({ + resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], + registrars: [new IndyVdrIndyDidRegistrar()], + }), + askar: new AskarModule(askarModuleConfig), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 100 }), + }), + } as const + + return modules +} + +export async function presentAnonCredsProof({ + verifierAgent, + verifierReplay, + + holderAgent, + holderReplay, + + verifierHolderConnectionId, + + request: { attributes, predicates }, +}: { + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + verifierAgent: AnonCredsTestsAgent + verifierReplay: EventReplaySubject + + verifierHolderConnectionId: string + request: { + attributes?: Record + predicates?: Record + } +}) { + let holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + state: ProofState.RequestReceived, + }) + + let verifierProofExchangeRecord = await verifierAgent.proofs.requestProof({ + connectionId: verifierHolderConnectionId, + proofFormats: { + anoncreds: { + name: 'Test Proof Request', + requested_attributes: attributes, + requested_predicates: predicates, + version: '1.0', + }, + }, + protocolVersion: 'v2', + }) + + let holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const selectedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const verifierProofExchangeRecordPromise = waitForProofExchangeRecordSubject(verifierReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holderAgent.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { anoncreds: selectedCredentials.proofFormats.anoncreds }, + }) + + verifierProofExchangeRecord = await verifierProofExchangeRecordPromise + + // assert presentation is valid + expect(verifierProofExchangeRecord.isVerified).toBe(true) + + holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + verifierProofExchangeRecord = await verifierAgent.proofs.acceptPresentation({ + proofRecordId: verifierProofExchangeRecord.id, + }) + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + return { + verifierProofExchangeRecord, + holderProofExchangeRecord, + } +} + +export async function issueAnonCredsCredential({ + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + issuerHolderConnectionId, + offer, +}: { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: string + offer: AnonCredsOfferCredentialFormat +}) { + let issuerCredentialExchangeRecord = await issuerAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: issuerHolderConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: offer, + }, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + let holderCredentialExchangeRecord = await waitForCredentialRecordSubject(holderReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + await holderAgent.credentials.acceptOffer({ + credentialRecordId: holderCredentialExchangeRecord.id, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + // Because we use auto-accept it can take a while to have the whole credential flow finished + // Both parties need to interact with the ledger and sign/verify the credential + holderCredentialExchangeRecord = await waitForCredentialRecordSubject(holderReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + issuerCredentialExchangeRecord = await waitForCredentialRecordSubject(issuerReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + return { + issuerCredentialExchangeRecord, + holderCredentialExchangeRecord, + } +} + +interface SetupAnonCredsTestsReturn { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: CreateConnections extends true ? string : undefined + holderIssuerConnectionId: CreateConnections extends true ? string : undefined + + verifierHolderConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + holderVerifierConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + + verifierAgent: VerifierName extends string ? AnonCredsTestsAgent : undefined + verifierReplay: VerifierName extends string ? EventReplaySubject : undefined + + schemaId: string + credentialDefinitionId: string +} + +export async function setupAnonCredsTests< + VerifierName extends string | undefined = undefined, + CreateConnections extends boolean = true +>({ + issuerName, + holderName, + verifierName, + autoAcceptCredentials, + autoAcceptProofs, + attributeNames, + createConnections, + supportRevocation, + registries, +}: { + issuerName: string + holderName: string + verifierName?: VerifierName + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + attributeNames: string[] + createConnections?: CreateConnections + supportRevocation?: boolean + registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] +}): Promise> { + const issuerAgent = new Agent( + getAgentOptions( + issuerName, + { + endpoints: ['rxjs:issuer'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + + const holderAgent = new Agent( + getAgentOptions( + holderName, + { + endpoints: ['rxjs:holder'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + + const verifierAgent = verifierName + ? new Agent( + getAgentOptions( + verifierName, + { + endpoints: ['rxjs:verifier'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + : undefined + + setupSubjectTransports(verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent]) + const [issuerReplay, holderReplay, verifierReplay] = setupEventReplaySubjects( + verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent], + [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + ) + + await issuerAgent.initialize() + await holderAgent.initialize() + if (verifierAgent) await verifierAgent.initialize() + + // Create default link secret for holder + await holderAgent.modules.anoncreds.createLinkSecret({ + linkSecretId: 'default', + setAsDefault: true, + }) + + const { credentialDefinition, schema } = await prepareForAnonCredsIssuance(issuerAgent, { + attributeNames, + supportRevocation, + }) + + let issuerHolderConnection: ConnectionRecord | undefined + let holderIssuerConnection: ConnectionRecord | undefined + let verifierHolderConnection: ConnectionRecord | undefined + let holderVerifierConnection: ConnectionRecord | undefined + + if (createConnections ?? true) { + ;[issuerHolderConnection, holderIssuerConnection] = await makeConnection(issuerAgent, holderAgent) + + if (verifierAgent) { + ;[holderVerifierConnection, verifierHolderConnection] = await makeConnection(holderAgent, verifierAgent) + } + } + + return { + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + verifierAgent: verifierName ? verifierAgent : undefined, + verifierReplay: verifierName ? verifierReplay : undefined, + + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + schemaId: schema.schemaId, + + issuerHolderConnectionId: issuerHolderConnection?.id, + holderIssuerConnectionId: holderIssuerConnection?.id, + holderVerifierConnectionId: holderVerifierConnection?.id, + verifierHolderConnectionId: verifierHolderConnection?.id, + } as unknown as SetupAnonCredsTestsReturn +} + +export async function prepareForAnonCredsIssuance( + agent: Agent, + { attributeNames, supportRevocation }: { attributeNames: string[]; supportRevocation?: boolean } +) { + // Add existing endorser did to the wallet + const unqualifiedDid = await importExistingIndyDidFromPrivateKey(agent, TypedArrayEncoder.fromString(publicDidSeed)) + const didIndyDid = `did:indy:pool:localtest:${unqualifiedDid}` + + const schema = await registerSchema(agent, { + // TODO: update attrNames to attributeNames + attrNames: attributeNames, + name: `Schema ${randomUUID()}`, + version: '1.0', + issuerId: didIndyDid, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + const credentialDefinition = await registerCredentialDefinition(agent, { + schemaId: schema.schemaId, + issuerId: didIndyDid, + tag: 'default', + supportRevocation, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + return { + schema: { + ...schema, + schemaId: schema.schemaId, + }, + credentialDefinition: { + ...credentialDefinition, + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + }, + } +} + +async function registerSchema( + agent: AnonCredsTestsAgent, + schema: AnonCredsSchema +): Promise { + const { schemaState } = await agent.modules.anoncreds.registerSchema({ + schema, + options: {}, + }) + + testLogger.test(`created schema with id ${schemaState.schemaId}`, schema) + + if (schemaState.state !== 'finished') { + throw new AriesFrameworkError( + `Schema not created: ${schemaState.state === 'failed' ? schemaState.reason : 'Not finished'}` + ) + } + + return schemaState +} + +async function registerCredentialDefinition( + agent: AnonCredsTestsAgent, + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions +): Promise { + const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition, + options: {}, + }) + + if (credentialDefinitionState.state !== 'finished') { + throw new AriesFrameworkError( + `Credential definition not created: ${ + credentialDefinitionState.state === 'failed' ? credentialDefinitionState.reason : 'Not finished' + }` + ) + } + + return credentialDefinitionState +} diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts new file mode 100644 index 0000000000..51e3366202 --- /dev/null +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -0,0 +1,229 @@ +import type { EventReplaySubject } from '../../core/tests' + +import { waitForCredentialRecordSubject } from '../../core/tests' +import testLogger from '../../core/tests/logger' +import { + DidCommMessageRepository, + JsonTransformer, + CredentialState, + CredentialExchangeRecord, + V2CredentialPreview, + V2OfferCredentialMessage, +} from '@aries-framework/core' +import { AnonCredsProposeCredentialFormat } from '@aries-framework/anoncreds' +import { AnonCredsTestsAgent, setupAnonCredsTests } from './anoncredsSetup' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', +}) + +describe('v2 credential revocation', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let faberConnectionId: string + let aliceConnectionId: string + + let faberReplay: EventReplaySubject + let aliceReplay: EventReplaySubject + + let anonCredsCredentialProposal: AnonCredsProposeCredentialFormat + + const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) + + const newCredentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + supportRevocation: true, + registries: [inMemoryRegistry], + })) + + anonCredsCredentialProposal = { + credentialDefinitionId: credentialDefinitionId, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + } + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + }, + comment: 'v2 propose credential test', + }) + + expect(credentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.ProposalSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + const offerMessage = await didCommMessageRepository.findAgentMessage(faberAgent.context, { + associatedRecordId: faberCredentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'V2 AnonCreds Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + connectionId: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + // below values are not in json object + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + credentialIds: [], + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.RequestSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for state done') + const doneCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + // Now revoke the credential + const revocationRegistryDefinitionId = doneCredentialRecord.getTag('anonCredsRevocationRegistryId') as string + const credentialRevocationId = doneCredentialRecord.getTag('anonCredsCredentialRevocationId') as string + await faberAgent.modules.anoncreds.revokeCredentials({ + revocationRegistryDefinitionId, + revokedIndexes: [Number(credentialRevocationId)], + }) + }) +}) diff --git a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts new file mode 100644 index 0000000000..e629708de2 --- /dev/null +++ b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts @@ -0,0 +1,672 @@ +import type { EventReplaySubject } from '../../core/tests' + +import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../core/tests' +import testLogger from '../../core/tests/logger' +import { + DidCommMessageRepository, + JsonTransformer, + CredentialState, + CredentialExchangeRecord, + V2CredentialPreview, + V2IssueCredentialMessage, + V2OfferCredentialMessage, + V2ProposeCredentialMessage, + V2RequestCredentialMessage, +} from '@aries-framework/core' +import { + AnonCredsHolderService, + AnonCredsHolderServiceSymbol, + AnonCredsProposeCredentialFormat, +} from '@aries-framework/anoncreds' +import { AnonCredsTestsAgent, issueAnonCredsCredential } from './anoncredsSetup' +import { setupAnonCredsTests } from './anoncredsSetup' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', +}) + +describe('v2 credentials', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let faberConnectionId: string + let aliceConnectionId: string + + let faberReplay: EventReplaySubject + let aliceReplay: EventReplaySubject + + let anonCredsCredentialProposal: AnonCredsProposeCredentialFormat + + const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) + + const newCredentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + registries: [inMemoryRegistry], + })) + + anonCredsCredentialProposal = { + credentialDefinitionId: credentialDefinitionId, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + } + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + }, + comment: 'v2 propose credential test', + }) + + expect(credentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.ProposalSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + const offerMessage = await didCommMessageRepository.findAgentMessage(faberAgent.context, { + associatedRecordId: faberCredentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'V2 AnonCreds Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + connectionId: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + // below values are not in json object + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + credentialIds: [], + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.RequestSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for state done') + await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + }) + + test('Faber issues credential which is then deleted from Alice`s wallet', async () => { + const { holderCredentialExchangeRecord } = await issueAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + offer: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }) + + // test that delete credential removes from both repository and wallet + // latter is tested by spying on holder service to + // see if deleteCredential is called + const holderService = aliceAgent.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const deleteCredentialSpy = jest.spyOn(holderService, 'deleteCredential') + await aliceAgent.credentials.deleteById(holderCredentialExchangeRecord.id, { + deleteAssociatedCredentials: true, + deleteAssociatedDidCommMessages: true, + }) + expect(deleteCredentialSpy).toHaveBeenNthCalledWith( + 1, + aliceAgent.context, + holderCredentialExchangeRecord.credentials[0].credentialRecordId + ) + + return expect(aliceAgent.credentials.getById(holderCredentialExchangeRecord.id)).rejects.toThrowError( + `CredentialRecord: record with id ${holderCredentialExchangeRecord.id} not found.` + ) + }) + + test('Alice starts with proposal, faber sends a counter offer, alice sends second proposal, faber sends second offer', async () => { + // proposeCredential -> negotiateProposal -> negotiateOffer -> negotiateProposal -> acceptOffer -> acceptRequest -> DONE (credential issued) + + let faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: credentialPreview.attributes, + }, + }, + comment: 'v2 propose credential test', + }) + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await faberCredentialRecordPromise + + let aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await aliceCredentialRecordPromise + + // Check if the state of the credential records did not change + faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberCredentialRecord.assertState(CredentialState.OfferSent) + + aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id) + aliceCredentialRecord.assertState(CredentialState.OfferReceived) + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + // second proposal + aliceCredentialExchangeRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await aliceCredentialRecordPromise + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialExchangeRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + state: CredentialState.RequestSent, + protocolVersion: 'v2', + threadId: aliceCredentialExchangeRecord.threadId, + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.RequestReceived, + }) + testLogger.test('Faber sends credential to Alice') + + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + // testLogger.test('Alice sends credential ack to Faber') + await aliceAgent.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: expect.any(String), + connectionId: expect.any(String), + state: CredentialState.CredentialReceived, + }) + }) + + test('Faber starts with offer, alice sends counter proposal, faber sends second offer, alice sends second proposal', async () => { + let aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + let faberCredentialRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await aliceCredentialRecordPromise + + let faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await aliceCredentialRecordPromise + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await aliceCredentialRecordPromise + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + state: CredentialState.RequestSent, + protocolVersion: 'v2', + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await aliceCredentialRecordPromise + + const proposalMessage = await aliceAgent.credentials.findProposalMessage(aliceCredentialRecord.id) + const offerMessage = await aliceAgent.credentials.findOfferMessage(aliceCredentialRecord.id) + const requestMessage = await aliceAgent.credentials.findRequestMessage(aliceCredentialRecord.id) + const credentialMessage = await aliceAgent.credentials.findCredentialMessage(aliceCredentialRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposeCredentialMessage) + expect(offerMessage).toBeInstanceOf(V2OfferCredentialMessage) + expect(requestMessage).toBeInstanceOf(V2RequestCredentialMessage) + expect(credentialMessage).toBeInstanceOf(V2IssueCredentialMessage) + + const formatData = await aliceAgent.credentials.getFormatData(aliceCredentialRecord.id) + expect(formatData).toMatchObject({ + proposalAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'another x-ray value', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'another profile picture', + }, + ], + proposal: { + anoncreds: { + schema_issuer_did: expect.any(String), + schema_id: expect.any(String), + schema_name: expect.any(String), + schema_version: expect.any(String), + cred_def_id: expect.any(String), + issuer_did: expect.any(String), + }, + }, + offer: { + anoncreds: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + key_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + offerAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'profile picture', + }, + ], + request: { + anoncreds: { + entropy: expect.any(String), + cred_def_id: expect.any(String), + blinded_ms: expect.any(Object), + blinded_ms_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + credential: { + anoncreds: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + rev_reg_id: null, + values: { + age: { raw: '99', encoded: '99' }, + profile_picture: { + raw: 'profile picture', + encoded: '28661874965215723474150257281172102867522547934697168414362313592277831163345', + }, + name: { + raw: 'John', + encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', + }, + 'x-ray': { + raw: 'some x-ray', + encoded: '43715611391396952879378357808399363551139229809726238083934532929974486114650', + }, + }, + signature: expect.any(Object), + signature_correctness_proof: expect.any(Object), + rev_reg: null, + witness: null, + }, + }, + }) + }) + + test('Faber starts with V2 offer, alice declines the offer', async () => { + testLogger.test('Faber sends credential offer to Alice') + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + testLogger.test('Alice declines offer') + aliceCredentialRecord = await aliceAgent.credentials.declineOffer(aliceCredentialRecord.id) + + expect(aliceCredentialRecord.state).toBe(CredentialState.Declined) + }) +}) diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 388fd937a9..ffd96a1fce 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -3,6 +3,8 @@ import type { AnonCredsRegisterCredentialDefinitionOptions, AnonCredsRegisterRevocationRegistryDefinitionOptions, AnonCredsRegisterRevocationStatusListOptions, + AnonCredsRevokeCredentialOptions, + AnonCredsUpdateRevocationStatusListOptions, } from './AnonCredsApiOptions' import type { GetCredentialDefinitionReturn, @@ -18,7 +20,7 @@ import type { RegisterRevocationStatusListReturn, } from './services' import type { Extensible } from './services/registry/base' -import type { SimpleQuery } from '@aries-framework/core' +import { AriesFrameworkError, SimpleQuery } from '@aries-framework/core' import { AgentContext, inject, injectable } from '@aries-framework/core' @@ -52,6 +54,8 @@ import { AnonCredsHolderService, } from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' +import { DefaultTailsFileUploader } from './services/TailsFileUploader' +import { downloadTailsFile } from './utils' @injectable() export class AnonCredsApi { @@ -297,26 +301,31 @@ export class AnonCredsApi { // If it supports revocation, create the first revocation registry definition if (options.credentialDefinition.supportRevocation && result.credentialDefinitionState.credentialDefinitionId) { - const { revocationRegistryDefinitionState } = await this.registerRevocationRegistryDefinition({ - credentialDefinition, - credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, - issuerId: options.credentialDefinition.issuerId, - maximumCredentialNumber: this.config.maximumCredentialNumberPerRevocationRegistry, - tailsDirectoryPath: - this.config.tailsDirectoryPath ?? new this.agentContext.config.agentDependencies.FileSystem().dataPath, - tag: '', // TODO: define tags - }) + const { revocationRegistryDefinitionState, revocationRegistryDefinitionMetadata } = + await this.registerRevocationRegistryDefinition({ + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + issuerId: options.credentialDefinition.issuerId, + maximumCredentialNumber: this.config.maximumCredentialNumberPerRevocationRegistry, + tailsDirectoryPath: + this.config.tailsDirectoryPath ?? new this.agentContext.config.agentDependencies.FileSystem().dataPath, + tag: '', // TODO: define tags + }) if ( revocationRegistryDefinitionState.revocationRegistryDefinition && revocationRegistryDefinitionState.revocationRegistryDefinitionId ) { + const tailsLocation = revocationRegistryDefinitionMetadata.localTailsLocation as string + + if (!tailsLocation) { + throw new AriesFrameworkError('Cannot find tails file locally') + } // Create and register an initial revocation status list await this.registerRevocationStatusList({ issuanceByDefault: true, issuerId: options.credentialDefinition.issuerId, - revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + tailsLocation, }) } } @@ -335,11 +344,46 @@ export class AnonCredsApi { } } + public async getCreatedCredentialDefinitions(query: SimpleQuery) { + return this.anonCredsCredentialDefinitionRepository.findByQuery(this.agentContext, query) + } + + /** + * Retrieve a {@link AnonCredsRevocationRegistryDefinition} from the registry associated + * with the {@link revocationRegistryDefinitionId} + */ + public async getRevocationRegistryDefinition( + revocationRegistryDefinitionId: string + ): Promise { + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve revocation registry ${revocationRegistryDefinitionId}`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) + if (!registry) { + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}` + return failedReturnBase + } + + try { + const result = await registry.getRevocationRegistryDefinition(this.agentContext, revocationRegistryDefinitionId) + return result + } catch (error) { + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: ${error.message}` + return failedReturnBase + } + } + public async registerRevocationRegistryDefinition( options: AnonCredsRegisterRevocationRegistryDefinitionOptions ): Promise { - const { issuerId, tag, credentialDefinitionId, credentialDefinition, tailsDirectoryPath, maximumCredentialNumber } = - options + const { issuerId, tag, credentialDefinitionId, tailsDirectoryPath, maximumCredentialNumber } = options const failedReturnBase = { revocationRegistryDefinitionState: { @@ -356,6 +400,12 @@ export class AnonCredsApi { return failedReturnBase } + const { credentialDefinition } = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId) + + if (!credentialDefinition) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Unable to register revocation registry definition. No credential definition found for id ${credentialDefinitionId}` + return failedReturnBase + } try { const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = await this.anonCredsIssuerService.createRevocationRegistryDefinition(this.agentContext, { @@ -367,16 +417,24 @@ export class AnonCredsApi { tailsDirectoryPath, }) - // TODO: Publish tails file and get public URL for it + // At this moment, tails file should be published and a valid public URL will be received + // FIXME: Make configurable as interface + const localTailsLocation = revocationRegistryDefinition.value.tailsLocation + revocationRegistryDefinition.value.tailsLocation = await new DefaultTailsFileUploader().uploadTails( + this.agentContext, + { revocationRegistryDefinition } + ) const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { revocationRegistryDefinition, options: {}, }) - await this.storeRevocationRegistryDefinitionRecord(result, revocationRegistryDefinitionPrivate) - return result + return { + ...result, + revocationRegistryDefinitionMetadata: { ...result.revocationRegistryDefinitionMetadata, localTailsLocation }, + } } catch (error) { // Storage failed if (error instanceof AnonCredsStoreRecordError) { @@ -389,42 +447,6 @@ export class AnonCredsApi { } } - public async getCreatedCredentialDefinitions(query: SimpleQuery) { - return this.anonCredsCredentialDefinitionRepository.findByQuery(this.agentContext, query) - } - - /** - * Retrieve a {@link AnonCredsRevocationRegistryDefinition} from the registry associated - * with the {@link revocationRegistryDefinitionId} - */ - public async getRevocationRegistryDefinition( - revocationRegistryDefinitionId: string - ): Promise { - const failedReturnBase = { - resolutionMetadata: { - error: 'error', - message: `Unable to resolve revocation registry ${revocationRegistryDefinitionId}`, - }, - revocationRegistryDefinitionId, - revocationRegistryDefinitionMetadata: {}, - } - - const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) - if (!registry) { - failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' - failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}` - return failedReturnBase - } - - try { - const result = await registry.getRevocationRegistryDefinition(this.agentContext, revocationRegistryDefinitionId) - return result - } catch (error) { - failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: ${error.message}` - return failedReturnBase - } - } - /** * Retrieve the {@link AnonCredsRevocationStatusList} for the given {@link timestamp} from the registry associated * with the {@link revocationRegistryDefinitionId} @@ -464,7 +486,7 @@ export class AnonCredsApi { public async registerRevocationStatusList( options: AnonCredsRegisterRevocationStatusListOptions ): Promise { - const { issuanceByDefault, issuerId, revocationRegistryDefinition, revocationRegistryDefinitionId } = options + const { issuanceByDefault, issuerId, revocationRegistryDefinitionId, tailsLocation } = options const failedReturnBase = { revocationStatusListState: { @@ -481,12 +503,98 @@ export class AnonCredsApi { return failedReturnBase } + const { revocationRegistryDefinition } = await registry.getRevocationRegistryDefinition( + this.agentContext, + revocationRegistryDefinitionId + ) + + if (!revocationRegistryDefinition) { + failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No revocation registry definition found for ${revocationRegistryDefinitionId}` + return failedReturnBase + } + try { const revocationStatusList = await this.anonCredsIssuerService.createRevocationStatusList(this.agentContext, { issuanceByDefault, issuerId, revocationRegistryDefinition, revocationRegistryDefinitionId, + tailsLocation, + }) + + const result = await registry.registerRevocationStatusList(this.agentContext, { + revocationStatusList, + options: {}, + }) + + await this.storeRevocationStatusListRecord(result, revocationRegistryDefinition.credDefId) + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationStatusListState.reason = `Error storing revocation status list records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationStatusListState.reason = `Error registering revocation status list: ${error.message}` + return failedReturnBase + } + } + + public async updateRevocationStatusList( + options: AnonCredsUpdateRevocationStatusListOptions + ): Promise { + const { issuedCredentialIndexes, revokedCredentialIndexes, revocationRegistryDefinitionId } = options + + const failedReturnBase = { + revocationStatusListState: { + state: 'failed' as const, + reason: `Error updating revocation status list for revocation registry definition id ${options.revocationRegistryDefinitionId}`, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(options.revocationRegistryDefinitionId) + if (!registry) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No registry found for id ${options.revocationRegistryDefinitionId}` + return failedReturnBase + } + + const { revocationRegistryDefinition } = await registry.getRevocationRegistryDefinition( + this.agentContext, + revocationRegistryDefinitionId + ) + + if (!revocationRegistryDefinition) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No revocation registry definition found for ${revocationRegistryDefinitionId}` + return failedReturnBase + } + + const { revocationStatusList: previousRevocationStatusList } = await this.getRevocationStatusList( + revocationRegistryDefinitionId, + Math.floor(new Date().getTime() / 1000) + ) + + if (!previousRevocationStatusList) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No previous revocation status list found for ${options.revocationRegistryDefinitionId}` + return failedReturnBase + } + + const { tailsFilePath: tailsLocation } = await downloadTailsFile( + this.agentContext, + revocationRegistryDefinition.value.tailsLocation, + revocationRegistryDefinition.value.tailsHash + ) + + try { + const revocationStatusList = await this.anonCredsIssuerService.updateRevocationStatusList(this.agentContext, { + issued: issuedCredentialIndexes, + revoked: revokedCredentialIndexes, + revocationStatusList: previousRevocationStatusList, + revocationRegistryDefinition, + tailsLocation }) const result = await registry.registerRevocationStatusList(this.agentContext, { @@ -494,7 +602,7 @@ export class AnonCredsApi { options: {}, }) - await this.storeRevocationStatusListRecord(result, options.revocationRegistryDefinition.credDefId) + await this.storeRevocationStatusListRecord(result, revocationRegistryDefinition.credDefId) return result } catch (error) { @@ -509,6 +617,66 @@ export class AnonCredsApi { } } + public async revokeCredentials(options: AnonCredsRevokeCredentialOptions): Promise { + const { revocationRegistryDefinitionId, revokedIndexes } = options + const anonCredsIssuerService = + this.agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + // get current revocation status list + const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) + + // FIXME: Do not throw. Instead, return a fail status like other methods from the API + if (!registry) { + throw new AriesFrameworkError( + `Could not get registry for revocation registry definition ${revocationRegistryDefinitionId}` + ) + } + + const { revocationRegistryDefinition } = await registry.getRevocationRegistryDefinition( + this.agentContext, + revocationRegistryDefinitionId + ) + + if (!revocationRegistryDefinition) { + throw new AriesFrameworkError(`Could not get revocation registry definition ${revocationRegistryDefinitionId}`) + } + + const { revocationStatusList } = await registry.getRevocationStatusList( + this.agentContext, + revocationRegistryDefinitionId, + Math.floor(new Date().getTime() / 1000) // FIXME: Make optional and use now if not defined + ) + + if (!revocationStatusList) { + throw new AriesFrameworkError( + `Could not get current revocation status list for ${revocationRegistryDefinitionId}` + ) + } + + const { tailsFilePath: tailsLocation } = await downloadTailsFile( + this.agentContext, + revocationRegistryDefinition.value.tailsLocation, + revocationRegistryDefinition.value.tailsHash + ) + + // Update and publish the new revocation status list + const newRevocationStatusList = await anonCredsIssuerService.updateRevocationStatusList(this.agentContext, { + revocationStatusList, + revocationRegistryDefinition, + revoked: revokedIndexes, + tailsLocation + }) + + const result = await registry.registerRevocationStatusList(this.agentContext, { + revocationStatusList: newRevocationStatusList, + options: {}, + }) + + if (!result.revocationStatusListState) { + throw new AriesFrameworkError(`Cannot update revocation registry for ${revocationRegistryDefinitionId}`) + } + } + public async getCredential(credentialId: string) { return this.anonCredsHolderService.getCredential(this.agentContext, { credentialId }) } diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index e665e754f0..252a27af3a 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -16,14 +16,24 @@ export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { issuerId: string tag: string credentialDefinitionId: string - credentialDefinition: AnonCredsCredentialDefinition tailsDirectoryPath: string maximumCredentialNumber: number } export interface AnonCredsRegisterRevocationStatusListOptions { issuerId: string - issuanceByDefault: true - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + issuanceByDefault: boolean revocationRegistryDefinitionId: string + tailsLocation: string +} + +export interface AnonCredsUpdateRevocationStatusListOptions { + revokedCredentialIndexes: number[] + issuedCredentialIndexes: number[] + revocationRegistryDefinitionId: string +} + +export interface AnonCredsRevokeCredentialOptions { + revocationRegistryDefinitionId: string + revokedIndexes: number[] } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 7d2efcf7e3..066ee8ada9 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -331,6 +331,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService RevocationRegistryState.Active ) + // TODO: should a new revocation registry be generated/published/etc automatically? if (!revocationRegistryDefinitionPrivateRecord) { throw new AriesFrameworkError(`No available revocation registry found for ${credentialRequest.cred_def_id}`) } @@ -343,7 +344,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const result = await registry.getRevocationStatusList( agentContext, revocationRegistryDefinitionId, - new Date().getTime() + Math.floor(new Date().getTime() / 1000) ) if (!result.revocationStatusList) { @@ -357,9 +358,11 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService .resolve(AnonCredsRevocationRegistryDefinitionRepository) .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) - ;({ tailsFilePath } = await downloadTailsFile(agentContext, - revocationRegistryDefinition.value.tailsLocation, - revocationRegistryDefinition.value.tailsHash)) + ;({ tailsFilePath } = await downloadTailsFile( + agentContext, + revocationRegistryDefinition.value.tailsLocation, + revocationRegistryDefinition.value.tailsHash + )) } const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index c1d0931cff..5a613e5d14 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -86,7 +86,7 @@ export class AnonCredsProofFormatService implements ProofFormatService + updateRevocationStatusList( + agentContext: AgentContext, + options: UpdateRevocationStatusListOptions + ): Promise + createCredentialOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index e309c9c4df..247ddf989a 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -40,17 +40,18 @@ export interface CreateRevocationStatusListOptions { issuanceByDefault: boolean revocationRegistryDefinitionId: string revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + tailsLocation: string } export interface UpdateRevocationStatusListOptions { revocationStatusList: AnonCredsRevocationStatusList revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition - revoked: number[] - issued: number[] + revoked?: number[] + issued?: number[] timestamp?: number + tailsLocation: string } - export interface CreateCredentialOfferOptions { credentialDefinitionId: string } @@ -79,4 +80,3 @@ export interface CreateRevocationRegistryDefinitionReturn { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionPrivate?: Record } - diff --git a/packages/anoncreds/src/services/TailsFileUploader.ts b/packages/anoncreds/src/services/TailsFileUploader.ts new file mode 100644 index 0000000000..1d403ad2c3 --- /dev/null +++ b/packages/anoncreds/src/services/TailsFileUploader.ts @@ -0,0 +1,42 @@ +import { AgentContext, utils } from '@aries-framework/core' +import { AnonCredsRevocationRegistryDefinition } from '../models' +import FormData from 'form-data' +import fs from 'fs' + +export interface TailsFileUploader { + uploadTails( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + localTailsFilePath: string + } + ): Promise +} + +export class DefaultTailsFileUploader implements TailsFileUploader { + public async uploadTails( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise { + const revocationRegistryDefinition = options.revocationRegistryDefinition + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const tailsFileId = utils.uuid() + const data = new FormData() + const readStream = fs.createReadStream(localTailsFilePath) + data.append('file', readStream) + const response = await agentContext.config.agentDependencies.fetch( + `http://localhost:3001/${encodeURIComponent(tailsFileId)}`, + { + method: 'PUT', + body: data, + } + ) + if (response.status !== 200) { + throw new Error('Cannot upload tails file') + } + return `http://localhost:3001/${encodeURIComponent(tailsFileId)}` + } +} diff --git a/packages/anoncreds/src/utils/createRequestFromPreview.ts b/packages/anoncreds/src/utils/createRequestFromPreview.ts index 5faa8f9516..5c08ebd83d 100644 --- a/packages/anoncreds/src/utils/createRequestFromPreview.ts +++ b/packages/anoncreds/src/utils/createRequestFromPreview.ts @@ -12,7 +12,7 @@ export function createRequestFromPreview({ nonce, attributes, predicates, - nonRevokedInterval + nonRevokedInterval, }: { name: string version: string diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 6e60a807ba..1c99e1f0aa 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -15,6 +15,7 @@ import type { ProposeCredentialOptions, SendCredentialProblemReportOptions, DeleteCredentialOptions, + SendRevocationNotificationOptions, } from './CredentialsApiOptions' import type { CredentialProtocol } from './protocol/CredentialProtocol' import type { CredentialFormatsFromProtocols } from './protocol/CredentialProtocolOptions' @@ -62,6 +63,9 @@ export interface CredentialsApi { // Issue Credential Methods acceptCredential(options: AcceptCredentialOptions): Promise + // Revoke Credential Methods + sendRevocationNotification(options: SendRevocationNotificationOptions): Promise + // out of band createOffer(options: CreateCredentialOfferOptions): Promise<{ message: AgentMessage diff --git a/packages/core/src/modules/credentials/CredentialsApiOptions.ts b/packages/core/src/modules/credentials/CredentialsApiOptions.ts index 19e9f17295..430e65d317 100644 --- a/packages/core/src/modules/credentials/CredentialsApiOptions.ts +++ b/packages/core/src/modules/credentials/CredentialsApiOptions.ts @@ -121,6 +121,13 @@ export interface AcceptCredentialOptions { credentialRecordId: string } +/** + * Interface for CredentialsApi.sendRevocationNotification. Will send notifications + */ +export interface SendRevocationNotificationOptions { + credentialRecordIds: string[] +} + /** * Interface for CredentialsApi.sendProblemReport. Will send a problem-report message */ diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index 9eaef14dea..633b5f6802 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -37,6 +37,10 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { throw new AriesFrameworkError('Method not implemented.') } + public async updateRevocationStatusList(): Promise { + throw new AriesFrameworkError('Method not implemented.') + } + public async createRevocationRegistryDefinition(): Promise { throw new AriesFrameworkError('Method not implemented.') } diff --git a/samples/tails/README.md b/samples/tails/README.md index 62001cceaa..449ad3d10d 100644 --- a/samples/tails/README.md +++ b/samples/tails/README.md @@ -2,4 +2,4 @@ This is a very simple server that is used to host tails files during for tests suites where revocable credentials are issued and verified. -It offers a single endpoint at the root that takes an URI-encoded `tailsFileId` as URL path and allows to upload (using PUT method and a through a multi-part encoded form) or retrieve a tails file (using GET method). \ No newline at end of file +It offers a single endpoint at the root that takes an URI-encoded `tailsFileId` as URL path and allows to upload (using PUT method and a through a multi-part encoded form) or retrieve a tails file (using GET method). From 8fc938057c6c5ed9bf25bd87ec5c8cdd36d665bc Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 10 Apr 2023 23:25:05 -0300 Subject: [PATCH 09/27] add revocation notification and generic tails file manager Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 22 +-- .../tests/InMemoryTailsFileManager.ts | 60 +++++++ .../anoncreds-rs/tests/anoncreds-flow.test.ts | 41 ++--- .../tests/anoncreds-revocation.test.ts | 48 ++---- packages/anoncreds-rs/tests/anoncredsSetup.ts | 100 +++++++++-- packages/anoncreds-rs/tests/helpers.ts | 46 +++++ .../v2-credential-revocation.e2e.test.ts | 61 +++---- packages/anoncreds/src/AnonCredsApi.ts | 162 +++++------------- packages/anoncreds/src/AnonCredsApiOptions.ts | 10 +- .../anoncreds/src/AnonCredsModuleConfig.ts | 15 +- .../AnonCredsCredentialFormatService.ts | 26 +-- packages/anoncreds/src/index.ts | 1 + .../services/AnonCredsIssuerServiceOptions.ts | 4 +- .../src/services/BasicTailsFileManager.ts | 86 ++++++++++ .../src/services/TailsFileManager.ts | 26 +++ packages/anoncreds/src/services/index.ts | 2 + .../src/utils/getRevocationRegistries.ts | 8 +- packages/anoncreds/src/utils/index.ts | 1 - packages/anoncreds/src/utils/tails.ts | 57 ------ packages/anoncreds/src/utils/timestamp.ts | 1 + .../tests/InMemoryAnonCredsRegistry.ts | 4 +- .../src/modules/credentials/CredentialsApi.ts | 68 +++++++- .../credentials/CredentialsApiOptions.ts | 8 +- .../services/RevocationNotificationService.ts | 38 +++- .../RevocationNotificationServiceOptions.ts | 6 + .../revocation-notification/services/index.ts | 1 + .../util/revocationIdentifier.ts | 5 + .../tails/FullTailsFileManager.ts | 29 ++-- samples/tails/package.json | 2 + 29 files changed, 588 insertions(+), 350 deletions(-) create mode 100644 packages/anoncreds-rs/tests/InMemoryTailsFileManager.ts create mode 100644 packages/anoncreds-rs/tests/helpers.ts create mode 100644 packages/anoncreds/src/services/BasicTailsFileManager.ts create mode 100644 packages/anoncreds/src/services/TailsFileManager.ts delete mode 100644 packages/anoncreds/src/utils/tails.ts create mode 100644 packages/anoncreds/src/utils/timestamp.ts create mode 100644 packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts rename packages/anoncreds/src/services/TailsFileUploader.ts => samples/tails/FullTailsFileManager.ts (51%) diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index c567a338fe..6c201f4ebf 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -1,4 +1,4 @@ -import { +import type { AnonCredsIssuerService, CreateCredentialDefinitionOptions, CreateCredentialOfferOptions, @@ -15,13 +15,13 @@ import { AnonCredsRevocationRegistryDefinition, CreateRevocationStatusListOptions, AnonCredsRevocationStatusList, - RevocationRegistryState, UpdateRevocationStatusListOptions, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared' import { + RevocationRegistryState, AnonCredsRevocationRegistryDefinitionRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, @@ -136,7 +136,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateRevocationStatusListOptions ): Promise { - const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, issuanceByDefault, tailsLocation } = + const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, issuanceByDefault, tailsFilePath } = options let revocationStatusList: RevocationStatusList | undefined @@ -146,10 +146,9 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { revocationRegistryDefinitionId, revocationRegistryDefinition: { ...revocationRegistryDefinition, - value: { ...revocationRegistryDefinition.value, tailsLocation }, + value: { ...revocationRegistryDefinition.value, tailsLocation: tailsFilePath }, } as unknown as JsonObject, issuerId, - timestamp: Math.floor(new Date().getTime() / 1000), // TODO: fix optional field issue in anoncreds-rs }) return revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList @@ -162,7 +161,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: UpdateRevocationStatusListOptions ): Promise { - const { revocationStatusList, revocationRegistryDefinition, issued, revoked, timestamp, tailsLocation } = options + const { revocationStatusList, revocationRegistryDefinition, issued, revoked, timestamp, tailsFilePath } = options let updatedRevocationStatusList: RevocationStatusList | undefined let revocationRegistryDefinitionObj: RevocationRegistryDefinition | undefined @@ -175,14 +174,15 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { timestamp, }) } else { - revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson( - { ...revocationRegistryDefinition, value: { ...revocationRegistryDefinition.value, tailsLocation }} as unknown as JsonObject - ) + revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson({ + ...revocationRegistryDefinition, + value: { ...revocationRegistryDefinition.value, tailsLocation: tailsFilePath }, + } as unknown as JsonObject) updatedRevocationStatusList.update({ // TODO: Fix parameters in anoncreds-rs revocationRegstryDefinition: revocationRegistryDefinitionObj, - issued: [3], - revoked: [2], + issued: options.issued, + revoked: options.revoked, timestamp: timestamp ?? -1, }) } diff --git a/packages/anoncreds-rs/tests/InMemoryTailsFileManager.ts b/packages/anoncreds-rs/tests/InMemoryTailsFileManager.ts new file mode 100644 index 0000000000..fefdf23526 --- /dev/null +++ b/packages/anoncreds-rs/tests/InMemoryTailsFileManager.ts @@ -0,0 +1,60 @@ +import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' +import type { AgentContext } from '@aries-framework/core' + +import { BasicTailsFileManager } from '@aries-framework/anoncreds' + +export class InMemoryTailsFileManager extends BasicTailsFileManager { + private tailsFilePaths: Record = {} + + public async uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise { + this.tailsFilePaths[options.revocationRegistryDefinition.value.tailsHash] = + options.revocationRegistryDefinition.value.tailsLocation + + return options.revocationRegistryDefinition.value.tailsHash + } + + public async downloadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise<{ + tailsFilePath: string + }> { + const { revocationRegistryDefinition } = options + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + + try { + agentContext.config.logger.debug( + `Checking to see if tails file for URL ${revocationRegistryDefinition.value.tailsLocation} has been stored in the FileSystem` + ) + + // hash is used as file identifier + const tailsExists = await this.tailsFileExists(agentContext, tailsHash) + const tailsFilePath = this.getTailsFilePath(agentContext, tailsHash) + agentContext.config.logger.debug( + `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` + ) + + if (!tailsExists) { + agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) + // TODO + agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) + } + + return { + tailsFilePath, + } + } catch (error) { + agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { + error, + }) + throw error + } + } +} diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 1dc9829ce1..2ec6f6785e 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -27,7 +27,6 @@ import { AnonCredsCredentialFormatService, } from '@aries-framework/anoncreds' import { - utils, ConsoleLogger, LogLevel, CredentialState, @@ -37,22 +36,25 @@ import { ProofState, ProofExchangeRecord, } from '@aries-framework/core' -import FormData from 'form-data' -import fs from 'fs' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' import { describeRunInNodeVersion } from '../../../tests/runInVersion' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' 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' +import { InMemoryTailsFileManager } from './InMemoryTailsFileManager' + const registry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) +const tailsFileManager = new InMemoryTailsFileManager() const anonCredsModuleConfig = new AnonCredsModuleConfig({ registries: [registry], + tailsFileManager, }) const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs', { @@ -453,30 +455,13 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( credentialDefinition, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, maximumCredentialNumber: 100, - tailsDirectoryPath: new agentDependencies.FileSystem().dataPath, + tailsDirectoryPath: tailsFileManager.getTailsBasePath(agentContext), tag: 'default', }) // At this moment, tails file should be published and a valid public URL will be received const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation - const tailsFileId = utils.uuid() - const data = new FormData() - const readStream = fs.createReadStream(localTailsFilePath) - data.append('file', readStream) - const response = await agentContext.config.agentDependencies.fetch( - `http://localhost:3001/${encodeURIComponent(tailsFileId)}`, - { - method: 'PUT', - body: data, - } - ) - if (response.status !== 200) { - throw new Error('Cannot upload tails file') - } - - revocationRegistryDefinition.value.tailsLocation = `http://localhost:3001/${encodeURIComponent(tailsFileId)}` - const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { revocationRegistryDefinition, options: {}, @@ -508,18 +493,16 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( }) ) - const revocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { + const createdRevocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { issuanceByDefault: true, issuerId: indyDid, - revocationRegistryDefinition: { - ...revocationRegistryDefinition, - value: { ...revocationRegistryDefinition.value, tailsLocation: localTailsFilePath }, - }, + revocationRegistryDefinition, revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + tailsFilePath: localTailsFilePath, }) const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { - revocationStatusList, + revocationStatusList: createdRevocationStatusList, options: {}, }) @@ -531,7 +514,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( agentContext, new AnonCredsRevocationStatusListRecord({ credentialDefinitionId: revocationRegistryDefinition.credDefId, - revocationStatusList, + revocationStatusList: revocationStatusListState.revocationStatusList, }) ) @@ -686,7 +669,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', }) - const nrpRequestedTime = Math.floor(new Date().getTime() / 1000) + const nrpRequestedTime = dateToTimestamp(new Date()) const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { proofFormats: { diff --git a/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts b/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts index 0c595e8443..8ea5426035 100644 --- a/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts @@ -25,9 +25,9 @@ import { AnonCredsLinkSecretRecord, AnonCredsProofFormatService, AnonCredsCredentialFormatService, + AnonCredsRegistryService, } from '@aries-framework/anoncreds' import { - utils, ConsoleLogger, LogLevel, CredentialState, @@ -37,23 +37,24 @@ import { ProofState, ProofExchangeRecord, } from '@aries-framework/core' -import FormData from 'form-data' -import fs from 'fs' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' import { describeRunInNodeVersion } from '../../../tests/runInVersion' -import { AnonCredsRegistryService } from '@aries-framework/anoncreds/src/services/registry/AnonCredsRegistryService' -import { InMemoryAnonCredsRegistry } from '@aries-framework/anoncreds/tests/InMemoryAnonCredsRegistry' -import { agentDependencies, getAgentConfig, getAgentContext } from '@aries-framework/core/tests/helpers' +import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' +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' -import { AnonCredsApi } from 'packages/anoncreds/build' + +import { InMemoryTailsFileManager } from './InMemoryTailsFileManager' const registry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) +const tailsFileManager = new InMemoryTailsFileManager() const anonCredsModuleConfig = new AnonCredsModuleConfig({ registries: [registry], + tailsFileManager, }) const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs', { @@ -168,30 +169,13 @@ describeRunInNodeVersion([18], 'AnonCreds API using anoncreds-rs', () => { credentialDefinition, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, maximumCredentialNumber: 100, - tailsDirectoryPath: new agentDependencies.FileSystem().dataPath, + tailsDirectoryPath: tailsFileManager.getTailsBasePath(agentContext), tag: 'default', }) // At this moment, tails file should be published and a valid public URL will be received const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation - const tailsFileId = utils.uuid() - const data = new FormData() - const readStream = fs.createReadStream(localTailsFilePath) - data.append('file', readStream) - const response = await agentContext.config.agentDependencies.fetch( - `http://localhost:3001/${encodeURIComponent(tailsFileId)}`, - { - method: 'PUT', - body: data, - } - ) - if (response.status !== 200) { - throw new Error('Cannot upload tails file') - } - - revocationRegistryDefinition.value.tailsLocation = `http://localhost:3001/${encodeURIComponent(tailsFileId)}` - const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { revocationRegistryDefinition, options: {}, @@ -223,18 +207,16 @@ describeRunInNodeVersion([18], 'AnonCreds API using anoncreds-rs', () => { }) ) - const revocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { + const createdRevocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { issuanceByDefault: true, issuerId: indyDid, - revocationRegistryDefinition: { - ...revocationRegistryDefinition, - value: { ...revocationRegistryDefinition.value, tailsLocation: localTailsFilePath }, - }, + revocationRegistryDefinition, revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + tailsFilePath: localTailsFilePath, }) const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { - revocationStatusList, + revocationStatusList: createdRevocationStatusList, options: {}, }) @@ -246,7 +228,7 @@ describeRunInNodeVersion([18], 'AnonCreds API using anoncreds-rs', () => { agentContext, new AnonCredsRevocationStatusListRecord({ credentialDefinitionId: revocationRegistryDefinition.credDefId, - revocationStatusList, + revocationStatusList: revocationStatusListState.revocationStatusList, }) ) @@ -401,7 +383,7 @@ describeRunInNodeVersion([18], 'AnonCreds API using anoncreds-rs', () => { threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', }) - const nrpRequestedTime = Math.floor(new Date().getTime() / 1000) + const nrpRequestedTime = dateToTimestamp(new Date()) const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { proofFormats: { diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index d6c4e56b94..3634865719 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -1,5 +1,4 @@ -import type { EventReplaySubject } from '../../core/tests' -import { +import type { AnonCredsRegisterCredentialDefinitionOptions, AnonCredsRequestedAttribute, AnonCredsRequestedPredicate, @@ -7,10 +6,13 @@ import { AnonCredsSchema, RegisterCredentialDefinitionReturnStateFinished, RegisterSchemaReturnStateFinished, - AnonCredsCredentialFormatService, - AnonCredsProofFormatService, AnonCredsRegistry, + AnonCredsRegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturnStateFinished, + AnonCredsRegisterRevocationStatusListOptions, + RegisterRevocationStatusListReturnStateFinished, } from '../../anoncreds/src' +import type { EventReplaySubject } from '../../core/tests' import type { AutoAcceptProof, ConnectionRecord } from '@aries-framework/core' import { @@ -33,7 +35,7 @@ import { import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { randomUUID } from 'crypto' -import { AnonCredsRsModule } from '../src' +import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../../anoncreds/src' import { AskarModule } from '../../askar/src' import { askarModuleConfig } from '../../askar/tests/helpers' import { sleep } from '../../core/src/utils/sleep' @@ -55,7 +57,9 @@ import { IndyVdrIndyDidRegistrar, } from '../../indy-vdr/src' import { indyVdrModuleConfig } from '../../indy-vdr/tests/helpers' -import { AnonCredsModule } from '../../anoncreds/src' +import { AnonCredsRsModule } from '../src' + +import { InMemoryTailsFileManager } from './InMemoryTailsFileManager' // Helper type to get the type of the agents (with the custom modules) for the credential tests export type AnonCredsTestsAgent = Agent< @@ -93,6 +97,7 @@ export const getAnonCredsModules = ({ }), anoncreds: new AnonCredsModule({ registries: registries ?? [new IndyVdrAnonCredsRegistry()], + tailsFileManager: new InMemoryTailsFileManager(), }), anoncredsRs: new AnonCredsRsModule({ anoncreds, @@ -270,6 +275,7 @@ interface SetupAnonCredsTestsReturn { + const { revocationRegistryDefinitionState } = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ + revocationRegistryDefinition, + options: {}, + }) + + if (revocationRegistryDefinitionState.state !== 'finished') { + throw new AriesFrameworkError( + `Revocation registry definition not created: ${ + revocationRegistryDefinitionState.state === 'failed' ? revocationRegistryDefinitionState.reason : 'Not finished' + }` + ) + } + + return revocationRegistryDefinitionState +} + +async function registerRevocationStatusList( + agent: AnonCredsTestsAgent, + revocationStatusList: AnonCredsRegisterRevocationStatusListOptions +): Promise { + const { revocationStatusListState } = await agent.modules.anoncreds.registerRevocationStatusList({ + revocationStatusList, + options: {}, + }) + + if (revocationStatusListState.state !== 'finished') { + throw new AriesFrameworkError( + `Revocation status list not created: ${ + revocationStatusListState.state === 'failed' ? revocationStatusListState.reason : 'Not finished' + }` + ) + } + + return revocationStatusListState +} diff --git a/packages/anoncreds-rs/tests/helpers.ts b/packages/anoncreds-rs/tests/helpers.ts new file mode 100644 index 0000000000..dddde28048 --- /dev/null +++ b/packages/anoncreds-rs/tests/helpers.ts @@ -0,0 +1,46 @@ +import type { Agent, RevocationNotificationReceivedEvent } from '@aries-framework/core' +import type { Observable } from 'rxjs' + +import { CredentialEventTypes } from '@aries-framework/core' +import { catchError, filter, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' + +export async function waitForRevocationNotification( + agent: Agent, + options: { + threadId?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable( + CredentialEventTypes.RevocationNotificationReceived + ) + + return waitForRevocationNotificationSubject(observable, options) +} + +export function waitForRevocationNotificationSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + }: { + threadId?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter((e) => threadId === undefined || e.payload.credentialRecord.threadId === threadId), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `RevocationNotificationReceivedEvent event not emitted within specified timeout: { + threadId: ${threadId}, + }` + ) + }), + map((e) => e.payload.credentialRecord) + ) + ) +} diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts index 51e3366202..bd7b96c96f 100644 --- a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -1,7 +1,6 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' import type { EventReplaySubject } from '../../core/tests' -import { waitForCredentialRecordSubject } from '../../core/tests' -import testLogger from '../../core/tests/logger' import { DidCommMessageRepository, JsonTransformer, @@ -10,9 +9,13 @@ import { V2CredentialPreview, V2OfferCredentialMessage, } from '@aries-framework/core' -import { AnonCredsProposeCredentialFormat } from '@aries-framework/anoncreds' -import { AnonCredsTestsAgent, setupAnonCredsTests } from './anoncredsSetup' + import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { waitForCredentialRecordSubject } from '../../core/tests' +import testLogger from '../../core/tests/logger' + +import { setupAnonCredsTests } from './anoncredsSetup' +import { waitForRevocationNotification } from './helpers' const credentialPreview = V2CredentialPreview.fromRecord({ name: 'John', @@ -25,23 +28,14 @@ describe('v2 credential revocation', () => { let faberAgent: AnonCredsTestsAgent let aliceAgent: AnonCredsTestsAgent let credentialDefinitionId: string - let faberConnectionId: string + let revocationRegistryDefinitionId: string | undefined let aliceConnectionId: string let faberReplay: EventReplaySubject let aliceReplay: EventReplaySubject - let anonCredsCredentialProposal: AnonCredsProposeCredentialFormat - const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) - const newCredentialPreview = V2CredentialPreview.fromRecord({ - name: 'John', - age: '99', - 'x-ray': 'another x-ray value', - profile_picture: 'another profile picture', - }) - beforeAll(async () => { ;({ issuerAgent: faberAgent, @@ -49,7 +43,7 @@ describe('v2 credential revocation', () => { holderAgent: aliceAgent, holderReplay: aliceReplay, credentialDefinitionId, - issuerHolderConnectionId: faberConnectionId, + revocationRegistryDefinitionId, holderIssuerConnectionId: aliceConnectionId, } = await setupAnonCredsTests({ issuerName: 'Faber Agent Credentials v2', @@ -58,15 +52,6 @@ describe('v2 credential revocation', () => { supportRevocation: true, registries: [inMemoryRegistry], })) - - anonCredsCredentialProposal = { - credentialDefinitionId: credentialDefinitionId, - schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', - schemaName: 'ahoy', - schemaVersion: '1.0', - schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', - issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', - } }) afterAll(async () => { @@ -219,11 +204,29 @@ describe('v2 credential revocation', () => { }) // Now revoke the credential - const revocationRegistryDefinitionId = doneCredentialRecord.getTag('anonCredsRevocationRegistryId') as string - const credentialRevocationId = doneCredentialRecord.getTag('anonCredsCredentialRevocationId') as string - await faberAgent.modules.anoncreds.revokeCredentials({ - revocationRegistryDefinitionId, - revokedIndexes: [Number(credentialRevocationId)], + const credentialRevocationRegistryDefinitionId = doneCredentialRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = doneCredentialRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + expect(credentialRevocationRegistryDefinitionId).toEqual(revocationRegistryDefinitionId) + + await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }) + + await faberAgent.credentials.sendRevocationNotification({ + credentialRecordId: doneCredentialRecord.id, + revocationFormat: 'anoncreds', + revocationId: `${credentialRevocationRegistryDefinitionId}::${credentialRevocationIndex}`, + }) + + testLogger.test('Alice waits for credential revocation notification from Faber') + await waitForRevocationNotification(aliceAgent, { + threadId: faberCredentialRecord.threadId, }) }) }) diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index ffd96a1fce..f02a17bdd7 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -3,7 +3,6 @@ import type { AnonCredsRegisterCredentialDefinitionOptions, AnonCredsRegisterRevocationRegistryDefinitionOptions, AnonCredsRegisterRevocationStatusListOptions, - AnonCredsRevokeCredentialOptions, AnonCredsUpdateRevocationStatusListOptions, } from './AnonCredsApiOptions' import type { @@ -20,7 +19,7 @@ import type { RegisterRevocationStatusListReturn, } from './services' import type { Extensible } from './services/registry/base' -import { AriesFrameworkError, SimpleQuery } from '@aries-framework/core' +import type { SimpleQuery } from '@aries-framework/core' import { AgentContext, inject, injectable } from '@aries-framework/core' @@ -54,8 +53,7 @@ import { AnonCredsHolderService, } from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' -import { DefaultTailsFileUploader } from './services/TailsFileUploader' -import { downloadTailsFile } from './utils' +import { dateToTimestamp } from './utils/timestamp' @injectable() export class AnonCredsApi { @@ -299,37 +297,6 @@ export class AnonCredsApi { await this.storeCredentialDefinitionRecord(registry, result, credentialDefinitionPrivate, keyCorrectnessProof) - // If it supports revocation, create the first revocation registry definition - if (options.credentialDefinition.supportRevocation && result.credentialDefinitionState.credentialDefinitionId) { - const { revocationRegistryDefinitionState, revocationRegistryDefinitionMetadata } = - await this.registerRevocationRegistryDefinition({ - credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, - issuerId: options.credentialDefinition.issuerId, - maximumCredentialNumber: this.config.maximumCredentialNumberPerRevocationRegistry, - tailsDirectoryPath: - this.config.tailsDirectoryPath ?? new this.agentContext.config.agentDependencies.FileSystem().dataPath, - tag: '', // TODO: define tags - }) - - if ( - revocationRegistryDefinitionState.revocationRegistryDefinition && - revocationRegistryDefinitionState.revocationRegistryDefinitionId - ) { - const tailsLocation = revocationRegistryDefinitionMetadata.localTailsLocation as string - - if (!tailsLocation) { - throw new AriesFrameworkError('Cannot find tails file locally') - } - // Create and register an initial revocation status list - await this.registerRevocationStatusList({ - issuanceByDefault: true, - issuerId: options.credentialDefinition.issuerId, - revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - tailsLocation, - }) - } - } - return result } catch (error) { // Storage failed @@ -380,23 +347,29 @@ export class AnonCredsApi { } } - public async registerRevocationRegistryDefinition( - options: AnonCredsRegisterRevocationRegistryDefinitionOptions - ): Promise { - const { issuerId, tag, credentialDefinitionId, tailsDirectoryPath, maximumCredentialNumber } = options + public async registerRevocationRegistryDefinition(options: { + revocationRegistryDefinition: AnonCredsRegisterRevocationRegistryDefinitionOptions + options: Extensible + }): Promise { + const { issuerId, tag, credentialDefinitionId, maximumCredentialNumber } = options.revocationRegistryDefinition + + const tailsFileManager = + this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager + + const tailsDirectoryPath = tailsFileManager.getTailsBasePath(this.agentContext) const failedReturnBase = { revocationRegistryDefinitionState: { state: 'failed' as const, - reason: `Error registering revocation registry definition for issuerId ${options.issuerId}`, + reason: `Error registering revocation registry definition for issuerId ${issuerId}`, }, registrationMetadata: {}, revocationRegistryDefinitionMetadata: {}, } - const registry = this.findRegistryForIdentifier(options.issuerId) + const registry = this.findRegistryForIdentifier(issuerId) if (!registry) { - failedReturnBase.revocationRegistryDefinitionState.reason = `Unable to register revocation registry definition. No registry found for issuerId ${options.issuerId}` + failedReturnBase.revocationRegistryDefinitionState.reason = `Unable to register revocation registry definition. No registry found for issuerId ${issuerId}` return failedReturnBase } @@ -418,12 +391,11 @@ export class AnonCredsApi { }) // At this moment, tails file should be published and a valid public URL will be received - // FIXME: Make configurable as interface const localTailsLocation = revocationRegistryDefinition.value.tailsLocation - revocationRegistryDefinition.value.tailsLocation = await new DefaultTailsFileUploader().uploadTails( - this.agentContext, - { revocationRegistryDefinition } - ) + + revocationRegistryDefinition.value.tailsLocation = await tailsFileManager.uploadTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { revocationRegistryDefinition, @@ -483,23 +455,24 @@ export class AnonCredsApi { } } - public async registerRevocationStatusList( - options: AnonCredsRegisterRevocationStatusListOptions - ): Promise { - const { issuanceByDefault, issuerId, revocationRegistryDefinitionId, tailsLocation } = options + public async registerRevocationStatusList(options: { + revocationStatusList: AnonCredsRegisterRevocationStatusListOptions + options: Extensible + }): Promise { + const { issuanceByDefault, issuerId, revocationRegistryDefinitionId } = options.revocationStatusList const failedReturnBase = { revocationStatusListState: { state: 'failed' as const, - reason: `Error registering revocation status list for issuerId ${options.issuerId}`, + reason: `Error registering revocation status list for issuerId ${issuerId}`, }, registrationMetadata: {}, revocationStatusListMetadata: {}, } - const registry = this.findRegistryForIdentifier(options.issuerId) + const registry = this.findRegistryForIdentifier(issuerId) if (!registry) { - failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No registry found for issuerId ${options.issuerId}` + failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No registry found for issuerId ${issuerId}` return failedReturnBase } @@ -512,6 +485,10 @@ export class AnonCredsApi { failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No revocation registry definition found for ${revocationRegistryDefinitionId}` return failedReturnBase } + const tailsFileManager = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager + const { tailsFilePath } = await tailsFileManager.downloadTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) try { const revocationStatusList = await this.anonCredsIssuerService.createRevocationStatusList(this.agentContext, { @@ -519,7 +496,7 @@ export class AnonCredsApi { issuerId, revocationRegistryDefinition, revocationRegistryDefinitionId, - tailsLocation, + tailsFilePath, }) const result = await registry.registerRevocationStatusList(this.agentContext, { @@ -574,7 +551,7 @@ export class AnonCredsApi { const { revocationStatusList: previousRevocationStatusList } = await this.getRevocationStatusList( revocationRegistryDefinitionId, - Math.floor(new Date().getTime() / 1000) + dateToTimestamp(new Date()) ) if (!previousRevocationStatusList) { @@ -582,19 +559,18 @@ export class AnonCredsApi { return failedReturnBase } - const { tailsFilePath: tailsLocation } = await downloadTailsFile( - this.agentContext, - revocationRegistryDefinition.value.tailsLocation, - revocationRegistryDefinition.value.tailsHash - ) - + const tailsFileManager = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager + const { tailsFilePath } = await tailsFileManager.downloadTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) + try { const revocationStatusList = await this.anonCredsIssuerService.updateRevocationStatusList(this.agentContext, { issued: issuedCredentialIndexes, revoked: revokedCredentialIndexes, revocationStatusList: previousRevocationStatusList, revocationRegistryDefinition, - tailsLocation + tailsFilePath, }) const result = await registry.registerRevocationStatusList(this.agentContext, { @@ -617,66 +593,6 @@ export class AnonCredsApi { } } - public async revokeCredentials(options: AnonCredsRevokeCredentialOptions): Promise { - const { revocationRegistryDefinitionId, revokedIndexes } = options - const anonCredsIssuerService = - this.agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) - - // get current revocation status list - const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) - - // FIXME: Do not throw. Instead, return a fail status like other methods from the API - if (!registry) { - throw new AriesFrameworkError( - `Could not get registry for revocation registry definition ${revocationRegistryDefinitionId}` - ) - } - - const { revocationRegistryDefinition } = await registry.getRevocationRegistryDefinition( - this.agentContext, - revocationRegistryDefinitionId - ) - - if (!revocationRegistryDefinition) { - throw new AriesFrameworkError(`Could not get revocation registry definition ${revocationRegistryDefinitionId}`) - } - - const { revocationStatusList } = await registry.getRevocationStatusList( - this.agentContext, - revocationRegistryDefinitionId, - Math.floor(new Date().getTime() / 1000) // FIXME: Make optional and use now if not defined - ) - - if (!revocationStatusList) { - throw new AriesFrameworkError( - `Could not get current revocation status list for ${revocationRegistryDefinitionId}` - ) - } - - const { tailsFilePath: tailsLocation } = await downloadTailsFile( - this.agentContext, - revocationRegistryDefinition.value.tailsLocation, - revocationRegistryDefinition.value.tailsHash - ) - - // Update and publish the new revocation status list - const newRevocationStatusList = await anonCredsIssuerService.updateRevocationStatusList(this.agentContext, { - revocationStatusList, - revocationRegistryDefinition, - revoked: revokedIndexes, - tailsLocation - }) - - const result = await registry.registerRevocationStatusList(this.agentContext, { - revocationStatusList: newRevocationStatusList, - options: {}, - }) - - if (!result.revocationStatusListState) { - throw new AriesFrameworkError(`Cannot update revocation registry for ${revocationRegistryDefinitionId}`) - } - } - public async getCredential(credentialId: string) { return this.anonCredsHolderService.getCredential(this.agentContext, { credentialId }) } diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index 252a27af3a..663d9d0fda 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -1,5 +1,3 @@ -import type { AnonCredsCredentialDefinition, AnonCredsRevocationRegistryDefinition } from './models' - export interface AnonCredsCreateLinkSecretOptions { linkSecretId?: string setAsDefault?: boolean @@ -16,7 +14,6 @@ export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { issuerId: string tag: string credentialDefinitionId: string - tailsDirectoryPath: string maximumCredentialNumber: number } @@ -24,16 +21,15 @@ export interface AnonCredsRegisterRevocationStatusListOptions { issuerId: string issuanceByDefault: boolean revocationRegistryDefinitionId: string - tailsLocation: string } export interface AnonCredsUpdateRevocationStatusListOptions { - revokedCredentialIndexes: number[] - issuedCredentialIndexes: number[] + revokedCredentialIndexes?: number[] + issuedCredentialIndexes?: number[] revocationRegistryDefinitionId: string } export interface AnonCredsRevokeCredentialOptions { revocationRegistryDefinitionId: string - revokedIndexes: number[] + revokedCredentialIndexes: number[] } diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts index df3229d791..e693b723ac 100644 --- a/packages/anoncreds/src/AnonCredsModuleConfig.ts +++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts @@ -1,4 +1,7 @@ import type { AnonCredsRegistry } from './services' +import type { TailsFileManager } from './services/TailsFileManager' + +import { BasicTailsFileManager } from './services/BasicTailsFileManager' /** * @public @@ -17,10 +20,10 @@ export interface AnonCredsModuleConfigOptions { maximumCredentialNumberPerRevocationRegistry?: number /** - * Maximum credential number per revocation registry - * @default agent's data path + * Tails file manager for download/uploading tails files + * @default DefaultTailsFileManager (only for downloading tails files) */ - tailsDirectoryPath?: string + tailsFileManager?: TailsFileManager } /** @@ -43,8 +46,8 @@ export class AnonCredsModuleConfig { return this.options.maximumCredentialNumberPerRevocationRegistry ?? 1000 } - /** See {@link AnonCredsModuleConfigOptions.tailsDirectoryPath} */ - public get tailsDirectoryPath() { - return this.options.tailsDirectoryPath + /** See {@link AnonCredsModuleConfigOptions.tailsFileManager} */ + public get tailsFileManager() { + return this.options.tailsFileManager ?? new BasicTailsFileManager() } } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 066ee8ada9..4f05e6aa81 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -41,8 +41,15 @@ import { JsonTransformer, } from '@aries-framework/core' +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' import { AnonCredsError } from '../error' import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal' +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, + RevocationRegistryState, +} from '../repository' import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' import { @@ -53,13 +60,7 @@ import { createAndLinkAttachmentsToPreview, } from '../utils/credential' import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' -import { - AnonCredsCredentialDefinitionRepository, - AnonCredsRevocationRegistryDefinitionPrivateRepository, - AnonCredsRevocationRegistryDefinitionRepository, - RevocationRegistryState, -} from '../repository' -import { downloadTailsFile } from '../utils' +import { dateToTimestamp } from '../utils/timestamp' const ANONCREDS_CREDENTIAL_OFFER = 'anoncreds/credential-offer@v1.0' const ANONCREDS_CREDENTIAL_REQUEST = 'anoncreds/credential-request@v1.0' @@ -344,7 +345,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const result = await registry.getRevocationStatusList( agentContext, revocationRegistryDefinitionId, - Math.floor(new Date().getTime() / 1000) + dateToTimestamp(new Date()) ) if (!result.revocationStatusList) { @@ -358,11 +359,10 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService .resolve(AnonCredsRevocationRegistryDefinitionRepository) .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) - ;({ tailsFilePath } = await downloadTailsFile( - agentContext, - revocationRegistryDefinition.value.tailsLocation, - revocationRegistryDefinition.value.tailsHash - )) + const tailsFileManager = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager + ;({ tailsFilePath } = await tailsFileManager.downloadTailsFile(agentContext, { + revocationRegistryDefinition, + })) } const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index 2ad16e028a..a9f93eaba6 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -9,6 +9,7 @@ export * from './protocols' export { AnonCredsModule } from './AnonCredsModule' export { AnonCredsModuleConfig, AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' +export { TailsFileManager, BasicTailsFileManager } from './services' export { AnonCredsApi } from './AnonCredsApi' export * from './AnonCredsApiOptions' export { generateLegacyProverDidLikeString } from './utils/proverDid' diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index 247ddf989a..77427764fd 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -40,7 +40,7 @@ export interface CreateRevocationStatusListOptions { issuanceByDefault: boolean revocationRegistryDefinitionId: string revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition - tailsLocation: string + tailsFilePath: string } export interface UpdateRevocationStatusListOptions { @@ -49,7 +49,7 @@ export interface UpdateRevocationStatusListOptions { revoked?: number[] issued?: number[] timestamp?: number - tailsLocation: string + tailsFilePath: string } export interface CreateCredentialOfferOptions { diff --git a/packages/anoncreds/src/services/BasicTailsFileManager.ts b/packages/anoncreds/src/services/BasicTailsFileManager.ts new file mode 100644 index 0000000000..d4720c0776 --- /dev/null +++ b/packages/anoncreds/src/services/BasicTailsFileManager.ts @@ -0,0 +1,86 @@ +import type { TailsFileManager } from './TailsFileManager' +import type { AnonCredsRevocationRegistryDefinition } from '../models' +import type { AgentContext, FileSystem } from '@aries-framework/core' + +import { AriesFrameworkError, InjectionSymbols, TypedArrayEncoder } from '@aries-framework/core' + +export class BasicTailsFileManager implements TailsFileManager { + private tailsDirectoryPath?: string + + public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { + this.tailsDirectoryPath = options?.tailsDirectoryPath + } + + public getTailsBasePath(agentContext: AgentContext) { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + return `${this.tailsDirectoryPath ?? fileSystem.cachePath}/anoncreds/tails` + } + + public getTailsFilePath(agentContext: AgentContext, tailsHash: string) { + return `${this.getTailsBasePath(agentContext)}/${tailsHash}` + } + + public tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + const tailsFilePath = this.getTailsFilePath(agentContext, tailsHash) + return fileSystem.exists(tailsFilePath) + } + + public async uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise { + throw new AriesFrameworkError('BasicTailsFileManager only supports tails file downloading') + } + + public async downloadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise<{ + tailsFilePath: string + }> { + const { revocationRegistryDefinition } = options + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + + try { + agentContext.config.logger.debug( + `Checking to see if tails file for URL ${revocationRegistryDefinition.value.tailsLocation} has been stored in the FileSystem` + ) + + // hash is used as file identifier + const tailsExists = await this.tailsFileExists(agentContext, tailsHash) + const tailsFilePath = this.getTailsFilePath(agentContext, tailsHash) + agentContext.config.logger.debug( + `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` + ) + + if (!tailsExists) { + agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) + + // download file and verify hash + await fileSystem.downloadToFile(tailsLocation, tailsFilePath, { + verifyHash: { + algorithm: 'sha256', + hash: TypedArrayEncoder.fromBase58(tailsHash), + }, + }) + agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) + } + + return { + tailsFilePath, + } + } catch (error) { + agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { + error, + }) + throw error + } + } +} diff --git a/packages/anoncreds/src/services/TailsFileManager.ts b/packages/anoncreds/src/services/TailsFileManager.ts new file mode 100644 index 0000000000..20014395ad --- /dev/null +++ b/packages/anoncreds/src/services/TailsFileManager.ts @@ -0,0 +1,26 @@ +import type { AnonCredsRevocationRegistryDefinition } from '../models' +import type { AgentContext } from '@aries-framework/core' + +export interface TailsFileManager { + getTailsBasePath(agentContext: AgentContext): string + + getTailsFilePath(agentContext: AgentContext, tailsHash: string): string + + tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise + + uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise + + downloadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise<{ + tailsFilePath: string + }> +} diff --git a/packages/anoncreds/src/services/index.ts b/packages/anoncreds/src/services/index.ts index fe7b176754..ef5f39793f 100644 --- a/packages/anoncreds/src/services/index.ts +++ b/packages/anoncreds/src/services/index.ts @@ -5,3 +5,5 @@ export * from './AnonCredsIssuerServiceOptions' export * from './registry' export * from './AnonCredsVerifierService' export * from './AnonCredsVerifierServiceOptions' +export * from './TailsFileManager' +export * from './BasicTailsFileManager' diff --git a/packages/anoncreds/src/utils/getRevocationRegistries.ts b/packages/anoncreds/src/utils/getRevocationRegistries.ts index ffc402d2a4..3592aac960 100644 --- a/packages/anoncreds/src/utils/getRevocationRegistries.ts +++ b/packages/anoncreds/src/utils/getRevocationRegistries.ts @@ -4,10 +4,10 @@ import type { AgentContext } from '@aries-framework/core' import { AriesFrameworkError } from '@aries-framework/core' +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' import { AnonCredsRegistryService } from '../services' import { assertBestPracticeRevocationInterval } from './revocationInterval' -import { downloadTailsFile } from './tails' export async function getRevocationRegistriesForRequest( agentContext: AgentContext, @@ -88,8 +88,10 @@ export async function getRevocationRegistriesForRequest( ) } - const { tailsLocation, tailsHash } = revocationRegistryDefinition.value - const { tailsFilePath } = await downloadTailsFile(agentContext, tailsLocation, tailsHash) + const tailsFileManager = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager + const { tailsFilePath } = await tailsFileManager.downloadTailsFile(agentContext, { + revocationRegistryDefinition, + }) // const tails = await this.indyUtilitiesService.downloadTails(tailsHash, tailsLocation) revocationRegistries[revocationRegistryId] = { diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index 7fa0da87ed..206c94c79a 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -2,7 +2,6 @@ export { createRequestFromPreview } from './createRequestFromPreview' export { sortRequestedCredentialsMatches } from './sortRequestedCredentialsMatches' export { assertNoDuplicateGroupsNamesInProofRequest } from './hasDuplicateGroupNames' export { areAnonCredsProofRequestsEqual } from './areRequestsEqual' -export { downloadTailsFile } from './tails' export { assertBestPracticeRevocationInterval } from './revocationInterval' export { getRevocationRegistriesForRequest, getRevocationRegistriesForProof } from './getRevocationRegistries' export { encodeCredentialValue, checkValidCredentialValueEncoding } from './credential' diff --git a/packages/anoncreds/src/utils/tails.ts b/packages/anoncreds/src/utils/tails.ts deleted file mode 100644 index e706f00914..0000000000 --- a/packages/anoncreds/src/utils/tails.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { AgentContext, FileSystem } from '@aries-framework/core' - -import { TypedArrayEncoder, InjectionSymbols } from '@aries-framework/core' - -const getTailsFilePath = (cachePath: string, tailsHash: string) => `${cachePath}/anoncreds/tails/${tailsHash}` - -export function tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { - const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) - const tailsFilePath = getTailsFilePath(fileSystem.cachePath, tailsHash) - - return fileSystem.exists(tailsFilePath) -} - -export async function downloadTailsFile( - agentContext: AgentContext, - tailsLocation: string, - tailsHashBase58: string -): Promise<{ - tailsFilePath: string -}> { - const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) - - try { - agentContext.config.logger.debug( - `Checking to see if tails file for URL ${tailsLocation} has been stored in the FileSystem` - ) - - // hash is used as file identifier - const tailsExists = await tailsFileExists(agentContext, tailsHashBase58) - const tailsFilePath = getTailsFilePath(fileSystem.cachePath, tailsHashBase58) - agentContext.config.logger.debug( - `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` - ) - - if (!tailsExists) { - agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) - - // download file and verify hash - await fileSystem.downloadToFile(tailsLocation, tailsFilePath, { - verifyHash: { - algorithm: 'sha256', - hash: TypedArrayEncoder.fromBase58(tailsHashBase58), - }, - }) - agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) - } - - return { - tailsFilePath, - } - } catch (error) { - agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { - error, - }) - throw error - } -} diff --git a/packages/anoncreds/src/utils/timestamp.ts b/packages/anoncreds/src/utils/timestamp.ts new file mode 100644 index 0000000000..5e7da1c370 --- /dev/null +++ b/packages/anoncreds/src/utils/timestamp.ts @@ -0,0 +1 @@ +export const dateToTimestamp = (date: Date) => Math.floor(date.getTime() / 1000) diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index b5996f9ed4..5ebdba3ff0 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -34,6 +34,7 @@ import { parseSchemaId, } from '../../indy-sdk/src/anoncreds/utils/identifiers' import { parseIndyDid } from '../../indy-sdk/src/dids/didIndyUtil' +import { dateToTimestamp } from '../src/utils/timestamp' /** * In memory implementation of the {@link AnonCredsRegistry} interface. Useful for testing. @@ -343,7 +344,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { agentContext: AgentContext, options: RegisterRevocationStatusListOptions ): Promise { - const timestamp = (options.options.timestamp as number) ?? Math.floor(new Date().getTime() / 1000) + const timestamp = (options.options.timestamp as number) ?? dateToTimestamp(new Date()) const revocationStatusList = { ...options.revocationStatusList, timestamp, @@ -351,6 +352,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { if (!this.revocationStatusLists[options.revocationStatusList.revRegDefId]) { this.revocationStatusLists[options.revocationStatusList.revRegDefId] = {} } + this.revocationStatusLists[revocationStatusList.revRegDefId][timestamp.toString()] = revocationStatusList return { diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 1c99e1f0aa..2e646d3cb9 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -102,6 +102,7 @@ export class CredentialsApi implements Credent private credentialRepository: CredentialRepository private agentContext: AgentContext private didCommMessageRepository: DidCommMessageRepository + private revocationNotificationService: RevocationNotificationService private routingService: RoutingService private logger: Logger @@ -113,9 +114,7 @@ export class CredentialsApi implements Credent credentialRepository: CredentialRepository, mediationRecipientService: RoutingService, didCommMessageRepository: DidCommMessageRepository, - // only injected so the handlers will be registered - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _revocationNotificationService: RevocationNotificationService, + revocationNotificationService: RevocationNotificationService, config: CredentialsModuleConfig ) { this.messageSender = messageSender @@ -124,6 +123,7 @@ export class CredentialsApi implements Credent this.routingService = mediationRecipientService this.agentContext = agentContext this.didCommMessageRepository = didCommMessageRepository + this.revocationNotificationService = revocationNotificationService this.logger = logger this.config = config } @@ -569,6 +569,68 @@ export class CredentialsApi implements Credent } } + /** + * Send a revocation notification for a credential exchange record. Currently Revocation Notification V2 protocol is supported + * + * @param credentialRecordId The id of the credential record for which to send revocation notification + */ + public async sendRevocationNotification(options: SendRevocationNotificationOptions): Promise { + const { credentialRecordId, revocationId, revocationFormat, comment, requestAck } = options + + const credentialRecord = await this.getById(credentialRecordId) + + const { message } = await this.revocationNotificationService.v2CreateRevocationNotification({ + credentialId: revocationId, + revocationFormat, + comment, + requestAck, + }) + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + + // Use connection if present + if (credentialRecord.connectionId) { + const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection, + associatedRecord: credentialRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + } + // Use ~service decorator otherwise + else if (requestMessage?.service && offerMessage?.service) { + const recipientService = requestMessage.service + const ourService = offerMessage.service + + message.service = ourService + await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + await this.messageSender.sendMessageToService( + new OutboundMessageContext(message, { + agentContext: this.agentContext, + serviceParams: { + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + returnRoute: true, + }, + }) + ) + } + // Cannot send message without connectionId or ~service decorator + else { + throw new AriesFrameworkError( + `Cannot send revocation notification for credential record without connectionId or ~service decorator on credential offer / request.` + ) + } + } + /** * Send problem report message for a credential record * @param credentialRecordId The id of the credential record for which to send problem report diff --git a/packages/core/src/modules/credentials/CredentialsApiOptions.ts b/packages/core/src/modules/credentials/CredentialsApiOptions.ts index 430e65d317..9f49b1ca98 100644 --- a/packages/core/src/modules/credentials/CredentialsApiOptions.ts +++ b/packages/core/src/modules/credentials/CredentialsApiOptions.ts @@ -122,10 +122,14 @@ export interface AcceptCredentialOptions { } /** - * Interface for CredentialsApi.sendRevocationNotification. Will send notifications + * Interface for CredentialsApi.sendRevocationNotification. Will send a revoke message */ export interface SendRevocationNotificationOptions { - credentialRecordIds: string[] + credentialRecordId: string + revocationId: string // TODO: Get from record? + revocationFormat: string // TODO: Get from record? + comment?: string + requestAck?: boolean } /** diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts index 6a641d9457..1eab50e586 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts @@ -1,9 +1,9 @@ +import type { V2CreateRevocationNotificationMessageOptions } from './RevocationNotificationServiceOptions' import type { AgentContext } from '../../../../../agent' import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' import type { ConnectionRecord } from '../../../../connections' import type { RevocationNotificationReceivedEvent } from '../../../CredentialEvents' import type { V1RevocationNotificationMessage } from '../messages/V1RevocationNotificationMessage' -import type { V2RevocationNotificationMessage } from '../messages/V2RevocationNotificationMessage' import { EventEmitter } from '../../../../../agent/EventEmitter' import { MessageHandlerRegistry } from '../../../../../agent/MessageHandlerRegistry' @@ -16,7 +16,14 @@ import { CredentialEventTypes } from '../../../CredentialEvents' import { RevocationNotification } from '../../../models/RevocationNotification' import { CredentialRepository } from '../../../repository' import { V1RevocationNotificationHandler, V2RevocationNotificationHandler } from '../handlers' -import { v1ThreadRegex, v2IndyRevocationFormat, v2IndyRevocationIdentifierRegex } from '../util/revocationIdentifier' +import { V2RevocationNotificationMessage } from '../messages/V2RevocationNotificationMessage' +import { + v1ThreadRegex, + v2AnonCredsRevocationFormat, + v2AnonCredsRevocationIdentifierRegex, + v2IndyRevocationFormat, + v2IndyRevocationIdentifierRegex, +} from '../util/revocationIdentifier' @injectable() export class RevocationNotificationService { @@ -103,6 +110,26 @@ export class RevocationNotificationService { } } + /** + * Create a V2 Revocation Notification message + */ + + public async v2CreateRevocationNotification( + options: V2CreateRevocationNotificationMessageOptions + ): Promise<{ message: V2RevocationNotificationMessage }> { + const { credentialId, revocationFormat, comment, requestAck } = options + const message = new V2RevocationNotificationMessage({ + credentialId, + revocationFormat, + comment, + }) + if (requestAck) { + message.setPleaseAck() + } + + return { message } + } + /** * Process a received {@link V2RevocationNotificationMessage}. This will create a * {@link RevocationNotification} and store it in the corresponding {@link CredentialRecord} @@ -116,14 +143,15 @@ export class RevocationNotificationService { const credentialId = messageContext.message.credentialId - if (messageContext.message.revocationFormat !== v2IndyRevocationFormat) { + if (![v2IndyRevocationFormat, v2AnonCredsRevocationFormat].includes(messageContext.message.revocationFormat)) { throw new AriesFrameworkError( - `Unknown revocation format: ${messageContext.message.revocationFormat}. Supported formats are indy-anoncreds` + `Unknown revocation format: ${messageContext.message.revocationFormat}. Supported formats are indy-anoncreds and anoncreds` ) } try { - const credentialIdGroups = credentialId.match(v2IndyRevocationIdentifierRegex) + const credentialIdGroups = + credentialId.match(v2IndyRevocationIdentifierRegex) ?? credentialId.match(v2AnonCredsRevocationIdentifierRegex) if (!credentialIdGroups) { throw new AriesFrameworkError( `Incorrect revocation notification credentialId format: \n${credentialId}\ndoes not match\n"::"` diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts new file mode 100644 index 0000000000..406013b18b --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts @@ -0,0 +1,6 @@ +export interface V2CreateRevocationNotificationMessageOptions { + credentialId: string + revocationFormat: string + comment?: string + requestAck?: boolean +} diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts index 5413d4da87..e5696a92e1 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts @@ -1 +1,2 @@ export * from './RevocationNotificationService' +export * from './RevocationNotificationServiceOptions' diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts index 12f75569d7..c1bc1d35f2 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts @@ -9,3 +9,8 @@ export const v2IndyRevocationIdentifierRegex = /((?:[\dA-z]{21,22}):4:(?:[\dA-z]{21,22}):3:[Cc][Ll]:(?:(?:[1-9][0-9]*)|(?:[\dA-z]{21,22}:2:.+:[0-9.]+)):.+?:CL_ACCUM:(?:[\dA-z-]+))::(\d+)$/ export const v2IndyRevocationFormat = 'indy-anoncreds' + +// CredentialID = :: +export const v2AnonCredsRevocationIdentifierRegex = /([a-zA-Z0-9+\-.]+:.+)::(\d+)$/ + +export const v2AnonCredsRevocationFormat = 'anoncreds' diff --git a/packages/anoncreds/src/services/TailsFileUploader.ts b/samples/tails/FullTailsFileManager.ts similarity index 51% rename from packages/anoncreds/src/services/TailsFileUploader.ts rename to samples/tails/FullTailsFileManager.ts index 1d403ad2c3..66e0b7466d 100644 --- a/packages/anoncreds/src/services/TailsFileUploader.ts +++ b/samples/tails/FullTailsFileManager.ts @@ -1,20 +1,19 @@ -import { AgentContext, utils } from '@aries-framework/core' -import { AnonCredsRevocationRegistryDefinition } from '../models' +import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' +import type { AgentContext } from '@aries-framework/core' + +import { BasicTailsFileManager } from '@aries-framework/anoncreds' +import { utils } from '@aries-framework/core' import FormData from 'form-data' import fs from 'fs' -export interface TailsFileUploader { - uploadTails( - agentContext: AgentContext, - options: { - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition - localTailsFilePath: string - } - ): Promise -} +export class FullTailsFileManager extends BasicTailsFileManager { + private tailsServerBaseUrl?: string + public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { + super(options) + this.tailsServerBaseUrl = options?.tailsServerBaseUrl + } -export class DefaultTailsFileUploader implements TailsFileUploader { - public async uploadTails( + public async uploadTailsFile( agentContext: AgentContext, options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition @@ -28,7 +27,7 @@ export class DefaultTailsFileUploader implements TailsFileUploader { const readStream = fs.createReadStream(localTailsFilePath) data.append('file', readStream) const response = await agentContext.config.agentDependencies.fetch( - `http://localhost:3001/${encodeURIComponent(tailsFileId)}`, + `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}`, { method: 'PUT', body: data, @@ -37,6 +36,6 @@ export class DefaultTailsFileUploader implements TailsFileUploader { if (response.status !== 200) { throw new Error('Cannot upload tails file') } - return `http://localhost:3001/${encodeURIComponent(tailsFileId)}` + return `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}` } } diff --git a/samples/tails/package.json b/samples/tails/package.json index 8bc0e430ca..3c0e6f3761 100644 --- a/samples/tails/package.json +++ b/samples/tails/package.json @@ -15,11 +15,13 @@ "ts-node": "^10.4.0" }, "dependencies": { + "@aries-framework/anoncreds": "^0.3.3", "@aries-framework/core": "^0.3.3", "@types/express": "^4.17.13", "@types/multer": "^1.4.7", "@types/uuid": "^9.0.1", "@types/ws": "^8.5.4", + "form-data": "^4.0.0", "multer": "^1.4.5-lts.1" } } From a8d824d55695cefc0c34b47be8af0af2f42402cd Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 11 Apr 2023 12:08:11 -0300 Subject: [PATCH 10/27] some clean up of tests and naming Signed-off-by: Ariel Gentile --- ...Manager.ts => InMemoryTailsFileService.ts} | 4 +- .../anoncreds-rs/tests/LocalDidResolver.ts | 30 + .../anoncreds-rs/tests/anoncreds-flow.test.ts | 790 ++++++------------ .../tests/anoncreds-revocation.test.ts | 444 ---------- packages/anoncreds-rs/tests/anoncredsSetup.ts | 49 +- .../v2-credential-revocation.e2e.test.ts | 3 + .../tests/v2-credentials.e2e.test.ts | 22 +- packages/anoncreds/src/AnonCredsModule.ts | 2 + .../anoncreds/src/AnonCredsModuleConfig.ts | 10 +- .../src/__tests__/AnonCredsModule.test.ts | 10 +- packages/anoncreds/src/index.ts | 1 - packages/anoncreds/src/services/index.ts | 3 +- .../BasicTailsFileService.ts} | 8 +- .../TailsFileService.ts} | 4 +- .../anoncreds/src/services/tails/index.ts | 2 + 15 files changed, 357 insertions(+), 1025 deletions(-) rename packages/anoncreds-rs/tests/{InMemoryTailsFileManager.ts => InMemoryTailsFileService.ts} (93%) create mode 100644 packages/anoncreds-rs/tests/LocalDidResolver.ts delete mode 100644 packages/anoncreds-rs/tests/anoncreds-revocation.test.ts rename packages/anoncreds/src/services/{BasicTailsFileManager.ts => tails/BasicTailsFileService.ts} (90%) rename packages/anoncreds/src/services/{TailsFileManager.ts => tails/TailsFileService.ts} (85%) create mode 100644 packages/anoncreds/src/services/tails/index.ts diff --git a/packages/anoncreds-rs/tests/InMemoryTailsFileManager.ts b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts similarity index 93% rename from packages/anoncreds-rs/tests/InMemoryTailsFileManager.ts rename to packages/anoncreds-rs/tests/InMemoryTailsFileService.ts index fefdf23526..a802fe4dd8 100644 --- a/packages/anoncreds-rs/tests/InMemoryTailsFileManager.ts +++ b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts @@ -1,9 +1,9 @@ import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' -import { BasicTailsFileManager } from '@aries-framework/anoncreds' +import { BasicTailsFileService } from '@aries-framework/anoncreds' -export class InMemoryTailsFileManager extends BasicTailsFileManager { +export class InMemoryTailsFileService extends BasicTailsFileService { private tailsFilePaths: Record = {} public async uploadTailsFile( diff --git a/packages/anoncreds-rs/tests/LocalDidResolver.ts b/packages/anoncreds-rs/tests/LocalDidResolver.ts new file mode 100644 index 0000000000..1f50c1b3aa --- /dev/null +++ b/packages/anoncreds-rs/tests/LocalDidResolver.ts @@ -0,0 +1,30 @@ +import type { DidResolutionResult, DidResolver, AgentContext } from '@aries-framework/core' + +import { DidsApi } from '@aries-framework/core' + +export class LocalDidResolver implements DidResolver { + public readonly supportedMethods = ['sov', 'indy'] + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocumentMetadata = {} + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + const didRecord = (await didsApi.getCreatedDids()).find((record) => record.did === did) + if (!didRecord) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}'`, + }, + } + } + return { + didDocument: didRecord.didDocument ?? null, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } +} diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 2ec6f6785e..69cb04e0f9 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -27,8 +27,6 @@ import { AnonCredsCredentialFormatService, } from '@aries-framework/anoncreds' import { - ConsoleLogger, - LogLevel, CredentialState, CredentialExchangeRecord, CredentialPreviewAttribute, @@ -48,18 +46,16 @@ import { AnonCredsRsHolderService } from '../src/services/AnonCredsRsHolderServi import { AnonCredsRsIssuerService } from '../src/services/AnonCredsRsIssuerService' import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierService' -import { InMemoryTailsFileManager } from './InMemoryTailsFileManager' +import { InMemoryTailsFileService } from './InMemoryTailsFileService' const registry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) -const tailsFileManager = new InMemoryTailsFileManager() +const tailsFileManager = new InMemoryTailsFileService() const anonCredsModuleConfig = new AnonCredsModuleConfig({ registries: [registry], - tailsFileManager, + tailsFileService: tailsFileManager, }) -const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs', { - logger: new ConsoleLogger(LogLevel.debug), -}) +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') const anonCredsVerifierService = new AnonCredsRsVerifierService() const anonCredsHolderService = new AnonCredsRsHolderService() const anonCredsIssuerService = new AnonCredsRsIssuerService() @@ -90,365 +86,96 @@ const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' // FIXME: Re-include in tests when NodeJS wrapper performance is improved describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', () => { - test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { - const schema = await anonCredsIssuerService.createSchema(agentContext, { - attrNames: ['name', 'age'], - issuerId: indyDid, - name: 'Employee Credential', - version: '1.0.0', - }) - - const { schemaState } = await registry.registerSchema(agentContext, { - schema, - options: {}, - }) - - const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = - await anonCredsIssuerService.createCredentialDefinition(agentContext, { - issuerId: indyDid, - schemaId: schemaState.schemaId as string, - schema, - tag: 'Employee Credential', - supportRevocation: false, - }) - - const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { - credentialDefinition, - options: {}, - }) - - if ( - !credentialDefinitionState.credentialDefinition || - !credentialDefinitionState.credentialDefinitionId || - !schemaState.schema || - !schemaState.schemaId - ) { - throw new Error('Failed to create schema or credential definition') - } - - if ( - !credentialDefinitionState.credentialDefinition || - !credentialDefinitionState.credentialDefinitionId || - !schemaState.schema || - !schemaState.schemaId - ) { - throw new Error('Failed to create schema or credential definition') - } - - if (!credentialDefinitionPrivate || !keyCorrectnessProof) { - throw new Error('Failed to get private part of credential definition') - } - - await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( - agentContext, - new AnonCredsSchemaRecord({ - schema: schemaState.schema, - schemaId: schemaState.schemaId, - methodName: 'inMemory', - }) - ) - - await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( - agentContext, - new AnonCredsCredentialDefinitionRecord({ - credentialDefinition: credentialDefinitionState.credentialDefinition, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - methodName: 'inMemory', - }) - ) - - await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( - agentContext, - new AnonCredsCredentialDefinitionPrivateRecord({ - value: credentialDefinitionPrivate, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }) - ) - - await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( - agentContext, - new AnonCredsKeyCorrectnessProofRecord({ - value: keyCorrectnessProof, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }) - ) - - const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) - expect(linkSecret.linkSecretId).toBe('linkSecretId') - - await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( - agentContext, - new AnonCredsLinkSecretRecord({ - value: linkSecret.linkSecretValue, - linkSecretId: linkSecret.linkSecretId, - }) - ) - - const holderCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalSent, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', - }) - - const issuerCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalReceived, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', - }) - - const credentialAttributes = [ - new CredentialPreviewAttribute({ - name: 'name', - value: 'John', - }), - new CredentialPreviewAttribute({ - name: 'age', - value: '25', - }), - ] - - // Holder creates proposal - holderCredentialRecord.credentialAttributes = credentialAttributes - const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { - credentialRecord: holderCredentialRecord, - credentialFormats: { - anoncreds: { - attributes: credentialAttributes, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, - }, - }) - - // Issuer processes and accepts proposal - await anoncredsCredentialFormatService.processProposal(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: proposalAttachment, - }) - // Set attributes on the credential record, this is normally done by the protocol service - issuerCredentialRecord.credentialAttributes = credentialAttributes - const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { - credentialRecord: issuerCredentialRecord, - proposalAttachment: proposalAttachment, - }) + afterEach(() => { + inMemoryStorageService.records = {} + }) - // Holder processes and accepts offer - await anoncredsCredentialFormatService.processOffer(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: offerAttachment, - }) - const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { - credentialRecord: holderCredentialRecord, - offerAttachment, - credentialFormats: { - anoncreds: { - linkSecretId: linkSecret.linkSecretId, - }, - }, - }) + test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: false }) + }) - // Make sure the request contains an entropy and does not contain a prover_did field - expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() - expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() + test('issuance and verification flow starting from proposal without negotiation and with revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: true }) + }) +}) - // Issuer processes and accepts request - await anoncredsCredentialFormatService.processRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: requestAttachment, - }) - const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - requestAttachment, - offerAttachment, - }) +async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean }) { + const { issuerId, revocable } = options - // Holder processes and accepts credential - await anoncredsCredentialFormatService.processCredential(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: credentialAttachment, - requestAttachment, - }) + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId, + name: 'Employee Credential', + version: '1.0.0', + }) - expect(holderCredentialRecord.credentials).toEqual([ - { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, - ]) + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) - const credentialId = holderCredentialRecord.credentials[0].credentialRecordId - const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { - credentialId, - }) + if (!schemaState.schema || !schemaState.schemaId) { + throw new Error('Failed to create schema') + } - expect(anonCredsCredential).toEqual({ - credentialId, - attributes: { - age: '25', - name: 'John', - }, + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: null, - credentialRevocationId: undefined, // FIXME: should be null? methodName: 'inMemory', }) + ) - expect(holderCredentialRecord.metadata.data).toEqual({ - '_anoncreds/credential': { - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, - '_anoncreds/credentialRequest': { - link_secret_blinding_data: expect.any(Object), - link_secret_name: expect.any(String), - nonce: expect.any(String), - }, - }) - - expect(issuerCredentialRecord.metadata.data).toEqual({ - '_anoncreds/credential': { - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, - }) - - const holderProofRecord = new ProofExchangeRecord({ - protocolVersion: 'v1', - state: ProofState.ProposalSent, - threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', - }) - const verifierProofRecord = new ProofExchangeRecord({ - protocolVersion: 'v1', - state: ProofState.ProposalReceived, - threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', - }) - - const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { - proofFormats: { - anoncreds: { - attributes: [ - { - name: 'name', - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - value: 'John', - referent: '1', - }, - ], - predicates: [ - { - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - name: 'age', - predicate: '>=', - threshold: 18, - }, - ], - name: 'Proof Request', - version: '1.0', - }, - }, - proofRecord: holderProofRecord, - }) - - await anoncredsProofFormatService.processProposal(agentContext, { - attachment: proofProposalAttachment, - proofRecord: verifierProofRecord, - }) - - const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { - proofRecord: verifierProofRecord, - proposalAttachment: proofProposalAttachment, - }) - - await anoncredsProofFormatService.processRequest(agentContext, { - attachment: proofRequestAttachment, - proofRecord: holderProofRecord, - }) - - const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { - proofRecord: holderProofRecord, - requestAttachment: proofRequestAttachment, - proposalAttachment: proofProposalAttachment, - }) - - const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { - attachment: proofAttachment, - proofRecord: verifierProofRecord, - requestAttachment: proofRequestAttachment, - }) - - expect(isValid).toBe(true) - }) - - test.only('issuance and verification flow starting from proposal without negotiation and with revocation', async () => { - const schema = await anonCredsIssuerService.createSchema(agentContext, { - attrNames: ['name', 'age'], + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { issuerId: indyDid, - name: 'Employee Credential', - version: '1.0.0', - }) - - const { schemaState } = await registry.registerSchema(agentContext, { + schemaId: schemaState.schemaId as string, schema, - options: {}, + tag: 'Employee Credential', + supportRevocation: revocable, }) - if (!schemaState.schema || !schemaState.schemaId) { - throw new Error('Failed to create schema') - } + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) - await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( - agentContext, - new AnonCredsSchemaRecord({ - schema: schemaState.schema, - schemaId: schemaState.schemaId, - methodName: 'inMemory', - }) - ) + if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { + throw new Error('Failed to create credential definition') + } - const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = - await anonCredsIssuerService.createCredentialDefinition(agentContext, { - issuerId: indyDid, - schemaId: schemaState.schemaId as string, - schema, - tag: 'Employee Credential', - supportRevocation: true, - }) + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } - const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { - credentialDefinition, - options: {}, + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', }) + ) - if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { - throw new Error('Failed to create credential definition') - } - - if (!credentialDefinitionPrivate || !keyCorrectnessProof) { - throw new Error('Failed to get private part of credential definition') - } - - await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( - agentContext, - new AnonCredsCredentialDefinitionRecord({ - credentialDefinition: credentialDefinitionState.credentialDefinition, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - methodName: 'inMemory', - }) - ) - - await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( - agentContext, - new AnonCredsCredentialDefinitionPrivateRecord({ - value: credentialDefinitionPrivate, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }) - ) + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) - await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( - agentContext, - new AnonCredsKeyCorrectnessProofRecord({ - value: keyCorrectnessProof, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }) - ) + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + let revocationRegistryDefinitionId: string | undefined + if (revocable) { const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { issuerId: indyDid, @@ -467,9 +194,11 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( options: {}, }) + revocationRegistryDefinitionId = revocationRegistryDefinitionState.revocationRegistryDefinitionId + if ( !revocationRegistryDefinitionState.revocationRegistryDefinition || - !revocationRegistryDefinitionState.revocationRegistryDefinitionId || + !revocationRegistryDefinitionId || !revocationRegistryDefinitionPrivate ) { throw new Error('Failed to create revocation registry') @@ -479,7 +208,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( agentContext, new AnonCredsRevocationRegistryDefinitionRecord({ revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, - revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + revocationRegistryDefinitionId, }) ) @@ -489,7 +218,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( state: RevocationRegistryState.Active, value: revocationRegistryDefinitionPrivate, credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, - revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + revocationRegistryDefinitionId, }) ) @@ -497,7 +226,7 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( issuanceByDefault: true, issuerId: indyDid, revocationRegistryDefinition, - revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + revocationRegistryDefinitionId, tailsFilePath: localTailsFilePath, }) @@ -517,214 +246,215 @@ describeRunInNodeVersion([18], 'AnonCreds format services using anoncreds-rs', ( revocationStatusList: revocationStatusListState.revocationStatusList, }) ) + } - const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) - expect(linkSecret.linkSecretId).toBe('linkSecretId') + 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', + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, }) + ) - const issuerCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalReceived, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', - }) + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) - const credentialAttributes = [ - new CredentialPreviewAttribute({ - name: 'name', - value: 'John', - }), - new CredentialPreviewAttribute({ - name: 'age', - value: '25', - }), - ] - - // Holder creates proposal - holderCredentialRecord.credentialAttributes = credentialAttributes - const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { - credentialRecord: holderCredentialRecord, - credentialFormats: { - anoncreds: { - attributes: credentialAttributes, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, + const credentialAttributes = [ + new CredentialPreviewAttribute({ + name: 'name', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + value: '25', + }), + ] + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + anoncreds: { + attributes: credentialAttributes, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, }, - }) + }, + }) - // Issuer processes and accepts proposal - await anoncredsCredentialFormatService.processProposal(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: proposalAttachment, - }) - // Set attributes on the credential record, this is normally done by the protocol service - issuerCredentialRecord.credentialAttributes = credentialAttributes - const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { - credentialRecord: issuerCredentialRecord, - proposalAttachment: proposalAttachment, - }) + // Issuer processes and accepts proposal + await anoncredsCredentialFormatService.processProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: proposalAttachment, + }) + // Set attributes on the credential record, this is normally done by the protocol service + issuerCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + proposalAttachment: proposalAttachment, + }) - // Holder processes and accepts offer - await anoncredsCredentialFormatService.processOffer(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: offerAttachment, - }) - const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { - credentialRecord: holderCredentialRecord, - offerAttachment, - credentialFormats: { - anoncreds: { - linkSecretId: linkSecret.linkSecretId, - }, + // Holder processes and accepts offer + await anoncredsCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + anoncreds: { + linkSecretId: linkSecret.linkSecretId, }, - }) + }, + }) - // Make sure the request contains an entropy and does not contain a prover_did field - expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() - expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() + // Make sure the request contains an entropy and does not contain a prover_did field + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() - // Issuer processes and accepts request - await anoncredsCredentialFormatService.processRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: requestAttachment, - }) - const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - requestAttachment, - offerAttachment, - }) + // Issuer processes and accepts request + await anoncredsCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + }) - // Holder processes and accepts credential - await anoncredsCredentialFormatService.processCredential(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: credentialAttachment, - requestAttachment, - }) + // Holder processes and accepts credential + await anoncredsCredentialFormatService.processCredential(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) - expect(holderCredentialRecord.credentials).toEqual([ - { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, - ]) + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, + ]) - const credentialId = holderCredentialRecord.credentials[0].credentialRecordId - const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { - credentialId, - }) + 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: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - credentialRevocationId: '1', - methodName: 'inMemory', - }) + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: '25', + name: 'John', + }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, + credentialRevocationId: revocable ? '1' : undefined, + methodName: 'inMemory', + }) - expect(holderCredentialRecord.metadata.data).toEqual({ - '_anoncreds/credential': { + const expectedCredentialMetadata = revocable + ? { schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - credentialRevocationId: '1', - }, - '_anoncreds/credentialRequest': { - link_secret_blinding_data: expect.any(Object), - link_secret_name: expect.any(String), - nonce: expect.any(String), - }, - }) - - expect(issuerCredentialRecord.metadata.data).toEqual({ - '_anoncreds/credential': { - revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, + revocationRegistryId: revocationRegistryDefinitionId, credentialRevocationId: '1', + } + : { schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, - }) + } + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': expectedCredentialMetadata, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) - const holderProofRecord = new ProofExchangeRecord({ - protocolVersion: 'v1', - state: ProofState.ProposalSent, - threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', - }) - const verifierProofRecord = new ProofExchangeRecord({ - protocolVersion: 'v1', - state: ProofState.ProposalReceived, - threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', - }) + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': expectedCredentialMetadata, + }) - const nrpRequestedTime = dateToTimestamp(new Date()) - - const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { - proofFormats: { - anoncreds: { - attributes: [ - { - name: 'name', - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - value: 'John', - referent: '1', - }, - ], - predicates: [ - { - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - name: 'age', - predicate: '>=', - threshold: 18, - }, - ], - name: 'Proof Request', - version: '1.0', - nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, - }, - }, - proofRecord: holderProofRecord, - }) + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalReceived, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) - await anoncredsProofFormatService.processProposal(agentContext, { - attachment: proofProposalAttachment, - proofRecord: verifierProofRecord, - }) + const nrpRequestedTime = dateToTimestamp(new Date()) + + const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { + proofFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, + }, + }, + proofRecord: holderProofRecord, + }) - const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { - proofRecord: verifierProofRecord, - proposalAttachment: proofProposalAttachment, - }) + await anoncredsProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) - await anoncredsProofFormatService.processRequest(agentContext, { - attachment: proofRequestAttachment, - proofRecord: holderProofRecord, - }) + const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) - const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { - proofRecord: holderProofRecord, - requestAttachment: proofRequestAttachment, - proposalAttachment: proofProposalAttachment, - }) + await anoncredsProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) - const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { - attachment: proofAttachment, - proofRecord: verifierProofRecord, - requestAttachment: proofRequestAttachment, - }) + const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) - expect(isValid).toBe(true) + const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, }) -}) + + expect(isValid).toBe(true) +} diff --git a/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts b/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts deleted file mode 100644 index 8ea5426035..0000000000 --- a/packages/anoncreds-rs/tests/anoncreds-revocation.test.ts +++ /dev/null @@ -1,444 +0,0 @@ -import type { AnonCredsCredentialRequest } from '@aries-framework/anoncreds' -import type { Wallet } from '@aries-framework/core' - -import { - AnonCredsRevocationRegistryDefinitionPrivateRecord, - AnonCredsRevocationRegistryDefinitionPrivateRepository, - AnonCredsRevocationRegistryDefinitionRecord, - AnonCredsRevocationRegistryDefinitionRepository, - AnonCredsRevocationStatusListRecord, - AnonCredsRevocationStatusListRepository, - RevocationRegistryState, - AnonCredsModuleConfig, - AnonCredsHolderServiceSymbol, - AnonCredsIssuerServiceSymbol, - AnonCredsVerifierServiceSymbol, - AnonCredsSchemaRecord, - AnonCredsSchemaRepository, - AnonCredsCredentialDefinitionRepository, - AnonCredsCredentialDefinitionRecord, - AnonCredsCredentialDefinitionPrivateRepository, - AnonCredsCredentialDefinitionPrivateRecord, - AnonCredsKeyCorrectnessProofRepository, - AnonCredsKeyCorrectnessProofRecord, - AnonCredsLinkSecretRepository, - AnonCredsLinkSecretRecord, - AnonCredsProofFormatService, - AnonCredsCredentialFormatService, - AnonCredsRegistryService, -} from '@aries-framework/anoncreds' -import { - ConsoleLogger, - LogLevel, - CredentialState, - CredentialExchangeRecord, - CredentialPreviewAttribute, - InjectionSymbols, - ProofState, - ProofExchangeRecord, -} from '@aries-framework/core' -import { Subject } from 'rxjs' - -import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' -import { describeRunInNodeVersion } from '../../../tests/runInVersion' -import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' -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' - -import { InMemoryTailsFileManager } from './InMemoryTailsFileManager' - -const registry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) -const tailsFileManager = new InMemoryTailsFileManager() -const anonCredsModuleConfig = new AnonCredsModuleConfig({ - registries: [registry], - tailsFileManager, -}) - -const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs', { - logger: new ConsoleLogger(LogLevel.debug), -}) -const anonCredsVerifierService = new AnonCredsRsVerifierService() -const anonCredsHolderService = new AnonCredsRsHolderService() -const anonCredsIssuerService = new AnonCredsRsIssuerService() - -const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet - -const inMemoryStorageService = new InMemoryStorageService() -const agentContext = getAgentContext({ - registerInstances: [ - [InjectionSymbols.Stop$, new Subject()], - [InjectionSymbols.AgentDependencies, agentDependencies], - [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], - [InjectionSymbols.StorageService, inMemoryStorageService], - [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], - [AnonCredsHolderServiceSymbol, anonCredsHolderService], - [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], - [AnonCredsRegistryService, new AnonCredsRegistryService()], - [AnonCredsModuleConfig, anonCredsModuleConfig], - ], - agentConfig, - wallet, -}) - -const anoncredsCredentialFormatService = new AnonCredsCredentialFormatService() -const anoncredsProofFormatService = new AnonCredsProofFormatService() - -const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' - -// FIXME: Re-include in tests when NodeJS wrapper performance is improved -describeRunInNodeVersion([18], 'AnonCreds API using anoncreds-rs', () => { - test.only('issuance and revocation', async () => { - const schema = await anonCredsIssuerService.createSchema(agentContext, { - attrNames: ['name', 'age'], - issuerId: indyDid, - name: 'Employee Credential', - version: '1.0.0', - }) - - const { schemaState } = await registry.registerSchema(agentContext, { - schema, - options: {}, - }) - - if (!schemaState.schema || !schemaState.schemaId) { - throw new Error('Failed to create schema') - } - - await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( - agentContext, - new AnonCredsSchemaRecord({ - schema: schemaState.schema, - schemaId: schemaState.schemaId, - methodName: 'inMemory', - }) - ) - - const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = - await anonCredsIssuerService.createCredentialDefinition(agentContext, { - issuerId: indyDid, - schemaId: schemaState.schemaId as string, - schema, - tag: 'Employee Credential', - supportRevocation: true, - }) - - const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { - credentialDefinition, - options: {}, - }) - - if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { - throw new Error('Failed to create credential definition') - } - - if (!credentialDefinitionPrivate || !keyCorrectnessProof) { - throw new Error('Failed to get private part of credential definition') - } - - await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( - agentContext, - new AnonCredsCredentialDefinitionRecord({ - credentialDefinition: credentialDefinitionState.credentialDefinition, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - methodName: 'inMemory', - }) - ) - - await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( - agentContext, - new AnonCredsCredentialDefinitionPrivateRecord({ - value: credentialDefinitionPrivate, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }) - ) - - await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( - agentContext, - new AnonCredsKeyCorrectnessProofRecord({ - value: keyCorrectnessProof, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }) - ) - - const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = - await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { - issuerId: indyDid, - credentialDefinition, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - maximumCredentialNumber: 100, - tailsDirectoryPath: tailsFileManager.getTailsBasePath(agentContext), - tag: 'default', - }) - - // At this moment, tails file should be published and a valid public URL will be received - const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation - - const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { - revocationRegistryDefinition, - options: {}, - }) - - if ( - !revocationRegistryDefinitionState.revocationRegistryDefinition || - !revocationRegistryDefinitionState.revocationRegistryDefinitionId || - !revocationRegistryDefinitionPrivate - ) { - throw new Error('Failed to create revocation registry') - } - - await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionRepository).save( - agentContext, - new AnonCredsRevocationRegistryDefinitionRecord({ - revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, - revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - }) - ) - - await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository).save( - agentContext, - new AnonCredsRevocationRegistryDefinitionPrivateRecord({ - state: RevocationRegistryState.Active, - value: revocationRegistryDefinitionPrivate, - credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, - revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - }) - ) - - const createdRevocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { - issuanceByDefault: true, - issuerId: indyDid, - revocationRegistryDefinition, - revocationRegistryDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - tailsFilePath: localTailsFilePath, - }) - - const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { - revocationStatusList: createdRevocationStatusList, - options: {}, - }) - - if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { - throw new Error('Failed to create revocation status list') - } - - await agentContext.dependencyManager.resolve(AnonCredsRevocationStatusListRepository).save( - agentContext, - new AnonCredsRevocationStatusListRecord({ - credentialDefinitionId: revocationRegistryDefinition.credDefId, - revocationStatusList: revocationStatusListState.revocationStatusList, - }) - ) - - const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) - expect(linkSecret.linkSecretId).toBe('linkSecretId') - - await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( - agentContext, - new AnonCredsLinkSecretRecord({ - value: linkSecret.linkSecretValue, - linkSecretId: linkSecret.linkSecretId, - }) - ) - - const holderCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalSent, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', - }) - - const issuerCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalReceived, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', - }) - - const credentialAttributes = [ - new CredentialPreviewAttribute({ - name: 'name', - value: 'John', - }), - new CredentialPreviewAttribute({ - name: 'age', - value: '25', - }), - ] - - // Holder creates proposal - holderCredentialRecord.credentialAttributes = credentialAttributes - const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { - credentialRecord: holderCredentialRecord, - credentialFormats: { - anoncreds: { - attributes: credentialAttributes, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, - }, - }) - - // Issuer processes and accepts proposal - await anoncredsCredentialFormatService.processProposal(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: proposalAttachment, - }) - // Set attributes on the credential record, this is normally done by the protocol service - issuerCredentialRecord.credentialAttributes = credentialAttributes - const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { - credentialRecord: issuerCredentialRecord, - proposalAttachment: proposalAttachment, - }) - - // Holder processes and accepts offer - await anoncredsCredentialFormatService.processOffer(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: offerAttachment, - }) - const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { - credentialRecord: holderCredentialRecord, - offerAttachment, - credentialFormats: { - anoncreds: { - linkSecretId: linkSecret.linkSecretId, - }, - }, - }) - - // Make sure the request contains an entropy and does not contain a prover_did field - expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() - expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() - - // Issuer processes and accepts request - await anoncredsCredentialFormatService.processRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: requestAttachment, - }) - const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - requestAttachment, - offerAttachment, - }) - - // Holder processes and accepts credential - await anoncredsCredentialFormatService.processCredential(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: credentialAttachment, - requestAttachment, - }) - - expect(holderCredentialRecord.credentials).toEqual([ - { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, - ]) - - const credentialId = holderCredentialRecord.credentials[0].credentialRecordId - const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { - credentialId, - }) - - expect(anonCredsCredential).toEqual({ - credentialId, - attributes: { - age: '25', - name: 'John', - }, - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - credentialRevocationId: '1', - methodName: 'inMemory', - }) - - expect(holderCredentialRecord.metadata.data).toEqual({ - '_anoncreds/credential': { - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - credentialRevocationId: '1', - }, - '_anoncreds/credentialRequest': { - link_secret_blinding_data: expect.any(Object), - link_secret_name: expect.any(String), - nonce: expect.any(String), - }, - }) - - expect(issuerCredentialRecord.metadata.data).toEqual({ - '_anoncreds/credential': { - revocationRegistryId: revocationRegistryDefinitionState.revocationRegistryDefinitionId, - credentialRevocationId: '1', - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, - }) - - const holderProofRecord = new ProofExchangeRecord({ - protocolVersion: 'v1', - state: ProofState.ProposalSent, - threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', - }) - const verifierProofRecord = new ProofExchangeRecord({ - protocolVersion: 'v1', - state: ProofState.ProposalReceived, - threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', - }) - - const nrpRequestedTime = dateToTimestamp(new Date()) - - const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { - proofFormats: { - anoncreds: { - attributes: [ - { - name: 'name', - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - value: 'John', - referent: '1', - }, - ], - predicates: [ - { - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - name: 'age', - predicate: '>=', - threshold: 18, - }, - ], - name: 'Proof Request', - version: '1.0', - nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, - }, - }, - proofRecord: holderProofRecord, - }) - - await anoncredsProofFormatService.processProposal(agentContext, { - attachment: proofProposalAttachment, - proofRecord: verifierProofRecord, - }) - - const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { - proofRecord: verifierProofRecord, - proposalAttachment: proofProposalAttachment, - }) - - await anoncredsProofFormatService.processRequest(agentContext, { - attachment: proofRequestAttachment, - proofRecord: holderProofRecord, - }) - - const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { - proofRecord: holderProofRecord, - requestAttachment: proofRequestAttachment, - proposalAttachment: proofProposalAttachment, - }) - - const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { - attachment: proofAttachment, - proofRecord: verifierProofRecord, - requestAttachment: proofRequestAttachment, - }) - - expect(isValid).toBe(true) - }) -}) diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index 3634865719..836cc513e6 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -16,7 +16,7 @@ import type { EventReplaySubject } from '../../core/tests' import type { AutoAcceptProof, ConnectionRecord } from '@aries-framework/core' import { - TypedArrayEncoder, + DidDocumentBuilder, CacheModule, InMemoryLruCache, Agent, @@ -36,30 +36,22 @@ import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { randomUUID } from 'crypto' import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../../anoncreds/src' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { AskarModule } from '../../askar/src' import { askarModuleConfig } from '../../askar/tests/helpers' import { sleep } from '../../core/src/utils/sleep' import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' import { getAgentOptions, - importExistingIndyDidFromPrivateKey, makeConnection, - publicDidSeed, waitForCredentialRecordSubject, waitForProofExchangeRecordSubject, } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' -import { - IndyVdrAnonCredsRegistry, - IndyVdrSovDidResolver, - IndyVdrModule, - IndyVdrIndyDidResolver, - IndyVdrIndyDidRegistrar, -} from '../../indy-vdr/src' -import { indyVdrModuleConfig } from '../../indy-vdr/tests/helpers' import { AnonCredsRsModule } from '../src' -import { InMemoryTailsFileManager } from './InMemoryTailsFileManager' +import { InMemoryTailsFileService } from './InMemoryTailsFileService' +import { LocalDidResolver } from './LocalDidResolver' // Helper type to get the type of the agents (with the custom modules) for the credential tests export type AnonCredsTestsAgent = Agent< @@ -96,16 +88,14 @@ export const getAnonCredsModules = ({ ], }), anoncreds: new AnonCredsModule({ - registries: registries ?? [new IndyVdrAnonCredsRegistry()], - tailsFileManager: new InMemoryTailsFileManager(), + registries: registries ?? [new InMemoryAnonCredsRegistry()], + tailsFileService: new InMemoryTailsFileService(), }), anoncredsRs: new AnonCredsRsModule({ anoncreds, }), - indyVdr: new IndyVdrModule(indyVdrModuleConfig), dids: new DidsModule({ - resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], - registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new LocalDidResolver()], }), askar: new AskarModule(askarModuleConfig), cache: new CacheModule({ @@ -282,6 +272,7 @@ export async function setupAnonCredsTests< VerifierName extends string | undefined = undefined, CreateConnections extends boolean = true >({ + issuerId, issuerName, holderName, verifierName, @@ -292,6 +283,7 @@ export async function setupAnonCredsTests< supportRevocation, registries, }: { + issuerId: string issuerName: string holderName: string verifierName?: VerifierName @@ -365,6 +357,7 @@ export async function setupAnonCredsTests< const { credentialDefinition, revocationRegistryDefinition, schema } = await prepareForAnonCredsIssuance( issuerAgent, { + issuerId, attributeNames, supportRevocation, } @@ -406,18 +399,24 @@ export async function setupAnonCredsTests< export async function prepareForAnonCredsIssuance( agent: Agent, - { attributeNames, supportRevocation }: { attributeNames: string[]; supportRevocation?: boolean } + { + attributeNames, + supportRevocation, + issuerId, + }: { attributeNames: string[]; supportRevocation?: boolean; issuerId: string } ) { - // Add existing endorser did to the wallet - const unqualifiedDid = await importExistingIndyDidFromPrivateKey(agent, TypedArrayEncoder.fromString(publicDidSeed)) - const didIndyDid = `did:indy:pool:localtest:${unqualifiedDid}` + //const key = await agent.wallet.createKey({ keyType: KeyType.Ed25519 }) + + const didDocument = new DidDocumentBuilder(issuerId).build() + + await agent.dids.import({ did: issuerId, didDocument }) const schema = await registerSchema(agent, { // TODO: update attrNames to attributeNames attrNames: attributeNames, name: `Schema ${randomUUID()}`, version: '1.0', - issuerId: didIndyDid, + issuerId, }) // Wait some time pass to let ledger settle the object @@ -425,7 +424,7 @@ export async function prepareForAnonCredsIssuance( const credentialDefinition = await registerCredentialDefinition(agent, { schemaId: schema.schemaId, - issuerId: didIndyDid, + issuerId, tag: 'default', supportRevocation, }) @@ -437,7 +436,7 @@ export async function prepareForAnonCredsIssuance( let revocationStatusList if (supportRevocation) { revocationRegistryDefinition = await registerRevocationRegistryDefinition(agent, { - issuerId: didIndyDid, + issuerId, tag: 'default', credentialDefinitionId: credentialDefinition.credentialDefinitionId, maximumCredentialNumber: 10, @@ -449,7 +448,7 @@ export async function prepareForAnonCredsIssuance( revocationStatusList = await registerRevocationStatusList(agent, { issuanceByDefault: true, revocationRegistryDefinitionId: revocationRegistryDefinition?.revocationRegistryDefinitionId, - issuerId: didIndyDid, + issuerId, }) // Wait some time pass to let ledger settle the object diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts index bd7b96c96f..4cdec51581 100644 --- a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -36,6 +36,8 @@ describe('v2 credential revocation', () => { const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + beforeAll(async () => { ;({ issuerAgent: faberAgent, @@ -46,6 +48,7 @@ describe('v2 credential revocation', () => { revocationRegistryDefinitionId, holderIssuerConnectionId: aliceConnectionId, } = await setupAnonCredsTests({ + issuerId, issuerName: 'Faber Agent Credentials v2', holderName: 'Alice Agent Credentials v2', attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], diff --git a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts index e629708de2..6dd1b378c9 100644 --- a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts @@ -1,7 +1,8 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' import type { EventReplaySubject } from '../../core/tests' +import type { AnonCredsHolderService, AnonCredsProposeCredentialFormat } from '@aries-framework/anoncreds' -import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../core/tests' -import testLogger from '../../core/tests/logger' +import { AnonCredsHolderServiceSymbol } from '@aries-framework/anoncreds' import { DidCommMessageRepository, JsonTransformer, @@ -13,14 +14,12 @@ import { V2ProposeCredentialMessage, V2RequestCredentialMessage, } from '@aries-framework/core' -import { - AnonCredsHolderService, - AnonCredsHolderServiceSymbol, - AnonCredsProposeCredentialFormat, -} from '@aries-framework/anoncreds' -import { AnonCredsTestsAgent, issueAnonCredsCredential } from './anoncredsSetup' -import { setupAnonCredsTests } from './anoncredsSetup' + import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../core/tests' +import testLogger from '../../core/tests/logger' + +import { issueAnonCredsCredential, setupAnonCredsTests } from './anoncredsSetup' const credentialPreview = V2CredentialPreview.fromRecord({ name: 'John', @@ -29,7 +28,7 @@ const credentialPreview = V2CredentialPreview.fromRecord({ profile_picture: 'profile picture', }) -describe('v2 credentials', () => { +describe('IC/PP V2 AnonCreds credentials', () => { let faberAgent: AnonCredsTestsAgent let aliceAgent: AnonCredsTestsAgent let credentialDefinitionId: string @@ -43,6 +42,8 @@ describe('v2 credentials', () => { const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + const newCredentialPreview = V2CredentialPreview.fromRecord({ name: 'John', age: '99', @@ -60,6 +61,7 @@ describe('v2 credentials', () => { issuerHolderConnectionId: faberConnectionId, holderIssuerConnectionId: aliceConnectionId, } = await setupAnonCredsTests({ + issuerId, issuerName: 'Faber Agent Credentials v2', holderName: 'Alice Agent Credentials v2', attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index afc698beda..409604f2d7 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -9,6 +9,7 @@ import { AnonCredsLinkSecretRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationStatusListRepository, } from './repository' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' @@ -40,6 +41,7 @@ export class AnonCredsModule implements Module { dependencyManager.registerSingleton(AnonCredsLinkSecretRepository) dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionRepository) dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionPrivateRepository) + dependencyManager.registerSingleton(AnonCredsRevocationStatusListRepository) } public updates = [ diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts index e693b723ac..67a812cd50 100644 --- a/packages/anoncreds/src/AnonCredsModuleConfig.ts +++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts @@ -1,7 +1,7 @@ import type { AnonCredsRegistry } from './services' -import type { TailsFileManager } from './services/TailsFileManager' +import type { TailsFileService } from './services/tails' -import { BasicTailsFileManager } from './services/BasicTailsFileManager' +import { BasicTailsFileService } from './services/tails' /** * @public @@ -23,7 +23,7 @@ export interface AnonCredsModuleConfigOptions { * Tails file manager for download/uploading tails files * @default DefaultTailsFileManager (only for downloading tails files) */ - tailsFileManager?: TailsFileManager + tailsFileService?: TailsFileService } /** @@ -46,8 +46,8 @@ export class AnonCredsModuleConfig { return this.options.maximumCredentialNumberPerRevocationRegistry ?? 1000 } - /** See {@link AnonCredsModuleConfigOptions.tailsFileManager} */ + /** See {@link AnonCredsModuleConfigOptions.tailsFileService} */ public get tailsFileManager() { - return this.options.tailsFileManager ?? new BasicTailsFileManager() + return this.options.tailsFileService ?? new BasicTailsFileService() } } diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts index f9c868c14c..a7d7455a7f 100644 --- a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -9,6 +9,9 @@ import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationStatusListRepository, } from '../repository' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' @@ -26,13 +29,18 @@ describe('AnonCredsModule', () => { }) anonCredsModule.register(dependencyManager) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(6) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(9) 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.registerSingleton).toHaveBeenCalledWith(AnonCredsRevocationRegistryDefinitionRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + AnonCredsRevocationRegistryDefinitionPrivateRepository + ) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRevocationStatusListRepository) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(AnonCredsModuleConfig, anonCredsModule.config) diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index a9f93eaba6..2ad16e028a 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -9,7 +9,6 @@ export * from './protocols' export { AnonCredsModule } from './AnonCredsModule' export { AnonCredsModuleConfig, AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' -export { TailsFileManager, BasicTailsFileManager } from './services' export { AnonCredsApi } from './AnonCredsApi' export * from './AnonCredsApiOptions' export { generateLegacyProverDidLikeString } from './utils/proverDid' diff --git a/packages/anoncreds/src/services/index.ts b/packages/anoncreds/src/services/index.ts index ef5f39793f..419436fde9 100644 --- a/packages/anoncreds/src/services/index.ts +++ b/packages/anoncreds/src/services/index.ts @@ -3,7 +3,6 @@ export * from './AnonCredsHolderServiceOptions' export * from './AnonCredsIssuerService' export * from './AnonCredsIssuerServiceOptions' export * from './registry' +export { TailsFileService, BasicTailsFileService } from './tails' export * from './AnonCredsVerifierService' export * from './AnonCredsVerifierServiceOptions' -export * from './TailsFileManager' -export * from './BasicTailsFileManager' diff --git a/packages/anoncreds/src/services/BasicTailsFileManager.ts b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts similarity index 90% rename from packages/anoncreds/src/services/BasicTailsFileManager.ts rename to packages/anoncreds/src/services/tails/BasicTailsFileService.ts index d4720c0776..d47e5dfa47 100644 --- a/packages/anoncreds/src/services/BasicTailsFileManager.ts +++ b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts @@ -1,10 +1,10 @@ -import type { TailsFileManager } from './TailsFileManager' -import type { AnonCredsRevocationRegistryDefinition } from '../models' +import type { TailsFileService } from './TailsFileService' +import type { AnonCredsRevocationRegistryDefinition } from '../../models' import type { AgentContext, FileSystem } from '@aries-framework/core' import { AriesFrameworkError, InjectionSymbols, TypedArrayEncoder } from '@aries-framework/core' -export class BasicTailsFileManager implements TailsFileManager { +export class BasicTailsFileService implements TailsFileService { private tailsDirectoryPath?: string public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { @@ -27,7 +27,9 @@ export class BasicTailsFileManager implements TailsFileManager { } public async uploadTailsFile( + // eslint-disable-next-line @typescript-eslint/no-unused-vars agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } diff --git a/packages/anoncreds/src/services/TailsFileManager.ts b/packages/anoncreds/src/services/tails/TailsFileService.ts similarity index 85% rename from packages/anoncreds/src/services/TailsFileManager.ts rename to packages/anoncreds/src/services/tails/TailsFileService.ts index 20014395ad..c43a5b6958 100644 --- a/packages/anoncreds/src/services/TailsFileManager.ts +++ b/packages/anoncreds/src/services/tails/TailsFileService.ts @@ -1,7 +1,7 @@ -import type { AnonCredsRevocationRegistryDefinition } from '../models' +import type { AnonCredsRevocationRegistryDefinition } from '../../models' import type { AgentContext } from '@aries-framework/core' -export interface TailsFileManager { +export interface TailsFileService { getTailsBasePath(agentContext: AgentContext): string getTailsFilePath(agentContext: AgentContext, tailsHash: string): string diff --git a/packages/anoncreds/src/services/tails/index.ts b/packages/anoncreds/src/services/tails/index.ts new file mode 100644 index 0000000000..104617b380 --- /dev/null +++ b/packages/anoncreds/src/services/tails/index.ts @@ -0,0 +1,2 @@ +export * from './BasicTailsFileService' +export * from './TailsFileService' From 9c41913a2775458234a3e19f52474b1429fd422d Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 11 Apr 2023 19:03:24 -0300 Subject: [PATCH 11/27] add proofs tests with revocation + fixes in timestamp Signed-off-by: Ariel Gentile --- .../v2-credential-revocation.e2e.test.ts | 3 +- .../tests/v2-credentials.e2e.test.ts | 17 +- .../anoncreds-rs/tests/v2-proofs.e2e.test.ts | 955 ++++++++++++++++++ .../formats/AnonCredsProofFormatService.ts | 3 +- .../formats/LegacyIndyProofFormatService.ts | 3 +- .../src/utils/getRevocationRegistries.ts | 2 +- packages/anoncreds/src/utils/timestamp.ts | 1 + ...FileManager.ts => FullTailsFileService.ts} | 4 +- 8 files changed, 974 insertions(+), 14 deletions(-) create mode 100644 packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts rename samples/tails/{FullTailsFileManager.ts => FullTailsFileService.ts} (91%) diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts index 4cdec51581..1ad5985e5e 100644 --- a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -10,6 +10,7 @@ import { V2OfferCredentialMessage, } from '@aries-framework/core' +import { describeRunInNodeVersion } from '../../../tests/runInVersion' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { waitForCredentialRecordSubject } from '../../core/tests' import testLogger from '../../core/tests/logger' @@ -24,7 +25,7 @@ const credentialPreview = V2CredentialPreview.fromRecord({ profile_picture: 'profile picture', }) -describe('v2 credential revocation', () => { +describeRunInNodeVersion([18], 'IC v2 credential revocation', () => { let faberAgent: AnonCredsTestsAgent let aliceAgent: AnonCredsTestsAgent let credentialDefinitionId: string diff --git a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts index 6dd1b378c9..5b1cc1d620 100644 --- a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts @@ -15,6 +15,7 @@ import { V2RequestCredentialMessage, } from '@aries-framework/core' +import { describeRunInNodeVersion } from '../../../tests/runInVersion' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../core/tests' import testLogger from '../../core/tests/logger' @@ -28,7 +29,7 @@ const credentialPreview = V2CredentialPreview.fromRecord({ profile_picture: 'profile picture', }) -describe('IC/PP V2 AnonCreds credentials', () => { +describeRunInNodeVersion([18], 'IC V2 AnonCreds credentials', () => { let faberAgent: AnonCredsTestsAgent let aliceAgent: AnonCredsTestsAgent let credentialDefinitionId: string @@ -70,11 +71,11 @@ describe('IC/PP V2 AnonCreds credentials', () => { anonCredsCredentialProposal = { credentialDefinitionId: credentialDefinitionId, - schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaIssuerDid: issuerId, schemaName: 'ahoy', schemaVersion: '1.0', - schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', - issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaId: `${issuerId}/q7ATwTYbQDgiigVijUAej:2:test:1.0`, + issuerDid: issuerId, } }) @@ -94,12 +95,12 @@ describe('IC/PP V2 AnonCreds credentials', () => { credentialFormats: { anoncreds: { attributes: credentialPreview.attributes, - schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaIssuerDid: issuerId, schemaName: 'ahoy', schemaVersion: '1.0', - schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', - issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', - credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + schemaId: `${issuerId}/q7ATwTYbQDgiigVijUAej:2:test:1.0`, + issuerDid: issuerId, + credentialDefinitionId: `${issuerId}/:3:CL:12:tag`, }, }, comment: 'v2 propose credential test', diff --git a/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts new file mode 100644 index 0000000000..a03e6eebab --- /dev/null +++ b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts @@ -0,0 +1,955 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' +import type { CredentialExchangeRecord } from '@aries-framework/core' + +import { + Attachment, + AttachmentData, + LinkedAttachment, + ProofState, + ProofExchangeRecord, + V2ProposePresentationMessage, + V2RequestPresentationMessage, + V2PresentationMessage, +} from '@aries-framework/core' + +import { describeRunInNodeVersion } from '../../../tests/runInVersion' +import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { waitForProofExchangeRecord } from '../../core/tests' +import testLogger from '../../core/tests/logger' + +import { issueAnonCredsCredential, setupAnonCredsTests } from './anoncredsSetup' + +describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let revocationRegistryDefinitionId: string | undefined + let aliceConnectionId: string + let faberConnectionId: string + let faberProofExchangeRecord: ProofExchangeRecord + let aliceProofExchangeRecord: ProofExchangeRecord + let faberCredentialExchangeRecord: CredentialExchangeRecord + + const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) + + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + revocationRegistryDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerId, + issuerName: 'Faber agent AnonCreds proofs', + holderName: 'Alice agent AnonCreds proofs', + attributeNames: ['name', 'age', 'image_0', 'image_1'], + registries: [inMemoryRegistry], + supportRevocation: true, + })) + ;({ issuerCredentialExchangeRecord: faberCredentialExchangeRecord } = await issueAnonCredsCredential({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '99', + }, + ], + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + })) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber', async () => { + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + let faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + anoncreds: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'Alice', + credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + }) + + // Faber waits for a presentation proposal from Alice + testLogger.test('Faber waits for a presentation proposal from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id) + const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposePresentationMessage) + expect(requestMessage).toBeInstanceOf(V2RequestPresentationMessage) + expect(presentationMessage).toBeInstanceOf(V2PresentationMessage) + + const formatData = await aliceAgent.proofs.getFormatData(aliceProofExchangeRecord.id) + + expect(formatData).toMatchObject({ + proposal: { + anoncreds: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.proposal?.anoncreds?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.proposal?.anoncreds?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + request: { + anoncreds: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.request?.anoncreds?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.request?.anoncreds?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + presentation: { + anoncreds: { + proof: { + proofs: [ + { + primary_proof: expect.any(Object), + non_revoc_proof: null, + }, + { + primary_proof: expect.any(Object), + non_revoc_proof: null, + }, + ], + aggregated_proof: { + c_hash: expect.any(String), + c_list: expect.any(Array), + }, + }, + requested_proof: expect.any(Object), + identifiers: expect.any(Array), + }, + }, + }) + }) + + test('Faber starts with proof request to Alice', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('Alice provides credentials via call to getRequestedCredentials', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const retrievedCredentials = await aliceAgent.proofs.getCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + expect(retrievedCredentials).toMatchObject({ + proofFormats: { + anoncreds: { + attributes: { + name: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + age: '99', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + image_0: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + age: '99', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + }, + predicates: { + age: [ + { + credentialId: expect.any(String), + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + name: 'John', + age: '99', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + }, + }, + }, + }) + }) + + test('Faber starts with proof request to Alice but gets Problem Reported', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport({ + description: 'Problem inside proof request', + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + protocolVersion: 'v2', + }) + }) + + test('Credential is revoked after proof request and before presentation', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + const nrpRequestedTime = dateToTimestamp(new Date()) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + non_revoked: { from: nrpRequestedTime, to: nrpRequestedTime }, + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + // Revoke the credential + const credentialRevocationRegistryDefinitionId = faberCredentialExchangeRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = faberCredentialExchangeRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + + await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index 5a613e5d14..e996a16e6f 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -58,6 +58,7 @@ import { getRevocationRegistriesForRequest, getRevocationRegistriesForProof, } from '../utils' +import { dateToTimestamp } from '../utils/timestamp' const ANONCREDS_PRESENTATION_PROPOSAL = 'anoncreds/proof-request@v1.0' const ANONCREDS_PRESENTATION_REQUEST = 'anoncreds/proof-request@v1.0' @@ -542,7 +543,7 @@ export class AnonCredsProofFormatService implements ProofFormatService Math.floor(date.getTime() / 1000) diff --git a/samples/tails/FullTailsFileManager.ts b/samples/tails/FullTailsFileService.ts similarity index 91% rename from samples/tails/FullTailsFileManager.ts rename to samples/tails/FullTailsFileService.ts index 66e0b7466d..1ffa4c03c6 100644 --- a/samples/tails/FullTailsFileManager.ts +++ b/samples/tails/FullTailsFileService.ts @@ -1,12 +1,12 @@ import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' -import { BasicTailsFileManager } from '@aries-framework/anoncreds' +import { BasicTailsFileService } from '@aries-framework/anoncreds' import { utils } from '@aries-framework/core' import FormData from 'form-data' import fs from 'fs' -export class FullTailsFileManager extends BasicTailsFileManager { +export class FullTailsFileService extends BasicTailsFileService { private tailsServerBaseUrl?: string public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { super(options) From 970a17a0abcfc547f6efd6679ee23530f8ab7fcc Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 11 Apr 2023 19:15:23 -0300 Subject: [PATCH 12/27] remove unused interfaces and implementation for indy-vdr Signed-off-by: Ariel Gentile --- packages/anoncreds/src/AnonCredsApiOptions.ts | 5 - .../services/registry/AnonCredsRegistry.ts | 2 +- .../tests/InMemoryAnonCredsRegistry.ts | 2 +- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 106 +----------------- 4 files changed, 5 insertions(+), 110 deletions(-) diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index 663d9d0fda..a7629778a8 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -28,8 +28,3 @@ export interface AnonCredsUpdateRevocationStatusListOptions { issuedCredentialIndexes?: number[] revocationRegistryDefinitionId: string } - -export interface AnonCredsRevokeCredentialOptions { - revocationRegistryDefinitionId: string - revokedCredentialIndexes: number[] -} diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts index 8305af35b2..d641befdef 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts @@ -10,7 +10,7 @@ import type { } from './RevocationRegistryDefinitionOptions' import type { GetRevocationStatusListReturn, - RegisterRevocationListOptions as RegisterRevocationStatusListOptions, + RegisterRevocationStatusListOptions, RegisterRevocationStatusListReturn, } from './RevocationStatusListOptions' import type { GetSchemaReturn, RegisterSchemaOptions, RegisterSchemaReturn } from './SchemaOptions' diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 5ebdba3ff0..0d8ae06a7f 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -16,7 +16,7 @@ import type { RegisterRevocationRegistryDefinitionOptions, RegisterRevocationRegistryDefinitionReturn, RegisterRevocationStatusListReturn, - RegisterRevocationListOptions as RegisterRevocationStatusListOptions, + RegisterRevocationStatusListOptions, } from '../src' import type { AgentContext } from '@aries-framework/core' diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index 8ad52fe058..b76cdd85e0 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -11,7 +11,7 @@ import type { AnonCredsRevocationRegistryDefinition, RegisterRevocationRegistryDefinitionOptions, RegisterRevocationRegistryDefinitionReturn, - RegisterRevocationListOptions, + RegisterRevocationStatusListOptions, RegisterRevocationStatusListReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' @@ -25,7 +25,6 @@ import { GetTransactionRequest, GetRevocationRegistryDeltaRequest, GetRevocationRegistryDefinitionRequest, - RevocationRegistryDefinitionRequest, } from '@hyperledger/indy-vdr-shared' import { parseIndyDid, verificationKeyForIndyDid } from '../dids/didIndyUtil' @@ -491,106 +490,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { agentContext: AgentContext, options: RegisterRevocationRegistryDefinitionOptions ): Promise { - try { - // This will throw an error if trying to register a revocation registry definition with a legacy indy identifier. We only support did:indy - // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. - const { namespaceIdentifier, namespace } = parseIndyDid(options.revocationRegistryDefinition.issuerId) - - const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) - - const pool = indyVdrPoolService.getPoolForNamespace(namespace) - agentContext.config.logger.debug( - `Registering revocation registry definition on ledger '${pool.indyNamespace}' with did '${options.revocationRegistryDefinition.issuerId}'`, - options.revocationRegistryDefinition - ) - - // TODO: this will bypass caching if done on a higher level. - const { credentialDefinition, credentialDefinitionMetadata, resolutionMetadata } = - await this.getCredentialDefinition(agentContext, options.revocationRegistryDefinition.credDefId) - - if ( - !credentialDefinition || - !credentialDefinitionMetadata.indyLedgerSeqNo || - typeof credentialDefinitionMetadata.indyLedgerSeqNo !== 'number' - ) { - return { - registrationMetadata: {}, - revocationRegistryDefinitionMetadata: { - didIndyNamespace: pool.indyNamespace, - }, - revocationRegistryDefinitionState: { - revocationRegistryDefinition: options.revocationRegistryDefinition, - state: 'failed', - reason: `error resolving credential definition with id ${options.revocationRegistryDefinition.credDefId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, - }, - } - } - - const legacyRevocationRegistryDefinitionId = getLegacyRevocationRegistryId( - options.revocationRegistryDefinition.issuerId, - credentialDefinitionMetadata.indyLedgerSeqNo, - credentialDefinition.tag, - options.revocationRegistryDefinition.tag - ) - const didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryId( - namespace, - namespaceIdentifier, - credentialDefinitionMetadata.indyLedgerSeqNo, - credentialDefinition.tag, - options.revocationRegistryDefinition.tag - ) - - const credentialDefinitionRequest = new RevocationRegistryDefinitionRequest({ - submitterDid: namespaceIdentifier, - revocationRegistryDefinitionV1: { - ver: '1.0', - id: legacyRevocationRegistryDefinitionId, - credDefId: `${credentialDefinitionMetadata.indyLedgerSeqNo}`, - revocDefType: 'CL_ACCUM', - tag: options.revocationRegistryDefinition.tag, - value: options.revocationRegistryDefinition.value, - }, - }) - - const submitterKey = await verificationKeyForIndyDid(agentContext, options.revocationRegistryDefinition.issuerId) - const response = await pool.submitWriteRequest(agentContext, credentialDefinitionRequest, submitterKey) - agentContext.config.logger.debug( - `Registered revocation registry definition '${didIndyRevocationRegistryDefinitionId}' on ledger '${pool.indyNamespace}'`, - { - response, - credentialDefinition: options.revocationRegistryDefinition, - } - ) - - return { - revocationRegistryDefinitionMetadata: {}, - revocationRegistryDefinitionState: { - revocationRegistryDefinition: options.revocationRegistryDefinition, - revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, - state: 'finished', - }, - registrationMetadata: {}, - } - } catch (error) { - agentContext.config.logger.error( - `Error registering credential definition for credential definition '${options.revocationRegistryDefinition.credDefId}'`, - { - error, - did: options.revocationRegistryDefinition.issuerId, - credentialDefinition: options.revocationRegistryDefinition, - } - ) - - return { - revocationRegistryDefinitionMetadata: {}, - registrationMetadata: {}, - revocationRegistryDefinitionState: { - revocationRegistryDefinition: options.revocationRegistryDefinition, - state: 'failed', - reason: `unknownError: ${error.message}`, - }, - } - } + throw new AriesFrameworkError('Not implemented!') } public async getRevocationStatusList( @@ -699,7 +599,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { public async registerRevocationStatusList( agentContext: AgentContext, - options: RegisterRevocationListOptions + options: RegisterRevocationStatusListOptions ): Promise { throw new AriesFrameworkError('Not implemented!') } From ea2e8ef13118b5973a986a667f30db030000382c Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 12 Apr 2023 11:17:37 -0300 Subject: [PATCH 13/27] fix: types errors Signed-off-by: Ariel Gentile --- .../tests/InMemoryTailsFileService.ts | 2 +- .../anoncreds-rs/tests/anoncreds-flow.test.ts | 6 ++--- packages/anoncreds/src/AnonCredsApi.ts | 16 +++++++------- .../anoncreds/src/AnonCredsModuleConfig.ts | 4 ++-- .../AnonCredsCredentialFormatService.ts | 4 ++-- .../registry/RevocationStatusListOptions.ts | 2 +- .../services/tails/BasicTailsFileService.ts | 22 +++++++++++-------- .../src/services/tails/TailsFileService.ts | 6 ++--- .../src/utils/getRevocationRegistries.ts | 4 ++-- 9 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts index a802fe4dd8..c71887047f 100644 --- a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts +++ b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts @@ -36,7 +36,7 @@ export class InMemoryTailsFileService extends BasicTailsFileService { // hash is used as file identifier const tailsExists = await this.tailsFileExists(agentContext, tailsHash) - const tailsFilePath = this.getTailsFilePath(agentContext, tailsHash) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) agentContext.config.logger.debug( `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` ) diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 69cb04e0f9..05e722fa28 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -49,10 +49,10 @@ import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierS import { InMemoryTailsFileService } from './InMemoryTailsFileService' const registry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) -const tailsFileManager = new InMemoryTailsFileService() +const tailsFileService = new InMemoryTailsFileService() const anonCredsModuleConfig = new AnonCredsModuleConfig({ registries: [registry], - tailsFileService: tailsFileManager, + tailsFileService, }) const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') @@ -182,7 +182,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean credentialDefinition, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, maximumCredentialNumber: 100, - tailsDirectoryPath: tailsFileManager.getTailsBasePath(agentContext), + tailsDirectoryPath: await tailsFileService.getTailsBasePath(agentContext), tag: 'default', }) diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index f02a17bdd7..948c095eeb 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -353,10 +353,10 @@ export class AnonCredsApi { }): Promise { const { issuerId, tag, credentialDefinitionId, maximumCredentialNumber } = options.revocationRegistryDefinition - const tailsFileManager = - this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager + const tailsFileService = + this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const tailsDirectoryPath = tailsFileManager.getTailsBasePath(this.agentContext) + const tailsDirectoryPath = await tailsFileService.getTailsBasePath(this.agentContext) const failedReturnBase = { revocationRegistryDefinitionState: { @@ -393,7 +393,7 @@ export class AnonCredsApi { // At this moment, tails file should be published and a valid public URL will be received const localTailsLocation = revocationRegistryDefinition.value.tailsLocation - revocationRegistryDefinition.value.tailsLocation = await tailsFileManager.uploadTailsFile(this.agentContext, { + revocationRegistryDefinition.value.tailsLocation = await tailsFileService.uploadTailsFile(this.agentContext, { revocationRegistryDefinition, }) @@ -485,8 +485,8 @@ export class AnonCredsApi { failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No revocation registry definition found for ${revocationRegistryDefinitionId}` return failedReturnBase } - const tailsFileManager = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager - const { tailsFilePath } = await tailsFileManager.downloadTailsFile(this.agentContext, { + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.downloadTailsFile(this.agentContext, { revocationRegistryDefinition, }) @@ -559,8 +559,8 @@ export class AnonCredsApi { return failedReturnBase } - const tailsFileManager = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager - const { tailsFilePath } = await tailsFileManager.downloadTailsFile(this.agentContext, { + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.downloadTailsFile(this.agentContext, { revocationRegistryDefinition, }) diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts index 67a812cd50..58ea15ecd7 100644 --- a/packages/anoncreds/src/AnonCredsModuleConfig.ts +++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts @@ -21,7 +21,7 @@ export interface AnonCredsModuleConfigOptions { /** * Tails file manager for download/uploading tails files - * @default DefaultTailsFileManager (only for downloading tails files) + * @default BasicTailsFileService (only for downloading tails files) */ tailsFileService?: TailsFileService } @@ -47,7 +47,7 @@ export class AnonCredsModuleConfig { } /** See {@link AnonCredsModuleConfigOptions.tailsFileService} */ - public get tailsFileManager() { + public get tailsFileService() { return this.options.tailsFileService ?? new BasicTailsFileService() } } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 4f05e6aa81..47d4356d26 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -359,8 +359,8 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService .resolve(AnonCredsRevocationRegistryDefinitionRepository) .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) - const tailsFileManager = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager - ;({ tailsFilePath } = await tailsFileManager.downloadTailsFile(agentContext, { + const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + ;({ tailsFilePath } = await tailsFileService.downloadTailsFile(agentContext, { revocationRegistryDefinition, })) } diff --git a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index c79201dbfd..d75e829e63 100644 --- a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -13,7 +13,7 @@ export interface GetRevocationStatusListReturn { revocationStatusListMetadata: Extensible } -export interface RegisterRevocationListOptions { +export interface RegisterRevocationStatusListOptions { // Timestamp is often calculated by the ledger, otherwise method should just take current time // Return type does include the timestamp. revocationStatusList: Omit diff --git a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts index d47e5dfa47..66b9fda23b 100644 --- a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts +++ b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts @@ -11,19 +11,23 @@ export class BasicTailsFileService implements TailsFileService { this.tailsDirectoryPath = options?.tailsDirectoryPath } - public getTailsBasePath(agentContext: AgentContext) { + public async getTailsBasePath(agentContext: AgentContext) { const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) - return `${this.tailsDirectoryPath ?? fileSystem.cachePath}/anoncreds/tails` + const basePath = `${this.tailsDirectoryPath ?? fileSystem.cachePath}/anoncreds/tails` + if (!(await fileSystem.exists(basePath))) { + await fileSystem.createDirectory(basePath) + } + return basePath } - public getTailsFilePath(agentContext: AgentContext, tailsHash: string) { - return `${this.getTailsBasePath(agentContext)}/${tailsHash}` + public async getTailsFilePath(agentContext: AgentContext, tailsHash: string) { + return `${await this.getTailsBasePath(agentContext)}/${tailsHash}` } - public tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { + public async tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) - const tailsFilePath = this.getTailsFilePath(agentContext, tailsHash) - return fileSystem.exists(tailsFilePath) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) + return await fileSystem.exists(tailsFilePath) } public async uploadTailsFile( @@ -34,7 +38,7 @@ export class BasicTailsFileService implements TailsFileService { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } ): Promise { - throw new AriesFrameworkError('BasicTailsFileManager only supports tails file downloading') + throw new AriesFrameworkError('BasicTailsFileService only supports tails file downloading') } public async downloadTailsFile( @@ -57,7 +61,7 @@ export class BasicTailsFileService implements TailsFileService { // hash is used as file identifier const tailsExists = await this.tailsFileExists(agentContext, tailsHash) - const tailsFilePath = this.getTailsFilePath(agentContext, tailsHash) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) agentContext.config.logger.debug( `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` ) diff --git a/packages/anoncreds/src/services/tails/TailsFileService.ts b/packages/anoncreds/src/services/tails/TailsFileService.ts index c43a5b6958..71ab35dcb0 100644 --- a/packages/anoncreds/src/services/tails/TailsFileService.ts +++ b/packages/anoncreds/src/services/tails/TailsFileService.ts @@ -2,11 +2,11 @@ import type { AnonCredsRevocationRegistryDefinition } from '../../models' import type { AgentContext } from '@aries-framework/core' export interface TailsFileService { - getTailsBasePath(agentContext: AgentContext): string + getTailsBasePath(agentContext: AgentContext): string | Promise - getTailsFilePath(agentContext: AgentContext, tailsHash: string): string + getTailsFilePath(agentContext: AgentContext, tailsHash: string): string | Promise - tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise + tailsFileExists(agentContext: AgentContext, tailsHash: string): boolean | Promise uploadTailsFile( agentContext: AgentContext, diff --git a/packages/anoncreds/src/utils/getRevocationRegistries.ts b/packages/anoncreds/src/utils/getRevocationRegistries.ts index 461a6f6221..2606619a7a 100644 --- a/packages/anoncreds/src/utils/getRevocationRegistries.ts +++ b/packages/anoncreds/src/utils/getRevocationRegistries.ts @@ -88,8 +88,8 @@ export async function getRevocationRegistriesForRequest( ) } - const tailsFileManager = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileManager - const { tailsFilePath } = await tailsFileManager.downloadTailsFile(agentContext, { + const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.downloadTailsFile(agentContext, { revocationRegistryDefinition, }) From 70e7651837854f0bec9766763986eacacf774dad Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 12 Apr 2023 11:41:08 -0300 Subject: [PATCH 14/27] fix: ensure tails directory is created Signed-off-by: Ariel Gentile --- packages/anoncreds/src/services/tails/BasicTailsFileService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts index 66b9fda23b..36e6b57335 100644 --- a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts +++ b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts @@ -15,7 +15,7 @@ export class BasicTailsFileService implements TailsFileService { const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) const basePath = `${this.tailsDirectoryPath ?? fileSystem.cachePath}/anoncreds/tails` if (!(await fileSystem.exists(basePath))) { - await fileSystem.createDirectory(basePath) + await fileSystem.createDirectory(`${basePath}/file`) } return basePath } From 998ead96f4572d9ef469cde9b2c95db0bb1289db Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 15 Apr 2023 17:11:38 -0300 Subject: [PATCH 15/27] feat: override timestamps and add revoked credential test Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 5 +- .../services/AnonCredsRsVerifierService.ts | 101 ++++++- packages/anoncreds-rs/tests/anoncredsSetup.ts | 10 +- .../anoncreds-rs/tests/v2-proofs.e2e.test.ts | 252 +++++++++++------- 4 files changed, 255 insertions(+), 113 deletions(-) diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index c3aa0bc594..d2d4283004 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -183,11 +183,10 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { value: { ...revocationRegistryDefinition.value, tailsLocation: tailsFilePath }, } as unknown as JsonObject) updatedRevocationStatusList.update({ - // TODO: Fix parameters in anoncreds-rs - revocationRegstryDefinition: revocationRegistryDefinitionObj, + revocationRegistryDefinition: revocationRegistryDefinitionObj, issued: options.issued, revoked: options.revoked, - timestamp: timestamp ?? -1, + timestamp: timestamp ?? -1, // TODO: Fix parameters in anoncreds-rs }) } diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts index 26573309ff..4d788a006c 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts @@ -1,7 +1,14 @@ -import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' +import type { + AnonCredsNonRevokedInterval, + AnonCredsProof, + AnonCredsProofRequest, + AnonCredsVerifierService, + VerifyProofOptions, +} from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' -import type { JsonObject } from '@hyperledger/anoncreds-shared' +import type { JsonObject, NonRevokedIntervalOverride } from '@hyperledger/anoncreds-shared' +import { AnonCredsRegistryService } from '@aries-framework/anoncreds' import { injectable } from '@aries-framework/core' import { Presentation } from '@hyperledger/anoncreds-shared' @@ -12,6 +19,16 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { let presentation: Presentation | undefined try { + // Check that provided timestamps correspond to the active ones from the VDR. If they are and differ from the originally + // requested ones, create overrides for anoncreds-rs to consider them valid + const { verified, nonRevokedIntervalOverrides } = await this.verifyTimestamps(agentContext, proof, proofRequest) + + // No need to call anoncreds-rs as we already know that the proof will not be valid + if (!verified) { + agentContext.config.logger.debug('Invalid timestamps for provided identifiers') + return false + } + presentation = Presentation.fromJson(proof as unknown as JsonObject) const rsCredentialDefinitions: Record = {} @@ -41,9 +58,89 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { schemas: rsSchemas, revocationRegistryDefinitions, revocationStatusLists: lists, + nonRevokedIntervalOverrides, }) } finally { presentation?.handle.clear() } } + + private async verifyTimestamps( + agentContext: AgentContext, + proof: AnonCredsProof, + proofRequest: AnonCredsProofRequest + ): Promise<{ verified: boolean; nonRevokedIntervalOverrides?: NonRevokedIntervalOverride[] }> { + const nonRevokedIntervalOverrides: NonRevokedIntervalOverride[] = [] + + // Override expected timestamps if the requested ones don't exacly match the values from VDR + const globalNonRevokedInterval = proofRequest.non_revoked + + const requestedNonRevokedRestrictions: { + nonRevokedInterval: AnonCredsNonRevokedInterval + schemaId?: string + credentialDefinitionId?: string + revocationRegistryDefinitionId?: string + }[] = [] + + for (const value of [ + ...Object.values(proofRequest.requested_attributes), + ...Object.values(proofRequest.requested_predicates), + ]) { + const nonRevokedInterval = globalNonRevokedInterval ?? value.non_revoked + if (nonRevokedInterval) { + value.restrictions?.forEach((restriction) => + requestedNonRevokedRestrictions.push({ + nonRevokedInterval, + schemaId: restriction.schema_id, + credentialDefinitionId: restriction.cred_def_id, + revocationRegistryDefinitionId: restriction.rev_reg_id, + }) + ) + } + } + + for (const identifier of proof.identifiers) { + if (!identifier.timestamp || !identifier.rev_reg_id) { + continue + } + const relatedNonRevokedRestrictionItem = requestedNonRevokedRestrictions.find( + (item) => + item.revocationRegistryDefinitionId === item.revocationRegistryDefinitionId || + item.credentialDefinitionId === identifier.cred_def_id || + item.schemaId === item.schemaId + ) + + const requestedFrom = relatedNonRevokedRestrictionItem?.nonRevokedInterval.from + if (requestedFrom && requestedFrom > identifier.timestamp) { + // Check VDR if the active revocation status list at requestedFrom was the one from provided timestamp. + // If it matches, add to the override list + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, identifier.rev_reg_id) + const { revocationStatusList } = await registry.getRevocationStatusList( + agentContext, + identifier.rev_reg_id, + requestedFrom + ) + const vdrTimestamp = revocationStatusList?.timestamp + if (vdrTimestamp && vdrTimestamp === identifier.timestamp) { + nonRevokedIntervalOverrides.push({ + overrideRevocationStatusListTimestamp: identifier.timestamp, + requestedFromTimestamp: requestedFrom, + revocationRegistryDefinitionId: identifier.rev_reg_id, + }) + } else { + agentContext.config.logger.debug( + `VDR timestamp for ${requestedFrom} does not correspond to the one provided in proof identifiers. Expected: ${identifier.timestamp} and received ${vdrTimestamp}` + ) + return { verified: false } + } + } + } + + return { + verified: true, + nonRevokedIntervalOverrides: nonRevokedIntervalOverrides.length ? nonRevokedIntervalOverrides : undefined, + } + } } diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index 836cc513e6..60924e7a95 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -266,6 +266,7 @@ interface SetupAnonCredsTestsReturn { let aliceProofExchangeRecord: ProofExchangeRecord let faberCredentialExchangeRecord: CredentialExchangeRecord - const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) + const inMemoryRegistry = new InMemoryAnonCredsRegistry() const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' @@ -48,6 +50,7 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { holderReplay: aliceReplay, credentialDefinitionId, revocationRegistryDefinitionId, + //revocationStatusListTimestamp, issuerHolderConnectionId: faberConnectionId, holderIssuerConnectionId: aliceConnectionId, } = await setupAnonCredsTests({ @@ -777,7 +780,43 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { state: ProofState.RequestReceived, }) - const nrpRequestedTime = dateToTimestamp(new Date()) + const nrpRequestedTime = dateToTimestamp(new Date()) + 1 + + const requestProofFormat: AnonCredsRequestProofFormat = { + non_revoked: { from: nrpRequestedTime, to: nrpRequestedTime }, + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + } // Faber sends a presentation request to Alice testLogger.test('Faber sends a presentation request to Alice') @@ -785,41 +824,7 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { protocolVersion: 'v2', connectionId: faberConnectionId, proofFormats: { - anoncreds: { - non_revoked: { from: nrpRequestedTime, to: nrpRequestedTime }, - name: 'Proof Request', - version: '1.0.0', - requested_attributes: { - name: { - name: 'name', - restrictions: [ - { - cred_def_id: credentialDefinitionId, - }, - ], - }, - image_0: { - name: 'image_0', - restrictions: [ - { - cred_def_id: credentialDefinitionId, - }, - ], - }, - }, - requested_predicates: { - age: { - name: 'age', - p_type: '>=', - p_value: 50, - restrictions: [ - { - cred_def_id: credentialDefinitionId, - }, - ], - }, - }, - }, + anoncreds: requestProofFormat, }, }) @@ -827,34 +832,6 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { testLogger.test('Alice waits for presentation request from Faber') aliceProofExchangeRecord = await aliceProofExchangeRecordPromise - const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) - expect(request).toMatchObject({ - type: 'https://didcomm.org/present-proof/2.0/request-presentation', - formats: [ - { - attachmentId: expect.any(String), - format: 'anoncreds/proof-request@v1.0', - }, - ], - requestAttachments: [ - { - id: expect.any(String), - mimeType: 'application/json', - data: { - base64: expect.any(String), - }, - }, - ], - id: expect.any(String), - }) - - expect(aliceProofExchangeRecord.id).not.toBeNull() - expect(aliceProofExchangeRecord).toMatchObject({ - threadId: aliceProofExchangeRecord.threadId, - state: ProofState.RequestReceived, - protocolVersion: 'v2', - }) - // Alice retrieves the requested credentials and accepts the presentation request testLogger.test('Alice accepts presentation request from Faber') @@ -876,6 +853,9 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { expect(credentialRevocationRegistryDefinitionId).toBeDefined() expect(credentialRevocationIndex).toBeDefined() + // FIXME: do not use delays. Maybe we can add the timestamp to parameters? + // InMemoryAnonCredsRegistry would respect what we ask while actual VDRs will use their own + await sleep(2000) await faberAgent.modules.anoncreds.updateRevocationStatusList({ revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, revokedCredentialIndexes: [Number(credentialRevocationIndex)], @@ -890,36 +870,6 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { testLogger.test('Faber waits for presentation from Alice') faberProofExchangeRecord = await faberProofExchangeRecordPromise - const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) - expect(presentation).toMatchObject({ - type: 'https://didcomm.org/present-proof/2.0/presentation', - formats: [ - { - attachmentId: expect.any(String), - format: 'anoncreds/proof@v1.0', - }, - ], - presentationAttachments: [ - { - id: expect.any(String), - mimeType: 'application/json', - data: { - base64: expect.any(String), - }, - }, - ], - id: expect.any(String), - thread: { - threadId: faberProofExchangeRecord.threadId, - }, - }) - expect(faberProofExchangeRecord.id).not.toBeNull() - expect(faberProofExchangeRecord).toMatchObject({ - threadId: faberProofExchangeRecord.threadId, - state: ProofState.PresentationReceived, - protocolVersion: 'v2', - }) - aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { threadId: aliceProofExchangeRecord.threadId, state: ProofState.Done, @@ -934,22 +884,118 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { aliceProofExchangeRecord = await aliceProofExchangeRecordPromise expect(faberProofExchangeRecord).toMatchObject({ - type: ProofExchangeRecord.type, - id: expect.any(String), - createdAt: expect.any(Date), threadId: aliceProofExchangeRecord.threadId, - connectionId: expect.any(String), isVerified: true, state: ProofState.PresentationReceived, }) expect(aliceProofExchangeRecord).toMatchObject({ - type: ProofExchangeRecord.type, - id: expect.any(String), - createdAt: expect.any(Date), threadId: faberProofExchangeRecord.threadId, - connectionId: expect.any(String), state: ProofState.Done, }) }) + + test('Credential is revoked before proof request', async () => { + // Revoke the credential + const credentialRevocationRegistryDefinitionId = faberCredentialExchangeRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = faberCredentialExchangeRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + + const { revocationStatusListState } = await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }) + + expect(revocationStatusListState.revocationStatusList).toBeDefined() + const revokedTimestamp = revocationStatusListState.revocationStatusList?.timestamp + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + const nrpRequestedTime = (revokedTimestamp ?? dateToTimestamp(new Date())) + 1 + + const requestProofFormat: AnonCredsRequestProofFormat = { + non_revoked: { from: nrpRequestedTime, to: nrpRequestedTime }, + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: requestProofFormat, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: { filterByNonRevocationRequirements: false } }, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + // Faber receives presentation and checks that it is not valid + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + isVerified: false, + state: ProofState.PresentationReceived, + }) + }) }) From 599d9d014e085f95d315fdde43ff046fe00e748b Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 28 Apr 2023 00:57:59 -0300 Subject: [PATCH 16/27] feat: update anoncreds-rs, adjust interfaces Signed-off-by: Ariel Gentile --- demo/package.json | 2 +- packages/anoncreds-rs/package.json | 4 ++-- .../tests/v2-credential-revocation.e2e.test.ts | 2 +- .../tests/v2-credentials.e2e.test.ts | 2 +- packages/anoncreds/package.json | 2 +- .../anoncreds/src/AnonCredsModuleConfig.ts | 2 +- .../src/services/tails/TailsFileService.ts | 2 ++ .../services/CheqdAnonCredsRegistry.ts | 10 ++++++++++ .../cheqd/src/anoncreds/utils/transform.ts | 3 +++ .../anoncreds/services/IndySdkIssuerService.ts | 4 ++-- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 2 -- yarn.lock | 18 +++++++++--------- 12 files changed, 33 insertions(+), 20 deletions(-) diff --git a/demo/package.json b/demo/package.json index 85b1951908..16ae0447fe 100644 --- a/demo/package.json +++ b/demo/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@hyperledger/indy-vdr-nodejs": "^0.1.0-dev.14", - "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.14", + "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.15", "@hyperledger/aries-askar-nodejs": "^0.1.0-dev.8", "inquirer": "^8.2.5" }, diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json index e4a42ac41a..633ea1f08e 100644 --- a/packages/anoncreds-rs/package.json +++ b/packages/anoncreds-rs/package.json @@ -26,14 +26,14 @@ "dependencies": { "@aries-framework/core": "0.3.3", "@aries-framework/anoncreds": "0.3.3", - "@hyperledger/anoncreds-shared": "^0.1.0-dev.14", + "@hyperledger/anoncreds-shared": "^0.1.0-dev.15", "class-transformer": "^0.5.1", "class-validator": "0.14.0", "rxjs": "^7.2.0", "tsyringe": "^4.7.0" }, "devDependencies": { - "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.14", + "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.15", "reflect-metadata": "^0.1.13", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts index 1ad5985e5e..edba2aef65 100644 --- a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -35,7 +35,7 @@ describeRunInNodeVersion([18], 'IC v2 credential revocation', () => { let faberReplay: EventReplaySubject let aliceReplay: EventReplaySubject - const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) + const inMemoryRegistry = new InMemoryAnonCredsRegistry() const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' diff --git a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts index 5b1cc1d620..1a90a8397d 100644 --- a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts @@ -41,7 +41,7 @@ describeRunInNodeVersion([18], 'IC V2 AnonCreds credentials', () => { let anonCredsCredentialProposal: AnonCredsProposeCredentialFormat - const inMemoryRegistry = new InMemoryAnonCredsRegistry({ useLegacyIdentifiers: false }) + const inMemoryRegistry = new InMemoryAnonCredsRegistry() const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 5104079b86..fabc377594 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "@aries-framework/node": "0.3.3", - "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.14", + "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.15", "indy-sdk": "^1.16.0-dev-1636", "rimraf": "^4.4.0", "rxjs": "^7.8.0", diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts index 58ea15ecd7..44917cc123 100644 --- a/packages/anoncreds/src/AnonCredsModuleConfig.ts +++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts @@ -20,7 +20,7 @@ export interface AnonCredsModuleConfigOptions { maximumCredentialNumberPerRevocationRegistry?: number /** - * Tails file manager for download/uploading tails files + * Tails file service for download/uploading tails files * @default BasicTailsFileService (only for downloading tails files) */ tailsFileService?: TailsFileService diff --git a/packages/anoncreds/src/services/tails/TailsFileService.ts b/packages/anoncreds/src/services/tails/TailsFileService.ts index 71ab35dcb0..57f2c94fc9 100644 --- a/packages/anoncreds/src/services/tails/TailsFileService.ts +++ b/packages/anoncreds/src/services/tails/TailsFileService.ts @@ -12,6 +12,7 @@ export interface TailsFileService { agentContext: AgentContext, options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string } ): Promise @@ -19,6 +20,7 @@ export interface TailsFileService { agentContext: AgentContext, options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string } ): Promise<{ tailsFilePath: string diff --git a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts index 4b3d5db18c..c26acf1d0c 100644 --- a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts +++ b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts @@ -9,6 +9,8 @@ import type { RegisterCredentialDefinitionReturn, RegisterSchemaReturn, RegisterSchemaOptions, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' @@ -283,6 +285,10 @@ export class CheqdAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationRegistryDefinition(): Promise { + throw new Error('Not implemented!') + } + // FIXME: this method doesn't retrieve the revocation status list at a specified time, it just resolves the revocation registry definition public async getRevocationStatusList( agentContext: AgentContext, @@ -337,4 +343,8 @@ export class CheqdAnonCredsRegistry implements AnonCredsRegistry { } } } + + public async registerRevocationStatusList(): Promise { + throw new Error('Not implemented!') + } } diff --git a/packages/cheqd/src/anoncreds/utils/transform.ts b/packages/cheqd/src/anoncreds/utils/transform.ts index 47c7b076a7..fa7061210d 100644 --- a/packages/cheqd/src/anoncreds/utils/transform.ts +++ b/packages/cheqd/src/anoncreds/utils/transform.ts @@ -102,6 +102,9 @@ export class CheqdRevocationRegistryDefinitionValue { @IsString() public tailsHash!: string + + @IsString() + public issuanceType!: 'ISSUANCE_BY_DEFAULT' | 'ISSUANCE_ON_DEMAND' } export class CheqdRevocationRegistryDefinition { diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index 07cdcbba4f..cd7d31edca 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -137,8 +137,8 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { assertIndySdkWallet(agentContext.wallet) assertUnqualifiedCredentialOffer(options.credentialOffer) assertUnqualifiedCredentialRequest(options.credentialRequest) - if (options.revocationRegistryId) { - assertUnqualifiedRevocationRegistryId(options.revocationRegistryId) + if (options.revocationRegistryDefinitionId) { + assertUnqualifiedRevocationRegistryId(options.revocationRegistryDefinitionId) } try { diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index e56a11c6e8..dd18548baa 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -9,9 +9,7 @@ import type { GetRevocationStatusListReturn, GetRevocationRegistryDefinitionReturn, AnonCredsRevocationRegistryDefinition, - RegisterRevocationRegistryDefinitionOptions, RegisterRevocationRegistryDefinitionReturn, - RegisterRevocationStatusListOptions, RegisterRevocationStatusListReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' diff --git a/yarn.lock b/yarn.lock index af87e49f6e..3449386aa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1036,12 +1036,12 @@ 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.14": - version "0.1.0-dev.14" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.1.0-dev.14.tgz#00d4dca419f04a580cc02611d0c803d3d5c04fbf" - integrity sha512-N0H89rRBOaLeX+j5NHSibU1m88lWOXWg2t/orZZtY9iTt8op1oFV0IcWHeUJ1lPDJnOiozpn+AF/f5G0kipK7w== +"@hyperledger/anoncreds-nodejs@^0.1.0-dev.15": + version "0.1.0-dev.15" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.1.0-dev.15.tgz#8d248f53c318d91e82030a7fb23740fe4b2bef7a" + integrity sha512-3QiKzjVhbQ+N07vMiR0XCBxxy51RwhKf/z0/mHiVYy1ZcmuTok5dE/jTjAwmHh+jvuAwqk+O4ebWmFItRx1K7Q== dependencies: - "@hyperledger/anoncreds-shared" "0.1.0-dev.14" + "@hyperledger/anoncreds-shared" "0.1.0-dev.15" "@mapbox/node-pre-gyp" "^1.0.10" ffi-napi "4.0.3" node-cache "5.1.2" @@ -1049,10 +1049,10 @@ ref-napi "3.0.3" ref-struct-di "1.1.1" -"@hyperledger/anoncreds-shared@0.1.0-dev.14", "@hyperledger/anoncreds-shared@^0.1.0-dev.14": - version "0.1.0-dev.14" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.1.0-dev.14.tgz#0cdaa18a59b8223bb3eb2116970b1aa56cf5acfc" - integrity sha512-4yS7NgZZF9nfE50OquvbLWuZSzjOOiPufC/n2Ywb5lL2VloBblXMI2CezCZU1POmyAl7xnoT99pi2Od1fQaJxQ== +"@hyperledger/anoncreds-shared@0.1.0-dev.15", "@hyperledger/anoncreds-shared@^0.1.0-dev.15": + version "0.1.0-dev.15" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.1.0-dev.15.tgz#644e9cbc16a174f46b2f54dd7cb215a251d3da0a" + integrity sha512-nTO5KDTlDxpadk1j/r5T8E4wfS16rWKgZpyyG6dxg/7WhwZQkIcTsbPNnPH+NHklGju/ee+WT7rWlojpJ6XFVQ== "@hyperledger/aries-askar-nodejs@^0.1.0-dev.8": version "0.1.0-dev.8" From 87d88c84dfe6ab1e2763817fefc34b0c27137add Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 1 Jun 2023 18:17:13 -0300 Subject: [PATCH 17/27] fix: address several PR feedback Signed-off-by: Ariel Gentile --- demo/src/Faber.ts | 1 + .../src/services/AnonCredsRsIssuerService.ts | 47 +++++----- .../services/AnonCredsRsVerifierService.ts | 2 +- .../tests/InMemoryTailsFileService.ts | 6 +- .../anoncreds-rs/tests/anoncreds-flow.test.ts | 22 ++--- packages/anoncreds-rs/tests/anoncredsSetup.ts | 7 +- packages/anoncreds-rs/tests/helpers.ts | 46 ---------- .../v2-credential-revocation.e2e.test.ts | 4 +- .../anoncreds-rs/tests/v2-proofs.e2e.test.ts | 1 + packages/anoncreds/src/AnonCredsApi.ts | 43 ++-------- packages/anoncreds/src/AnonCredsApiOptions.ts | 5 +- packages/anoncreds/src/AnonCredsModule.ts | 2 - .../anoncreds/src/AnonCredsModuleConfig.ts | 11 --- .../src/__tests__/AnonCredsModule.test.ts | 2 - .../src/formats/AnonCredsCredentialFormat.ts | 4 + .../AnonCredsCredentialFormatService.ts | 86 +++++++++++++------ packages/anoncreds/src/models/registry.ts | 1 - ...vocationRegistryDefinitionPrivateRecord.ts | 13 ++- ...tionRegistryDefinitionPrivateRepository.ts | 4 +- .../AnonCredsRevocationStatusListRecord.ts | 46 ---------- ...AnonCredsRevocationStatusListRepository.ts | 43 ---------- packages/anoncreds/src/repository/index.ts | 2 - .../services/AnonCredsIssuerServiceOptions.ts | 2 +- packages/anoncreds/tests/anoncreds.test.ts | 2 +- .../anoncreds/tests/legacyAnonCredsSetup.ts | 1 + .../cheqd/src/anoncreds/utils/transform.ts | 3 - packages/core/tests/helpers.ts | 42 +++++++++ .../services/IndySdkAnonCredsRegistry.ts | 1 - .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 1 - 29 files changed, 168 insertions(+), 282 deletions(-) delete mode 100644 packages/anoncreds-rs/tests/helpers.ts delete mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts delete mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationStatusListRepository.ts diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index 0aae2e702f..1850825a0c 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -174,6 +174,7 @@ export class Faber extends BaseAgent { schemaId, issuerId: this.anonCredsIssuerId, tag: 'latest', + supportRevocation: false, }, options: { didIndyNamespace: 'bcovrin:test', diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index d2d4283004..7fd921c496 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -30,7 +30,7 @@ import { AnonCredsCredentialDefinitionRepository, AnonCredsRevocationRegistryDefinitionRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, - RevocationRegistryState, + AnonCredsRevocationRegistryState, } from '@aries-framework/anoncreds' import { injectable, AriesFrameworkError } from '@aries-framework/core' import { @@ -140,13 +140,12 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateRevocationStatusListOptions ): Promise { - const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, issuanceByDefault, tailsFilePath } = - options + const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, tailsFilePath } = options let revocationStatusList: RevocationStatusList | undefined try { revocationStatusList = RevocationStatusList.create({ - issuanceByDefault, + issuanceByDefault: true, revocationRegistryDefinitionId, revocationRegistryDefinition: { ...revocationRegistryDefinition, @@ -253,14 +252,18 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { revocationRegistryDefinitionId, tailsFilePath, revocationStatusList, + revocationRegistryIndex, } = options - const definedRevocationOptions = [revocationRegistryDefinitionId, tailsFilePath, revocationStatusList].filter( - (e) => e !== undefined - ) - if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 3) { + const definedRevocationOptions = [ + revocationRegistryDefinitionId, + tailsFilePath, + revocationStatusList, + revocationRegistryIndex, + ].filter((e) => e !== undefined) + if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 4) { throw new AriesFrameworkError( - 'Revocation requires all of revocationRegistryDefinitionId, revocationStatusList and tailsFilePath' + 'Revocation requires all of revocationRegistryDefinitionId, revocationStatusList, revocationRegistryIndex and tailsFilePath' ) } @@ -299,29 +302,21 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } let revocationConfiguration: CredentialRevocationConfig | undefined - if (options.revocationRegistryDefinitionId && options.tailsFilePath) { + if (revocationRegistryDefinitionId && tailsFilePath && revocationRegistryIndex) { const revocationRegistryDefinitionRecord = await agentContext.dependencyManager .resolve(AnonCredsRevocationRegistryDefinitionRepository) - .getByRevocationRegistryDefinitionId(agentContext, options.revocationRegistryDefinitionId) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) - .getByRevocationRegistryDefinitionId(agentContext, options.revocationRegistryDefinitionId) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) - const registryIndex = revocationRegistryDefinitionPrivateRecord.currentIndex + 1 - - if (registryIndex >= revocationRegistryDefinitionRecord.revocationRegistryDefinition.value.maxCredNum) { - revocationRegistryDefinitionPrivateRecord.state = RevocationRegistryState.Full + if ( + revocationRegistryIndex >= revocationRegistryDefinitionRecord.revocationRegistryDefinition.value.maxCredNum + ) { + revocationRegistryDefinitionPrivateRecord.state = AnonCredsRevocationRegistryState.Full } - // Update current registry index in storage - // Note: if an error is produced or the credential is not effectively sent, - // the previous index will be skipped - revocationRegistryDefinitionPrivateRecord.currentIndex = registryIndex - await agentContext.dependencyManager - .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) - .update(agentContext, revocationRegistryDefinitionPrivateRecord) - revocationConfiguration = new CredentialRevocationConfig({ registryDefinition: RevocationRegistryDefinition.fromJson( revocationRegistryDefinitionRecord.revocationRegistryDefinition as unknown as JsonObject @@ -329,8 +324,8 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { registryDefinitionPrivate: RevocationRegistryDefinitionPrivate.fromJson( revocationRegistryDefinitionPrivateRecord.value ), - tailsPath: options.tailsFilePath, - registryIndex, + tailsPath: tailsFilePath, + registryIndex: revocationRegistryIndex, }) } diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts index 4d788a006c..d9f615a964 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts @@ -86,7 +86,7 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { ...Object.values(proofRequest.requested_attributes), ...Object.values(proofRequest.requested_predicates), ]) { - const nonRevokedInterval = globalNonRevokedInterval ?? value.non_revoked + const nonRevokedInterval = value.non_revoked ?? globalNonRevokedInterval if (nonRevokedInterval) { value.restrictions?.forEach((restriction) => requestedNonRevokedRestrictions.push({ diff --git a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts index c71887047f..fe2088b2b9 100644 --- a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts +++ b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts @@ -1,7 +1,8 @@ import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' -import type { AgentContext } from '@aries-framework/core' +import type { AgentContext, FileSystem } from '@aries-framework/core' import { BasicTailsFileService } from '@aries-framework/anoncreds' +import { InjectionSymbols } from '@aries-framework/core' export class InMemoryTailsFileService extends BasicTailsFileService { private tailsFilePaths: Record = {} @@ -43,7 +44,8 @@ export class InMemoryTailsFileService extends BasicTailsFileService { if (!tailsExists) { agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) - // TODO + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + await fileSystem.downloadToFile(tailsLocation, tailsFilePath) agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) } diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 8a2becee90..a683723866 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -6,9 +6,7 @@ import { AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryDefinitionRecord, AnonCredsRevocationRegistryDefinitionRepository, - AnonCredsRevocationStatusListRecord, - AnonCredsRevocationStatusListRepository, - RevocationRegistryState, + AnonCredsRevocationRegistryState, AnonCredsModuleConfig, AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, @@ -215,7 +213,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository).save( agentContext, new AnonCredsRevocationRegistryDefinitionPrivateRecord({ - state: RevocationRegistryState.Active, + state: AnonCredsRevocationRegistryState.Active, value: revocationRegistryDefinitionPrivate, credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, revocationRegistryDefinitionId, @@ -223,7 +221,6 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean ) const createdRevocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { - issuanceByDefault: true, issuerId: indyDid, revocationRegistryDefinition, revocationRegistryDefinitionId, @@ -238,14 +235,6 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { throw new Error('Failed to create revocation status list') } - - await agentContext.dependencyManager.resolve(AnonCredsRevocationStatusListRepository).save( - agentContext, - new AnonCredsRevocationStatusListRecord({ - credentialDefinitionId: revocationRegistryDefinition.credDefId, - revocationStatusList: revocationStatusListState.revocationStatusList, - }) - ) } const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) @@ -301,9 +290,16 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean }) // Set attributes on the credential record, this is normally done by the protocol service issuerCredentialRecord.credentialAttributes = credentialAttributes + + // If revocable, specify revocation registry definition id and index + const credentialFormats = revocable + ? { anoncreds: { revocationRegistryDefinitionId, revocationRegistryIndex: 1 } } + : undefined + const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { credentialRecord: issuerCredentialRecord, proposalAttachment: proposalAttachment, + credentialFormats, }) // Holder processes and accepts offer diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index 60924e7a95..e3de7393f8 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -191,6 +191,7 @@ export async function issueAnonCredsCredential({ holderReplay, issuerHolderConnectionId, + revocationRegistryDefinitionId, offer, }: { issuerAgent: AnonCredsTestsAgent @@ -200,6 +201,7 @@ export async function issueAnonCredsCredential({ holderReplay: EventReplaySubject issuerHolderConnectionId: string + revocationRegistryDefinitionId?: string offer: AnonCredsOfferCredentialFormat }) { let issuerCredentialExchangeRecord = await issuerAgent.credentials.offerCredential({ @@ -207,7 +209,7 @@ export async function issueAnonCredsCredential({ connectionId: issuerHolderConnectionId, protocolVersion: 'v2', credentialFormats: { - anoncreds: offer, + anoncreds: { ...offer, revocationRegistryDefinitionId, revocationRegistryIndex: 1 }, }, autoAcceptCredential: AutoAcceptCredential.ContentApproved, }) @@ -426,7 +428,7 @@ export async function prepareForAnonCredsIssuance( schemaId: schema.schemaId, issuerId, tag: 'default', - supportRevocation, + supportRevocation: supportRevocation ?? false, }) // Wait some time pass to let ledger settle the object @@ -446,7 +448,6 @@ export async function prepareForAnonCredsIssuance( await sleep(1000) revocationStatusList = await registerRevocationStatusList(agent, { - issuanceByDefault: true, revocationRegistryDefinitionId: revocationRegistryDefinition?.revocationRegistryDefinitionId, issuerId, }) diff --git a/packages/anoncreds-rs/tests/helpers.ts b/packages/anoncreds-rs/tests/helpers.ts deleted file mode 100644 index dddde28048..0000000000 --- a/packages/anoncreds-rs/tests/helpers.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Agent, RevocationNotificationReceivedEvent } from '@aries-framework/core' -import type { Observable } from 'rxjs' - -import { CredentialEventTypes } from '@aries-framework/core' -import { catchError, filter, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' - -export async function waitForRevocationNotification( - agent: Agent, - options: { - threadId?: string - timeoutMs?: number - } -) { - const observable = agent.events.observable( - CredentialEventTypes.RevocationNotificationReceived - ) - - return waitForRevocationNotificationSubject(observable, options) -} - -export function waitForRevocationNotificationSubject( - subject: ReplaySubject | Observable, - { - threadId, - timeoutMs = 10000, - }: { - threadId?: string - timeoutMs?: number - } -) { - const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject - return firstValueFrom( - observable.pipe( - filter((e) => threadId === undefined || e.payload.credentialRecord.threadId === threadId), - timeout(timeoutMs), - catchError(() => { - throw new Error( - `RevocationNotificationReceivedEvent event not emitted within specified timeout: { - threadId: ${threadId}, - }` - ) - }), - map((e) => e.payload.credentialRecord) - ) - ) -} diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts index edba2aef65..b23d189201 100644 --- a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -13,10 +13,10 @@ import { import { describeRunInNodeVersion } from '../../../tests/runInVersion' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { waitForCredentialRecordSubject } from '../../core/tests' +import { waitForRevocationNotification } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' import { setupAnonCredsTests } from './anoncredsSetup' -import { waitForRevocationNotification } from './helpers' const credentialPreview = V2CredentialPreview.fromRecord({ name: 'John', @@ -106,6 +106,8 @@ describeRunInNodeVersion([18], 'IC v2 credential revocation', () => { anoncreds: { credentialDefinitionId: credentialDefinitionId, attributes: credentialPreview.attributes, + revocationRegistryDefinitionId, + revocationRegistryIndex: 1, }, }, }) diff --git a/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts index eeea245046..cfbca7ad41 100644 --- a/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts @@ -67,6 +67,7 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { holderReplay: aliceReplay, issuerReplay: faberReplay, issuerHolderConnectionId: faberConnectionId, + revocationRegistryDefinitionId, offer: { credentialDefinitionId, attributes: [ diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 948c095eeb..c2f23b3b29 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -36,9 +36,7 @@ import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository, AnonCredsRevocationRegistryDefinitionRecord, - AnonCredsRevocationStatusListRecord, - AnonCredsRevocationStatusListRepository, - RevocationRegistryState, + AnonCredsRevocationRegistryState, } from './repository' import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' @@ -66,7 +64,6 @@ export class AnonCredsApi { private anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository private anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository private anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository - private anonCredsRevocationStatusListRepository: AnonCredsRevocationStatusListRepository private anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository private anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository private anonCredsIssuerService: AnonCredsIssuerService @@ -79,7 +76,6 @@ export class AnonCredsApi { @inject(AnonCredsIssuerServiceSymbol) anonCredsIssuerService: AnonCredsIssuerService, @inject(AnonCredsHolderServiceSymbol) anonCredsHolderService: AnonCredsHolderService, anonCredsSchemaRepository: AnonCredsSchemaRepository, - anonCredsRevocationStatusListRepository: AnonCredsRevocationStatusListRepository, anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository, anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository, anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository, @@ -95,7 +91,6 @@ export class AnonCredsApi { this.anonCredsSchemaRepository = anonCredsSchemaRepository this.anonCredsRevocationRegistryDefinitionRepository = anonCredsRevocationRegistryDefinitionRepository this.anonCredsRevocationRegistryDefinitionPrivateRepository = anonCredsRevocationRegistryDefinitionPrivateRepository - this.anonCredsRevocationStatusListRepository = anonCredsRevocationStatusListRepository this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository this.anonCredsCredentialDefinitionPrivateRepository = anonCredsCredentialDefinitionPrivateRepository this.anonCredsKeyCorrectnessProofRepository = anonCredsKeyCorrectnessProofRepository @@ -280,8 +275,8 @@ export class AnonCredsApi { { issuerId: options.credentialDefinition.issuerId, schemaId: options.credentialDefinition.schemaId, - tag: options.credentialDefinition.tag ?? 'default', - supportRevocation: options.credentialDefinition.supportRevocation ?? false, + tag: options.credentialDefinition.tag, + supportRevocation: options.credentialDefinition.supportRevocation, schema: schemaResult.schema, }, // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. @@ -292,7 +287,7 @@ export class AnonCredsApi { const result = await registry.registerCredentialDefinition(this.agentContext, { credentialDefinition, - options: {}, + options: options.options, }) await this.storeCredentialDefinitionRecord(registry, result, credentialDefinitionPrivate, keyCorrectnessProof) @@ -353,8 +348,7 @@ export class AnonCredsApi { }): Promise { const { issuerId, tag, credentialDefinitionId, maximumCredentialNumber } = options.revocationRegistryDefinition - const tailsFileService = - this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService const tailsDirectoryPath = await tailsFileService.getTailsBasePath(this.agentContext) @@ -459,7 +453,7 @@ export class AnonCredsApi { revocationStatusList: AnonCredsRegisterRevocationStatusListOptions options: Extensible }): Promise { - const { issuanceByDefault, issuerId, revocationRegistryDefinitionId } = options.revocationStatusList + const { issuerId, revocationRegistryDefinitionId } = options.revocationStatusList const failedReturnBase = { revocationStatusListState: { @@ -492,7 +486,6 @@ export class AnonCredsApi { try { const revocationStatusList = await this.anonCredsIssuerService.createRevocationStatusList(this.agentContext, { - issuanceByDefault, issuerId, revocationRegistryDefinition, revocationRegistryDefinitionId, @@ -504,8 +497,6 @@ export class AnonCredsApi { options: {}, }) - await this.storeRevocationStatusListRecord(result, revocationRegistryDefinition.credDefId) - return result } catch (error) { // Storage failed @@ -578,8 +569,6 @@ export class AnonCredsApi { options: {}, }) - await this.storeRevocationStatusListRecord(result, revocationRegistryDefinition.credDefId) - return result } catch (error) { // Storage failed @@ -641,7 +630,7 @@ export class AnonCredsApi { revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, credentialDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, value: revocationRegistryDefinitionPrivate, - state: RevocationRegistryState.Active, + state: AnonCredsRevocationRegistryState.Active, }) await this.anonCredsRevocationRegistryDefinitionPrivateRepository.save( this.agentContext, @@ -654,24 +643,6 @@ export class AnonCredsApi { } } - private async storeRevocationStatusListRecord( - result: RegisterRevocationStatusListReturn, - credentialDefinitionId: string - ): Promise { - try { - if (result.revocationStatusListState.revocationStatusList && result.revocationStatusListState.timestamp) { - const revocationStatusListRecord = new AnonCredsRevocationStatusListRecord({ - revocationStatusList: result.revocationStatusListState.revocationStatusList, - credentialDefinitionId, - }) - - await this.anonCredsRevocationStatusListRepository.save(this.agentContext, revocationStatusListRecord) - } - } catch (error) { - throw new AnonCredsStoreRecordError(`Error storing revocation status list record`, { cause: error }) - } - } - private async storeCredentialDefinitionRecord( registry: AnonCredsRegistry, result: RegisterCredentialDefinitionReturn, diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index a7629778a8..c91da426ed 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -6,8 +6,8 @@ export interface AnonCredsCreateLinkSecretOptions { export interface AnonCredsRegisterCredentialDefinitionOptions { issuerId: string schemaId: string - tag?: string - supportRevocation?: boolean + tag: string + supportRevocation: boolean } export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { @@ -19,7 +19,6 @@ export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { export interface AnonCredsRegisterRevocationStatusListOptions { issuerId: string - issuanceByDefault: boolean revocationRegistryDefinitionId: string } diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index 409604f2d7..afc698beda 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -9,7 +9,6 @@ import { AnonCredsLinkSecretRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryDefinitionRepository, - AnonCredsRevocationStatusListRepository, } from './repository' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' @@ -41,7 +40,6 @@ export class AnonCredsModule implements Module { dependencyManager.registerSingleton(AnonCredsLinkSecretRepository) dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionRepository) dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionPrivateRepository) - dependencyManager.registerSingleton(AnonCredsRevocationStatusListRepository) } public updates = [ diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts index 44917cc123..c0cbbf9b48 100644 --- a/packages/anoncreds/src/AnonCredsModuleConfig.ts +++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts @@ -13,12 +13,6 @@ export interface AnonCredsModuleConfigOptions { */ registries: [AnonCredsRegistry, ...AnonCredsRegistry[]] - /** - * Maximum credential number per revocation registry - * @default 1000 - */ - maximumCredentialNumberPerRevocationRegistry?: number - /** * Tails file service for download/uploading tails files * @default BasicTailsFileService (only for downloading tails files) @@ -41,11 +35,6 @@ export class AnonCredsModuleConfig { return this.options.registries } - /** See {@link AnonCredsModuleConfigOptions.maximumCredentialNumberPerRevocationRegistry} */ - public get maximumCredentialNumberPerRevocationRegistry() { - return this.options.maximumCredentialNumberPerRevocationRegistry ?? 1000 - } - /** See {@link AnonCredsModuleConfigOptions.tailsFileService} */ public get tailsFileService() { return this.options.tailsFileService ?? new BasicTailsFileService() diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts index a7d7455a7f..4d50faefd8 100644 --- a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -11,7 +11,6 @@ import { AnonCredsLinkSecretRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryDefinitionRepository, - AnonCredsRevocationStatusListRepository, } from '../repository' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' @@ -40,7 +39,6 @@ describe('AnonCredsModule', () => { expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( AnonCredsRevocationRegistryDefinitionPrivateRepository ) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRevocationStatusListRepository) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(AnonCredsModuleConfig, anonCredsModule.config) diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts index dba5361a41..716265588a 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts @@ -42,6 +42,8 @@ export interface AnonCredsProposeCredentialFormat { */ export interface AnonCredsAcceptProposalFormat { credentialDefinitionId?: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number attributes?: CredentialPreviewAttributeOptions[] linkedAttachments?: LinkedAttachment[] } @@ -60,6 +62,8 @@ export interface AnonCredsAcceptOfferFormat { */ export interface AnonCredsOfferCredentialFormat { credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number attributes: CredentialPreviewAttributeOptions[] linkedAttachments?: LinkedAttachment[] } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 47d4356d26..5ed715f649 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -48,7 +48,7 @@ import { AnonCredsCredentialDefinitionRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryDefinitionRepository, - RevocationRegistryState, + AnonCredsRevocationRegistryState, } from '../repository' import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' @@ -168,6 +168,8 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService attachmentId, attributes, credentialDefinitionId, + revocationRegistryDefinitionId: anoncredsFormat?.revocationRegistryDefinitionId, + revocationRegistryIndex: anoncredsFormat?.revocationRegistryIndex, linkedAttachments: anoncredsFormat?.linkedAttachments, }) @@ -196,6 +198,8 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService attachmentId, attributes: anoncredsFormat.attributes, credentialDefinitionId: anoncredsFormat.credentialDefinitionId, + revocationRegistryDefinitionId: anoncredsFormat.revocationRegistryDefinitionId, + revocationRegistryIndex: anoncredsFormat.revocationRegistryIndex, linkedAttachments: anoncredsFormat.linkedAttachments, }) @@ -320,28 +324,36 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService ).credentialDefinition.value let revocationRegistryDefinitionId + let revocationRegistryIndex let revocationStatusList let tailsFilePath if (credentialDefinition.revocation) { - const [revocationRegistryDefinitionPrivateRecord] = await agentContext.dependencyManager - .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) - .findAllByCredentialDefinitionIdAndState( - agentContext, - credentialRequest.cred_def_id, - RevocationRegistryState.Active - ) + const credentialMetadata = + credentialRecord.metadata.get(AnonCredsCredentialMetadataKey) + revocationRegistryDefinitionId = credentialMetadata?.revocationRegistryId + if (credentialMetadata?.credentialRevocationId) { + revocationRegistryIndex = Number(credentialMetadata.credentialRevocationId) + } - // TODO: should a new revocation registry be generated/published/etc automatically? - if (!revocationRegistryDefinitionPrivateRecord) { - throw new AriesFrameworkError(`No available revocation registry found for ${credentialRequest.cred_def_id}`) + if (!revocationRegistryDefinitionId || !revocationRegistryIndex) { + throw new AriesFrameworkError( + 'Revocation registry definition id and revocation index are mandatory to issue AnonCreds revocable credentials' + ) } + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) - revocationRegistryDefinitionId = revocationRegistryDefinitionPrivateRecord.revocationRegistryDefinitionId + if (revocationRegistryDefinitionPrivateRecord.state !== AnonCredsRevocationRegistryState.Active) { + throw new AriesFrameworkError( + `Revocation registry ${revocationRegistryDefinitionId} is in ${revocationRegistryDefinitionPrivateRecord.state} state` + ) + } // get current revocation status list const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - const registry = registryService.getRegistryForIdentifier(agentContext, credentialRequest.cred_def_id) + const registry = registryService.getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) const result = await registry.getRevocationStatusList( agentContext, revocationRegistryDefinitionId, @@ -360,31 +372,22 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - ;({ tailsFilePath } = await tailsFileService.downloadTailsFile(agentContext, { + const downloadTailsFileReturn = await tailsFileService.downloadTailsFile(agentContext, { revocationRegistryDefinition, - })) + }) + tailsFilePath = downloadTailsFileReturn.tailsFilePath } - const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { + const { credential } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer, credentialRequest, credentialValues: convertAttributesToCredentialValues(credentialAttributes), revocationRegistryDefinitionId, + revocationRegistryIndex, revocationStatusList, tailsFilePath, }) - if (credential.rev_reg_id) { - credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { - credentialRevocationId: credentialRevocationId, - revocationRegistryId: credential.rev_reg_id, - }) - credentialRecord.setTags({ - anonCredsRevocationRegistryId: credential.rev_reg_id, - anonCredsCredentialRevocationId: credentialRevocationId, - }) - } - const format = new CredentialFormatSpec({ attachmentId, format: ANONCREDS_CREDENTIAL, @@ -589,10 +592,14 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService credentialRecord, attachmentId, credentialDefinitionId, + revocationRegistryDefinitionId, + revocationRegistryIndex, attributes, linkedAttachments, }: { credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number credentialRecord: CredentialExchangeRecord attachmentId?: string attributes: CredentialPreviewAttributeOptions[] @@ -619,9 +626,34 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService await this.assertPreviewAttributesMatchSchemaAttributes(agentContext, offer, previewAttributes) + // We check locally for credential definition info. If it supports revocation, revocationRegistryIndex + // and revocationRegistryDefinitionId are mandatory + const credentialDefinition = ( + await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, offer.cred_def_id) + ).credentialDefinition.value + + if (credentialDefinition.revocation) { + if (!revocationRegistryDefinitionId || !revocationRegistryIndex) { + throw new AriesFrameworkError( + 'AnonCreds revocable credentials require revocationRegistryDefinitionId and revocationRegistryIndex' + ) + } + + // Set revocation tags + credentialRecord.setTags({ + anonCredsRevocationRegistryId: revocationRegistryDefinitionId, + anonCredsCredentialRevocationId: revocationRegistryIndex.toString(), + }) + } + + // Set the metadata credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { schemaId: offer.schema_id, credentialDefinitionId: offer.cred_def_id, + credentialRevocationId: revocationRegistryIndex?.toString(), + revocationRegistryId: revocationRegistryDefinitionId, }) const attachment = this.getFormatData(offer, format.attachmentId) diff --git a/packages/anoncreds/src/models/registry.ts b/packages/anoncreds/src/models/registry.ts index 10112a69c8..bc4ad8a152 100644 --- a/packages/anoncreds/src/models/registry.ts +++ b/packages/anoncreds/src/models/registry.ts @@ -23,7 +23,6 @@ export interface AnonCredsRevocationRegistryDefinition { credDefId: string tag: string value: { - issuanceType: 'ISSUANCE_BY_DEFAULT' | 'ISSUANCE_ON_DEMAND' publicKeys: { accumKey: { z: string diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts index d3748c610b..50f6c39f08 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts @@ -2,7 +2,7 @@ import type { TagsBase } from '@aries-framework/core' import { BaseRecord, utils } from '@aries-framework/core' -export enum RevocationRegistryState { +export enum AnonCredsRevocationRegistryState { Created = 'created', Active = 'active', Full = 'full', @@ -14,13 +14,13 @@ export interface AnonCredsRevocationRegistryDefinitionPrivateRecordProps { credentialDefinitionId: string value: Record index?: number - state?: RevocationRegistryState + state?: AnonCredsRevocationRegistryState } export type DefaultAnonCredsRevocationRegistryPrivateTags = { revocationRegistryDefinitionId: string credentialDefinitionId: string - state: RevocationRegistryState + state: AnonCredsRevocationRegistryState } export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseRecord< @@ -34,9 +34,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco public readonly credentialDefinitionId!: string public readonly value!: Record // TODO: Define structure - public currentIndex!: number - - public state!: RevocationRegistryState + public state!: AnonCredsRevocationRegistryState public constructor(props: AnonCredsRevocationRegistryDefinitionPrivateRecordProps) { super() @@ -46,8 +44,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseReco this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId this.credentialDefinitionId = props.credentialDefinitionId this.value = props.value - this.currentIndex = props.index ?? 0 - this.state = props.state ?? RevocationRegistryState.Created + this.state = props.state ?? AnonCredsRevocationRegistryState.Created } } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts index 6bb4523db9..0f571d2a1a 100644 --- a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts @@ -1,4 +1,4 @@ -import type { RevocationRegistryState } from './AnonCredsRevocationRegistryDefinitionPrivateRecord' +import type { AnonCredsRevocationRegistryState } from './AnonCredsRevocationRegistryDefinitionPrivateRecord' import type { AgentContext } from '@aries-framework/core' import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' @@ -29,7 +29,7 @@ export class AnonCredsRevocationRegistryDefinitionPrivateRepository extends Repo public async findAllByCredentialDefinitionIdAndState( agentContext: AgentContext, credentialDefinitionId: string, - state?: RevocationRegistryState + state?: AnonCredsRevocationRegistryState ) { return this.findByQuery(agentContext, { credentialDefinitionId, state }) } diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts deleted file mode 100644 index 234be32696..0000000000 --- a/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRecord.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { AnonCredsRevocationStatusList } from '../models' -import type { TagsBase } from '@aries-framework/core' - -import { BaseRecord, utils } from '@aries-framework/core' - -export interface AnonCredsRevocationStatusListRecordProps { - id?: string - credentialDefinitionId: string - revocationStatusList: AnonCredsRevocationStatusList -} - -export type DefaultAnonCredsRevocationStatusListTags = { - revocationRegistryDefinitionId: string - credentialDefinitionId: string - timestamp: string -} - -export class AnonCredsRevocationStatusListRecord extends BaseRecord< - DefaultAnonCredsRevocationStatusListTags, - TagsBase -> { - public static readonly type = 'AnonCredsRevocationStatusListRecord' - public readonly type = AnonCredsRevocationStatusListRecord.type - - public readonly credentialDefinitionId!: string - public readonly revocationStatusList!: AnonCredsRevocationStatusList - - public constructor(props: AnonCredsRevocationStatusListRecordProps) { - super() - - if (props) { - this.id = props.id ?? utils.uuid() - this.credentialDefinitionId = props.credentialDefinitionId - this.revocationStatusList = props.revocationStatusList - } - } - - public getTags() { - return { - ...this._tags, - revocationRegistryDefinitionId: this.revocationStatusList.revRegDefId, - credentialDefinitionId: this.credentialDefinitionId, - timestamp: this.revocationStatusList.timestamp.toString(), - } - } -} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRepository.ts deleted file mode 100644 index 010f059dd9..0000000000 --- a/packages/anoncreds/src/repository/AnonCredsRevocationStatusListRepository.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { AgentContext } from '@aries-framework/core' - -import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' - -import { AnonCredsRevocationStatusListRecord } from './AnonCredsRevocationStatusListRecord' - -@injectable() -export class AnonCredsRevocationStatusListRepository extends Repository { - public constructor( - @inject(InjectionSymbols.StorageService) - storageService: StorageService, - eventEmitter: EventEmitter - ) { - super(AnonCredsRevocationStatusListRecord, storageService, eventEmitter) - } - - public async getByRevocationRegistryDefinitionIdAndTimestamp( - agentContext: AgentContext, - revocationRegistryDefinitionId: string, - timestamp: string - ) { - return this.getSingleByQuery(agentContext, { revocationRegistryDefinitionId, timestamp }) - } - - public async findByRevocationRegistryDefinitionIdAndTimestamp( - agentContext: AgentContext, - revocationRegistryDefinitionId: string, - timestamp: string - ) { - return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId, timestamp }) - } - - public async findAllByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { - return this.findByQuery(agentContext, { credentialDefinitionId }) - } - - public async findAllByRevocationRegistryDefinitionId( - agentContext: AgentContext, - revocationRegistryDefinitionId: string - ) { - return this.findByQuery(agentContext, { revocationRegistryDefinitionId }) - } -} diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts index a87ac8b2a9..8772b528e7 100644 --- a/packages/anoncreds/src/repository/index.ts +++ b/packages/anoncreds/src/repository/index.ts @@ -12,7 +12,5 @@ export * from './AnonCredsRevocationRegistryDefinitionRecord' export * from './AnonCredsRevocationRegistryDefinitionRepository' export * from './AnonCredsRevocationRegistryDefinitionPrivateRecord' export * from './AnonCredsRevocationRegistryDefinitionPrivateRepository' -export * from './AnonCredsRevocationStatusListRecord' -export * from './AnonCredsRevocationStatusListRepository' export * from './AnonCredsSchemaRecord' export * from './AnonCredsSchemaRepository' diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index 77427764fd..92de9b80c9 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -37,7 +37,6 @@ export interface CreateRevocationRegistryDefinitionOptions { export interface CreateRevocationStatusListOptions { issuerId: string - issuanceByDefault: boolean revocationRegistryDefinitionId: string revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition tailsFilePath: string @@ -63,6 +62,7 @@ export interface CreateCredentialOptions { revocationRegistryDefinitionId?: string tailsFilePath?: string revocationStatusList?: AnonCredsRevocationStatusList + revocationRegistryIndex?: number } export interface CreateCredentialReturn { diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index 62448ee0b7..76ad4f7682 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -46,7 +46,6 @@ const existingRevocationRegistryDefinitions = { issuerId: 'VsKV7grR1BUE29mG2Fm2kX', revocDefType: 'CL_ACCUM', value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', publicKeys: { accumKey: { z: 'ab81257c-be63-4051-9e21-c7d384412f64', @@ -200,6 +199,7 @@ describe('AnonCreds API', () => { issuerId, schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', tag: 'TAG', + supportRevocation: false, }, options: {}, }) diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 7565352e10..8720c2dfc9 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -465,6 +465,7 @@ export async function prepareForAnonCredsIssuance(agent: Agent, { attributeNames schemaId: schema.schemaId, issuerId: didIndyDid, tag: 'default', + supportRevocation: false, }) const s = parseIndySchemaId(schema.schemaId) diff --git a/packages/cheqd/src/anoncreds/utils/transform.ts b/packages/cheqd/src/anoncreds/utils/transform.ts index fa7061210d..47c7b076a7 100644 --- a/packages/cheqd/src/anoncreds/utils/transform.ts +++ b/packages/cheqd/src/anoncreds/utils/transform.ts @@ -102,9 +102,6 @@ export class CheqdRevocationRegistryDefinitionValue { @IsString() public tailsHash!: string - - @IsString() - public issuanceType!: 'ISSUANCE_BY_DEFAULT' | 'ISSUANCE_ON_DEMAND' } export class CheqdRevocationRegistryDefinition { diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 8537d5c35f..7ca66942aa 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -14,6 +14,7 @@ import type { CredentialState, ConnectionStateChangedEvent, Buffer, + RevocationNotificationReceivedEvent, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../src/modules/connections/TrustPingEvents' @@ -455,6 +456,47 @@ export async function waitForBasicMessage(agent: Agent, { content }: { content?: }) } +export async function waitForRevocationNotification( + agent: Agent, + options: { + threadId?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable( + CredentialEventTypes.RevocationNotificationReceived + ) + + return waitForRevocationNotificationSubject(observable, options) +} + +export function waitForRevocationNotificationSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + }: { + threadId?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter((e) => threadId === undefined || e.payload.credentialRecord.threadId === threadId), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `RevocationNotificationReceivedEvent event not emitted within specified timeout: { + threadId: ${threadId}, + }` + ) + }), + map((e) => e.payload.credentialRecord) + ) + ) +} + export function getMockConnection({ state = DidExchangeState.InvitationReceived, role = DidExchangeRole.Requester, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index 827ec96669..a386675b1e 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -437,7 +437,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { issuerId: did, credDefId: credentialDefinitionId, value: { - issuanceType: revocationRegistryDefinition.value.issuanceType, maxCredNum: revocationRegistryDefinition.value.maxCredNum, publicKeys: revocationRegistryDefinition.value.publicKeys, tailsHash: revocationRegistryDefinition.value.tailsHash, diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index dd18548baa..a3e69929b0 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -446,7 +446,6 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { issuerId: did, revocDefType: response.result.data.revocDefType, value: { - issuanceType: response.result.data.value.issuanceType, maxCredNum: response.result.data.value.maxCredNum, tailsHash: response.result.data.value.tailsHash, tailsLocation: response.result.data.value.tailsLocation, From d5c7b6d24a2bf347f4ef73d4727c27f1c960093f Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 1 Jun 2023 19:42:49 -0300 Subject: [PATCH 18/27] feat: simplify TailsFileService interface Signed-off-by: Ariel Gentile --- .../tests/InMemoryTailsFileService.ts | 10 ++---- packages/anoncreds/src/AnonCredsApi.ts | 4 +-- .../AnonCredsCredentialFormatService.ts | 3 +- .../services/tails/BasicTailsFileService.ts | 30 +++++++--------- .../src/services/tails/TailsFileService.ts | 35 ++++++++++++++----- .../src/utils/getRevocationRegistries.ts | 2 +- samples/tails/README.md | 4 +-- 7 files changed, 49 insertions(+), 39 deletions(-) diff --git a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts index fe2088b2b9..32cb2d48f4 100644 --- a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts +++ b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts @@ -19,14 +19,12 @@ export class InMemoryTailsFileService extends BasicTailsFileService { return options.revocationRegistryDefinition.value.tailsHash } - public async downloadTailsFile( + public async getTailsFile( agentContext: AgentContext, options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } - ): Promise<{ - tailsFilePath: string - }> { + ): Promise { const { revocationRegistryDefinition } = options const { tailsLocation, tailsHash } = revocationRegistryDefinition.value @@ -49,9 +47,7 @@ export class InMemoryTailsFileService extends BasicTailsFileService { agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) } - return { - tailsFilePath, - } + return tailsFilePath } catch (error) { agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { error, diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index c2f23b3b29..ece6d0728b 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -480,7 +480,7 @@ export class AnonCredsApi { return failedReturnBase } const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const { tailsFilePath } = await tailsFileService.downloadTailsFile(this.agentContext, { + const tailsFilePath = await tailsFileService.getTailsFile(this.agentContext, { revocationRegistryDefinition, }) @@ -551,7 +551,7 @@ export class AnonCredsApi { } const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const { tailsFilePath } = await tailsFileService.downloadTailsFile(this.agentContext, { + const tailsFilePath = await tailsFileService.getTailsFile(this.agentContext, { revocationRegistryDefinition, }) diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 5ed715f649..a7b0262d68 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -372,10 +372,9 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const downloadTailsFileReturn = await tailsFileService.downloadTailsFile(agentContext, { + tailsFilePath = await tailsFileService.getTailsFile(agentContext, { revocationRegistryDefinition, }) - tailsFilePath = downloadTailsFileReturn.tailsFilePath } const { credential } = await anonCredsIssuerService.createCredential(agentContext, { diff --git a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts index 36e6b57335..f2cf3eeae1 100644 --- a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts +++ b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts @@ -20,16 +20,6 @@ export class BasicTailsFileService implements TailsFileService { return basePath } - public async getTailsFilePath(agentContext: AgentContext, tailsHash: string) { - return `${await this.getTailsBasePath(agentContext)}/${tailsHash}` - } - - public async tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { - const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) - const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) - return await fileSystem.exists(tailsFilePath) - } - public async uploadTailsFile( // eslint-disable-next-line @typescript-eslint/no-unused-vars agentContext: AgentContext, @@ -41,14 +31,12 @@ export class BasicTailsFileService implements TailsFileService { throw new AriesFrameworkError('BasicTailsFileService only supports tails file downloading') } - public async downloadTailsFile( + public async getTailsFile( agentContext: AgentContext, options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } - ): Promise<{ - tailsFilePath: string - }> { + ): Promise { const { revocationRegistryDefinition } = options const { tailsLocation, tailsHash } = revocationRegistryDefinition.value @@ -79,9 +67,7 @@ export class BasicTailsFileService implements TailsFileService { agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) } - return { - tailsFilePath, - } + return tailsFilePath } catch (error) { agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { error, @@ -89,4 +75,14 @@ export class BasicTailsFileService implements TailsFileService { throw error } } + + protected async getTailsFilePath(agentContext: AgentContext, tailsHash: string) { + return `${await this.getTailsBasePath(agentContext)}/${tailsHash}` + } + + protected async tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) + return await fileSystem.exists(tailsFilePath) + } } diff --git a/packages/anoncreds/src/services/tails/TailsFileService.ts b/packages/anoncreds/src/services/tails/TailsFileService.ts index 57f2c94fc9..d8e0dd7167 100644 --- a/packages/anoncreds/src/services/tails/TailsFileService.ts +++ b/packages/anoncreds/src/services/tails/TailsFileService.ts @@ -2,12 +2,23 @@ import type { AnonCredsRevocationRegistryDefinition } from '../../models' import type { AgentContext } from '@aries-framework/core' export interface TailsFileService { + /** + * Retrieve base directory for tail file storage + * + * @param agentContext + */ getTailsBasePath(agentContext: AgentContext): string | Promise - getTailsFilePath(agentContext: AgentContext, tailsHash: string): string | Promise - - tailsFileExists(agentContext: AgentContext, tailsHash: string): boolean | Promise - + /** + * Upload the tails file for a given revocation registry definition. + * + * Optionally, receives revocationRegistryDefinitionId in case the ID is + * known beforehand. + * + * Returns the published tail file URL + * @param agentContext + * @param options + */ uploadTailsFile( agentContext: AgentContext, options: { @@ -16,13 +27,21 @@ export interface TailsFileService { } ): Promise - downloadTailsFile( + /** + * Retrieve the tails file for a given revocation registry, downloading it + * from the tailsLocation URL if not present in internal cache + * + * Classes implementing this interface should verify integrity of the downloaded + * file. + * + * @param agentContext + * @param options + */ + getTailsFile( agentContext: AgentContext, options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId?: string } - ): Promise<{ - tailsFilePath: string - }> + ): Promise } diff --git a/packages/anoncreds/src/utils/getRevocationRegistries.ts b/packages/anoncreds/src/utils/getRevocationRegistries.ts index 2606619a7a..42601413f6 100644 --- a/packages/anoncreds/src/utils/getRevocationRegistries.ts +++ b/packages/anoncreds/src/utils/getRevocationRegistries.ts @@ -89,7 +89,7 @@ export async function getRevocationRegistriesForRequest( } const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const { tailsFilePath } = await tailsFileService.downloadTailsFile(agentContext, { + const tailsFilePath = await tailsFileService.getTailsFile(agentContext, { revocationRegistryDefinition, }) diff --git a/samples/tails/README.md b/samples/tails/README.md index 449ad3d10d..838d207160 100644 --- a/samples/tails/README.md +++ b/samples/tails/README.md @@ -1,5 +1,5 @@ -

Test tails file server

+

Sample tails file server

-This is a very simple server that is used to host tails files during for tests suites where revocable credentials are issued and verified. +This is a very simple server that can be used to host AnonCreds tails files. It is intended to be used only for development purposes. It offers a single endpoint at the root that takes an URI-encoded `tailsFileId` as URL path and allows to upload (using PUT method and a through a multi-part encoded form) or retrieve a tails file (using GET method). From d4685cf161636eb5fb6a13139629e0722953714e Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 1 Jun 2023 20:34:09 -0300 Subject: [PATCH 19/27] fix: some adjustments after merge Signed-off-by: Ariel Gentile --- packages/anoncreds-rs/tests/anoncredsSetup.ts | 19 ++++++++++++------- packages/anoncreds/src/AnonCredsApiOptions.ts | 1 - .../services/AnonCredsIssuerServiceOptions.ts | 2 +- .../RevocationRegistryDefinitionOptions.ts | 4 ++-- .../registry/RevocationStatusListOptions.ts | 4 ++-- .../anoncreds/tests/legacyAnonCredsSetup.ts | 5 +++-- 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index e3de7393f8..5f3cbd85ab 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -424,12 +424,15 @@ export async function prepareForAnonCredsIssuance( // Wait some time pass to let ledger settle the object await sleep(1000) - const credentialDefinition = await registerCredentialDefinition(agent, { - schemaId: schema.schemaId, - issuerId, - tag: 'default', - supportRevocation: supportRevocation ?? false, - }) + const credentialDefinition = await registerCredentialDefinition( + agent, + { + schemaId: schema.schemaId, + issuerId, + tag: 'default', + }, + supportRevocation + ) // Wait some time pass to let ledger settle the object await sleep(1000) @@ -497,10 +500,12 @@ async function registerSchema( async function registerCredentialDefinition( agent: AnonCredsTestsAgent, - credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions, + supportRevocation?: boolean ): Promise { const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition, + supportRevocation: supportRevocation ?? false, options: {}, }) diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index c91da426ed..64bb1e1d61 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -7,7 +7,6 @@ export interface AnonCredsRegisterCredentialDefinitionOptions { issuerId: string schemaId: string tag: string - supportRevocation: boolean } export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index 92de9b80c9..309a0196af 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -21,7 +21,7 @@ export interface CreateSchemaOptions { export interface CreateCredentialDefinitionOptions { issuerId: string tag: string - supportRevocation?: boolean + supportRevocation: boolean schemaId: string schema: AnonCredsSchema } diff --git a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts index caf50268b9..3f7a07ed77 100644 --- a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts @@ -1,5 +1,5 @@ import type { - AnonCredsOperationState, + AnonCredsOperationStateWait, AnonCredsOperationStateFailed, AnonCredsOperationStateFinished, AnonCredsResolutionMetadata, @@ -29,7 +29,7 @@ export interface RegisterRevocationRegistryDefinitionReturnStateFinished extends revocationRegistryDefinitionId: string } -export interface RegisterRevocationRegistryDefinitionReturnState extends AnonCredsOperationState { +export interface RegisterRevocationRegistryDefinitionReturnState extends AnonCredsOperationStateWait { revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId?: string } diff --git a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index d75e829e63..05b1353801 100644 --- a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -1,5 +1,5 @@ import type { - AnonCredsOperationState, + AnonCredsOperationStateWait, AnonCredsOperationStateFailed, AnonCredsOperationStateFinished, AnonCredsResolutionMetadata, @@ -30,7 +30,7 @@ export interface RegisterRevocationStatusListReturnStateFinished extends AnonCre timestamp: string } -export interface RegisterRevocationStatusListReturnState extends AnonCredsOperationState { +export interface RegisterRevocationStatusListReturnState extends AnonCredsOperationStateWait { revocationStatusList?: AnonCredsRevocationStatusList timestamp?: string } diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 8720c2dfc9..84ec29d202 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -465,7 +465,6 @@ export async function prepareForAnonCredsIssuance(agent: Agent, { attributeNames schemaId: schema.schemaId, issuerId: didIndyDid, tag: 'default', - supportRevocation: false, }) const s = parseIndySchemaId(schema.schemaId) @@ -518,10 +517,12 @@ async function registerSchema( async function registerCredentialDefinition( agent: AnonCredsTestsAgent, - credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions, + supportRevocation?: boolean ): Promise { const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition, + supportRevocation: supportRevocation ?? false, options: {}, }) From 505bf080889ac5ef5a46d796560cc3c02b454832 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 1 Jun 2023 20:36:08 -0300 Subject: [PATCH 20/27] fix: types Signed-off-by: Ariel Gentile --- packages/anoncreds/tests/anoncreds.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index 76ad4f7682..6885da18ff 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -199,8 +199,8 @@ describe('AnonCreds API', () => { issuerId, schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', tag: 'TAG', - supportRevocation: false, }, + supportRevocation: false, options: {}, }) From 3ebb665f45a78c72cbc5736c1c58208741a8708e Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 22 Jul 2023 15:53:52 -0300 Subject: [PATCH 21/27] fix: remove revocStatusList from credential creation interface Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 32 +++++++++++++------ .../AnonCredsCredentialFormatService.ts | 17 ---------- .../services/AnonCredsIssuerServiceOptions.ts | 1 - samples/tails/package.json | 4 +-- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index 7fd921c496..457e49d6c7 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -251,19 +251,15 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { credentialValues, revocationRegistryDefinitionId, tailsFilePath, - revocationStatusList, revocationRegistryIndex, } = options - const definedRevocationOptions = [ - revocationRegistryDefinitionId, - tailsFilePath, - revocationStatusList, - revocationRegistryIndex, - ].filter((e) => e !== undefined) - if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 4) { + const definedRevocationOptions = [revocationRegistryDefinitionId, tailsFilePath, revocationRegistryIndex].filter( + (e) => e !== undefined + ) + if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 3) { throw new AriesFrameworkError( - 'Revocation requires all of revocationRegistryDefinitionId, revocationStatusList, revocationRegistryIndex and tailsFilePath' + 'Revocation requires all of revocationRegistryDefinitionId, revocationRegistryIndex and tailsFilePath' ) } @@ -302,6 +298,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } let revocationConfiguration: CredentialRevocationConfig | undefined + let revocationStatusList: RevocationStatusList | undefined if (revocationRegistryDefinitionId && tailsFilePath && revocationRegistryIndex) { const revocationRegistryDefinitionRecord = await agentContext.dependencyManager .resolve(AnonCredsRevocationRegistryDefinitionRepository) @@ -327,6 +324,21 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { tailsPath: tailsFilePath, registryIndex: revocationRegistryIndex, }) + + // Dummy revocation status list object to pass to anoncreds library + // (FIXME: remove as soon as it is not required anymore by anoncreds-rs) + revocationStatusList = RevocationStatusList.create({ + issuanceByDefault: true, + issuerId: revocationRegistryDefinitionRecord.revocationRegistryDefinition.issuerId, + revocationRegistryDefinition: { + ...revocationRegistryDefinitionRecord.revocationRegistryDefinition, + value: { + ...revocationRegistryDefinitionRecord.revocationRegistryDefinition.value, + tailsLocation: tailsFilePath, + }, + } as unknown as JsonObject, + revocationRegistryDefinitionId: revocationRegistryDefinitionRecord.revocationRegistryDefinitionId, + }) } credential = Credential.create({ @@ -338,7 +350,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { attributeRawValues, credentialDefinitionPrivate: credentialDefinitionPrivateRecord.value, revocationConfiguration, - revocationStatusList: revocationStatusList ? (revocationStatusList as unknown as JsonObject) : undefined, + revocationStatusList, }) return { diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index a7b0262d68..19f594c653 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -351,22 +351,6 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService ) } - // get current revocation status list - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - const registry = registryService.getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) - const result = await registry.getRevocationStatusList( - agentContext, - revocationRegistryDefinitionId, - dateToTimestamp(new Date()) - ) - - if (!result.revocationStatusList) { - throw new AriesFrameworkError( - `Could not get current revocation status list for ${revocationRegistryDefinitionId}` - ) - } - revocationStatusList = result.revocationStatusList - const { revocationRegistryDefinition } = await agentContext.dependencyManager .resolve(AnonCredsRevocationRegistryDefinitionRepository) .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) @@ -383,7 +367,6 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService credentialValues: convertAttributesToCredentialValues(credentialAttributes), revocationRegistryDefinitionId, revocationRegistryIndex, - revocationStatusList, tailsFilePath, }) diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index 309a0196af..0968ac5524 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -61,7 +61,6 @@ export interface CreateCredentialOptions { credentialValues: AnonCredsCredentialValues revocationRegistryDefinitionId?: string tailsFilePath?: string - revocationStatusList?: AnonCredsRevocationStatusList revocationRegistryIndex?: number } diff --git a/samples/tails/package.json b/samples/tails/package.json index 3c0e6f3761..44cb263a53 100644 --- a/samples/tails/package.json +++ b/samples/tails/package.json @@ -15,8 +15,8 @@ "ts-node": "^10.4.0" }, "dependencies": { - "@aries-framework/anoncreds": "^0.3.3", - "@aries-framework/core": "^0.3.3", + "@aries-framework/anoncreds": "^0.4.0", + "@aries-framework/core": "^0.4.0", "@types/express": "^4.17.13", "@types/multer": "^1.4.7", "@types/uuid": "^9.0.1", From 98e5380a7fbe7cba75e3209aad5b79d6e28a8899 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 22 Jul 2023 16:04:26 -0300 Subject: [PATCH 22/27] fix: unused variable Signed-off-by: Ariel Gentile --- .../anoncreds/src/formats/AnonCredsCredentialFormatService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 19f594c653..1e2dce1414 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -325,7 +325,6 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService let revocationRegistryDefinitionId let revocationRegistryIndex - let revocationStatusList let tailsFilePath if (credentialDefinition.revocation) { From da6aa637ee7d8c1b707cb84f950f4c4396276ba7 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 22 Jul 2023 16:48:40 -0300 Subject: [PATCH 23/27] test: fix anoncreds module registration Signed-off-by: Ariel Gentile --- packages/anoncreds/src/__tests__/AnonCredsModule.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts index 4d50faefd8..02d0d13076 100644 --- a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -28,7 +28,7 @@ describe('AnonCredsModule', () => { }) anonCredsModule.register(dependencyManager) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(9) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(8) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRegistryService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsSchemaRepository) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionRepository) From 2c1a3b3d8681940e75bdfd67d7607bb25745cc49 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 22 Jul 2023 16:49:59 -0300 Subject: [PATCH 24/27] fix: remove unused import Signed-off-by: Ariel Gentile --- .../anoncreds/src/formats/AnonCredsCredentialFormatService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 1e2dce1414..f9153b41d6 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -60,7 +60,6 @@ import { createAndLinkAttachmentsToPreview, } from '../utils/credential' import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' -import { dateToTimestamp } from '../utils/timestamp' const ANONCREDS_CREDENTIAL_OFFER = 'anoncreds/credential-offer@v1.0' const ANONCREDS_CREDENTIAL_REQUEST = 'anoncreds/credential-request@v1.0' From e2f49640813144e7396571699e25bd5a15732657 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 17 Aug 2023 15:46:08 -0300 Subject: [PATCH 25/27] fix: use getOutboundMessageContext Signed-off-by: Ariel Gentile --- .../src/modules/credentials/CredentialsApi.ts | 58 ++++++------------- 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 8aecf44720..344368b138 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -418,7 +418,7 @@ export class CredentialsApi implements Credent } const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) if (!offerMessage) { - throw new AriesFrameworkError(`No offer message found for proof record with id '${credentialRecord.id}'`) + throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) } const { message } = await protocol.acceptRequest(this.agentContext, { @@ -508,47 +508,27 @@ export class CredentialsApi implements Credent const protocol = this.getProtocol(credentialRecord.protocolVersion) const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) + } // Use connection if present - if (credentialRecord.connectionId) { - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, - associatedRecord: credentialRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) - } - // Use ~service decorator otherwise - else if (requestMessage?.service && offerMessage?.service) { - const recipientService = requestMessage.service - const ourService = offerMessage.service - - message.service = ourService - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - ) - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot send revocation notification for credential record without connectionId or ~service decorator on credential offer / request.` - ) - } + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) } /** From 89d60cd659a0f251166af10ee7f86879edd513e9 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Fri, 3 Nov 2023 20:38:05 -0300 Subject: [PATCH 26/27] fix: adapt to latest anoncreds-rs API Signed-off-by: Ariel Gentile --- demo/package.json | 2 +- .../src/services/AnonCredsRsIssuerService.ts | 67 ++++++++++--------- .../AnonCredsCredentialFormatService.ts | 26 ++++--- .../services/AnonCredsIssuerServiceOptions.ts | 2 +- .../src/utils/getRevocationRegistries.ts | 55 ++++++++------- .../tests/InMemoryAnonCredsRegistry.ts | 1 - .../services/IndySdkIssuerService.ts | 17 ++--- packages/indy-vdr/package.json | 6 +- yarn.lock | 18 ++--- 9 files changed, 101 insertions(+), 93 deletions(-) diff --git a/demo/package.json b/demo/package.json index db91916361..9165d00063 100644 --- a/demo/package.json +++ b/demo/package.json @@ -14,7 +14,7 @@ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { - "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.3", + "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.4", "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "inquirer": "^8.2.5" diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index 457e49d6c7..f11c65b1b4 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -140,17 +140,24 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateRevocationStatusListOptions ): Promise { - const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition, tailsFilePath } = options + const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition } = options + + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, revocationRegistryDefinition.credDefId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) let revocationStatusList: RevocationStatusList | undefined try { revocationStatusList = RevocationStatusList.create({ issuanceByDefault: true, revocationRegistryDefinitionId, - revocationRegistryDefinition: { - ...revocationRegistryDefinition, - value: { ...revocationRegistryDefinition.value, tailsLocation: tailsFilePath }, - } as unknown as JsonObject, + credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, + revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, + revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value as unknown as JsonObject, issuerId, }) @@ -177,15 +184,25 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { timestamp, }) } else { + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, revocationRegistryDefinition.credDefId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationStatusList.revRegDefId) + revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson({ ...revocationRegistryDefinition, value: { ...revocationRegistryDefinition.value, tailsLocation: tailsFilePath }, } as unknown as JsonObject) updatedRevocationStatusList.update({ + credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, revocationRegistryDefinition: revocationRegistryDefinitionObj, + revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value, issued: options.issued, revoked: options.revoked, - timestamp: timestamp ?? -1, // TODO: Fix parameters in anoncreds-rs + timestamp, }) } @@ -250,16 +267,18 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { credentialRequest, credentialValues, revocationRegistryDefinitionId, - tailsFilePath, + revocationStatusList, revocationRegistryIndex, } = options - const definedRevocationOptions = [revocationRegistryDefinitionId, tailsFilePath, revocationRegistryIndex].filter( - (e) => e !== undefined - ) + const definedRevocationOptions = [ + revocationRegistryDefinitionId, + revocationStatusList, + revocationRegistryIndex, + ].filter((e) => e !== undefined) if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 3) { throw new AriesFrameworkError( - 'Revocation requires all of revocationRegistryDefinitionId, revocationRegistryIndex and tailsFilePath' + 'Revocation requires all of revocationRegistryDefinitionId, revocationRegistryIndex and revocationStatusList' ) } @@ -298,8 +317,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } let revocationConfiguration: CredentialRevocationConfig | undefined - let revocationStatusList: RevocationStatusList | undefined - if (revocationRegistryDefinitionId && tailsFilePath && revocationRegistryIndex) { + if (revocationRegistryDefinitionId && revocationStatusList && revocationRegistryIndex) { const revocationRegistryDefinitionRecord = await agentContext.dependencyManager .resolve(AnonCredsRevocationRegistryDefinitionRepository) .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) @@ -321,26 +339,10 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { registryDefinitionPrivate: RevocationRegistryDefinitionPrivate.fromJson( revocationRegistryDefinitionPrivateRecord.value ), - tailsPath: tailsFilePath, + statusList: RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject), registryIndex: revocationRegistryIndex, }) - - // Dummy revocation status list object to pass to anoncreds library - // (FIXME: remove as soon as it is not required anymore by anoncreds-rs) - revocationStatusList = RevocationStatusList.create({ - issuanceByDefault: true, - issuerId: revocationRegistryDefinitionRecord.revocationRegistryDefinition.issuerId, - revocationRegistryDefinition: { - ...revocationRegistryDefinitionRecord.revocationRegistryDefinition, - value: { - ...revocationRegistryDefinitionRecord.revocationRegistryDefinition.value, - tailsLocation: tailsFilePath, - }, - } as unknown as JsonObject, - revocationRegistryDefinitionId: revocationRegistryDefinitionRecord.revocationRegistryDefinitionId, - }) } - credential = Credential.create({ credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, credentialOffer: credentialOffer as unknown as JsonObject, @@ -350,7 +352,10 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { attributeRawValues, credentialDefinitionPrivate: credentialDefinitionPrivateRecord.value, revocationConfiguration, - revocationStatusList, + // FIXME: duplicated input parameter? + revocationStatusList: revocationStatusList + ? RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject) + : undefined, }) return { diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index f9153b41d6..9a53590d12 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -41,17 +41,16 @@ import { JsonTransformer, } from '@aries-framework/core' -import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' import { AnonCredsError } from '../error' import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal' import { AnonCredsCredentialDefinitionRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, - AnonCredsRevocationRegistryDefinitionRepository, AnonCredsRevocationRegistryState, } from '../repository' import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' +import { dateToTimestamp } from '../utils' import { convertAttributesToCredentialValues, assertCredentialValuesMatch, @@ -324,7 +323,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService let revocationRegistryDefinitionId let revocationRegistryIndex - let tailsFilePath + let revocationStatusList if (credentialDefinition.revocation) { const credentialMetadata = @@ -349,14 +348,19 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService ) } - const { revocationRegistryDefinition } = await agentContext.dependencyManager - .resolve(AnonCredsRevocationRegistryDefinitionRepository) - .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + const revocationStatusListResult = await registryService + .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) + .getRevocationStatusList(agentContext, revocationRegistryDefinitionId, dateToTimestamp(new Date())) - const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - tailsFilePath = await tailsFileService.getTailsFile(agentContext, { - revocationRegistryDefinition, - }) + if (!revocationStatusListResult.revocationStatusList) { + throw new AriesFrameworkError( + `Unable to resolve revocation status list for ${revocationRegistryDefinitionId}: + ${revocationStatusListResult.resolutionMetadata.error} ${revocationStatusListResult.resolutionMetadata.message}` + ) + } + + revocationStatusList = revocationStatusListResult.revocationStatusList } const { credential } = await anonCredsIssuerService.createCredential(agentContext, { @@ -365,7 +369,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService credentialValues: convertAttributesToCredentialValues(credentialAttributes), revocationRegistryDefinitionId, revocationRegistryIndex, - tailsFilePath, + revocationStatusList, }) const format = new CredentialFormatSpec({ diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index 0968ac5524..936ea91af1 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -60,7 +60,7 @@ export interface CreateCredentialOptions { credentialRequest: AnonCredsCredentialRequest credentialValues: AnonCredsCredentialValues revocationRegistryDefinitionId?: string - tailsFilePath?: string + revocationStatusList?: AnonCredsRevocationStatusList revocationRegistryIndex?: number } diff --git a/packages/anoncreds/src/utils/getRevocationRegistries.ts b/packages/anoncreds/src/utils/getRevocationRegistries.ts index 9ceb388eba..421fefe61a 100644 --- a/packages/anoncreds/src/utils/getRevocationRegistries.ts +++ b/packages/anoncreds/src/utils/getRevocationRegistries.ts @@ -102,42 +102,41 @@ export async function getRevocationRegistriesForRequest( revocationStatusLists: {}, } } - } - - revocationRegistryPromises.push(getRevocationRegistry()) - // In most cases we will have a timestamp, but if it's not defined, we use the nonRevoked.to value - const timestampToFetch = timestamp ?? nonRevoked.to + // In most cases we will have a timestamp, but if it's not defined, we use the nonRevoked.to value + const timestampToFetch = timestamp ?? nonRevoked.to - // Fetch revocation status list if we don't already have a revocation status list for the given timestamp - if (!revocationRegistries[revocationRegistryId].revocationStatusLists[timestampToFetch]) { - const { revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = - await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestampToFetch) + // Fetch revocation status list if we don't already have a revocation status list for the given timestamp + if (!revocationRegistries[revocationRegistryId].revocationStatusLists[timestampToFetch]) { + const { revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = + await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestampToFetch) - if (!revocationStatusList) { - throw new AriesFrameworkError( - `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` - ) - } + if (!revocationStatusList) { + throw new AriesFrameworkError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` + ) + } - revocationRegistries[revocationRegistryId].revocationStatusLists[revocationStatusList.timestamp] = - revocationStatusList - - // If we don't have a timestamp on the selected credential, we set it to the timestamp of the revocation status list - // this way we know which revocation status list to use when creating the proof. - if (!timestamp) { - updatedSelectedCredentials = { - ...updatedSelectedCredentials, - [type]: { - ...updatedSelectedCredentials[type], - [referent]: { - ...updatedSelectedCredentials[type][referent], - timestamp: revocationStatusList.timestamp, + revocationRegistries[revocationRegistryId].revocationStatusLists[revocationStatusList.timestamp] = + revocationStatusList + + // If we don't have a timestamp on the selected credential, we set it to the timestamp of the revocation status list + // this way we know which revocation status list to use when creating the proof. + if (!timestamp) { + updatedSelectedCredentials = { + ...updatedSelectedCredentials, + [type]: { + ...updatedSelectedCredentials[type], + [referent]: { + ...updatedSelectedCredentials[type][referent], + timestamp: revocationStatusList.timestamp, + }, }, - }, + } } } } + revocationRegistryPromises.push(getRevocationRegistry()) } } // await all revocation registry statuses asynchronously diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index f0bb37d4c9..da51aa12ec 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -335,7 +335,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } this.revocationStatusLists[revocationStatusList.revRegDefId][timestamp.toString()] = revocationStatusList - return { registrationMetadata: {}, revocationStatusListMetadata: {}, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index cd7d31edca..b303bed598 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -26,7 +26,6 @@ import { assertUnqualifiedCredentialRequest, assertUnqualifiedRevocationRegistryId, } from '../utils/assertUnqualified' -import { createTailsReader } from '../utils/tails' import { indySdkSchemaFromAnonCreds } from '../utils/transform' @injectable() @@ -131,8 +130,13 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialOptions ): Promise { - const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryDefinitionId } = - options + const { + revocationStatusList, + credentialOffer, + credentialRequest, + credentialValues, + revocationRegistryDefinitionId, + } = options assertIndySdkWallet(agentContext.wallet) assertUnqualifiedCredentialOffer(options.credentialOffer) @@ -142,10 +146,7 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { } try { - // Indy SDK requires tailsReaderHandle. Use null if no tailsFilePath is present - const tailsReaderHandle = tailsFilePath ? await createTailsReader(agentContext, tailsFilePath) : 0 - - if (revocationRegistryDefinitionId || tailsFilePath) { + if (revocationRegistryDefinitionId || revocationStatusList) { throw new AriesFrameworkError('Revocation not supported yet') } @@ -158,7 +159,7 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { { ...credentialRequest, prover_did: proverDid }, credentialValues, revocationRegistryDefinitionId ?? null, - tailsReaderHandle + 0 ) return { diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index 7f2d052726..978c820ea5 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -28,8 +28,8 @@ "@aries-framework/core": "0.4.2" }, "devDependencies": { - "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.3", - "@hyperledger/indy-vdr-shared": "^0.2.0-dev.3", + "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.4", + "@hyperledger/indy-vdr-shared": "^0.2.0-dev.4", "@stablelib/ed25519": "^1.0.2", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", @@ -38,6 +38,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/indy-vdr-shared": "^0.2.0-dev.3" + "@hyperledger/indy-vdr-shared": "^0.2.0-dev.4" } } diff --git a/yarn.lock b/yarn.lock index 4bde223af9..3ad9f59eee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1162,22 +1162,22 @@ dependencies: buffer "^6.0.3" -"@hyperledger/indy-vdr-nodejs@^0.2.0-dev.3": - version "0.2.0-dev.3" - resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.2.0-dev.3.tgz#c7262df6545606c893994e236c635c287cd64fd0" - integrity sha512-W4z8AtrNGb4hbbdisz6HAFqyAoX9c4oT3qo/8mizKyuYCSiSwlmAllkIFjCt93xyYmecTivkY2V2BcWpaUW47A== +"@hyperledger/indy-vdr-nodejs@^0.2.0-dev.4": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.2.0-dev.5.tgz#ea40095116e0abdd4c28214122f01669367f1db5" + integrity sha512-TeeapuZBRS7+Tbn8QQ3RoMpGaI/QHjeU7TlDU5UHNoEFuZcBdDcdH6V9QAoJ1RNxc6k7tiUYKFir8LMQ+hXrXQ== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/indy-vdr-shared" "0.2.0-dev.3" + "@hyperledger/indy-vdr-shared" "0.2.0-dev.5" "@mapbox/node-pre-gyp" "^1.0.10" ref-array-di "^1.2.2" ref-struct-di "^1.1.1" -"@hyperledger/indy-vdr-shared@0.2.0-dev.3", "@hyperledger/indy-vdr-shared@^0.2.0-dev.3": - version "0.2.0-dev.3" - resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-shared/-/indy-vdr-shared-0.2.0-dev.3.tgz#14994a034691ffe08e14e5f7d60fd63237616d04" - integrity sha512-5/zgSxp4zZUuFLabWdpB6ttfD0aLIquR9qab+HAJFUYDiWGHKP7bztiu07p4Dvhtgah+ZFOFgQo7WC492DXBoQ== +"@hyperledger/indy-vdr-shared@0.2.0-dev.5", "@hyperledger/indy-vdr-shared@^0.2.0-dev.4": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-shared/-/indy-vdr-shared-0.2.0-dev.5.tgz#ab33feda90dcbf457f3ff59da266ab32968ab48c" + integrity sha512-oPvNG5ePvtuz3H+KxWdCdxWXeo3Jxs8AFAAuG8qLPSlicEHpWchbT1amun8ccp1lk7pIBx9J0aLf08yrM5H8iw== "@isaacs/string-locale-compare@^1.1.0": version "1.1.0" From 61ac750d6624f697f9b7c91b4d57f71a2ff9422f Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 6 Nov 2023 20:10:12 -0300 Subject: [PATCH 27/27] fix: optional timestamp in updaterevstatuslist Signed-off-by: Ariel Gentile --- .../src/services/AnonCredsRsIssuerService.ts | 2 +- .../tests/v2-credential-revocation.e2e.test.ts | 3 +-- .../anoncreds-rs/tests/v2-credentials.e2e.test.ts | 3 +-- packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts | 15 ++++++++++----- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index f11c65b1b4..f4c666de56 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -202,7 +202,7 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value, issued: options.issued, revoked: options.revoked, - timestamp, + timestamp: timestamp ?? -1, // FIXME: this should be fixed in anoncreds-rs wrapper }) } diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts index b23d189201..f3db7e54cf 100644 --- a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -10,7 +10,6 @@ import { V2OfferCredentialMessage, } from '@aries-framework/core' -import { describeRunInNodeVersion } from '../../../tests/runInVersion' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { waitForCredentialRecordSubject } from '../../core/tests' import { waitForRevocationNotification } from '../../core/tests/helpers' @@ -25,7 +24,7 @@ const credentialPreview = V2CredentialPreview.fromRecord({ profile_picture: 'profile picture', }) -describeRunInNodeVersion([18], 'IC v2 credential revocation', () => { +describe('IC v2 credential revocation', () => { let faberAgent: AnonCredsTestsAgent let aliceAgent: AnonCredsTestsAgent let credentialDefinitionId: string diff --git a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts index 1a90a8397d..e198a16828 100644 --- a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts @@ -15,7 +15,6 @@ import { V2RequestCredentialMessage, } from '@aries-framework/core' -import { describeRunInNodeVersion } from '../../../tests/runInVersion' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../core/tests' import testLogger from '../../core/tests/logger' @@ -29,7 +28,7 @@ const credentialPreview = V2CredentialPreview.fromRecord({ profile_picture: 'profile picture', }) -describeRunInNodeVersion([18], 'IC V2 AnonCreds credentials', () => { +describe('IC V2 AnonCreds credentials', () => { let faberAgent: AnonCredsTestsAgent let aliceAgent: AnonCredsTestsAgent let credentialDefinitionId: string diff --git a/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts index cfbca7ad41..b304ef5e73 100644 --- a/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts @@ -14,7 +14,6 @@ import { V2PresentationMessage, } from '@aries-framework/core' -import { describeRunInNodeVersion } from '../../../tests/runInVersion' import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { sleep } from '../../core/src/utils/sleep' @@ -23,7 +22,7 @@ import testLogger from '../../core/tests/logger' import { issueAnonCredsCredential, setupAnonCredsTests } from './anoncredsSetup' -describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { +describe('PP V2 AnonCreds Proofs', () => { let faberAgent: AnonCredsTestsAgent let faberReplay: EventReplaySubject let aliceAgent: AnonCredsTestsAgent @@ -896,7 +895,7 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { }) }) - test('Credential is revoked before proof request', async () => { + test.only('Credential is revoked before proof request', async () => { // Revoke the credential const credentialRevocationRegistryDefinitionId = faberCredentialExchangeRecord.getTag( 'anonCredsRevocationRegistryId' @@ -980,7 +979,6 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { threadId: aliceProofExchangeRecord.threadId, - state: ProofState.PresentationReceived, }) await aliceAgent.proofs.acceptRequest({ @@ -996,7 +994,14 @@ describeRunInNodeVersion([18], 'PP V2 AnonCreds Proofs', () => { expect(faberProofExchangeRecord).toMatchObject({ threadId: aliceProofExchangeRecord.threadId, isVerified: false, - state: ProofState.PresentationReceived, + state: ProofState.Abandoned, + }) + + // Faber will send a problem report, meaning for Alice that the proof state is abandoned + // as well + await waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, }) }) })