From bb9d5ae42692ccc65529ee79c514abdb18329f81 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 18 Jan 2023 15:51:45 +0100 Subject: [PATCH 1/8] feat(anoncreds): add legacy indy credential format Signed-off-by: Timo Glastra --- packages/anoncreds/package.json | 5 +- .../src/formats/AnonCredsCredentialFormat.ts | 89 +++ .../src/formats/LegacyIndyCredentialFormat.ts | 67 ++ .../LegacyIndyCredentialFormatService.ts | 603 ++++++++++++++++++ .../LegacyIndyCredentialFormatService.test.ts | 224 +++++++ packages/anoncreds/src/models/exchange.ts | 18 +- packages/anoncreds/src/models/internal.ts | 27 +- .../src/services/AnonCredsHolderService.ts | 10 +- .../services/AnonCredsHolderServiceOptions.ts | 24 +- .../src/services/AnonCredsIssuerService.ts | 2 + .../services/AnonCredsIssuerServiceOptions.ts | 4 +- .../src/services/AnonCredsVerifierService.ts | 2 + .../registry/AnonCredsRegistryService.ts | 2 +- .../registry/CredentialDefinitionOptions.ts | 10 +- .../src/services/registry/SchemaOptions.ts | 8 +- .../AnonCredsRegistryService.test.ts | 6 +- .../src/utils/__tests__/credential.test.ts | 225 +++++++ packages/anoncreds/src/utils/credential.ts | 200 ++++++ packages/anoncreds/src/utils/metadata.ts | 29 + .../tests/InMemoryAnonCredsRegistry.ts | 157 +++++ packages/core/src/index.ts | 4 + .../formats/indy/IndyCredentialFormat.ts | 5 - .../core/src/modules/credentials/index.ts | 1 + packages/core/src/storage/Metadata.ts | 14 +- packages/core/tests/helpers.ts | 9 +- packages/indy-sdk/package.json | 2 +- .../services/IndySdkAnonCredsRegistry.ts | 55 +- .../services/IndySdkHolderService.ts | 30 +- .../services/IndySdkIssuerService.ts | 16 +- .../services/IndySdkRevocationService.ts | 23 +- .../services/IndySdkUtilitiesService.ts | 65 -- .../indy-sdk/src/anoncreds/utils/proverDid.ts | 12 + .../indy-sdk/src/anoncreds/utils/tails.ts | 45 ++ yarn.lock | 46 +- 34 files changed, 1813 insertions(+), 226 deletions(-) create mode 100644 packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts create mode 100644 packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts create mode 100644 packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts create mode 100644 packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts create mode 100644 packages/anoncreds/src/utils/__tests__/credential.test.ts create mode 100644 packages/anoncreds/src/utils/credential.ts create mode 100644 packages/anoncreds/src/utils/metadata.ts create mode 100644 packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts delete mode 100644 packages/indy-sdk/src/anoncreds/services/IndySdkUtilitiesService.ts create mode 100644 packages/indy-sdk/src/anoncreds/utils/proverDid.ts create mode 100644 packages/indy-sdk/src/anoncreds/utils/tails.ts diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 473c115b01..edbfbbeca8 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -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" } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts new file mode 100644 index 0000000000..fd6ebf7fcb --- /dev/null +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts @@ -0,0 +1,89 @@ +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' +import type { CredentialPreviewAttributeOptions, CredentialFormat, LinkedAttachment } from '@aries-framework/core' + +/** + * This defines the module payload for calling CredentialsApi.createProposal + * or CredentialsApi.negotiateOffer + */ +export interface AnonCredsProposeCredentialFormat { + schemaIssuerId?: string + schemaId?: string + schemaName?: string + schemaVersion?: string + + credentialDefinitionId?: string + issuerId?: string + + attributes?: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] + + // Kept for backwards compatibility + schemaIssuerDid?: string + issuerDid?: string +} + +/** + * This defines the module payload for calling CredentialsApi.acceptProposal + */ +export interface AnonCredsAcceptProposalFormat { + credentialDefinitionId?: string + attributes?: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] +} + +/** + * This defines the module payload for calling CredentialsApi.acceptOffer. No options are available for this + * method, so it's an empty object + */ +export type AnonCredsAcceptOfferFormat = Record + +/** + * This defines the module payload for calling CredentialsApi.offerCredential + * or CredentialsApi.negotiateProposal + */ +export interface AnonCredsOfferCredentialFormat { + credentialDefinitionId: string + attributes: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] +} + +/** + * This defines the module payload for calling CredentialsApi.acceptRequest. No options are available for this + * method, so it's an empty object + */ +export type AnonCredsAcceptRequestFormat = Record + +export interface AnonCredsCredentialFormat extends CredentialFormat { + formatKey: 'anoncreds' + credentialRecordType: 'anoncreds' + credentialFormats: { + createProposal: AnonCredsProposeCredentialFormat + acceptProposal: AnonCredsAcceptProposalFormat + createOffer: AnonCredsOfferCredentialFormat + acceptOffer: AnonCredsAcceptOfferFormat + createRequest: never // cannot start from createRequest + acceptRequest: AnonCredsAcceptRequestFormat + } + // TODO: update to new RFC once available + // Format data is based on RFC 0592 + // https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments + formatData: { + proposal: { + schema_issuer_id?: string + schema_name?: string + schema_version?: string + schema_id?: string + + cred_def_id?: string + issuer_id?: string + + // TODO: we don't necessarily need to include these in the AnonCreds Format RFC + // as it's a new one and we can just forbid the use of legacy properties + schema_issuer_did?: string + issuer_did?: string + } + offer: AnonCredsCredentialOffer + request: AnonCredsCredentialRequest + credential: AnonCredsCredential + } +} diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts new file mode 100644 index 0000000000..ce9be1e3eb --- /dev/null +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts @@ -0,0 +1,67 @@ +import type { + AnonCredsAcceptOfferFormat, + AnonCredsAcceptProposalFormat, + AnonCredsAcceptRequestFormat, + AnonCredsOfferCredentialFormat, +} from './AnonCredsCredentialFormat' +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' +import type { CredentialPreviewAttributeOptions, CredentialFormat, LinkedAttachment } from '@aries-framework/core' + +/** + * This defines the module payload for calling CredentialsApi.createProposal + * or CredentialsApi.negotiateOffer + * + * NOTE: This doesn't include the `issuerId` and `schemaIssuerId` properties that are present in the newer format. + */ +export interface LegacyIndyProposeCredentialFormat { + schemaIssuerDid?: string + schemaId?: string + schemaName?: string + schemaVersion?: string + + credentialDefinitionId?: string + issuerDid?: string + + attributes?: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] +} + +export interface LegacyIndyCredentialRequest extends AnonCredsCredentialRequest { + // prover_did is optional in AnonCreds credential request, but required in legacy format + prover_did: string +} + +export interface LegacyIndyCredentialFormat extends CredentialFormat { + formatKey: 'indy' + + // The stored type is the same as the anoncreds credential service + credentialRecordType: 'anoncreds' + + // credential formats are the same as the AnonCreds credential format + credentialFormats: { + // The createProposal interface is different between the interfaces + createProposal: LegacyIndyProposeCredentialFormat + acceptProposal: AnonCredsAcceptProposalFormat + createOffer: AnonCredsOfferCredentialFormat + acceptOffer: AnonCredsAcceptOfferFormat + createRequest: never // cannot start from createRequest + acceptRequest: AnonCredsAcceptRequestFormat + } + + // Format data is based on RFC 0592 + // https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments + formatData: { + proposal: { + schema_name?: string + schema_issuer_did?: string + schema_version?: string + schema_id?: string + + cred_def_id?: string + issuer_did?: string + } + offer: AnonCredsCredentialOffer + request: LegacyIndyCredentialRequest + credential: AnonCredsCredential + } +} diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts new file mode 100644 index 0000000000..1992f9318b --- /dev/null +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -0,0 +1,603 @@ +import type { LegacyIndyCredentialFormat } from './LegacyIndyCredentialFormat' +import type { + AnonCredsCredential, + AnonCredsCredentialOffer, + AnonCredsCredentialRequest, + AnonCredsCredentialRequestMetadata, +} from '../models' +import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' +import type { AnonCredsCredentialMetadata } from '../utils/metadata' +import type { + CredentialFormatService, + AgentContext, + FormatCreateProposalOptions, + FormatCreateProposalReturn, + FormatProcessOptions, + FormatAcceptProposalOptions, + FormatCreateOfferReturn, + FormatCreateOfferOptions, + FormatAcceptOfferOptions, + CredentialFormatCreateReturn, + FormatAcceptRequestOptions, + FormatProcessCredentialOptions, + FormatAutoRespondProposalOptions, + FormatAutoRespondOfferOptions, + FormatAutoRespondRequestOptions, + FormatAutoRespondCredentialOptions, + CredentialExchangeRecord, + CredentialPreviewAttributeOptions, + LinkedAttachment, +} from '@aries-framework/core' + +import { + CredentialFormatSpec, + AriesFrameworkError, + IndyCredPropose, + JsonTransformer, + Attachment, + CredentialPreviewAttribute, + AttachmentData, + JsonEncoder, + utils, + MessageValidator, + CredentialProblemReportError, + CredentialProblemReportReason, +} from '@aries-framework/core' + +import { AnonCredsError } from '../error' +import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' +import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' +import { + convertAttributesToCredentialValues, + assertCredentialValuesMatch, + checkCredentialValuesMatch, + assertAttributesMatch, + createAndLinkAttachmentsToPreview, +} from '../utils/credential' +import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' + +const INDY_CRED_ABSTRACT = 'hlindy/cred-abstract@v2.0' +const INDY_CRED_REQUEST = 'hlindy/cred-req@v2.0' +const INDY_CRED_FILTER = 'hlindy/cred-filter@v2.0' +const INDY_CRED = 'hlindy/cred@v2.0' + +export class LegacyIndyCredentialFormatService implements CredentialFormatService { + public readonly formatKey = 'indy' as const + public readonly credentialRecordType = 'anoncreds' as const + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the proposed credential + * @returns object containing associated attachment, format and optionally the credential preview + * + */ + public async createProposal( + agentContext: AgentContext, + { credentialFormats, credentialRecord }: FormatCreateProposalOptions + ): Promise { + const format = new CredentialFormatSpec({ + format: INDY_CRED_FILTER, + }) + + const indyFormat = credentialFormats.indy + + if (!indyFormat) { + throw new AriesFrameworkError('Missing indy payload in createProposal') + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { attributes, linkedAttachments, ...indyCredentialProposal } = indyFormat + + const proposal = new IndyCredPropose(indyCredentialProposal) + + try { + MessageValidator.validateSync(proposal) + } catch (error) { + throw new AriesFrameworkError(`Invalid proposal supplied: ${indyCredentialProposal} in Indy Format Service`) + } + + const proposalJson = JsonTransformer.toJSON(proposal) + const attachment = this.getFormatData(proposalJson, format.attachId) + + const { previewAttributes } = this.getCredentialLinkedAttachments( + indyFormat.attributes, + indyFormat.linkedAttachments + ) + + // Set the metadata + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + schemaId: proposal.schemaId, + credentialDefinitionId: proposal.credentialDefinitionId, + }) + + return { format, attachment, previewAttributes } + } + + public async processProposal(agentContext: AgentContext, { attachment }: FormatProcessOptions): Promise { + const proposalJson = attachment.getDataAsJson() + + // fromJSON also validates + JsonTransformer.fromJSON(proposalJson, IndyCredPropose) + } + + public async acceptProposal( + agentContext: AgentContext, + { + attachId, + credentialFormats, + credentialRecord, + proposalAttachment, + }: FormatAcceptProposalOptions + ): Promise { + const indyFormat = credentialFormats?.indy + + const credentialProposal = JsonTransformer.fromJSON(proposalAttachment.getDataAsJson(), IndyCredPropose) + + const credentialDefinitionId = indyFormat?.credentialDefinitionId ?? credentialProposal.credentialDefinitionId + + // TODO: we may want to extract the + const attributes = indyFormat?.attributes ?? credentialRecord.credentialAttributes + + if (!credentialDefinitionId) { + throw new AriesFrameworkError( + 'No credentialDefinitionId in proposal or provided as input to accept proposal method.' + ) + } + + if (!attributes) { + throw new AriesFrameworkError('No attributes in proposal or provided as input to accept proposal method.') + } + + const { format, attachment, previewAttributes } = await this.createIndyOffer(agentContext, { + credentialRecord, + attachId, + attributes, + credentialDefinitionId, + linkedAttachments: indyFormat?.linkedAttachments, + }) + + return { format, attachment, previewAttributes } + } + + /** + * Create a credential attachment format for a credential request. + * + * @param options The object containing all the options for the credential offer + * @returns object containing associated attachment, formats and offersAttach elements + * + */ + public async createOffer( + agentContext: AgentContext, + { credentialFormats, credentialRecord, attachId }: FormatCreateOfferOptions + ): Promise { + const indyFormat = credentialFormats.indy + + if (!indyFormat) { + throw new AriesFrameworkError('Missing indy credentialFormat data') + } + + const { format, attachment, previewAttributes } = await this.createIndyOffer(agentContext, { + credentialRecord, + attachId, + attributes: indyFormat.attributes, + credentialDefinitionId: indyFormat.credentialDefinitionId, + linkedAttachments: indyFormat.linkedAttachments, + }) + + return { format, attachment, previewAttributes } + } + + public async processOffer(agentContext: AgentContext, { attachment, credentialRecord }: FormatProcessOptions) { + agentContext.config.logger.debug(`Processing indy credential offer for credential record ${credentialRecord.id}`) + + const credOffer = attachment.getDataAsJson() + + if (!credOffer.schema_id || !credOffer.cred_def_id) { + throw new CredentialProblemReportError('Invalid credential offer', { + problemCode: CredentialProblemReportReason.IssuanceAbandoned, + }) + } + } + + public async acceptOffer( + agentContext: AgentContext, + { credentialRecord, attachId, offerAttachment }: FormatAcceptOfferOptions + ): Promise { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentialOffer = offerAttachment.getDataAsJson() + + // Get credential definition + const registry = registryService.getRegistryForIdentifier(agentContext, credentialOffer.cred_def_id) + const { credentialDefinition, resolutionMetadata } = await registry.getCredentialDefinition( + agentContext, + credentialOffer.cred_def_id + ) + + if (!credentialDefinition) { + throw new AnonCredsError( + `Unable to retrieve credential definition with id ${credentialOffer.cred_def_id}: ${resolutionMetadata.error} ${resolutionMetadata.message}` + ) + } + + const { credentialRequest, credentialRequestMetadata } = await holderService.createCredentialRequest(agentContext, { + credentialOffer, + credentialDefinition, + }) + + credentialRecord.metadata.set( + AnonCredsCredentialRequestMetadataKey, + credentialRequestMetadata + ) + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + credentialDefinitionId: credentialOffer.cred_def_id, + schemaId: credentialOffer.schema_id, + }) + + const format = new CredentialFormatSpec({ + attachId, + format: INDY_CRED_REQUEST, + }) + + const attachment = this.getFormatData(credentialRequest, format.attachId) + return { format, attachment } + } + + /** + * Starting from a request is not supported for indy credentials, this method only throws an error. + */ + public async createRequest(): Promise { + throw new AriesFrameworkError('Starting from a request is not supported for indy credentials') + } + + /** + * We don't have any models to validate an indy request object, for now this method does nothing + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async processRequest(agentContext: AgentContext, options: FormatProcessOptions): Promise { + // not needed for Indy + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialRecord, + attachId, + offerAttachment, + requestAttachment, + }: FormatAcceptRequestOptions + ): Promise { + // Assert credential attributes + const credentialAttributes = credentialRecord.credentialAttributes + if (!credentialAttributes) { + throw new CredentialProblemReportError( + `Missing required credential attribute values on credential record with id ${credentialRecord.id}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + const anonCredsIssuerService = + agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + const credentialOffer = offerAttachment?.getDataAsJson() + const credentialRequest = requestAttachment.getDataAsJson() + + if (!credentialOffer || !credentialRequest) { + throw new AriesFrameworkError('Missing indy credential offer or credential request in createCredential') + } + + const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { + credentialOffer, + credentialRequest, + credentialValues: convertAttributesToCredentialValues(credentialAttributes), + }) + + if (credential.rev_reg_id) { + credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { + credentialRevocationId: credentialRevocationId, + revocationRegistryId: credential.rev_reg_id, + }) + } + + const format = new CredentialFormatSpec({ + attachId, + format: INDY_CRED, + }) + + const attachment = this.getFormatData(credential, format.attachId) + return { format, attachment } + } + + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in the Indy wallet + * @param options the issue credential message wrapped inside this object + * @param credentialRecord the credential exchange record for this credential + */ + public async processCredential( + agentContext: AgentContext, + { credentialRecord, attachment }: FormatProcessCredentialOptions + ): Promise { + const credentialRequestMetadata = credentialRecord.metadata.get( + AnonCredsCredentialRequestMetadataKey + ) + + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + if (!credentialRequestMetadata) { + throw new CredentialProblemReportError( + `Missing required request metadata for credential with id ${credentialRecord.id}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + const anonCredsCredential = attachment.getDataAsJson() + + // We can use the same registry for the credential definition and revocation registry as they MUST have the same issuerId + const registry = registryService.getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id) + + const credentialDefinitionResult = await registry.getCredentialDefinition( + agentContext, + anonCredsCredential.cred_def_id + ) + if (!credentialDefinitionResult.credentialDefinition) { + throw new AriesFrameworkError( + `Unable to resolve credential definition ${anonCredsCredential.cred_def_id}: ${credentialDefinitionResult.resolutionMetadata.error} ${credentialDefinitionResult.resolutionMetadata.message}` + ) + } + + const revocationRegistryResult = anonCredsCredential.rev_reg_id + ? await registry.getRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id) + : null + if (revocationRegistryResult && !revocationRegistryResult.revocationRegistryDefinition) { + throw new AriesFrameworkError( + `Unable to resolve revocation registry definition ${anonCredsCredential.cred_def_id}: ${credentialDefinitionResult.resolutionMetadata.error} ${credentialDefinitionResult.resolutionMetadata.message}` + ) + } + + if (!credentialRecord.credentialAttributes) { + throw new AriesFrameworkError( + 'Missing credential attributes on credential record. Unable to check credential attributes' + ) + } + + // assert the credential values match the offer values + const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) + assertCredentialValuesMatch(anonCredsCredential.values, recordCredentialValues) + + const credentialId = await anonCredsHolderService.storeCredential(agentContext, { + credentialId: utils.uuid(), + credentialRequestMetadata, + credential: anonCredsCredential, + credentialDefinitionId: credentialDefinitionResult.credentialDefinitionId, + credentialDefinition: credentialDefinitionResult.credentialDefinition, + revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition + ? { + definition: revocationRegistryResult.revocationRegistryDefinition, + id: revocationRegistryResult.revocationRegistryDefinitionId, + } + : undefined, + }) + + // If the credential is revocable, store the revocation identifiers in the credential record + if (anonCredsCredential.rev_reg_id) { + const credential = await anonCredsHolderService.getCredential(agentContext, { credentialId }) + + credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { + credentialRevocationId: credential.credentialRevocationId, + revocationRegistryId: anonCredsCredential.rev_reg_id, + }) + } + + credentialRecord.credentials.push({ + credentialRecordType: this.credentialRecordType, + credentialRecordId: credentialId, + }) + } + + public supportsFormat(format: string): boolean { + const supportedFormats = [INDY_CRED_ABSTRACT, INDY_CRED_REQUEST, INDY_CRED_FILTER, INDY_CRED] + + return supportedFormats.includes(format) + } + + /** + * Gets the attachment object for a given attachId. We need to get out the correct attachId for + * indy and then find the corresponding attachment (if there is one) + * @param formats the formats object containing the attachId + * @param messageAttachments the attachments containing the payload + * @returns The Attachment if found or undefined + * + */ + public getAttachment(formats: CredentialFormatSpec[], messageAttachments: Attachment[]): Attachment | undefined { + const supportedAttachmentIds = formats.filter((f) => this.supportsFormat(f.format)).map((f) => f.attachId) + const supportedAttachments = messageAttachments.filter((attachment) => + supportedAttachmentIds.includes(attachment.id) + ) + + return supportedAttachments[0] + } + + public async deleteCredentialById(agentContext: AgentContext, credentialRecordId: string): Promise { + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + await anonCredsHolderService.deleteCredential(agentContext, credentialRecordId) + } + + public shouldAutoRespondToProposal( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: FormatAutoRespondProposalOptions + ) { + const credentialProposalJson = proposalAttachment.getDataAsJson() + const credentialProposal = JsonTransformer.fromJSON(credentialProposalJson, IndyCredPropose) + + const credentialOfferJson = offerAttachment.getDataAsJson() + + // We want to make sure the credential definition matches. + // TODO: If no credential definition is present on the proposal, we could check whether the other fields + // of the proposal match with the credential definition id. + return credentialProposal.credentialDefinitionId === credentialOfferJson.cred_def_id + } + + public shouldAutoRespondToOffer( + agentContext: AgentContext, + { offerAttachment, proposalAttachment }: FormatAutoRespondOfferOptions + ) { + const credentialProposalJson = proposalAttachment.getDataAsJson() + const credentialProposal = JsonTransformer.fromJSON(credentialProposalJson, IndyCredPropose) + + const credentialOfferJson = offerAttachment.getDataAsJson() + + // We want to make sure the credential definition matches. + // TODO: If no credential definition is present on the proposal, we could check whether the other fields + // of the proposal match with the credential definition id. + return credentialProposal.credentialDefinitionId === credentialOfferJson.cred_def_id + } + + public shouldAutoRespondToRequest( + agentContext: AgentContext, + { offerAttachment, requestAttachment }: FormatAutoRespondRequestOptions + ) { + const credentialOfferJson = offerAttachment.getDataAsJson() + const credentialRequestJson = requestAttachment.getDataAsJson() + + return credentialOfferJson.cred_def_id == credentialRequestJson.cred_def_id + } + + public shouldAutoRespondToCredential( + agentContext: AgentContext, + { credentialRecord, requestAttachment, credentialAttachment }: FormatAutoRespondCredentialOptions + ) { + const credentialJson = credentialAttachment.getDataAsJson() + const credentialRequestJson = requestAttachment.getDataAsJson() + + // make sure the credential definition matches + if (credentialJson.cred_def_id !== credentialRequestJson.cred_def_id) return false + + // If we don't have any attributes stored we can't compare so always return false. + if (!credentialRecord.credentialAttributes) return false + const attributeValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) + + // check whether the values match the values in the record + return checkCredentialValuesMatch(attributeValues, credentialJson.values) + } + + private async createIndyOffer( + agentContext: AgentContext, + { + credentialRecord, + attachId, + credentialDefinitionId, + attributes, + linkedAttachments, + }: { + credentialDefinitionId: string + credentialRecord: CredentialExchangeRecord + attachId?: string + attributes: CredentialPreviewAttributeOptions[] + linkedAttachments?: LinkedAttachment[] + } + ): Promise { + const anonCredsIssuerService = + agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + // if the proposal has an attachment Id use that, otherwise the generated id of the formats object + const format = new CredentialFormatSpec({ + attachId: attachId, + format: INDY_CRED_ABSTRACT, + }) + + const offer = await anonCredsIssuerService.createCredentialOffer(agentContext, { + credentialDefinitionId, + }) + + const { previewAttributes } = this.getCredentialLinkedAttachments(attributes, linkedAttachments) + if (!previewAttributes) { + throw new AriesFrameworkError('Missing required preview attributes for indy offer') + } + + await this.assertPreviewAttributesMatchSchemaAttributes(agentContext, offer, previewAttributes) + + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { + schemaId: offer.schema_id, + credentialDefinitionId: offer.cred_def_id, + }) + + const attachment = this.getFormatData(offer, format.attachId) + + return { format, attachment, previewAttributes } + } + + private async assertPreviewAttributesMatchSchemaAttributes( + agentContext: AgentContext, + offer: AnonCredsCredentialOffer, + attributes: CredentialPreviewAttribute[] + ): Promise { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + const registry = registryService.getRegistryForIdentifier(agentContext, offer.schema_id) + + const schemaResult = await registry.getSchema(agentContext, offer.schema_id) + + if (!schemaResult.schema) { + throw new AriesFrameworkError( + `Unable to resolve schema ${offer.schema_id} from registry: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` + ) + } + + assertAttributesMatch(schemaResult.schema, attributes) + } + + /** + * Get linked attachments for indy format from a proposal message. This allows attachments + * to be copied across to old style credential records + * + * @param options ProposeCredentialOptions object containing (optionally) the linked attachments + * @return array of linked attachments or undefined if none present + */ + private getCredentialLinkedAttachments( + attributes?: CredentialPreviewAttributeOptions[], + linkedAttachments?: LinkedAttachment[] + ): { + attachments?: Attachment[] + previewAttributes?: CredentialPreviewAttribute[] + } { + if (!linkedAttachments && !attributes) { + return {} + } + + let previewAttributes = attributes?.map((attribute) => new CredentialPreviewAttribute(attribute)) ?? [] + let attachments: Attachment[] | undefined + + if (linkedAttachments) { + // there are linked attachments so transform into the attribute field of the CredentialPreview object for + // this proposal + previewAttributes = createAndLinkAttachmentsToPreview(linkedAttachments, previewAttributes) + attachments = linkedAttachments.map((linkedAttachment) => linkedAttachment.attachment) + } + + return { attachments, previewAttributes } + } + + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + public getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(data), + }), + }) + + return attachment + } +} diff --git a/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts b/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts new file mode 100644 index 0000000000..7e1e1909da --- /dev/null +++ b/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts @@ -0,0 +1,224 @@ +import { + CredentialState, + CredentialExchangeRecord, + SigningProviderRegistry, + KeyType, + CredentialPreviewAttribute, +} from '@aries-framework/core' +import * as indySdk from 'indy-sdk' + +import { getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { + IndySdkHolderService, + IndySdkIssuerService, + IndySdkVerifierService, + IndySdkWallet, +} from '../../../../indy-sdk/src' +import { IndySdkRevocationService } from '../../../../indy-sdk/src/anoncreds/services/IndySdkRevocationService' +import { indyDidFromPublicKeyBase58 } from '../../../../indy-sdk/src/utils/did' +import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' +import { AnonCredsModuleConfig } from '../../AnonCredsModuleConfig' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, +} from '../../services' +import { AnonCredsRegistryService } from '../../services/registry/AnonCredsRegistryService' +import { LegacyIndyCredentialFormatService } from '../LegacyIndyCredentialFormatService' + +const registry = new InMemoryAnonCredsRegistry() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], +}) + +const agentConfig = getAgentConfig('LegacyIndyCredentialFormatServiceTest') +const anonCredsRevocationService = new IndySdkRevocationService(indySdk) +const anonCredsVerifierService = new IndySdkVerifierService(indySdk) +const anonCredsHolderService = new IndySdkHolderService(anonCredsRevocationService, indySdk) +const anonCredsIssuerService = new IndySdkIssuerService(indySdk) +const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) +const agentContext = getAgentContext({ + registerInstances: [ + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + ], + agentConfig, + wallet, +}) + +const indyCredentialFormatService = new LegacyIndyCredentialFormatService() + +describe('LegacyIndyCredentialFormatService', () => { + beforeEach(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + }) + + afterEach(async () => { + await wallet.delete() + }) + + test('issuance flow starting from proposal without negotiation and without revocation', async () => { + // This is just so we don't have to register an actually indy did (as we don't have the indy did registrar configured) + const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) + const indyDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId: indyDid, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState, schemaMetadata } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + const credentialDefinition = await anonCredsIssuerService.createCredentialDefinition( + agentContext, + { + issuerId: indyDid, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: false, + }, + { + // Need to pass this as the indy-sdk MUST have the seqNo + indyLedgerSchemaSeqNo: schemaMetadata.indyLedgerSeqNo as number, + } + ) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if ( + !credentialDefinitionState.credentialDefinition || + !credentialDefinitionState.credentialDefinitionId || + !schemaState.schema || + !schemaState.schemaId + ) { + throw new Error('Failed to create schema or credential definition') + } + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ + name: 'name', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + value: '25', + }), + ] + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await indyCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + indy: { + attributes: credentialAttributes, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }, + }) + + // Issuer processes and accepts proposal + await indyCredentialFormatService.processProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: proposalAttachment, + }) + // Set attributes on the credential record, this is normally done by the protocol service + issuerCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: offerAttachment } = await indyCredentialFormatService.acceptProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + proposalAttachment: proposalAttachment, + }) + + // Holder processes and accepts offer + await indyCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment } = await indyCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + }) + + // Issuer processes and accepts request + await indyCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await indyCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + }) + + // Holder processes and accepts credential + await indyCredentialFormatService.processCredential(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, + ]) + + const credentialId = holderCredentialRecord.credentials[0].credentialRecordId + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + credentialId, + }) + + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: '25', + name: 'John', + }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: null, + credentialRevocationId: null, + }) + + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anonCreds/anonCredsCredential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + '_anonCreds/anonCredsCredentialRequest': { + master_secret_blinding_data: expect.any(Object), + master_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anonCreds/anonCredsCredential': { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + }) + }) +}) diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index bd30979a86..f49cc67452 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -5,7 +5,7 @@ export interface ReferentWalletQuery { [key: string]: WalletQuery } -export interface NonRevokedInterval { +export interface AnonCredsNonRevokedInterval { from?: number to?: number } @@ -18,16 +18,16 @@ export interface AnonCredsCredentialOffer { } export interface AnonCredsCredentialRequest { - // TODO: Why is this needed? It is just used as context in Ursa, can be any string. Should we remove it? - // Should we not make it did related? - prover_did: string + // prover_did is deprecated, however it is kept for backwards compatibility with legacy anoncreds implementations + prover_did?: string cred_def_id: string blinded_ms: Record blinded_ms_correctness_proof: Record nonce: string } -export interface CredValue { +export type AnonCredsCredentialValues = Record +export interface AnonCredsCredentialValue { raw: string encoded: string // Raw value as number in string } @@ -36,7 +36,7 @@ export interface AnonCredsCredential { schema_id: string cred_def_id: string rev_reg_id?: string - values: Record + values: Record signature: unknown signature_correctness_proof: unknown } @@ -92,7 +92,7 @@ export interface AnonCredsProofRequest { name?: string names?: string[] restrictions?: WalletQuery[] - non_revoked?: NonRevokedInterval + non_revoked?: AnonCredsNonRevokedInterval } > requested_predicates: Record< @@ -102,9 +102,9 @@ export interface AnonCredsProofRequest { p_type: '>=' | '>' | '<=' | '<' p_value: number restrictions?: WalletQuery[] - non_revoked?: NonRevokedInterval + non_revoked?: AnonCredsNonRevokedInterval } > - non_revoked?: NonRevokedInterval + non_revoked?: AnonCredsNonRevokedInterval ver?: '1.0' | '2.0' } diff --git a/packages/anoncreds/src/models/internal.ts b/packages/anoncreds/src/models/internal.ts index c838dcf865..27d476ebb3 100644 --- a/packages/anoncreds/src/models/internal.ts +++ b/packages/anoncreds/src/models/internal.ts @@ -1,5 +1,5 @@ -export interface CredentialInfo { - referent: string +export interface AnonCredsCredentialInfo { + credentialId: string attributes: { [key: string]: string } @@ -9,23 +9,32 @@ export interface CredentialInfo { credentialRevocationId?: string | undefined } -export interface RequestedAttribute { +export interface AnonCredsRequestedAttribute { credentialId: string timestamp?: number revealed: boolean - credentialInfo: CredentialInfo + credentialInfo: AnonCredsCredentialInfo revoked?: boolean } -export interface RequestedPredicate { +export interface AnonCredsRequestedPredicate { credentialId: string timestamp?: number - credentialInfo: CredentialInfo + credentialInfo: AnonCredsCredentialInfo revoked?: boolean } -export interface RequestedCredentials { - requestedAttributes?: Record - requestedPredicates?: Record +export interface AnonCredsRequestedCredentials { + requestedAttributes?: Record + requestedPredicates?: Record selfAttestedAttributes: Record } + +export interface AnonCredsCredentialRequestMetadata { + master_secret_blinding_data: { + v_prime: string + vr_prime: string | null + } + master_secret_name: string + nonce: string +} diff --git a/packages/anoncreds/src/services/AnonCredsHolderService.ts b/packages/anoncreds/src/services/AnonCredsHolderService.ts index 4991dbca1f..a7c0dcb22e 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderService.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderService.ts @@ -7,10 +7,12 @@ import type { GetCredentialsForProofRequestOptions, GetCredentialsForProofRequestReturn, } from './AnonCredsHolderServiceOptions' -import type { CredentialInfo } from '../models' +import type { AnonCredsCredentialInfo } from '../models' import type { AnonCredsProof } from '../models/exchange' import type { AgentContext } from '@aries-framework/core' +export const AnonCredsHolderServiceSymbol = Symbol('AnonCredsHolderService') + export interface AnonCredsHolderService { createProof(agentContext: AgentContext, options: CreateProofOptions): Promise storeCredential( @@ -19,8 +21,10 @@ export interface AnonCredsHolderService { metadata?: Record ): Promise - // TODO: indy has different return types for the credential - getCredential(agentContext: AgentContext, options: GetCredentialOptions): Promise + // TODO: this doesn't actually return the credential, as the indy-sdk doesn't support that + // We could come up with a hack (as we've received the credential at one point), but for + // now I think it's not that much of an issue + getCredential(agentContext: AgentContext, options: GetCredentialOptions): Promise createCredentialRequest( agentContext: AgentContext, diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index 3de66df703..40a52ef567 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -1,10 +1,14 @@ -import type { CredentialInfo, RequestedCredentials } from '../models' +import type { + AnonCredsCredentialInfo, + AnonCredsCredentialRequestMetadata, + AnonCredsRequestedCredentials, +} from '../models' import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest, AnonCredsProofRequest, - NonRevokedInterval, + AnonCredsNonRevokedInterval, ReferentWalletQuery, } from '../models/exchange' import type { @@ -14,14 +18,14 @@ import type { AnonCredsSchema, } from '../models/registry' -export interface AttributeInfo { +export interface AnonCredsAttributeInfo { name?: string names?: string[] } export interface CreateProofOptions { proofRequest: AnonCredsProofRequest - requestedCredentials: RequestedCredentials + requestedCredentials: AnonCredsRequestedCredentials schemas: { [schemaId: string]: AnonCredsSchema } @@ -41,8 +45,7 @@ export interface CreateProofOptions { } export interface StoreCredentialOptions { - // TODO: what is in credential request metadata? - credentialRequestMetadata: Record + credentialRequestMetadata: AnonCredsCredentialRequestMetadata credential: AnonCredsCredential credentialDefinition: AnonCredsCredentialDefinition credentialDefinitionId: string @@ -66,19 +69,16 @@ export interface GetCredentialsForProofRequestOptions { } export type GetCredentialsForProofRequestReturn = Array<{ - credentialInfo: CredentialInfo - interval?: NonRevokedInterval + credentialInfo: AnonCredsCredentialInfo + interval?: AnonCredsNonRevokedInterval }> export interface CreateCredentialRequestOptions { - // TODO: Why is this needed? It is just used as context in Ursa, can be any string. Should we remove it? - // Should we not make it did related? (related to comment in AnonCredsCredentialRequest) - holderDid: string credentialOffer: AnonCredsCredentialOffer credentialDefinition: AnonCredsCredentialDefinition } export interface CreateCredentialRequestReturn { credentialRequest: AnonCredsCredentialRequest - credentialRequestMetadata: Record + credentialRequestMetadata: AnonCredsCredentialRequestMetadata } diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts index 0f34d300ef..41cb4ebf9f 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerService.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -9,6 +9,8 @@ import type { AnonCredsCredentialOffer } from '../models/exchange' import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../models/registry' import type { AgentContext } from '@aries-framework/core' +export const AnonCredsIssuerServiceSymbol = Symbol('AnonCredsIssuerService') + export interface AnonCredsIssuerService { createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index e3bb8dcdfb..58d6cd9048 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -2,7 +2,7 @@ import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest, - CredValue, + AnonCredsCredentialValues, } from '../models/exchange' import type { AnonCredsSchema } from '../models/registry' @@ -29,7 +29,7 @@ export interface CreateCredentialOfferOptions { export interface CreateCredentialOptions { credentialOffer: AnonCredsCredentialOffer credentialRequest: AnonCredsCredentialRequest - credentialValues: Record + credentialValues: AnonCredsCredentialValues revocationRegistryId?: string // TODO: should this just be the tails file instead of a path? tailsFilePath?: string diff --git a/packages/anoncreds/src/services/AnonCredsVerifierService.ts b/packages/anoncreds/src/services/AnonCredsVerifierService.ts index ec68021817..00e2a5670d 100644 --- a/packages/anoncreds/src/services/AnonCredsVerifierService.ts +++ b/packages/anoncreds/src/services/AnonCredsVerifierService.ts @@ -1,5 +1,7 @@ import type { VerifyProofOptions } from './AnonCredsVerifierServiceOptions' +export const AnonCredsVerifierServiceSymbol = Symbol('AnonCredsVerifierService') + export interface AnonCredsVerifierService { // TODO: do we want to extend the return type with more info besides a boolean. // If the value is false it would be nice to have some extra contexts about why it failed diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts index 8ee8eb4b50..a860d1e8f5 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts @@ -13,7 +13,7 @@ import { AnonCredsError } from '../../error' */ @injectable() export class AnonCredsRegistryService { - public async getRegistryForIdentifier(agentContext: AgentContext, identifier: string): Promise { + public getRegistryForIdentifier(agentContext: AgentContext, identifier: string): AnonCredsRegistry { const registries = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).registries // TODO: should we check if multiple are registered? diff --git a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts index 142e784405..5b430e05b5 100644 --- a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts @@ -20,18 +20,18 @@ export interface RegisterCredentialDefinitionOptions { } export interface RegisterCredentialDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { - credentialDefinition: AnonCredsCredentialDefinition - credentialDefinitionId?: string + credentialDefinition?: AnonCredsCredentialDefinition + credentialDefinitionId: string | null } export interface RegisterCredentialDefinitionReturnStateFinished extends AnonCredsOperationStateFinished { credentialDefinition: AnonCredsCredentialDefinition - credentialDefinitionId?: string + credentialDefinitionId: string | null } export interface RegisterCredentialDefinitionReturnState extends AnonCredsOperationState { - credentialDefinition: AnonCredsCredentialDefinition - credentialDefinitionId?: string + credentialDefinition?: AnonCredsCredentialDefinition + credentialDefinitionId: string | null } export interface RegisterCredentialDefinitionReturn { diff --git a/packages/anoncreds/src/services/registry/SchemaOptions.ts b/packages/anoncreds/src/services/registry/SchemaOptions.ts index c436859060..2fa6842f71 100644 --- a/packages/anoncreds/src/services/registry/SchemaOptions.ts +++ b/packages/anoncreds/src/services/registry/SchemaOptions.ts @@ -24,8 +24,8 @@ export interface RegisterSchemaOptions { } export interface RegisterSchemaReturnStateFailed extends AnonCredsOperationStateFailed { - schema: AnonCredsSchema - schemaId?: string + schema: AnonCredsSchema | null + schemaId: string | null } export interface RegisterSchemaReturnStateFinished extends AnonCredsOperationStateFinished { @@ -34,8 +34,8 @@ export interface RegisterSchemaReturnStateFinished extends AnonCredsOperationSta } export interface RegisterSchemaReturnState extends AnonCredsOperationState { - schema: AnonCredsSchema - schemaId?: string + schema: AnonCredsSchema | null + schemaId: string | null } export interface RegisterSchemaReturn { diff --git a/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts b/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts index 096626f805..2cb39bc2e5 100644 --- a/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts +++ b/packages/anoncreds/src/services/registry/__tests__/AnonCredsRegistryService.test.ts @@ -28,11 +28,11 @@ const anonCredsRegistryService = new AnonCredsRegistryService() describe('AnonCredsRegistryService', () => { test('returns the registry for an identifier based on the supportedMethods regex', async () => { - await expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'a')).resolves.toEqual(registryOne) - await expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'b')).resolves.toEqual(registryTwo) + expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'a')).toEqual(registryOne) + expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'b')).toEqual(registryTwo) }) test('throws AnonCredsError if no registry is found for the given identifier', async () => { - await expect(anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'c')).rejects.toThrow(AnonCredsError) + expect(() => anonCredsRegistryService.getRegistryForIdentifier(agentContext, 'c')).toThrow(AnonCredsError) }) }) diff --git a/packages/anoncreds/src/utils/__tests__/credential.test.ts b/packages/anoncreds/src/utils/__tests__/credential.test.ts new file mode 100644 index 0000000000..0b81afe881 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/credential.test.ts @@ -0,0 +1,225 @@ +import { CredentialPreviewAttribute } from '@aries-framework/core' + +import { assertCredentialValuesMatch, checkValidEncoding, convertAttributesToCredentialValues } from '../credential' + +/** + * Sample test cases for encoding/decoding of verifiable credential claims - Aries RFCs 0036 and 0037 + * @see https://gist.github.com/swcurran/78e5a9e8d11236f003f6a6263c6619a6 + */ +const testEncodings: { [key: string]: { raw: string | number | boolean | null; encoded: string } } = { + address2: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + zip: { + raw: '87121', + encoded: '87121', + }, + city: { + raw: 'SLC', + encoded: '101327353979588246869873249766058188995681113722618593621043638294296500696424', + }, + address1: { + raw: '101 Tela Lane', + encoded: '63690509275174663089934667471948380740244018358024875547775652380902762701972', + }, + state: { + raw: 'UT', + encoded: '93856629670657830351991220989031130499313559332549427637940645777813964461231', + }, + Empty: { + raw: '', + encoded: '102987336249554097029535212322581322789799900648198034993379397001115665086549', + }, + Null: { + raw: null, + encoded: '99769404535520360775991420569103450442789945655240760487761322098828903685777', + }, + 'bool True': { + raw: true, + encoded: '1', + }, + 'bool False': { + raw: false, + encoded: '0', + }, + 'str True': { + raw: 'True', + encoded: '27471875274925838976481193902417661171675582237244292940724984695988062543640', + }, + 'str False': { + raw: 'False', + encoded: '43710460381310391454089928988014746602980337898724813422905404670995938820350', + }, + 'max i32': { + raw: 2147483647, + encoded: '2147483647', + }, + 'max i32 + 1': { + raw: 2147483648, + encoded: '26221484005389514539852548961319751347124425277437769688639924217837557266135', + }, + 'min i32': { + raw: -2147483648, + encoded: '-2147483648', + }, + 'min i32 - 1': { + raw: -2147483649, + encoded: '68956915425095939579909400566452872085353864667122112803508671228696852865689', + }, + 'float 0.1': { + raw: 0.1, + encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', + }, + 'str 0.1': { + raw: '0.1', + encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', + }, + 'str 1.0': { + raw: '1.0', + encoded: '94532235908853478633102631881008651863941875830027892478278578250784387892726', + }, + 'str 1': { + raw: '1', + encoded: '1', + }, + 'leading zero number string': { + raw: '012345', + encoded: '12345', + }, + 'chr 0': { + raw: String.fromCharCode(0), + encoded: '49846369543417741186729467304575255505141344055555831574636310663216789168157', + }, + 'chr 1': { + raw: String.fromCharCode(1), + encoded: '34356466678672179216206944866734405838331831190171667647615530531663699592602', + }, + 'chr 2': { + raw: String.fromCharCode(2), + encoded: '99398763056634537812744552006896172984671876672520535998211840060697129507206', + }, +} + +describe('Utils | Credentials', () => { + describe('convertAttributesToCredentialValues', () => { + test('returns object with raw and encoded attributes', () => { + const attributes = [ + new CredentialPreviewAttribute({ + name: 'name', + mimeType: 'text/plain', + value: '101 Wilson Lane', + }), + new CredentialPreviewAttribute({ + name: 'age', + mimeType: 'text/plain', + value: '1234', + }), + ] + + expect(convertAttributesToCredentialValues(attributes)).toEqual({ + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + }) + }) + }) + + describe('assertCredentialValuesMatch', () => { + test('does not throw if attributes match', () => { + const firstValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).not.toThrow() + }) + + test('throws if number of values in the entries do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + 'Number of values in first entry (1) does not match number of values in second entry (2)' + ) + }) + + test('throws if second value does not contain key from first value', () => { + const firstValues = { + name: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + anotherName: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + age: { raw: '1234', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Second cred values object has no value for key 'name'" + ) + }) + + test('throws if encoded values do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + age: { raw: '1234', encoded: '12345' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Encoded credential values for key 'age' do not match" + ) + }) + + test('throws if raw values do not match', () => { + const firstValues = { + age: { raw: '1234', encoded: '1234' }, + } + const secondValues = { + age: { raw: '12345', encoded: '1234' }, + } + + expect(() => assertCredentialValuesMatch(firstValues, secondValues)).toThrow( + "Raw credential values for key 'age' do not match" + ) + }) + }) + + describe('checkValidEncoding', () => { + // Formatted for test.each + const testEntries = Object.entries(testEncodings).map( + ([name, { raw, encoded }]) => [name, raw, encoded] as [string, string | number | boolean | null, string] + ) + + test.each(testEntries)('returns true for valid encoding %s', (_, raw, encoded) => { + expect(checkValidEncoding(raw, encoded)).toEqual(true) + }) + }) +}) diff --git a/packages/anoncreds/src/utils/credential.ts b/packages/anoncreds/src/utils/credential.ts new file mode 100644 index 0000000000..6310270980 --- /dev/null +++ b/packages/anoncreds/src/utils/credential.ts @@ -0,0 +1,200 @@ +import type { AnonCredsSchema, AnonCredsCredentialValues } from '../models' +import type { CredentialPreviewAttributeOptions, LinkedAttachment } from '@aries-framework/core' + +import { CredentialPreviewAttribute, AriesFrameworkError, Hasher, encodeAttachment } from '@aries-framework/core' +import BigNumber from 'bn.js' + +const isString = (value: unknown): value is string => typeof value === 'string' +const isNumber = (value: unknown): value is number => typeof value === 'number' +const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean' +const isNumeric = (value: string) => /^-?\d+$/.test(value) + +const isInt32 = (number: number) => { + const minI32 = -2147483648 + const maxI32 = 2147483647 + + // Check if number is integer and in range of int32 + return Number.isInteger(number) && number >= minI32 && number <= maxI32 +} + +/** + * Converts int value to string + * Converts string value: + * - hash with sha256, + * - convert to byte array and reverse it + * - convert it to BigInteger and return as a string + * @param attributes + * + * @returns CredValues + */ +export function convertAttributesToCredentialValues( + attributes: CredentialPreviewAttributeOptions[] +): AnonCredsCredentialValues { + return attributes.reduce((credentialValues, attribute) => { + return { + [attribute.name]: { + raw: attribute.value, + encoded: encode(attribute.value), + }, + ...credentialValues, + } + }, {}) +} + +/** + * Check whether the values of two credentials match (using {@link assertCredentialValuesMatch}) + * + * @returns a boolean whether the values are equal + * + */ +export function checkCredentialValuesMatch( + firstValues: AnonCredsCredentialValues, + secondValues: AnonCredsCredentialValues +): boolean { + try { + assertCredentialValuesMatch(firstValues, secondValues) + return true + } catch { + return false + } +} + +/** + * Assert two credential values objects match. + * + * @param firstValues The first values object + * @param secondValues The second values object + * + * @throws If not all values match + */ +export function assertCredentialValuesMatch( + firstValues: AnonCredsCredentialValues, + secondValues: AnonCredsCredentialValues +) { + const firstValuesKeys = Object.keys(firstValues) + const secondValuesKeys = Object.keys(secondValues) + + if (firstValuesKeys.length !== secondValuesKeys.length) { + throw new Error( + `Number of values in first entry (${firstValuesKeys.length}) does not match number of values in second entry (${secondValuesKeys.length})` + ) + } + + for (const key of firstValuesKeys) { + const firstValue = firstValues[key] + const secondValue = secondValues[key] + + if (!secondValue) { + throw new Error(`Second cred values object has no value for key '${key}'`) + } + + if (firstValue.encoded !== secondValue.encoded) { + throw new Error(`Encoded credential values for key '${key}' do not match`) + } + + if (firstValue.raw !== secondValue.raw) { + throw new Error(`Raw credential values for key '${key}' do not match`) + } + } +} + +/** + * Check whether the raw value matches the encoded version according to the encoding format described in Aries RFC 0037 + * Use this method to ensure the received proof (over the encoded) value is the same as the raw value of the data. + * + * @param raw + * @param encoded + * @returns Whether raw and encoded value match + * + * @see https://github.com/hyperledger/aries-framework-dotnet/blob/a18bef91e5b9e4a1892818df7408e2383c642dfa/src/Hyperledger.Aries/Utils/CredentialUtils.cs#L78-L89 + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0037-present-proof/README.md#verifying-claims-of-indy-based-verifiable-credentials + */ +export function checkValidEncoding(raw: unknown, encoded: string) { + return encoded === encode(raw) +} + +/** + * Encode value according to the encoding format described in Aries RFC 0036/0037 + * + * @param value + * @returns Encoded version of value + * + * @see https://github.com/hyperledger/aries-cloudagent-python/blob/0000f924a50b6ac5e6342bff90e64864672ee935/aries_cloudagent/messaging/util.py#L106-L136 + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0037-present-proof/README.md#verifying-claims-of-indy-based-verifiable-credentials + * @see https://github.com/hyperledger/aries-rfcs/blob/be4ad0a6fb2823bb1fc109364c96f077d5d8dffa/features/0036-issue-credential/README.md#encoding-claims-for-indy-based-verifiable-credentials + */ +export function encode(value: unknown) { + const isEmpty = (value: unknown) => isString(value) && value === '' + + // If bool return bool as number string + if (isBoolean(value)) { + return Number(value).toString() + } + + // If value is int32 return as number string + if (isNumber(value) && isInt32(value)) { + return value.toString() + } + + // If value is an int32 number string return as number string + if (isString(value) && !isEmpty(value) && !isNaN(Number(value)) && isNumeric(value) && isInt32(Number(value))) { + return Number(value).toString() + } + + if (isNumber(value)) { + value = value.toString() + } + + // If value is null we must use the string value 'None' + if (value === null || value === undefined) { + value = 'None' + } + + return new BigNumber(Hasher.hash(Buffer.from(value as string), 'sha2-256')).toString() +} + +export function assertAttributesMatch(schema: AnonCredsSchema, attributes: CredentialPreviewAttribute[]) { + const schemaAttributes = schema.attrNames + const credAttributes = attributes.map((a) => a.name) + + const difference = credAttributes + .filter((x) => !schemaAttributes.includes(x)) + .concat(schemaAttributes.filter((x) => !credAttributes.includes(x))) + + if (difference.length > 0) { + throw new AriesFrameworkError( + `The credential preview attributes do not match the schema attributes (difference is: ${difference}, needs: ${schemaAttributes})` + ) + } +} + +/** + * Adds attribute(s) to the credential preview that is linked to the given attachment(s) + * + * @param attachments a list of the attachments that need to be linked to a credential + * @param preview the credential previews where the new linked credential has to be appended to + * + * @returns a modified version of the credential preview with the linked credentials + * */ +export function createAndLinkAttachmentsToPreview( + attachments: LinkedAttachment[], + previewAttributes: CredentialPreviewAttribute[] +) { + const credentialPreviewAttributeNames = previewAttributes.map((attribute) => attribute.name) + const newPreviewAttributes = [...previewAttributes] + + attachments.forEach((linkedAttachment) => { + if (credentialPreviewAttributeNames.includes(linkedAttachment.attributeName)) { + throw new AriesFrameworkError(`linkedAttachment ${linkedAttachment.attributeName} already exists in the preview`) + } else { + const credentialPreviewAttribute = new CredentialPreviewAttribute({ + name: linkedAttachment.attributeName, + mimeType: linkedAttachment.attachment.mimeType, + value: encodeAttachment(linkedAttachment.attachment), + }) + newPreviewAttributes.push(credentialPreviewAttribute) + } + }) + + return newPreviewAttributes +} diff --git a/packages/anoncreds/src/utils/metadata.ts b/packages/anoncreds/src/utils/metadata.ts new file mode 100644 index 0000000000..1d8448ebfa --- /dev/null +++ b/packages/anoncreds/src/utils/metadata.ts @@ -0,0 +1,29 @@ +// TODO: we may want to already support multiple credentials in the metadata of a credential +// record, as that's what the RFCs support. We already need to write a migration script for modules + +/** + * Metadata key for strong metadata on an AnonCreds credential. + * + * MUST be used with {@link AnonCredsCredentialMetadata} + */ +export const AnonCredsCredentialMetadataKey = '_anonCreds/anonCredsCredential' + +/** + * Metadata key for strong metadata on an AnonCreds credential request. + * + * MUST be used with {@link AnonCredsCredentialRequestMetadata} + */ +export const AnonCredsCredentialRequestMetadataKey = '_anonCreds/anonCredsCredentialRequest' + +/** + * Metadata for an AnonCreds credential that will be stored + * in the credential record. + * + * MUST be used with {@link AnonCredsCredentialMetadataKey} + */ +export interface AnonCredsCredentialMetadata { + schemaId?: string + credentialDefinitionId?: string + revocationRegistryId?: string + credentialRevocationId?: string +} diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts new file mode 100644 index 0000000000..bc49d939f7 --- /dev/null +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -0,0 +1,157 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { + AnonCredsRegistry, + GetSchemaReturn, + RegisterSchemaOptions, + RegisterSchemaReturn, + GetCredentialDefinitionReturn, + RegisterCredentialDefinitionOptions, + RegisterCredentialDefinitionReturn, + GetRevocationRegistryDefinitionReturn, + GetRevocationListReturn, + AnonCredsSchema, + AnonCredsCredentialDefinition, +} from '../src' +import type { AgentContext } from '@aries-framework/core' + +import { Hasher, TypedArrayEncoder } from '@aries-framework/core' +import BigNumber from 'bn.js' + +/** + * In memory implementation of the {@link AnonCredsRegistry} interface. Useful for testing. + */ +export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { + // Roughly match that the identifier starts with an unqualified indy did. Once the + // anoncreds tests are not based on the indy-sdk anymore, we can use any identifier + // we want, but the indy-sdk is picky about the identifier format. + public readonly supportedIdentifier = /^[a-zA-Z0-9]{21,22}/ + + private schemas: Record = {} + private credentialDefinitions: Record = {} + + public async getSchema(agentContext: AgentContext, schemaId: string): Promise { + const schema = this.schemas[schemaId] + const indyLedgerSeqNo = getSeqNoFromSchemaId(schemaId) + + if (!schema) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Schema with id ${schemaId} not found in memory registry`, + }, + schema: null, + schemaId, + schemaMetadata: { + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo, + }, + } + } + + return { + resolutionMetadata: {}, + schema, + schemaId, + schemaMetadata: {}, + } + } + + public async registerSchema( + agentContext: AgentContext, + options: RegisterSchemaOptions + ): Promise { + const schemaId = `${options.schema.issuerId}:2:${options.schema.name}:${options.schema.version}` + const indyLedgerSeqNo = getSeqNoFromSchemaId(schemaId) + + this.schemas[schemaId] = options.schema + + return { + registrationMetadata: {}, + schemaMetadata: { + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo, + }, + schemaState: { + state: 'finished', + schema: options.schema, + schemaId, + }, + } + } + + public async getCredentialDefinition( + agentContext: AgentContext, + credentialDefinitionId: string + ): Promise { + const credentialDefinition = this.credentialDefinitions[credentialDefinitionId] + + if (!credentialDefinition) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Credential definition with id ${credentialDefinitionId} not found in memory registry`, + }, + credentialDefinition: null, + credentialDefinitionId, + credentialDefinitionMetadata: {}, + } + } + + return { + resolutionMetadata: {}, + credentialDefinition, + credentialDefinitionId, + credentialDefinitionMetadata: {}, + } + } + + public async registerCredentialDefinition( + agentContext: AgentContext, + options: RegisterCredentialDefinitionOptions + ): Promise { + const indyLedgerSeqNo = getSeqNoFromSchemaId(options.credentialDefinition.schemaId) + const credentialDefinitionId = `${options.credentialDefinition.issuerId}:3:CL:${indyLedgerSeqNo}:${options.credentialDefinition.tag}` + + this.credentialDefinitions[credentialDefinitionId] = options.credentialDefinition + + return { + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + state: 'finished', + credentialDefinition: options.credentialDefinition, + credentialDefinitionId, + }, + } + } + + public getRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ): Promise { + throw new Error('Method not implemented.') + } + + public getRevocationList( + agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number + ): Promise { + throw new Error('Method not implemented.') + } +} + +/** + * Calculates a consistent sequence number for a given schema id. + * + * Does this by hashing the schema id, transforming the hash to a number and taking the first 6 digits. + */ +function getSeqNoFromSchemaId(schemaId: string) { + const seqNo = Number( + new BigNumber(Hasher.hash(TypedArrayEncoder.fromString(schemaId), 'sha2-256')).toString().slice(0, 5) + ) + + return seqNo +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5b2eaf1762..919e2e712a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,6 +67,10 @@ export * from './agent/Events' export * from './crypto/' export { PersistedLruCache, CacheRepository } from './cache' +export { encodeAttachment } from './utils/attachment' +export { Hasher } from './utils/Hasher' +export { MessageValidator } from './utils/MessageValidator' +export { LinkedAttachment, LinkedAttachmentOptions } from './utils/LinkedAttachment' import { parseInvitationUrl } from './utils/parseInvitation' import { uuid } from './utils/uuid' diff --git a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormat.ts b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormat.ts index eeee56e5d9..73c8082372 100644 --- a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormat.ts +++ b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormat.ts @@ -36,11 +36,6 @@ export interface IndyOfferCredentialFormat { linkedAttachments?: LinkedAttachment[] } -export interface IndyIssueCredentialFormat { - credentialDefinitionId?: string - attributes?: CredentialPreviewAttributeOptions[] -} - export interface IndyCredentialFormat extends CredentialFormat { formatKey: 'indy' credentialRecordType: 'indy' diff --git a/packages/core/src/modules/credentials/index.ts b/packages/core/src/modules/credentials/index.ts index d34680afe1..286f34276d 100644 --- a/packages/core/src/modules/credentials/index.ts +++ b/packages/core/src/modules/credentials/index.ts @@ -7,3 +7,4 @@ export * from './formats' export * from './protocol' export * from './CredentialsModule' export * from './CredentialsModuleConfig' +export { CredentialProblemReportError, CredentialProblemReportReason } from './errors' diff --git a/packages/core/src/storage/Metadata.ts b/packages/core/src/storage/Metadata.ts index 87c3e0d298..c635c1c2c5 100644 --- a/packages/core/src/storage/Metadata.ts +++ b/packages/core/src/storage/Metadata.ts @@ -1,5 +1,9 @@ +// Any is used to prevent frustrating TS errors if we just want to store arbitrary json data +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type MetadataValue = Record + export type MetadataBase = { - [key: string]: Record + [key: string]: MetadataValue } /** @@ -31,7 +35,7 @@ export class Metadata { * @returns the value saved in the key value pair * @returns null when the key could not be found */ - public get, Key extends string = string>( + public get( key: Key ): (Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value) | null { return (this.data[key] as Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value) ?? null @@ -43,11 +47,11 @@ export class Metadata { * @param key the key to set the metadata by * @param value the value to set in the metadata */ - public set, Key extends string = string>( + public set( key: Key, value: Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value ): void { - this.data[key] = value as Record + this.data[key] = value as MetadataValue } /** @@ -56,7 +60,7 @@ export class Metadata { * @param key the key to add the metadata at * @param value the value to add in the metadata */ - public add, Key extends string = string>( + public add( key: Key, value: Partial ): void { diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 9e57c21943..21240f1618 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -19,7 +19,7 @@ import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../ import type { IndyOfferCredentialFormat } from '../src/modules/credentials/formats/indy/IndyCredentialFormat' import type { ProofAttributeInfo, ProofPredicateInfo } from '../src/modules/proofs/formats/indy/models' import type { AutoAcceptProof } from '../src/modules/proofs/models/ProofAutoAcceptType' -import type { Awaited } from '../src/types' +import type { Awaited, WalletConfig } from '../src/types' import type { CredDef, Schema } from 'indy-sdk' import type { Observable } from 'rxjs' @@ -159,9 +159,12 @@ export function getPostgresAgentOptions(name: string, extraConfig: Partial = {}) { +export function getAgentConfig( + name: string, + extraConfig: Partial = {} +): AgentConfig & { walletConfig: WalletConfig } { const { config, dependencies } = getAgentOptions(name, extraConfig) - return new AgentConfig(config, dependencies) + return new AgentConfig(config, dependencies) as AgentConfig & { walletConfig: WalletConfig } } export function getAgentContext({ diff --git a/packages/indy-sdk/package.json b/packages/indy-sdk/package.json index 02dec486ae..eaba865449 100644 --- a/packages/indy-sdk/package.json +++ b/packages/indy-sdk/package.json @@ -2,7 +2,7 @@ "name": "@aries-framework/indy-sdk", "main": "build/index", "types": "build/index", - "version": "0.2.5", + "version": "0.3.2", "private": true, "files": [ "build" diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index 95b08fa88b..0668219172 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -75,13 +75,13 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { issuerId: issuerId, }, schemaId: schema.id, - resolutionMetadata: { + resolutionMetadata: {}, + schemaMetadata: { didIndyNamespace: pool.didIndyNamespace, // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. // For this reason we return it in the metadata. indyLedgerSeqNo: schema.seqNo, }, - schemaMetadata: {}, } } catch (error) { agentContext.config.logger.error(`Error retrieving schema '${schemaId}'`, { @@ -111,6 +111,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { schemaMetadata: {}, registrationMetadata: {}, schemaState: { + schemaId: null, reason: 'no didIndyNamespace defined in the options. didIndyNamespace is required when using the Indy SDK', schema: options.schema, state: 'failed', @@ -157,13 +158,13 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { }, schemaId: schema.id, }, - registrationMetadata: { + registrationMetadata: {}, + schemaMetadata: { // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. // For this reason we return it in the metadata. indyLedgerSeqNo: schema.seqNo, didIndyNamespace: pool.didIndyNamespace, }, - schemaMetadata: {}, } } catch (error) { agentContext.config.logger.error(`Error registering schema for did '${options.schema.issuerId}'`, { @@ -176,6 +177,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { schemaMetadata: {}, registrationMetadata: {}, schemaState: { + schemaId: null, state: 'failed', schema: options.schema, reason: `unknownError: ${error.message}`, @@ -229,10 +231,10 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { type: 'CL', value: credentialDefinition.value, }, - credentialDefinitionMetadata: {}, - resolutionMetadata: { + credentialDefinitionMetadata: { didIndyNamespace: pool.didIndyNamespace, }, + resolutionMetadata: {}, } } catch (error) { agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`, { @@ -265,6 +267,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { reason: 'no didIndyNamespace defined in the options. didIndyNamespace is required when using the Indy SDK', credentialDefinition: options.credentialDefinition, state: 'failed', + credentialDefinitionId: null, }, } } @@ -280,25 +283,29 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) // TODO: this will bypass caching if done on a higher level. - const { schema, resolutionMetadata } = await this.getSchema(agentContext, options.credentialDefinition.schemaId) + const { schema, schemaMetadata, resolutionMetadata } = await this.getSchema( + agentContext, + options.credentialDefinition.schemaId + ) - if (!schema || !resolutionMetadata.indyLedgerSeqNo || typeof resolutionMetadata.indyLedgerSeqNo !== 'number') { + if (!schema || !schemaMetadata.indyLedgerSeqNo || typeof schemaMetadata.indyLedgerSeqNo !== 'number') { return { - registrationMetadata: { + registrationMetadata: {}, + credentialDefinitionMetadata: { didIndyNamespace: pool.didIndyNamespace, }, - credentialDefinitionMetadata: {}, credentialDefinitionState: { credentialDefinition: options.credentialDefinition, state: 'failed', reason: `error resolving schema with id ${options.credentialDefinition.schemaId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, + credentialDefinitionId: null, }, } } const credentialDefinitionId = getLegacyCredentialDefinitionId( options.credentialDefinition.issuerId, - resolutionMetadata.indyLedgerSeqNo, + schemaMetadata.indyLedgerSeqNo, options.credentialDefinition.tag ) @@ -327,15 +334,15 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) return { - credentialDefinitionMetadata: {}, + credentialDefinitionMetadata: { + didIndyNamespace: pool.didIndyNamespace, + }, credentialDefinitionState: { credentialDefinition: options.credentialDefinition, credentialDefinitionId, state: 'finished', }, - registrationMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, + registrationMetadata: {}, } } catch (error) { agentContext.config.logger.error( @@ -388,13 +395,12 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) return { - resolutionMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, + resolutionMetadata: {}, revocationRegistryDefinition: anonCredsRevocationRegistryDefinitionFromIndySdk(revocationRegistryDefinition), revocationRegistryDefinitionId, revocationRegistryDefinitionMetadata: { issuanceType: revocationRegistryDefinition.value.issuanceType, + didIndyNamespace: pool.didIndyNamespace, }, } } catch (error) { @@ -469,10 +475,11 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) { return { resolutionMetadata: { - didIndyNamespace: pool.didIndyNamespace, error: `error resolving revocation registry definition with id ${revocationRegistryId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, }, - revocationListMetadata: {}, + revocationListMetadata: { + didIndyNamespace: pool.didIndyNamespace, + }, revocationList: null, } } @@ -480,9 +487,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { const isIssuanceByDefault = revocationRegistryDefinitionMetadata.issuanceType === 'ISSUANCE_BY_DEFAULT' return { - resolutionMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, + resolutionMetadata: {}, revocationList: anonCredsRevocationListFromIndySdk( revocationRegistryId, revocationRegistryDefinition, @@ -490,7 +495,9 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { deltaTimestamp, isIssuanceByDefault ), - revocationListMetadata: {}, + revocationListMetadata: { + didIndyNamespace: pool.didIndyNamespace, + }, } } catch (error) { agentContext.config.logger.error( diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts index 49b619332d..f9cb3bc22c 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts @@ -4,12 +4,13 @@ import type { CreateCredentialRequestOptions, CreateCredentialRequestReturn, CreateProofOptions, - CredentialInfo, + AnonCredsCredentialInfo, GetCredentialOptions, StoreCredentialOptions, GetCredentialsForProofRequestOptions, GetCredentialsForProofRequestReturn, - RequestedCredentials, + AnonCredsRequestedCredentials, + AnonCredsCredentialRequestMetadata, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { @@ -18,6 +19,7 @@ import type { RevStates, Schemas, IndyCredential as IndySdkCredential, + CredReqMetadata, } from 'indy-sdk' import { inject } from '@aries-framework/core' @@ -26,6 +28,7 @@ import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' import { getIndySeqNoFromUnqualifiedCredentialDefinitionId } from '../utils/identifiers' +import { generateLegacyProverDidLikeString } from '../utils/proverDid' import { indySdkCredentialDefinitionFromAnonCreds, indySdkRevocationRegistryDefinitionFromAnonCreds, @@ -122,7 +125,8 @@ export class IndySdkHolderService implements AnonCredsHolderService { return await this.indySdk.proverStoreCredential( agentContext.wallet.handle, options.credentialId ?? null, - options.credentialRequestMetadata, + // The type is typed as a Record in the indy-sdk, but the anoncreds package contains the correct type + options.credentialRequestMetadata as unknown as CredReqMetadata, options.credential, indySdkCredentialDefinitionFromAnonCreds(options.credentialDefinitionId, options.credentialDefinition), indyRevocationRegistryDefinition @@ -136,7 +140,10 @@ export class IndySdkHolderService implements AnonCredsHolderService { } } - public async getCredential(agentContext: AgentContext, options: GetCredentialOptions): Promise { + public async getCredential( + agentContext: AgentContext, + options: GetCredentialOptions + ): Promise { assertIndySdkWallet(agentContext.wallet) try { @@ -145,7 +152,7 @@ export class IndySdkHolderService implements AnonCredsHolderService { return { credentialDefinitionId: result.cred_def_id, attributes: result.attrs, - referent: result.referent, + credentialId: result.referent, schemaId: result.schema_id, credentialRevocationId: result.cred_rev_id, revocationRegistryId: result.rev_reg_id, @@ -165,10 +172,14 @@ export class IndySdkHolderService implements AnonCredsHolderService { ): Promise { assertIndySdkWallet(agentContext.wallet) + // We just generate a prover did like string, as it's not used for anything and we don't need + // to prove ownership of the did. It's deprecated in AnonCreds v1, but kept for backwards compatibility + const proverDid = generateLegacyProverDidLikeString() + try { const result = await this.indySdk.proverCreateCredentialReq( agentContext.wallet.handle, - options.holderDid, + proverDid, options.credentialOffer, // NOTE: Is it safe to use the cred_def_id from the offer? I think so. You can't create a request // for a cred def that is not in the offer @@ -180,7 +191,8 @@ export class IndySdkHolderService implements AnonCredsHolderService { return { credentialRequest: result[0], - credentialRequestMetadata: result[1], + // The type is typed as a Record in the indy-sdk, but the anoncreds package contains the correct type + credentialRequestMetadata: result[1] as unknown as AnonCredsCredentialRequestMetadata, } } catch (error) { agentContext.config.logger.error(`Error creating Indy Credential Request`, { @@ -240,7 +252,7 @@ export class IndySdkHolderService implements AnonCredsHolderService { return credentials.map((credential) => ({ credentialInfo: { credentialDefinitionId: credential.cred_info.cred_def_id, - referent: credential.cred_info.referent, + credentialId: credential.cred_info.referent, attributes: credential.cred_info.attrs, schemaId: credential.cred_info.schema_id, revocationRegistryId: credential.cred_info.rev_reg_id, @@ -299,7 +311,7 @@ export class IndySdkHolderService implements AnonCredsHolderService { /** * Converts a public api form of {@link RequestedCredentials} interface into a format {@link Indy.IndyRequestedCredentials} that Indy SDK expects. **/ - private parseRequestedCredentials(requestedCredentials: RequestedCredentials): IndyRequestedCredentials { + private parseRequestedCredentials(requestedCredentials: AnonCredsRequestedCredentials): IndyRequestedCredentials { const indyRequestedCredentials: IndyRequestedCredentials = { requested_attributes: {}, requested_predicates: {}, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index f877be4f75..96e9ef266a 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -1,5 +1,4 @@ import type { CreateCredentialDefinitionMetadata } from './IndySdkIssuerServiceMetadata' -import type { IndySdkUtilitiesService } from './IndySdkUtilitiesService' import type { AnonCredsIssuerService, CreateCredentialDefinitionOptions, @@ -18,15 +17,15 @@ import { AriesFrameworkError, inject } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' +import { generateLegacyProverDidLikeString } from '../utils/proverDid' +import { createTailsReader } from '../utils/tails' import { indySdkSchemaFromAnonCreds } from '../utils/transform' export class IndySdkIssuerService implements AnonCredsIssuerService { private indySdk: IndySdk - private IndySdkUtilitiesService: IndySdkUtilitiesService - public constructor(IndySdkUtilitiesService: IndySdkUtilitiesService, @inject(IndySdkSymbol) indySdk: IndySdk) { + public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { this.indySdk = indySdk - this.IndySdkUtilitiesService = IndySdkUtilitiesService } public async createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise { @@ -73,7 +72,7 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { return { issuerId, tag: credentialDefinition.tag, - schemaId: credentialDefinition.schemaId, + schemaId, type: 'CL', value: credentialDefinition.value, } @@ -103,16 +102,19 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { assertIndySdkWallet(agentContext.wallet) try { // Indy SDK requires tailsReaderHandle. Use null if no tailsFilePath is present - const tailsReaderHandle = tailsFilePath ? await this.IndySdkUtilitiesService.createTailsReader(tailsFilePath) : 0 + const tailsReaderHandle = tailsFilePath ? await createTailsReader(agentContext, tailsFilePath) : 0 if (revocationRegistryId || tailsFilePath) { throw new AriesFrameworkError('Revocation not supported yet') } + // prover_did is deprecated and thus if not provided we generate something on our side, as it's still required by the indy sdk + const proverDid = credentialRequest.prover_did ?? generateLegacyProverDidLikeString() + const [credential, credentialRevocationId] = await this.indySdk.issuerCreateCredential( agentContext.wallet.handle, credentialOffer, - credentialRequest, + { ...credentialRequest, prover_did: proverDid }, credentialValues, revocationRegistryId ?? null, tailsReaderHandle diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts index 0ed637a6ee..4f7eb6ef42 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts @@ -2,9 +2,9 @@ import type { AnonCredsRevocationRegistryDefinition, AnonCredsRevocationList, AnonCredsProofRequest, - RequestedCredentials, - CredentialInfo, - NonRevokedInterval, + AnonCredsRequestedCredentials, + AnonCredsCredentialInfo, + AnonCredsNonRevokedInterval, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { RevStates } from 'indy-sdk' @@ -13,13 +13,12 @@ import { AriesFrameworkError, inject, injectable } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' +import { createTailsReader } from '../utils/tails' import { indySdkRevocationDeltaFromAnonCreds, indySdkRevocationRegistryDefinitionFromAnonCreds, } from '../utils/transform' -import { IndySdkUtilitiesService } from './IndySdkUtilitiesService' - enum RequestReferentType { Attribute = 'attribute', Predicate = 'predicate', @@ -34,11 +33,9 @@ enum RequestReferentType { @injectable() export class IndySdkRevocationService { private indySdk: IndySdk - private indySdkUtilitiesService: IndySdkUtilitiesService - public constructor(indyUtilitiesService: IndySdkUtilitiesService, @inject(IndySdkSymbol) indySdk: IndySdk) { + public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { this.indySdk = indySdk - this.indySdkUtilitiesService = indyUtilitiesService } /** @@ -47,7 +44,7 @@ export class IndySdkRevocationService { public async createRevocationState( agentContext: AgentContext, proofRequest: AnonCredsProofRequest, - requestedCredentials: RequestedCredentials, + requestedCredentials: AnonCredsRequestedCredentials, revocationRegistries: { [revocationRegistryDefinitionId: string]: { // Tails is already downloaded @@ -68,8 +65,8 @@ export class IndySdkRevocationService { const referentCredentials: Array<{ type: RequestReferentType referent: string - credentialInfo: CredentialInfo - referentRevocationInterval: NonRevokedInterval | undefined + credentialInfo: AnonCredsCredentialInfo + referentRevocationInterval: AnonCredsNonRevokedInterval | undefined }> = [] //Retrieve information for referents and push to single array @@ -114,7 +111,7 @@ export class IndySdkRevocationService { // most accurate revocation list for a given timestamp. It doesn't have to be that the revocationList is from the `to` timestamp however. const revocationList = revocationLists[requestRevocationInterval.to] - const tails = await this.indySdkUtilitiesService.createTailsReader(tailsFilePath) + const tails = await createTailsReader(agentContext, tailsFilePath) const revocationState = await this.indySdk.createRevocationState( tails, @@ -152,7 +149,7 @@ export class IndySdkRevocationService { // TODO: we should do this verification on a higher level I think? // Check revocation interval in accordance with https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0441-present-proof-best-practices/README.md#semantics-of-non-revocation-interval-endpoints private assertRevocationInterval( - revocationInterval: NonRevokedInterval + revocationInterval: AnonCredsNonRevokedInterval ): asserts revocationInterval is BestPracticeNonRevokedInterval { if (!revocationInterval.to) { throw new AriesFrameworkError(`Presentation requests proof of non-revocation with no 'to' value specified`) diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkUtilitiesService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkUtilitiesService.ts deleted file mode 100644 index 1ac0dec33e..0000000000 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkUtilitiesService.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { BlobReaderHandle } from 'indy-sdk' - -import { - AriesFrameworkError, - FileSystem, - getDirFromFilePath, - IndySdkError, - InjectionSymbols, - Logger, -} from '@aries-framework/core' -import { inject, injectable } from 'tsyringe' - -import { isIndyError } from '../../error' -import { IndySdk, IndySdkSymbol } from '../../types' - -@injectable() -export class IndySdkUtilitiesService { - private indySdk: IndySdk - private logger: Logger - private fileSystem: FileSystem - - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - @inject(InjectionSymbols.FileSystem) fileSystem: FileSystem, - @inject(IndySdkSymbol) indySdk: IndySdk - ) { - this.indySdk = indySdk - this.logger = logger - this.fileSystem = fileSystem - } - - /** - * Get a handler for the blob storage tails file reader. - * - * @param tailsFilePath The path of the tails file - * @returns The blob storage reader handle - */ - public async createTailsReader(tailsFilePath: string): Promise { - try { - this.logger.debug(`Opening tails reader at path ${tailsFilePath}`) - const tailsFileExists = await this.fileSystem.exists(tailsFilePath) - - // Extract directory from path (should also work with windows paths) - const dirname = getDirFromFilePath(tailsFilePath) - - if (!tailsFileExists) { - throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) - } - - const tailsReaderConfig = { - base_dir: dirname, - } - - const tailsReader = await this.indySdk.openBlobStorageReader('default', tailsReaderConfig) - this.logger.debug(`Opened tails reader at path ${tailsFilePath}`) - return tailsReader - } catch (error) { - if (isIndyError(error)) { - throw new IndySdkError(error) - } - - throw error - } - } -} diff --git a/packages/indy-sdk/src/anoncreds/utils/proverDid.ts b/packages/indy-sdk/src/anoncreds/utils/proverDid.ts new file mode 100644 index 0000000000..2d12648c70 --- /dev/null +++ b/packages/indy-sdk/src/anoncreds/utils/proverDid.ts @@ -0,0 +1,12 @@ +import { TypedArrayEncoder, utils } from '@aries-framework/core' + +/** + * generates a string that adheres to the format of a legacy indy did. + * + * This can be used for the `prover_did` property that is required in the legacy anoncreds credential + * request. This doesn't actually have to be a did, but some frameworks (like ACA-Py) require it to be + * an unqualified indy did. + */ +export function generateLegacyProverDidLikeString() { + return TypedArrayEncoder.toBase58(TypedArrayEncoder.fromString(utils.uuid()).slice(0, 16)) +} diff --git a/packages/indy-sdk/src/anoncreds/utils/tails.ts b/packages/indy-sdk/src/anoncreds/utils/tails.ts new file mode 100644 index 0000000000..f803ea5d78 --- /dev/null +++ b/packages/indy-sdk/src/anoncreds/utils/tails.ts @@ -0,0 +1,45 @@ +import type { IndySdk } from '../../types' +import type { AgentContext, FileSystem } from '@aries-framework/core' + +import { AriesFrameworkError, getDirFromFilePath, IndySdkError, InjectionSymbols } from '@aries-framework/core' + +import { isIndyError } from '../../error' +import { IndySdkSymbol } from '../../types' + +/** + * Get a handler for the blob storage tails file reader. + * + * @param agentContext The agent context + * @param tailsFilePath The path of the tails file + * @returns The blob storage reader handle + */ +export async function createTailsReader(agentContext: AgentContext, tailsFilePath: string) { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) + + try { + agentContext.config.logger.debug(`Opening tails reader at path ${tailsFilePath}`) + const tailsFileExists = await fileSystem.exists(tailsFilePath) + + // Extract directory from path (should also work with windows paths) + const dirname = getDirFromFilePath(tailsFilePath) + + if (!tailsFileExists) { + throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) + } + + const tailsReaderConfig = { + base_dir: dirname, + } + + const tailsReader = await indySdk.openBlobStorageReader('default', tailsReaderConfig) + agentContext.config.logger.debug(`Opened tails reader at path ${tailsFilePath}`) + return tailsReader + } catch (error) { + if (isIndyError(error)) { + throw new IndySdkError(error) + } + + throw error + } +} diff --git a/yarn.lock b/yarn.lock index 359ab25c51..9ff249988b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,50 +10,6 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@aries-framework/core@file:packages/core": - version "0.3.2" - dependencies: - "@digitalcredentials/jsonld" "^5.2.1" - "@digitalcredentials/jsonld-signatures" "^9.3.1" - "@digitalcredentials/vc" "^1.1.2" - "@multiformats/base-x" "^4.0.1" - "@stablelib/ed25519" "^1.0.2" - "@stablelib/random" "^1.0.1" - "@stablelib/sha256" "^1.0.1" - "@types/indy-sdk" "1.16.24" - "@types/node-fetch" "^2.5.10" - "@types/ws" "^7.4.6" - abort-controller "^3.0.0" - bn.js "^5.2.0" - borc "^3.0.0" - buffer "^6.0.3" - class-transformer "0.5.1" - class-validator "0.13.1" - did-resolver "^3.1.3" - lru_map "^0.4.1" - luxon "^1.27.0" - make-error "^1.3.6" - object-inspect "^1.10.3" - query-string "^7.0.1" - reflect-metadata "^0.1.13" - rxjs "^7.2.0" - tsyringe "^4.7.0" - uuid "^8.3.2" - varint "^6.0.0" - web-did-resolver "^2.0.8" - -"@aries-framework/node@file:packages/node": - version "0.3.2" - dependencies: - "@aries-framework/core" "0.3.2" - "@types/express" "^4.17.15" - express "^4.17.1" - ffi-napi "^4.0.3" - indy-sdk "^1.16.0-dev-1636" - node-fetch "^2.6.1" - ref-napi "^3.0.3" - ws "^7.5.3" - "@azure/core-asynciterator-polyfill@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz#0dd3849fb8d97f062a39db0e5cadc9ffaf861fec" @@ -3500,7 +3456,7 @@ bindings@^1.3.1: dependencies: file-uri-to-path "1.0.0" -bn.js@^5.2.0: +bn.js@^5.2.0, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== From 60206e196506c91b029ea2ea6ff92f26707b6080 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 23 Jan 2023 13:03:22 +0100 Subject: [PATCH 2/8] feat: structure for anoncreds api Signed-off-by: Timo Glastra --- packages/anoncreds/src/AnonCredsApi.ts | 254 ++++++++++++++++++ packages/anoncreds/src/AnonCredsModule.ts | 8 + .../src/error/AnonCredsStoreRecordError.ts | 7 + packages/anoncreds/src/error/index.ts | 1 + packages/anoncreds/src/index.ts | 1 + .../AnonCredsCredentialDefinitionRecord.ts | 50 ++++ ...AnonCredsCredentialDefinitionRepository.ts | 23 ++ .../src/repository/AnonCredsSchemaRecord.ts | 50 ++++ .../repository/AnonCredsSchemaRepository.ts | 23 ++ ...CredentialDefinitionRecordMetadataTypes.ts | 11 + .../anonCredsSchemaRecordMetadataTypes.ts | 11 + 11 files changed, 439 insertions(+) create mode 100644 packages/anoncreds/src/AnonCredsApi.ts create mode 100644 packages/anoncreds/src/error/AnonCredsStoreRecordError.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts create mode 100644 packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts create mode 100644 packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts new file mode 100644 index 0000000000..351c593210 --- /dev/null +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -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 { + 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 { + 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 { + const registry = this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, credentialDefinitionId) + + const result = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId) + return result + } + + public async registerCredentialDefinition(options: { + credentialDefinition: Omit + // TODO: options should support supportsRevocation at some points + options: Extensible + }): Promise { + 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 { + 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 { + 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 { + // 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 { + try { + // If we have both the schema and the schemaId we will store a copy of the schema. We may need to handle an + // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel + if (result.schemaState.schema && result.schemaState.schemaId) { + const schemaRecord = new AnonCredsSchemaRecord({ + schemaId: result.schemaState.schemaId, + schema: result.schemaState.schema, + }) + + await this.anonCredsSchemaRepository.save(this.agentContext, schemaRecord) + } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing schema record`, { cause: error }) + } + } + + private findRegistryForIdentifier(identifier: string) { + try { + return this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, identifier) + } catch { + return null + } + } +} diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index 0da6e242f7..490247224a 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -1,7 +1,10 @@ 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' /** @@ -9,6 +12,7 @@ import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryS */ export class AnonCredsModule implements Module { public readonly config: AnonCredsModuleConfig + public api = AnonCredsApi public constructor(config: AnonCredsModuleConfigOptions) { this.config = new AnonCredsModuleConfig(config) @@ -19,5 +23,9 @@ export class AnonCredsModule implements Module { dependencyManager.registerInstance(AnonCredsModuleConfig, this.config) dependencyManager.registerSingleton(AnonCredsRegistryService) + + // Repositories + dependencyManager.registerSingleton(AnonCredsSchemaRepository) + dependencyManager.registerSingleton(AnonCredsCredentialDefinitionRepository) } } diff --git a/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts b/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts new file mode 100644 index 0000000000..11437d7b64 --- /dev/null +++ b/packages/anoncreds/src/error/AnonCredsStoreRecordError.ts @@ -0,0 +1,7 @@ +import { AnonCredsError } from './AnonCredsError' + +export class AnonCredsStoreRecordError extends AnonCredsError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/anoncreds/src/error/index.ts b/packages/anoncreds/src/error/index.ts index d9786950bf..6d25bc4dbb 100644 --- a/packages/anoncreds/src/error/index.ts +++ b/packages/anoncreds/src/error/index.ts @@ -1 +1,2 @@ export * from './AnonCredsError' +export * from './AnonCredsStoreRecordError' diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index 759e343c2c..3db43c0f2b 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -3,3 +3,4 @@ export * from './services' export * from './error' export { AnonCredsModule } from './AnonCredsModule' export { AnonCredsModuleConfig, AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' +export { AnonCredsApi } from './AnonCredsApi' diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts new file mode 100644 index 0000000000..6bbf78e357 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts @@ -0,0 +1,50 @@ +import type { AnonCredsCredentialDefinition } from '../models' +import type { AnonCredsCredentialDefinitionRecordMetadata } from './anonCredsCredentialDefinitionRecordMetadataTypes' +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsCredentialDefinitionRecordProps { + id?: string + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition +} + +export type DefaultAnonCredsCredentialDefinitionTags = { + schemaId: string + credentialDefinitionId: string + issuerId: string + tag: string +} + +export class AnonCredsCredentialDefinitionRecord extends BaseRecord< + DefaultAnonCredsCredentialDefinitionTags, + TagsBase, + AnonCredsCredentialDefinitionRecordMetadata +> { + public static readonly type = 'AnonCredsCredentialDefinitionRecord' + public readonly type = AnonCredsCredentialDefinitionRecord.type + + public readonly credentialDefinitionId!: string + public readonly credentialDefinition!: AnonCredsCredentialDefinition + + public constructor(props: AnonCredsCredentialDefinitionRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.credentialDefinition = props.credentialDefinition + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + schemaId: this.credentialDefinition.schemaId, + issuerId: this.credentialDefinition.issuerId, + tag: this.credentialDefinition.tag, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts new file mode 100644 index 0000000000..7677dd76b8 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsCredentialDefinitionRecord } from './AnonCredsCredentialDefinitionRecord' + +@injectable() +export class AnonCredsCredentialDefinitionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsCredentialDefinitionRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts b/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts new file mode 100644 index 0000000000..c2ea988e18 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts @@ -0,0 +1,50 @@ +import type { AnonCredsSchema } from '../models' +import type { AnonCredsSchemaRecordMetadata } from './anonCredsSchemaRecordMetadataTypes' +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsSchemaRecordProps { + id?: string + schemaId: string + schema: AnonCredsSchema +} + +export type DefaultAnonCredsSchemaTags = { + schemaId: string + issuerId: string + schemaName: string + schemaVersion: string +} + +export class AnonCredsSchemaRecord extends BaseRecord< + DefaultAnonCredsSchemaTags, + TagsBase, + AnonCredsSchemaRecordMetadata +> { + public static readonly type = 'AnonCredsSchemaRecord' + public readonly type = AnonCredsSchemaRecord.type + + public readonly schemaId!: string + public readonly schema!: AnonCredsSchema + + public constructor(props: AnonCredsSchemaRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.schema = props.schema + this.schemaId = props.schemaId + } + } + + public getTags() { + return { + ...this._tags, + schemaId: this.schemaId, + issuerId: this.schema.issuerId, + schemaName: this.schema.name, + schemaVersion: this.schema.version, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts b/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts new file mode 100644 index 0000000000..0d0ab84b9f --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsSchemaRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, inject, injectable } from '@aries-framework/core' + +import { AnonCredsSchemaRecord } from './AnonCredsSchemaRecord' + +@injectable() +export class AnonCredsSchemaRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsSchemaRecord, storageService, eventEmitter) + } + + public async getBySchemaId(agentContext: AgentContext, schemaId: string) { + return this.getSingleByQuery(agentContext, { schemaId: schemaId }) + } + + public async findBySchemaId(agentContext: AgentContext, schemaId: string) { + return await this.findSingleByQuery(agentContext, { schemaId: schemaId }) + } +} diff --git a/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts b/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts new file mode 100644 index 0000000000..05806802e4 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsCredentialDefinitionRecordMetadataTypes.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsCredentialDefinitionRecordMetadataKeys { + CredentialDefinitionRegistrationMetadata = '_internal/anonCredsCredentialDefinitionRegistrationMetadata', + CredentialDefinitionMetadata = '_internal/anonCredsCredentialDefinitionMetadata', +} + +export type AnonCredsCredentialDefinitionRecordMetadata = { + [AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata]: Extensible + [AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata]: Extensible +} diff --git a/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts b/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts new file mode 100644 index 0000000000..9880a50625 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsSchemaRecordMetadataTypes.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsSchemaRecordMetadataKeys { + SchemaRegistrationMetadata = '_internal/anonCredsSchemaRegistrationMetadata', + SchemaMetadata = '_internal/anonCredsSchemaMetadata', +} + +export type AnonCredsSchemaRecordMetadata = { + [AnonCredsSchemaRecordMetadataKeys.SchemaRegistrationMetadata]: Extensible + [AnonCredsSchemaRecordMetadataKeys.SchemaMetadata]: Extensible +} From dec100b09d7c6bce994d895bb5a3df1424bf4da0 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 30 Jan 2023 19:22:57 -0300 Subject: [PATCH 3/8] feat: add records and methods for master secret and private part of credential defs Signed-off-by: Ariel Gentile --- packages/anoncreds/package.json | 6 +- packages/anoncreds/src/AnonCredsApi.ts | 103 ++++++++++++++++-- packages/anoncreds/src/AnonCredsApiOptions.ts | 4 + packages/anoncreds/src/AnonCredsModule.ts | 8 ++ packages/anoncreds/src/index.ts | 3 + ...nCredsCredentialDefinitionPrivateRecord.ts | 41 +++++++ ...dsCredentialDefinitionPrivateRepository.ts | 23 ++++ .../AnonCredsCredentialDefinitionRecord.ts | 2 +- .../AnonCredsKeyCorrectnessProofRecord.ts | 41 +++++++ .../AnonCredsKeyCorrectnessProofRepository.ts | 23 ++++ .../repository/AnonCredsMasterSecretRecord.ts | 39 +++++++ .../AnonCredsMasterSecretRepository.ts | 31 ++++++ .../src/repository/AnonCredsSchemaRecord.ts | 2 +- packages/anoncreds/src/repository/index.ts | 10 ++ .../src/services/AnonCredsHolderService.ts | 4 + .../services/AnonCredsHolderServiceOptions.ts | 10 ++ .../src/services/AnonCredsIssuerService.ts | 5 +- .../services/AnonCredsIssuerServiceOptions.ts | 8 +- 18 files changed, 344 insertions(+), 19 deletions(-) create mode 100644 packages/anoncreds/src/AnonCredsApiOptions.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsMasterSecretRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsMasterSecretRepository.ts create mode 100644 packages/anoncreds/src/repository/index.ts diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index edbfbbeca8..25ade73e3d 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -2,7 +2,7 @@ "name": "@aries-framework/anoncreds", "main": "build/index", "types": "build/index", - "version": "0.3.2", + "version": "0.3.3", "files": [ "build" ], @@ -25,8 +25,8 @@ "test": "jest" }, "dependencies": { - "@aries-framework/core": "0.3.2", - "@aries-framework/node": "^0.3.2", + "@aries-framework/core": "0.3.3", + "@aries-framework/node": "^0.3.3", "bn.js": "^5.2.1" }, "devDependencies": { diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 351c593210..b6b94c489b 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -1,3 +1,4 @@ +import type { AnonCredsCreateMasterSecretOptions } from './AnonCredsApiOptions' import type { AnonCredsCredentialDefinition } from './models' import type { GetCredentialDefinitionReturn, @@ -14,12 +15,20 @@ import { AgentContext, injectable } from '@aries-framework/core' import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' import { AnonCredsStoreRecordError } from './error' +import { + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsMasterSecretRecord, + AnonCredsMasterSecretRepository, +} from './repository' import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRecord } from './repository/AnonCredsSchemaRecord' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' import { AnonCredsCredentialDefinitionRecordMetadataKeys } from './repository/anonCredsCredentialDefinitionRecordMetadataTypes' -import { AnonCredsIssuerService } from './services' +import { AnonCredsIssuerService, AnonCredsHolderService } from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' @injectable() @@ -30,24 +39,71 @@ export class AnonCredsApi { private anonCredsRegistryService: AnonCredsRegistryService private anonCredsSchemaRepository: AnonCredsSchemaRepository private anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository + private anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository + private anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository + private anonCredsMasterSecretRepository: AnonCredsMasterSecretRepository // TODO: how do we inject the anoncreds services? private anonCredsIssuerService: AnonCredsIssuerService + private anonCredsHolderService: AnonCredsHolderService public constructor( agentContext: AgentContext, anonCredsRegistryService: AnonCredsRegistryService, config: AnonCredsModuleConfig, anonCredsIssuerService: AnonCredsIssuerService, + anonCredsHolderService: AnonCredsHolderService, anonCredsSchemaRepository: AnonCredsSchemaRepository, - anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository + anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository, + anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository, + anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository, + anonCredsMasterSecretRepository: AnonCredsMasterSecretRepository ) { this.agentContext = agentContext this.anonCredsRegistryService = anonCredsRegistryService this.config = config this.anonCredsIssuerService = anonCredsIssuerService + this.anonCredsHolderService = anonCredsHolderService this.anonCredsSchemaRepository = anonCredsSchemaRepository this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository + this.anonCredsCredentialDefinitionPrivateRepository = anonCredsCredentialDefinitionPrivateRepository + this.anonCredsKeyCorrectnessProofRepository = anonCredsKeyCorrectnessProofRepository + this.anonCredsMasterSecretRepository = anonCredsMasterSecretRepository + } + + /** + * Create a Master Secret, optionally indicating its ID and if it will be the default one + * If there is no other Master Secret, this will be the default one. + * + */ + public async createMasterSecret(options?: AnonCredsCreateMasterSecretOptions) { + const { masterSecretId, masterSecretValue } = await this.anonCredsHolderService.createMasterSecret( + this.agentContext, + { + masterSecretId: options?.masterSecretId, + } + ) + + // TODO: If no value is stored (indy-sdk case), should it still be saved in order to retrieve wallet's default master secret id? + if (masterSecretValue) { + const masterSecretRecord = new AnonCredsMasterSecretRecord({ masterSecretId, value: masterSecretValue }) + + // If it is the first master secret registered, set as default + const allRecords = await this.anonCredsMasterSecretRepository.getAll(this.agentContext) + if (allRecords.length === 0 || options?.setAsDefault) { + masterSecretRecord.setTag('default', true) + } + + // Update any other record if this one is set as default + if (options?.setAsDefault) { + for (const record of allRecords) { + record.setTag('default', false) + await this.anonCredsMasterSecretRepository.update(this.agentContext, record) + } + } + + await this.anonCredsMasterSecretRepository.save(this.agentContext, masterSecretRecord) + } } /** @@ -148,20 +204,21 @@ export class AnonCredsApi { } } - 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 { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await this.anonCredsIssuerService.createCredentialDefinition(this.agentContext, { + issuerId: options.credentialDefinition.issuerId, + schemaId: options.credentialDefinition.schemaId, + tag: options.credentialDefinition.tag, + supportRevocation: false, + schema: schemaResult.schema, + }) const result = await registry.registerCredentialDefinition(this.agentContext, { credentialDefinition, options: options.options, }) - await this.storeCredentialDefinitionRecord(result) + await this.storeCredentialDefinitionRecord(result, credentialDefinitionPrivate, keyCorrectnessProof) return result } @@ -199,7 +256,11 @@ export class AnonCredsApi { return result } - private async storeCredentialDefinitionRecord(result: RegisterCredentialDefinitionReturn): Promise { + private async storeCredentialDefinitionRecord( + result: RegisterCredentialDefinitionReturn, + credentialDefinitionPrivate?: Record, + keyCorrectnessProof?: Record + ): Promise { // 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 ( @@ -224,6 +285,26 @@ export class AnonCredsApi { ) 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) + } } } diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts new file mode 100644 index 0000000000..fc219c36cd --- /dev/null +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -0,0 +1,4 @@ +export interface AnonCredsCreateMasterSecretOptions { + masterSecretId?: string + setAsDefault?: boolean +} diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index 490247224a..06b763b3fa 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -3,6 +3,11 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AnonCredsApi } from './AnonCredsApi' import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' +import { + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsMasterSecretRepository, +} from './repository' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' @@ -27,5 +32,8 @@ export class AnonCredsModule implements Module { // Repositories dependencyManager.registerSingleton(AnonCredsSchemaRepository) dependencyManager.registerSingleton(AnonCredsCredentialDefinitionRepository) + dependencyManager.registerSingleton(AnonCredsCredentialDefinitionPrivateRepository) + dependencyManager.registerSingleton(AnonCredsKeyCorrectnessProofRepository) + dependencyManager.registerSingleton(AnonCredsMasterSecretRepository) } } diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index 3db43c0f2b..4eba2a2931 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -1,6 +1,9 @@ export * from './models' export * from './services' export * from './error' +export * from './repository' export { AnonCredsModule } from './AnonCredsModule' export { AnonCredsModuleConfig, AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' export { AnonCredsApi } from './AnonCredsApi' +export { AnonCredsCredentialFormatService } from './formats/AnonCredsCredentialFormatService' +export { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts new file mode 100644 index 0000000000..bc0c1c99ee --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRecord.ts @@ -0,0 +1,41 @@ +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsCredentialDefinitionPrivateRecordProps { + id?: string + credentialDefinitionId: string + value: Record +} + +export type DefaultAnonCredsCredentialDefinitionPrivateTags = { + credentialDefinitionId: string +} + +export class AnonCredsCredentialDefinitionPrivateRecord extends BaseRecord< + DefaultAnonCredsCredentialDefinitionPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsCredentialDefinitionPrivateRecord' + public readonly type = AnonCredsCredentialDefinitionPrivateRecord.type + + public readonly credentialDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public constructor(props: AnonCredsCredentialDefinitionPrivateRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.value = props.value + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts new file mode 100644 index 0000000000..31c7737143 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionPrivateRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsCredentialDefinitionPrivateRecord } from './AnonCredsCredentialDefinitionPrivateRecord' + +@injectable() +export class AnonCredsCredentialDefinitionPrivateRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsCredentialDefinitionPrivateRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts index 6bbf78e357..f9c7df43f7 100644 --- a/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsCredentialDefinitionRecord.ts @@ -1,5 +1,5 @@ -import type { AnonCredsCredentialDefinition } from '../models' import type { AnonCredsCredentialDefinitionRecordMetadata } from './anonCredsCredentialDefinitionRecordMetadataTypes' +import type { AnonCredsCredentialDefinition } from '../models' import type { TagsBase } from '@aries-framework/core' import { BaseRecord, utils } from '@aries-framework/core' diff --git a/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts new file mode 100644 index 0000000000..cac331bd6c --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRecord.ts @@ -0,0 +1,41 @@ +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsKeyCorrectnessProofRecordProps { + id?: string + credentialDefinitionId: string + value: Record +} + +export type DefaultAnonCredsKeyCorrectnessProofPrivateTags = { + credentialDefinitionId: string +} + +export class AnonCredsKeyCorrectnessProofRecord extends BaseRecord< + DefaultAnonCredsKeyCorrectnessProofPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsKeyCorrectnessProofRecord' + public readonly type = AnonCredsKeyCorrectnessProofRecord.type + + public readonly credentialDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public constructor(props: AnonCredsKeyCorrectnessProofRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.credentialDefinitionId = props.credentialDefinitionId + this.value = props.value + } + } + + public getTags() { + return { + ...this._tags, + credentialDefinitionId: this.credentialDefinitionId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts new file mode 100644 index 0000000000..959ba8b4a5 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsKeyCorrectnessProofRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsKeyCorrectnessProofRecord } from './AnonCredsKeyCorrectnessProofRecord' + +@injectable() +export class AnonCredsKeyCorrectnessProofRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsKeyCorrectnessProofRecord, storageService, eventEmitter) + } + + public async getByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.getSingleByQuery(agentContext, { credentialDefinitionId }) + } + + public async findByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findSingleByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsMasterSecretRecord.ts b/packages/anoncreds/src/repository/AnonCredsMasterSecretRecord.ts new file mode 100644 index 0000000000..bad305ff4d --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsMasterSecretRecord.ts @@ -0,0 +1,39 @@ +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsMasterSecretRecordProps { + id?: string + masterSecretId: string + value?: string // If value is not provided, only reference to master secret is stored in regular storage +} + +export type DefaultAnonCredsMasterSecretTags = { + default?: boolean + masterSecretId: string +} + +export class AnonCredsMasterSecretRecord extends BaseRecord { + public static readonly type = 'AnonCredsMasterSecretRecord' + public readonly type = AnonCredsMasterSecretRecord.type + + public readonly masterSecretId!: string + public readonly value?: string + + public constructor(props: AnonCredsMasterSecretRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.masterSecretId = props.masterSecretId + this.value = props.value + } + } + + public getTags() { + return { + ...this._tags, + masterSecretId: this.masterSecretId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsMasterSecretRepository.ts b/packages/anoncreds/src/repository/AnonCredsMasterSecretRepository.ts new file mode 100644 index 0000000000..a40d5384b4 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsMasterSecretRepository.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsMasterSecretRecord } from './AnonCredsMasterSecretRecord' + +@injectable() +export class AnonCredsMasterSecretRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsMasterSecretRecord, storageService, eventEmitter) + } + + public async getDefault(agentContext: AgentContext) { + return this.getSingleByQuery(agentContext, { default: true }) + } + + public async findDefault(agentContext: AgentContext) { + return this.findSingleByQuery(agentContext, { default: true }) + } + + public async getByMasterSecretId(agentContext: AgentContext, masterSecretId: string) { + return this.getSingleByQuery(agentContext, { masterSecretId }) + } + + public async findByMasterSecretId(agentContext: AgentContext, masterSecretId: string) { + return this.findSingleByQuery(agentContext, { masterSecretId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts b/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts index c2ea988e18..13ad5d757c 100644 --- a/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts +++ b/packages/anoncreds/src/repository/AnonCredsSchemaRecord.ts @@ -1,5 +1,5 @@ -import type { AnonCredsSchema } from '../models' import type { AnonCredsSchemaRecordMetadata } from './anonCredsSchemaRecordMetadataTypes' +import type { AnonCredsSchema } from '../models' import type { TagsBase } from '@aries-framework/core' import { BaseRecord, utils } from '@aries-framework/core' diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts new file mode 100644 index 0000000000..d93780e2bd --- /dev/null +++ b/packages/anoncreds/src/repository/index.ts @@ -0,0 +1,10 @@ +export * from './AnonCredsCredentialDefinitionRecord' +export * from './AnonCredsCredentialDefinitionRepository' +export * from './AnonCredsCredentialDefinitionPrivateRecord' +export * from './AnonCredsCredentialDefinitionPrivateRepository' +export * from './AnonCredsKeyCorrectnessProofRecord' +export * from './AnonCredsKeyCorrectnessProofRepository' +export * from './AnonCredsMasterSecretRecord' +export * from './AnonCredsMasterSecretRepository' +export * from './AnonCredsSchemaRecord' +export * from './AnonCredsSchemaRepository' diff --git a/packages/anoncreds/src/services/AnonCredsHolderService.ts b/packages/anoncreds/src/services/AnonCredsHolderService.ts index a7c0dcb22e..6aa644d8a4 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderService.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderService.ts @@ -6,6 +6,8 @@ import type { StoreCredentialOptions, GetCredentialsForProofRequestOptions, GetCredentialsForProofRequestReturn, + CreateMasterSecretReturn, + CreateMasterSecretOptions, } from './AnonCredsHolderServiceOptions' import type { AnonCredsCredentialInfo } from '../models' import type { AnonCredsProof } from '../models/exchange' @@ -14,6 +16,8 @@ import type { AgentContext } from '@aries-framework/core' export const AnonCredsHolderServiceSymbol = Symbol('AnonCredsHolderService') export interface AnonCredsHolderService { + createMasterSecret(agentContext: AgentContext, options: CreateMasterSecretOptions): Promise + createProof(agentContext: AgentContext, options: CreateProofOptions): Promise storeCredential( agentContext: AgentContext, diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index 40a52ef567..0514672854 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -76,9 +76,19 @@ export type GetCredentialsForProofRequestReturn = Array<{ export interface CreateCredentialRequestOptions { credentialOffer: AnonCredsCredentialOffer credentialDefinition: AnonCredsCredentialDefinition + masterSecretId?: string } export interface CreateCredentialRequestReturn { credentialRequest: AnonCredsCredentialRequest credentialRequestMetadata: AnonCredsCredentialRequestMetadata } + +export interface CreateMasterSecretOptions { + masterSecretId?: string +} + +export interface CreateMasterSecretReturn { + masterSecretId: string + masterSecretValue?: string +} diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts index 41cb4ebf9f..3090b1759b 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerService.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -4,9 +4,10 @@ import type { CreateCredentialOfferOptions, CreateCredentialReturn, CreateCredentialOptions, + CreateCredentialDefinitionReturn, } from './AnonCredsIssuerServiceOptions' import type { AnonCredsCredentialOffer } from '../models/exchange' -import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../models/registry' +import type { AnonCredsSchema } from '../models/registry' import type { AgentContext } from '@aries-framework/core' export const AnonCredsIssuerServiceSymbol = Symbol('AnonCredsIssuerService') @@ -20,7 +21,7 @@ export interface AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialDefinitionOptions, metadata?: Record - ): Promise + ): Promise createCredentialOffer( agentContext: AgentContext, diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index 58d6cd9048..c7da246b9b 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -4,7 +4,7 @@ import type { AnonCredsCredentialRequest, AnonCredsCredentialValues, } from '../models/exchange' -import type { AnonCredsSchema } from '../models/registry' +import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../models/registry' export interface CreateSchemaOptions { issuerId: string @@ -39,3 +39,9 @@ export interface CreateCredentialReturn { credential: AnonCredsCredential credentialRevocationId?: string } + +export interface CreateCredentialDefinitionReturn { + credentialDefinition: AnonCredsCredentialDefinition + credentialDefinitionPrivate?: Record + keyCorrectnessProof?: Record +} From c137bf57d1b69358a1fd2bd3d16539abb4b60b98 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Fri, 3 Feb 2023 13:56:20 +0100 Subject: [PATCH 4/8] feat: anoncreds api Signed-off-by: Timo Glastra --- packages/anoncreds/package.json | 6 +- packages/anoncreds/src/AnonCredsApi.ts | 212 ++++++++---- packages/anoncreds/src/AnonCredsApiOptions.ts | 4 +- packages/anoncreds/src/AnonCredsModule.ts | 4 +- .../src/__tests__/AnonCredsModule.test.ts | 14 +- .../LegacyIndyCredentialFormatService.test.ts | 2 +- packages/anoncreds/src/index.ts | 2 +- packages/anoncreds/src/models/registry.ts | 2 +- .../repository/AnonCredsLinkSecretRecord.ts | 42 +++ .../AnonCredsLinkSecretRepository.ts | 31 ++ .../repository/AnonCredsMasterSecretRecord.ts | 39 --- .../AnonCredsMasterSecretRepository.ts | 31 -- packages/anoncreds/src/repository/index.ts | 4 +- .../src/services/AnonCredsHolderService.ts | 6 +- .../services/AnonCredsHolderServiceOptions.ts | 16 +- .../AnonCredsVerifierServiceOptions.ts | 6 +- .../services/registry/AnonCredsRegistry.ts | 7 +- .../registry/AnonCredsRegistryService.ts | 2 +- .../registry/CredentialDefinitionOptions.ts | 8 +- .../RevocationRegistryDefinitionOptions.ts | 2 +- ...ions.ts => RevocationStatusListOptions.ts} | 8 +- .../src/services/registry/SchemaOptions.ts | 10 +- .../anoncreds/src/services/registry/index.ts | 2 +- .../tests/InMemoryAnonCredsRegistry.ts | 86 ++++- packages/anoncreds/tests/anoncreds.test.ts | 312 ++++++++++++++++++ packages/indy-sdk/src/IndySdkModule.ts | 18 + .../services/IndySdkAnonCredsRegistry.ts | 25 +- .../services/IndySdkHolderService.ts | 30 +- .../services/IndySdkIssuerService.ts | 19 +- .../services/IndySdkRevocationService.ts | 18 +- .../services/IndySdkVerifierService.ts | 11 +- .../utils/__tests__/transform.test.ts | 2 +- .../indy-sdk/src/anoncreds/utils/transform.ts | 20 +- 33 files changed, 747 insertions(+), 254 deletions(-) create mode 100644 packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts delete mode 100644 packages/anoncreds/src/repository/AnonCredsMasterSecretRecord.ts delete mode 100644 packages/anoncreds/src/repository/AnonCredsMasterSecretRepository.ts rename packages/anoncreds/src/services/registry/{RevocationListOptions.ts => RevocationStatusListOptions.ts} (70%) create mode 100644 packages/anoncreds/tests/anoncreds.test.ts diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 25ade73e3d..8f3059cbe5 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -2,7 +2,7 @@ "name": "@aries-framework/anoncreds", "main": "build/index", "types": "build/index", - "version": "0.3.3", + "version": "0.3.2", "files": [ "build" ], @@ -25,8 +25,8 @@ "test": "jest" }, "dependencies": { - "@aries-framework/core": "0.3.3", - "@aries-framework/node": "^0.3.3", + "@aries-framework/core": "0.3.2", + "@aries-framework/node": "0.3.2", "bn.js": "^5.2.1" }, "devDependencies": { diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index b6b94c489b..12ef5e736d 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -1,8 +1,8 @@ -import type { AnonCredsCreateMasterSecretOptions } from './AnonCredsApiOptions' +import type { AnonCredsCreateLinkSecretOptions } from './AnonCredsApiOptions' import type { AnonCredsCredentialDefinition } from './models' import type { GetCredentialDefinitionReturn, - GetRevocationListReturn, + GetRevocationStatusListReturn, GetRevocationRegistryDefinitionReturn, GetSchemaReturn, RegisterCredentialDefinitionReturn, @@ -11,7 +11,7 @@ import type { } from './services' import type { Extensible } from './services/registry/base' -import { AgentContext, injectable } from '@aries-framework/core' +import { AgentContext, inject, injectable } from '@aries-framework/core' import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' import { AnonCredsStoreRecordError } from './error' @@ -20,15 +20,20 @@ import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRecord, AnonCredsKeyCorrectnessProofRepository, - AnonCredsMasterSecretRecord, - AnonCredsMasterSecretRepository, + AnonCredsLinkSecretRecord, + AnonCredsLinkSecretRepository, } from './repository' import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRecord } from './repository/AnonCredsSchemaRecord' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' import { AnonCredsCredentialDefinitionRecordMetadataKeys } from './repository/anonCredsCredentialDefinitionRecordMetadataTypes' -import { AnonCredsIssuerService, AnonCredsHolderService } from './services' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsIssuerService, + AnonCredsHolderService, +} from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' @injectable() @@ -41,9 +46,7 @@ export class AnonCredsApi { private anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository private anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository private anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository - private anonCredsMasterSecretRepository: AnonCredsMasterSecretRepository - - // TODO: how do we inject the anoncreds services? + private anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository private anonCredsIssuerService: AnonCredsIssuerService private anonCredsHolderService: AnonCredsHolderService @@ -51,13 +54,13 @@ export class AnonCredsApi { agentContext: AgentContext, anonCredsRegistryService: AnonCredsRegistryService, config: AnonCredsModuleConfig, - anonCredsIssuerService: AnonCredsIssuerService, - anonCredsHolderService: AnonCredsHolderService, + @inject(AnonCredsIssuerServiceSymbol) anonCredsIssuerService: AnonCredsIssuerService, + @inject(AnonCredsHolderServiceSymbol) anonCredsHolderService: AnonCredsHolderService, anonCredsSchemaRepository: AnonCredsSchemaRepository, anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository, anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository, anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository, - anonCredsMasterSecretRepository: AnonCredsMasterSecretRepository + anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository ) { this.agentContext = agentContext this.anonCredsRegistryService = anonCredsRegistryService @@ -68,42 +71,44 @@ export class AnonCredsApi { this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository this.anonCredsCredentialDefinitionPrivateRepository = anonCredsCredentialDefinitionPrivateRepository this.anonCredsKeyCorrectnessProofRepository = anonCredsKeyCorrectnessProofRepository - this.anonCredsMasterSecretRepository = anonCredsMasterSecretRepository + this.anonCredsLinkSecretRepository = anonCredsLinkSecretRepository } /** - * Create a Master Secret, optionally indicating its ID and if it will be the default one - * If there is no other Master Secret, this will be the default one. + * Create a Link Secret, optionally indicating its ID and if it will be the default one + * If there is no default Link Secret, this will be set as default (even if setAsDefault is true). * */ - public async createMasterSecret(options?: AnonCredsCreateMasterSecretOptions) { - const { masterSecretId, masterSecretValue } = await this.anonCredsHolderService.createMasterSecret( - this.agentContext, - { - masterSecretId: options?.masterSecretId, - } - ) - - // TODO: If no value is stored (indy-sdk case), should it still be saved in order to retrieve wallet's default master secret id? - if (masterSecretValue) { - const masterSecretRecord = new AnonCredsMasterSecretRecord({ masterSecretId, value: masterSecretValue }) + public async createLinkSecret(options?: AnonCredsCreateLinkSecretOptions) { + const { linkSecretId, linkSecretValue } = await this.anonCredsHolderService.createLinkSecret(this.agentContext, { + linkSecretId: options?.linkSecretId, + }) - // If it is the first master secret registered, set as default - const allRecords = await this.anonCredsMasterSecretRepository.getAll(this.agentContext) - if (allRecords.length === 0 || options?.setAsDefault) { - masterSecretRecord.setTag('default', true) - } + // In some cases we don't have the linkSecretValue. However we still want a record so we know which link secret ids are valid + const linkSecretRecord = new AnonCredsLinkSecretRecord({ linkSecretId, value: linkSecretValue }) - // Update any other record if this one is set as default - if (options?.setAsDefault) { - for (const record of allRecords) { - record.setTag('default', false) - await this.anonCredsMasterSecretRepository.update(this.agentContext, record) - } - } + // If it is the first link secret registered, set as default + const defaultLinkSecretRecord = await this.anonCredsLinkSecretRepository.findDefault(this.agentContext) + if (!defaultLinkSecretRecord || options?.setAsDefault) { + linkSecretRecord.setTag('isDefault', true) + } - await this.anonCredsMasterSecretRepository.save(this.agentContext, masterSecretRecord) + // Set the current default link secret as not default + if (defaultLinkSecretRecord && options?.setAsDefault) { + defaultLinkSecretRecord.setTag('isDefault', false) + await this.anonCredsLinkSecretRepository.update(this.agentContext, defaultLinkSecretRecord) } + + await this.anonCredsLinkSecretRepository.save(this.agentContext, linkSecretRecord) + } + + /** + * Get a list of ids for the created link secrets + */ + public async getLinkSecretIds(): Promise { + const linkSecrets = await this.anonCredsLinkSecretRepository.getAll(this.agentContext) + + return linkSecrets.map((linkSecret) => linkSecret.linkSecretId) } /** @@ -111,7 +116,17 @@ export class AnonCredsApi { * with the {@link schemaId} */ public async getSchema(schemaId: string): Promise { - const registry = this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, schemaId) + const registry = this.findRegistryForIdentifier(schemaId) + if (!registry) { + return { + resolutionMetadata: { + error: 'unsupportedAnonCredsMethod', + message: `Unable to resolve schema ${schemaId}: No registry found for identifier ${schemaId}`, + }, + schemaId, + schemaMetadata: {}, + } + } try { const result = await registry.getSchema(this.agentContext, schemaId) @@ -122,7 +137,6 @@ export class AnonCredsApi { error: 'error', message: `Unable to resolve schema ${schemaId}: ${error.message}`, }, - schema: null, schemaId, schemaMetadata: {}, } @@ -134,7 +148,6 @@ export class AnonCredsApi { schemaState: { state: 'failed' as const, schema: options.schema, - schemaId: null, reason: `Error registering schema for issuerId ${options.schema.issuerId}`, }, registrationMetadata: {}, @@ -142,6 +155,16 @@ export class AnonCredsApi { } const registry = this.findRegistryForIdentifier(options.schema.issuerId) + if (!registry) { + return { + schemaState: { + state: 'failed', + reason: `Unable to register schema. No registry found for issuerId ${options.schema.issuerId}`, + }, + registrationMetadata: {}, + schemaMetadata: {}, + } + } if (!registry) { failedReturnBase.schemaState.reason = `Could not find a registry for issuerId ${options.schema.issuerId}` @@ -171,7 +194,18 @@ export class AnonCredsApi { * with the {@link credentialDefinitionId} */ public async getCredentialDefinition(credentialDefinitionId: string): Promise { - const registry = this.anonCredsRegistryService.getRegistryForIdentifier(this.agentContext, credentialDefinitionId) + const registry = this.findRegistryForIdentifier(credentialDefinitionId) + + if (!registry) { + return { + resolutionMetadata: { + error: 'unsupportedAnonCredsMethod', + message: `Unable to resolve credential definition ${credentialDefinitionId}: No registry found for identifier ${credentialDefinitionId}`, + }, + credentialDefinitionId, + credentialDefinitionMetadata: {}, + } + } const result = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId) return result @@ -182,15 +216,29 @@ export class AnonCredsApi { // TODO: options should support supportsRevocation at some points options: Extensible }): Promise { - const registry = this.anonCredsRegistryService.getRegistryForIdentifier( - this.agentContext, - options.credentialDefinition.issuerId - ) - - const schemaRegistry = this.anonCredsRegistryService.getRegistryForIdentifier( - this.agentContext, - options.credentialDefinition.schemaId - ) + const registry = this.findRegistryForIdentifier(options.credentialDefinition.issuerId) + if (!registry) { + return { + credentialDefinitionState: { + state: 'failed', + reason: `Unable to register credential definition. No registry found for issuerId ${options.credentialDefinition.issuerId}`, + }, + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + } + } + + const schemaRegistry = this.findRegistryForIdentifier(options.credentialDefinition.schemaId) + if (!schemaRegistry) { + return { + credentialDefinitionState: { + state: 'failed', + reason: `Unable to register credential definition. No registry found for schemaId ${options.credentialDefinition.schemaId}`, + }, + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + } + } const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId) if (!schemaResult.schema) { @@ -205,13 +253,20 @@ export class AnonCredsApi { } const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = - await this.anonCredsIssuerService.createCredentialDefinition(this.agentContext, { - issuerId: options.credentialDefinition.issuerId, - schemaId: options.credentialDefinition.schemaId, - tag: options.credentialDefinition.tag, - supportRevocation: false, - schema: schemaResult.schema, - }) + await this.anonCredsIssuerService.createCredentialDefinition( + this.agentContext, + { + issuerId: options.credentialDefinition.issuerId, + schemaId: options.credentialDefinition.schemaId, + tag: options.credentialDefinition.tag, + supportRevocation: false, + schema: schemaResult.schema, + }, + // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. + { + indyLedgerSchemaSeqNo: schemaResult.schemaMetadata.indyLedgerSeqNo, + } + ) const result = await registry.registerCredentialDefinition(this.agentContext, { credentialDefinition, @@ -230,29 +285,44 @@ export class AnonCredsApi { public async getRevocationRegistryDefinition( revocationRegistryDefinitionId: string ): Promise { - const registry = this.anonCredsRegistryService.getRegistryForIdentifier( - this.agentContext, - revocationRegistryDefinitionId - ) + const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) + + if (!registry) { + return { + resolutionMetadata: { + error: 'unsupportedAnonCredsMethod', + message: `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + } const result = await registry.getRevocationRegistryDefinition(this.agentContext, revocationRegistryDefinitionId) return result } /** - * Retrieve the {@link AnonCredsRevocationList} for the given {@link timestamp} from the registry associated + * Retrieve the {@link AnonCredsRevocationStatusList} for the given {@link timestamp} from the registry associated * with the {@link revocationRegistryDefinitionId} */ - public async getRevocationList( + public async getRevocationStatusList( revocationRegistryDefinitionId: string, timestamp: number - ): Promise { - const registry = this.anonCredsRegistryService.getRegistryForIdentifier( - this.agentContext, - revocationRegistryDefinitionId - ) + ): Promise { + const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) + + if (!registry) { + return { + resolutionMetadata: { + error: 'unsupportedAnonCredsMethod', + message: `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}`, + }, + revocationStatusListMetadata: {}, + } + } - const result = await registry.getRevocationList(this.agentContext, revocationRegistryDefinitionId, timestamp) + const result = await registry.getRevocationStatusList(this.agentContext, revocationRegistryDefinitionId, timestamp) return result } diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index fc219c36cd..78a8e77728 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -1,4 +1,4 @@ -export interface AnonCredsCreateMasterSecretOptions { - masterSecretId?: string +export interface AnonCredsCreateLinkSecretOptions { + linkSecretId?: string setAsDefault?: boolean } diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index 06b763b3fa..3d6eff0b74 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -6,7 +6,7 @@ import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, - AnonCredsMasterSecretRepository, + AnonCredsLinkSecretRepository, } from './repository' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' @@ -34,6 +34,6 @@ export class AnonCredsModule implements Module { dependencyManager.registerSingleton(AnonCredsCredentialDefinitionRepository) dependencyManager.registerSingleton(AnonCredsCredentialDefinitionPrivateRepository) dependencyManager.registerSingleton(AnonCredsKeyCorrectnessProofRepository) - dependencyManager.registerSingleton(AnonCredsMasterSecretRepository) + dependencyManager.registerSingleton(AnonCredsLinkSecretRepository) } } diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts index 90aa51ce66..f9c868c14c 100644 --- a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -3,6 +3,13 @@ import type { DependencyManager } from '@aries-framework/core' import { AnonCredsModule } from '../AnonCredsModule' import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' +import { + AnonCredsSchemaRepository, + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRepository, +} from '../repository' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' const dependencyManager = { @@ -19,8 +26,13 @@ describe('AnonCredsModule', () => { }) anonCredsModule.register(dependencyManager) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(6) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRegistryService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsSchemaRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionPrivateRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsKeyCorrectnessProofRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsLinkSecretRepository) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(AnonCredsModuleConfig, anonCredsModule.config) diff --git a/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts b/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts index 7e1e1909da..2449c81124 100644 --- a/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts +++ b/packages/anoncreds/src/formats/__tests__/LegacyIndyCredentialFormatService.test.ts @@ -77,7 +77,7 @@ describe('LegacyIndyCredentialFormatService', () => { options: {}, }) - const credentialDefinition = await anonCredsIssuerService.createCredentialDefinition( + const { credentialDefinition } = await anonCredsIssuerService.createCredentialDefinition( agentContext, { issuerId: indyDid, diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index 4eba2a2931..9ef264f501 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -5,5 +5,5 @@ export * from './repository' export { AnonCredsModule } from './AnonCredsModule' export { AnonCredsModuleConfig, AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' export { AnonCredsApi } from './AnonCredsApi' -export { AnonCredsCredentialFormatService } from './formats/AnonCredsCredentialFormatService' +export { LegacyIndyCredentialFormatService } from './formats/LegacyIndyCredentialFormatService' export { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' diff --git a/packages/anoncreds/src/models/registry.ts b/packages/anoncreds/src/models/registry.ts index 1e5e6d7879..f4f3429ec2 100644 --- a/packages/anoncreds/src/models/registry.ts +++ b/packages/anoncreds/src/models/registry.ts @@ -32,7 +32,7 @@ export interface AnonCredsRevocationRegistryDefinition { tailsHash: string } -export interface AnonCredsRevocationList { +export interface AnonCredsRevocationStatusList { issuerId: string revRegId: string revocationList: number[] diff --git a/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts b/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts new file mode 100644 index 0000000000..ffb775526e --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsLinkSecretRecord.ts @@ -0,0 +1,42 @@ +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsLinkSecretRecordProps { + id?: string + linkSecretId: string + value?: string // If value is not provided, only reference to link secret is stored in regular storage +} + +export type DefaultAnonCredsLinkSecretTags = { + linkSecretId: string +} + +export type CustomAnonCredsLinkSecretTags = TagsBase & { + isDefault?: boolean +} + +export class AnonCredsLinkSecretRecord extends BaseRecord { + public static readonly type = 'AnonCredsLinkSecretRecord' + public readonly type = AnonCredsLinkSecretRecord.type + + public readonly linkSecretId!: string + public readonly value?: string + + public constructor(props: AnonCredsLinkSecretRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.linkSecretId = props.linkSecretId + this.value = props.value + } + } + + public getTags() { + return { + ...this._tags, + linkSecretId: this.linkSecretId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts b/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts new file mode 100644 index 0000000000..a4b69b08db --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsLinkSecretRepository.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsLinkSecretRecord } from './AnonCredsLinkSecretRecord' + +@injectable() +export class AnonCredsLinkSecretRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsLinkSecretRecord, storageService, eventEmitter) + } + + public async getDefault(agentContext: AgentContext) { + return this.getSingleByQuery(agentContext, { isDefault: true }) + } + + public async findDefault(agentContext: AgentContext) { + return this.findSingleByQuery(agentContext, { isDefault: true }) + } + + public async getByLinkSecretId(agentContext: AgentContext, linkSecretId: string) { + return this.getSingleByQuery(agentContext, { linkSecretId }) + } + + public async findByLinkSecretId(agentContext: AgentContext, linkSecretId: string) { + return this.findSingleByQuery(agentContext, { linkSecretId }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsMasterSecretRecord.ts b/packages/anoncreds/src/repository/AnonCredsMasterSecretRecord.ts deleted file mode 100644 index bad305ff4d..0000000000 --- a/packages/anoncreds/src/repository/AnonCredsMasterSecretRecord.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { TagsBase } from '@aries-framework/core' - -import { BaseRecord, utils } from '@aries-framework/core' - -export interface AnonCredsMasterSecretRecordProps { - id?: string - masterSecretId: string - value?: string // If value is not provided, only reference to master secret is stored in regular storage -} - -export type DefaultAnonCredsMasterSecretTags = { - default?: boolean - masterSecretId: string -} - -export class AnonCredsMasterSecretRecord extends BaseRecord { - public static readonly type = 'AnonCredsMasterSecretRecord' - public readonly type = AnonCredsMasterSecretRecord.type - - public readonly masterSecretId!: string - public readonly value?: string - - public constructor(props: AnonCredsMasterSecretRecordProps) { - super() - - if (props) { - this.id = props.id ?? utils.uuid() - this.masterSecretId = props.masterSecretId - this.value = props.value - } - } - - public getTags() { - return { - ...this._tags, - masterSecretId: this.masterSecretId, - } - } -} diff --git a/packages/anoncreds/src/repository/AnonCredsMasterSecretRepository.ts b/packages/anoncreds/src/repository/AnonCredsMasterSecretRepository.ts deleted file mode 100644 index a40d5384b4..0000000000 --- a/packages/anoncreds/src/repository/AnonCredsMasterSecretRepository.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { AgentContext } from '@aries-framework/core' - -import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' - -import { AnonCredsMasterSecretRecord } from './AnonCredsMasterSecretRecord' - -@injectable() -export class AnonCredsMasterSecretRepository extends Repository { - public constructor( - @inject(InjectionSymbols.StorageService) storageService: StorageService, - eventEmitter: EventEmitter - ) { - super(AnonCredsMasterSecretRecord, storageService, eventEmitter) - } - - public async getDefault(agentContext: AgentContext) { - return this.getSingleByQuery(agentContext, { default: true }) - } - - public async findDefault(agentContext: AgentContext) { - return this.findSingleByQuery(agentContext, { default: true }) - } - - public async getByMasterSecretId(agentContext: AgentContext, masterSecretId: string) { - return this.getSingleByQuery(agentContext, { masterSecretId }) - } - - public async findByMasterSecretId(agentContext: AgentContext, masterSecretId: string) { - return this.findSingleByQuery(agentContext, { masterSecretId }) - } -} diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts index d93780e2bd..5e17e19941 100644 --- a/packages/anoncreds/src/repository/index.ts +++ b/packages/anoncreds/src/repository/index.ts @@ -4,7 +4,7 @@ export * from './AnonCredsCredentialDefinitionPrivateRecord' export * from './AnonCredsCredentialDefinitionPrivateRepository' export * from './AnonCredsKeyCorrectnessProofRecord' export * from './AnonCredsKeyCorrectnessProofRepository' -export * from './AnonCredsMasterSecretRecord' -export * from './AnonCredsMasterSecretRepository' +export * from './AnonCredsLinkSecretRecord' +export * from './AnonCredsLinkSecretRepository' export * from './AnonCredsSchemaRecord' export * from './AnonCredsSchemaRepository' diff --git a/packages/anoncreds/src/services/AnonCredsHolderService.ts b/packages/anoncreds/src/services/AnonCredsHolderService.ts index 6aa644d8a4..85e51ce529 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderService.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderService.ts @@ -6,8 +6,8 @@ import type { StoreCredentialOptions, GetCredentialsForProofRequestOptions, GetCredentialsForProofRequestReturn, - CreateMasterSecretReturn, - CreateMasterSecretOptions, + CreateLinkSecretReturn, + CreateLinkSecretOptions, } from './AnonCredsHolderServiceOptions' import type { AnonCredsCredentialInfo } from '../models' import type { AnonCredsProof } from '../models/exchange' @@ -16,7 +16,7 @@ import type { AgentContext } from '@aries-framework/core' export const AnonCredsHolderServiceSymbol = Symbol('AnonCredsHolderService') export interface AnonCredsHolderService { - createMasterSecret(agentContext: AgentContext, options: CreateMasterSecretOptions): Promise + createLinkSecret(agentContext: AgentContext, options: CreateLinkSecretOptions): Promise createProof(agentContext: AgentContext, options: CreateProofOptions): Promise storeCredential( diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index 0514672854..25bba02d50 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -13,7 +13,7 @@ import type { } from '../models/exchange' import type { AnonCredsCredentialDefinition, - AnonCredsRevocationList, + AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition, AnonCredsSchema, } from '../models/registry' @@ -37,8 +37,8 @@ export interface CreateProofOptions { // tails file MUST already be downloaded on a higher level and stored tailsFilePath: string definition: AnonCredsRevocationRegistryDefinition - revocationLists: { - [timestamp: string]: AnonCredsRevocationList + revocationStatusLists: { + [timestamp: string]: AnonCredsRevocationStatusList } } } @@ -84,11 +84,11 @@ export interface CreateCredentialRequestReturn { credentialRequestMetadata: AnonCredsCredentialRequestMetadata } -export interface CreateMasterSecretOptions { - masterSecretId?: string +export interface CreateLinkSecretOptions { + linkSecretId?: string } -export interface CreateMasterSecretReturn { - masterSecretId: string - masterSecretValue?: string +export interface CreateLinkSecretReturn { + linkSecretId: string + linkSecretValue?: string } diff --git a/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts index f3ecb3b70c..85593764af 100644 --- a/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts @@ -1,7 +1,7 @@ import type { AnonCredsProof, AnonCredsProofRequest } from '../models/exchange' import type { AnonCredsCredentialDefinition, - AnonCredsRevocationList, + AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition, AnonCredsSchema, } from '../models/registry' @@ -23,8 +23,8 @@ export interface VerifyProofOptions { // as a verifier. This is just following the data models from the AnonCreds spec, but for e.g. indy // this means we need to retrieve _ALL_ deltas from the ledger to verify a proof. While currently we // only need to fetch the registry. - revocationLists: { - [timestamp: number]: AnonCredsRevocationList + revocationStatusLists: { + [timestamp: number]: AnonCredsRevocationStatusList } } } diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts index e3061043dd..870eb90571 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts @@ -3,8 +3,8 @@ import type { RegisterCredentialDefinitionOptions, RegisterCredentialDefinitionReturn, } from './CredentialDefinitionOptions' -import type { GetRevocationListReturn } from './RevocationListOptions' import type { GetRevocationRegistryDefinitionReturn } from './RevocationRegistryDefinitionOptions' +import type { GetRevocationStatusListReturn } from './RevocationStatusListOptions' import type { GetSchemaReturn, RegisterSchemaOptions, RegisterSchemaReturn } from './SchemaOptions' import type { AgentContext } from '@aries-framework/core' @@ -37,12 +37,11 @@ export interface AnonCredsRegistry { // options: RegisterRevocationRegistryDefinitionOptions // ): Promise - // TODO: The name of this data model is still tbd. - getRevocationList( + getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, timestamp: number - ): Promise + ): Promise // TODO: issuance of revocable credentials // registerRevocationList( diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts index a860d1e8f5..23c393bb38 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistryService.ts @@ -20,7 +20,7 @@ export class AnonCredsRegistryService { const registry = registries.find((registry) => registry.supportedIdentifier.test(identifier)) if (!registry) { - throw new AnonCredsError(`No AnonCredsRegistry registered for identifier '${registry}'`) + throw new AnonCredsError(`No AnonCredsRegistry registered for identifier '${identifier}'`) } return registry diff --git a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts index 5b430e05b5..1bf5614720 100644 --- a/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/CredentialDefinitionOptions.ts @@ -8,7 +8,7 @@ import type { import type { AnonCredsCredentialDefinition } from '../../models/registry' export interface GetCredentialDefinitionReturn { - credentialDefinition: AnonCredsCredentialDefinition | null + credentialDefinition?: AnonCredsCredentialDefinition credentialDefinitionId: string resolutionMetadata: AnonCredsResolutionMetadata credentialDefinitionMetadata: Extensible @@ -21,17 +21,17 @@ export interface RegisterCredentialDefinitionOptions { export interface RegisterCredentialDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { credentialDefinition?: AnonCredsCredentialDefinition - credentialDefinitionId: string | null + credentialDefinitionId?: string } export interface RegisterCredentialDefinitionReturnStateFinished extends AnonCredsOperationStateFinished { credentialDefinition: AnonCredsCredentialDefinition - credentialDefinitionId: string | null + credentialDefinitionId?: string } export interface RegisterCredentialDefinitionReturnState extends AnonCredsOperationState { credentialDefinition?: AnonCredsCredentialDefinition - credentialDefinitionId: string | null + credentialDefinitionId?: string } export interface RegisterCredentialDefinitionReturn { diff --git a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts index 6d45377114..6e9d1349fe 100644 --- a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts @@ -2,7 +2,7 @@ import type { AnonCredsResolutionMetadata, Extensible } from './base' import type { AnonCredsRevocationRegistryDefinition } from '../../models/registry' export interface GetRevocationRegistryDefinitionReturn { - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | null + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId: string resolutionMetadata: AnonCredsResolutionMetadata revocationRegistryDefinitionMetadata: Extensible diff --git a/packages/anoncreds/src/services/registry/RevocationListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts similarity index 70% rename from packages/anoncreds/src/services/registry/RevocationListOptions.ts rename to packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index b6f0edea42..6396fe6df0 100644 --- a/packages/anoncreds/src/services/registry/RevocationListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -1,10 +1,10 @@ import type { AnonCredsResolutionMetadata, Extensible } from './base' -import type { AnonCredsRevocationList } from '../../models/registry' +import type { AnonCredsRevocationStatusList } from '../../models/registry' -export interface GetRevocationListReturn { - revocationList: AnonCredsRevocationList | null +export interface GetRevocationStatusListReturn { + revocationStatusList?: AnonCredsRevocationStatusList resolutionMetadata: AnonCredsResolutionMetadata - revocationListMetadata: Extensible + revocationStatusListMetadata: Extensible } // TODO: Support for issuance of revocable credentials diff --git a/packages/anoncreds/src/services/registry/SchemaOptions.ts b/packages/anoncreds/src/services/registry/SchemaOptions.ts index 2fa6842f71..9ff42c9bc4 100644 --- a/packages/anoncreds/src/services/registry/SchemaOptions.ts +++ b/packages/anoncreds/src/services/registry/SchemaOptions.ts @@ -9,7 +9,7 @@ import type { AnonCredsSchema } from '../../models/registry' // Get Schema export interface GetSchemaReturn { - schema: AnonCredsSchema | null + schema?: AnonCredsSchema schemaId: string // Can contain e.g. the ledger transaction request/response resolutionMetadata: AnonCredsResolutionMetadata @@ -24,8 +24,8 @@ export interface RegisterSchemaOptions { } export interface RegisterSchemaReturnStateFailed extends AnonCredsOperationStateFailed { - schema: AnonCredsSchema | null - schemaId: string | null + schema?: AnonCredsSchema + schemaId?: string } export interface RegisterSchemaReturnStateFinished extends AnonCredsOperationStateFinished { @@ -34,8 +34,8 @@ export interface RegisterSchemaReturnStateFinished extends AnonCredsOperationSta } export interface RegisterSchemaReturnState extends AnonCredsOperationState { - schema: AnonCredsSchema | null - schemaId: string | null + schema?: AnonCredsSchema + schemaId?: string } export interface RegisterSchemaReturn { diff --git a/packages/anoncreds/src/services/registry/index.ts b/packages/anoncreds/src/services/registry/index.ts index 5d36ce3dd9..fd154074fd 100644 --- a/packages/anoncreds/src/services/registry/index.ts +++ b/packages/anoncreds/src/services/registry/index.ts @@ -2,5 +2,5 @@ export * from './AnonCredsRegistry' export * from './CredentialDefinitionOptions' export * from './SchemaOptions' export * from './RevocationRegistryDefinitionOptions' -export * from './RevocationListOptions' +export * from './RevocationStatusListOptions' export { AnonCredsResolutionMetadata } from './base' diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index bc49d939f7..7b234fe38e 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -8,9 +8,11 @@ import type { RegisterCredentialDefinitionOptions, RegisterCredentialDefinitionReturn, GetRevocationRegistryDefinitionReturn, - GetRevocationListReturn, + GetRevocationStatusListReturn, AnonCredsSchema, AnonCredsCredentialDefinition, + AnonCredsRevocationStatusList, + AnonCredsRevocationRegistryDefinition, } from '../src' import type { AgentContext } from '@aries-framework/core' @@ -26,8 +28,28 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { // we want, but the indy-sdk is picky about the identifier format. public readonly supportedIdentifier = /^[a-zA-Z0-9]{21,22}/ - private schemas: Record = {} - private credentialDefinitions: Record = {} + private schemas: Record + private credentialDefinitions: Record + private revocationRegistryDefinitions: Record + // { revocationRegistryDefinitionId: { timestamp: revocationStatusList } + private revocationStatusLists: Record> + + public constructor({ + existingSchemas = {}, + existingCredentialDefinitions = {}, + existingRevocationRegistryDefinitions = {}, + existingRevocationStatusLists = {}, + }: { + existingSchemas?: Record + existingCredentialDefinitions?: Record + existingRevocationRegistryDefinitions?: Record + existingRevocationStatusLists?: Record> + } = {}) { + this.schemas = existingSchemas + this.credentialDefinitions = existingCredentialDefinitions + this.revocationRegistryDefinitions = existingRevocationRegistryDefinitions + this.revocationStatusLists = existingRevocationStatusLists + } public async getSchema(agentContext: AgentContext, schemaId: string): Promise { const schema = this.schemas[schemaId] @@ -39,13 +61,8 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { error: 'notFound', message: `Schema with id ${schemaId} not found in memory registry`, }, - schema: null, schemaId, - schemaMetadata: { - // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. - // For this reason we return it in the metadata. - indyLedgerSeqNo, - }, + schemaMetadata: {}, } } @@ -53,7 +70,11 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { resolutionMetadata: {}, schema, schemaId, - schemaMetadata: {}, + schemaMetadata: { + // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. + // For this reason we return it in the metadata. + indyLedgerSeqNo, + }, } } @@ -93,7 +114,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { error: 'notFound', message: `Credential definition with id ${credentialDefinitionId} not found in memory registry`, }, - credentialDefinition: null, credentialDefinitionId, credentialDefinitionMetadata: {}, } @@ -127,19 +147,53 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } - public getRevocationRegistryDefinition( + public async getRevocationRegistryDefinition( agentContext: AgentContext, revocationRegistryDefinitionId: string ): Promise { - throw new Error('Method not implemented.') + const revocationRegistryDefinition = this.revocationRegistryDefinitions[revocationRegistryDefinitionId] + + if (!revocationRegistryDefinition) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Revocation registry definition with id ${revocationRegistryDefinition} not found in memory registry`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + } + + return { + resolutionMetadata: {}, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } } - public getRevocationList( + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, timestamp: number - ): Promise { - throw new Error('Method not implemented.') + ): Promise { + const revocationStatusLists = this.revocationStatusLists[revocationRegistryId] + + if (!revocationStatusLists || !revocationStatusLists[timestamp]) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Revocation status list for revocation registry with id ${revocationRegistryId} not found in memory registry`, + }, + revocationStatusListMetadata: {}, + } + } + + return { + resolutionMetadata: {}, + revocationStatusList: revocationStatusLists[timestamp], + revocationStatusListMetadata: {}, + } } } diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts new file mode 100644 index 0000000000..e7abd466c4 --- /dev/null +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -0,0 +1,312 @@ +import { Agent, KeyDerivationMethod } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' + +import { IndySdkModule } from '../../indy-sdk/src/IndySdkModule' +import { AnonCredsCredentialDefinitionRepository, AnonCredsModule, AnonCredsSchemaRepository } from '../src' + +import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' + +const existingSchemas = { + '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0': { + attrNames: ['one', 'two'], + issuerId: '7Cd2Yj9yEZNcmNoH54tq9i', + name: 'Test Schema', + version: '1.0.0', + }, +} + +const existingCredentialDefinitions = { + 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG': { + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', + s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', + r: { + one: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', + master_secret: + '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + two: '60366631925664005237432731340682977203246802182440530784833565276111958129922833461368205267143124766208499918438803966972947830682551774196763124331578934778868938718942789067536194229546670608604626738087066151521062180022991840618459591148096543440942293686250499935227881144460486543061212259250663566176469333982946568767707989969471450673037590849807300874360022327312564559087769485266016496010132793446151658150957771177955095876947792797176338483943233433284791481746843006255371654617950568875773118157773566188096075078351362095061968279597354733768049622048871890495958175847017320945873812850638157518451', + }, + rctxt: + '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', + z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', + }, + }, + }, +} as const + +const existingRevocationRegistryDefinitions = { + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG': { + credDefId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + maxCredNum: 100, + type: 'CL_ACCUM', + publicKeys: { + accumKey: { + z: 'ab81257c-be63-4051-9e21-c7d384412f64', + }, + }, + tag: 'TAG', + tailsHash: 'ab81257c-be63-4051-9e21-c7d384412f64', + tailsLocation: 'http://localhost:7200/tails', + }, +} as const + +const existingRevocationStatusLists = { + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG': { + 10123: { + currentAccumulator: 'ab81257c-be63-4051-9e21-c7d384412f64', + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + revocationList: [1, 0, 1], + revRegId: 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + timestamp: 10123, + }, + }, +} + +const agent = new Agent({ + config: { + label: '@aries-framework/anoncreds', + walletConfig: { + id: '@aries-framework/anoncreds', + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: KeyDerivationMethod.Raw, + }, + }, + modules: { + indySdk: new IndySdkModule({ + indySdk: agentDependencies.indy, + }), + anoncreds: new AnonCredsModule({ + registries: [ + new InMemoryAnonCredsRegistry({ + existingSchemas, + existingCredentialDefinitions, + existingRevocationRegistryDefinitions, + existingRevocationStatusLists, + }), + ], + }), + }, + dependencies: agentDependencies, +}) + +describe('AnonCreds API', () => { + beforeEach(async () => { + await agent.initialize() + }) + + afterEach(async () => { + await agent.wallet.delete() + await agent.shutdown() + }) + + test('create and get link secret', async () => { + await agent.modules.anoncreds.createLinkSecret({ + linkSecretId: 'anoncreds-link-secret', + }) + + const linkSecretIds = await agent.modules.anoncreds.getLinkSecretIds() + + expect(linkSecretIds).toEqual(['anoncreds-link-secret']) + }) + + test('register a schema', async () => { + const schemaResult = await agent.modules.anoncreds.registerSchema({ + options: {}, + schema: { + attrNames: ['name', 'age'], + issuerId: '6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + }) + + expect(schemaResult).toEqual({ + registrationMetadata: {}, + schemaMetadata: { indyLedgerSeqNo: 16908 }, + schemaState: { + state: 'finished', + schema: { + attrNames: ['name', 'age'], + issuerId: '6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + }, + }) + + // Check if record was created + const anonCredsSchemaRepository = agent.dependencyManager.resolve(AnonCredsSchemaRepository) + const schemaRecord = await anonCredsSchemaRepository.getBySchemaId( + agent.context, + '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0' + ) + + expect(schemaRecord).toMatchObject({ + schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + schema: { + attrNames: ['name', 'age'], + issuerId: '6xDN7v3AiGgusRp4bqZACZ', + name: 'Employee Credential', + version: '1.0.0', + }, + }) + + expect(schemaRecord.getTags()).toEqual({ + schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + issuerId: '6xDN7v3AiGgusRp4bqZACZ', + schemaName: 'Employee Credential', + schemaVersion: '1.0.0', + }) + }) + + test('resolve a schema', async () => { + const schemaResult = await agent.modules.anoncreds.getSchema('7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0') + + expect(schemaResult).toEqual({ + resolutionMetadata: {}, + schemaMetadata: { indyLedgerSeqNo: 75206 }, + schema: { + attrNames: ['one', 'two'], + issuerId: '7Cd2Yj9yEZNcmNoH54tq9i', + name: 'Test Schema', + version: '1.0.0', + }, + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + }) + }) + + test('register a credential definition', async () => { + // NOTE: the indy-sdk MUST have a did created, we can't just create a key + await agent.context.wallet.initPublicDid({ seed: '00000000000000000000000000000My1' }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const issuerId = agent.context.wallet.publicDid!.did + + const credentialDefinitionResult = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition: { + issuerId, + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + tag: 'TAG', + }, + options: {}, + }) + + expect(credentialDefinitionResult).toEqual({ + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + state: 'finished', + credentialDefinition: { + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: expect.any(String), + s: expect.any(String), + r: { + one: expect.any(String), + master_secret: expect.any(String), + two: expect.any(String), + }, + rctxt: expect.any(String), + z: expect.any(String), + }, + }, + }, + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + }, + }) + + // Check if record was created + const anonCredsCredentialDefinitionRepository = agent.dependencyManager.resolve( + AnonCredsCredentialDefinitionRepository + ) + const credentialDefinitionRecord = await anonCredsCredentialDefinitionRepository.getByCredentialDefinitionId( + agent.context, + 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG' + ) + + expect(credentialDefinitionRecord).toMatchObject({ + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + credentialDefinition: { + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + type: 'CL', + value: { + primary: { + n: expect.any(String), + s: expect.any(String), + r: { + one: expect.any(String), + master_secret: expect.any(String), + two: expect.any(String), + }, + rctxt: expect.any(String), + z: expect.any(String), + }, + }, + }, + }) + + expect(credentialDefinitionRecord.getTags()).toEqual({ + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', + issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + tag: 'TAG', + }) + }) + + test('resolve a credential definition', async () => { + const credentialDefinitionResult = await agent.modules.anoncreds.getCredentialDefinition( + 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG' + ) + + expect(credentialDefinitionResult).toEqual({ + resolutionMetadata: {}, + credentialDefinitionMetadata: {}, + credentialDefinition: existingCredentialDefinitions['VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG'], + credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + }) + }) + + test('resolve a revocation regsitry definition', async () => { + const revocationRegistryDefinition = await agent.modules.anoncreds.getRevocationRegistryDefinition( + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ) + + expect(revocationRegistryDefinition).toEqual({ + revocationRegistryDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + revocationRegistryDefinition: + existingRevocationRegistryDefinitions[ + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ], + resolutionMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + }) + }) + + test('resolve a revocation status list', async () => { + const revocationStatusList = await agent.modules.anoncreds.getRevocationStatusList( + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG', + 10123 + ) + + expect(revocationStatusList).toEqual({ + revocationStatusList: + existingRevocationStatusLists[ + 'VsKV7grR1BUE29mG2Fm2kX:4:VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG:CL_ACCUM:TAG' + ][10123], + resolutionMetadata: {}, + revocationStatusListMetadata: {}, + }) + }) +}) diff --git a/packages/indy-sdk/src/IndySdkModule.ts b/packages/indy-sdk/src/IndySdkModule.ts index ea3baa5a9a..20574f3d46 100644 --- a/packages/indy-sdk/src/IndySdkModule.ts +++ b/packages/indy-sdk/src/IndySdkModule.ts @@ -1,8 +1,18 @@ import type { IndySdkModuleConfigOptions } from './IndySdkModuleConfig' import type { DependencyManager, Module } from '@aries-framework/core' +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsVerifierServiceSymbol, +} from '@aries-framework/anoncreds' +import { InjectionSymbols } from '@aries-framework/core' + import { IndySdkModuleConfig } from './IndySdkModuleConfig' +import { IndySdkHolderService, IndySdkIssuerService, IndySdkVerifierService } from './anoncreds' +import { IndySdkStorageService } from './storage' import { IndySdkSymbol } from './types' +import { IndySdkWallet } from './wallet' export class IndySdkModule implements Module { public readonly config: IndySdkModuleConfig @@ -13,5 +23,13 @@ export class IndySdkModule implements Module { public register(dependencyManager: DependencyManager) { dependencyManager.registerInstance(IndySdkSymbol, this.config.indySdk) + + // NOTE: for now we are registering the needed indy services. We may want to make this + // more explicit and require the user to register the services they need on the specific modules. + dependencyManager.registerSingleton(InjectionSymbols.Wallet, IndySdkWallet) + dependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) + dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, IndySdkIssuerService) + dependencyManager.registerSingleton(AnonCredsHolderServiceSymbol, IndySdkHolderService) + dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, IndySdkVerifierService) } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index 0668219172..7ddb4a5db5 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -2,7 +2,7 @@ import type { IndySdk } from '../../types' import type { AnonCredsRegistry, GetCredentialDefinitionReturn, - GetRevocationListReturn, + GetRevocationStatusListReturn, GetRevocationRegistryDefinitionReturn, GetSchemaReturn, RegisterCredentialDefinitionOptions, @@ -25,7 +25,7 @@ import { indySdkAnonCredsRegistryIdentifierRegex, } from '../utils/identifiers' import { - anonCredsRevocationListFromIndySdk, + anonCredsRevocationStatusListFromIndySdk, anonCredsRevocationRegistryDefinitionFromIndySdk, } from '../utils/transform' @@ -90,7 +90,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { }) return { - schema: null, schemaId, resolutionMetadata: { error: 'notFound', @@ -111,7 +110,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { schemaMetadata: {}, registrationMetadata: {}, schemaState: { - schemaId: null, reason: 'no didIndyNamespace defined in the options. didIndyNamespace is required when using the Indy SDK', schema: options.schema, state: 'failed', @@ -177,7 +175,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { schemaMetadata: {}, registrationMetadata: {}, schemaState: { - schemaId: null, state: 'failed', schema: options.schema, reason: `unknownError: ${error.message}`, @@ -244,7 +241,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { return { credentialDefinitionId, - credentialDefinition: null, credentialDefinitionMetadata: {}, resolutionMetadata: { error: 'notFound', @@ -267,7 +263,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { reason: 'no didIndyNamespace defined in the options. didIndyNamespace is required when using the Indy SDK', credentialDefinition: options.credentialDefinition, state: 'failed', - credentialDefinitionId: null, }, } } @@ -298,7 +293,6 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { credentialDefinition: options.credentialDefinition, state: 'failed', reason: `error resolving schema with id ${options.credentialDefinition.schemaId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, - credentialDefinitionId: null, }, } } @@ -417,18 +411,17 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { error: 'notFound', message: `unable to resolve revocation registry definition: ${error.message}`, }, - revocationRegistryDefinition: null, revocationRegistryDefinitionId, revocationRegistryDefinitionMetadata: {}, } } } - public async getRevocationList( + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, timestamp: number - ): Promise { + ): Promise { try { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) @@ -477,10 +470,9 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { resolutionMetadata: { error: `error resolving revocation registry definition with id ${revocationRegistryId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, }, - revocationListMetadata: { + revocationStatusListMetadata: { didIndyNamespace: pool.didIndyNamespace, }, - revocationList: null, } } @@ -488,14 +480,14 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { return { resolutionMetadata: {}, - revocationList: anonCredsRevocationListFromIndySdk( + revocationStatusList: anonCredsRevocationStatusListFromIndySdk( revocationRegistryId, revocationRegistryDefinition, revocationRegistryDelta, deltaTimestamp, isIssuanceByDefault ), - revocationListMetadata: { + revocationStatusListMetadata: { didIndyNamespace: pool.didIndyNamespace, }, } @@ -513,8 +505,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { error: 'notFound', message: `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, }, - revocationList: null, - revocationListMetadata: {}, + revocationStatusListMetadata: {}, } } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts index f9cb3bc22c..4086f8c171 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts @@ -11,6 +11,8 @@ import type { GetCredentialsForProofRequestReturn, AnonCredsRequestedCredentials, AnonCredsCredentialRequestMetadata, + CreateLinkSecretOptions, + CreateLinkSecretReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { @@ -22,7 +24,7 @@ import type { CredReqMetadata, } from 'indy-sdk' -import { inject } from '@aries-framework/core' +import { injectable, inject, utils } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' @@ -37,6 +39,7 @@ import { import { IndySdkRevocationService } from './IndySdkRevocationService' +@injectable() export class IndySdkHolderService implements AnonCredsHolderService { private indySdk: IndySdk private indyRevocationService: IndySdkRevocationService @@ -46,6 +49,31 @@ export class IndySdkHolderService implements AnonCredsHolderService { this.indyRevocationService = indyRevocationService } + public async createLinkSecret( + agentContext: AgentContext, + options: CreateLinkSecretOptions + ): Promise { + assertIndySdkWallet(agentContext.wallet) + + const linkSecretId = options.linkSecretId ?? utils.uuid() + + try { + await this.indySdk.proverCreateMasterSecret(agentContext.wallet.handle, linkSecretId) + + // We don't have the value for the link secret when using the indy-sdk so we can't return it. + return { + linkSecretId, + } + } catch (error) { + agentContext.config.logger.error(`Error creating link secret`, { + error, + linkSecretId, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise { const { credentialDefinitions, proofRequest, requestedCredentials, schemas } = options diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index 96e9ef266a..ba6c2a1780 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -8,11 +8,11 @@ import type { CreateSchemaOptions, AnonCredsCredentialOffer, AnonCredsSchema, - AnonCredsCredentialDefinition, + CreateCredentialDefinitionReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' -import { AriesFrameworkError, inject } from '@aries-framework/core' +import { injectable, AriesFrameworkError, inject } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' @@ -21,6 +21,7 @@ import { generateLegacyProverDidLikeString } from '../utils/proverDid' import { createTailsReader } from '../utils/tails' import { indySdkSchemaFromAnonCreds } from '../utils/transform' +@injectable() export class IndySdkIssuerService implements AnonCredsIssuerService { private indySdk: IndySdk @@ -50,7 +51,7 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialDefinitionOptions, metadata?: CreateCredentialDefinitionMetadata - ): Promise { + ): Promise { const { tag, supportRevocation, schema, issuerId, schemaId } = options if (!metadata) @@ -70,11 +71,13 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { ) return { - issuerId, - tag: credentialDefinition.tag, - schemaId, - type: 'CL', - value: credentialDefinition.value, + credentialDefinition: { + issuerId, + tag: credentialDefinition.tag, + schemaId, + type: 'CL', + value: credentialDefinition.value, + }, } } catch (error) { throw isIndyError(error) ? new IndySdkError(error) : error diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts index 4f7eb6ef42..30f78bcbff 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts @@ -1,6 +1,6 @@ import type { AnonCredsRevocationRegistryDefinition, - AnonCredsRevocationList, + AnonCredsRevocationStatusList, AnonCredsProofRequest, AnonCredsRequestedCredentials, AnonCredsCredentialInfo, @@ -50,8 +50,8 @@ export class IndySdkRevocationService { // Tails is already downloaded tailsFilePath: string definition: AnonCredsRevocationRegistryDefinition - revocationLists: { - [timestamp: string]: AnonCredsRevocationList + revocationStatusLists: { + [timestamp: string]: AnonCredsRevocationStatusList } } } @@ -106,18 +106,18 @@ export class IndySdkRevocationService { this.assertRevocationInterval(requestRevocationInterval) - const { definition, revocationLists, tailsFilePath } = revocationRegistries[revocationRegistryId] - // NOTE: we assume that the revocationLists have been added based on timestamps of the `to` query. On a higher level it means we'll find the - // most accurate revocation list for a given timestamp. It doesn't have to be that the revocationList is from the `to` timestamp however. - const revocationList = revocationLists[requestRevocationInterval.to] + const { definition, revocationStatusLists, tailsFilePath } = revocationRegistries[revocationRegistryId] + // NOTE: we assume that the revocationStatusLists have been added based on timestamps of the `to` query. On a higher level it means we'll find the + // most accurate revocation list for a given timestamp. It doesn't have to be that the revocationStatusList is from the `to` timestamp however. + const revocationStatusList = revocationStatusLists[requestRevocationInterval.to] const tails = await createTailsReader(agentContext, tailsFilePath) const revocationState = await this.indySdk.createRevocationState( tails, indySdkRevocationRegistryDefinitionFromAnonCreds(revocationRegistryId, definition), - indySdkRevocationDeltaFromAnonCreds(revocationList), - revocationList.timestamp, + indySdkRevocationDeltaFromAnonCreds(revocationStatusList), + revocationStatusList.timestamp, credentialRevocationId ) const timestamp = revocationState.timestamp diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts index d302e66c97..6de2fbc276 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts @@ -1,7 +1,7 @@ import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' import type { CredentialDefs, Schemas, RevocRegDefs, RevRegs } from 'indy-sdk' -import { inject } from '@aries-framework/core' +import { inject, injectable } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' @@ -13,6 +13,7 @@ import { indySdkSchemaFromAnonCreds, } from '../utils/transform' +@injectable() export class IndySdkVerifierService implements AnonCredsVerifierService { private indySdk: IndySdk @@ -53,7 +54,7 @@ export class IndySdkVerifierService implements AnonCredsVerifierService { const indyRevocationRegistries: RevRegs = {} for (const revocationRegistryDefinitionId in options.revocationStates) { - const { definition, revocationLists } = options.revocationStates[revocationRegistryDefinitionId] + const { definition, revocationStatusLists } = options.revocationStates[revocationRegistryDefinitionId] indyRevocationDefinitions[revocationRegistryDefinitionId] = indySdkRevocationRegistryDefinitionFromAnonCreds( revocationRegistryDefinitionId, definition @@ -64,10 +65,10 @@ export class IndySdkVerifierService implements AnonCredsVerifierService { // Also transform the revocation lists for the specified timestamps into the revocation registry // format Indy expects - for (const timestamp in revocationLists) { - const revocationList = revocationLists[timestamp] + for (const timestamp in revocationStatusLists) { + const revocationStatusList = revocationStatusLists[timestamp] indyRevocationRegistries[revocationRegistryDefinitionId][timestamp] = - indySdkRevocationRegistryFromAnonCreds(revocationList) + indySdkRevocationRegistryFromAnonCreds(revocationStatusList) } } diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts index 20b16fa0ff..7930bfb2fb 100644 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts +++ b/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts @@ -108,7 +108,7 @@ describe('transform', () => { test.todo( 'indySdkRevocationRegistryDefinitionFromAnonCreds should return a valid indy sdk revocation registry definition' ) - test.todo('anonCredsRevocationListFromIndySdk should return a valid anoncreds revocation list') + test.todo('anonCredsRevocationStatusListFromIndySdk should return a valid anoncreds revocation list') test.todo('indySdkRevocationRegistryFromAnonCreds should return a valid indy sdk revocation registry') test.todo('indySdkRevocationDeltaFromAnonCreds should return a valid indy sdk revocation delta') }) diff --git a/packages/indy-sdk/src/anoncreds/utils/transform.ts b/packages/indy-sdk/src/anoncreds/utils/transform.ts index a5ad8afd60..6a91928f70 100644 --- a/packages/indy-sdk/src/anoncreds/utils/transform.ts +++ b/packages/indy-sdk/src/anoncreds/utils/transform.ts @@ -1,6 +1,6 @@ import type { AnonCredsCredentialDefinition, - AnonCredsRevocationList, + AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition, AnonCredsSchema, } from '@aries-framework/anoncreds' @@ -92,13 +92,13 @@ export function indySdkRevocationRegistryDefinitionFromAnonCreds( } } -export function anonCredsRevocationListFromIndySdk( +export function anonCredsRevocationStatusListFromIndySdk( revocationRegistryDefinitionId: string, revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, delta: RevocRegDelta, timestamp: number, isIssuanceByDefault: boolean -): AnonCredsRevocationList { +): AnonCredsRevocationStatusList { // 0 means unrevoked, 1 means revoked const defaultState = isIssuanceByDefault ? 0 : 1 @@ -124,25 +124,27 @@ export function anonCredsRevocationListFromIndySdk( } } -export function indySdkRevocationRegistryFromAnonCreds(revocationList: AnonCredsRevocationList): RevocReg { +export function indySdkRevocationRegistryFromAnonCreds(revocationStatusList: AnonCredsRevocationStatusList): RevocReg { return { ver: '1.0', value: { - accum: revocationList.currentAccumulator, + accum: revocationStatusList.currentAccumulator, }, } } -export function indySdkRevocationDeltaFromAnonCreds(revocationList: AnonCredsRevocationList): RevocRegDelta { - // Get all indices from the revocationList that are revoked (so have value '1') - const revokedIndices = revocationList.revocationList.reduce( +export function indySdkRevocationDeltaFromAnonCreds( + revocationStatusList: AnonCredsRevocationStatusList +): RevocRegDelta { + // Get all indices from the revocationStatusList that are revoked (so have value '1') + const revokedIndices = revocationStatusList.revocationList.reduce( (revoked, current, index) => (current === 1 ? [...revoked, index] : revoked), [] ) return { value: { - accum: revocationList.currentAccumulator, + accum: revocationStatusList.currentAccumulator, issued: [], revoked: revokedIndices, // NOTE: I don't think this is used? From 719356ed09346559609cb470b59eb00505884a06 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 5 Feb 2023 11:07:32 +0100 Subject: [PATCH 5/8] refactor: anoncreds api Signed-off-by: Timo Glastra --- packages/anoncreds/src/AnonCredsApi.ts | 317 +++++++++++++------------ 1 file changed, 170 insertions(+), 147 deletions(-) diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 12ef5e736d..b52f4dbc0f 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -116,30 +116,28 @@ export class AnonCredsApi { * with the {@link schemaId} */ public async getSchema(schemaId: string): Promise { + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve schema ${schemaId}`, + }, + schemaId, + schemaMetadata: {}, + } + const registry = this.findRegistryForIdentifier(schemaId) if (!registry) { - return { - resolutionMetadata: { - error: 'unsupportedAnonCredsMethod', - message: `Unable to resolve schema ${schemaId}: No registry found for identifier ${schemaId}`, - }, - schemaId, - schemaMetadata: {}, - } + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve schema ${schemaId}: No registry found for identifier ${schemaId}` + return failedReturnBase } try { const result = await registry.getSchema(this.agentContext, schemaId) return result } catch (error) { - return { - resolutionMetadata: { - error: 'error', - message: `Unable to resolve schema ${schemaId}: ${error.message}`, - }, - schemaId, - schemaMetadata: {}, - } + failedReturnBase.resolutionMetadata.message = `Unable to resolve schema ${schemaId}: ${error.message}` + return failedReturnBase } } @@ -156,18 +154,7 @@ export class AnonCredsApi { const registry = this.findRegistryForIdentifier(options.schema.issuerId) if (!registry) { - return { - schemaState: { - state: 'failed', - reason: `Unable to register schema. No registry found for issuerId ${options.schema.issuerId}`, - }, - registrationMetadata: {}, - schemaMetadata: {}, - } - } - - if (!registry) { - failedReturnBase.schemaState.reason = `Could not find a registry for issuerId ${options.schema.issuerId}` + failedReturnBase.schemaState.reason = `Unable to register schema. No registry found for issuerId ${options.schema.issuerId}` return failedReturnBase } @@ -194,21 +181,29 @@ export class AnonCredsApi { * with the {@link credentialDefinitionId} */ public async getCredentialDefinition(credentialDefinitionId: string): Promise { - const registry = this.findRegistryForIdentifier(credentialDefinitionId) + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve credential definition ${credentialDefinitionId}`, + }, + credentialDefinitionId, + credentialDefinitionMetadata: {}, + } + const registry = this.findRegistryForIdentifier(credentialDefinitionId) if (!registry) { - return { - resolutionMetadata: { - error: 'unsupportedAnonCredsMethod', - message: `Unable to resolve credential definition ${credentialDefinitionId}: No registry found for identifier ${credentialDefinitionId}`, - }, - credentialDefinitionId, - credentialDefinitionMetadata: {}, - } + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve credential definition ${credentialDefinitionId}: No registry found for identifier ${credentialDefinitionId}` + return failedReturnBase } - const result = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId) - return result + try { + const result = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId) + return result + } catch (error) { + failedReturnBase.resolutionMetadata.message = `Unable to resolve credential definition ${credentialDefinitionId}: ${error.message}` + return failedReturnBase + } } public async registerCredentialDefinition(options: { @@ -216,66 +211,70 @@ export class AnonCredsApi { // TODO: options should support supportsRevocation at some points options: Extensible }): Promise { + const failedReturnBase = { + credentialDefinitionState: { + state: 'failed' as const, + reason: `Error registering credential definition for issuerId ${options.credentialDefinition.issuerId}`, + }, + registrationMetadata: {}, + credentialDefinitionMetadata: {}, + } + const registry = this.findRegistryForIdentifier(options.credentialDefinition.issuerId) if (!registry) { - return { - credentialDefinitionState: { - state: 'failed', - reason: `Unable to register credential definition. No registry found for issuerId ${options.credentialDefinition.issuerId}`, - }, - registrationMetadata: {}, - credentialDefinitionMetadata: {}, - } + failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for issuerId ${options.credentialDefinition.issuerId}` + return failedReturnBase } const schemaRegistry = this.findRegistryForIdentifier(options.credentialDefinition.schemaId) if (!schemaRegistry) { - return { - credentialDefinitionState: { - state: 'failed', - reason: `Unable to register credential definition. No registry found for schemaId ${options.credentialDefinition.schemaId}`, - }, - registrationMetadata: {}, - credentialDefinitionMetadata: {}, - } + failedReturnBase.credentialDefinitionState.reason = `Unable to register credential definition. No registry found for schemaId ${options.credentialDefinition.schemaId}` + return failedReturnBase } - 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: {}, + + try { + const schemaResult = await schemaRegistry.getSchema(this.agentContext, options.credentialDefinition.schemaId) + + if (!schemaResult.schema) { + failedReturnBase.credentialDefinitionState.reason = `error resolving schema with id ${options.credentialDefinition.schemaId}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` + return failedReturnBase } - } - const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = - await this.anonCredsIssuerService.createCredentialDefinition( - this.agentContext, - { - issuerId: options.credentialDefinition.issuerId, - schemaId: options.credentialDefinition.schemaId, - tag: options.credentialDefinition.tag, - supportRevocation: false, - schema: schemaResult.schema, - }, - // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. - { - indyLedgerSchemaSeqNo: schemaResult.schemaMetadata.indyLedgerSeqNo, - } - ) + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await this.anonCredsIssuerService.createCredentialDefinition( + this.agentContext, + { + issuerId: options.credentialDefinition.issuerId, + schemaId: options.credentialDefinition.schemaId, + tag: options.credentialDefinition.tag, + supportRevocation: false, + schema: schemaResult.schema, + }, + // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. + { + indyLedgerSchemaSeqNo: schemaResult.schemaMetadata.indyLedgerSeqNo, + } + ) - const result = await registry.registerCredentialDefinition(this.agentContext, { - credentialDefinition, - options: options.options, - }) + const result = await registry.registerCredentialDefinition(this.agentContext, { + credentialDefinition, + options: options.options, + }) + + await this.storeCredentialDefinitionRecord(result, credentialDefinitionPrivate, keyCorrectnessProof) - await this.storeCredentialDefinitionRecord(result, credentialDefinitionPrivate, keyCorrectnessProof) + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.credentialDefinitionState.reason = `Error storing credential definition records: ${error.message}` + return failedReturnBase + } - return result + // In theory registerCredentialDefinition SHOULD NOT throw, but we can't know for sure + failedReturnBase.credentialDefinitionState.reason = `Error registering credential definition: ${error.message}` + return failedReturnBase + } } /** @@ -285,21 +284,29 @@ export class AnonCredsApi { public async getRevocationRegistryDefinition( revocationRegistryDefinitionId: string ): Promise { - const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve revocation registry ${revocationRegistryDefinitionId}`, + }, + revocationRegistryDefinitionId, + revocationRegistryDefinitionMetadata: {}, + } + const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) if (!registry) { - return { - resolutionMetadata: { - error: 'unsupportedAnonCredsMethod', - message: `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}`, - }, - revocationRegistryDefinitionId, - revocationRegistryDefinitionMetadata: {}, - } + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}` + return failedReturnBase } - const result = await registry.getRevocationRegistryDefinition(this.agentContext, revocationRegistryDefinitionId) - return result + try { + const result = await registry.getRevocationRegistryDefinition(this.agentContext, revocationRegistryDefinitionId) + return result + } catch (error) { + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation registry ${revocationRegistryDefinitionId}: ${error.message}` + return failedReturnBase + } } /** @@ -310,20 +317,32 @@ export class AnonCredsApi { revocationRegistryDefinitionId: string, timestamp: number ): Promise { - const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) + const failedReturnBase = { + resolutionMetadata: { + error: 'error', + message: `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}`, + }, + revocationStatusListMetadata: {}, + } + const registry = this.findRegistryForIdentifier(revocationRegistryDefinitionId) if (!registry) { - return { - resolutionMetadata: { - error: 'unsupportedAnonCredsMethod', - message: `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}`, - }, - revocationStatusListMetadata: {}, - } + failedReturnBase.resolutionMetadata.error = 'unsupportedAnonCredsMethod' + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}: No registry found for identifier ${revocationRegistryDefinitionId}` + return failedReturnBase } - const result = await registry.getRevocationStatusList(this.agentContext, revocationRegistryDefinitionId, timestamp) - return result + try { + const result = await registry.getRevocationStatusList( + this.agentContext, + revocationRegistryDefinitionId, + timestamp + ) + return result + } catch (error) { + failedReturnBase.resolutionMetadata.message = `Unable to resolve revocation status list for revocation registry ${revocationRegistryDefinitionId}: ${error.message}` + return failedReturnBase + } } private async storeCredentialDefinitionRecord( @@ -331,50 +350,54 @@ export class AnonCredsApi { credentialDefinitionPrivate?: Record, keyCorrectnessProof?: Record ): Promise { - // If we have both the credentialDefinition and the credentialDefinitionId we will store a copy of the credential definition. We may need to handle an - // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel - if ( - result.credentialDefinitionState.credentialDefinition && - result.credentialDefinitionState.credentialDefinitionId - ) { - const credentialDefinitionRecord = new AnonCredsCredentialDefinitionRecord({ - credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, - credentialDefinition: result.credentialDefinitionState.credentialDefinition, - }) - - // TODO: do we need to store this metadata? For indy, the registration metadata contains e.g. - // the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions - // are stored in the metadata - credentialDefinitionRecord.metadata.set( - AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionMetadata, - result.credentialDefinitionMetadata - ) - credentialDefinitionRecord.metadata.set( - AnonCredsCredentialDefinitionRecordMetadataKeys.CredentialDefinitionRegistrationMetadata, - result.registrationMetadata - ) - - await this.anonCredsCredentialDefinitionRepository.save(this.agentContext, credentialDefinitionRecord) - - // Store Credential Definition private data (if provided by issuer service) - if (credentialDefinitionPrivate) { - const credentialDefinitionPrivateRecord = new AnonCredsCredentialDefinitionPrivateRecord({ + try { + // If we have both the credentialDefinition and the credentialDefinitionId we will store a copy of the credential definition. We may need to handle an + // edge case in the future where we e.g. don't have the id yet, and it is registered through a different channel + if ( + result.credentialDefinitionState.credentialDefinition && + result.credentialDefinitionState.credentialDefinitionId + ) { + const credentialDefinitionRecord = new AnonCredsCredentialDefinitionRecord({ credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, - value: credentialDefinitionPrivate, + credentialDefinition: result.credentialDefinitionState.credentialDefinition, }) - await this.anonCredsCredentialDefinitionPrivateRepository.save( - this.agentContext, - credentialDefinitionPrivateRecord + + // 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 ) - } - if (keyCorrectnessProof) { - const keyCorrectnessProofRecord = new AnonCredsKeyCorrectnessProofRecord({ - credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, - value: keyCorrectnessProof, - }) - await this.anonCredsKeyCorrectnessProofRepository.save(this.agentContext, keyCorrectnessProofRecord) + await this.anonCredsCredentialDefinitionRepository.save(this.agentContext, credentialDefinitionRecord) + + // Store Credential Definition private data (if provided by issuer service) + if (credentialDefinitionPrivate) { + const credentialDefinitionPrivateRecord = new AnonCredsCredentialDefinitionPrivateRecord({ + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + value: credentialDefinitionPrivate, + }) + await this.anonCredsCredentialDefinitionPrivateRepository.save( + this.agentContext, + credentialDefinitionPrivateRecord + ) + } + + if (keyCorrectnessProof) { + const keyCorrectnessProofRecord = new AnonCredsKeyCorrectnessProofRecord({ + credentialDefinitionId: result.credentialDefinitionState.credentialDefinitionId, + value: keyCorrectnessProof, + }) + await this.anonCredsKeyCorrectnessProofRepository.save(this.agentContext, keyCorrectnessProofRecord) + } } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing credential definition records`, { cause: error }) } } From eba54a10f85ef95549313bfc2681a620d48aaf94 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 5 Feb 2023 21:06:21 +0100 Subject: [PATCH 6/8] Update packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts Signed-off-by: Timo Glastra --- packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 51257c8b65..18bd9cfaab 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -31,7 +31,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { private schemas: Record private credentialDefinitions: Record private revocationRegistryDefinitions: Record - // { revocationRegistryDefinitionId: { timestamp: revocationStatusList } private revocationStatusLists: Record> public constructor({ From bfc4812a9406260b949c4678f88739441ca4448b Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 5 Feb 2023 21:07:55 +0100 Subject: [PATCH 7/8] Update packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts Co-authored-by: Ariel Gentile Signed-off-by: Timo Glastra --- .../anoncreds/src/services/AnonCredsHolderServiceOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index 8102d127bd..fcbc5e913c 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -81,7 +81,7 @@ export type GetCredentialsForProofRequestReturn = Array<{ export interface CreateCredentialRequestOptions { credentialOffer: AnonCredsCredentialOffer credentialDefinition: AnonCredsCredentialDefinition - masterSecretId?: string + linkSecretId?: string } export interface CreateCredentialRequestReturn { From fecb957c7f52b67792cdbca665fe790e2a12db51 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 6 Feb 2023 10:22:53 +0100 Subject: [PATCH 8/8] test(anoncreds): increase timeout Signed-off-by: Timo Glastra --- packages/anoncreds/tests/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/anoncreds/tests/setup.ts b/packages/anoncreds/tests/setup.ts index 719a473b6e..b60b932be5 100644 --- a/packages/anoncreds/tests/setup.ts +++ b/packages/anoncreds/tests/setup.ts @@ -1 +1 @@ -jest.setTimeout(10000) +jest.setTimeout(25000)