Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anoncreds): add anoncreds API #1232

Merged
merged 10 commits into from
Feb 6, 2023
5 changes: 4 additions & 1 deletion packages/anoncreds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
"test": "jest"
},
"dependencies": {
"@aries-framework/core": "0.3.2"
"@aries-framework/core": "0.3.2",
"@aries-framework/node": "^0.3.2",
"bn.js": "^5.2.1"
},
"devDependencies": {
"indy-sdk": "^1.16.0-dev-1636",
"rimraf": "^4.0.7",
"typescript": "~4.9.4"
}
Expand Down
254 changes: 254 additions & 0 deletions packages/anoncreds/src/AnonCredsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import type { AnonCredsCredentialDefinition } from './models'
import type {
GetCredentialDefinitionReturn,
GetRevocationListReturn,
GetRevocationRegistryDefinitionReturn,
GetSchemaReturn,
RegisterCredentialDefinitionReturn,
RegisterSchemaOptions,
RegisterSchemaReturn,
} from './services'
import type { Extensible } from './services/registry/base'

import { AgentContext, injectable } from '@aries-framework/core'

import { AnonCredsModuleConfig } from './AnonCredsModuleConfig'
import { AnonCredsStoreRecordError } from './error'
import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord'
import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository'
import { AnonCredsSchemaRecord } from './repository/AnonCredsSchemaRecord'
import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository'
import { AnonCredsCredentialDefinitionRecordMetadataKeys } from './repository/anonCredsCredentialDefinitionRecordMetadataTypes'
import { AnonCredsIssuerService } from './services'
import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService'

@injectable()
export class AnonCredsApi {
public config: AnonCredsModuleConfig

private agentContext: AgentContext
private anonCredsRegistryService: AnonCredsRegistryService
private anonCredsSchemaRepository: AnonCredsSchemaRepository
private anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository

// TODO: how do we inject the anoncreds services?
private anonCredsIssuerService: AnonCredsIssuerService

public constructor(
agentContext: AgentContext,
anonCredsRegistryService: AnonCredsRegistryService,
config: AnonCredsModuleConfig,
anonCredsIssuerService: AnonCredsIssuerService,
anonCredsSchemaRepository: AnonCredsSchemaRepository,
anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository
) {
this.agentContext = agentContext
this.anonCredsRegistryService = anonCredsRegistryService
this.config = config
this.anonCredsIssuerService = anonCredsIssuerService
this.anonCredsSchemaRepository = anonCredsSchemaRepository
this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository
}

/**
* Retrieve a {@link AnonCredsSchema} from the registry associated
* with the {@link schemaId}
*/
public async getSchema(schemaId: string): Promise<GetSchemaReturn> {
const registry = this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, schemaId)

try {
const result = await registry.getSchema(this.agentContext, schemaId)
return result
} catch (error) {
return {
resolutionMetadata: {
error: 'error',
message: `Unable to resolve schema ${schemaId}: ${error.message}`,
},
schema: null,
schemaId,
schemaMetadata: {},
}
}
}

public async registerSchema(options: RegisterSchemaOptions): Promise<RegisterSchemaReturn> {
const failedReturnBase = {
schemaState: {
state: 'failed' as const,
schema: options.schema,
schemaId: null,
reason: `Error registering schema for issuerId ${options.schema.issuerId}`,
},
registrationMetadata: {},
schemaMetadata: {},
}

const registry = this.findRegistryForIdentifier(options.schema.issuerId)

if (!registry) {
failedReturnBase.schemaState.reason = `Could not find a registry for issuerId ${options.schema.issuerId}`
return failedReturnBase
}

try {
const result = await registry.registerSchema(this.agentContext, options)
await this.storeSchemaRecord(result)

return result
} catch (error) {
// Storage failed
if (error instanceof AnonCredsStoreRecordError) {
failedReturnBase.schemaState.reason = `Error storing schema record: ${error.message}`
return failedReturnBase
}

// In theory registerSchema SHOULD NOT throw, but we can't know for sure
failedReturnBase.schemaState.reason = `Error registering schema: ${error.message}`
return failedReturnBase
}
}

/**
* Retrieve a {@link AnonCredsCredentialDefinition} from the registry associated
* with the {@link credentialDefinitionId}
*/
public async getCredentialDefinition(credentialDefinitionId: string): Promise<GetCredentialDefinitionReturn> {
const registry = this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, credentialDefinitionId)

