Skip to content

Commit

Permalink
feat(indy-vdr): schema + credential definition endorsement (#1451)
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Auer <[email protected]>
  • Loading branch information
auer-martin authored May 19, 2023
1 parent 9ee8c10 commit 25b981b
Show file tree
Hide file tree
Showing 12 changed files with 1,268 additions and 248 deletions.
35 changes: 23 additions & 12 deletions demo/src/Faber.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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<IndyVdrRegisterSchemaOptions>({
schema: schemaTemplate,
options: {},
options: {
endorserMode: 'internal',
endorserDid: this.anonCredsIssuerId,
},
})

if (schemaState.state !== 'finished') {
Expand All @@ -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<IndyVdrRegisterCredentialDefinitionOptions>({
credentialDefinition: {
schemaId,
issuerId: this.anonCredsIssuerId,
tag: 'latest',
},
options: {
endorserMode: 'internal',
endorserDid: this.anonCredsIssuerId,
},
})

if (credentialDefinitionState.state !== 'finished') {
throw new Error(
Expand Down
192 changes: 123 additions & 69 deletions packages/anoncreds/src/AnonCredsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'

Expand Down Expand Up @@ -146,7 +146,9 @@ export class AnonCredsApi {
}
}

public async registerSchema(options: RegisterSchemaOptions): Promise<RegisterSchemaReturn> {
public async registerSchema<T extends Extensible = Extensible>(
options: AnonCredsRegisterSchema<T>
): Promise<RegisterSchemaReturn> {
const failedReturnBase = {
schemaState: {
state: 'failed' as const,
Expand All @@ -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) {
Expand All @@ -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<GetCredentialDefinitionReturn> {
Expand Down Expand Up @@ -215,11 +219,9 @@ export class AnonCredsApi {
}
}

public async registerCredentialDefinition(options: {
credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions
// TODO: options should support supportsRevocation at some points
options: Extensible
}): Promise<RegisterCredentialDefinitionReturn> {
public async registerCredentialDefinition<T extends Extensible = Extensible>(
options: AnonCredsRegisterCredentialDefinition<T>
): Promise<RegisterCredentialDefinitionReturn> {
const failedReturnBase = {
credentialDefinitionState: {
state: 'failed' as const,
Expand All @@ -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<string, unknown> | undefined = undefined
let keyCorrectnessProof: Record<string, unknown> | 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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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<string, unknown>,
keyCorrectnessProof?: Record<string, unknown>
): Promise<void> {
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<void> {
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 })
}
Expand Down Expand Up @@ -450,3 +488,19 @@ export class AnonCredsApi {
}
}
}

interface AnonCredsRegisterCredentialDefinition<T extends Extensible = Extensible> {
credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions
options: T
}

interface AnonCredsRegisterSchema<T extends Extensible = Extensible> {
schema: AnonCredsSchema
options: T
}

function isFullCredentialDefinitionInput(
credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions
): credentialDefinition is AnonCredsCredentialDefinition {
return 'value' in credentialDefinition
}
4 changes: 3 additions & 1 deletion packages/anoncreds/src/AnonCredsApiOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export interface AnonCredsCreateLinkSecretOptions {
setAsDefault?: boolean
}

export type AnonCredsRegisterCredentialDefinitionOptions = Omit<AnonCredsCredentialDefinition, 'value' | 'type'>
export type AnonCredsRegisterCredentialDefinitionOptions =
| Omit<AnonCredsCredentialDefinition, 'value' | 'type'>
| AnonCredsCredentialDefinition
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type {
AnonCredsResolutionMetadata,
Extensible,
AnonCredsOperationStateAction,
AnonCredsOperationStateFailed,
AnonCredsOperationStateFinished,
AnonCredsOperationState,
AnonCredsOperationStateWait,
AnonCredsResolutionMetadata,
Extensible,
} from './base'
import type { AnonCredsCredentialDefinition } from '../../models/registry'

Expand All @@ -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
Expand Down
Loading

0 comments on commit 25b981b

Please sign in to comment.