From 25b981b6e23d02409e90dabdccdccc8904d4e357 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 19 May 2023 16:26:21 +0200 Subject: [PATCH] feat(indy-vdr): schema + credential definition endorsement (#1451) Signed-off-by: Martin Auer --- demo/src/Faber.ts | 35 +- packages/anoncreds/src/AnonCredsApi.ts | 192 ++-- packages/anoncreds/src/AnonCredsApiOptions.ts | 4 +- .../registry/CredentialDefinitionOptions.ts | 17 +- .../src/services/registry/SchemaOptions.ts | 21 +- .../anoncreds/src/services/registry/base.ts | 9 +- packages/indy-vdr/src/IndyVdrApi.ts | 21 +- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 331 +++++-- packages/indy-vdr/src/dids/index.ts | 2 +- packages/indy-vdr/src/index.ts | 8 +- packages/indy-vdr/src/utils/sign.ts | 38 + .../indy-vdr-anoncreds-registry.e2e.test.ts | 838 +++++++++++++++++- 12 files changed, 1268 insertions(+), 248 deletions(-) create mode 100644 packages/indy-vdr/src/utils/sign.ts diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index 0fa9c7e537..91befb8918 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -1,12 +1,16 @@ import type { RegisterCredentialDefinitionReturnStateFinished } from '@aries-framework/anoncreds' import type { ConnectionRecord, ConnectionStateChangedEvent } from '@aries-framework/core' +import type { + IndyVdrRegisterSchemaOptions, + IndyVdrRegisterCredentialDefinitionOptions, +} from '@aries-framework/indy-vdr' import type BottomBar from 'inquirer/lib/ui/bottom-bar' -import { KeyType, TypedArrayEncoder, utils, ConnectionEventTypes } from '@aries-framework/core' +import { ConnectionEventTypes, KeyType, TypedArrayEncoder, utils } from '@aries-framework/core' import { ui } from 'inquirer' import { BaseAgent, indyNetworkConfig } from './BaseAgent' -import { Color, greenText, Output, purpleText, redText } from './OutputClass' +import { Color, Output, greenText, purpleText, redText } from './OutputClass' export enum RegistryOptions { indy = 'did:indy', @@ -142,9 +146,12 @@ export class Faber extends BaseAgent { this.printSchema(schemaTemplate.name, schemaTemplate.version, schemaTemplate.attrNames) this.ui.updateBottomBar(greenText('\nRegistering schema...\n', false)) - const { schemaState } = await this.agent.modules.anoncreds.registerSchema({ + const { schemaState } = await this.agent.modules.anoncreds.registerSchema({ schema: schemaTemplate, - options: {}, + options: { + endorserMode: 'internal', + endorserDid: this.anonCredsIssuerId, + }, }) if (schemaState.state !== 'finished') { @@ -162,14 +169,18 @@ export class Faber extends BaseAgent { } this.ui.updateBottomBar('\nRegistering credential definition...\n') - const { credentialDefinitionState } = await this.agent.modules.anoncreds.registerCredentialDefinition({ - credentialDefinition: { - schemaId, - issuerId: this.anonCredsIssuerId, - tag: 'latest', - }, - options: {}, - }) + const { credentialDefinitionState } = + await this.agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition: { + schemaId, + issuerId: this.anonCredsIssuerId, + tag: 'latest', + }, + options: { + endorserMode: 'internal', + endorserDid: this.anonCredsIssuerId, + }, + }) if (credentialDefinitionState.state !== 'finished') { throw new Error( diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index ede2e691e3..a4beea9b20 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -2,16 +2,16 @@ import type { AnonCredsCreateLinkSecretOptions, AnonCredsRegisterCredentialDefinitionOptions, } from './AnonCredsApiOptions' +import type { AnonCredsCredentialDefinition, AnonCredsSchema } from './models' import type { + AnonCredsRegistry, GetCredentialDefinitionReturn, - GetRevocationStatusListReturn, + GetCredentialsOptions, GetRevocationRegistryDefinitionReturn, + GetRevocationStatusListReturn, GetSchemaReturn, RegisterCredentialDefinitionReturn, - RegisterSchemaOptions, RegisterSchemaReturn, - AnonCredsRegistry, - GetCredentialsOptions, } from './services' import type { Extensible } from './services/registry/base' import type { SimpleQuery } from '@aries-framework/core' @@ -34,10 +34,10 @@ import { AnonCredsSchemaRecord } from './repository/AnonCredsSchemaRecord' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' import { AnonCredsCredentialDefinitionRecordMetadataKeys } from './repository/anonCredsCredentialDefinitionRecordMetadataTypes' import { + AnonCredsHolderService, AnonCredsHolderServiceSymbol, - AnonCredsIssuerServiceSymbol, AnonCredsIssuerService, - AnonCredsHolderService, + AnonCredsIssuerServiceSymbol, } from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' @@ -146,7 +146,9 @@ export class AnonCredsApi { } } - public async registerSchema(options: RegisterSchemaOptions): Promise { + public async registerSchema( + options: AnonCredsRegisterSchema + ): Promise { const failedReturnBase = { schemaState: { state: 'failed' as const, @@ -165,7 +167,9 @@ export class AnonCredsApi { try { const result = await registry.registerSchema(this.agentContext, options) - await this.storeSchemaRecord(registry, result) + if (result.schemaState.state === 'finished') { + await this.storeSchemaRecord(registry, result) + } return result } catch (error) { @@ -186,7 +190,7 @@ export class AnonCredsApi { } /** - * Retrieve a {@link AnonCredsCredentialDefinition} from the registry associated + * Retrieve a {@link GetCredentialDefinitionReturn} from the registry associated * with the {@link credentialDefinitionId} */ public async getCredentialDefinition(credentialDefinitionId: string): Promise { @@ -215,11 +219,9 @@ export class AnonCredsApi { } } - public async registerCredentialDefinition(options: { - credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions - // TODO: options should support supportsRevocation at some points - options: Extensible - }): Promise { + public async registerCredentialDefinition( + options: AnonCredsRegisterCredentialDefinition + ): Promise { const failedReturnBase = { credentialDefinitionState: { state: 'failed' as const, @@ -235,22 +237,31 @@ export class AnonCredsApi { return failedReturnBase } - const schemaRegistry = this.findRegistryForIdentifier(options.credentialDefinition.schemaId) - if (!schemaRegistry) { - failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for schemaId ${options.credentialDefinition.schemaId}` - return failedReturnBase - } + let credentialDefinition: AnonCredsCredentialDefinition + let credentialDefinitionPrivate: Record | undefined = undefined + let keyCorrectnessProof: Record | undefined = undefined try { - const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId) + if (isFullCredentialDefinitionInput(options.credentialDefinition)) { + credentialDefinition = options.credentialDefinition + } else { + // If the input credential definition is not a full credential definition, we need to create one first + // There's a caveat to when the input contains a full credential, that the credential definition private + // and key correctness proof must already be stored in the wallet + const schemaRegistry = this.findRegistryForIdentifier(options.credentialDefinition.schemaId) + if (!schemaRegistry) { + failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for schemaId ${options.credentialDefinition.schemaId}` + return failedReturnBase + } - if (!schemaResult.schema) { - failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.credentialDefinition.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` - return failedReturnBase - } + const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId) - const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = - await this.anonCredsIssuerService.createCredentialDefinition( + if (!schemaResult.schema) { + failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.credentialDefinition.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` + return failedReturnBase + } + + const createCredentialDefinitionResult = await this.anonCredsIssuerService.createCredentialDefinition( this.agentContext, { issuerId: options.credentialDefinition.issuerId, @@ -265,12 +276,26 @@ export class AnonCredsApi { } ) + credentialDefinition = createCredentialDefinitionResult.credentialDefinition + credentialDefinitionPrivate = createCredentialDefinitionResult.credentialDefinitionPrivate + keyCorrectnessProof = createCredentialDefinitionResult.keyCorrectnessProof + } + const result = await registry.registerCredentialDefinition(this.agentContext, { credentialDefinition, options: options.options, }) - await this.storeCredentialDefinitionRecord(registry, result, credentialDefinitionPrivate, keyCorrectnessProof) + // Once a credential definition is created, the credential definition private and the key correctness proof must be stored because they change even if they the credential is recreated with the same arguments. + // To avoid having unregistered credential definitions in the wallet, the credential definitions itself are stored only when the credential definition status is finished, meaning that the credential definition has been successfully registered. + await this.storeCredentialDefinitionPrivateAndKeyCorrectnessRecord( + result, + credentialDefinitionPrivate, + keyCorrectnessProof + ) + if (result.credentialDefinitionState.state === 'finished') { + await this.storeCredentialDefinitionRecord(registry, result) + } return result } catch (error) { @@ -366,59 +391,72 @@ export class AnonCredsApi { return this.anonCredsHolderService.getCredentials(this.agentContext, options) } - private async storeCredentialDefinitionRecord( - registry: AnonCredsRegistry, + private async storeCredentialDefinitionPrivateAndKeyCorrectnessRecord( result: RegisterCredentialDefinitionReturn, credentialDefinitionPrivate?: Record, keyCorrectnessProof?: Record ): Promise { try { - // If we have both the credentialDefinition and the credentialDefinitionId we will store a copy of the credential definition. We may need to handle an - // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel - if ( - result.credentialDefinitionState.credentialDefinition && - result.credentialDefinitionState.credentialDefinitionId - ) { - const credentialDefinitionRecord = new AnonCredsCredentialDefinitionRecord({ + if (!result.credentialDefinitionState.credentialDefinitionId) return + + // Store Credential Definition private data (if provided by issuer service) + if (credentialDefinitionPrivate) { + const credentialDefinitionPrivateRecord = new AnonCredsCredentialDefinitionPrivateRecord({ credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, - credentialDefinition: result.credentialDefinitionState.credentialDefinition, - methodName: registry.methodName, + value: credentialDefinitionPrivate, }) - - // TODO: do we need to store this metadata? For indy, the registration metadata contains e.g. - // the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions - // are stored in the metadata - credentialDefinitionRecord.metadata.set( - AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata, - result.credentialDefinitionMetadata - ) - credentialDefinitionRecord.metadata.set( - AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata, - result.registrationMetadata + await this.anonCredsCredentialDefinitionPrivateRepository.save( + this.agentContext, + credentialDefinitionPrivateRecord ) + } - await this.anonCredsCredentialDefinitionRepository.save(this.agentContext, credentialDefinitionRecord) - - // Store Credential Definition private data (if provided by issuer service) - if (credentialDefinitionPrivate) { - const credentialDefinitionPrivateRecord = new AnonCredsCredentialDefinitionPrivateRecord({ - credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, - value: credentialDefinitionPrivate, - }) - await this.anonCredsCredentialDefinitionPrivateRepository.save( - this.agentContext, - credentialDefinitionPrivateRecord - ) - } + if (keyCorrectnessProof) { + const keyCorrectnessProofRecord = new AnonCredsKeyCorrectnessProofRecord({ + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + value: keyCorrectnessProof, + }) + await this.anonCredsKeyCorrectnessProofRepository.save(this.agentContext, keyCorrectnessProofRecord) + } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing credential definition key-correctness-proof and private`, { + cause: error, + }) + } + } - if (keyCorrectnessProof) { - const keyCorrectnessProofRecord = new AnonCredsKeyCorrectnessProofRecord({ - credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, - value: keyCorrectnessProof, - }) - await this.anonCredsKeyCorrectnessProofRepository.save(this.agentContext, keyCorrectnessProofRecord) - } + private async storeCredentialDefinitionRecord( + registry: AnonCredsRegistry, + result: RegisterCredentialDefinitionReturn + ): Promise { + try { + // If we have both the credentialDefinition and the credentialDefinitionId we will store a copy of the credential definition. We may need to handle an + // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel + if ( + !result.credentialDefinitionState.credentialDefinition || + !result.credentialDefinitionState.credentialDefinitionId + ) { + return } + const credentialDefinitionRecord = new AnonCredsCredentialDefinitionRecord({ + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + credentialDefinition: result.credentialDefinitionState.credentialDefinition, + methodName: registry.methodName, + }) + + // TODO: do we need to store this metadata? For indy, the registration metadata contains e.g. + // the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions + // are stored in the metadata + credentialDefinitionRecord.metadata.set( + AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata, + result.credentialDefinitionMetadata + ) + credentialDefinitionRecord.metadata.set( + AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata, + result.registrationMetadata + ) + + await this.anonCredsCredentialDefinitionRepository.save(this.agentContext, credentialDefinitionRecord) } catch (error) { throw new AnonCredsStoreRecordError(`Error storing credential definition records`, { cause: error }) } @@ -450,3 +488,19 @@ export class AnonCredsApi { } } } + +interface AnonCredsRegisterCredentialDefinition { + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions + options: T +} + +interface AnonCredsRegisterSchema { + schema: AnonCredsSchema + options: T +} + +function isFullCredentialDefinitionInput( + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions +): credentialDefinition is AnonCredsCredentialDefinition { + return 'value' in credentialDefinition +} diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index 860ea059df..f70e422ec4 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -5,4 +5,6 @@ export interface AnonCredsCreateLinkSecretOptions { setAsDefault?: boolean } -export type AnonCredsRegisterCredentialDefinitionOptions = Omit +export type AnonCredsRegisterCredentialDefinitionOptions = + | Omit + | AnonCredsCredentialDefinition diff --git a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts index 6e35c6ca76..be835da0d6 100644 --- a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts @@ -1,9 +1,10 @@ import type { - AnonCredsResolutionMetadata, - Extensible, + AnonCredsOperationStateAction, AnonCredsOperationStateFailed, AnonCredsOperationStateFinished, - AnonCredsOperationState, + AnonCredsOperationStateWait, + AnonCredsResolutionMetadata, + Extensible, } from './base' import type { AnonCredsCredentialDefinition } from '../../models/registry' @@ -29,15 +30,21 @@ export interface RegisterCredentialDefinitionReturnStateFinished extends AnonCre credentialDefinitionId: string } -export interface RegisterCredentialDefinitionReturnState extends AnonCredsOperationState { +export interface RegisterCredentialDefinitionReturnStateWait extends AnonCredsOperationStateWait { credentialDefinition?: AnonCredsCredentialDefinition credentialDefinitionId?: string } +export interface RegisterCredentialDefinitionReturnStateAction extends AnonCredsOperationStateAction { + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition +} + export interface RegisterCredentialDefinitionReturn { jobId?: string credentialDefinitionState: - | RegisterCredentialDefinitionReturnState + | RegisterCredentialDefinitionReturnStateWait + | RegisterCredentialDefinitionReturnStateAction | RegisterCredentialDefinitionReturnStateFinished | RegisterCredentialDefinitionReturnStateFailed credentialDefinitionMetadata: Extensible diff --git a/packages/anoncreds/src/services/registry/SchemaOptions.ts b/packages/anoncreds/src/services/registry/SchemaOptions.ts index 9ff42c9bc4..bd0541d676 100644 --- a/packages/anoncreds/src/services/registry/SchemaOptions.ts +++ b/packages/anoncreds/src/services/registry/SchemaOptions.ts @@ -1,9 +1,10 @@ import type { - AnonCredsResolutionMetadata, - Extensible, + AnonCredsOperationStateAction, AnonCredsOperationStateFailed, AnonCredsOperationStateFinished, - AnonCredsOperationState, + AnonCredsOperationStateWait, + AnonCredsResolutionMetadata, + Extensible, } from './base' import type { AnonCredsSchema } from '../../models/registry' @@ -17,7 +18,6 @@ export interface GetSchemaReturn { schemaMetadata: Extensible } -// export interface RegisterSchemaOptions { schema: AnonCredsSchema options: Extensible @@ -33,14 +33,23 @@ export interface RegisterSchemaReturnStateFinished extends AnonCredsOperationSta schemaId: string } -export interface RegisterSchemaReturnState extends AnonCredsOperationState { +export interface RegisterSchemaReturnStateAction extends AnonCredsOperationStateAction { + schema: AnonCredsSchema + schemaId: string +} + +export interface RegisterSchemaReturnStateWait extends AnonCredsOperationStateWait { schema?: AnonCredsSchema schemaId?: string } export interface RegisterSchemaReturn { jobId?: string - schemaState: RegisterSchemaReturnState | RegisterSchemaReturnStateFinished | RegisterSchemaReturnStateFailed + schemaState: + | RegisterSchemaReturnStateWait + | RegisterSchemaReturnStateAction + | RegisterSchemaReturnStateFinished + | RegisterSchemaReturnStateFailed schemaMetadata: Extensible registrationMetadata: Extensible } diff --git a/packages/anoncreds/src/services/registry/base.ts b/packages/anoncreds/src/services/registry/base.ts index af9b52ee43..48ae451ea0 100644 --- a/packages/anoncreds/src/services/registry/base.ts +++ b/packages/anoncreds/src/services/registry/base.ts @@ -1,7 +1,12 @@ export type Extensible = Record -export interface AnonCredsOperationState { - state: 'action' | 'wait' +export interface AnonCredsOperationStateWait { + state: 'wait' +} + +export interface AnonCredsOperationStateAction { + state: 'action' + action: string } export interface AnonCredsOperationStateFinished { diff --git a/packages/indy-vdr/src/IndyVdrApi.ts b/packages/indy-vdr/src/IndyVdrApi.ts index 4cd0ff3154..6601729c79 100644 --- a/packages/indy-vdr/src/IndyVdrApi.ts +++ b/packages/indy-vdr/src/IndyVdrApi.ts @@ -2,12 +2,13 @@ import type { Key } from '@aries-framework/core' import type { IndyVdrRequest } from '@hyperledger/indy-vdr-shared' import { parseIndyDid } from '@aries-framework/anoncreds' -import { AgentContext, injectable, TypedArrayEncoder } from '@aries-framework/core' +import { AgentContext, injectable } from '@aries-framework/core' import { CustomRequest } from '@hyperledger/indy-vdr-shared' import { verificationKeyForIndyDid } from './dids/didIndyUtil' import { IndyVdrError } from './error' import { IndyVdrPoolService } from './pool' +import { multiSignRequest, signRequest } from './utils/sign' @injectable() export class IndyVdrApi { @@ -24,26 +25,12 @@ export class IndyVdrApi { signingKey: Key, identifier: string ) { - const signature = await this.agentContext.wallet.sign({ - data: TypedArrayEncoder.fromString(request.signatureInput), - key: signingKey, - }) - - request.setMultiSignature({ - signature, - identifier, - }) - - return request + return multiSignRequest(this.agentContext, request, signingKey, identifier) } private async signRequest(request: Request, submitterDid: string) { - const signingKey = await verificationKeyForIndyDid(this.agentContext, submitterDid) - const { pool } = await this.indyVdrPoolService.getPoolForDid(this.agentContext, submitterDid) - const signedRequest = await pool.prepareWriteRequest(this.agentContext, request, signingKey) - - return signedRequest + return signRequest(this.agentContext, pool, request, submitterDid) } /** diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index 412d7f91c0..fbfaa49a0a 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -2,15 +2,24 @@ import type { AnonCredsRegistry, GetCredentialDefinitionReturn, GetSchemaReturn, - RegisterSchemaOptions, - RegisterCredentialDefinitionOptions, RegisterSchemaReturn, RegisterCredentialDefinitionReturn, GetRevocationStatusListReturn, GetRevocationRegistryDefinitionReturn, AnonCredsRevocationRegistryDefinition, + AnonCredsSchema, + AnonCredsCredentialDefinition, + RegisterSchemaReturnStateFailed, + RegisterSchemaReturnStateFinished, + RegisterSchemaReturnStateAction, + RegisterSchemaReturnStateWait, + RegisterCredentialDefinitionReturnStateAction, + RegisterCredentialDefinitionReturnStateWait, + RegisterCredentialDefinitionReturnStateFinished, + RegisterCredentialDefinitionReturnStateFailed, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' +import type { SchemaResponse } from '@hyperledger/indy-vdr-shared' import { getUnqualifiedCredentialDefinitionId, @@ -29,10 +38,12 @@ import { GetTransactionRequest, GetRevocationRegistryDeltaRequest, GetRevocationRegistryDefinitionRequest, + CustomRequest, } from '@hyperledger/indy-vdr-shared' import { verificationKeyForIndyDid } from '../dids/didIndyUtil' import { IndyVdrPoolService } from '../pool' +import { multiSignRequest } from '../utils/sign' import { indyVdrAnonCredsRegistryIdentifierRegex, @@ -115,72 +126,93 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { public async registerSchema( agentContext: AgentContext, - options: RegisterSchemaOptions - ): Promise { + options: IndyVdrRegisterSchema + ): Promise { + const schema = options.schema + const { issuerId, name, version, attrNames } = schema try { // This will throw an error if trying to register a schema 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.schema.issuerId) + const { namespaceIdentifier, namespace } = parseIndyDid(issuerId) + const { endorserDid, endorserMode } = options.options const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) const pool = indyVdrPoolService.getPoolForNamespace(namespace) - agentContext.config.logger.debug( - `Register schema on ledger '${pool.indyNamespace}' with did '${options.schema.issuerId}'`, - options.schema - ) - const didIndySchemaId = getDidIndySchemaId( - namespace, - namespaceIdentifier, - options.schema.name, - options.schema.version - ) - const legacySchemaId = getUnqualifiedSchemaId(namespaceIdentifier, options.schema.name, options.schema.version) + let writeRequest: CustomRequest + const didIndySchemaId = getDidIndySchemaId(namespace, namespaceIdentifier, schema.name, schema.version) - const schemaRequest = new SchemaRequest({ - submitterDid: namespaceIdentifier, - schema: { - id: legacySchemaId, - name: options.schema.name, - ver: '1.0', - version: options.schema.version, - attrNames: options.schema.attrNames, - }, - }) + const endorsedTransaction = options.options.endorsedTransaction + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${issuerId}'`, + schema + ) + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + } else { + agentContext.config.logger.debug(`Create schema tx on ledger '${namespace}' with did '${issuerId}'`, schema) + const legacySchemaId = getUnqualifiedSchemaId(namespaceIdentifier, name, version) + + const schemaRequest = new SchemaRequest({ + submitterDid: namespaceIdentifier, + schema: { id: legacySchemaId, name, ver: '1.0', version, attrNames }, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + schemaRequest, + submitterKey, + endorserDid !== issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndySchemaId, + schemaState: { + state: 'action', + action: 'endorseIndyTransaction', + schemaId: didIndySchemaId, + schema: schema, + schemaRequest: writeRequest.body, + }, + registrationMetadata: {}, + schemaMetadata: {}, + } + } - const submitterKey = await verificationKeyForIndyDid(agentContext, options.schema.issuerId) - const writeRequest = await pool.prepareWriteRequest(agentContext, schemaRequest, submitterKey) + if (endorserMode === 'internal' && endorserDid !== issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } const response = await pool.submitRequest(writeRequest) agentContext.config.logger.debug(`Registered schema '${didIndySchemaId}' on ledger '${pool.indyNamespace}'`, { response, - schemaRequest, + writeRequest, }) return { schemaState: { state: 'finished', - schema: { - attrNames: options.schema.attrNames, - issuerId: options.schema.issuerId, - name: options.schema.name, - version: options.schema.version, - }, + schema: schema, schemaId: didIndySchemaId, }, registrationMetadata: {}, schemaMetadata: { // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. // For this reason we return it in the metadata. - indyLedgerSeqNo: response.result.txnMetadata.seqNo, + // Cast to SchemaResponse to pass type check + indyLedgerSeqNo: (response as SchemaResponse)?.result?.txnMetadata?.seqNo, }, } } catch (error) { - agentContext.config.logger.error(`Error registering schema for did '${options.schema.issuerId}'`, { + agentContext.config.logger.error(`Error registering schema for did '${issuerId}'`, { error, - did: options.schema.issuerId, - schema: options.schema, + did: issuerId, + schema: schema, }) return { @@ -188,7 +220,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { registrationMetadata: {}, schemaState: { state: 'failed', - schema: options.schema, + schema: schema, reason: `unknownError: ${error.message}`, }, } @@ -274,67 +306,102 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { public async registerCredentialDefinition( agentContext: AgentContext, - options: RegisterCredentialDefinitionOptions - ): Promise { + options: IndyVdrRegisterCredentialDefinition + ): Promise { + const credentialDefinition = options.credentialDefinition + const { schemaId, issuerId, tag, value } = credentialDefinition + try { // This will throw an error if trying to register a credential defintion 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) - + const { namespaceIdentifier, namespace } = parseIndyDid(issuerId) + const { endorserDid, endorserMode } = options.options const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) - const pool = indyVdrPoolService.getPoolForNamespace(namespace) + agentContext.config.logger.debug( - `Registering credential definition on ledger '${pool.indyNamespace}' with did '${options.credentialDefinition.issuerId}'`, + `Registering credential definition on ledger '${namespace}' with did '${issuerId}'`, options.credentialDefinition ) - // TODO: this will bypass caching if done on a higher level. - const { schema, schemaMetadata, resolutionMetadata } = await this.getSchema( - agentContext, - options.credentialDefinition.schemaId - ) + let writeRequest: CustomRequest + let didIndyCredentialDefinitionId: string + let seqNo: number - if (!schema || !schemaMetadata.indyLedgerSeqNo || typeof schemaMetadata.indyLedgerSeqNo !== 'number') { - return { - registrationMetadata: {}, - credentialDefinitionMetadata: { - didIndyNamespace: pool.indyNamespace, - }, - credentialDefinitionState: { - credentialDefinition: options.credentialDefinition, - state: 'failed', - reason: `error resolving schema with id ${options.credentialDefinition.schemaId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, - }, + const endorsedTransaction = options.options.endorsedTransaction + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${issuerId}'`, + credentialDefinition + ) + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + const operation = JSON.parse(endorsedTransaction)?.operation + // extract the seqNo from the endorsed transaction, which is contained in the ref field of the operation + seqNo = Number(operation?.ref) + didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId(namespace, namespaceIdentifier, seqNo, tag) + } else { + // TODO: this will bypass caching if done on a higher level. + const { schemaMetadata, resolutionMetadata } = await this.getSchema(agentContext, schemaId) + + if (!schemaMetadata?.indyLedgerSeqNo || typeof schemaMetadata.indyLedgerSeqNo !== 'number') { + return { + registrationMetadata: {}, + credentialDefinitionMetadata: { + didIndyNamespace: pool.indyNamespace, + }, + credentialDefinitionState: { + credentialDefinition: options.credentialDefinition, + state: 'failed', + reason: `error resolving schema with id ${schemaId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, + }, + } } - } + seqNo = schemaMetadata.indyLedgerSeqNo + + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(issuerId, seqNo, tag) + didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId(namespace, namespaceIdentifier, seqNo, tag) + + const credentialDefinitionRequest = new CredentialDefinitionRequest({ + submitterDid: namespaceIdentifier, + credentialDefinition: { + ver: '1.0', + id: legacyCredentialDefinitionId, + schemaId: `${seqNo}`, + type: 'CL', + tag: tag, + value: value, + }, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + credentialDefinitionRequest, + submitterKey, + endorserDid !== issuerId ? endorserDid : undefined + ) - const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( - options.credentialDefinition.issuerId, - schemaMetadata.indyLedgerSeqNo, - options.credentialDefinition.tag - ) - const didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( - namespace, - namespaceIdentifier, - schemaMetadata.indyLedgerSeqNo, - options.credentialDefinition.tag - ) + if (endorserMode === 'external') { + return { + jobId: didIndyCredentialDefinitionId, + credentialDefinitionState: { + state: 'action', + action: 'endorseIndyTransaction', + credentialDefinition: credentialDefinition, + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinitionRequest: writeRequest.body, + }, + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + } + } - const credentialDefinitionRequest = new CredentialDefinitionRequest({ - submitterDid: namespaceIdentifier, - credentialDefinition: { - ver: '1.0', - id: legacyCredentialDefinitionId, - schemaId: `${schemaMetadata.indyLedgerSeqNo}`, - type: 'CL', - tag: options.credentialDefinition.tag, - value: options.credentialDefinition.value, - }, - }) + if (endorserMode === 'internal' && endorserDid !== issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } - const submitterKey = await verificationKeyForIndyDid(agentContext, options.credentialDefinition.issuerId) - const writeRequest = await pool.prepareWriteRequest(agentContext, credentialDefinitionRequest, submitterKey) const response = await pool.submitRequest(writeRequest) agentContext.config.logger.debug( `Registered credential definition '${didIndyCredentialDefinitionId}' on ledger '${pool.indyNamespace}'`, @@ -347,21 +414,18 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { return { credentialDefinitionMetadata: {}, credentialDefinitionState: { - credentialDefinition: options.credentialDefinition, + credentialDefinition: credentialDefinition, credentialDefinitionId: didIndyCredentialDefinitionId, state: 'finished', }, registrationMetadata: {}, } } catch (error) { - agentContext.config.logger.error( - `Error registering credential definition for schema '${options.credentialDefinition.schemaId}'`, - { - error, - did: options.credentialDefinition.issuerId, - credentialDefinition: options.credentialDefinition, - } - ) + agentContext.config.logger.error(`Error registering credential definition for schema '${schemaId}'`, { + error, + did: issuerId, + credentialDefinition: options.credentialDefinition, + }) return { credentialDefinitionMetadata: {}, @@ -631,3 +695,78 @@ interface SchemaType { name: string } } + +type InternalEndorsement = { endorserMode: 'internal'; endorserDid: string; endorsedTransaction?: never } +type ExternalEndorsementCreate = { endorserMode: 'external'; endorserDid: string; endorsedTransaction?: never } +type ExternalEndorsementSubmit = { endorserMode: 'external'; endorserDid?: never; endorsedTransaction: string } + +export interface IndyVdrRegisterSchemaInternalOptions { + schema: AnonCredsSchema + options: InternalEndorsement +} + +export interface IndyVdrRegisterSchemaExternalCreateOptions { + schema: AnonCredsSchema + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterSchemaExternalSubmitOptions { + schema: AnonCredsSchema + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterSchemaReturnStateAction extends RegisterSchemaReturnStateAction { + action: 'endorseIndyTransaction' + schemaRequest: string +} + +export interface IndyVdrRegisterSchemaReturn extends RegisterSchemaReturn { + schemaState: + | RegisterSchemaReturnStateWait + | IndyVdrRegisterSchemaReturnStateAction + | RegisterSchemaReturnStateFinished + | RegisterSchemaReturnStateFailed +} + +export type IndyVdrRegisterSchema = + | IndyVdrRegisterSchemaInternalOptions + | IndyVdrRegisterSchemaExternalCreateOptions + | IndyVdrRegisterSchemaExternalSubmitOptions + +export type IndyVdrRegisterSchemaOptions = IndyVdrRegisterSchema['options'] + +export interface IndyVdrRegisterCredentialDefinitionInternalOptions { + credentialDefinition: AnonCredsCredentialDefinition + options: InternalEndorsement +} + +export interface IndyVdrRegisterCredentialDefinitionExternalCreateOptions { + credentialDefinition: AnonCredsCredentialDefinition + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterCredentialDefinitionExternalSubmitOptions { + credentialDefinition: AnonCredsCredentialDefinition + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterCredentialDefinitionReturnStateAction + extends RegisterCredentialDefinitionReturnStateAction { + action: 'endorseIndyTransaction' + credentialDefinitionRequest: string +} + +export interface IndyVdrRegisterCredentialDefinitionReturn extends RegisterCredentialDefinitionReturn { + credentialDefinitionState: + | RegisterCredentialDefinitionReturnStateWait + | IndyVdrRegisterCredentialDefinitionReturnStateAction + | RegisterCredentialDefinitionReturnStateFinished + | RegisterCredentialDefinitionReturnStateFailed +} + +export type IndyVdrRegisterCredentialDefinition = + | IndyVdrRegisterCredentialDefinitionInternalOptions + | IndyVdrRegisterCredentialDefinitionExternalCreateOptions + | IndyVdrRegisterCredentialDefinitionExternalSubmitOptions + +export type IndyVdrRegisterCredentialDefinitionOptions = IndyVdrRegisterCredentialDefinition['options'] diff --git a/packages/indy-vdr/src/dids/index.ts b/packages/indy-vdr/src/dids/index.ts index 24a7a4ae9f..09983bad74 100644 --- a/packages/indy-vdr/src/dids/index.ts +++ b/packages/indy-vdr/src/dids/index.ts @@ -1,3 +1,3 @@ -export { IndyVdrIndyDidRegistrar, IndyVdrDidCreateResult } from './IndyVdrIndyDidRegistrar' +export { IndyVdrIndyDidRegistrar, IndyVdrDidCreateResult, IndyVdrDidCreateOptions } from './IndyVdrIndyDidRegistrar' export { IndyVdrIndyDidResolver } from './IndyVdrIndyDidResolver' export { IndyVdrSovDidResolver } from './IndyVdrSovDidResolver' diff --git a/packages/indy-vdr/src/index.ts b/packages/indy-vdr/src/index.ts index 19fdf8dad2..aaa233c56e 100644 --- a/packages/indy-vdr/src/index.ts +++ b/packages/indy-vdr/src/index.ts @@ -1,4 +1,10 @@ -export { IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrSovDidResolver, IndyVdrDidCreateResult } from './dids' +export { + IndyVdrIndyDidRegistrar, + IndyVdrIndyDidResolver, + IndyVdrSovDidResolver, + IndyVdrDidCreateResult, + IndyVdrDidCreateOptions, +} from './dids' export { IndyVdrPoolConfig } from './pool' export * from './IndyVdrModule' export * from './IndyVdrModuleConfig' diff --git a/packages/indy-vdr/src/utils/sign.ts b/packages/indy-vdr/src/utils/sign.ts new file mode 100644 index 0000000000..3a7d031175 --- /dev/null +++ b/packages/indy-vdr/src/utils/sign.ts @@ -0,0 +1,38 @@ +import type { IndyVdrPool } from '../pool' +import type { AgentContext, Key } from '@aries-framework/core' +import type { IndyVdrRequest } from '@hyperledger/indy-vdr-shared' + +import { TypedArrayEncoder } from '@aries-framework/core' + +import { verificationKeyForIndyDid } from '../dids/didIndyUtil' + +export async function multiSignRequest( + agentContext: AgentContext, + request: Request, + signingKey: Key, + identifier: string +) { + const signature = await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(request.signatureInput), + key: signingKey, + }) + + request.setMultiSignature({ + signature, + identifier, + }) + + return request +} + +export async function signRequest( + agentContext: AgentContext, + pool: IndyVdrPool, + request: Request, + submitterDid: string +) { + const signingKey = await verificationKeyForIndyDid(agentContext, submitterDid) + const signedRequest = await pool.prepareWriteRequest(agentContext, request, signingKey) + + return signedRequest +} diff --git a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts index f71251b948..c98a807b91 100644 --- a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts @@ -1,26 +1,50 @@ -import { Agent, DidsModule, Key, KeyType, TypedArrayEncoder } from '@aries-framework/core' -import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { RevocationRegistryDefinitionRequest, RevocationRegistryEntryRequest } from '@hyperledger/indy-vdr-shared' +import type { IndyVdrDidCreateOptions, IndyVdrDidCreateResult } from '../src/dids/IndyVdrIndyDidRegistrar' +import type { RevocationRegistryEntryResponse } from '@hyperledger/indy-vdr-shared' +import { parseIndyDid } from '@aries-framework/anoncreds' +import { Agent, DidsModule, TypedArrayEncoder } from '@aries-framework/core' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' import { - agentDependencies, - getAgentConfig, - importExistingIndyDidFromPrivateKey, - publicDidSeed, -} from '../../core/tests/helpers' + CustomRequest, + RevocationRegistryDefinitionRequest, + RevocationRegistryEntryRequest, +} from '@hyperledger/indy-vdr-shared' + +import { agentDependencies, getAgentConfig, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndySdkModule } from '../../indy-sdk/src' import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { IndyVdrIndyDidResolver, IndyVdrModule, IndyVdrSovDidResolver } from '../src' import { IndyVdrAnonCredsRegistry } from '../src/anoncreds/IndyVdrAnonCredsRegistry' +import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' +import { verificationKeyForIndyDid } from '../src/dids/didIndyUtil' import { IndyVdrPoolService } from '../src/pool' import { credentialDefinitionValue } from './__fixtures__/anoncreds' import { indyVdrModuleConfig } from './helpers' -const agentConfig = getAgentConfig('IndyVdrAnonCredsRegistry') +const endorserConfig = getAgentConfig('IndyVdrAnonCredsRegistryEndorser') +const agentConfig = getAgentConfig('IndyVdrAnonCredsRegistryAgent') const indyVdrAnonCredsRegistry = new IndyVdrAnonCredsRegistry() +const endorser = new Agent({ + config: endorserConfig, + dependencies: agentDependencies, + modules: { + indyVdr: new IndyVdrModule({ + indyVdr, + networks: indyVdrModuleConfig.networks, + }), + indySdk: new IndySdkModule({ + indySdk, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], + }), + }, +}) + const agent = new Agent({ config: agentConfig, dependencies: agentDependencies, @@ -33,19 +57,26 @@ const agent = new Agent({ indySdk, }), dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], }), }, }) -const indyVdrPoolService = agent.dependencyManager.resolve(IndyVdrPoolService) +const indyVdrPoolService = endorser.dependencyManager.resolve(IndyVdrPoolService) const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') describe('IndyVdrAnonCredsRegistry', () => { + let endorserDid: string beforeAll(async () => { - await agent.initialize() + await endorser.initialize() + const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( + endorser, + TypedArrayEncoder.fromString('00000000000000000000000Endorser9') + ) + endorserDid = `did:indy:pool:localtest:${unqualifiedSubmitterDid}` - await importExistingIndyDidFromPrivateKey(agent, TypedArrayEncoder.fromString(publicDidSeed)) + await agent.initialize() }) afterAll(async () => { @@ -53,23 +84,35 @@ describe('IndyVdrAnonCredsRegistry', () => { pool.close() } + await endorser.shutdown() + await endorser.wallet.delete() await agent.shutdown() await agent.wallet.delete() }) - // One test as the credential definition depends on the schema - test('register and resolve a schema and credential definition', async () => { - const dynamicVersion = `1.${Math.random() * 100}` + test('register and resolve a schema and credential definition (internal, issuerDid != endorserDid)', async () => { + const didCreateResult = (await endorser.dids.create({ + method: 'indy', + options: { + endorserMode: 'internal', + endorserDid, + }, + })) as IndyVdrDidCreateResult - const legacyIssuerId = 'TL1EaPFCZ8Si5aUrqScBDt' - const signingKey = Key.fromPublicKeyBase58('FMGcFuU3QwAQLywxvmEnSorQT3NwU9wgDMMTaDFtvswm', KeyType.Ed25519) - const didIndyIssuerId = 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt' + if (didCreateResult.didState.state !== 'finished') throw Error('did was not successfully created') - const legacySchemaId = `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}` - const didIndySchemaId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + const didIndyIssuerId = didCreateResult.didState.did + const { namespaceIdentifier: legacyIssuerId } = parseIndyDid(didIndyIssuerId) + const dynamicVersion = `1.${Math.random() * 100}` + const signingKey = await verificationKeyForIndyDid(endorser.context, didIndyIssuerId) + const legacySchemaId = `${legacyIssuerId}:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` - const schemaResult = await indyVdrAnonCredsRegistry.registerSchema(agent.context, { - options: {}, + const schemaResult = await indyVdrAnonCredsRegistry.registerSchema(endorser.context, { + options: { + endorserMode: 'internal', + endorserDid, + }, schema: { attrNames: ['age'], issuerId: didIndyIssuerId, @@ -98,7 +141,7 @@ describe('IndyVdrAnonCredsRegistry', () => { // Wait some time before resolving credential definition object await new Promise((res) => setTimeout(res, 1000)) - const legacySchema = await indyVdrAnonCredsRegistry.getSchema(agent.context, legacySchemaId) + const legacySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, legacySchemaId) expect(legacySchema).toMatchObject({ schema: { attrNames: ['age'], @@ -115,7 +158,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }) // Resolve using did indy schema id - const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(agent.context, didIndySchemaId) + const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, didIndySchemaId) expect(didIndySchema).toMatchObject({ schema: { attrNames: ['age'], @@ -131,9 +174,9 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) - const legacyCredentialDefinitionId = `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` - const didIndyCredentialDefinitionId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` - const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + const legacyCredentialDefinitionId = `${legacyIssuerId}:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` + const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(endorser.context, { credentialDefinition: { issuerId: didIndyIssuerId, tag: 'TAG', @@ -141,7 +184,10 @@ describe('IndyVdrAnonCredsRegistry', () => { type: 'CL', value: credentialDefinitionValue, }, - options: {}, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, }) expect(credentialDefinitionResult).toMatchObject({ @@ -164,7 +210,7 @@ describe('IndyVdrAnonCredsRegistry', () => { await new Promise((res) => setTimeout(res, 1000)) const legacyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( - agent.context, + endorser.context, legacyCredentialDefinitionId ) @@ -185,7 +231,7 @@ describe('IndyVdrAnonCredsRegistry', () => { // resolve using did indy credential definition id const didIndyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( - agent.context, + endorser.context, didIndyCredentialDefinitionId ) @@ -205,10 +251,10 @@ describe('IndyVdrAnonCredsRegistry', () => { }) // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry - const legacyRevocationRegistryId = `TL1EaPFCZ8Si5aUrqScBDt:4:TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` - const didIndyRevocationRegistryId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` + const legacyRevocationRegistryId = `${legacyIssuerId}:4:${legacyIssuerId}:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` + const didIndyRevocationRegistryId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ - submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', + submitterDid: legacyIssuerId, revocationRegistryDefinitionV1: { credDefId: legacyCredentialDefinitionId, id: legacyRevocationRegistryId, @@ -231,8 +277,14 @@ describe('IndyVdrAnonCredsRegistry', () => { }) // After this call, the revocation registry should now be resolvable - const writeRequest = await pool.prepareWriteRequest(agent.context, revocationRegistryRequest, signingKey) - await pool.submitRequest(writeRequest) + const writeRequest = await pool.prepareWriteRequest( + endorser.context, + revocationRegistryRequest, + signingKey, + endorserDid + ) + const endorsedRequest = await endorser.modules.indyVdr.endorseTransaction(writeRequest.body, endorserDid) + await pool.submitRequest(new CustomRequest({ customRequest: endorsedRequest })) // Also create a revocation registry entry const revocationEntryRequest = new RevocationRegistryEntryRequest({ @@ -250,11 +302,721 @@ describe('IndyVdrAnonCredsRegistry', () => { // After this call we can query the revocation registry entries (using timestamp now) const revocationEntryWriteRequest = await pool.prepareWriteRequest( - agent.context, + endorser.context, revocationEntryRequest, - signingKey + signingKey, + endorserDid ) - const entryResponse = await pool.submitRequest(revocationEntryWriteRequest) + const endorsedRevocationEntryWriteRequest = await endorser.modules.indyVdr.endorseTransaction( + revocationEntryWriteRequest.body, + endorserDid + ) + const entryResponse = (await pool.submitRequest( + new CustomRequest({ customRequest: endorsedRevocationEntryWriteRequest }) + )) as RevocationRegistryEntryResponse + + const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + endorser.context, + legacyRevocationRegistryId + ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinition: { + issuerId: legacyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'tag', + credDefId: legacyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + endorser.context, + didIndyRevocationRegistryId + ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinition: { + issuerId: didIndyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'tag', + credDefId: didIndyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + endorser.context, + legacyRevocationRegistryId, + entryResponse.result.txnMetadata.txnTime + ) + + expect(legacyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: legacyIssuerId, + currentAccumulator: '1', + revRegDefId: legacyRevocationRegistryId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: entryResponse.result.txnMetadata.txnTime, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + + const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + endorser.context, + didIndyRevocationRegistryId, + entryResponse.result.txnMetadata.txnTime + ) + + expect(didIndyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: didIndyIssuerId, + currentAccumulator: '1', + revRegDefId: didIndyRevocationRegistryId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: entryResponse.result.txnMetadata.txnTime, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + }) + + test('register and resolve a schema and credential definition (internal, issuerDid == endorserDid)', async () => { + const dynamicVersion = `1.${Math.random() * 100}` + + const legacyIssuerId = 'DJKobikPAaYWAu9vfhEEo5' + const didIndyIssuerId = 'did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5' + const signingKey = await verificationKeyForIndyDid(agent.context, didIndyIssuerId) + + const legacySchemaId = `DJKobikPAaYWAu9vfhEEo5:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + + const schemaResult = await indyVdrAnonCredsRegistry.registerSchema(endorser.context, { + options: { + endorserMode: 'internal', + endorserDid, + }, + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + }) + + expect(schemaResult).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + schemaId: didIndySchemaId, + }, + registrationMetadata: {}, + schemaMetadata: { + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, legacySchemaId) + expect(legacySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: legacyIssuerId, + }, + schemaId: legacySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Resolve using did indy schema id + const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, didIndySchemaId) + expect(didIndySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: didIndyIssuerId, + }, + schemaId: didIndySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + const legacyCredentialDefinitionId = `DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` + const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(endorser.context, { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + expect(credentialDefinitionResult).toMatchObject({ + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionId: didIndyCredentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + endorser.context, + legacyCredentialDefinitionId + ) + + expect(legacyCredentialDefinition).toMatchObject({ + credentialDefinitionId: legacyCredentialDefinitionId, + credentialDefinition: { + issuerId: legacyIssuerId, + schemaId: legacySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // resolve using did indy credential definition id + const didIndyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + endorser.context, + didIndyCredentialDefinitionId + ) + + expect(didIndyCredentialDefinition).toMatchObject({ + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinition: { + issuerId: didIndyIssuerId, + schemaId: didIndySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry + const legacyRevocationRegistryId = `DJKobikPAaYWAu9vfhEEo5:4:DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` + const didIndyRevocationRegistryId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` + const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ + submitterDid: 'DJKobikPAaYWAu9vfhEEo5', + revocationRegistryDefinitionV1: { + credDefId: legacyCredentialDefinitionId, + id: legacyRevocationRegistryId, + revocDefType: 'CL_ACCUM', + tag: 'tag', + value: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + maxCredNum: 100, + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + }, + ver: '1.0', + }, + }) + + // After this call, the revocation registry should now be resolvable + const writeRequest = await pool.prepareWriteRequest(endorser.context, revocationRegistryRequest, signingKey) + await pool.submitRequest(writeRequest) + + // Also create a revocation registry entry + const revocationEntryRequest = new RevocationRegistryEntryRequest({ + revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinitionType: 'CL_ACCUM', + revocationRegistryEntry: { + ver: '1.0', + value: { + accum: '1', + }, + }, + submitterDid: legacyIssuerId, + }) + + // After this call we can query the revocation registry entries (using timestamp now) + + const revocationEntryWriteRequest = await pool.prepareWriteRequest( + endorser.context, + revocationEntryRequest, + signingKey + ) + const entryResponse = await pool.submitRequest(revocationEntryWriteRequest) + + const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + endorser.context, + legacyRevocationRegistryId + ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinition: { + issuerId: legacyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'tag', + credDefId: legacyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( + endorser.context, + didIndyRevocationRegistryId + ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinition: { + issuerId: didIndyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'tag', + credDefId: didIndyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + endorser.context, + legacyRevocationRegistryId, + entryResponse.result.txnMetadata.txnTime + ) + + expect(legacyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: legacyIssuerId, + currentAccumulator: '1', + revRegDefId: legacyRevocationRegistryId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: entryResponse.result.txnMetadata.txnTime, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + + const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( + endorser.context, + didIndyRevocationRegistryId, + entryResponse.result.txnMetadata.txnTime + ) + + expect(didIndyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: didIndyIssuerId, + currentAccumulator: '1', + revRegDefId: didIndyRevocationRegistryId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: entryResponse.result.txnMetadata.txnTime, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + }) + + test('register and resolve a schema and credential definition (external)', async () => { + const didCreateTxResult = (await agent.dids.create({ + method: 'indy', + options: { + endorserMode: 'external', + endorserDid, + }, + })) as IndyVdrDidCreateResult + + const didState = didCreateTxResult.didState + if (didState.state !== 'action' || didState.action !== 'endorseIndyTransaction') throw Error('unexpected did state') + + const signedNymRequest = await endorser.modules.indyVdr.endorseTransaction( + didState.nymRequest, + didState.endorserDid + ) + const didCreateSubmitResult = await agent.dids.create({ + did: didState.did, + options: { + endorserMode: 'external', + endorsedTransaction: { + nymRequest: signedNymRequest, + }, + }, + secret: didState.secret, + }) + + if (!didCreateSubmitResult.didState.did) throw Error('did was not correctly created') + + const agentDid = didCreateSubmitResult.didState.did + const { namespaceIdentifier } = parseIndyDid(agentDid) + + const dynamicVersion = `1.${Math.random() * 100}` + + const legacyIssuerId = namespaceIdentifier + const didIndyIssuerId = agentDid + const signingKey = await verificationKeyForIndyDid(agent.context, didIndyIssuerId) + + const legacySchemaId = `${namespaceIdentifier}:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + + const createSchemaTxResult = await indyVdrAnonCredsRegistry.registerSchema(agent.context, { + options: { + endorserMode: 'external', + endorserDid, + }, + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + }) + + const { schemaState } = createSchemaTxResult + + if (schemaState.state !== 'action' || schemaState.action !== 'endorseIndyTransaction') + throw Error('unexpected schema state') + + const endorsedTx = await endorser.modules.indyVdr.endorseTransaction(schemaState.schemaRequest, endorserDid) + + const submitSchemaTxResult = await indyVdrAnonCredsRegistry.registerSchema(agent.context, { + schema: schemaState.schema, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedTx, + }, + }) + + expect(submitSchemaTxResult).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + schemaId: didIndySchemaId, + }, + registrationMetadata: {}, + schemaMetadata: { + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacySchema = await indyVdrAnonCredsRegistry.getSchema(agent.context, legacySchemaId) + expect(legacySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: legacyIssuerId, + }, + schemaId: legacySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + // Resolve using did indy schema id + const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(agent.context, didIndySchemaId) + expect(didIndySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: didIndyIssuerId, + }, + schemaId: didIndySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + const legacyCredentialDefinitionId = `${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG` + + const createCredDefTxResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + options: { + endorserMode: 'external', + endorserDid, + }, + }) + + const { credentialDefinitionState } = createCredDefTxResult + + if (credentialDefinitionState.state !== 'action' || credentialDefinitionState.action !== 'endorseIndyTransaction') + throw Error('unexpected schema state') + + const endorsedCredDefTx = await endorser.modules.indyVdr.endorseTransaction( + credentialDefinitionState.credentialDefinitionRequest, + endorserDid + ) + const SubmitCredDefTxResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + credentialDefinition: credentialDefinitionState.credentialDefinition, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedCredDefTx, + }, + }) + + expect(SubmitCredDefTxResult).toMatchObject({ + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + credentialDefinition: { + issuerId: didIndyIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionId: didIndyCredentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + + const legacyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + legacyCredentialDefinitionId + ) + + expect(legacyCredentialDefinition).toMatchObject({ + credentialDefinitionId: legacyCredentialDefinitionId, + credentialDefinition: { + issuerId: legacyIssuerId, + schemaId: legacySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // resolve using did indy credential definition id + const didIndyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + didIndyCredentialDefinitionId + ) + + expect(didIndyCredentialDefinition).toMatchObject({ + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinition: { + issuerId: didIndyIssuerId, + schemaId: didIndySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry + const legacyRevocationRegistryId = `${namespaceIdentifier}:4:${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` + const didIndyRevocationRegistryId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` + const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ + submitterDid: namespaceIdentifier, + revocationRegistryDefinitionV1: { + credDefId: legacyCredentialDefinitionId, + id: legacyRevocationRegistryId, + revocDefType: 'CL_ACCUM', + tag: 'tag', + value: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + maxCredNum: 100, + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + }, + ver: '1.0', + }, + }) + + // After this call, the revocation registry should now be resolvable + const writeRequest = await pool.prepareWriteRequest( + agent.context, + revocationRegistryRequest, + signingKey, + endorserDid + ) + const endorsedRequest = await endorser.modules.indyVdr.endorseTransaction(writeRequest.body, endorserDid) + await pool.submitRequest(new CustomRequest({ customRequest: endorsedRequest })) + + // Also create a revocation registry entry + const revocationEntryRequest = new RevocationRegistryEntryRequest({ + revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinitionType: 'CL_ACCUM', + revocationRegistryEntry: { + ver: '1.0', + value: { + accum: '1', + }, + }, + submitterDid: legacyIssuerId, + }) + + // After this call we can query the revocation registry entries (using timestamp now) + + const revocationEntryWriteRequest = await pool.prepareWriteRequest( + agent.context, + revocationEntryRequest, + signingKey, + endorserDid + ) + const endorsedRevEntryWriteRequest = await endorser.modules.indyVdr.endorseTransaction( + revocationEntryWriteRequest.body, + endorserDid + ) + const entryResponse = (await pool.submitRequest( + new CustomRequest({ customRequest: endorsedRevEntryWriteRequest }) + )) as RevocationRegistryEntryResponse const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( agent.context,