const result = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId)
return result
}

public async registerCredentialDefinition(options: {
credentialDefinition: Omit<AnonCredsCredentialDefinition, 'value' | 'type'>
// TODO: options should support supportsRevocation at some points
options: Extensible
}): Promise<RegisterCredentialDefinitionReturn> {
const registry = this.anonCredsRegistryService.getRegistryForIdentifier(
this.agentContext,
options.credentialDefinition.issuerId
)

const schemaRegistry = this.anonCredsRegistryService.getRegistryForIdentifier(
this.agentContext,
options.credentialDefinition.schemaId
)
const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId)

if (!schemaResult.schema) {
return {
credentialDefinitionMetadata: {},
credentialDefinitionState: {
reason: `error resolving schema with id ${options.credentialDefinition.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}`,
state: 'failed',
},
registrationMetadata: {},
}
}

const credentialDefinition = await this.anonCredsIssuerService.createCredentialDefinition(this.agentContext, {
issuerId: options.credentialDefinition.issuerId,
schemaId: options.credentialDefinition.schemaId,
tag: options.credentialDefinition.tag,
supportRevocation: false,
schema: schemaResult.schema,
})

const result = await registry.registerCredentialDefinition(this.agentContext, {
credentialDefinition,
options: options.options,
})

await this.storeCredentialDefinitionRecord(result)

return result
}

/**
* Retrieve a {@link AnonCredsRevocationRegistryDefinition} from the registry associated
* with the {@link revocationRegistryDefinitionId}
*/
public async getRevocationRegistryDefinition(
revocationRegistryDefinitionId: string
): Promise<GetRevocationRegistryDefinitionReturn> {
const registry = this.anonCredsRegistryService.getRegistryForIdentifier(
this.agentContext,
revocationRegistryDefinitionId
)

const result = await registry.getRevocationRegistryDefinition(this.agentContext, revocationRegistryDefinitionId)
return result
}

/**
* Retrieve the {@link AnonCredsRevocationList} for the given {@link timestamp} from the registry associated
* with the {@link revocationRegistryDefinitionId}
*/
public async getRevocationList(
revocationRegistryDefinitionId: string,
timestamp: number
): Promise<GetRevocationListReturn> {
const registry = this.anonCredsRegistryService.getRegistryForIdentifier(
this.agentContext,
revocationRegistryDefinitionId
)

const result = await registry.getRevocationList(this.agentContext, revocationRegistryDefinitionId, timestamp)
return result
}

private async storeCredentialDefinitionRecord(result: RegisterCredentialDefinitionReturn): Promise<void> {
// If we have both the credentialDefinition and the credentialDefinitionId we will store a copy of the credential definition. We may need to handle an
// edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel
if (
result.credentialDefinitionState.credentialDefinition &&
result.credentialDefinitionState.credentialDefinitionId
) {
const credentialDefinitionRecord = new AnonCredsCredentialDefinitionRecord({
credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId,
credentialDefinition: result.credentialDefinitionState.credentialDefinition,
})

// TODO: do we need to store this metadata? For indy, the registration metadata contains e.g.
// the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions
// are stored in the metadata
credentialDefinitionRecord.metadata.set(
AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata,
result.credentialDefinitionMetadata
)
credentialDefinitionRecord.metadata.set(
AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata,
result.registrationMetadata
)

await this.anonCredsCredentialDefinitionRepository.save(this.agentContext, credentialDefinitionRecord)
}
}

private async storeSchemaRecord(result: RegisterSchemaReturn): Promise<void> {
try {
// If we have both the schema and the schemaId we will store a copy of the schema. We may need to handle an
// edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel
if (result.schemaState.schema && result.schemaState.schemaId) {
const schemaRecord = new AnonCredsSchemaRecord({
schemaId: result.schemaState.schemaId,
schema: result.schemaState.schema,
})

await this.anonCredsSchemaRepository.save(this.agentContext, schemaRecord)
}
} catch (error) {
throw new AnonCredsStoreRecordError(`Error storing schema record`, { cause: error })
}
}

private findRegistryForIdentifier(identifier: string) {
try {
return this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, identifier)
} catch {
return null
}
}
}
8 changes: 8 additions & 0 deletions packages/anoncreds/src/AnonCredsModule.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig'
import type { DependencyManager, Module } from '@aries-framework/core'

import { AnonCredsApi } from './AnonCredsApi'
import { AnonCredsModuleConfig } from './AnonCredsModuleConfig'
import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository'
import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository'
import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService'

/**
* @public
*/
export class AnonCredsModule implements Module {
public readonly config: AnonCredsModuleConfig
public api = AnonCredsApi

public constructor(config: AnonCredsModuleConfigOptions) {
this.config = new AnonCredsModuleConfig(config)
Expand All @@ -19,5 +23,9 @@ export class AnonCredsModule implements Module {
dependencyManager.registerInstance(AnonCredsModuleConfig, this.config)

dependencyManager.registerSingleton(AnonCredsRegistryService)

// Repositories
dependencyManager.registerSingleton(AnonCredsSchemaRepository)
dependencyManager.registerSingleton(AnonCredsCredentialDefinitionRepository)
}
}
7 changes: 7 additions & 0 deletions packages/anoncreds/src/error/AnonCredsStoreRecordError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AnonCredsError } from './AnonCredsError'

export class AnonCredsStoreRecordError extends AnonCredsError {
public constructor(message: string, { cause }: { cause?: Error } = {}) {
super(message, { cause })
}
}
1 change: 1 addition & 0 deletions packages/anoncreds/src/error/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './AnonCredsError'
export * from './AnonCredsStoreRecordError'
89 changes: 89 additions & 0 deletions packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models'
import type { CredentialPreviewAttributeOptions, CredentialFormat, LinkedAttachment } from '@aries-framework/core'

/**
* This defines the module payload for calling CredentialsApi.createProposal
* or CredentialsApi.negotiateOffer
*/
export interface AnonCredsProposeCredentialFormat {
schemaIssuerId?: string
schemaId?: string
schemaName?: string
schemaVersion?: string

credentialDefinitionId?: string
issuerId?: string

attributes?: CredentialPreviewAttributeOptions[]
linkedAttachments?: LinkedAttachment[]

// Kept for backwards compatibility
schemaIssuerDid?: string
issuerDid?: string
}

/**
* This defines the module payload for calling CredentialsApi.acceptProposal
*/
export interface AnonCredsAcceptProposalFormat {
credentialDefinitionId?: string
attributes?: CredentialPreviewAttributeOptions[]
linkedAttachments?: LinkedAttachment[]
}

/**
* This defines the module payload for calling CredentialsApi.acceptOffer. No options are available for this
* method, so it's an empty object
*/
export type AnonCredsAcceptOfferFormat = Record<string, never>

/**
* This defines the module payload for calling CredentialsApi.offerCredential
* or CredentialsApi.negotiateProposal
*/
export interface AnonCredsOfferCredentialFormat {
credentialDefinitionId: string
attributes: CredentialPreviewAttributeOptions[]
linkedAttachments?: LinkedAttachment[]
}

/**
* This defines the module payload for calling CredentialsApi.acceptRequest. No options are available for this
* method, so it's an empty object
*/
export type AnonCredsAcceptRequestFormat = Record<string, never>

export interface AnonCredsCredentialFormat extends CredentialFormat {
formatKey: 'anoncreds'
credentialRecordType: 'anoncreds'
credentialFormats: {
createProposal: AnonCredsProposeCredentialFormat
acceptProposal: AnonCredsAcceptProposalFormat
createOffer: AnonCredsOfferCredentialFormat
acceptOffer: AnonCredsAcceptOfferFormat
createRequest: never // cannot start from createRequest
acceptRequest: AnonCredsAcceptRequestFormat
}
// TODO: update to new RFC once available
// Format data is based on RFC 0592
// https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments
formatData: {
proposal: {
schema_issuer_id?: string
schema_name?: string
schema_version?: string
schema_id?: string

cred_def_id?: string
issuer_id?: string

// TODO: we don't necessarily need to include these in the AnonCreds Format RFC
// as it's a new one and we can just forbid the use of legacy properties
schema_issuer_did?: string
issuer_did?: string
}
offer: AnonCredsCredentialOffer
request: AnonCredsCredentialRequest
credential: AnonCredsCredential
}
}
Loading