From 6f7193d404fb18fcc487435639102dba20811a38 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 18 Jan 2024 09:50:55 +0100 Subject: [PATCH 01/38] feat: w3c migration + data integrity issuance protocol alpha Signed-off-by: Martin Auer --- demo/package.json | 2 +- packages/anoncreds-rs/package.json | 6 +- .../src/services/AnonCredsRsHolderService.ts | 246 ++-- .../src/services/AnonCredsRsIssuerService.ts | 1 - .../AnonCredsRsHolderService.test.ts | 239 ++-- .../__tests__/AnonCredsRsServices.test.ts | 67 +- .../src/services/__tests__/helpers.ts | 28 +- .../anoncreds-rs/tests/anoncreds-flow.test.ts | 65 +- .../data-integrity-flow-anoncreds.test.ts | 523 ++++++++ .../tests/data-integrity-flow-w3c.test.ts | 286 +++++ packages/anoncreds-rs/tests/indy-flow.test.ts | 28 +- packages/anoncreds/package.json | 7 +- packages/anoncreds/src/AnonCredsModule.ts | 6 + .../src/formats/AnonCredsCredentialFormat.ts | 2 +- .../AnonCredsCredentialFormatService.ts | 4 +- .../formats/AnonCredsProofFormatService.ts | 2 +- .../DataIntegrityCredentialFormatService.ts | 1051 +++++++++++++++++ .../formats/LegacyIndyProofFormatService.ts | 2 +- packages/anoncreds/src/formats/index.ts | 1 + packages/anoncreds/src/index.ts | 2 + packages/anoncreds/src/models/exchange.ts | 6 +- packages/anoncreds/src/models/internal.ts | 6 +- .../services/AnonCredsHolderServiceOptions.ts | 16 +- .../w3cCredentialRecordMigration.test.ts | 194 +++ .../0.4-0.5/anonCredsCredentialRecord.ts | 67 ++ .../anoncreds/src/updates/0.4-0.5/index.ts | 7 + packages/anoncreds/src/utils/credential.ts | 56 +- packages/anoncreds/src/utils/index.ts | 4 +- packages/anoncreds/src/utils/ledgerObjects.ts | 105 ++ packages/anoncreds/src/utils/w3cUtils.ts | 53 + packages/anoncreds/tests/setup.ts | 3 + packages/core/src/crypto/index.ts | 1 + .../formats/CredentialFormatServiceOptions.ts | 6 +- .../DataIntegrityCredentialFormat.ts | 71 ++ .../dataIntegrity/dataIntegrityExchange.ts | 81 ++ .../dataIntegrity/dataIntegrityMetadata.ts | 45 + .../formats/dataIntegrity/index.ts | 3 + .../src/modules/credentials/formats/index.ts | 1 + .../core/src/modules/credentials/index.ts | 10 +- .../v2/CredentialFormatCoordinator.ts | 7 +- .../v2/messages/V2RequestCredentialMessage.ts | 2 + .../src/modules/vc/W3cCredentialService.ts | 1 + .../modules/vc/W3cCredentialServiceOptions.ts | 2 + .../data-integrity/models/LinkedDataProof.ts | 1 + packages/core/src/modules/vc/index.ts | 1 + .../vc/models/credential/W3cCredential.ts | 11 +- .../models/credential/W3cCredentialSubject.ts | 67 +- .../__tests__/W3cCredential.test.ts | 50 +- .../W3CAnoncredsCredentialMetadata.ts | 36 + .../vc/repository/W3cCredentialRecord.ts | 135 ++- .../vc/repository/W3cCredentialRepository.ts | 16 +- .../__tests__/W3cCredentialRecord.test.ts | 75 +- .../anonCredsCredentialValue.test.ts | 124 ++ .../vc/repository/anonCredsCredentialValue.ts | 73 ++ .../core/src/modules/vc/repository/index.ts | 6 + .../src/IndySdkToAskarMigrationUpdater.ts | 3 +- yarn.lock | 18 +- 57 files changed, 3560 insertions(+), 371 deletions(-) create mode 100644 packages/anoncreds-rs/tests/data-integrity-flow-anoncreds.test.ts create mode 100644 packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts create mode 100644 packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts create mode 100644 packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts create mode 100644 packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts create mode 100644 packages/anoncreds/src/updates/0.4-0.5/index.ts create mode 100644 packages/anoncreds/src/utils/ledgerObjects.ts create mode 100644 packages/anoncreds/src/utils/w3cUtils.ts create mode 100644 packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts create mode 100644 packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts create mode 100644 packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityMetadata.ts create mode 100644 packages/core/src/modules/credentials/formats/dataIntegrity/index.ts create mode 100644 packages/core/src/modules/vc/repository/W3CAnoncredsCredentialMetadata.ts create mode 100644 packages/core/src/modules/vc/repository/__tests__/anonCredsCredentialValue.test.ts create mode 100644 packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts diff --git a/demo/package.json b/demo/package.json index 4ffbef505b..fcbc8ac394 100644 --- a/demo/package.json +++ b/demo/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.7", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "inquirer": "^8.2.5" }, diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json index aaafaf228d..92fbc15b38 100644 --- a/packages/anoncreds-rs/package.json +++ b/packages/anoncreds-rs/package.json @@ -32,8 +32,8 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.4", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.7", + "@hyperledger/anoncreds-shared": "^0.2.0-dev.7", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", "reflect-metadata": "^0.1.13", @@ -41,6 +41,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.4" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.7" } } diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts index 7249da2662..4dbbd68482 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts @@ -1,5 +1,4 @@ import type { - AnonCredsCredential, AnonCredsCredentialInfo, AnonCredsCredentialRequest, AnonCredsCredentialRequestMetadata, @@ -18,8 +17,9 @@ import type { GetCredentialsForProofRequestReturn, GetCredentialsOptions, StoreCredentialOptions, + StoreW3cCredentialOptions, } from '@aries-framework/anoncreds' -import type { AgentContext, Query, SimpleQuery } from '@aries-framework/core' +import type { AgentContext, AnonCredsClaimRecord, Query, SimpleQuery, W3cCredentialRecord } from '@aries-framework/core' import type { CredentialEntry, CredentialProve, @@ -28,17 +28,27 @@ import type { } from '@hyperledger/anoncreds-shared' import { - AnonCredsCredentialRecord, - AnonCredsCredentialRepository, AnonCredsLinkSecretRepository, - AnonCredsRestrictionWrapper, - unqualifiedCredentialDefinitionIdRegex, AnonCredsRegistryService, + AnonCredsRestrictionWrapper, + fetchQualifiedIds, + legacyCredentialToW3cCredential, storeLinkSecret, + unqualifiedCredentialDefinitionIdRegex, + w3cToLegacyCredential, } from '@aries-framework/anoncreds' -import { AriesFrameworkError, JsonTransformer, TypedArrayEncoder, injectable, utils } from '@aries-framework/core' import { - Credential, + AriesFrameworkError, + JsonTransformer, + TypedArrayEncoder, + W3cCredentialRepository, + W3cCredentialService, + W3cJsonLdVerifiableCredential, + injectable, + utils, +} from '@aries-framework/core' +import { + W3cCredential as AW3cCredential, CredentialRequest, CredentialRevocationState, LinkSecret, @@ -78,10 +88,10 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { rsSchemas[schemaId] = schemas[schemaId] as unknown as JsonObject } - const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) // Cache retrieved credentials in order to minimize storage calls - const retrievedCredentials = new Map() + const retrievedCredentials = new Map() const credentialEntryFromAttribute = async ( attribute: AnonCredsRequestedAttributeMatch | AnonCredsRequestedPredicateMatch @@ -92,8 +102,15 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { retrievedCredentials.set(attribute.credentialId, credentialRecord) } - const revocationRegistryDefinitionId = credentialRecord.credential.rev_reg_id - const revocationRegistryIndex = credentialRecord.credentialRevocationId + if (!credentialRecord.anonCredsCredentialMetadata) { + throw new AriesFrameworkError('AnonCreds metadata not found on credential record.') + } + + if (credentialRecord.credential instanceof W3cJsonLdVerifiableCredential === false) { + throw new AriesFrameworkError('Credential must be a W3cJsonLdVerifiableCredential.') + } + + const { revocationRegistryId, credentialRevocationId } = this.anoncredsMetadataFromRecord(credentialRecord) // TODO: Check if credential has a revocation registry id (check response from anoncreds-rs API, as it is // sending back a mandatory string in Credential.revocationRegistryId) @@ -102,34 +119,37 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { let revocationState: CredentialRevocationState | undefined let revocationRegistryDefinition: RevocationRegistryDefinition | undefined try { - if (timestamp && revocationRegistryIndex && revocationRegistryDefinitionId) { - if (!options.revocationRegistries[revocationRegistryDefinitionId]) { - throw new AnonCredsRsError(`Revocation Registry ${revocationRegistryDefinitionId} not found`) + if (timestamp && credentialRevocationId && revocationRegistryId) { + if (!options.revocationRegistries[revocationRegistryId]) { + throw new AnonCredsRsError(`Revocation Registry ${revocationRegistryId} not found`) } const { definition, revocationStatusLists, tailsFilePath } = - options.revocationRegistries[revocationRegistryDefinitionId] + options.revocationRegistries[revocationRegistryId] // Extract revocation status list for the given timestamp const revocationStatusList = revocationStatusLists[timestamp] if (!revocationStatusList) { throw new AriesFrameworkError( - `Revocation status list for revocation registry ${revocationRegistryDefinitionId} and timestamp ${timestamp} not found in revocation status lists. All revocation status lists must be present.` + `Revocation status list for revocation registry ${revocationRegistryId} and timestamp ${timestamp} not found in revocation status lists. All revocation status lists must be present.` ) } revocationRegistryDefinition = RevocationRegistryDefinition.fromJson(definition as unknown as JsonObject) revocationState = CredentialRevocationState.create({ - revocationRegistryIndex: Number(revocationRegistryIndex), + revocationRegistryIndex: Number(credentialRevocationId), revocationRegistryDefinition, tailsPath: tailsFilePath, revocationStatusList: RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject), }) } + return { - linkSecretId: credentialRecord.linkSecretId, + linkSecretId: credentialRecord.anonCredsCredentialMetadata.linkSecretId, credentialEntry: { - credential: credentialRecord.credential as unknown as JsonObject, + credential: w3cToLegacyCredential( + credentialRecord.credential as W3cJsonLdVerifiableCredential + ) as unknown as JsonObject, revocationState: revocationState?.toJson(), timestamp, }, @@ -246,8 +266,8 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } - public async storeCredential(agentContext: AgentContext, options: StoreCredentialOptions): Promise { - const { credential, credentialDefinition, credentialRequestMetadata, revocationRegistry, schema } = options + public async legacyToW3cCredential(agentContext: AgentContext, options: StoreCredentialOptions) { + const { credential, credentialDefinition, credentialRequestMetadata, revocationRegistry } = options const linkSecretRecord = await agentContext.dependencyManager .resolve(AnonCredsLinkSecretRepository) @@ -257,69 +277,111 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { throw new AnonCredsRsError('Link Secret value not stored') } - const revocationRegistryDefinition = revocationRegistry?.definition as unknown as JsonObject + const w3cJsonLdCredential = await legacyCredentialToW3cCredential(agentContext, credential, { + credentialDefinition: credentialDefinition as unknown as JsonObject, + credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject, + linkSecret: linkSecretRecord.value, + revocationRegistryDefinition: revocationRegistry?.definition as unknown as JsonObject, + }) + + return w3cJsonLdCredential + } - const credentialId = options.credentialId ?? utils.uuid() + public async storeW3cCredential(agentContext: AgentContext, options: StoreW3cCredentialOptions): Promise { + const { + credential, + credentialRequestMetadata, + schema, + revocationRegistry, + credentialDefinition, + credentialDefinitionId: credDefId, + } = options - let credentialObj: Credential | undefined - let processedCredential: Credential | undefined - try { - credentialObj = Credential.fromJson(credential as unknown as JsonObject) - processedCredential = credentialObj.process({ - credentialDefinition: credentialDefinition as unknown as JsonObject, - credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject, - linkSecret: linkSecretRecord.value, - revocationRegistryDefinition, - }) + const methodName = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, credDefId).methodName - const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) - - const methodName = agentContext.dependencyManager - .resolve(AnonCredsRegistryService) - .getRegistryForIdentifier(agentContext, credential.cred_def_id).methodName - - await credentialRepository.save( - agentContext, - new AnonCredsCredentialRecord({ - credential: processedCredential.toJson() as unknown as AnonCredsCredential, - credentialId, - linkSecretId: linkSecretRecord.linkSecretId, - issuerId: options.credentialDefinition.issuerId, - schemaName: schema.name, - schemaIssuerId: schema.issuerId, - schemaVersion: schema.version, - credentialRevocationId: processedCredential.revocationRegistryIndex?.toString(), - methodName, - }) - ) - - return credentialId - } finally { - credentialObj?.handle.clear() - processedCredential?.handle.clear() + const linkSecretRecord = await agentContext.dependencyManager + .resolve(AnonCredsLinkSecretRepository) + .getByLinkSecretId(agentContext, credentialRequestMetadata.link_secret_name) + + if (!linkSecretRecord.value) { + throw new AnonCredsRsError('Link Secret value not stored') } + + const { schemaId, schemaIssuerId, revocationRegistryId, credentialDefinitionId } = await fetchQualifiedIds( + agentContext, + { + schemaId: credentialDefinition.schemaId, + schemaIssuerId: schema.issuerId, + revocationRegistryId: revocationRegistry?.id, + credentialDefinitionId: credDefId, + } + ) + + const credentialRevocationId = AW3cCredential.fromJson(JsonTransformer.toJSON(credential)).revocationRegistryIndex + + const credentialId = options.credentialId ?? utils.uuid() + + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + await w3cCredentialService.storeCredential(agentContext, { + credential, + anonCredsCredentialRecordOptions: { + credentialId, + linkSecretId: linkSecretRecord.linkSecretId, + credentialDefinitionId, + schemaId, + schemaName: schema.name, + schemaIssuerId, + schemaVersion: schema.version, + methodName, + revocationRegistryId: revocationRegistryId, + credentialRevocationId: credentialRevocationId?.toString(), + }, + }) + + return credentialId + } + + // convert legacy to w3c and call store w3c + public async storeCredential(agentContext: AgentContext, options: StoreCredentialOptions): Promise { + const w3cJsonLdCredential = await this.legacyToW3cCredential(agentContext, options) + + return await this.storeW3cCredential(agentContext, { + ...options, + credential: w3cJsonLdCredential, + }) } public async getCredential( agentContext: AgentContext, options: GetCredentialOptions ): Promise { - const credentialRecord = await agentContext.dependencyManager - .resolve(AnonCredsCredentialRepository) - .getByCredentialId(agentContext, options.credentialId) + const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const credentialRecord = await credentialRepository.getByCredentialId(agentContext, options.credentialId) + + return this.anoncredsMetadataFromRecord(credentialRecord) + } + + private anoncredsMetadataFromRecord(w3cCredentialRecord: W3cCredentialRecord): AnonCredsCredentialInfo { + if (Array.isArray(w3cCredentialRecord.credential.credentialSubject)) + throw new AriesFrameworkError('Credential subject must be an object, not an array.') + + const anonCredsTags = w3cCredentialRecord.getAnonCredsTags() + if (!anonCredsTags) throw new AriesFrameworkError('AnonCreds tags not found on credential record.') + + const anoncredsCredentialMetadata = w3cCredentialRecord.anonCredsCredentialMetadata + if (!anoncredsCredentialMetadata) + throw new AriesFrameworkError('AnonCreds metadata not found on credential record.') - const attributes: { [key: string]: string } = {} - for (const attribute in credentialRecord.credential.values) { - attributes[attribute] = credentialRecord.credential.values[attribute].raw - } return { - attributes, - credentialDefinitionId: credentialRecord.credential.cred_def_id, - credentialId: credentialRecord.credentialId, - schemaId: credentialRecord.credential.schema_id, - credentialRevocationId: credentialRecord.credentialRevocationId, - revocationRegistryId: credentialRecord.credential.rev_reg_id, - methodName: credentialRecord.methodName, + attributes: (w3cCredentialRecord.credential.credentialSubject.claims as AnonCredsClaimRecord) ?? {}, + credentialId: anoncredsCredentialMetadata.credentialId, + credentialDefinitionId: anonCredsTags.credentialDefinitionId, + schemaId: anonCredsTags.schemaId, + credentialRevocationId: anoncredsCredentialMetadata.credentialRevocationId, + revocationRegistryId: anonCredsTags.revocationRegistryId, + methodName: anoncredsCredentialMetadata.methodName, } } @@ -328,7 +390,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { options: GetCredentialsOptions ): Promise { const credentialRecords = await agentContext.dependencyManager - .resolve(AnonCredsCredentialRepository) + .resolve(W3cCredentialRepository) .findByQuery(agentContext, { credentialDefinitionId: options.credentialDefinitionId, schemaId: options.schemaId, @@ -339,21 +401,11 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { methodName: options.methodName, }) - return credentialRecords.map((credentialRecord) => ({ - attributes: Object.fromEntries( - Object.entries(credentialRecord.credential.values).map(([key, value]) => [key, value.raw]) - ), - credentialDefinitionId: credentialRecord.credential.cred_def_id, - credentialId: credentialRecord.credentialId, - schemaId: credentialRecord.credential.schema_id, - credentialRevocationId: credentialRecord.credentialRevocationId, - revocationRegistryId: credentialRecord.credential.rev_reg_id, - methodName: credentialRecord.methodName, - })) + return credentialRecords.map((credentialRecord) => this.anoncredsMetadataFromRecord(credentialRecord)) } public async deleteCredential(agentContext: AgentContext, credentialId: string): Promise { - const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) const credentialRecord = await credentialRepository.getByCredentialId(agentContext, credentialId) await credentialRepository.delete(agentContext, credentialRecord) } @@ -376,7 +428,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { // Make sure the attribute(s) that are requested are present using the marker tag const attributes = requestedAttribute.names ?? [requestedAttribute.name] - const attributeQuery: SimpleQuery = {} + const attributeQuery: SimpleQuery = {} for (const attribute of attributes) { attributeQuery[`attr::${attribute}::marker`] = true } @@ -396,38 +448,26 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } const credentials = await agentContext.dependencyManager - .resolve(AnonCredsCredentialRepository) + .resolve(W3cCredentialRepository) .findByQuery(agentContext, { $and, }) return credentials.map((credentialRecord) => { - const attributes: { [key: string]: string } = {} - for (const attribute in credentialRecord.credential.values) { - attributes[attribute] = credentialRecord.credential.values[attribute].raw - } return { - credentialInfo: { - attributes, - credentialDefinitionId: credentialRecord.credential.cred_def_id, - credentialId: credentialRecord.credentialId, - schemaId: credentialRecord.credential.schema_id, - credentialRevocationId: credentialRecord.credentialRevocationId, - revocationRegistryId: credentialRecord.credential.rev_reg_id, - methodName: credentialRecord.methodName, - }, + credentialInfo: this.anoncredsMetadataFromRecord(credentialRecord), interval: proofRequest.non_revoked, } }) } private queryFromRestrictions(restrictions: AnonCredsProofRequestRestriction[]) { - const query: Query[] = [] + const query: Query[] = [] const { restrictions: parsedRestrictions } = JsonTransformer.fromJSON({ restrictions }, AnonCredsRestrictionWrapper) for (const restriction of parsedRestrictions) { - const queryElements: SimpleQuery = {} + const queryElements: SimpleQuery = {} if (restriction.credentialDefinitionId) { queryElements.credentialDefinitionId = restriction.credentialDefinitionId diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index f4c666de56..631c6a7c9e 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -308,7 +308,6 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { if (isUnqualifiedCredentialDefinitionId(options.credentialRequest.cred_def_id)) { const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(credentialDefinition.schemaId) const { namespaceIdentifier: unqualifiedDid } = parseIndyDid(credentialDefinition.issuerId) - parseIndyDid credentialDefinition = { ...credentialDefinition, schemaId: getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion), diff --git a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts index e16561fa86..15f171a11a 100644 --- a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts +++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts @@ -1,28 +1,43 @@ import type { AnonCredsCredentialDefinition, + AnonCredsCredentialRequestMetadata, AnonCredsProofRequest, + AnonCredsRevocationRegistryDefinition, AnonCredsRevocationStatusList, - AnonCredsCredential, AnonCredsSchema, AnonCredsSelectedCredentials, - AnonCredsRevocationRegistryDefinition, - AnonCredsCredentialRequestMetadata, } from '@aries-framework/anoncreds' -import type { JsonObject } from '@hyperledger/anoncreds-nodejs' +import type { JsonObject } from '@hyperledger/anoncreds-shared' import { - AnonCredsModuleConfig, AnonCredsHolderServiceSymbol, AnonCredsLinkSecretRecord, - AnonCredsCredentialRecord, + AnonCredsModuleConfig, } from '@aries-framework/anoncreds' -import { anoncreds, RevocationRegistryDefinition } from '@hyperledger/anoncreds-nodejs' - +import { + ConsoleLogger, + DidResolverService, + DidsModuleConfig, + Ed25519Signature2018, + InjectionSymbols, + KeyType, + SignatureSuiteToken, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredentialRecord, + W3cCredentialRepository, + W3cCredentialSubject, + W3cCredentialsModuleConfig, + W3cJsonLdVerifiableCredential, +} from '@aries-framework/core' +import { RevocationRegistryDefinition, anoncreds } from '@hyperledger/anoncreds-nodejs' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' import { AnonCredsCredentialDefinitionRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialDefinitionRepository' -import { AnonCredsCredentialRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialRepository' import { AnonCredsLinkSecretRepository } from '../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository' import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService' import { @@ -44,15 +59,21 @@ jest.mock('../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository') const AnonCredsLinkSecretRepositoryMock = AnonCredsLinkSecretRepository as jest.Mock const anoncredsLinkSecretRepositoryMock = new AnonCredsLinkSecretRepositoryMock() -jest.mock('../../../../anoncreds/src/repository/AnonCredsCredentialRepository') -const AnonCredsCredentialRepositoryMock = AnonCredsCredentialRepository as jest.Mock -const anoncredsCredentialRepositoryMock = new AnonCredsCredentialRepositoryMock() +jest.mock('../../../../core/src/modules/vc/repository/W3cCredentialRepository') +const W3cCredentialRepositoryMock = W3cCredentialRepository as jest.Mock +const w3cCredentialRepositoryMock = new W3cCredentialRepositoryMock() + +const inMemoryStorageService = new InMemoryStorageService() +const logger = new ConsoleLogger() const agentContext = getAgentContext({ registerInstances: [ + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.StorageService, inMemoryStorageService], + [InjectionSymbols.Stop$, new Subject()], [AnonCredsCredentialDefinitionRepository, credentialDefinitionRepositoryMock], [AnonCredsLinkSecretRepository, anoncredsLinkSecretRepositoryMock], - [AnonCredsCredentialRepository, anoncredsCredentialRepositoryMock], + [W3cCredentialRepository, w3cCredentialRepositoryMock], [AnonCredsHolderServiceSymbol, anonCredsHolderService], [ AnonCredsModuleConfig, @@ -60,13 +81,28 @@ const agentContext = getAgentContext({ registries: [new InMemoryAnonCredsRegistry({})], }), ], + [InjectionSymbols.Logger, logger], + [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], ], agentConfig, }) describe('AnonCredsRsHolderService', () => { - const getByCredentialIdMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'getByCredentialId') - const findByQueryMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'findByQuery') + const getByCredentialIdMock = jest.spyOn(w3cCredentialRepositoryMock, 'getByCredentialId') + const findByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findByQuery') beforeEach(() => { getByCredentialIdMock.mockClear() @@ -217,27 +253,37 @@ describe('AnonCredsRsHolderService', () => { } getByCredentialIdMock.mockResolvedValueOnce( - new AnonCredsCredentialRecord({ + new W3cCredentialRecord({ credential: personCredential, - credentialId: 'personCredId', - linkSecretId: 'linkSecretId', - issuerId: 'issuerDid', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', + tags: {}, + anonCredsCredentialRecordOptions: { + credentialId: 'personCredId', + linkSecretId: 'linkSecretId', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'inMemory', + schemaId: personCredentialInfo.schemaId, + credentialDefinitionId: personCredentialInfo.credentialDefinitionId, + revocationRegistryId: personCredentialInfo.revocationRegistryId, + }, }) ) getByCredentialIdMock.mockResolvedValueOnce( - new AnonCredsCredentialRecord({ + new W3cCredentialRecord({ credential: phoneCredential, - credentialId: 'phoneCredId', - linkSecretId: 'linkSecretId', - issuerId: 'issuerDid', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', + tags: {}, + anonCredsCredentialRecordOptions: { + credentialId: 'phoneCredId', + linkSecretId: 'linkSecretId', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'inMemory', + schemaId: phoneCredentialInfo.schemaId, + credentialDefinitionId: phoneCredentialInfo.credentialDefinitionId, + revocationRegistryId: phoneCredentialInfo.revocationRegistryId, + }, }) ) @@ -278,7 +324,7 @@ describe('AnonCredsRsHolderService', () => { }) describe('getCredentialsForProofRequest', () => { - const findByQueryMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'findByQuery') + const findByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findByQuery') const proofRequest: AnonCredsProofRequest = { nonce: anoncreds.generateNonce(), @@ -436,15 +482,20 @@ describe('AnonCredsRsHolderService', () => { test('deleteCredential', async () => { getByCredentialIdMock.mockRejectedValueOnce(new Error()) getByCredentialIdMock.mockResolvedValueOnce( - new AnonCredsCredentialRecord({ - credential: {} as AnonCredsCredential, - credentialId: 'personCredId', - linkSecretId: 'linkSecretId', - issuerId: 'issuerDid', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', + new W3cCredentialRecord({ + credential: {} as W3cJsonLdVerifiableCredential, + tags: {}, + anonCredsCredentialRecordOptions: { + credentialId: 'personCredId', + linkSecretId: 'linkSecretId', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'inMemory', + schemaId: 'schemaId', + credentialDefinitionId: 'credDefId', + revocationRegistryId: 'revRegId', + }, }) ) @@ -455,29 +506,39 @@ describe('AnonCredsRsHolderService', () => { expect(getByCredentialIdMock).toHaveBeenCalledWith(agentContext, 'credentialId') }) - test('getCredential', async () => { + test('get single Credential', async () => { getByCredentialIdMock.mockRejectedValueOnce(new Error()) getByCredentialIdMock.mockResolvedValueOnce( - new AnonCredsCredentialRecord({ - credential: { - cred_def_id: 'credDefId', - schema_id: 'schemaId', - signature: 'signature', - signature_correctness_proof: 'signatureCorrectnessProof', - values: { attr1: { raw: 'value1', encoded: 'encvalue1' }, attr2: { raw: 'value2', encoded: 'encvalue2' } }, - rev_reg_id: 'revRegId', - } as AnonCredsCredential, - credentialId: 'myCredentialId', - credentialRevocationId: 'credentialRevocationId', - linkSecretId: 'linkSecretId', - issuerId: 'issuerDid', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', + new W3cCredentialRecord({ + credential: new W3cJsonLdVerifiableCredential({ + credentialSubject: new W3cCredentialSubject({ claims: { attr1: 'value1', attr2: 'value2' } }), + issuer: 'test', + issuanceDate: Date.now().toString(), + type: ['VerifiableCredential'], + proof: { + created: Date.now().toString(), + type: 'test', + proofPurpose: 'test', + verificationMethod: 'test', + }, + }), + tags: {}, + anonCredsCredentialRecordOptions: { + credentialId: 'myCredentialId', + credentialRevocationId: 'credentialRevocationId', + linkSecretId: 'linkSecretId', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'inMemory', + schemaId: 'schemaId', + credentialDefinitionId: 'credDefId', + revocationRegistryId: 'revRegId', + }, }) ) + expect( anonCredsHolderService.getCredential(agentContext, { credentialId: 'myCredentialId' }) ).rejects.toThrowError() @@ -496,23 +557,32 @@ describe('AnonCredsRsHolderService', () => { test('getCredentials', async () => { findByQueryMock.mockResolvedValueOnce([ - new AnonCredsCredentialRecord({ - credential: { - cred_def_id: 'credDefId', - schema_id: 'schemaId', - signature: 'signature', - signature_correctness_proof: 'signatureCorrectnessProof', - values: { attr1: { raw: 'value1', encoded: 'encvalue1' }, attr2: { raw: 'value2', encoded: 'encvalue2' } }, - rev_reg_id: 'revRegId', - } as AnonCredsCredential, - credentialId: 'myCredentialId', - credentialRevocationId: 'credentialRevocationId', - linkSecretId: 'linkSecretId', - issuerId: 'issuerDid', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', + new W3cCredentialRecord({ + credential: new W3cJsonLdVerifiableCredential({ + credentialSubject: new W3cCredentialSubject({ claims: { attr1: 'value1', attr2: 'value2' } }), + issuer: 'test', + issuanceDate: Date.now().toString(), + type: ['VerifiableCredential'], + proof: { + created: Date.now().toString(), + type: 'test', + proofPurpose: 'test', + verificationMethod: 'test', + }, + }), + tags: {}, + anonCredsCredentialRecordOptions: { + credentialId: 'myCredentialId', + credentialRevocationId: 'credentialRevocationId', + linkSecretId: 'linkSecretId', + schemaIssuerId: 'schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'inMemory', + schemaId: 'schemaId', + credentialDefinitionId: 'credDefId', + revocationRegistryId: 'revRegId', + }, }), ]) @@ -561,7 +631,7 @@ describe('AnonCredsRsHolderService', () => { const schema: AnonCredsSchema = { attrNames: ['name', 'sex', 'height', 'age'], - issuerId: 'issuerId', + issuerId: 'did:indy:example:issuerId', name: 'schemaName', version: '1', } @@ -584,11 +654,11 @@ describe('AnonCredsRsHolderService', () => { revocationRegistryDefinitionId: 'personrevregid:uri', }) - const saveCredentialMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'save') + const saveCredentialMock = jest.spyOn(w3cCredentialRepositoryMock, 'save') saveCredentialMock.mockResolvedValue() - const credentialId = await anonCredsHolderService.storeCredential(agentContext, { + const credentialId = await anonCredsHolderService.storeW3cCredential(agentContext, { credential, credentialDefinition, schema, @@ -607,13 +677,14 @@ describe('AnonCredsRsHolderService', () => { expect(saveCredentialMock).toHaveBeenCalledWith( agentContext, expect.objectContaining({ + anonCredsCredentialMetadata: expect.objectContaining({ + credentialId: 'personCredId', + linkSecretId: 'linkSecretId', + }), // The stored credential is different from the one received originally - credentialId: 'personCredId', - linkSecretId: 'linkSecretId', _tags: expect.objectContaining({ - issuerId: credentialDefinition.issuerId, schemaName: 'schemaName', - schemaIssuerId: 'issuerId', + schemaIssuerId: 'did:indy:example:issuerId', schemaVersion: '1', }), }) diff --git a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts index e8c28fa31e..30eedaf4b7 100644 --- a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts +++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts @@ -1,31 +1,42 @@ import type { AnonCredsProofRequest } from '@aries-framework/anoncreds' import { - getUnqualifiedSchemaId, - parseIndySchemaId, - getUnqualifiedCredentialDefinitionId, - parseIndyCredentialDefinitionId, - AnonCredsModuleConfig, - AnonCredsHolderServiceSymbol, - AnonCredsIssuerServiceSymbol, - AnonCredsVerifierServiceSymbol, - AnonCredsSchemaRepository, - AnonCredsSchemaRecord, - AnonCredsCredentialDefinitionRecord, - AnonCredsCredentialDefinitionRepository, AnonCredsCredentialDefinitionPrivateRecord, AnonCredsCredentialDefinitionPrivateRepository, - AnonCredsKeyCorrectnessProofRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionRepository, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, AnonCredsKeyCorrectnessProofRecord, - AnonCredsLinkSecretRepository, + AnonCredsKeyCorrectnessProofRepository, AnonCredsLinkSecretRecord, + AnonCredsLinkSecretRepository, + AnonCredsModuleConfig, + AnonCredsSchemaRecord, + AnonCredsSchemaRepository, + AnonCredsVerifierServiceSymbol, + getUnqualifiedCredentialDefinitionId, + getUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, + parseIndySchemaId, } from '@aries-framework/anoncreds' -import { InjectionSymbols } from '@aries-framework/core' +import { + ConsoleLogger, + DidResolverService, + DidsModuleConfig, + Ed25519Signature2018, + InjectionSymbols, + KeyType, + SignatureSuiteToken, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredentialsModuleConfig, + encodeCredentialValue, +} from '@aries-framework/core' import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { encodeCredentialValue } from '../../../../anoncreds/src/utils/credential' import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService' @@ -39,6 +50,8 @@ const anonCredsIssuerService = new AnonCredsRsIssuerService() const storageService = new InMemoryStorageService() const registry = new InMemoryAnonCredsRegistry() +const logger = new ConsoleLogger() + const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], @@ -53,6 +66,22 @@ const agentContext = getAgentContext({ registries: [registry], }), ], + + [InjectionSymbols.Logger, logger], + [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], ], agentConfig, }) @@ -184,12 +213,12 @@ describe('AnonCredsRsServices', () => { expect(credentialInfo).toEqual({ credentialId, attributes: { - age: '25', + age: 25, name: 'John', }, schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: null, + revocationRegistryId: undefined, credentialRevocationId: undefined, // Should it be null in this case? methodName: 'inMemory', }) @@ -397,7 +426,7 @@ describe('AnonCredsRsServices', () => { }, schemaId: unqualifiedSchemaId, credentialDefinitionId: unqualifiedCredentialDefinitionId, - revocationRegistryId: null, + revocationRegistryId: undefined, credentialRevocationId: undefined, // Should it be null in this case? methodName: 'inMemory', }) diff --git a/packages/anoncreds-rs/src/services/__tests__/helpers.ts b/packages/anoncreds-rs/src/services/__tests__/helpers.ts index 47af6c909c..a05848d9d3 100644 --- a/packages/anoncreds-rs/src/services/__tests__/helpers.ts +++ b/packages/anoncreds-rs/src/services/__tests__/helpers.ts @@ -1,14 +1,12 @@ -import type { - AnonCredsCredential, - AnonCredsCredentialDefinition, - AnonCredsCredentialInfo, - AnonCredsCredentialOffer, -} from '@aries-framework/anoncreds' -import type { JsonObject } from '@hyperledger/anoncreds-nodejs' +import type { JsonObject } from '@hyperledger/anoncreds-shared' import { - anoncreds, - Credential, + type AnonCredsCredentialDefinition, + type AnonCredsCredentialInfo, + type AnonCredsCredentialOffer, +} from '@aries-framework/anoncreds' +import { JsonTransformer, W3cJsonLdVerifiableCredential } from '@aries-framework/core' +import { CredentialDefinition, CredentialOffer, CredentialRequest, @@ -18,6 +16,8 @@ import { RevocationRegistryDefinitionPrivate, RevocationStatusList, Schema, + W3cCredential, + anoncreds, } from '@hyperledger/anoncreds-shared' /** @@ -37,7 +37,7 @@ export function createCredentialDefinition(options: { attributeNames: string[]; const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = CredentialDefinition.create({ issuerId, schema, - schemaId: 'schema:uri', + schemaId: 'did:indy:sovrin:F72i3Y3Q4i466efjYJYCHM/anoncreds/v0/SCHEMA/npdb/4.3.4', signatureType: 'CL', supportRevocation: true, // FIXME: Revocation should not be mandatory but current anoncreds-rs is requiring it tag: 'TAG', @@ -65,7 +65,7 @@ export function createCredentialOffer(keyCorrectnessProof: Record Promise.resolve('947121108704767252195123') } as Wallet const inMemoryStorageService = new InMemoryStorageService() + +const logger = new ConsoleLogger() + const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], @@ -69,8 +81,23 @@ const agentContext = getAgentContext({ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [InjectionSymbols.Logger, logger], + [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], ], agentConfig, wallet, @@ -338,7 +365,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean }) expect(holderCredentialRecord.credentials).toEqual([ - { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, ]) const credentialId = holderCredentialRecord.credentials[0].credentialRecordId @@ -349,12 +376,12 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean expect(anonCredsCredential).toEqual({ credentialId, attributes: { - age: '25', + age: 25, name: 'John', }, schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, + revocationRegistryId: revocable ? revocationRegistryDefinitionId : undefined, credentialRevocationId: revocable ? '1' : undefined, methodName: 'inMemory', }) diff --git a/packages/anoncreds-rs/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds-rs/tests/data-integrity-flow-anoncreds.test.ts new file mode 100644 index 0000000000..531c1e4d3d --- /dev/null +++ b/packages/anoncreds-rs/tests/data-integrity-flow-anoncreds.test.ts @@ -0,0 +1,523 @@ +import type { DataIntegrityCredentialRequest } from '@aries-framework/core' + +import { + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionRepository, + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRecord, + AnonCredsLinkSecretRepository, + AnonCredsModuleConfig, + AnonCredsProofFormatService, + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationRegistryState, + AnonCredsSchemaRecord, + AnonCredsSchemaRepository, + AnonCredsVerifierServiceSymbol, +} from '@aries-framework/anoncreds' +import { + AgentContext, + CredentialExchangeRecord, + CredentialPreviewAttribute, + CredentialState, + DidResolverService, + DidsModuleConfig, + Ed25519Signature2018, + InjectionSymbols, + KeyDidRegistrar, + KeyDidResolver, + KeyType, + ProofExchangeRecord, + ProofState, + SignatureSuiteToken, + SigningProviderRegistry, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cCredentialsModuleConfig, +} from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' +import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { AnonCredsRsHolderService } from '../src/services/AnonCredsRsHolderService' +import { AnonCredsRsIssuerService } from '../src/services/AnonCredsRsIssuerService' +import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierService' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' + +const registry = new InMemoryAnonCredsRegistry() +const tailsFileService = new InMemoryTailsFileService() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], + tailsFileService, +}) + +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const inMemoryStorageService = new InMemoryStorageService() + +const logger = agentConfig.logger + +const didsModuleConfig = new DidsModuleConfig({ + registrars: [new KeyDidRegistrar()], + resolvers: [new KeyDidResolver()], +}) +const fileSystem = new agentDependencies.FileSystem() + +const wallet = new RegisteredAskarTestWallet( + agentConfig.logger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([]) +) + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, fileSystem], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [InjectionSymbols.Logger, logger], + [InjectionSymbols.Logger, logger], + [DidsModuleConfig, didsModuleConfig], + [DidResolverService, new DidResolverService(logger, didsModuleConfig)], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], + ], + agentConfig, + wallet, +}) + +agentContext.dependencyManager.registerInstance(AgentContext, agentContext) + +const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() +const anoncredsProofFormatService = new AnonCredsProofFormatService() + +const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + +describe('data integrity format service (anoncreds)', () => { + beforeAll(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + }) + + afterEach(async () => { + inMemoryStorageService.records = {} + }) + + test('issuance and verification flow anoncreds starting from offer without negotiation and without revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: false }) + }) + + test('issuance and verification flow anoncreds starting from offer without negotiation and with revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: true }) + }) +}) + +async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean }) { + const { issuerId, revocable } = options + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + if (!schemaState.schema || !schemaState.schemaId) { + throw new Error('Failed to create schema') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', + }) + ) + + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId, + schemaId: schemaState.schemaId as string, + schema, + tag: 'Employee Credential', + supportRevocation: revocable, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { + throw new Error('Failed to create credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + let revocationRegistryDefinitionId: string | undefined + if (revocable) { + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { + issuerId: issuerId, + credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + maximumCredentialNumber: 100, + tailsDirectoryPath: await tailsFileService.getTailsBasePath(agentContext), + tag: 'default', + }) + + // At this moment, tails file should be published and a valid public URL will be received + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { + revocationRegistryDefinition, + options: {}, + }) + + revocationRegistryDefinitionId = revocationRegistryDefinitionState.revocationRegistryDefinitionId + + if ( + !revocationRegistryDefinitionState.revocationRegistryDefinition || + !revocationRegistryDefinitionId || + !revocationRegistryDefinitionPrivate + ) { + throw new Error('Failed to create revocation registry') + } + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionRecord({ + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + revocationRegistryDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository).save( + agentContext, + new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + state: AnonCredsRevocationRegistryState.Active, + value: revocationRegistryDefinitionPrivate, + credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, + revocationRegistryDefinitionId, + }) + ) + + const createdRevocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { + issuerId: issuerId, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + tailsFilePath: localTailsFilePath, + }) + + const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { + revocationStatusList: createdRevocationStatusList, + options: {}, + }) + + if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { + throw new Error('Failed to create revocation status list') + } + } + + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) + expect(linkSecret.linkSecretId).toBe('linkSecretId') + + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, + }) + ) + + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const credentialAttributes = [ + new CredentialPreviewAttribute({ name: 'name', value: 'John' }), + new CredentialPreviewAttribute({ name: 'age', value: '25' }), + ] + + // Set attributes on the credential record, this is normally done by the protocol service + holderCredentialRecord.credentialAttributes = credentialAttributes + issuerCredentialRecord.credentialAttributes = credentialAttributes + + // -------------------------------------------------------------------------------------------------------- + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerId, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: '25' } }), + }) + + const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { + credentialRecord: issuerCredentialRecord, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + anonCredsLinkSecretBindingMethodOptions: { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryDefinitionId, + revocationRegistryIndex: revocable ? 1 : undefined, + }, + didCommSignedAttachmentBindingMethodOptions: {}, + }, + }, + }) + + // Holder processes and accepts offer + await dataIntegrityCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment, appendAttachments: requestAppendAttachments } = + await dataIntegrityCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + dataIntegrity: { + dataModelVersion: '1.1', + anonCredsLinkSecretCredentialRequestOptions: { + linkSecretId: linkSecret.linkSecretId, + }, + }, + }, + }) + + // Make sure the request contains an entropy and does not contain a prover_did field + expect( + (requestAttachment.getDataAsJson() as DataIntegrityCredentialRequest).binding_proof?.anoncreds_link_secret?.entropy + ).toBeDefined() + expect((requestAttachment.getDataAsJson() as any).prover_did).toBeUndefined() + + // Issuer processes and accepts request + await dataIntegrityCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await dataIntegrityCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + requestAppendAttachments, + credentialFormats: { dataIntegrity: {} }, + }) + + // Holder processes and accepts credential + await dataIntegrityCredentialFormatService.processCredential(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) + const credentialId = credentialRecord.getAnonCredsTags()?.credentialId + if (!credentialId) throw new Error('Credential ID not found') + + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + credentialId, + }) + + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: 25, + name: 'John', + }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocable ? revocationRegistryDefinitionId : undefined, + credentialRevocationId: revocable ? '1' : undefined, + methodName: 'inMemory', + }) + + const expectedCredentialMetadata = revocable + ? { + linkSecretMetadata: { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocationRegistryDefinitionId, + credentialRevocationId: '1', + }, + } + : { + linkSecretMetadata: { + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }, + } + expect(holderCredentialRecord.metadata.data).toEqual({ + '_dataIntegrity/credential': expectedCredentialMetadata, + '_dataIntegrity/credentialRequest': { + linkSecretRequestMetadata: { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_dataIntegrity/credential': expectedCredentialMetadata, + }) + + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalReceived, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + + const nrpRequestedTime = dateToTimestamp(new Date()) + + const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { + proofFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, + }, + }, + proofRecord: holderProofRecord, + }) + + await anoncredsProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) + + const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) + + await anoncredsProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) + + const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) + + const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, + }) + + expect(isValid).toBe(true) +} diff --git a/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts new file mode 100644 index 0000000000..54dfa49e89 --- /dev/null +++ b/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts @@ -0,0 +1,286 @@ +import type { KeyDidCreateOptions } from '@aries-framework/core' + +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsModuleConfig, + AnonCredsVerifierServiceSymbol, +} from '@aries-framework/anoncreds' +import { + AgentContext, + CredentialExchangeRecord, + CredentialPreviewAttribute, + CredentialState, + DidKey, + DidResolverService, + DidsApi, + DidsModuleConfig, + Ed25519Signature2018, + InjectionSymbols, + KeyDidRegistrar, + KeyDidResolver, + KeyType, + SignatureSuiteToken, + SigningProviderRegistry, + TypedArrayEncoder, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cCredentialsModuleConfig, +} from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' +import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { AnonCredsRsHolderService } from '../src/services/AnonCredsRsHolderService' +import { AnonCredsRsIssuerService } from '../src/services/AnonCredsRsIssuerService' +import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierService' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' + +const registry = new InMemoryAnonCredsRegistry() +const tailsFileService = new InMemoryTailsFileService() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], + tailsFileService, +}) + +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const inMemoryStorageService = new InMemoryStorageService() + +const logger = agentConfig.logger + +const didsModuleConfig = new DidsModuleConfig({ + registrars: [new KeyDidRegistrar()], + resolvers: [new KeyDidResolver()], +}) +const fileSystem = new agentDependencies.FileSystem() + +const wallet = new RegisteredAskarTestWallet( + agentConfig.logger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([]) +) + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, fileSystem], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [InjectionSymbols.Logger, logger], + [InjectionSymbols.Logger, logger], + [DidsModuleConfig, didsModuleConfig], + [DidResolverService, new DidResolverService(logger, didsModuleConfig)], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], + ], + agentConfig, + wallet, +}) + +agentContext.dependencyManager.registerInstance(AgentContext, agentContext) + +const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() + +const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + +describe('data integrity format service (w3c)', () => { + let issuer: Awaited> + let holder: Awaited> + + beforeAll(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + + issuer = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') + holder = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') + }) + + afterEach(async () => { + inMemoryStorageService.records = {} + }) + + test('issuance and verification flow w3c starting from offer without negotiation and without revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: false, issuer, holder }) + }) +}) + +export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey: string) { + const dids = agentContext.dependencyManager.resolve(DidsApi) + const didCreateResult = await dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString(secretKey) }, + }) + + const did = didCreateResult.didState.did as string + const didKey = DidKey.fromDid(did) + const kid = `${did}#${didKey.key.fingerprint}` + + const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + return { + did, + kid, + verificationMethod, + } +} + +async function anonCredsFlowTest(options: { + issuerId: string + revocable: boolean + issuer: Awaited> + holder: Awaited> +}) { + const { issuer, holder } = options + + 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' }), + ] + + // Set attributes on the credential record, this is normally done by the protocol service + holderCredentialRecord.credentialAttributes = credentialAttributes + issuerCredentialRecord.credentialAttributes = credentialAttributes + + // -------------------------------------------------------------------------------------------------------- + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuer.did, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: '25' } }), + }) + + const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { + credentialRecord: issuerCredentialRecord, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + didCommSignedAttachmentBindingMethodOptions: {}, + }, + }, + }) + + // Holder processes and accepts offer + await dataIntegrityCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment, appendAttachments: requestAppendAttachments } = + await dataIntegrityCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + dataIntegrity: { + didCommSignedAttachmentCredentialRequestOptions: { + kid: holder.kid, + }, + }, + }, + }) + + // Issuer processes and accepts request + await dataIntegrityCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await dataIntegrityCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + requestAppendAttachments, + credentialFormats: { + dataIntegrity: { + didCommSignedAttachmentAcceptRequestOptions: { + kid: issuer.kid, + }, + }, + }, + }) + + // Holder processes and accepts credential + await dataIntegrityCredentialFormatService.processCredential(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + await expect( + anonCredsHolderService.getCredential(agentContext, { + credentialId: holderCredentialRecord.id, + }) + ).rejects.toThrow() + + const expectedCredentialMetadata = {} + expect(holderCredentialRecord.metadata.data).toEqual({ + '_dataIntegrity/credential': expectedCredentialMetadata, + '_dataIntegrity/credentialRequest': {}, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_dataIntegrity/credential': expectedCredentialMetadata, + }) + + const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) + const credentialId = credentialRecord.getAnonCredsTags()?.credentialId + expect(credentialId).toBeUndefined() + + expect(credentialRecord.credential).toEqual({ + ...credential, + proof: expect.any(Object), + }) +} diff --git a/packages/anoncreds-rs/tests/indy-flow.test.ts b/packages/anoncreds-rs/tests/indy-flow.test.ts index 44adce6fc8..eebadc8166 100644 --- a/packages/anoncreds-rs/tests/indy-flow.test.ts +++ b/packages/anoncreds-rs/tests/indy-flow.test.ts @@ -30,6 +30,16 @@ import { InjectionSymbols, ProofState, ProofExchangeRecord, + Ed25519Signature2018, + KeyType, + SignatureSuiteRegistry, + SignatureSuiteToken, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredentialsModuleConfig, + ConsoleLogger, + DidResolverService, + DidsModuleConfig, } from '@aries-framework/core' import { Subject } from 'rxjs' @@ -53,6 +63,7 @@ const anonCredsIssuerService = new AnonCredsRsIssuerService() const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet +const logger = new ConsoleLogger() const inMemoryStorageService = new InMemoryStorageService() const agentContext = getAgentContext({ registerInstances: [ @@ -63,7 +74,22 @@ const agentContext = getAgentContext({ [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], [AnonCredsRegistryService, new AnonCredsRegistryService()], + [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], + [InjectionSymbols.Logger, logger], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], [AnonCredsModuleConfig, anonCredsModuleConfig], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], ], agentConfig, wallet, @@ -284,7 +310,7 @@ describe('Legacy indy format services using anoncreds-rs', () => { }, schemaId: unqualifiedSchemaId, credentialDefinitionId: unqualifiedCredentialDefinitionId, - revocationRegistryId: null, + revocationRegistryId: undefined, credentialRevocationId: undefined, // FIXME: should be null? methodName: 'inMemory', }) diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 2a232a35c3..e1f87a4b06 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -28,13 +28,18 @@ "bn.js": "^5.2.1", "class-transformer": "0.5.1", "class-validator": "0.14.0", - "reflect-metadata": "^0.1.13" + "reflect-metadata": "^0.1.13", + "@hyperledger/anoncreds-shared": "^0.2.0-dev.7" }, "devDependencies": { "@aries-framework/node": "0.4.2", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.7", "indy-sdk": "^1.16.0-dev-1636", "rimraf": "^4.4.0", "rxjs": "^7.8.0", "typescript": "~4.9.5" + }, + "peerDependencies": { + "@hyperledger/anoncreds-shared": "^0.2.0-dev.7" } } diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index afc698beda..4516353162 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -14,6 +14,7 @@ import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsC import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' import { updateAnonCredsModuleV0_3_1ToV0_4 } from './updates/0.3.1-0.4' +import { updateAnonCredsModuleV0_4_1ToV0_5 } from './updates/0.4-0.5' /** * @public @@ -48,5 +49,10 @@ export class AnonCredsModule implements Module { toVersion: '0.4', doUpdate: updateAnonCredsModuleV0_3_1ToV0_4, }, + { + fromVersion: '0.4.1', + toVersion: '0.5', + doUpdate: updateAnonCredsModuleV0_4_1ToV0_5, + }, ] satisfies Update[] } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts index 716265588a..4a1ac306a1 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts @@ -76,7 +76,7 @@ export type AnonCredsAcceptRequestFormat = Record export interface AnonCredsCredentialFormat extends CredentialFormat { formatKey: 'anoncreds' - credentialRecordType: 'anoncreds' + credentialRecordType: 'w3c' credentialFormats: { createProposal: AnonCredsProposeCredentialFormat acceptProposal: AnonCredsAcceptProposalFormat diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 9a53590d12..2e4a171254 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -73,7 +73,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService * credentialRecordType is the type of record that stores the credential. It is stored in the credential * record binding in the credential exchange record. */ - public readonly credentialRecordType = 'anoncreds' as const + public readonly credentialRecordType = 'w3c' as const /** * Create a {@link AttachmentFormats} object dependent on the message type. @@ -355,7 +355,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService if (!revocationStatusListResult.revocationStatusList) { throw new AriesFrameworkError( - `Unable to resolve revocation status list for ${revocationRegistryDefinitionId}: + `Unable to resolve revocation status list for ${revocationRegistryDefinitionId}: ${revocationStatusListResult.resolutionMetadata.error} ${revocationStatusListResult.resolutionMetadata.message}` ) } diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index e996a16e6f..419a9c3ebe 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -42,6 +42,7 @@ import { JsonEncoder, ProofFormatSpec, JsonTransformer, + encodeCredentialValue, } from '@aries-framework/core' import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest' @@ -53,7 +54,6 @@ import { areAnonCredsProofRequestsEqual, assertBestPracticeRevocationInterval, checkValidCredentialValueEncoding, - encodeCredentialValue, assertNoDuplicateGroupsNamesInProofRequest, getRevocationRegistriesForRequest, getRevocationRegistriesForProof, diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts new file mode 100644 index 0000000000..b7d5b6d2c9 --- /dev/null +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -0,0 +1,1051 @@ +import type { AnonCredsRevocationStatusList } from '../models' +import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' +import type { + DataIntegrityCredentialRequest, + DataIntegrityCredentialOffer, + AnonCredsLinkSecretBindingMethod, + DidCommSignedAttachmentBindingMethod, + DataIntegrityCredentialRequestBindingProof, + W3C_VC_DATA_MODEL_VERSION, + DataIntegrityCredential, + AnonCredsLinkSecretDataIntegrityBindingProof, + DidCommSignedAttachmentDataIntegrityBindingProof, + DataIntegrityOfferCredentialFormat, + DataIntegrityCredentialFormat, + DataIntegrityRequestMetadata, + DataIntegrityMetadata, + CredentialFormatService, + AgentContext, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatProcessOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateOfferOptions, + CredentialFormatAcceptOfferOptions, + CredentialFormatCreateReturn, + CredentialFormatAcceptRequestOptions, + CredentialFormatProcessCredentialOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatAutoRespondCredentialOptions, + CredentialExchangeRecord, + CredentialPreviewAttributeOptions, + JsonObject, + AnonCredsClaimRecord, + JwaSignatureAlgorithm, + JwsDetachedFormat, + AnonCredsCredentialRecordOptions, + DataIntegrityLinkSecretRequestMetadata, + DataIntegrityLinkSecretMetadata, +} from '@aries-framework/core' + +import { + ProblemReportError, + CredentialFormatSpec, + AriesFrameworkError, + Attachment, + JsonEncoder, + utils, + CredentialProblemReportReason, + JsonTransformer, + W3cCredential, + DidsApi, + W3cCredentialService, + W3cJsonLdVerifiableCredential, + getJwkClassFromKeyType, + AttachmentData, + JwsService, + getKeyFromVerificationMethod, + getJwkFromKey, + DataIntegrityRequestMetadataKey, + DataIntegrityMetadataKey, + ClaimFormat, + JwtPayload, + SignatureSuiteRegistry, + parseDid, +} from '@aries-framework/core' +import { W3cCredential as AW3cCredential } from '@hyperledger/anoncreds-shared' + +import { AnonCredsError } from '../error' +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryState, +} from '../repository' +import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' +import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' +import { dateToTimestamp, fetchObjectsFromLedger, fetchQualifiedIds, legacyCredentialToW3cCredential } from '../utils' +import { + convertAttributesToCredentialValues, + assertAttributesMatch as assertAttributesMatchSchema, +} from '../utils/credential' + +const W3C_DATA_INTEGRITY_CREDENTIAL_OFFER = 'didcomm/w3c-di-vc-offer@v0.1' +const W3C_DATA_INTEGRITY_CREDENTIAL_REQUEST = 'didcomm/w3c-di-vc-request@v0.1' +const W3C_DATA_INTEGRITY_CREDENTIAL = 'didcomm/w3c-di-vc@v0.1' + +export class DataIntegrityCredentialFormatService implements CredentialFormatService { + /** formatKey is the key used when calling agent.credentials.xxx with credentialFormats.anoncreds */ + public readonly formatKey = 'dataIntegrity' as const + + /** + * credentialRecordType is the type of record that stores the credential. It is stored in the credential + * record binding in the credential exchange record. + */ + public readonly credentialRecordType = 'w3c' 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( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { credentialFormats, credentialRecord }: CredentialFormatCreateProposalOptions + ): Promise { + throw new AriesFrameworkError('Not defined') + } + + public async processProposal( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { attachment }: CredentialFormatProcessOptions + ): Promise { + throw new AriesFrameworkError('Not defined') + } + + public async acceptProposal( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + input: CredentialFormatAcceptProposalOptions + ): Promise { + throw new AriesFrameworkError('Not defined') + } + + /** + * 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, + attachmentId, + }: CredentialFormatCreateOfferOptions + ): Promise { + const dataIntegrityFormat = credentialFormats.dataIntegrity + if (!dataIntegrityFormat) throw new AriesFrameworkError('Missing data integrity credential format data') + + const format = new CredentialFormatSpec({ + attachmentId: attachmentId, + format: W3C_DATA_INTEGRITY_CREDENTIAL_OFFER, + }) + + const credential = dataIntegrityFormat.credential + if ('proof' in credential) throw new AriesFrameworkError('Cannot offer a credential that already has a proof.') + + const { dataIntegrityCredentialOffer, previewAttributes } = await this.createDataIntegrityCredentialOffer( + agentContext, + credentialRecord, + dataIntegrityFormat + ) + + const attachment = this.getFormatData(dataIntegrityCredentialOffer, format.attachmentId) + return { format, attachment, previewAttributes } + } + + private enhanceCredentialOffer(credential: JsonObject, version: W3C_VC_DATA_MODEL_VERSION) { + // these modification ensure that the credential is valid + if (!credential.issuer) credential.issuer = 'https://example.issuer.com' + + if (version === '1.1') { + if (!credential.issuanceDate) credential.issuanceDate = new Date().toISOString() + } else if (version === '2.0') { + // do nothing + } else { + throw new AriesFrameworkError(`Unsupported data model version: ${version}`) + } + + return credential + } + + public async processOffer( + agentContext: AgentContext, + { attachment, credentialRecord }: CredentialFormatProcessOptions + ) { + agentContext.config.logger.debug( + `Processing data integrity credential offer for credential record ${credentialRecord.id}` + ) + + const { credential, data_model_versions_supported, binding_method, binding_required } = + attachment.getDataAsJson() + + // validate the credential + const credentialToBeValidated = this.enhanceCredentialOffer(credential, data_model_versions_supported[0]) + JsonTransformer.fromJSON(credentialToBeValidated, W3cCredential) + + const missingBindingMethod = + binding_required && !binding_method?.anoncreds_link_secret && !binding_method?.didcomm_signed_attachment + + const invalidDataModelVersions = + !data_model_versions_supported || + data_model_versions_supported.length === 0 || + data_model_versions_supported.some((v) => v !== '1.1' && v !== '2.0') + + const invalidLinkSecretBindingMethod = + binding_method?.anoncreds_link_secret && + (!binding_method.anoncreds_link_secret.cred_def_id || + !binding_method.anoncreds_link_secret.key_correctness_proof || + !binding_method.anoncreds_link_secret.nonce) + + const invalidDidCommSignedAttachmentBindingMethod = + binding_method?.didcomm_signed_attachment && + (!binding_method.didcomm_signed_attachment.algs_supported || + !binding_method.didcomm_signed_attachment.did_methods_supported || + !binding_method.didcomm_signed_attachment.nonce) + + if ( + missingBindingMethod || + invalidDataModelVersions || + invalidLinkSecretBindingMethod || + invalidDidCommSignedAttachmentBindingMethod + ) { + throw new ProblemReportError('Invalid credential offer', { + problemCode: CredentialProblemReportReason.IssuanceAbandoned, + }) + } + } + + private async createSignedAttachment( + agentContext: AgentContext, + data: { aud: string; nonce: string }, + options: { alg?: string; kid: string }, + issuerSupportedAlgs: string[] + ) { + const { alg, kid } = options + + if (!kid.startsWith('did:')) { + throw new AriesFrameworkError(`kid '${kid}' is not a DID. Only dids are supported for kid`) + } else if (!kid.includes('#')) { + throw new AriesFrameworkError( + `kid '${kid}' does not contain a fragment. kid MUST point to a specific key in the did document.` + ) + } + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid) + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (alg && !jwk.supportsSignatureAlgorithm(alg)) { + throw new AriesFrameworkError(`key type '${jwk.keyType}', does not support the JWS signature alg '${alg}'`) + } + + const signingAlg = issuerSupportedAlgs.find( + (supportedAlg) => jwk.supportsSignatureAlgorithm(supportedAlg) && (alg === undefined || alg === supportedAlg) + ) + if (!signingAlg) throw new AriesFrameworkError('No signing algorithm supported by the issuer found') + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + const jws = await jwsService.createJws(agentContext, { + key, + header: {}, + payload: new JwtPayload({ aud: data.aud, additionalClaims: { nonce: data.nonce } }), + protectedHeaderOptions: { alg: signingAlg, kid }, + }) + + const signedAttach = new Attachment({ + mimeType: typeof data === 'string' ? undefined : 'application/json', + data: new AttachmentData({ base64: jws.payload }), + }) + + signedAttach.addJws(jws) + + return signedAttach + } + + private async getSignedAttachmentPayload(agentContext: AgentContext, signedAttachment: Attachment) { + const jws = signedAttachment.data.jws as JwsDetachedFormat + if (!jws) throw new AriesFrameworkError('Missing jws in signed attachment') + if (!jws.protected) throw new AriesFrameworkError('Missing protected header in signed attachment') + if (!signedAttachment.data.base64) throw new AriesFrameworkError('Missing payload in signed attachment') + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + const { isValid } = await jwsService.verifyJws(agentContext, { + jws: { + header: jws.header, + protected: jws.protected, + signature: jws.signature, + payload: signedAttachment.data.base64, + }, + jwkResolver: async ({ protectedHeader: { kid } }) => { + if (!kid || typeof kid !== 'string') throw new AriesFrameworkError('Missing kid in protected header.') + if (!kid.startsWith('did:')) throw new AriesFrameworkError('Only did is supported for kid identifier') + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid) + const key = getKeyFromVerificationMethod(verificationMethod) + return getJwkFromKey(key) + }, + }) + + if (!isValid) throw new AriesFrameworkError('Failed to validate signature of signed attachment') + const payload = JsonEncoder.fromBase64(signedAttachment.data.base64) as { aud: string; nonce: string } + if (!payload.aud || !payload.nonce) throw new AriesFrameworkError('Invalid payload in signed attachment') + + return payload + } + + public async acceptOffer( + agentContext: AgentContext, + { + credentialRecord, + attachmentId, + offerAttachment, + credentialFormats, + }: CredentialFormatAcceptOfferOptions + ): Promise { + const dataIntegrityFormat = credentialFormats?.dataIntegrity + if (!dataIntegrityFormat) throw new AriesFrameworkError('Missing data integrity credential format data') + + const credentialOffer = offerAttachment.getDataAsJson() + + const dataIntegrityMetadata: DataIntegrityMetadata = {} + const dataIntegrityRequestMetadata: DataIntegrityRequestMetadata = {} + + let anonCredsLinkSecretDataIntegrityBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof | undefined = + undefined + if (dataIntegrityFormat.anonCredsLinkSecretCredentialRequestOptions) { + if (!credentialOffer.binding_method?.anoncreds_link_secret) { + throw new AriesFrameworkError('Cannot request credential with a binding method that was not offered.') + } + + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const credentialDefinitionId = credentialOffer.binding_method.anoncreds_link_secret.cred_def_id + const { credentialDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { credentialDefinitionId }) + if (!credentialDefinitionReturn.credentialDefinition) { + throw new AnonCredsError(`Unable to retrieve credential definition with id ${credentialDefinitionId}`) + } + + const { + credentialRequest: anonCredsCredentialRequest, + credentialRequestMetadata: anonCredsCredentialRequestMetadata, + } = await anonCredsHolderService.createCredentialRequest(agentContext, { + credentialOffer: { + ...credentialOffer.binding_method.anoncreds_link_secret, + schema_id: credentialDefinitionReturn.credentialDefinition.schemaId, + }, + credentialDefinition: credentialDefinitionReturn.credentialDefinition, + linkSecretId: dataIntegrityFormat.anonCredsLinkSecretCredentialRequestOptions?.linkSecretId, + }) + + dataIntegrityRequestMetadata.linkSecretRequestMetadata = anonCredsCredentialRequestMetadata + + dataIntegrityMetadata.linkSecretMetadata = { + credentialDefinitionId: credentialOffer.binding_method.anoncreds_link_secret.cred_def_id, + schemaId: credentialDefinitionReturn.credentialDefinition.schemaId, + } + + if (!anonCredsCredentialRequest.entropy) { + throw new AriesFrameworkError('Missing entropy for anonCredsCredentialRequest') + } + anonCredsLinkSecretDataIntegrityBindingProof = + anonCredsCredentialRequest as AnonCredsLinkSecretDataIntegrityBindingProof + } + + let didCommSignedAttachmentBindingProof: DidCommSignedAttachmentDataIntegrityBindingProof | undefined = undefined + let didCommSignedAttachment: Attachment | undefined = undefined + if (dataIntegrityFormat.didCommSignedAttachmentCredentialRequestOptions) { + if (!credentialOffer.binding_method?.didcomm_signed_attachment) { + throw new AriesFrameworkError('Cannot request credential with a binding method that was not offered.') + } + + const offeredCredential = credentialOffer.credential + + let aud: string + if (offeredCredential?.issuer) { + if (typeof offeredCredential.issuer === 'string') aud = offeredCredential.issuer + else if (Array.isArray(offeredCredential.issuer)) throw new AriesFrameworkError('Issuer cannot be an array') + else if (typeof offeredCredential.issuer === 'object' && typeof offeredCredential.issuer.id === 'string') + aud = offeredCredential.issuer.id + else { + throw new AriesFrameworkError('Wrong issuer format in credential offer') + } + } else { + // TODO: If the issuer is not included in the credential in the offer, the aud MUST be the same as the did of the recipient did of the DIDComm message containing the request message. + throw new AriesFrameworkError('Wrong issuer format in credential offer') + } + + const holderDidMethod = parseDid(aud).method + if (!credentialOffer.binding_method.didcomm_signed_attachment.did_methods_supported.includes(holderDidMethod)) { + throw new AriesFrameworkError(`Holder did method ${holderDidMethod} not supported by the issuer`) + } + + didCommSignedAttachment = await this.createSignedAttachment( + agentContext, + { aud, nonce: credentialOffer.binding_method.didcomm_signed_attachment.nonce }, + dataIntegrityFormat.didCommSignedAttachmentCredentialRequestOptions, + credentialOffer.binding_method.didcomm_signed_attachment.algs_supported + ) + + didCommSignedAttachmentBindingProof = { attachment_id: didCommSignedAttachment.id } + } + + const bindingProof: DataIntegrityCredentialRequestBindingProof | undefined = + !anonCredsLinkSecretDataIntegrityBindingProof && !didCommSignedAttachmentBindingProof + ? undefined + : { + anoncreds_link_secret: anonCredsLinkSecretDataIntegrityBindingProof, + didcomm_signed_attachment: didCommSignedAttachmentBindingProof, + } + + if (credentialOffer.binding_required && !bindingProof) { + throw new AriesFrameworkError('Missing required binding proof') + } + + const dataModelVersion = dataIntegrityFormat.dataModelVersion ?? credentialOffer.data_model_versions_supported[0] + if (!credentialOffer.data_model_versions_supported.includes(dataModelVersion)) { + throw new AriesFrameworkError('Cannot request credential with a data model version that was not offered.') + } + + credentialRecord.metadata.set(DataIntegrityMetadataKey, dataIntegrityMetadata) + credentialRecord.metadata.set( + DataIntegrityRequestMetadataKey, + dataIntegrityRequestMetadata + ) + + const credentialRequest: DataIntegrityCredentialRequest = { + data_model_version: dataModelVersion, + binding_proof: bindingProof, + } + + const format = new CredentialFormatSpec({ + attachmentId, + format: W3C_DATA_INTEGRITY_CREDENTIAL_REQUEST, + }) + + const attachment = this.getFormatData(credentialRequest, format.attachmentId) + return { format, attachment, appendAttachments: didCommSignedAttachment ? [didCommSignedAttachment] : undefined } + } + + /** + * Starting from a request is not supported for anoncreds credentials, this method only throws an error. + */ + public async createRequest(): Promise { + throw new AriesFrameworkError('Starting from a request is not supported for w3c credentials') + } + + /** + * We don't have any models to validate an anoncreds request object, for now this method does nothing + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise { + // not needed for dataIntegrity + // TODO: implement + } + + private async createCredentialWithAnonCredsDataIntegrityProof( + agentContext: AgentContext, + input: { + credentialRecord: CredentialExchangeRecord + anonCredsLinkSecretBindingMethod: AnonCredsLinkSecretBindingMethod + anonCredsLinkSecretBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof + linkSecretMetadata: DataIntegrityLinkSecretMetadata + } + ): Promise { + const { credentialRecord, anonCredsLinkSecretBindingMethod, anonCredsLinkSecretBindingProof, linkSecretMetadata } = + input + + const credentialAttributes = credentialRecord.credentialAttributes + if (!credentialAttributes) { + throw new AriesFrameworkError( + `Missing required credential attribute values on credential record with id ${credentialRecord.id}` + ) + } + + const anonCredsIssuerService = + agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) + + const credentialDefinition = ( + await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, linkSecretMetadata.credentialDefinitionId) + ).credentialDefinition.value + + // We check locally for credential definition info. If it supports revocation, we need to search locally for + // an active revocation registry + let revocationRegistryDefinitionId: string | undefined = undefined + let revocationRegistryIndex: number | undefined = undefined + let revocationStatusList: AnonCredsRevocationStatusList | undefined = undefined + + if (credentialDefinition.revocation) { + const { credentialRevocationId, revocationRegistryId } = linkSecretMetadata + + if (!credentialRevocationId || !revocationRegistryId) { + throw new AriesFrameworkError( + 'Revocation registry definition id and revocation index are mandatory to issue AnonCreds revocable credentials' + ) + } + + revocationRegistryDefinitionId = revocationRegistryId + revocationRegistryIndex = Number(credentialRevocationId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + if (revocationRegistryDefinitionPrivateRecord.state !== AnonCredsRevocationRegistryState.Active) { + throw new AriesFrameworkError( + `Revocation registry ${revocationRegistryDefinitionId} is in ${revocationRegistryDefinitionPrivateRecord.state} state` + ) + } + + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + const revocationStatusListResult = await registryService + .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) + .getRevocationStatusList(agentContext, revocationRegistryDefinitionId, dateToTimestamp(new Date())) + + if (!revocationStatusListResult.revocationStatusList) { + throw new AriesFrameworkError( + `Unable to resolve revocation status list for ${revocationRegistryDefinitionId}: + ${revocationStatusListResult.resolutionMetadata.error} ${revocationStatusListResult.resolutionMetadata.message}` + ) + } + + revocationStatusList = revocationStatusListResult.revocationStatusList + } + + // TODO: bad abd bad + const { credential } = await anonCredsIssuerService.createCredential(agentContext, { + credentialOffer: { + ...anonCredsLinkSecretBindingMethod, + schema_id: linkSecretMetadata.schemaId, + }, + credentialRequest: anonCredsLinkSecretBindingProof, + credentialValues: convertAttributesToCredentialValues(credentialAttributes), + revocationRegistryDefinitionId, + revocationRegistryIndex, + revocationStatusList, + }) + + return await legacyCredentialToW3cCredential(agentContext, credential) + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialFormats, + credentialRecord, + attachmentId, + offerAttachment, + requestAttachment, + requestAppendAttachments, + }: CredentialFormatAcceptRequestOptions + ): Promise { + // Assert credential attributes + const credentialAttributes = credentialRecord.credentialAttributes + if (!credentialAttributes) { + throw new AriesFrameworkError( + `Missing required credential attribute values on credential record with id ${credentialRecord.id}` + ) + } + + const dataIntegrityFormat = credentialFormats?.dataIntegrity + if (!dataIntegrityFormat) throw new AriesFrameworkError('Missing data integrity credential format data') + + const credentialOffer = offerAttachment?.getDataAsJson() + if (!credentialOffer) throw new AriesFrameworkError('Missing data integrity credential offer in createCredential') + + const credentialRequest = requestAttachment.getDataAsJson() + if (!credentialRequest) + throw new AriesFrameworkError('Missing data integrity credential request in createCredential') + + const dataIntegrityMetadata = credentialRecord.metadata.get(DataIntegrityMetadataKey) + if (!dataIntegrityMetadata) + throw new AriesFrameworkError('Missing data integrity credential metadata in createCredential') + + let credential: W3cJsonLdVerifiableCredential | undefined + if (credentialRequest.binding_proof?.anoncreds_link_secret) { + if (!credentialOffer.binding_method?.anoncreds_link_secret) { + throw new AriesFrameworkError('Cannot issue credential with a binding method that was not offered.') + } + + if (!dataIntegrityMetadata.linkSecretMetadata) { + throw new AriesFrameworkError('Missing anoncreds link secret metadata') + } + + credential = await this.createCredentialWithAnonCredsDataIntegrityProof(agentContext, { + credentialRecord, + anonCredsLinkSecretBindingMethod: credentialOffer.binding_method.anoncreds_link_secret, + linkSecretMetadata: dataIntegrityMetadata.linkSecretMetadata, + anonCredsLinkSecretBindingProof: credentialRequest.binding_proof.anoncreds_link_secret, + }) + } + + if (credentialRequest.binding_proof?.didcomm_signed_attachment) { + if (!credentialOffer.binding_method?.didcomm_signed_attachment) { + throw new AriesFrameworkError('Cannot issue credential with a binding method that was not offered.') + } + + const bindingProofAttachment = requestAppendAttachments?.find( + (attachments) => attachments.id === credentialRequest.binding_proof?.didcomm_signed_attachment?.attachment_id + ) + if (!bindingProofAttachment) throw new AriesFrameworkError('Missing binding proof attachment') + + const issuerKid = dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions?.kid + if (!issuerKid) throw new AriesFrameworkError('Missing kid') + + // very bad + const offeredCredential = JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential) + + const { aud, nonce } = await this.getSignedAttachmentPayload(agentContext, bindingProofAttachment) + if (nonce !== credentialOffer.binding_method.didcomm_signed_attachment.nonce) { + throw new AriesFrameworkError('Invalid nonce in signed attachment') + } + + const issuer = + typeof offeredCredential.issuer === 'string' ? offeredCredential.issuer : offeredCredential.issuer.id + if (issuer !== aud) throw new AriesFrameworkError('Invalid aud in signed attachment') + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(issuer) + const verificationMethod = didDocument.dereferenceVerificationMethod(issuerKid) + + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + const signatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!signatureSuite) { + throw new AriesFrameworkError( + `Could not find signature suite for verification method type ${verificationMethod.type}` + ) + } + + if (credential) { + //TODO: in this case we already have a credential, so we can use that and just add another signature + throw new AriesFrameworkError('TODO: implement and remove this!') + } else { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + credential = (await w3cCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential: offeredCredential, + proofType: signatureSuite.proofType, + verificationMethod: verificationMethod.id, + })) as W3cJsonLdVerifiableCredential + } + } + + if ( + !credentialRequest.binding_proof?.anoncreds_link_secret && + !credentialRequest.binding_proof?.didcomm_signed_attachment + ) { + // TODO: sign with an arbitrary cryptosuite, but cannot be anoncreds .... + throw new AriesFrameworkError('Not impelmented') + } + + const format = new CredentialFormatSpec({ + attachmentId, + format: W3C_DATA_INTEGRITY_CREDENTIAL, + }) + + const attachment = this.getFormatData({ credential: JsonTransformer.toJSON(credential) }, format.attachmentId) + return { format, attachment } + } + + private async processLinkSecretBoundCredential( + agentContext: AgentContext, + credential: W3cJsonLdVerifiableCredential, + credentialRecord: CredentialExchangeRecord, + linkSecretRequestMetadata: DataIntegrityLinkSecretRequestMetadata + ) { + if (!credentialRecord.credentialAttributes) { + throw new AriesFrameworkError( + 'Missing credential attributes on credential record. Unable to check credential attributes' + ) + } + + const aCredential = AW3cCredential.fromJson(JsonTransformer.toJSON(credential)) + const { schemaId, credentialDefinitionId, revocationRegistryId, revocationRegistryIndex } = aCredential.toLegacy() + + const { schemaReturn } = await fetchObjectsFromLedger(agentContext, { schemaId }) + if (!schemaReturn.schema) throw new AriesFrameworkError('Schema not found.') + + const { credentialDefinitionId: qCredentialDefinitionId, revocationRegistryId: qRevocationRegistryId } = + await fetchQualifiedIds(agentContext, { + credentialDefinitionId, + revocationRegistryId, + }) + + await this.assertCredentialAttributesMatchSchemaAttributes(agentContext, credential, schemaId) + + const methodName = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, credentialDefinitionId).methodName + + const linkSecretRecord = await agentContext.dependencyManager + .resolve(AnonCredsLinkSecretRepository) + .getByLinkSecretId(agentContext, linkSecretRequestMetadata.link_secret_name) + + if (!linkSecretRecord.value) throw new AriesFrameworkError('Link Secret value not stored') + + const anonCredsCredentialRecordOptions = { + credentialId: utils.uuid(), + linkSecretId: linkSecretRecord.linkSecretId, + credentialDefinitionId: qCredentialDefinitionId, + schemaId: schemaReturn.schemaId, + schemaName: schemaReturn.schema.name, + schemaIssuerId: schemaReturn.schema.issuerId, + schemaVersion: schemaReturn.schema.version, + methodName, + revocationRegistryId: qRevocationRegistryId, + credentialRevocationId: revocationRegistryIndex?.toString(), + } + + // If the credential is revocable, store the revocation identifiers in the credential record + if (revocationRegistryId) { + const metadata = credentialRecord.metadata.get(DataIntegrityMetadataKey) + if (!metadata?.linkSecretMetadata) throw new AriesFrameworkError('Missing link secret metadata') + + metadata.linkSecretMetadata.revocationRegistryId = qRevocationRegistryId + metadata.linkSecretMetadata.credentialRevocationId = revocationRegistryIndex?.toString() + credentialRecord.metadata.set(DataIntegrityMetadataKey, metadata) + } + + return anonCredsCredentialRecordOptions + } + + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in 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, requestAttachment }: CredentialFormatProcessCredentialOptions + ): Promise { + const credentialRequestMetadata = credentialRecord.metadata.get( + DataIntegrityRequestMetadataKey + ) + if (!credentialRequestMetadata) { + throw new AriesFrameworkError( + `Missing request metadata for credential exchange with thread id ${credentialRecord.id}` + ) + } + + const credentialRequest = requestAttachment.getDataAsJson() + if (!credentialRequest) + throw new AriesFrameworkError('Missing data integrity credential request in createCredential') + + if (!credentialRecord.credentialAttributes) { + throw new AriesFrameworkError('Missing credential attributes on credential record.') + } + + // TODO: validate credential structure + const { credential } = attachment.getDataAsJson() + const w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credential, W3cJsonLdVerifiableCredential) + + let anonCredsCredentialRecordOptions: AnonCredsCredentialRecordOptions | undefined + if (credentialRequest.binding_proof?.anoncreds_link_secret) { + if (!credentialRequestMetadata.linkSecretRequestMetadata) { + throw new AriesFrameworkError('Missing link secret request metadata') + } + + anonCredsCredentialRecordOptions = await this.processLinkSecretBoundCredential( + agentContext, + w3cJsonLdVerifiableCredential, + credentialRecord, + credentialRequestMetadata.linkSecretRequestMetadata + ) + } + + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const record = await w3cCredentialService.storeCredential(agentContext, { + credential: w3cJsonLdVerifiableCredential, + anonCredsCredentialRecordOptions, + }) + + credentialRecord.credentials.push({ + credentialRecordType: this.credentialRecordType, + credentialRecordId: record.id, + }) + } + + public supportsFormat(format: string): boolean { + const supportedFormats = [ + W3C_DATA_INTEGRITY_CREDENTIAL_REQUEST, + W3C_DATA_INTEGRITY_CREDENTIAL_OFFER, + W3C_DATA_INTEGRITY_CREDENTIAL, + ] + + return supportedFormats.includes(format) + } + + /** + * Gets the attachment object for a given attachmentId. We need to get out the correct attachmentId for + * anoncreds and then find the corresponding attachment (if there is one) + * @param formats the formats object containing the attachmentId + * @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.attachmentId) + const supportedAttachment = messageAttachments.find((attachment) => supportedAttachmentIds.includes(attachment.id)) + + return supportedAttachment + } + + public async deleteCredentialById(agentContext: AgentContext, credentialRecordId: string): Promise { + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + await anonCredsHolderService.deleteCredential(agentContext, credentialRecordId) + } + + public async shouldAutoRespondToProposal( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { offerAttachment, proposalAttachment }: CredentialFormatAutoRespondProposalOptions + ) { + throw new AriesFrameworkError('Not implemented') + return false + } + + public async shouldAutoRespondToOffer( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + input: CredentialFormatAutoRespondOfferOptions + ) { + return false + } + + public async shouldAutoRespondToRequest( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions + ) { + return false + } + + public async shouldAutoRespondToCredential( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { credentialRecord, requestAttachment, credentialAttachment }: CredentialFormatAutoRespondCredentialOptions + ) { + return false + } + + private async createDataIntegrityCredentialOffer( + agentContext: AgentContext, + credentialRecord: CredentialExchangeRecord, + options: DataIntegrityOfferCredentialFormat + ): Promise<{ + dataIntegrityCredentialOffer: DataIntegrityCredentialOffer + previewAttributes: CredentialPreviewAttributeOptions[] + }> { + const { + bindingRequired, + credential, + anonCredsLinkSecretBindingMethodOptions, + didCommSignedAttachmentBindingMethodOptions, + } = options + + const dataModelVersionsSupported: W3C_VC_DATA_MODEL_VERSION[] = ['1.1'] + + // validate the credential and get the preview attributes + const credentialJson = credential instanceof W3cCredential ? JsonTransformer.toJSON(credential) : credential + const validCredential = this.enhanceCredentialOffer(credentialJson, dataModelVersionsSupported[0]) + const validW3cCredential = JsonTransformer.fromJSON(validCredential, W3cCredential) + const previewAttributes = this.previewAttributesFromCredential(validW3cCredential) + + const dataIntegrityMetadata: DataIntegrityMetadata = {} + + let anonCredsLinkSecretBindingMethod: AnonCredsLinkSecretBindingMethod | undefined = undefined + if (anonCredsLinkSecretBindingMethodOptions) { + const { credentialDefinitionId, revocationRegistryDefinitionId, revocationRegistryIndex } = + anonCredsLinkSecretBindingMethodOptions + + const anoncredsCredentialOffer = await agentContext.dependencyManager + .resolve(AnonCredsIssuerServiceSymbol) + .createCredentialOffer(agentContext, { + credentialDefinitionId, + }) + + // We check locally for credential definition info. If it supports revocation, revocationRegistryIndex + // and revocationRegistryDefinitionId are mandatory + const { credentialDefinition } = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, anoncredsCredentialOffer.cred_def_id) + + if (credentialDefinition.value.revocation) { + if (!revocationRegistryDefinitionId || !revocationRegistryIndex) { + throw new AriesFrameworkError( + 'AnonCreds revocable credentials require revocationRegistryDefinitionId and revocationRegistryIndex' + ) + } + + // Set revocation tags + credentialRecord.setTags({ + anonCredsRevocationRegistryId: revocationRegistryDefinitionId, + anonCredsCredentialRevocationId: revocationRegistryIndex.toString(), + }) + } + + await this.assertCredentialAttributesMatchSchemaAttributes( + agentContext, + validW3cCredential, + credentialDefinition.schemaId + ) + + const { schema_id, ..._anonCredsLinkSecretBindingMethod } = anoncredsCredentialOffer + anonCredsLinkSecretBindingMethod = _anonCredsLinkSecretBindingMethod + + dataIntegrityMetadata.linkSecretMetadata = { + schemaId: schema_id, + credentialDefinitionId: credentialDefinitionId, + credentialRevocationId: revocationRegistryIndex?.toString(), + revocationRegistryId: revocationRegistryDefinitionId, + } + } + + let didCommSignedAttachmentBindingMethod: DidCommSignedAttachmentBindingMethod | undefined = undefined + if (didCommSignedAttachmentBindingMethodOptions) { + const { didMethodsSupported, algsSupported } = didCommSignedAttachmentBindingMethodOptions + didCommSignedAttachmentBindingMethod = { + did_methods_supported: didMethodsSupported ?? this.getSupportedDidMethods(agentContext), + algs_supported: algsSupported ?? this.getSupportedJwaSignatureAlgorithms(agentContext), + nonce: await agentContext.wallet.generateNonce(), + } + + if (didCommSignedAttachmentBindingMethod.algs_supported.length === 0) { + throw new AriesFrameworkError('No supported JWA signature algorithms found.') + } + + // TODO: this can be empty according to spec + if (didCommSignedAttachmentBindingMethod.did_methods_supported.length === 0) { + throw new AriesFrameworkError('No supported DID methods found.') + } + } + + if (bindingRequired && !anonCredsLinkSecretBindingMethod && !didCommSignedAttachmentBindingMethod) { + throw new AriesFrameworkError('Missing required binding method.') + } + + const dataIntegrityCredentialOffer: DataIntegrityCredentialOffer = { + data_model_versions_supported: dataModelVersionsSupported, + binding_required: bindingRequired, + binding_method: { + anoncreds_link_secret: anonCredsLinkSecretBindingMethod, + didcomm_signed_attachment: didCommSignedAttachmentBindingMethod, + }, + credential: credentialJson, + } + + credentialRecord.metadata.set(DataIntegrityMetadataKey, dataIntegrityMetadata) + + return { dataIntegrityCredentialOffer, previewAttributes } + } + + private previewAttributesFromCredential(credential: W3cCredential): CredentialPreviewAttributeOptions[] { + if (Array.isArray(credential.credentialSubject)) { + throw new AriesFrameworkError('Credential subject must be an object.') + } + + const claims = (credential.credentialSubject.claims ?? {}) as AnonCredsClaimRecord + const attributes = Object.entries(claims).map(([key, value]): CredentialPreviewAttributeOptions => { + return { name: key, value: value.toString() } + }) + return attributes + } + + private async assertCredentialAttributesMatchSchemaAttributes( + agentContext: AgentContext, + credential: W3cCredential, + schemaId: string + ) { + const attributes = this.previewAttributesFromCredential(credential) + + const { schemaReturn } = await fetchObjectsFromLedger(agentContext, { schemaId }) + if (!schemaReturn.schema) { + throw new AriesFrameworkError( + `Unable to resolve schema ${schemaId} from registry: ${schemaReturn.resolutionMetadata.error} ${schemaReturn.resolutionMetadata.message}` + ) + } + + assertAttributesMatchSchema(schemaReturn.schema, attributes) + + return { attributes } + } + + /** + * 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: { + base64: JsonEncoder.toBase64(data), + }, + }) + + return attachment + } + + private getSupportedDidMethods(agentContext: AgentContext) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const supportedDidMethods: Set = new Set() + + for (const resolver of didsApi.config.resolvers) { + resolver.supportedMethods.forEach((method) => supportedDidMethods.add(method)) + } + + return Array.from(supportedDidMethods) + } + + /** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ + private getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) + + return supportedJwaSignatureAlgorithms + } +} diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts index 178e83d3a3..56dbfee16d 100644 --- a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts @@ -42,6 +42,7 @@ import { JsonEncoder, ProofFormatSpec, JsonTransformer, + encodeCredentialValue, } from '@aries-framework/core' import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest' @@ -53,7 +54,6 @@ import { areAnonCredsProofRequestsEqual, assertBestPracticeRevocationInterval, checkValidCredentialValueEncoding, - encodeCredentialValue, assertNoDuplicateGroupsNamesInProofRequest, getRevocationRegistriesForRequest, getRevocationRegistriesForProof, diff --git a/packages/anoncreds/src/formats/index.ts b/packages/anoncreds/src/formats/index.ts index 07f76522ba..2036d28145 100644 --- a/packages/anoncreds/src/formats/index.ts +++ b/packages/anoncreds/src/formats/index.ts @@ -1,6 +1,7 @@ export * from './AnonCredsCredentialFormat' export * from './LegacyIndyCredentialFormat' export { AnonCredsCredentialFormatService } from './AnonCredsCredentialFormatService' +export { DataIntegrityCredentialFormatService } from './DataIntegrityCredentialFormatService' export { LegacyIndyCredentialFormatService } from './LegacyIndyCredentialFormatService' export * from './AnonCredsProofFormat' diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index fad5355d54..f6e7693c95 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -15,3 +15,5 @@ export { generateLegacyProverDidLikeString } from './utils/proverDid' export * from './utils/indyIdentifiers' export { assertBestPracticeRevocationInterval } from './utils/revocationInterval' export { storeLinkSecret } from './utils/linkSecret' +export { legacyCredentialToW3cCredential, w3cToLegacyCredential } from './utils/w3cUtils' +export { fetchObjectsFromLedger, fetchQualifiedIds } from './utils/ledgerObjects' diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index 5213153ff9..8c9a70d944 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -1,3 +1,5 @@ +import type { AnonCredsCredentialValue } from '@aries-framework/core' + export const anonCredsPredicateType = ['>=', '>', '<=', '<'] as const export type AnonCredsPredicateType = (typeof anonCredsPredicateType)[number] @@ -42,10 +44,6 @@ export interface AnonCredsCredentialRequest { } export type AnonCredsCredentialValues = Record -export interface AnonCredsCredentialValue { - raw: string - encoded: string // Raw value as number in string -} export interface AnonCredsCredential { schema_id: string diff --git a/packages/anoncreds/src/models/internal.ts b/packages/anoncreds/src/models/internal.ts index 8aacc72a52..165c3d7617 100644 --- a/packages/anoncreds/src/models/internal.ts +++ b/packages/anoncreds/src/models/internal.ts @@ -1,8 +1,8 @@ +import type { AnonCredsClaimRecord } from '@aries-framework/core' + export interface AnonCredsCredentialInfo { credentialId: string - attributes: { - [key: string]: string - } + attributes: AnonCredsClaimRecord schemaId: string credentialDefinitionId: string revocationRegistryId?: string | undefined diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index a657279715..8604405f1a 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -7,15 +7,16 @@ import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest, - AnonCredsProofRequest, AnonCredsNonRevokedInterval, + AnonCredsProofRequest, } from '../models/exchange' import type { AnonCredsCredentialDefinition, - AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, AnonCredsSchema, } from '../models/registry' +import type { W3cJsonLdVerifiableCredential } from '@aries-framework/core' export interface AnonCredsAttributeInfo { name?: string @@ -43,9 +44,8 @@ export interface CreateProofOptions { } } -export interface StoreCredentialOptions { +export interface BaseStoreCredentialOptions { credentialRequestMetadata: AnonCredsCredentialRequestMetadata - credential: AnonCredsCredential credentialDefinition: AnonCredsCredentialDefinition schema: AnonCredsSchema credentialDefinitionId: string @@ -56,6 +56,14 @@ export interface StoreCredentialOptions { } } +export interface StoreW3cCredentialOptions extends BaseStoreCredentialOptions { + credential: W3cJsonLdVerifiableCredential +} + +export interface StoreCredentialOptions extends BaseStoreCredentialOptions { + credential: AnonCredsCredential +} + export interface GetCredentialOptions { credentialId: string } diff --git a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts new file mode 100644 index 0000000000..bbadb71a1a --- /dev/null +++ b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts @@ -0,0 +1,194 @@ +import type { Wallet } from '@aries-framework/core' + +import { + Agent, + ConsoleLogger, + DidResolverService, + DidsModuleConfig, + Ed25519Signature2018, + EventEmitter, + InjectionSymbols, + KeyType, + SignatureSuiteToken, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredentialRepository, + W3cCredentialsModuleConfig, +} from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../../../core/tests' +import { InMemoryAnonCredsRegistry } from '../../../../tests/InMemoryAnonCredsRegistry' +import { AnonCredsModuleConfig } from '../../../AnonCredsModuleConfig' +import { AnonCredsCredentialRecord } from '../../../repository' +import { AnonCredsRegistryService } from '../../../services' +import * as testModule from '../anonCredsCredentialRecord' + +const agentConfig = getAgentConfig('Migration AnonCreds Credential Records 0.4-0.5') +const registry = new InMemoryAnonCredsRegistry() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], +}) + +const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet + +const stop = new Subject() +const eventEmitter = new EventEmitter(agentDependencies, stop) + +const w3cRepo = { + save: jest.fn(), +} + +const inMemoryStorageService = new InMemoryStorageService() +const logger = new ConsoleLogger() +const agentContext = getAgentContext({ + registerInstances: [ + [EventEmitter, eventEmitter], + [W3cCredentialRepository, w3cRepo], + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], + [InjectionSymbols.Logger, logger], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], + ], + agentConfig, + wallet, +}) + +const anonCredsRepo = { + getAll: jest.fn(), + delete: jest.fn(), +} + +jest.mock('../../../../../core/src/agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn((repo: any) => { + if (repo.prototype.constructor.name === 'AnonCredsCredentialRepository') { + return anonCredsRepo + } + throw new Error(`Couldn't resolve dependency`) + }), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.4-0.5 | AnonCredsRecord', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateW3cCredentialRecordToV0_5()', () => { + it('should fetch all w3c credential records and re-save them', async () => { + const records = [ + new AnonCredsCredentialRecord({ + credential: { + schema_id: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + cred_def_id: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/CLAIM_DEF/34815/Employee Credential', + + values: { + name: { + raw: 'John', + encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', + }, + age: { + raw: '25', + encoded: '25', + }, + }, + signature: { + p_credential: { + m_2: '96181142928573619139692730181044468294945970900261235940698944149443005219418', + a: '95552886901127172841432400616361951122825637102065915900211722444153579891548765880931308692457984326066263506661706967742637168349111737200116541217341739027256190535822337883555402874901690699603230292607481206740216276736875319709356355255797288879451730329296366840213920367976178079664448005608079197649139477441385127107355597906058676699377491628047651331689288017597714832563994968230904723400034478518535411493372596211553797813567090114739752408151368926090849149021350138796163980103411453098000223493524437564062789271302371287568506870484060911412715559140166845310368136412863128732929561146328431066870', + e: '259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742929837794489002147266183999965799605813', + v: '8070312275110314663750247899433202850238560575163878956819342967827136399370879736823043902982634515009588016797203155246614708232573921376646871743359587732590693401587607271972304303322060390310307460889523961550612965021232979808509508502354241838342542729225461467834597352210800168107201638861601487760961526713355932504366874557170337152964069325172574449356691055377568302458374147949937789910094307449082152173580675507028369533914480926873196435808261915052547630680304620062203647948590064800546491641963412948122135194369131128319694594446518925913583118382698018169919523769679141724867515604189334120099773703979769794325694804992635522127820413717601811493634024617930397944903746555691677663850240187799372670069559074549528342288602574968520156320273386872799429362106185458798531573424651644586691950218', + }, + r_credential: null, + }, + signature_correctness_proof: { + se: '22707379000451320101568757017184696744124237924783723059712360528872398590682272715197914336834321599243107036831239336605987281577690130807752876870302232265860540101807563741012022740942625464987934377354684266599492895835685698819662114798915664525092894122648542269399563759087759048742378622062870244156257780544523627249100818371255142174054148531811440128609220992508274170196108004985441276737673328642493312249112077836369109453214857237693701603680205115444482751700483514317558743227403858290707747986550689265796031162549838465391957776237071049436590886476581821857234951536091662216488995258175202055258', + c: '86499530658088050169174214946559930902913340880816576251403968391737698128027', + }, + //witness: null, + rev_reg_id: undefined, + //rev_reg: undefined, + }, + credentialId: 'myCredentialId', + credentialRevocationId: undefined, + linkSecretId: 'linkSecretId', + issuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', + schemaIssuerId: 'did:example:schemaIssuerDid', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'methodName', + }), + ] + + const registry = await agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier( + agentContext, + 'did:indy:local:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/CLAIM_DEF/34815/Employee Credential' + ) + + await registry.registerCredentialDefinition(agentContext, { + credentialDefinition: { + schemaId: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + type: 'CL', + tag: 'Employee Credential', + value: { + primary: { + n: '96580316873365712442732878101936646890604119889300256012760004147648019614357085076364923021085826868139621573684543249964678348356482485140527957732786530916400278400000660594438781319168272211306232441102713960203075436899295821371799038925693667322779688360706410505540407867607819490853610928774850151039047069357657140257065718659230885391255982730600838743036039711140083284918623906117435892506848479452322000479436955298502839148769930281251929368562720371560260726440893569655811165804238971700685368149522154328822673070750192788830837447670660152195003043802510899143110060139772708073728514051890251226573', + s: '75501169085950126423249157998833414929129208062284812993616444532525695129548804062583842133218092574263501104948737639625833940700883624316320978432322582288936701621781896861131284952998380826417162040016550587340823832731945229065884469806723217100370126833740077464404509861175397581089717495779179489233739975691055780558708056569691296866880514640011052194662545371451908889892210433975411453987754134291774476185207289195701174795140189362641644917865101153841235103322243375241496141786303488408131721122704625842138002478498178520263715598899259097315781832554764315008915688899555385079843761690822607379111', + r: { + age: '77540670431411230038763922593314057361920691860149780021247345546110594816960144474334769978922558437548167814211078474008950463908860798685487527066465227411414311215109347438752200023045503271169383262727401013107872116564443896905324906462332380026785798806953280066387451803949226448584225962096665020244191229063723249351163778395354282347357165322007286709571349618598645876371030907856017571738360851407364231328550357981247798517795822722356010859380461592920151980368953491924564759581591539937752386114770938355831372517555540534219652937595339962248857890418611836415170566769174263185424389504546847791061', + name: '56742811203198572257254422595806148480437594543198516349563027967943211653217799525148065500107783030709376059668814822301811566517601408461597171188532787265942263962719966788682945248064629136273708677025304469521003291988851716171767936997105137959854045442533627185824896706311588434426708666794422548240008058413804660062414897767172901561637004230184962449104905433874433106461860673266368007446282814453132977549811373164579634926487398703746240854572636222768903661936542049761028833196194927339141225860442129881312421875004614067598828714629143133815560576383442835845338263420621113398541139833020926358483', + master_secret: + '47747144545528691003767568337472105276331891233385663931584274593369979405459771996932889017746007711684586508906823242148854224004122637231405489854166589517019033322603946444431305440324935310636815918200611202700765046091022859325187263050783813756813224792976045471735525150004048149843525973339369133943560241544453714388862237336971069786113757093274533177228170822141225802024684552058049687105759446916872700318309370449824235232087307054291066123530983268176971897233515383614938649406180978604188604030816485303101208443369021847829704196934707372786773595567687934642471997496883786836109942269282274646821', + }, + rctxt: + '56624145913410031711009467194049739028044689257231550726399481216874451927585543568732728200991667356553765568186221627220562697315384161695993324560029249334601709666000269987161110370944904361123034293076300325831500797294972192392858769494862446579930065658123775287266632055490150224877768031718759385137678458946705469525103921298013633970637295409365635673547258006414068589487568446936418629870049873056708576696589883095398217681918429160130132727488662842876963800048249179530353781028982129766362865351617486193454223628637074575525915653459208863652607756131262546529918749753409703149380392151341320092701', + z: '48207831484908089113913456529606728278875173243133137568203149862235480864817131176165695429997836542014395411854617371967345903846590322848315574430219622375108777832406077167357765312048126429295008846417923207098159790545077579480434122704652997388986707634157186643373176212809933891460515705299787583898608744041271224726626894030124816906292858431898018633343059228110335652476641836263281987023563730093708908265403781917908475102010080313484277539579578010231066258146934633395220956275733173978548481848026533424513200278825491847270318469226963088243667105115637069262564294713288882078385391140385504192475', + }, + }, + issuerId: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL', + }, + options: {}, + }) + mockFunction(anonCredsRepo.getAll).mockResolvedValue(records) + + await testModule.storeAnonCredsInW3cFormatV0_5(agent) + + expect(anonCredsRepo.getAll).toHaveBeenCalledTimes(1) + expect(anonCredsRepo.getAll).toHaveBeenCalledWith(agent.context) + expect(w3cRepo.save).toHaveBeenCalledTimes(1) + expect(anonCredsRepo.delete).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts new file mode 100644 index 0000000000..886f5d03fe --- /dev/null +++ b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts @@ -0,0 +1,67 @@ +import type { AnonCredsCredentialRecord } from '../../repository' +import type { AgentContext, BaseAgent } from '@aries-framework/core' + +import { W3cCredentialService } from '@aries-framework/core' + +import { AnonCredsCredentialRepository } from '../../repository' +import { legacyCredentialToW3cCredential } from '../../utils' +import { fetchQualifiedIds } from '../../utils/ledgerObjects' + +async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRecord: AnonCredsCredentialRecord) { + const legacyCredential = legacyRecord.credential + const legacyTags = legacyRecord.getTags() + + const w3cJsonLdCredential = await legacyCredentialToW3cCredential(agentContext, legacyCredential) + + const { schemaId, schemaIssuerId, revocationRegistryId, credentialDefinitionId } = await fetchQualifiedIds( + agentContext, + { + schemaId: legacyTags.schemaId, + credentialDefinitionId: legacyTags.credentialDefinitionId, + revocationRegistryId: legacyTags.revocationRegistryId, + } + ) + + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + await w3cCredentialService.storeCredential(agentContext, { + credential: w3cJsonLdCredential, + anonCredsCredentialRecordOptions: { + credentialId: legacyRecord.credentialId, + linkSecretId: legacyRecord.linkSecretId, + credentialDefinitionId, + schemaId, + schemaName: legacyTags.schemaName, + schemaIssuerId, + schemaVersion: legacyTags.schemaVersion, + methodName: legacyRecord.methodName, + revocationRegistryId: revocationRegistryId, + credentialRevocationId: legacyTags.credentialRevocationId, + }, + }) + + return w3cJsonLdCredential +} + +/** + * Stores all anoncreds credentials in the new w3c format + */ +export async function storeAnonCredsInW3cFormatV0_5(agent: Agent) { + agent.config.logger.info('Migration of legacy AnonCreds records to the new W3C format version 0.5') + + const anoncredsRepository = agent.dependencyManager.resolve(AnonCredsCredentialRepository) + + agent.config.logger.debug(`Fetching all anoncreds credential records from storage`) + const records = await anoncredsRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${records.length} legacy anonCreds credential records to update.`) + + for (const record of records) { + agent.config.logger.debug( + `Re-saving anonCreds credential record with id ${record.id} in the new w3c format, and deleting the legacy record` + ) + await migrateLegacyToW3cCredential(agent.context, record) + await anoncredsRepository.delete(agent.context, record) + + agent.config.logger.debug(`Successfully migrated w3c credential record with id ${record.id} to storage version 0.5`) + } +} diff --git a/packages/anoncreds/src/updates/0.4-0.5/index.ts b/packages/anoncreds/src/updates/0.4-0.5/index.ts new file mode 100644 index 0000000000..f3f2741ee8 --- /dev/null +++ b/packages/anoncreds/src/updates/0.4-0.5/index.ts @@ -0,0 +1,7 @@ +import type { BaseAgent } from '@aries-framework/core' + +import { storeAnonCredsInW3cFormatV0_5 } from './anonCredsCredentialRecord' + +export async function updateAnonCredsModuleV0_4_1ToV0_5(agent: Agent): Promise { + await storeAnonCredsInW3cFormatV0_5(agent) +} diff --git a/packages/anoncreds/src/utils/credential.ts b/packages/anoncreds/src/utils/credential.ts index 33a7a05c41..c3ac9f6bbe 100644 --- a/packages/anoncreds/src/utils/credential.ts +++ b/packages/anoncreds/src/utils/credential.ts @@ -1,21 +1,7 @@ import type { AnonCredsSchema, AnonCredsCredentialValues } from '../models' import type { CredentialPreviewAttributeOptions, LinkedAttachment } from '@aries-framework/core' -import { AriesFrameworkError, Hasher, encodeAttachment, Buffer } 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 -} +import { AriesFrameworkError, encodeAttachment, encodeCredentialValue } from '@aries-framework/core' /** * Converts int value to string @@ -113,46 +99,6 @@ export function checkValidCredentialValueEncoding(raw: unknown, encoded: string) return encoded === encodeCredentialValue(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 encodeCredentialValue(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: CredentialPreviewAttributeOptions[]) { const schemaAttributes = schema.attrNames const credAttributes = attributes.map((a) => a.name) diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index b49440268b..e66d902264 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -4,7 +4,7 @@ export { assertNoDuplicateGroupsNamesInProofRequest } from './hasDuplicateGroupN export { areAnonCredsProofRequestsEqual } from './areRequestsEqual' export { assertBestPracticeRevocationInterval } from './revocationInterval' export { getRevocationRegistriesForRequest, getRevocationRegistriesForProof } from './getRevocationRegistries' -export { encodeCredentialValue, checkValidCredentialValueEncoding } from './credential' +export { checkValidCredentialValueEncoding } from './credential' export { IsMap } from './isMap' export { composeCredentialAutoAccept, composeProofAutoAccept } from './composeAutoAccept' export { areCredentialPreviewAttributesEqual } from './credentialPreviewAttributes' @@ -16,3 +16,5 @@ export { unqualifiedSchemaIdRegex, unqualifiedSchemaVersionRegex, } from './indyIdentifiers' +export { legacyCredentialToW3cCredential, w3cToLegacyCredential } from './w3cUtils' +export { fetchObjectsFromLedger, fetchQualifiedIds } from './ledgerObjects' diff --git a/packages/anoncreds/src/utils/ledgerObjects.ts b/packages/anoncreds/src/utils/ledgerObjects.ts new file mode 100644 index 0000000000..ef05779b29 --- /dev/null +++ b/packages/anoncreds/src/utils/ledgerObjects.ts @@ -0,0 +1,105 @@ +import type { GetCredentialDefinitionReturn, GetSchemaReturn, GetRevocationRegistryDefinitionReturn } from '../services' +import type { AgentContext } from '@aries-framework/core' + +import { AriesFrameworkError } from '@aries-framework/core' + +import { AnonCredsRegistryService } from '../services' + +import { + isUnqualifiedSchemaId, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedRevocationRegistryId, +} from './indyIdentifiers' + +export type FetchLedgerObjectsInput = { + credentialDefinitionId?: string + schemaId?: string + revocationRegistryId?: string +} + +export type FetchLedgerObjectsReturn = { + credentialDefinitionReturn: T['credentialDefinitionId'] extends string ? GetCredentialDefinitionReturn : undefined + schemaReturn: T['schemaId'] extends string ? GetSchemaReturn : undefined + revocationRegistryDefinitionReturn: T['revocationRegistryId'] extends string + ? GetRevocationRegistryDefinitionReturn + : undefined +} + +export async function fetchObjectsFromLedger(agentContext: AgentContext, input: T) { + const { credentialDefinitionId, schemaId, revocationRegistryId } = input + + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + let schemaReturn: GetSchemaReturn | undefined = undefined + if (schemaId) { + const result = await registryService + .getRegistryForIdentifier(agentContext, schemaId) + .getSchema(agentContext, schemaId) + + if (!result) throw new AriesFrameworkError('Schema not found') + schemaReturn = result + } + + let credentialDefinitionReturn: GetCredentialDefinitionReturn | undefined = undefined + if (credentialDefinitionId) { + const result = await registryService + .getRegistryForIdentifier(agentContext, credentialDefinitionId) + .getCredentialDefinition(agentContext, credentialDefinitionId) + if (!result) throw new AriesFrameworkError('CredentialDefinition not found') + credentialDefinitionReturn = result + } + + let revocationRegistryDefinitionReturn: GetRevocationRegistryDefinitionReturn | undefined = undefined + if (revocationRegistryId) { + const result = await registryService + .getRegistryForIdentifier(agentContext, revocationRegistryId) + .getRevocationRegistryDefinition(agentContext, revocationRegistryId) + if (!result) throw new AriesFrameworkError('RevocationRegistryDefinition not found') + revocationRegistryDefinitionReturn = result + } + + return { + credentialDefinitionReturn, + schemaReturn, + revocationRegistryDefinitionReturn, + } as FetchLedgerObjectsReturn +} + +export async function fetchQualifiedIds( + agentContext: AgentContext, + input: T +): Promise { + const { schemaId, credentialDefinitionId, revocationRegistryId, schemaIssuerId } = input + + let qSchemaId = schemaId ?? undefined + let qSchemaIssuerId = schemaIssuerId ?? undefined + if (schemaIssuerId && !schemaId) throw new AriesFrameworkError('Cannot fetch schemaIssuerId without schemaId') + if (schemaId && (isUnqualifiedSchemaId(schemaId) || schemaId.startsWith('did:') === false)) { + const { schemaReturn } = await fetchObjectsFromLedger(agentContext, { schemaId }) + qSchemaId = schemaReturn.schemaId + + if (schemaIssuerId && schemaIssuerId.startsWith('did') === false) { + if (!schemaReturn.schema) throw new AriesFrameworkError('Schema not found') + qSchemaIssuerId = schemaReturn.schema.issuerId + } + } + + let qCredentialDefinitionId = credentialDefinitionId ?? undefined + if (credentialDefinitionId && isUnqualifiedCredentialDefinitionId(credentialDefinitionId)) { + const { credentialDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { credentialDefinitionId }) + qCredentialDefinitionId = credentialDefinitionReturn.credentialDefinitionId + } + + let qRevocationRegistryId = revocationRegistryId ?? undefined + if (revocationRegistryId && isUnqualifiedRevocationRegistryId(revocationRegistryId)) { + const { revocationRegistryDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { revocationRegistryId }) + qRevocationRegistryId = revocationRegistryDefinitionReturn.revocationRegistryDefinitionId + } + + return { + schemaId: qSchemaId, + credentialDefinitionId: qCredentialDefinitionId, + revocationRegistryId: qRevocationRegistryId, + schemaIssuerId: qSchemaIssuerId, + } as T & (T['schemaId'] extends string ? { schemaIssuerId: string } : { schemaIssuerId: never }) +} diff --git a/packages/anoncreds/src/utils/w3cUtils.ts b/packages/anoncreds/src/utils/w3cUtils.ts new file mode 100644 index 0000000000..df485e627b --- /dev/null +++ b/packages/anoncreds/src/utils/w3cUtils.ts @@ -0,0 +1,53 @@ +import type { AnonCredsCredential } from '../models' +import type { ProcessCredentialOptions } from '@hyperledger/anoncreds-shared' + +import { + JsonTransformer, + W3cJsonLdVerifiableCredential, + type AgentContext, + type JsonObject, +} from '@aries-framework/core' +import { Credential, W3cCredential } from '@hyperledger/anoncreds-shared' + +import { fetchObjectsFromLedger } from './ledgerObjects' + +export async function legacyCredentialToW3cCredential( + agentContext: AgentContext, + legacyCredential: AnonCredsCredential, + process?: ProcessCredentialOptions +) { + const { credentialDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { + credentialDefinitionId: legacyCredential.cred_def_id, + }) + if (!credentialDefinitionReturn.credentialDefinition) throw new Error('Credential definition not found.') + + let credential: W3cJsonLdVerifiableCredential + let anonCredsCredential: Credential | undefined + let w3cCredentialObj: W3cCredential | undefined + let processed: W3cCredential | undefined + + try { + anonCredsCredential = Credential.fromJson(legacyCredential as unknown as JsonObject) + w3cCredentialObj = anonCredsCredential.toW3c({ + credentialDefinition: credentialDefinitionReturn.credentialDefinition as unknown as JsonObject, + w3cVersion: '1.1', + }) + + processed = process ? w3cCredentialObj.process(process) : w3cCredentialObj + const jsonObject = processed.toJson() + credential = JsonTransformer.fromJSON(jsonObject, W3cJsonLdVerifiableCredential) + } finally { + anonCredsCredential?.handle?.clear() + w3cCredentialObj?.handle?.clear() + processed?.handle?.clear() + } + + return credential +} + +export function w3cToLegacyCredential(credential: W3cJsonLdVerifiableCredential) { + const credentialJson = JsonTransformer.toJSON(credential) + const w3cCredentialObj = W3cCredential.fromJson(credentialJson) + const legacyCredential = w3cCredentialObj.toLegacy().toJson() as unknown as AnonCredsCredential + return legacyCredential +} diff --git a/packages/anoncreds/tests/setup.ts b/packages/anoncreds/tests/setup.ts index 34e38c9705..22e398df7c 100644 --- a/packages/anoncreds/tests/setup.ts +++ b/packages/anoncreds/tests/setup.ts @@ -1 +1,4 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import '@hyperledger/anoncreds-nodejs' + jest.setTimeout(120000) diff --git a/packages/core/src/crypto/index.ts b/packages/core/src/crypto/index.ts index 330a3c70ad..d0f9343ca0 100644 --- a/packages/core/src/crypto/index.ts +++ b/packages/core/src/crypto/index.ts @@ -1,5 +1,6 @@ export { JwsService } from './JwsService' +export { JwsDetachedFormat } from './JwsTypes' export * from './keyUtils' export { KeyType } from './KeyType' diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts index 9e438c6d1c..20262645e8 100644 --- a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts +++ b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts @@ -43,6 +43,7 @@ export type ExtractCredentialFormats = { export interface CredentialFormatCreateReturn { format: CredentialFormatSpec attachment: Attachment + appendAttachments?: Attachment[] } /** @@ -55,6 +56,7 @@ export interface CredentialFormatProcessOptions { export interface CredentialFormatProcessCredentialOptions extends CredentialFormatProcessOptions { requestAttachment: Attachment + requestAppendAttachments?: Attachment[] } export interface CredentialFormatCreateProposalOptions { @@ -102,9 +104,9 @@ export interface CredentialFormatAcceptRequestOptions attachmentId?: string - - requestAttachment: Attachment offerAttachment?: Attachment + requestAttachment: Attachment + requestAppendAttachments?: Attachment[] } // Auto accept method interfaces diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts new file mode 100644 index 0000000000..e9d8b052c2 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts @@ -0,0 +1,71 @@ +import type { + AnonCredsLinkSecretCredentialRequestOptions, + DataIntegrityCredential, + DataIntegrityCredentialOffer, + DataIntegrityCredentialRequest, + DidCommSignedAttachmentCredentialRequestOptions, + W3C_VC_DATA_MODEL_VERSION, +} from './dataIntegrityExchange' +import type { CredentialFormat, JsonObject } from '../../../..' +import type { W3cCredential } from '../../../vc' + +export interface AnonCredsLinkSecretBindingMethodOptions { + credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number +} + +export interface DidCommSignedAttachmentBindingMethodOptions { + didMethodsSupported?: string[] + algsSupported?: string[] +} + +/** + * This defines the module payload for calling CredentialsApi.acceptOffer. No options are available for this + * method, so it's an empty object + */ +export interface DataIntegrityAcceptOfferFormat { + dataModelVersion?: W3C_VC_DATA_MODEL_VERSION + didCommSignedAttachmentCredentialRequestOptions?: DidCommSignedAttachmentCredentialRequestOptions + anonCredsLinkSecretCredentialRequestOptions?: AnonCredsLinkSecretCredentialRequestOptions +} + +/** + * This defines the module payload for calling CredentialsApi.offerCredential + * or CredentialsApi.negotiateProposal + */ +export interface DataIntegrityOfferCredentialFormat { + credential: W3cCredential | JsonObject + bindingRequired: boolean + anonCredsLinkSecretBindingMethodOptions?: AnonCredsLinkSecretBindingMethodOptions + didCommSignedAttachmentBindingMethodOptions?: DidCommSignedAttachmentBindingMethodOptions +} + +/** + * This defines the module payload for calling CredentialsApi.acceptRequest. No options are available for this + * method, so it's an empty object + */ +export type DataIntegrityAcceptRequestFormat = { + didCommSignedAttachmentAcceptRequestOptions?: { + kid: string + } +} + +export interface DataIntegrityCredentialFormat extends CredentialFormat { + formatKey: 'dataIntegrity' + credentialRecordType: 'w3c' + credentialFormats: { + createProposal: never + acceptProposal: never + createOffer: DataIntegrityOfferCredentialFormat + acceptOffer: DataIntegrityAcceptOfferFormat + createRequest: never // cannot start from createRequest + acceptRequest: DataIntegrityAcceptRequestFormat + } + formatData: { + proposal: never + offer: DataIntegrityCredentialOffer + request: DataIntegrityCredentialRequest + credential: DataIntegrityCredential + } +} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts new file mode 100644 index 0000000000..04ee6199ba --- /dev/null +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts @@ -0,0 +1,81 @@ +import type { JsonObject } from '../../../..' +import type { W3cVerifiableCredential } from '../../../vc' + +export type W3C_VC_DATA_MODEL_VERSION = '1.1' | '2.0' + +// This binding method is intended to be used in combination with a credential containing an AnonCreds proof. +export interface AnonCredsLinkSecretBindingMethod { + cred_def_id: string + nonce: string + key_correctness_proof: Record +} + +export interface DidCommSignedAttachmentBindingMethod { + algs_supported: string[] + did_methods_supported: string[] + nonce: string +} + +export interface DataIntegrityBindingMethods { + anoncreds_link_secret?: AnonCredsLinkSecretBindingMethod + didcomm_signed_attachment?: DidCommSignedAttachmentBindingMethod +} + +export interface DataIntegrityCredentialOffer { + // List of strings indicating the supported VC Data Model versions. + // The list MUST contain at least one value. The values MUST be a valid data model version. Current supported values include 1.1 and 2.0. + data_model_versions_supported: W3C_VC_DATA_MODEL_VERSION[] + // Boolean indicating whether the credential MUST be bound to the holder. If omitted, the credential is not required to be bound to the holder. + // If set to true, the credential MUST be bound to the holder using at least one of the binding methods defined in binding_method. + binding_required?: boolean + // Required if binding_required is true. + // Object containing key-value pairs of binding methods supported by the issuer to bind the credential to a holder. + // If the value is omitted, this indicates the issuer does not support any binding methods for issuance of the credential. + binding_method?: DataIntegrityBindingMethods + // The credential should be compliant with the VC Data Model. + // The credential MUST NOT contain any proofs. + // Some properties MAY be omitted if they will only be available at time of issuance, such as issuanceDate, issuer, credentialSubject.id, credentialStatus, credentialStatus.id. + // The credential MUST be conformant with one of the data model versions indicated in data_model_versions_supported. + credential: JsonObject +} + +export interface AnonCredsLinkSecretDataIntegrityBindingProof { + cred_def_id: string + entropy: string + blinded_ms: Record + blinded_ms_correctness_proof: Record + nonce: string +} + +export interface DidCommSignedAttachmentDataIntegrityBindingProof { + // The id of the appended attachment included in the request message that contains the signed attachment. + attachment_id: string +} + +export interface DataIntegrityCredentialRequestBindingProof { + anoncreds_link_secret?: AnonCredsLinkSecretDataIntegrityBindingProof + didcomm_signed_attachment?: DidCommSignedAttachmentDataIntegrityBindingProof +} + +export interface DataIntegrityCredentialRequest { + // The data model version of the credential to be issued. The value MUST be a valid data model version and match one of the values from the data_model_versions_supported offer. + data_model_version: W3C_VC_DATA_MODEL_VERSION + // Required if binding_required is true in the offer. + // Object containing key-value pairs of proofs for the binding to the holder. + // The keys MUST match keys of the binding_method object from the offer. + // See Binding Methods for a registry of default binding methods supported as part of this RFC. + binding_proof?: DataIntegrityCredentialRequestBindingProof +} + +export interface AnonCredsLinkSecretCredentialRequestOptions { + linkSecretId?: string +} + +export interface DidCommSignedAttachmentCredentialRequestOptions { + kid: string + alg?: string +} + +export interface DataIntegrityCredential { + credential: W3cVerifiableCredential +} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityMetadata.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityMetadata.ts new file mode 100644 index 0000000000..08d7ecf11d --- /dev/null +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityMetadata.ts @@ -0,0 +1,45 @@ +/** + * Metadata key for storing metadata. + * + * MUST be used with {@link DataIntegrityMetadata} + */ +export const DataIntegrityMetadataKey = '_dataIntegrity/credential' + +export interface DataIntegrityLinkSecretMetadata { + schemaId: string + credentialDefinitionId: string + revocationRegistryId?: string + credentialRevocationId?: string +} + +/** + * Metadata for an data integrity credential offers / requests that will be stored + * in the credential record. + * + * MUST be used with {@link DataIntegrityMetadataKey} + */ +export interface DataIntegrityMetadata { + linkSecretMetadata?: DataIntegrityLinkSecretMetadata +} + +/** + * Metadata key for storing metadata on an anonCreds link secret credential request. + * + * MUST be used with {@link AnonCredsCredentialRequestMetadata} + */ +export const DataIntegrityRequestMetadataKey = '_dataIntegrity/credentialRequest' + +export interface DataIntegrityLinkSecretRequestMetadata { + link_secret_blinding_data: AnonCredsLinkSecretBlindingData + link_secret_name: string + nonce: string +} + +export interface DataIntegrityRequestMetadata { + linkSecretRequestMetadata?: DataIntegrityLinkSecretRequestMetadata +} + +export interface AnonCredsLinkSecretBlindingData { + v_prime: string + vr_prime: string | null +} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts new file mode 100644 index 0000000000..24724deb61 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts @@ -0,0 +1,3 @@ +export * from './DataIntegrityCredentialFormat' +export * from './dataIntegrityExchange' +export * from './dataIntegrityMetadata' diff --git a/packages/core/src/modules/credentials/formats/index.ts b/packages/core/src/modules/credentials/formats/index.ts index fb2b300b5e..38d2aaa5a1 100644 --- a/packages/core/src/modules/credentials/formats/index.ts +++ b/packages/core/src/modules/credentials/formats/index.ts @@ -2,3 +2,4 @@ export * from './CredentialFormatService' export * from './CredentialFormatServiceOptions' export * from './CredentialFormat' export * from './jsonld' +export * from './dataIntegrity' diff --git a/packages/core/src/modules/credentials/index.ts b/packages/core/src/modules/credentials/index.ts index d34680afe1..50577689ad 100644 --- a/packages/core/src/modules/credentials/index.ts +++ b/packages/core/src/modules/credentials/index.ts @@ -1,9 +1,9 @@ +export * from './CredentialEvents' export * from './CredentialsApi' export * from './CredentialsApiOptions' -export * from './repository' -export * from './CredentialEvents' -export * from './models' -export * from './formats' -export * from './protocol' export * from './CredentialsModule' export * from './CredentialsModuleConfig' +export * from './formats' +export * from './models' +export * from './protocol' +export * from './repository' diff --git a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts index c8fe64e86d..2c651beefc 100644 --- a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts +++ b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts @@ -319,6 +319,7 @@ export class CredentialFormatCoordinator // create message. there are two arrays in each message, one for formats the other for attachments const formats: CredentialFormatSpec[] = [] const requestAttachments: Attachment[] = [] + const requestAppendAttachments: Attachment[] = [] for (const formatService of formatServices) { const offerAttachment = this.getAttachmentForService( @@ -327,7 +328,7 @@ export class CredentialFormatCoordinator offerMessage.offerAttachments ) - const { attachment, format } = await formatService.acceptOffer(agentContext, { + const { attachment, format, appendAttachments } = await formatService.acceptOffer(agentContext, { offerAttachment, credentialRecord, credentialFormats, @@ -335,6 +336,7 @@ export class CredentialFormatCoordinator requestAttachments.push(attachment) formats.push(format) + if (appendAttachments) requestAppendAttachments.push(...appendAttachments) } credentialRecord.credentialAttributes = offerMessage.credentialPreview?.attributes @@ -343,6 +345,7 @@ export class CredentialFormatCoordinator formats, requestAttachments: requestAttachments, comment, + attachments: requestAppendAttachments, }) message.setThread({ threadId: credentialRecord.threadId, parentThreadId: credentialRecord.parentThreadId }) @@ -486,6 +489,7 @@ export class CredentialFormatCoordinator offerAttachment, credentialRecord, credentialFormats, + requestAppendAttachments: requestMessage.appendedAttachments, }) credentialAttachments.push(attachment) @@ -538,6 +542,7 @@ export class CredentialFormatCoordinator attachment, requestAttachment, credentialRecord, + requestAppendAttachments: requestMessage.appendedAttachments, }) } diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts index ed7e08f228..ea81f6aae9 100644 --- a/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts @@ -11,6 +11,7 @@ export interface V2RequestCredentialMessageOptions { formats: CredentialFormatSpec[] requestAttachments: Attachment[] comment?: string + attachments?: Attachment[] } export class V2RequestCredentialMessage extends AgentMessage { @@ -21,6 +22,7 @@ export class V2RequestCredentialMessage extends AgentMessage { this.comment = options.comment this.formats = options.formats this.requestAttachments = options.requestAttachments + this.appendedAttachments = options.attachments } } diff --git a/packages/core/src/modules/vc/W3cCredentialService.ts b/packages/core/src/modules/vc/W3cCredentialService.ts index 1cca4272de..781789e34f 100644 --- a/packages/core/src/modules/vc/W3cCredentialService.ts +++ b/packages/core/src/modules/vc/W3cCredentialService.ts @@ -174,6 +174,7 @@ export class W3cCredentialService { const w3cCredentialRecord = new W3cCredentialRecord({ tags: { expandedTypes }, credential: options.credential, + anonCredsCredentialRecordOptions: options.anonCredsCredentialRecordOptions, }) // Store the w3c credential record diff --git a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts index 6f15025e1c..3121936aa3 100644 --- a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts +++ b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts @@ -5,6 +5,7 @@ import type { W3cJwtVerifiablePresentation } from './jwt-vc/W3cJwtVerifiablePres import type { ClaimFormat, W3cVerifiableCredential } from './models' import type { W3cCredential } from './models/credential/W3cCredential' import type { W3cPresentation } from './models/presentation/W3cPresentation' +import type { AnonCredsCredentialRecordOptions } from './repository' import type { JwaSignatureAlgorithm } from '../../crypto/jose/jwa' import type { SingleOrArray } from '../../utils/type' @@ -175,4 +176,5 @@ export interface W3cJsonLdVerifyPresentationOptions extends W3cVerifyPresentatio export interface StoreCredentialOptions { credential: W3cVerifiableCredential + anonCredsCredentialRecordOptions?: AnonCredsCredentialRecordOptions } diff --git a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts index 881578f8cf..9db48232aa 100644 --- a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts +++ b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts @@ -48,6 +48,7 @@ export class LinkedDataProof { public verificationMethod!: string @IsString() + @IsOptional() public created!: string @IsUri() diff --git a/packages/core/src/modules/vc/index.ts b/packages/core/src/modules/vc/index.ts index 84a0da17c7..fb59e32f95 100644 --- a/packages/core/src/modules/vc/index.ts +++ b/packages/core/src/modules/vc/index.ts @@ -1,4 +1,5 @@ export * from './W3cCredentialService' +export * from './W3cCredentialsModuleConfig' export * from './W3cCredentialServiceOptions' export * from './repository' export * from './W3cCredentialsModule' diff --git a/packages/core/src/modules/vc/models/credential/W3cCredential.ts b/packages/core/src/modules/vc/models/credential/W3cCredential.ts index c81b1b3daf..e62cd75ad0 100644 --- a/packages/core/src/modules/vc/models/credential/W3cCredential.ts +++ b/packages/core/src/modules/vc/models/credential/W3cCredential.ts @@ -4,7 +4,7 @@ import type { JsonObject } from '../../../../types' import type { ValidationOptions } from 'class-validator' import { Expose, Type } from 'class-transformer' -import { IsInstance, buildMessage, IsOptional, IsRFC3339, ValidateBy, ValidateNested } from 'class-validator' +import { buildMessage, IsInstance, IsOptional, IsRFC3339, ValidateBy, ValidateNested } from 'class-validator' import { asArray, JsonTransformer, mapSingleOrArray } from '../../../../utils' import { SingleOrArray } from '../../../../utils/type' @@ -14,8 +14,8 @@ import { IsCredentialJsonLdContext } from '../../validators' import { W3cCredentialSchema } from './W3cCredentialSchema' import { W3cCredentialStatus } from './W3cCredentialStatus' -import { W3cCredentialSubject } from './W3cCredentialSubject' -import { W3cIssuer, IsW3cIssuer, W3cIssuerTransformer } from './W3cIssuer' +import { IsW3cCredentialSubject, W3cCredentialSubject, W3cCredentialSubjectTransformer } from './W3cCredentialSubject' +import { IsW3cIssuer, W3cIssuer, W3cIssuerTransformer } from './W3cIssuer' export interface W3cCredentialOptions { context?: Array @@ -75,9 +75,8 @@ export class W3cCredential { @IsOptional() public expirationDate?: string - @Type(() => W3cCredentialSubject) - @ValidateNested({ each: true }) - @IsInstanceOrArrayOfInstances({ classType: W3cCredentialSubject }) + @IsW3cCredentialSubject({ each: true }) + @W3cCredentialSubjectTransformer() public credentialSubject!: SingleOrArray @IsOptional() diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts index 056d6b43bc..f50f77e161 100644 --- a/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts @@ -1,41 +1,86 @@ -import { Transform, TransformationType, plainToInstance, instanceToPlain } from 'class-transformer' -import { IsOptional, isString } from 'class-validator' +import type { ValidationOptions } from 'class-validator' -import { IsUri } from '../../../../utils/validators' +import { Transform, TransformationType } from 'class-transformer' +import { IsOptional, ValidateBy, buildMessage, isInstance } from 'class-validator' + +import { AriesFrameworkError } from '../../../../error' +import { IsUri, isUri } from '../../../../utils/validators' /** - * TODO: check how to support arbitrary data in class * @see https://www.w3.org/TR/vc-data-model/#credential-subject */ export interface W3cCredentialSubjectOptions { id?: string + // note claims must not contain an id field + claims?: Record } export class W3cCredentialSubject { public constructor(options: W3cCredentialSubjectOptions) { if (options) { this.id = options.id + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, ...claims } = options.claims ?? {} + this.claims = Object.keys(claims).length > 0 ? claims : undefined } } @IsUri() @IsOptional() public id?: string -} -// Custom transformers + @IsOptional() + public claims?: Record +} export function W3cCredentialSubjectTransformer() { - return Transform(({ value, type }: { value: string | W3cCredentialSubjectOptions; type: TransformationType }) => { + return Transform(({ value, type }: { value: W3cCredentialSubjectOptions; type: TransformationType }) => { if (type === TransformationType.PLAIN_TO_CLASS) { - if (isString(value)) return value - return plainToInstance(W3cCredentialSubject, value) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const vToClass = (v: any) => { + if (isInstance(v, W3cCredentialSubject)) return v + const { id, ...claims } = v + return new W3cCredentialSubject({ id, claims }) + } + + if (Array.isArray(value) && value.length === 0) { + throw new AriesFrameworkError('At least one credential subject is required') + } + + return Array.isArray(value) ? value.map(vToClass) : vToClass(value) } else if (type === TransformationType.CLASS_TO_PLAIN) { - if (isString(value)) return value - return instanceToPlain(value) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const vToJson = (v: any) => { + if (isInstance(v, W3cCredentialSubject)) return v.id ? { ...v.claims, id: v.id } : { ...v.claims } + if (v.claims) throw new Error('Credential subject claims in plain json') + return v + } + + return Array.isArray(value) ? value.every(vToJson) : vToJson(value) } // PLAIN_TO_PLAIN return value }) } + +export function IsW3cCredentialSubject(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsW3cCredentialSubject', + validator: { + validate: (value): boolean => { + return isInstance(value, W3cCredentialSubject) && (!value.id || isUri(value.id)) + }, + defaultMessage: buildMessage( + (eachPrefix) => + eachPrefix + + '$property must be an object or an array of objects with an optional id property which is an URI', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/credential/__tests__/W3cCredential.test.ts b/packages/core/src/modules/vc/models/credential/__tests__/W3cCredential.test.ts index 99d1ede256..9aa625b3ac 100644 --- a/packages/core/src/modules/vc/models/credential/__tests__/W3cCredential.test.ts +++ b/packages/core/src/modules/vc/models/credential/__tests__/W3cCredential.test.ts @@ -138,9 +138,7 @@ describe('W3cCredential', () => { }) test('throws an error when credentialSubject is present and it is not a valid credentialSubject object/array', () => { - expect(() => JsonTransformer.fromJSON({ ...validCredential, credentialSubject: [] }, W3cCredential)).toThrowError( - /credentialSubject has failed the following constraints: credentialSubject value must be an instance of, or an array of instances containing W3cCredentialSubject/ - ) + expect(() => JsonTransformer.fromJSON({ ...validCredential, credentialSubject: [] }, W3cCredential)).toThrow() expect(() => JsonTransformer.fromJSON( @@ -154,7 +152,51 @@ describe('W3cCredential', () => { }, W3cCredential ) - ).toThrowError(/property credentialSubject\[0\]\.id has failed the following constraints: id must be an URI/) + ).toThrow() + + expect(() => + JsonTransformer.fromJSON( + { + ...validCredential, + credentialSubject: { id: 'urn:uri' }, + }, + W3cCredential + ) + ).not.toThrowError() + + expect(() => + JsonTransformer.fromJSON( + { + ...validCredential, + credentialSubject: { id: 'urn:uri', claims: { some: 'value', someOther: { nested: 'value' } } }, + }, + W3cCredential + ) + ).not.toThrowError() + + expect(() => + JsonTransformer.fromJSON( + [ + { + ...validCredential, + credentialSubject: { id: 'urn:uri', claims: { some: 'value1', someOther: { nested: 'value1' } } }, + }, + ], + W3cCredential + ) + ).not.toThrowError() + + expect(() => + JsonTransformer.fromJSON( + [ + { + ...validCredential, + credentialSubject: { id: 'urn:uri', claims: { some: 'value2', someOther: { nested: 'value2' } } }, + }, + ], + W3cCredential + ) + ).not.toThrowError() expect(() => JsonTransformer.fromJSON( diff --git a/packages/core/src/modules/vc/repository/W3CAnoncredsCredentialMetadata.ts b/packages/core/src/modules/vc/repository/W3CAnoncredsCredentialMetadata.ts new file mode 100644 index 0000000000..c1bb8ebda4 --- /dev/null +++ b/packages/core/src/modules/vc/repository/W3CAnoncredsCredentialMetadata.ts @@ -0,0 +1,36 @@ +import { IsOptional, IsString } from 'class-validator' + +export interface W3CAnonCredsCredentialMetadataOptions { + credentialId: string + methodName: string + credentialRevocationId?: string + linkSecretId: string +} + +export class W3cAnonCredsCredentialMetadata { + public constructor(options: W3CAnonCredsCredentialMetadataOptions) { + if (options) { + this.credentialId = options.credentialId + this.methodName = options.methodName + this.credentialRevocationId = options.credentialRevocationId + this.linkSecretId = options.linkSecretId + } + } + + @IsString() + public credentialId!: string + + @IsString() + @IsOptional() + public credentialRevocationId?: string + + @IsString() + public linkSecretId!: string + + /** + * AnonCreds method name. We don't use names explicitly from the registry (there's no identifier for a registry) + * @see https://hyperledger.github.io/anoncreds-methods-registry/ + */ + @IsString() + public methodName!: string +} diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts index a5aa1fe070..42a710d198 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts @@ -1,23 +1,39 @@ -import type { TagsBase } from '../../../storage/BaseRecord' +import type { AnonCredsClaimRecord } from './anonCredsCredentialValue' +import type { Tags, TagsBase } from '../../../storage/BaseRecord' import type { Constructable } from '../../../utils/mixins' +import { Type } from 'class-transformer' +import { IsOptional } from 'class-validator' + +import { AriesFrameworkError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' import { JsonTransformer } from '../../../utils' import { uuid } from '../../../utils/uuid' -import { W3cVerifiableCredential, W3cVerifiableCredentialTransformer, ClaimFormat } from '../models' +import { ClaimFormat, W3cVerifiableCredential, W3cVerifiableCredentialTransformer } from '../models' + +import { W3cAnonCredsCredentialMetadata } from './W3CAnoncredsCredentialMetadata' +import { mapAttributeRawValuesToAnonCredsCredentialValues } from './anonCredsCredentialValue' + +export interface AnonCredsCredentialRecordOptions { + credentialId: string + credentialRevocationId?: string + linkSecretId: string + schemaName: string + schemaVersion: string + schemaIssuerId: string + methodName: string + // TODO: derive from proof + schemaId: string + credentialDefinitionId: string + revocationRegistryId?: string +} export interface W3cCredentialRecordOptions { id?: string createdAt?: Date credential: W3cVerifiableCredential tags: CustomW3cCredentialTags -} - -export type CustomW3cCredentialTags = TagsBase & { - /** - * Expanded types are used for JSON-LD credentials to allow for filtering on the expanded type. - */ - expandedTypes?: Array + anonCredsCredentialRecordOptions?: AnonCredsCredentialRecordOptions } export type DefaultW3cCredentialTags = { @@ -34,20 +50,77 @@ export type DefaultW3cCredentialTags = { algs?: Array } -export class W3cCredentialRecord extends BaseRecord { +export type DefaultAnonCredsCredentialTags = { + credentialId: string + linkSecretId: string + credentialDefinitionId: string + credentialRevocationId?: string + revocationRegistryId?: string + schemaId: string + methodName: string + + // the following keys can be used for every `attribute name` in credential. + [key: `attr::${string}::marker`]: true | undefined + [key: `attr::${string}::value`]: string | undefined +} + +export type CustomW3cCredentialTags = TagsBase & { + /** + * Expanded types are used for JSON-LD credentials to allow for filtering on the expanded type. + */ + expandedTypes?: Array +} + +export type CustomAnonCredsCredentialTags = { + schemaName: string + schemaVersion: string + schemaIssuerId: string + + // TODO: derive from proof + schemaId: string + credentialDefinitionId: string + revocationRegistryId?: string +} + +export class W3cCredentialRecord extends BaseRecord< + DefaultW3cCredentialTags & Partial, + CustomW3cCredentialTags & Partial +> { public static readonly type = 'W3cCredentialRecord' public readonly type = W3cCredentialRecord.type @W3cVerifiableCredentialTransformer() public credential!: W3cVerifiableCredential + @IsOptional() + @Type(() => W3cAnonCredsCredentialMetadata) + public readonly anonCredsCredentialMetadata?: W3cAnonCredsCredentialMetadata + public constructor(props: W3cCredentialRecordOptions) { super() if (props) { this.id = props.id ?? uuid() this.createdAt = props.createdAt ?? new Date() - this._tags = props.tags this.credential = props.credential + + if (props.anonCredsCredentialRecordOptions) { + this.anonCredsCredentialMetadata = new W3cAnonCredsCredentialMetadata({ + credentialId: props.anonCredsCredentialRecordOptions.credentialId, + credentialRevocationId: props.anonCredsCredentialRecordOptions.credentialRevocationId, + linkSecretId: props.anonCredsCredentialRecordOptions.linkSecretId, + methodName: props.anonCredsCredentialRecordOptions.methodName, + }) + } + + this.setTags({ + ...props.tags, + schemaIssuerId: props.anonCredsCredentialRecordOptions?.schemaIssuerId, + schemaName: props.anonCredsCredentialRecordOptions?.schemaName, + schemaVersion: props.anonCredsCredentialRecordOptions?.schemaVersion, + schemaId: props.anonCredsCredentialRecordOptions?.schemaId, + credentialDefinitionId: props.anonCredsCredentialRecordOptions?.credentialDefinitionId, + revocationRegistryId: props.anonCredsCredentialRecordOptions?.revocationRegistryId, + }) } } @@ -76,7 +149,45 @@ export class W3cCredentialRecord extends BaseRecord | undefined { + if (!this.anonCredsCredentialMetadata) return undefined + + const { schemaId, schemaName, schemaVersion, schemaIssuerId, credentialDefinitionId } = this._tags + if (!schemaId || !schemaName || !schemaVersion || !schemaIssuerId || !credentialDefinitionId) return undefined + + const anonCredsCredentialTags: Tags = { + schemaIssuerId, + schemaName, + schemaVersion, + schemaId, + credentialDefinitionId, + revocationRegistryId: this._tags.revocationRegistryId, + credentialId: this.anonCredsCredentialMetadata?.credentialId as string, + credentialRevocationId: this.anonCredsCredentialMetadata?.credentialRevocationId as string, + linkSecretId: this.anonCredsCredentialMetadata?.linkSecretId as string, + methodName: this.anonCredsCredentialMetadata?.methodName as string, + } + + if (Array.isArray(this.credential.credentialSubject)) { + throw new AriesFrameworkError('Credential subject must be an object, not an array.') + } + + const values = mapAttributeRawValuesToAnonCredsCredentialValues( + (this.credential.credentialSubject.claims as AnonCredsClaimRecord) ?? {} + ) + + for (const [key, value] of Object.entries(values)) { + anonCredsCredentialTags[`attr::${key}::value`] = value.raw + anonCredsCredentialTags[`attr::${key}::marker`] = true + } + + return anonCredsCredentialTags } /** diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts b/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts index cb5a83b735..d53976e458 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts @@ -1,6 +1,8 @@ +import type { AgentContext } from '../../../agent' + import { EventEmitter } from '../../../agent/EventEmitter' import { InjectionSymbols } from '../../../constants' -import { injectable, inject } from '../../../plugins' +import { inject, injectable } from '../../../plugins' import { Repository } from '../../../storage/Repository' import { StorageService } from '../../../storage/StorageService' @@ -14,4 +16,16 @@ export class W3cCredentialRepository extends Repository { ) { super(W3cCredentialRecord, 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 }) + } + + public async getByCredentialId(agentContext: AgentContext, credentialId: string) { + return this.getSingleByQuery(agentContext, { credentialId }) + } } diff --git a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts index e64caa1f95..3f0da9de21 100644 --- a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts +++ b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts @@ -1,21 +1,93 @@ +import type { AnonCredsCredentialRecordOptions } from '../W3cCredentialRecord' + import { JsonTransformer } from '../../../../utils' import { Ed25519Signature2018Fixtures } from '../../data-integrity/__tests__/fixtures' import { W3cJsonLdVerifiableCredential } from '../../data-integrity/models' +import { W3cCredentialSubject } from '../../models' import { W3cCredentialRecord } from '../W3cCredentialRecord' describe('W3cCredentialRecord', () => { describe('getTags', () => { - it('should return default tags', () => { + it('should return default tags (w3c credential)', () => { + const credential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + + const w3cCredentialRecord = new W3cCredentialRecord({ + credential, + tags: { + expandedTypes: ['https://expanded.tag#1'], + }, + }) + + expect(w3cCredentialRecord.getTags()).toEqual({ + claimFormat: 'ldp_vc', + issuerId: credential.issuerId, + subjectIds: credential.credentialSubjectIds, + schemaIds: credential.credentialSchemaIds, + contexts: credential.contexts, + proofTypes: credential.proofTypes, + givenId: credential.id, + credentialDefinitionId: undefined, + revocationRegistryId: undefined, + schemaId: undefined, + schemaIssuerId: undefined, + schemaName: undefined, + schemaVersion: undefined, + expandedTypes: ['https://expanded.tag#1'], + }) + + expect(w3cCredentialRecord.getAnonCredsTags()).toBeUndefined() + }) + + it('should return default tags (w3cAnoncredsCredential)', () => { const credential = JsonTransformer.fromJSON( Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, W3cJsonLdVerifiableCredential ) + const anonCredsCredentialRecordOptions: AnonCredsCredentialRecordOptions = { + schemaIssuerId: 'schemaIssuerId', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + schemaId: 'schemaId', + credentialDefinitionId: 'credentialDefinitionId', + credentialId: 'credentialId', + credentialRevocationId: 'credentialRevocationId', + linkSecretId: 'linkSecretId', + methodName: 'methodName', + revocationRegistryId: 'revocationRegistryId', + } + + if (Array.isArray(credential.credentialSubject)) throw new Error('Invalid credentialSubject') + credential.credentialSubject = new W3cCredentialSubject({ claims: { degree: 'Bachelor of Science and Arts' } }) const w3cCredentialRecord = new W3cCredentialRecord({ credential, tags: { expandedTypes: ['https://expanded.tag#1'], }, + anonCredsCredentialRecordOptions, + }) + + const anoncredsCredentialTags = { + linkSecretId: 'linkSecretId', + methodName: 'methodName', + schemaId: 'schemaId', + schemaIssuerId: 'schemaIssuerId', + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + credentialDefinitionId: 'credentialDefinitionId', + credentialId: 'credentialId', + 'attr::degree::marker': true, + 'attr::degree::value': 'Bachelor of Science and Arts', + revocationRegistryId: 'revocationRegistryId', + credentialRevocationId: 'credentialRevocationId', + } + + const anonCredsTags = w3cCredentialRecord.getAnonCredsTags() + expect(anonCredsTags).toEqual({ + ...anoncredsCredentialTags, }) expect(w3cCredentialRecord.getTags()).toEqual({ @@ -27,6 +99,7 @@ describe('W3cCredentialRecord', () => { proofTypes: credential.proofTypes, givenId: credential.id, expandedTypes: ['https://expanded.tag#1'], + ...anoncredsCredentialTags, }) }) }) diff --git a/packages/core/src/modules/vc/repository/__tests__/anonCredsCredentialValue.test.ts b/packages/core/src/modules/vc/repository/__tests__/anonCredsCredentialValue.test.ts new file mode 100644 index 0000000000..74494a1984 --- /dev/null +++ b/packages/core/src/modules/vc/repository/__tests__/anonCredsCredentialValue.test.ts @@ -0,0 +1,124 @@ +import { encodeCredentialValue, mapAttributeRawValuesToAnonCredsCredentialValues } from '../anonCredsCredentialValue' + +const testVectors = { + 'str 0.0': { + raw: '0.0', + encoded: '62838607218564353630028473473939957328943626306458686867332534889076311281879', + }, + // conversion error! + // this does not work in js + // 'float 0.0': { + // raw: 0.0, + // encoded: '62838607218564353630028473473939957328943626306458686867332534889076311281879', + // }, + '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', + }, + 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', + }, + Undefined: { + raw: undefined, + encoded: '99769404535520360775991420569103450442789945655240760487761322098828903685777', + }, + 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', + }, + + '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', () => { + test('encoding algorithm', async () => { + Object.values(testVectors).forEach((vector) => { + expect(encodeCredentialValue(vector.raw)).toEqual(vector.encoded) + }) + }) + + test('test attribute record value mapping', () => { + const attrsExpected = { + address2: { + raw: '101 Wilson Lane', + encoded: '68086943237164982734333428280784300550565381723532936263016368251445461241953', + }, + zip: { + raw: '87121', + encoded: '87121', + }, + state: { + raw: 'UT', + encoded: '93856629670657830351991220989031130499313559332549427637940645777813964461231', + }, + } + + const attrs = { + address2: '101 Wilson Lane', + zip: '87121', + state: 'UT', + } + + expect(mapAttributeRawValuesToAnonCredsCredentialValues(attrs)).toMatchObject(attrsExpected) + }) +}) diff --git a/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts b/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts new file mode 100644 index 0000000000..52f386e997 --- /dev/null +++ b/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts @@ -0,0 +1,73 @@ +import bigInt from 'big-integer' + +import { AriesFrameworkError } from '../../../error' +import { Buffer, Hasher, TypedArrayEncoder } from '../../../utils' + +export type AnonCredsClaimRecord = Record + +export interface AnonCredsCredentialValue { + raw: string + encoded: string // Raw value as number in string +} + +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 +} + +// TODO: this function can only encode strings +// If encoding numbers we run into problems with 0.0 representing the same value as 0 and is implicitly converted to 0 +/** + * 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 const encodeCredentialValue = (data: unknown): string => { + if (typeof data === 'boolean') return data ? '1' : '0' + + // Keep any 32-bit integer as is + if (typeof data === 'number' && isInt32(data)) { + return String(data) + } + + // Convert any string integer (e.g. "1234") to be a 32-bit integer (e.g. 1234) + if (typeof data === 'string' && data !== '' && !isNaN(Number(data)) && isNumeric(data) && isInt32(Number(data))) { + return Number(data).toString() + } + + data = data === undefined || data === null ? 'None' : data + + const buffer = TypedArrayEncoder.fromString(String(data)) + const hash = Hasher.hash(buffer, 'sha2-256') + const hex = Buffer.from(hash).toString('hex') + + return bigInt(hex, 16).toString() +} + +export const mapAttributeRawValuesToAnonCredsCredentialValues = ( + record: AnonCredsClaimRecord +): Record => { + const credentialValues: Record = {} + + for (const [key, value] of Object.entries(record)) { + if (typeof value === 'object') { + throw new AriesFrameworkError(`Unsupported value type: object for W3cAnonCreds Credential`) + } + credentialValues[key] = { + raw: value.toString(), + encoded: encodeCredentialValue(value), + } + } + + return credentialValues +} diff --git a/packages/core/src/modules/vc/repository/index.ts b/packages/core/src/modules/vc/repository/index.ts index 64aae1fdcb..3f906c4edb 100644 --- a/packages/core/src/modules/vc/repository/index.ts +++ b/packages/core/src/modules/vc/repository/index.ts @@ -1,2 +1,8 @@ export * from './W3cCredentialRecord' export * from './W3cCredentialRepository' +export { + encodeCredentialValue, + AnonCredsCredentialValue, + mapAttributeRawValuesToAnonCredsCredentialValues, + AnonCredsClaimRecord, +} from './anonCredsCredentialValue' diff --git a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts index 0af8f27508..8f040c3588 100644 --- a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts +++ b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts @@ -1,5 +1,4 @@ -import type { AnonCredsCredentialValue } from '@aries-framework/anoncreds' -import type { Agent, FileSystem, WalletConfig } from '@aries-framework/core' +import type { Agent, FileSystem, WalletConfig, AnonCredsCredentialValue } from '@aries-framework/core' import type { EntryObject } from '@hyperledger/aries-askar-shared' import { AnonCredsCredentialRecord, AnonCredsLinkSecretRecord } from '@aries-framework/anoncreds' diff --git a/yarn.lock b/yarn.lock index 7f2ecb49e3..7b847e0f1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1212,22 +1212,22 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@hyperledger/anoncreds-nodejs@^0.2.0-dev.4": - version "0.2.0-dev.4" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.4.tgz#ac125817beb631dedbe27cb8d4c21d2123104d5e" - integrity sha512-EH/jAH+aATH9KByWF1lk1p76BN6VIsRZhG7jyRT1LAaaUNnmpQnjX6d/Mfkofvk4xFIRbp0cDl/UjaKaKfLsww== +"@hyperledger/anoncreds-nodejs@^0.2.0-dev.7": + version "0.2.0-dev.7" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.7.tgz#5a69f1915eb2e1b8e5dc6782d35f2c27b6034ddc" + integrity sha512-CfeVgwhhl4+W/9AVqgErXOi3k4xDt1sEOGU+WOcWGBHQs5ONLUhj6sOS7/wQHECajQIWtmmC66ktynbRK0FySg== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/anoncreds-shared" "0.2.0-dev.4" + "@hyperledger/anoncreds-shared" "0.2.0-dev.7" "@mapbox/node-pre-gyp" "^1.0.11" ref-array-di "1.2.2" ref-struct-di "1.1.1" -"@hyperledger/anoncreds-shared@0.2.0-dev.4", "@hyperledger/anoncreds-shared@^0.2.0-dev.4": - version "0.2.0-dev.4" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.4.tgz#8050647fcb153b594671270d4c51b3b423e575be" - integrity sha512-8hwXc9zab8MgXgwo0OL5bShxMAfDEiBAB1/r+8mbwgANefDZwHwNOkq0yQLwT2KfSsvH9la7N2ehrtUf5E2FKg== +"@hyperledger/anoncreds-shared@0.2.0-dev.7", "@hyperledger/anoncreds-shared@^0.2.0-dev.7": + version "0.2.0-dev.7" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.7.tgz#bd9d21448ea41bb2071a149d17606d1c3745314e" + integrity sha512-d+V2lD8kUlWzkS4oXD9GprPbQBNSd3XD2nQD3GIvlbpkpJU5d2H7nKqBWUbXYe+wpHK0H9boWgOC74gsFwxxUg== "@hyperledger/aries-askar-nodejs@^0.2.0-dev.1": version "0.2.0-dev.1" From 3de7105cfc27ed5ca5e10cc97a045a1c966045d3 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 23 Jan 2024 11:23:42 +0100 Subject: [PATCH 02/38] fix: mainly issue regarding credential not being processed after reception --- .../formats/AnonCredsProofFormatService.ts | 4 +- .../DataIntegrityCredentialFormatService.ts | 64 +++++++++++++------ packages/anoncreds/src/utils/ledgerObjects.ts | 14 +++- packages/core/package.json | 3 - .../dataIntegrity/dataIntegrityExchange.ts | 2 +- .../utils/credentialSelection.ts | 1 - 6 files changed, 61 insertions(+), 27 deletions(-) diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index 419a9c3ebe..d510759f72 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -239,13 +239,15 @@ export class AnonCredsProofFormatService implements ProofFormatService(DataIntegrityMetadataKey) if (!metadata?.linkSecretMetadata) throw new AriesFrameworkError('Missing link secret metadata') - metadata.linkSecretMetadata.revocationRegistryId = qRevocationRegistryId + metadata.linkSecretMetadata.revocationRegistryId = + revocationRegistryDefinitionReturn?.revocationRegistryDefinitionId metadata.linkSecretMetadata.credentialRevocationId = revocationRegistryIndex?.toString() credentialRecord.metadata.set(DataIntegrityMetadataKey, metadata) } - return anonCredsCredentialRecordOptions + return { processed: processed.toJson(), anonCredsCredentialRecordOptions } } /** @@ -756,21 +769,31 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer } // TODO: validate credential structure - const { credential } = attachment.getDataAsJson() - const w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credential, W3cJsonLdVerifiableCredential) + const { credential: credentialJson } = attachment.getDataAsJson() let anonCredsCredentialRecordOptions: AnonCredsCredentialRecordOptions | undefined + let w3cJsonLdVerifiableCredential: W3cJsonLdVerifiableCredential if (credentialRequest.binding_proof?.anoncreds_link_secret) { if (!credentialRequestMetadata.linkSecretRequestMetadata) { throw new AriesFrameworkError('Missing link secret request metadata') } - anonCredsCredentialRecordOptions = await this.processLinkSecretBoundCredential( + const { anonCredsCredentialRecordOptions: options, processed } = await this.processLinkSecretBoundCredential( agentContext, - w3cJsonLdVerifiableCredential, + credentialJson, credentialRecord, credentialRequestMetadata.linkSecretRequestMetadata ) + anonCredsCredentialRecordOptions = options + + w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(processed, W3cJsonLdVerifiableCredential) + await this.assertCredentialAttributesMatchSchemaAttributes( + agentContext, + w3cJsonLdVerifiableCredential, + anonCredsCredentialRecordOptions.schemaId + ) + } else { + w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) } const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) @@ -970,7 +993,10 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new AriesFrameworkError('Credential subject must be an object.') } - const claims = (credential.credentialSubject.claims ?? {}) as AnonCredsClaimRecord + const claims = { + ...credential.credentialSubject.claims, + ...(credential.credentialSubject.id && { id: credential.credentialSubject.id }), + } as AnonCredsClaimRecord const attributes = Object.entries(claims).map(([key, value]): CredentialPreviewAttributeOptions => { return { name: key, value: value.toString() } }) diff --git a/packages/anoncreds/src/utils/ledgerObjects.ts b/packages/anoncreds/src/utils/ledgerObjects.ts index ef05779b29..76c4377f3b 100644 --- a/packages/anoncreds/src/utils/ledgerObjects.ts +++ b/packages/anoncreds/src/utils/ledgerObjects.ts @@ -18,10 +18,20 @@ export type FetchLedgerObjectsInput = { } export type FetchLedgerObjectsReturn = { - credentialDefinitionReturn: T['credentialDefinitionId'] extends string ? GetCredentialDefinitionReturn : undefined - schemaReturn: T['schemaId'] extends string ? GetSchemaReturn : undefined + credentialDefinitionReturn: T['credentialDefinitionId'] extends string + ? GetCredentialDefinitionReturn + : T['credentialDefinitionId'] extends string | undefined + ? GetCredentialDefinitionReturn | undefined + : undefined + schemaReturn: T['schemaId'] extends string + ? GetSchemaReturn + : T['schemaId'] extends string | undefined + ? GetSchemaReturn | undefined + : undefined revocationRegistryDefinitionReturn: T['revocationRegistryId'] extends string ? GetRevocationRegistryDefinitionReturn + : T['revocationRegistryId'] extends string | undefined + ? GetRevocationRegistryDefinitionReturn | undefined : undefined } diff --git a/packages/core/package.json b/packages/core/package.json index 04813c5fdd..4a0306f9b8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,9 +33,6 @@ "@stablelib/ed25519": "^1.0.2", "@stablelib/random": "^1.0.1", "@stablelib/sha256": "^1.0.1", - "@sphereon/pex": "^2.2.2", - "@sphereon/pex-models": "^2.1.2", - "@sphereon/ssi-types": "^0.17.5", "@types/ws": "^8.5.4", "abort-controller": "^3.0.0", "big-integer": "^1.6.51", diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts index 04ee6199ba..1e820e39a4 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts @@ -77,5 +77,5 @@ export interface DidCommSignedAttachmentCredentialRequestOptions { } export interface DataIntegrityCredential { - credential: W3cVerifiableCredential + credential: JsonObject } diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index 1fca34b943..2d9c173d8c 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -29,7 +29,6 @@ export async function getCredentialsForRequest( const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) - // FIXME: there is a function for this in the VP library, but it is not usable atm const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { holderDIDs, // limitDisclosureSignatureSuites: [], From 879b9dd46b041d928458ba926330ee604891c2b5 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 23 Jan 2024 11:27:47 +0100 Subject: [PATCH 03/38] fix: lint issues --- .../src/formats/DataIntegrityCredentialFormatService.ts | 2 +- .../credentials/formats/dataIntegrity/dataIntegrityExchange.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index 323b07c183..9842d7bf19 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -77,7 +77,7 @@ import { } from '../repository' import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' -import { dateToTimestamp, fetchObjectsFromLedger, fetchQualifiedIds, legacyCredentialToW3cCredential } from '../utils' +import { dateToTimestamp, fetchObjectsFromLedger, legacyCredentialToW3cCredential } from '../utils' import { convertAttributesToCredentialValues, assertAttributesMatch as assertAttributesMatchSchema, diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts index 1e820e39a4..874b6d5b22 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts @@ -1,5 +1,4 @@ import type { JsonObject } from '../../../..' -import type { W3cVerifiableCredential } from '../../../vc' export type W3C_VC_DATA_MODEL_VERSION = '1.1' | '2.0' From 91da079356ffad5f15bad6d931de0a82fd2e6173 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 23 Jan 2024 15:02:27 +0100 Subject: [PATCH 04/38] feat: add cryptosuite tag to the credential record --- .../formats/DataIntegrityCredentialFormatService.ts | 12 +++++++++--- .../vc/data-integrity/models/LinkedDataProof.ts | 8 ++++++++ .../models/W3cJsonLdVerifiableCredential.ts | 7 +++++++ .../src/modules/vc/repository/W3cCredentialRecord.ts | 2 ++ .../repository/__tests__/W3cCredentialRecord.test.ts | 2 ++ 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index 9842d7bf19..deb884bc15 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -65,6 +65,7 @@ import { JwtPayload, SignatureSuiteRegistry, parseDid, + ConnectionRepository, } from '@aries-framework/core' import { W3cCredential as AW3cCredential } from '@hyperledger/anoncreds-shared' @@ -390,6 +391,12 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer } } else { // TODO: If the issuer is not included in the credential in the offer, the aud MUST be the same as the did of the recipient did of the DIDComm message containing the request message. + const connectionRepository = agentContext.dependencyManager.resolve(ConnectionRepository) + credentialRecord.connectionId // fetch connection -> get did prop + + const res = await connectionRepository.getByThreadId(agentContext, credentialRecord.threadId) + // ??? res.did + throw new AriesFrameworkError('Wrong issuer format in credential offer') } @@ -458,7 +465,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer // eslint-disable-next-line @typescript-eslint/no-unused-vars public async processRequest(agentContext: AgentContext, options: CredentialFormatProcessOptions): Promise { // not needed for dataIntegrity - // TODO: implement } private async createCredentialWithAnonCredsDataIntegrityProof( @@ -532,7 +538,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer revocationStatusList = revocationStatusListResult.revocationStatusList } - // TODO: bad abd bad + // TODO: const { credential } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer: { ...anonCredsLinkSecretBindingMethod, @@ -612,7 +618,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const issuerKid = dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions?.kid if (!issuerKid) throw new AriesFrameworkError('Missing kid') - // very bad + // TODO: for afj the offer is the issued credential const offeredCredential = JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential) const { aud, nonce } = await this.getSignedAttachmentPayload(agentContext, bindingProofAttachment) diff --git a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts index 9db48232aa..c8fd56137c 100644 --- a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts +++ b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts @@ -7,8 +7,11 @@ import { IsUri } from '../../../../utils' export interface LinkedDataProofOptions { type: string + // FIXME: cryptosuite is not optional when migrating to the new data integrity specification + cryptosuite?: string proofPurpose: string verificationMethod: string + // FIXME: created is optional when migrating to the new data integrity specification created: string domain?: string challenge?: string @@ -27,6 +30,7 @@ export class LinkedDataProof { public constructor(options: LinkedDataProofOptions) { if (options) { this.type = options.type + this.cryptosuite = options.cryptosuite this.proofPurpose = options.proofPurpose this.verificationMethod = options.verificationMethod this.created = options.created @@ -41,6 +45,10 @@ export class LinkedDataProof { @IsString() public type!: string + @IsString() + @IsOptional() + public cryptosuite: string | undefined + @IsString() public proofPurpose!: string diff --git a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts index 2fad970565..c8515e9c29 100644 --- a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts +++ b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts @@ -37,6 +37,13 @@ export class W3cJsonLdVerifiableCredential extends W3cCredential { return proofArray.map((proof) => proof.type) } + public get cryptoSuites(): Array { + const proofArray = asArray(this.proof) ?? [] + return proofArray + .map((proof) => proof.cryptosuite) + .filter((cryptosuite): cryptosuite is string => typeof cryptosuite === 'string') + } + public toJson() { return JsonTransformer.toJSON(this) } diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts index 42a710d198..43e4171db7 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts @@ -47,6 +47,7 @@ export type DefaultW3cCredentialTags = { claimFormat: W3cVerifiableCredential['claimFormat'] proofTypes?: Array + cryptosuites?: Array algs?: Array } @@ -142,6 +143,7 @@ export class W3cCredentialRecord extends BaseRecord< // Proof types is used for ldp_vc credentials if (this.credential.claimFormat === ClaimFormat.LdpVc) { tags.proofTypes = this.credential.proofTypes + tags.cryptosuites = this.credential.cryptoSuites } // Algs is used for jwt_vc credentials diff --git a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts index 3f0da9de21..d460f6e04f 100644 --- a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts +++ b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts @@ -25,6 +25,7 @@ describe('W3cCredentialRecord', () => { claimFormat: 'ldp_vc', issuerId: credential.issuerId, subjectIds: credential.credentialSubjectIds, + cryptosuites: [], schemaIds: credential.credentialSchemaIds, contexts: credential.contexts, proofTypes: credential.proofTypes, @@ -98,6 +99,7 @@ describe('W3cCredentialRecord', () => { contexts: credential.contexts, proofTypes: credential.proofTypes, givenId: credential.id, + cryptosuites: [], expandedTypes: ['https://expanded.tag#1'], ...anoncredsCredentialTags, }) From 32432a84951c062d3e553a074755e89998e8cabf Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 24 Jan 2024 09:53:54 +0100 Subject: [PATCH 05/38] fix: TODOS in credential format service --- .../tests/data-integrity-flow-w3c.test.ts | 10 +- .../tests/data-integrity-flow.test.ts | 284 ++++++++++++++++++ .../DataIntegrityCredentialFormatService.ts | 260 +++++++++------- .../DataIntegrityCredentialFormat.ts | 1 + 4 files changed, 444 insertions(+), 111 deletions(-) create mode 100644 packages/anoncreds-rs/tests/data-integrity-flow.test.ts diff --git a/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts index 54dfa49e89..f79e780fa5 100644 --- a/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts +++ b/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts @@ -82,7 +82,6 @@ const agentContext = getAgentContext({ [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], [InjectionSymbols.Logger, logger], - [InjectionSymbols.Logger, logger], [DidsModuleConfig, didsModuleConfig], [DidResolverService, new DidResolverService(logger, didsModuleConfig)], [AnonCredsRegistryService, new AnonCredsRegistryService()], @@ -239,6 +238,7 @@ async function anonCredsFlowTest(options: { requestAppendAttachments, credentialFormats: { dataIntegrity: { + credentialSubjectId: issuer.did, didCommSignedAttachmentAcceptRequestOptions: { kid: issuer.kid, }, @@ -280,7 +280,13 @@ async function anonCredsFlowTest(options: { expect(credentialId).toBeUndefined() expect(credentialRecord.credential).toEqual({ - ...credential, + ...{ + ...credential, + credentialSubject: new W3cCredentialSubject({ + id: issuer.did, + claims: (credential.credentialSubject as W3cCredentialSubject).claims, + }), + }, proof: expect.any(Object), }) } diff --git a/packages/anoncreds-rs/tests/data-integrity-flow.test.ts b/packages/anoncreds-rs/tests/data-integrity-flow.test.ts new file mode 100644 index 0000000000..856ceb9a19 --- /dev/null +++ b/packages/anoncreds-rs/tests/data-integrity-flow.test.ts @@ -0,0 +1,284 @@ +import type { KeyDidCreateOptions } from '@aries-framework/core' + +import { + AnonCredsHolderServiceSymbol, + AnonCredsIssuerServiceSymbol, + AnonCredsModuleConfig, + AnonCredsVerifierServiceSymbol, +} from '@aries-framework/anoncreds' +import { + AgentContext, + CredentialExchangeRecord, + CredentialPreviewAttribute, + CredentialState, + DidKey, + DidResolverService, + DidsApi, + DidsModuleConfig, + Ed25519Signature2018, + InjectionSymbols, + KeyDidRegistrar, + KeyDidResolver, + KeyType, + SignatureSuiteToken, + SigningProviderRegistry, + TypedArrayEncoder, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cCredentialsModuleConfig, +} from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' +import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { AnonCredsRsHolderService } from '../src/services/AnonCredsRsHolderService' +import { AnonCredsRsIssuerService } from '../src/services/AnonCredsRsIssuerService' +import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierService' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' + +const registry = new InMemoryAnonCredsRegistry() +const tailsFileService = new InMemoryTailsFileService() +const anonCredsModuleConfig = new AnonCredsModuleConfig({ + registries: [registry], + tailsFileService, +}) + +const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() + +const inMemoryStorageService = new InMemoryStorageService() + +const logger = agentConfig.logger + +const didsModuleConfig = new DidsModuleConfig({ + registrars: [new KeyDidRegistrar()], + resolvers: [new KeyDidResolver()], +}) +const fileSystem = new agentDependencies.FileSystem() + +const wallet = new RegisteredAskarTestWallet( + agentConfig.logger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([]) +) + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, fileSystem], + [InjectionSymbols.StorageService, inMemoryStorageService], + [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], + [AnonCredsHolderServiceSymbol, anonCredsHolderService], + [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [InjectionSymbols.Logger, logger], + [DidsModuleConfig, didsModuleConfig], + [DidResolverService, new DidResolverService(logger, didsModuleConfig)], + [AnonCredsRegistryService, new AnonCredsRegistryService()], + [AnonCredsModuleConfig, anonCredsModuleConfig], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], + ], + agentConfig, + wallet, +}) + +agentContext.dependencyManager.registerInstance(AgentContext, agentContext) + +const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() + +const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + +describe('data integrity format service (w3c)', () => { + let issuer: Awaited> + let holder: Awaited> + + beforeAll(async () => { + await wallet.createAndOpen(agentConfig.walletConfig) + + issuer = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') + holder = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') + }) + + afterEach(async () => { + inMemoryStorageService.records = {} + }) + + test('issuance and verification flow w3c starting from offer without negotiation and without revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: false, issuer, holder }) + }) +}) + +export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey: string) { + const dids = agentContext.dependencyManager.resolve(DidsApi) + const didCreateResult = await dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString(secretKey) }, + }) + + const did = didCreateResult.didState.did as string + const didKey = DidKey.fromDid(did) + const kid = `${did}#${didKey.key.fingerprint}` + + const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + return { + did, + kid, + verificationMethod, + } +} + +async function anonCredsFlowTest(options: { + issuerId: string + revocable: boolean + issuer: Awaited> + holder: Awaited> +}) { + const { issuer } = options + + 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' }), + ] + + // Set attributes on the credential record, this is normally done by the protocol service + holderCredentialRecord.credentialAttributes = credentialAttributes + issuerCredentialRecord.credentialAttributes = credentialAttributes + + // -------------------------------------------------------------------------------------------------------- + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuer.did, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: '25' } }), + }) + + const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { + credentialRecord: issuerCredentialRecord, + credentialFormats: { + dataIntegrity: { + bindingRequired: false, + credential, + }, + }, + }) + + // Holder processes and accepts offer + await dataIntegrityCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment, appendAttachments: requestAppendAttachments } = + await dataIntegrityCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + dataIntegrity: {}, + }, + }) + + // Issuer processes and accepts request + await dataIntegrityCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await dataIntegrityCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + requestAppendAttachments, + credentialFormats: { + dataIntegrity: { + credentialSubjectId: issuer.did, + }, + }, + }) + + // Holder processes and accepts credential + await dataIntegrityCredentialFormatService.processCredential(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) + + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + await expect( + anonCredsHolderService.getCredential(agentContext, { + credentialId: holderCredentialRecord.id, + }) + ).rejects.toThrow() + + const expectedCredentialMetadata = {} + expect(holderCredentialRecord.metadata.data).toEqual({ + '_dataIntegrity/credential': expectedCredentialMetadata, + '_dataIntegrity/credentialRequest': {}, + }) + + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_dataIntegrity/credential': expectedCredentialMetadata, + }) + + const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) + const credentialId = credentialRecord.getAnonCredsTags()?.credentialId + expect(credentialId).toBeUndefined() + + expect(credentialRecord.credential).toEqual({ + ...{ + ...credential, + credentialSubject: new W3cCredentialSubject({ + id: issuer.did, + claims: (credential.credentialSubject as W3cCredentialSubject).claims, + }), + }, + proof: expect.any(Object), + }) +} diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index deb884bc15..b40e39d641 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -39,6 +39,7 @@ import type { AnonCredsCredentialRecordOptions, DataIntegrityLinkSecretRequestMetadata, DataIntegrityLinkSecretMetadata, + VerificationMethod, } from '@aries-framework/core' import { @@ -64,8 +65,7 @@ import { ClaimFormat, JwtPayload, SignatureSuiteRegistry, - parseDid, - ConnectionRepository, + CredentialPreviewAttribute, } from '@aries-framework/core' import { W3cCredential as AW3cCredential } from '@hyperledger/anoncreds-shared' @@ -168,21 +168,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer return { format, attachment, previewAttributes } } - private enhanceCredentialOffer(credential: JsonObject, version: W3C_VC_DATA_MODEL_VERSION) { - // these modification ensure that the credential is valid - if (!credential.issuer) credential.issuer = 'https://example.issuer.com' - - if (version === '1.1') { - if (!credential.issuanceDate) credential.issuanceDate = new Date().toISOString() - } else if (version === '2.0') { - // do nothing - } else { - throw new AriesFrameworkError(`Unsupported data model version: ${version}`) - } - - return credential - } - public async processOffer( agentContext: AgentContext, { attachment, credentialRecord }: CredentialFormatProcessOptions @@ -195,8 +180,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer attachment.getDataAsJson() // validate the credential - const credentialToBeValidated = this.enhanceCredentialOffer(credential, data_model_versions_supported[0]) - JsonTransformer.fromJSON(credentialToBeValidated, W3cCredential) + JsonTransformer.fromJSON(credential, W3cCredential) const missingBindingMethod = binding_required && !binding_method?.anoncreds_link_secret && !binding_method?.didcomm_signed_attachment @@ -232,7 +216,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer private async createSignedAttachment( agentContext: AgentContext, - data: { aud: string; nonce: string }, + data: { nonce: string }, options: { alg?: string; kid: string }, issuerSupportedAlgs: string[] ) { @@ -265,7 +249,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const jws = await jwsService.createJws(agentContext, { key, header: {}, - payload: new JwtPayload({ aud: data.aud, additionalClaims: { nonce: data.nonce } }), + payload: new JwtPayload({ additionalClaims: { nonce: data.nonce } }), protectedHeaderOptions: { alg: signingAlg, kid }, }) @@ -306,8 +290,10 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer }) if (!isValid) throw new AriesFrameworkError('Failed to validate signature of signed attachment') - const payload = JsonEncoder.fromBase64(signedAttachment.data.base64) as { aud: string; nonce: string } - if (!payload.aud || !payload.nonce) throw new AriesFrameworkError('Invalid payload in signed attachment') + const payload = JsonEncoder.fromBase64(signedAttachment.data.base64) as { nonce: string } + if (!payload.nonce || typeof payload.nonce !== 'string') { + throw new AriesFrameworkError('Invalid payload in signed attachment') + } return payload } @@ -378,36 +364,9 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new AriesFrameworkError('Cannot request credential with a binding method that was not offered.') } - const offeredCredential = credentialOffer.credential - - let aud: string - if (offeredCredential?.issuer) { - if (typeof offeredCredential.issuer === 'string') aud = offeredCredential.issuer - else if (Array.isArray(offeredCredential.issuer)) throw new AriesFrameworkError('Issuer cannot be an array') - else if (typeof offeredCredential.issuer === 'object' && typeof offeredCredential.issuer.id === 'string') - aud = offeredCredential.issuer.id - else { - throw new AriesFrameworkError('Wrong issuer format in credential offer') - } - } else { - // TODO: If the issuer is not included in the credential in the offer, the aud MUST be the same as the did of the recipient did of the DIDComm message containing the request message. - const connectionRepository = agentContext.dependencyManager.resolve(ConnectionRepository) - credentialRecord.connectionId // fetch connection -> get did prop - - const res = await connectionRepository.getByThreadId(agentContext, credentialRecord.threadId) - // ??? res.did - - throw new AriesFrameworkError('Wrong issuer format in credential offer') - } - - const holderDidMethod = parseDid(aud).method - if (!credentialOffer.binding_method.didcomm_signed_attachment.did_methods_supported.includes(holderDidMethod)) { - throw new AriesFrameworkError(`Holder did method ${holderDidMethod} not supported by the issuer`) - } - didCommSignedAttachment = await this.createSignedAttachment( agentContext, - { aud, nonce: credentialOffer.binding_method.didcomm_signed_attachment.nonce }, + { nonce: credentialOffer.binding_method.didcomm_signed_attachment.nonce }, dataIntegrityFormat.didCommSignedAttachmentCredentialRequestOptions, credentialOffer.binding_method.didcomm_signed_attachment.algs_supported ) @@ -474,10 +433,16 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer anonCredsLinkSecretBindingMethod: AnonCredsLinkSecretBindingMethod anonCredsLinkSecretBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof linkSecretMetadata: DataIntegrityLinkSecretMetadata + credentialSubjectId?: string } ): Promise { - const { credentialRecord, anonCredsLinkSecretBindingMethod, anonCredsLinkSecretBindingProof, linkSecretMetadata } = - input + const { + credentialRecord, + anonCredsLinkSecretBindingMethod, + anonCredsLinkSecretBindingProof, + linkSecretMetadata, + credentialSubjectId, + } = input const credentialAttributes = credentialRecord.credentialAttributes if (!credentialAttributes) { @@ -486,6 +451,10 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ) } + if (credentialSubjectId && credentialAttributes.find((ca) => ca.name === 'id') === undefined) { + credentialAttributes.push(new CredentialPreviewAttribute({ name: 'id', value: credentialSubjectId })) + } + const anonCredsIssuerService = agentContext.dependencyManager.resolve(AnonCredsIssuerServiceSymbol) @@ -538,7 +507,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer revocationStatusList = revocationStatusListResult.revocationStatusList } - // TODO: const { credential } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer: { ...anonCredsLinkSecretBindingMethod, @@ -554,6 +522,91 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer return await legacyCredentialToW3cCredential(agentContext, credential) } + private async getSignatureMetadata(agentContext: AgentContext, offeredCredential: W3cCredential, issuerKid?: string) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(offeredCredential.issuerId) + + let verificationMethod: VerificationMethod + if (issuerKid) { + verificationMethod = didDocument.dereferenceKey(issuerKid, ['authentication', 'assertionMethod']) + } else { + const vms = didDocument.authentication ?? didDocument.assertionMethod + if (!vms || vms.length === 0) { + throw new AriesFrameworkError('Missing authentication or assertionMethod in did document') + } + + if (typeof vms[0] === 'string') { + verificationMethod = didDocument.dereferenceVerificationMethod(vms[0]) + } else { + verificationMethod = vms[0] + } + } + + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + const signatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!signatureSuite) { + throw new AriesFrameworkError( + `Could not find signature suite for verification method type ${verificationMethod.type}` + ) + } + + return { verificationMethod, signatureSuite, offeredCredential } + } + + private async assertAndSetCredentialSubjectId(credential: W3cCredential, credentialSubjectId: string | undefined) { + if (credentialSubjectId) { + if (Array.isArray(credential.credentialSubject)) { + throw new AriesFrameworkError( + 'Invalid credential subject relation. Cannot determine the subject to be updated.' + ) + } + + const subjectId = credential.credentialSubject.id + if (subjectId && credentialSubjectId !== subjectId) { + throw new AriesFrameworkError('Invalid credential subject id.') + } + + if (!subjectId) { + credential.credentialSubject.id = credentialSubjectId + } + } + + return credential + } + + private async signCredential( + agentContext: AgentContext, + credential: W3cCredential | W3cJsonLdVerifiableCredential, + issuerKid?: string + ) { + const { signatureSuite, verificationMethod } = await this.getSignatureMetadata(agentContext, credential, issuerKid) + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + let credentialToBeSigned = credential + if (credential instanceof W3cJsonLdVerifiableCredential) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { proof, ..._credentialToBeSigned } = credential + credentialToBeSigned = _credentialToBeSigned as W3cCredential + } + + const signed = (await w3cCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential: credentialToBeSigned as W3cCredential, + proofType: signatureSuite.proofType, + verificationMethod: verificationMethod.id, + })) as W3cJsonLdVerifiableCredential + + if (Array.isArray(signed.proof)) + throw new AriesFrameworkError('A newly signed credential can not have multiple proofs') + + if (credential instanceof W3cJsonLdVerifiableCredential) { + const combinedProofs = Array.isArray(credential.proof) ? credential.proof : [credential.proof] + combinedProofs.push(signed.proof) + signed.proof = combinedProofs + } + return signed + } + public async acceptRequest( agentContext: AgentContext, { @@ -565,20 +618,18 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer requestAppendAttachments, }: CredentialFormatAcceptRequestOptions ): Promise { - // Assert credential attributes - const credentialAttributes = credentialRecord.credentialAttributes - if (!credentialAttributes) { - throw new AriesFrameworkError( - `Missing required credential attribute values on credential record with id ${credentialRecord.id}` - ) - } - const dataIntegrityFormat = credentialFormats?.dataIntegrity if (!dataIntegrityFormat) throw new AriesFrameworkError('Missing data integrity credential format data') const credentialOffer = offerAttachment?.getDataAsJson() if (!credentialOffer) throw new AriesFrameworkError('Missing data integrity credential offer in createCredential') + const offeredCredential = JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential) + const assertedCredential = await this.assertAndSetCredentialSubjectId( + offeredCredential, + dataIntegrityFormat.credentialSubjectId + ) + const credentialRequest = requestAttachment.getDataAsJson() if (!credentialRequest) throw new AriesFrameworkError('Missing data integrity credential request in createCredential') @@ -587,7 +638,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer if (!dataIntegrityMetadata) throw new AriesFrameworkError('Missing data integrity credential metadata in createCredential') - let credential: W3cJsonLdVerifiableCredential | undefined + let signedCredential: W3cJsonLdVerifiableCredential | undefined if (credentialRequest.binding_proof?.anoncreds_link_secret) { if (!credentialOffer.binding_method?.anoncreds_link_secret) { throw new AriesFrameworkError('Cannot issue credential with a binding method that was not offered.') @@ -597,12 +648,24 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new AriesFrameworkError('Missing anoncreds link secret metadata') } - credential = await this.createCredentialWithAnonCredsDataIntegrityProof(agentContext, { + signedCredential = await this.createCredentialWithAnonCredsDataIntegrityProof(agentContext, { credentialRecord, anonCredsLinkSecretBindingMethod: credentialOffer.binding_method.anoncreds_link_secret, linkSecretMetadata: dataIntegrityMetadata.linkSecretMetadata, anonCredsLinkSecretBindingProof: credentialRequest.binding_proof.anoncreds_link_secret, + credentialSubjectId: dataIntegrityFormat.credentialSubjectId, }) + + if ( + dataIntegrityFormat.credentialSubjectId && + Array.isArray(signedCredential.credentialSubject) === false && + signedCredential.credentialSubject.id && + dataIntegrityFormat.credentialSubjectId !== signedCredential.credentialSubject.id + ) { + throw new AriesFrameworkError('Invalid credential subject id.') + } + + // TODO: check if any non integrity protected fields were on the offered credential. If so throw } if (credentialRequest.binding_proof?.didcomm_signed_attachment) { @@ -610,58 +673,29 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new AriesFrameworkError('Cannot issue credential with a binding method that was not offered.') } + if (!dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions) { + throw new AriesFrameworkError('Missing didCommSignedAttachmentAcceptRequestOptions') + } + const bindingProofAttachment = requestAppendAttachments?.find( (attachments) => attachments.id === credentialRequest.binding_proof?.didcomm_signed_attachment?.attachment_id ) if (!bindingProofAttachment) throw new AriesFrameworkError('Missing binding proof attachment') - const issuerKid = dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions?.kid - if (!issuerKid) throw new AriesFrameworkError('Missing kid') - - // TODO: for afj the offer is the issued credential - const offeredCredential = JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential) - - const { aud, nonce } = await this.getSignedAttachmentPayload(agentContext, bindingProofAttachment) + const { nonce } = await this.getSignedAttachmentPayload(agentContext, bindingProofAttachment) if (nonce !== credentialOffer.binding_method.didcomm_signed_attachment.nonce) { throw new AriesFrameworkError('Invalid nonce in signed attachment') } - const issuer = - typeof offeredCredential.issuer === 'string' ? offeredCredential.issuer : offeredCredential.issuer.id - if (issuer !== aud) throw new AriesFrameworkError('Invalid aud in signed attachment') - - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didDocument = await didsApi.resolveDidDocument(issuer) - const verificationMethod = didDocument.dereferenceVerificationMethod(issuerKid) - - const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - const signatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) - if (!signatureSuite) { - throw new AriesFrameworkError( - `Could not find signature suite for verification method type ${verificationMethod.type}` - ) - } - - if (credential) { - //TODO: in this case we already have a credential, so we can use that and just add another signature - throw new AriesFrameworkError('TODO: implement and remove this!') - } else { - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - credential = (await w3cCredentialService.signCredential(agentContext, { - format: ClaimFormat.LdpVc, - credential: offeredCredential, - proofType: signatureSuite.proofType, - verificationMethod: verificationMethod.id, - })) as W3cJsonLdVerifiableCredential - } + const issuerKid = dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions.kid + signedCredential = await this.signCredential(agentContext, assertedCredential, issuerKid) } if ( !credentialRequest.binding_proof?.anoncreds_link_secret && !credentialRequest.binding_proof?.didcomm_signed_attachment ) { - // TODO: sign with an arbitrary cryptosuite, but cannot be anoncreds .... - throw new AriesFrameworkError('Not impelmented') + signedCredential = await this.signCredential(agentContext, assertedCredential) } const format = new CredentialFormatSpec({ @@ -669,7 +703,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer format: W3C_DATA_INTEGRITY_CREDENTIAL, }) - const attachment = this.getFormatData({ credential: JsonTransformer.toJSON(credential) }, format.attachmentId) + const attachment = this.getFormatData({ credential: JsonTransformer.toJSON(signedCredential) }, format.attachmentId) return { format, attachment } } @@ -796,7 +830,8 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer await this.assertCredentialAttributesMatchSchemaAttributes( agentContext, w3cJsonLdVerifiableCredential, - anonCredsCredentialRecordOptions.schemaId + anonCredsCredentialRecordOptions.schemaId, + true ) } else { w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) @@ -902,8 +937,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer // validate the credential and get the preview attributes const credentialJson = credential instanceof W3cCredential ? JsonTransformer.toJSON(credential) : credential - const validCredential = this.enhanceCredentialOffer(credentialJson, dataModelVersionsSupported[0]) - const validW3cCredential = JsonTransformer.fromJSON(validCredential, W3cCredential) + const validW3cCredential = JsonTransformer.fromJSON(credentialJson, W3cCredential) const previewAttributes = this.previewAttributesFromCredential(validW3cCredential) const dataIntegrityMetadata: DataIntegrityMetadata = {} @@ -942,7 +976,8 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer await this.assertCredentialAttributesMatchSchemaAttributes( agentContext, validW3cCredential, - credentialDefinition.schemaId + credentialDefinition.schemaId, + false ) const { schema_id, ..._anonCredsLinkSecretBindingMethod } = anoncredsCredentialOffer @@ -969,7 +1004,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new AriesFrameworkError('No supported JWA signature algorithms found.') } - // TODO: this can be empty according to spec if (didCommSignedAttachmentBindingMethod.did_methods_supported.length === 0) { throw new AriesFrameworkError('No supported DID methods found.') } @@ -1012,7 +1046,8 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer private async assertCredentialAttributesMatchSchemaAttributes( agentContext: AgentContext, credential: W3cCredential, - schemaId: string + schemaId: string, + credentialSubjectIdMustBeSet: boolean ) { const attributes = this.previewAttributesFromCredential(credential) @@ -1023,7 +1058,14 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ) } - assertAttributesMatchSchema(schemaReturn.schema, attributes) + const enhancedAttributes = [...attributes] + if ( + !credentialSubjectIdMustBeSet && + schemaReturn.schema.attrNames.includes('id') && + attributes.find((attr) => attr.name === 'id') === undefined + ) + enhancedAttributes.push({ name: 'id', value: 'mock' }) + assertAttributesMatchSchema(schemaReturn.schema, enhancedAttributes) return { attributes } } diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts index e9d8b052c2..90a1071a26 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts @@ -46,6 +46,7 @@ export interface DataIntegrityOfferCredentialFormat { * method, so it's an empty object */ export type DataIntegrityAcceptRequestFormat = { + credentialSubjectId?: string didCommSignedAttachmentAcceptRequestOptions?: { kid: string } From 9fb93f5bf0c7e2bf33186e15b4f1fab0038225cf Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 30 Jan 2024 16:18:13 +0100 Subject: [PATCH 06/38] feat: presentation support --- demo/package.json | 2 +- demo/src/Alice.ts | 62 +- demo/src/BaseAgent.ts | 79 +-- demo/src/Faber.ts | 166 +++-- packages/anoncreds-rs/package.json | 8 +- .../anoncreds-rs/src/AnonCredsRsModule.ts | 4 +- .../src/AnonCredsVcDataIntegrityService.ts | 641 ++++++++++++++++++ packages/anoncreds-rs/tests/anoncredsSetup.ts | 8 +- .../tests/data-integrity.e2e.test.ts | 333 +++++++++ .../tests/fixtures/presentation-definition.ts | 54 ++ packages/anoncreds/package.json | 6 +- .../formats/AnonCredsProofFormatService.ts | 32 +- .../DataIntegrityCredentialFormatService.ts | 34 +- .../AnonCredsVcDataIntegrityService.ts | 31 + .../formats/dataIntegrity/index.ts | 1 + .../DifPresentationExchangeService.ts | 143 +++- .../models/DifPexCredentialsForRequest.ts | 2 +- .../utils/credentialSelection.ts | 2 +- ...fPresentationExchangeProofFormatService.ts | 67 +- .../core/src/modules/vc/models/ClaimFormat.ts | 3 + yarn.lock | 18 +- 21 files changed, 1481 insertions(+), 215 deletions(-) create mode 100644 packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts create mode 100644 packages/anoncreds-rs/tests/data-integrity.e2e.test.ts create mode 100644 packages/anoncreds-rs/tests/fixtures/presentation-definition.ts create mode 100644 packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService.ts diff --git a/demo/package.json b/demo/package.json index b0179fb1d2..453414bc46 100644 --- a/demo/package.json +++ b/demo/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.7", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.8", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "inquirer": "^8.2.5" }, diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts index 2de378d8c1..56328c57b7 100644 --- a/demo/src/Alice.ts +++ b/demo/src/Alice.ts @@ -1,20 +1,52 @@ -import type { ConnectionRecord, CredentialExchangeRecord, ProofExchangeRecord } from '@aries-framework/core' +import type { + KeyDidCreateOptions, + ConnectionRecord, + CredentialExchangeRecord, + ProofExchangeRecord, + CredentialStateChangedEvent, +} from '@aries-framework/core' +import type BottomBar from 'inquirer/lib/ui/bottom-bar' + +import { + AutoAcceptCredential, + CredentialEventTypes, + CredentialState, + DidsApi, + KeyType, + TypedArrayEncoder, +} from '@aries-framework/core' +import { randomInt } from 'crypto' +import { ui } from 'inquirer' import { BaseAgent } from './BaseAgent' -import { greenText, Output, redText } from './OutputClass' +import { Color, greenText, Output, redText } from './OutputClass' export class Alice extends BaseAgent { public connected: boolean public connectionRecordFaberId?: string + public did?: string + public ui: BottomBar public constructor(port: number, name: string) { super({ port, name, useLegacyIndySdk: true }) this.connected = false + this.ui = new ui.BottomBar() } public static async build(): Promise { - const alice = new Alice(9000, 'alice') + const alice = new Alice(9000, 'alice' + randomInt(100000)) await alice.initializeAgent() + + await alice.agent.modules.anoncreds.createLinkSecret({ linkSecretId: 'linkSecretId' }) + + const dids = alice.agent.context.dependencyManager.resolve(DidsApi) + const didCreateResult = await dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, + }) + + if (!didCreateResult.didState.did) throw new Error('failed to created did') return alice } @@ -47,7 +79,26 @@ export class Alice extends BaseAgent { public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { await this.agent.credentials.acceptOffer({ + autoAcceptCredential: AutoAcceptCredential.Never, credentialRecordId: credentialRecord.id, + credentialFormats: { + dataIntegrity: { + anonCredsLinkSecretCredentialRequestOptions: { + linkSecretId: 'linkSecretId', + }, + }, + }, + }) + + this.agent.events.on(CredentialEventTypes.CredentialStateChanged, async (afjEvent) => { + const credentialRecord = afjEvent.payload.credentialRecord + + if (afjEvent.payload.credentialRecord.state !== CredentialState.CredentialReceived) return + + console.log(`\nReceived Credential. Processing and storing it!\n\n${Color.Reset}`) + await this.agent.credentials.acceptCredential({ + credentialRecordId: credentialRecord.id, + }) }) } @@ -56,6 +107,11 @@ export class Alice extends BaseAgent { proofRecordId: proofRecord.id, }) + const selectedCredentials = requestedCredentials.proofFormats.presentationExchange?.credentials + if (!selectedCredentials) { + throw new Error('No credentials found for presentation exchange') + } + await this.agent.proofs.acceptRequest({ proofRecordId: proofRecord.id, proofFormats: requestedCredentials.proofFormats, diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts index c2e787e32a..2b2624d956 100644 --- a/demo/src/BaseAgent.ts +++ b/demo/src/BaseAgent.ts @@ -2,15 +2,7 @@ import type { InitConfig } from '@aries-framework/core' import type { IndySdkPoolConfig } from '@aries-framework/indy-sdk' import type { IndyVdrPoolConfig } from '@aries-framework/indy-vdr' -import { - AnonCredsCredentialFormatService, - AnonCredsModule, - AnonCredsProofFormatService, - LegacyIndyCredentialFormatService, - LegacyIndyProofFormatService, - V1CredentialProtocol, - V1ProofProtocol, -} from '@aries-framework/anoncreds' +import { AnonCredsModule, DataIntegrityCredentialFormatService } from '@aries-framework/anoncreds' import { AnonCredsRsModule } from '@aries-framework/anoncreds-rs' import { AskarModule } from '@aries-framework/askar' import { @@ -31,15 +23,16 @@ import { CredentialsModule, Agent, HttpOutboundTransport, + PresentationExchangeProofFormatService, + KeyDidResolver, + KeyDidRegistrar, } from '@aries-framework/core' -import { IndySdkAnonCredsRegistry, IndySdkModule, IndySdkSovDidResolver } from '@aries-framework/indy-sdk' import { IndyVdrIndyDidResolver, IndyVdrAnonCredsRegistry, IndyVdrModule } from '@aries-framework/indy-vdr' import { agentDependencies, HttpInboundTransport } from '@aries-framework/node' import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' import { randomUUID } from 'crypto' -import indySdk from 'indy-sdk' import { greenText } from './OutputClass' @@ -108,32 +101,23 @@ export class BaseAgent { } function getAskarAnonCredsIndyModules() { - const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() - const legacyIndyProofFormatService = new LegacyIndyProofFormatService() - return { connections: new ConnectionsModule({ autoAcceptConnections: true, }), credentials: new CredentialsModule({ - autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + autoAcceptCredentials: AutoAcceptCredential.Never, credentialProtocols: [ - new V1CredentialProtocol({ - indyCredentialFormat: legacyIndyCredentialFormatService, - }), new V2CredentialProtocol({ - credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()], + credentialFormats: [new DataIntegrityCredentialFormatService()], }), ], }), proofs: new ProofsModule({ - autoAcceptProofs: AutoAcceptProof.ContentApproved, + autoAcceptProofs: AutoAcceptProof.Never, proofProtocols: [ - new V1ProofProtocol({ - indyProofFormat: legacyIndyProofFormatService, - }), new V2ProofProtocol({ - proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService()], + proofFormats: [new PresentationExchangeProofFormatService()], }), ], }), @@ -159,54 +143,11 @@ function getAskarAnonCredsIndyModules() { }) ), dids: new DidsModule({ - resolvers: [new IndyVdrIndyDidResolver(), new CheqdDidResolver()], - registrars: [new CheqdDidRegistrar()], + resolvers: [new IndyVdrIndyDidResolver(), new CheqdDidResolver(), new KeyDidResolver()], + registrars: [new CheqdDidRegistrar(), new KeyDidRegistrar()], }), askar: new AskarModule({ ariesAskar, }), } as const } - -function getLegacyIndySdkModules() { - const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() - const legacyIndyProofFormatService = new LegacyIndyProofFormatService() - - return { - connections: new ConnectionsModule({ - autoAcceptConnections: true, - }), - credentials: new CredentialsModule({ - autoAcceptCredentials: AutoAcceptCredential.ContentApproved, - credentialProtocols: [ - new V1CredentialProtocol({ - indyCredentialFormat: legacyIndyCredentialFormatService, - }), - new V2CredentialProtocol({ - credentialFormats: [legacyIndyCredentialFormatService], - }), - ], - }), - proofs: new ProofsModule({ - autoAcceptProofs: AutoAcceptProof.ContentApproved, - proofProtocols: [ - new V1ProofProtocol({ - indyProofFormat: legacyIndyProofFormatService, - }), - new V2ProofProtocol({ - proofFormats: [legacyIndyProofFormatService], - }), - ], - }), - anoncreds: new AnonCredsModule({ - registries: [new IndySdkAnonCredsRegistry()], - }), - indySdk: new IndySdkModule({ - indySdk, - networks: [indyNetworkConfig], - }), - dids: new DidsModule({ - resolvers: [new IndySdkSovDidResolver()], - }), - } as const -} diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index abf44c67dc..e4300e2e5a 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -1,12 +1,29 @@ import type { RegisterCredentialDefinitionReturnStateFinished } from '@aries-framework/anoncreds' -import type { ConnectionRecord, ConnectionStateChangedEvent } from '@aries-framework/core' +import type { + ConnectionRecord, + ConnectionStateChangedEvent, + CredentialStateChangedEvent, + ProofStateChangedEvent, +} from '@aries-framework/core' import type { IndyVdrRegisterSchemaOptions, IndyVdrRegisterCredentialDefinitionOptions, } from '@aries-framework/indy-vdr' import type BottomBar from 'inquirer/lib/ui/bottom-bar' -import { ConnectionEventTypes, KeyType, TypedArrayEncoder, utils } from '@aries-framework/core' +import { + CredentialState, + ConnectionEventTypes, + CredentialEventTypes, + KeyType, + TypedArrayEncoder, + W3cCredential, + W3cCredentialSubject, + utils, + ProofEventTypes, + ProofState, +} from '@aries-framework/core' +import { randomInt } from 'crypto' import { ui } from 'inquirer' import { BaseAgent, indyNetworkConfig } from './BaseAgent' @@ -20,7 +37,7 @@ export enum RegistryOptions { export class Faber extends BaseAgent { public outOfBandId?: string public credentialDefinition?: RegisterCredentialDefinitionReturnStateFinished - public anonCredsIssuerId?: string + public anonCredsIssuerId!: string public ui: BottomBar public constructor(port: number, name: string) { @@ -29,8 +46,9 @@ export class Faber extends BaseAgent { } public static async build(): Promise { - const faber = new Faber(9001, 'faber') + const faber = new Faber(9001, 'faber' + randomInt(10000)) await faber.initializeAgent() + return faber } @@ -130,7 +148,9 @@ export class Faber extends BaseAgent { console.log(`\n\nThe credential definition will look like this:\n`) console.log(purpleText(`Name: ${Color.Reset}${name}`)) console.log(purpleText(`Version: ${Color.Reset}${version}`)) - console.log(purpleText(`Attributes: ${Color.Reset}${attributes[0]}, ${attributes[1]}, ${attributes[2]}\n`)) + console.log( + purpleText(`Attributes: ${Color.Reset}${attributes[0]}, ${attributes[1]}, ${attributes[2]}, ${attributes[3]}\n`) + ) } private async registerSchema() { @@ -140,7 +160,7 @@ export class Faber extends BaseAgent { const schemaTemplate = { name: 'Faber College' + utils.uuid(), version: '1.0.0', - attrNames: ['name', 'degree', 'date'], + attrNames: ['id', 'name', 'height', 'age'], issuerId: this.anonCredsIssuerId, } this.printSchema(schemaTemplate.name, schemaTemplate.version, schemaTemplate.attrNames) @@ -207,28 +227,48 @@ export class Faber extends BaseAgent { connectionId: connectionRecord.id, protocolVersion: 'v2', credentialFormats: { - anoncreds: { - attributes: [ - { - name: 'name', - value: 'Alice Smith', - }, - { - name: 'degree', - value: 'Computer Science', - }, - { - name: 'date', - value: '01/01/2022', - }, - ], - credentialDefinitionId: credentialDefinition.credentialDefinitionId, + dataIntegrity: { + bindingRequired: true, + anonCredsLinkSecretBindingMethodOptions: { + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + }, + credential: new W3cCredential({ + type: ['VerifiableCredential'], + issuanceDate: new Date().toISOString(), + issuer: this.anonCredsIssuerId as string, + credentialSubject: new W3cCredentialSubject({ + claims: { + name: 'Alice Smith', + age: 28, + height: 173, + }, + }), + }), }, }, }) this.ui.updateBottomBar( `\nCredential offer sent!\n\nGo to the Alice agent to accept the credential offer\n\n${Color.Reset}` ) + + this.agent.events.on(CredentialEventTypes.CredentialStateChanged, async (afjEvent) => { + const credentialRecord = afjEvent.payload.credentialRecord + if (afjEvent.payload.credentialRecord.state !== CredentialState.RequestReceived) return + + console.log(`\nAccepting Credential Request. Sending Credential!\n\n`) + + await this.agent.credentials.acceptRequest({ + credentialRecordId: credentialRecord.id, + credentialFormats: { + dataIntegrity: { + credentialSubjectId: 'did:key:z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9', + didCommSignedAttachmentAcceptRequestOptions: { + kid: 'did:key:z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9#z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9', + }, + }, + }, + }) + }) } private async printProofFlow(print: string) { @@ -236,41 +276,81 @@ export class Faber extends BaseAgent { await new Promise((f) => setTimeout(f, 2000)) } - private async newProofAttribute() { - await this.printProofFlow(greenText(`Creating new proof attribute for 'name' ...\n`)) - const proofAttribute = { - name: { - name: 'name', - restrictions: [ - { - cred_def_id: this.credentialDefinition?.credentialDefinitionId, - }, - ], - }, - } - - return proofAttribute - } - public async sendProofRequest() { const connectionRecord = await this.getConnectionRecord() - const proofAttribute = await this.newProofAttribute() await this.printProofFlow(greenText('\nRequesting proof...\n', false)) await this.agent.proofs.requestProof({ protocolVersion: 'v2', connectionId: connectionRecord.id, proofFormats: { - anoncreds: { - name: 'proof-request', - version: '1.0', - requested_attributes: proofAttribute, + presentationExchange: { + presentationDefinition: { + id: '1234567', + name: 'Age Verification', + purpose: 'We need to verify your age before entering a bar', + input_descriptors: [ + { + id: 'age-verification', + name: 'A specific type of VC + Issuer', + purpose: 'We want a VC of this type generated by this issuer', + schema: [ + { + uri: 'https://www.w3.org/2018/credentials/v1', + }, + ], + constraints: { + limit_disclosure: 'required', + fields: [ + { + path: ['$.issuer'], + filter: { + type: 'string', + const: 'did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675', + }, + }, + { + path: ['$.credentialSubject.name'], + }, + { + path: ['$.credentialSubject.height'], + }, + { + path: ['$.credentialSubject.age'], + predicate: 'preferred', + filter: { + type: 'number', + minimum: 18, + }, + }, + ], + }, + }, + ], + format: { + di_vc: { + proof_type: ['DataIntegrityProof'], + cryptosuite: ['anoncredsvc-2023', 'eddsa-rdfc-2022'], + }, + }, + }, }, }, }) this.ui.updateBottomBar( `\nProof request sent!\n\nGo to the Alice agent to accept the proof request\n\n${Color.Reset}` ) + + this.agent.events.on(ProofEventTypes.ProofStateChanged, async (afjEvent) => { + if (afjEvent.payload.proofRecord.state !== ProofState.PresentationReceived) return + + const proofRecord = afjEvent.payload.proofRecord + + console.log(`\nAccepting Presentation!\n\n${Color.Reset}`) + await this.agent.proofs.acceptPresentation({ + proofRecordId: proofRecord.id, + }) + }) } public async sendMessage(message: string) { diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json index 92fbc15b38..0fc559dd14 100644 --- a/packages/anoncreds-rs/package.json +++ b/packages/anoncreds-rs/package.json @@ -24,6 +24,7 @@ "test": "jest" }, "dependencies": { + "@astronautlabs/jsonpath": "^1.1.2", "@aries-framework/anoncreds": "0.4.2", "@aries-framework/core": "0.4.2", "class-transformer": "^0.5.1", @@ -32,8 +33,9 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.7", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.7", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.8", + "@hyperledger/anoncreds-shared": "^0.2.0-dev.8", + "@sphereon/pex-models": "^2.1.2", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", "reflect-metadata": "^0.1.13", @@ -41,6 +43,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.7" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.8" } } diff --git a/packages/anoncreds-rs/src/AnonCredsRsModule.ts b/packages/anoncreds-rs/src/AnonCredsRsModule.ts index 486233046a..03daed24ae 100644 --- a/packages/anoncreds-rs/src/AnonCredsRsModule.ts +++ b/packages/anoncreds-rs/src/AnonCredsRsModule.ts @@ -6,9 +6,10 @@ import { AnonCredsIssuerServiceSymbol, AnonCredsVerifierServiceSymbol, } from '@aries-framework/anoncreds' -import { AgentConfig } from '@aries-framework/core' +import { AgentConfig, anonCredsVcDataIntegrityServiceSymbol } from '@aries-framework/core' import { AnonCredsRsModuleConfig } from './AnonCredsRsModuleConfig' +import { AnonCredsVc2023DataIntegrityService } from './AnonCredsVcDataIntegrityService' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from './services' export class AnonCredsRsModule implements Module { @@ -32,5 +33,6 @@ export class AnonCredsRsModule implements Module { dependencyManager.registerSingleton(AnonCredsHolderServiceSymbol, AnonCredsRsHolderService) dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, AnonCredsRsIssuerService) dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, AnonCredsRsVerifierService) + dependencyManager.registerSingleton(anonCredsVcDataIntegrityServiceSymbol, AnonCredsVc2023DataIntegrityService) } } diff --git a/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts b/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts new file mode 100644 index 0000000000..c1567bb752 --- /dev/null +++ b/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts @@ -0,0 +1,641 @@ +import type { + AnonCredsCredentialDefinition, + AnonCredsNonRevokedInterval, + AnonCredsProofRequest, + AnonCredsRequestedPredicate, + AnonCredsSchema, +} from '@aries-framework/anoncreds' +import type { + AgentContext, + AnonCredsVcDataIntegrityService, + AnonCredsVcVerificationOptions, + JsonObject, + W3cCredentialRecord, +} from '@aries-framework/core' +import type { W3cCredentialEntry, CredentialProve, NonRevokedIntervalOverride } from '@hyperledger/anoncreds-shared' +import type { + Descriptor, + FieldV2, + PresentationDefinitionV1, + PresentationDefinitionV2, + PresentationSubmission, + InputDescriptorV2, + InputDescriptorV1, +} from '@sphereon/pex-models' + +import { + AnonCredsLinkSecretRepository, + AnonCredsModuleConfig, + AnonCredsRegistryService, + assertBestPracticeRevocationInterval, + fetchObjectsFromLedger, +} from '@aries-framework/anoncreds' +import { + W3cJsonLdVerifiableCredential, + AriesFrameworkError, + JsonTransformer, + deepEquality, + injectable, +} from '@aries-framework/core' +import { JSONPath } from '@astronautlabs/jsonpath' +import { + W3cCredential as AnonCredsW3cCredential, + W3cPresentation as AnonCredsW3cPresentation, + CredentialRevocationState, + RevocationRegistryDefinition, + RevocationStatusList, +} from '@hyperledger/anoncreds-shared' + +export interface CredentialWithMetadata { + credential: JsonObject + nonRevoked?: AnonCredsNonRevokedInterval + timestamp?: number +} + +export interface RevocationRegistryFetchMetadata { + timestamp?: number + revocationRegistryId: string + revocationRegistryIndex?: number + nonRevokedInterval: AnonCredsNonRevokedInterval +} + +export type PathComponent = string | number + +@injectable() +export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataIntegrityService { + private getDataIntegrityProof(credential: W3cJsonLdVerifiableCredential, cryptosuite: string) { + if (Array.isArray(credential.proof)) { + const proof = credential.proof.find( + (proof) => proof.type === 'DataIntegrityProof' && proof.cryptosuite === cryptosuite + ) + if (!proof) throw new AriesFrameworkError('Could not find anoncreds proof') + return proof + } + + if (credential.proof.type !== 'DataIntegrityProof' || credential.proof.cryptosuite !== cryptosuite) { + throw new AriesFrameworkError( + `Unsupported proof type '${credential.proof.type}' or cryptosuite '${credential.proof.cryptosuite}'.` + ) + } + + return credential.proof + } + private extractPathNodes(obj: unknown, paths: string[]): { value: unknown; path: PathComponent[] }[] { + let result: { value: unknown; path: PathComponent[] }[] = [] + if (paths) { + for (const path of paths) { + result = JSONPath.nodes(obj, path) + if (result.length) break + } + } + return result + } + + private getCredentialMetadata( + entryIndex: number, + selectedCredentials: JsonObject[], + selectedCredentialRecords: W3cCredentialRecord[] + ) { + const credentialRecord = selectedCredentialRecords[entryIndex] + if (!deepEquality(JsonTransformer.toJSON(credentialRecord.credential), selectedCredentials[entryIndex])) { + throw new AriesFrameworkError('selected credential does not match the selected credential record') + } + + const anonCredsTags = credentialRecord.getAnonCredsTags() + if (!anonCredsTags) throw new AriesFrameworkError('No anoncreds tags found on credential record') + + return { + entryIndex, + credentialRecord, + anonCredsTags, + } + } + + private getCredential(descriptorMapObject: Descriptor, selectedCredentials: JsonObject[]) { + const presentationWrapper = { + verifiableCredential: selectedCredentials, + } + + const credentialExtractionResult = this.extractPathNodes(presentationWrapper, [descriptorMapObject.path]) + if (credentialExtractionResult.length === 0 || credentialExtractionResult.length > 1) { + throw new Error('Could not extract credential from presentation submission') + } + + // only available on the holder side + const jsonLdVerifiableCredentialJson = credentialExtractionResult[0].value + + const entryIndex = selectedCredentials.findIndex((credential) => + deepEquality(credential, jsonLdVerifiableCredentialJson) + ) + if (entryIndex === -1) throw new AriesFrameworkError('Could not find selected credential') + + return { + entryIndex, + credential: JsonTransformer.fromJSON(jsonLdVerifiableCredentialJson, W3cJsonLdVerifiableCredential), + credentialJson: jsonLdVerifiableCredentialJson, + } + } + + private async getRevocationMetadata( + agentContext: AgentContext, + revocationRegistryFetchMetadata: RevocationRegistryFetchMetadata, + mustHaveTimeStamp = false + ) { + let nonRevokedIntervalOverride: NonRevokedIntervalOverride | undefined + + const { revocationRegistryId, revocationRegistryIndex, nonRevokedInterval, timestamp } = + revocationRegistryFetchMetadata + if (!revocationRegistryId || !nonRevokedInterval || (mustHaveTimeStamp && !timestamp)) { + throw new AriesFrameworkError('Invalid revocation metadata') + } + + // Make sure the revocation interval follows best practices from Aries RFC 0441 + assertBestPracticeRevocationInterval(nonRevokedInterval) + + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, revocationRegistryId) + + const { revocationRegistryDefinition: _revocationRegistryDefinition, resolutionMetadata } = + await registry.getRevocationRegistryDefinition(agentContext, revocationRegistryId) + if (!_revocationRegistryDefinition) { + throw new AriesFrameworkError( + `Could not retrieve revocation registry definition for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` + ) + } + + const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.getTailsFile(agentContext, { + revocationRegistryDefinition: _revocationRegistryDefinition, + }) + + const timestampToFetch = timestamp ?? nonRevokedInterval.to + if (!timestampToFetch) throw new AriesFrameworkError('Timestamp to fetch is required') + + // Fetch revocation status list if we don't already have a revocation status list for the given timestamp + const { revocationStatusList: _revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = + await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestampToFetch) + + if (!_revocationStatusList) { + throw new AriesFrameworkError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` + ) + } + + const updatedTimestamp = timestamp ?? _revocationStatusList.timestamp + + const revocationRegistryDefinition = RevocationRegistryDefinition.fromJson( + _revocationRegistryDefinition as unknown as JsonObject + ) + const revocationStatusList = RevocationStatusList.fromJson(_revocationStatusList as unknown as JsonObject) + const revocationState = revocationRegistryIndex + ? CredentialRevocationState.create({ + revocationRegistryIndex: Number(revocationRegistryIndex), + revocationRegistryDefinition: revocationRegistryDefinition, + tailsPath: tailsFilePath, + revocationStatusList, + }) + : undefined + + const requestedFrom = nonRevokedInterval.from + if (requestedFrom && requestedFrom > timestampToFetch) { + const { revocationStatusList: overrideRevocationStatusList } = await registry.getRevocationStatusList( + agentContext, + revocationRegistryId, + requestedFrom + ) + + const vdrTimestamp = overrideRevocationStatusList?.timestamp + if (vdrTimestamp && vdrTimestamp === timestampToFetch) { + nonRevokedIntervalOverride = { + overrideRevocationStatusListTimestamp: timestampToFetch, + requestedFromTimestamp: requestedFrom, + revocationRegistryDefinitionId: revocationRegistryId, + } + } else { + throw new AriesFrameworkError( + `VDR timestamp for ${requestedFrom} does not correspond to the one provided in proof identifiers. Expected: ${updatedTimestamp} and received ${vdrTimestamp}` + ) + } + } + + return { + updatedTimestamp, + revocationRegistryDefinition: [revocationRegistryId, revocationRegistryDefinition] as [ + string, + RevocationRegistryDefinition + ], + revocationStatusList, + revocationState, + nonRevokedIntervalOverride, + } + } + + private async getCredentialDefinitionsAndSchemas( + agentContext: AgentContext, + schemaIds: Set | undefined, + credentialDefinitionIds: Set + ) { + const schemaFetchPromises = [...(schemaIds ?? [])].map((schemaId) => + fetchObjectsFromLedger(agentContext, { schemaId }) + ) + const credentialDefinitionFetchPromises = [...credentialDefinitionIds].map((credentialDefinitionId) => + fetchObjectsFromLedger(agentContext, { credentialDefinitionId }) + ) + + const schemas: Record = {} + const credentialDefinitions: Record = {} + + const results = await Promise.all([ + Promise.all(schemaFetchPromises), + Promise.all(credentialDefinitionFetchPromises), + ]) + + const credentialDefinitionFetchResults = results[1] + for (const res of credentialDefinitionFetchResults) { + const credentialDefinitionId = res.credentialDefinitionReturn.credentialDefinitionId + const credentialDefinition = res.credentialDefinitionReturn.credentialDefinition + if (!credentialDefinition) { + throw new AriesFrameworkError('Credential definition not found') + } + + credentialDefinitions[credentialDefinitionId] = credentialDefinition + } + + const schemaFetchResults = + schemaFetchPromises.length > 0 + ? results[0] + : await Promise.all( + credentialDefinitionFetchResults.map((res) => + fetchObjectsFromLedger(agentContext, { + schemaId: res.credentialDefinitionReturn.credentialDefinition?.schemaId as string, + }) + ) + ) + + for (const schemaFetchResult of schemaFetchResults) { + const schemaId = schemaFetchResult.schemaReturn.schemaId + const schema = schemaFetchResult.schemaReturn.schema + if (!schema) { + throw new AriesFrameworkError('Credential definition not found') + } + + schemas[schemaId] = schema + } + + return { + schemas, + credentialDefinitions, + } + } + + private getPresentationMetadata = async ( + agentContext: AgentContext, + input: { + credentialsWithMetadata: CredentialWithMetadata[] + credentialsProve: CredentialProve[] + linkSecretIds: Set + schemaIds: Set + credentialDefinitionIds: Set + } + ) => { + const { linkSecretIds, schemaIds, credentialDefinitionIds, credentialsWithMetadata, credentialsProve } = input + const linkSecretIdArray = [...linkSecretIds] + if (linkSecretIdArray.length > 1) { + throw new AriesFrameworkError('Multiple linksecret cannot be used to create a single presentation') + } else if (linkSecretIdArray.length === 0) { + throw new AriesFrameworkError('Cannot create a presentation without a linksecret') + } + + const linkSecretRecord = await agentContext.dependencyManager + .resolve(AnonCredsLinkSecretRepository) + .getByLinkSecretId(agentContext, linkSecretIdArray[0]) + + if (!linkSecretRecord.value) { + throw new AriesFrameworkError('Link Secret value not stored') + } + + const credentials: W3cCredentialEntry[] = await Promise.all( + credentialsWithMetadata.map(async ({ credential, nonRevoked }) => { + const { revocationRegistryIndex, revocationRegistryId, timestamp } = AnonCredsW3cCredential.fromJson(credential) + + if (!nonRevoked) { + return { credential: credential as unknown as JsonObject, revocationState: undefined, timestamp: undefined } + } + + if (!revocationRegistryId || !revocationRegistryIndex) + throw new AriesFrameworkError('Missing revocation metadata') + + const { revocationState, updatedTimestamp } = await this.getRevocationMetadata(agentContext, { + nonRevokedInterval: nonRevoked, + timestamp, + revocationRegistryIndex, + revocationRegistryId, + }) + + return { credential: credential as unknown as JsonObject, revocationState, timestamp: updatedTimestamp } + }) + ) + + const { schemas, credentialDefinitions } = await this.getCredentialDefinitionsAndSchemas( + agentContext, + schemaIds, + credentialDefinitionIds + ) + + return { + schemas, + credentialDefinitions, + linkSecret: linkSecretRecord.value, + credentialsProve, + credentials, + } + } + + private getPredicateTypeAndValues(predicateFilter?: FieldV2['filter']) { + if (!predicateFilter) throw new AriesFrameworkError('Predicate filter is required') + + const predicates: { + predicateType: AnonCredsRequestedPredicate['p_type'] + predicateValue: AnonCredsRequestedPredicate['p_value'] + }[] = [] + + const supportedJsonSchemaNumericRangeProperties: Record = { + exclusiveMinimum: '>', + exclusiveMaximum: '<', + minimum: '>=', + maximum: '<=', + } + + for (const [key, value] of Object.entries(predicateFilter)) { + if (key === 'type') continue + const predicateType = supportedJsonSchemaNumericRangeProperties[key] + if (!predicateType) throw new AriesFrameworkError(`Unsupported predicate filter property '${key}'`) + predicates.push({ + predicateType, + predicateValue: value, + }) + } + + return predicates + } + + public createAnonCredsProofRequestAndMetadata = async ( + agentContext: AgentContext, + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, + presentationSubmission: PresentationSubmission, + credentials: JsonObject[], + holderOpts?: { + selectedCredentialRecords: W3cCredentialRecord[] + } + ) => { + const credentialsProve: CredentialProve[] = [] + const schemaIds = new Set() + const credentialDefinitionIds = new Set() + const linkSecretIds = new Set() + const credentialsWithMetadata: CredentialWithMetadata[] = [] + + const anonCredsProofRequest: AnonCredsProofRequest = { + version: '1.0', + name: presentationDefinition.name ?? 'Proof request', + nonce: presentationDefinition.id, + requested_attributes: {}, + requested_predicates: {}, + } + + const nonRevoked = Math.floor(Date.now() / 1000) + const nonRevokedInterval = { from: nonRevoked, to: nonRevoked } + + for (const descriptorMapObject of presentationSubmission.descriptor_map) { + // PresentationDefinitionV2 is the common denominator + const descriptor: InputDescriptorV1 | InputDescriptorV2 | undefined = ( + presentationDefinition.input_descriptors as InputDescriptorV2[] + ).find((descriptor) => descriptor.id === descriptorMapObject.id) + if (!descriptor) + throw new Error(`Descriptor with id ${descriptorMapObject.id} not found in presentation definition`) + + const referent = descriptorMapObject.id + const attributeReferent = `${referent}_attribute` + const predicateReferentBase = `${referent}_predicate` + let predicateReferentIndex = 0 + + const fields = descriptor.constraints?.fields + if (!fields) throw new AriesFrameworkError('Unclear mapping of constraint with no fields.') + + const { credential, entryIndex } = this.getCredential(descriptorMapObject, credentials) + const credentialJson = JsonTransformer.toJSON(credential) + const { credentialDefinitionId, revocationRegistryId, schemaId } = AnonCredsW3cCredential.fromJson(credentialJson) + + if (holderOpts) { + const credentialMetadata = this.getCredentialMetadata( + entryIndex, + credentials, + holderOpts.selectedCredentialRecords + ) + schemaIds.add(schemaId) + credentialDefinitionIds.add(credentialDefinitionId) + linkSecretIds.add(credentialMetadata.anonCredsTags.linkSecretId) + } + + let useNonRevoked = false + + const statuses = descriptor.constraints?.statuses + if (statuses) { + if ( + statuses?.active?.directive && + (statuses.active.directive === 'allowed' || statuses.active.directive === 'required') + ) { + if (!revocationRegistryId) { + throw new AriesFrameworkError('Selected credentials must be revocable but are not') + } + useNonRevoked = true + } else { + throw new AriesFrameworkError('Unsupported status directive') + } + } + + credentialsWithMetadata.push({ + credential: credentialJson, + nonRevoked: useNonRevoked ? nonRevokedInterval : undefined, + }) + + for (const field of fields) { + if (!field.path) throw new AriesFrameworkError('Field path is required') + // fixme: could the path start otherwise? + const claimPaths = field.path?.filter((path) => path.startsWith('$.credentialSubject.')) + if (!claimPaths) throw new AriesFrameworkError('No claim paths found') + if (claimPaths.length === 0) continue + + const claimNames = claimPaths.map((path) => { + const parts = path.split('$.credentialSubject.') + if (parts.length !== 2) throw new AriesFrameworkError('Invalid claim path') + if (parts[1] === '') throw new AriesFrameworkError('Invalid empty claim name') + return parts[1] + }) + + const propertyName = claimNames[0] + + if (field.predicate) { + const predicateTypeAndValues = this.getPredicateTypeAndValues(field.filter) + for (const { predicateType, predicateValue } of predicateTypeAndValues) { + const predicateReferent = `${predicateReferentBase}_${predicateReferentIndex++}` + anonCredsProofRequest.requested_predicates[predicateReferent] = { + name: propertyName, + p_type: predicateType, + p_value: predicateValue, + restrictions: [{ cred_def_id: credentialDefinitionId }], + non_revoked: useNonRevoked ? nonRevokedInterval : undefined, + } + + credentialsProve.push({ entryIndex, referent: predicateReferent, isPredicate: true, reveal: true }) + } + } else { + if (!anonCredsProofRequest.requested_attributes[attributeReferent]) { + anonCredsProofRequest.requested_attributes[attributeReferent] = { + name: propertyName, + names: [propertyName], + restrictions: [{ cred_def_id: credentialDefinitionId }], + non_revoked: useNonRevoked ? nonRevokedInterval : undefined, + } + } else { + const name = anonCredsProofRequest.requested_attributes[attributeReferent].name + const names = anonCredsProofRequest.requested_attributes[attributeReferent].names ?? [name ?? 'name'] + + anonCredsProofRequest.requested_attributes[attributeReferent].name = undefined + anonCredsProofRequest.requested_attributes[attributeReferent].names = [...names, propertyName] + } + + credentialsProve.push({ entryIndex, referent: attributeReferent, isPredicate: false, reveal: true }) + } + } + } + + const presentationMetadata = holderOpts + ? await this.getPresentationMetadata(agentContext, { + credentialsWithMetadata: credentialsWithMetadata, + credentialsProve, + linkSecretIds, + schemaIds, + credentialDefinitionIds, + }) + : undefined + + const revocationMetadata = !holderOpts + ? await Promise.all( + credentialsWithMetadata + .filter((cwm) => cwm.nonRevoked) + .map(async (credentialWithMetadata) => { + const { revocationRegistryIndex, revocationRegistryId, timestamp } = AnonCredsW3cCredential.fromJson( + credentialWithMetadata.credential + ) + return await this.getRevocationMetadata(agentContext, { + nonRevokedInterval: credentialWithMetadata.nonRevoked as AnonCredsNonRevokedInterval, + timestamp: timestamp, + revocationRegistryId, + revocationRegistryIndex, + }) + }, true) + ) + : undefined + + return { anonCredsProofRequest, presentationMetadata, revocationMetadata } + } + + public async createPresentation( + agentContext: AgentContext, + options: { + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 + presentationSubmission: PresentationSubmission + selectedCredentials: JsonObject[] + selectedCredentialRecords: W3cCredentialRecord[] + } + ) { + const { presentationDefinition, presentationSubmission, selectedCredentialRecords, selectedCredentials } = options + const { anonCredsProofRequest, presentationMetadata } = await this.createAnonCredsProofRequestAndMetadata( + agentContext, + presentationDefinition, + presentationSubmission, + selectedCredentials, + { + selectedCredentialRecords, + } + ) + + if (!presentationMetadata) throw new AriesFrameworkError('Presentation metadata not created') + const { schemas, credentialDefinitions, linkSecret, credentialsProve, credentials } = presentationMetadata + + let presentation: AnonCredsW3cPresentation | undefined + try { + presentation = AnonCredsW3cPresentation.create({ + credentials, + schemas: schemas as unknown as Record, + credentialDefinitions: credentialDefinitions as unknown as Record, + linkSecret, + credentialsProve, + presentationRequest: anonCredsProofRequest as unknown as JsonObject, + }) + const presentationJson = presentation.toJson() as unknown as JsonObject + return presentationJson + } finally { + presentation?.handle.clear() + } + } + + public async verifyPresentation(agentContext: AgentContext, options: AnonCredsVcVerificationOptions) { + const { presentation, presentationDefinition, presentationSubmission } = options + + let anonCredsW3cPresentation: AnonCredsW3cPresentation | undefined + let result = false + const credentialDefinitionIds = new Set() + try { + const verifiableCredentials = Array.isArray(presentation.verifiableCredential) + ? presentation.verifiableCredential + : [presentation.verifiableCredential] + + for (const verifiableCredential of verifiableCredentials) { + if (verifiableCredential instanceof W3cJsonLdVerifiableCredential) { + const proof = this.getDataIntegrityProof(verifiableCredential, 'anoncredspresvc-2023') + credentialDefinitionIds.add(proof.verificationMethod) + } else { + throw new AriesFrameworkError('Unsupported credential type') + } + } + + const verifiableCredentialsJson = verifiableCredentials.map((credential) => JsonTransformer.toJSON(credential)) + const { anonCredsProofRequest, revocationMetadata } = await this.createAnonCredsProofRequestAndMetadata( + agentContext, + presentationDefinition, + presentationSubmission, + verifiableCredentialsJson + ) + if (!revocationMetadata) throw new AriesFrameworkError('Missing revocation metadata') + + const { credentialDefinitions, schemas } = await this.getCredentialDefinitionsAndSchemas( + agentContext, + undefined, + credentialDefinitionIds + ) + const presentationJson = JsonTransformer.toJSON(presentation) + anonCredsW3cPresentation = AnonCredsW3cPresentation.fromJson(presentationJson) + + const revocationRegistryDefinitions: Record = {} + revocationMetadata.forEach( + (rm) => (revocationRegistryDefinitions[rm.revocationRegistryDefinition[0]] = rm.revocationRegistryDefinition[1]) + ) + result = anonCredsW3cPresentation.verify({ + presentationRequest: anonCredsProofRequest as unknown as JsonObject, + schemas: schemas as unknown as Record, + credentialDefinitions: credentialDefinitions as unknown as Record, + revocationRegistryDefinitions: revocationRegistryDefinitions, + revocationStatusLists: revocationMetadata.map((rm) => rm.revocationStatusList), + nonRevokedIntervalOverrides: revocationMetadata + .filter((rm) => rm.nonRevokedIntervalOverride) + .map((rm) => rm.nonRevokedIntervalOverride as NonRevokedIntervalOverride), + }) + } finally { + anonCredsW3cPresentation?.handle.clear() + } + + return result + } +} diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index 2ce0a49fee..d5e0e85803 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -31,11 +31,13 @@ import { V2CredentialProtocol, V2ProofProtocol, DidsModule, + PresentationExchangeProofFormatService, } from '@aries-framework/core' import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { randomUUID } from 'crypto' import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../../anoncreds/src' +import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { AskarModule } from '../../askar/src' import { askarModuleConfig } from '../../askar/tests/helpers' @@ -67,15 +69,17 @@ export const getAnonCredsModules = ({ autoAcceptProofs?: AutoAcceptProof registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] } = {}) => { + const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() const anonCredsCredentialFormatService = new AnonCredsCredentialFormatService() const anonCredsProofFormatService = new AnonCredsProofFormatService() + const presentationExchangeProofFormatService = new PresentationExchangeProofFormatService() const modules = { credentials: new CredentialsModule({ autoAcceptCredentials, credentialProtocols: [ new V2CredentialProtocol({ - credentialFormats: [anonCredsCredentialFormatService], + credentialFormats: [dataIntegrityCredentialFormatService, anonCredsCredentialFormatService], }), ], }), @@ -83,7 +87,7 @@ export const getAnonCredsModules = ({ autoAcceptProofs, proofProtocols: [ new V2ProofProtocol({ - proofFormats: [anonCredsProofFormatService], + proofFormats: [anonCredsProofFormatService, presentationExchangeProofFormatService], }), ], }), diff --git a/packages/anoncreds-rs/tests/data-integrity.e2e.test.ts b/packages/anoncreds-rs/tests/data-integrity.e2e.test.ts new file mode 100644 index 0000000000..b545961c2f --- /dev/null +++ b/packages/anoncreds-rs/tests/data-integrity.e2e.test.ts @@ -0,0 +1,333 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { AgentContext, KeyDidCreateOptions } from '@aries-framework/core' +import type { EventReplaySubject } from '@aries-framework/core/tests' +import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' + +import { + AutoAcceptCredential, + CredentialExchangeRecord, + CredentialState, + DidKey, + DidsApi, + KeyType, + ProofState, + TypedArrayEncoder, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, +} from '@aries-framework/core' + +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { waitForCredentialRecordSubject, waitForProofExchangeRecord } from '../../core/tests/helpers' + +import { setupAnonCredsTests } from './anoncredsSetup' +import { presentationDefinition } from './fixtures/presentation-definition' + +export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey: string) { + const dids = agentContext.dependencyManager.resolve(DidsApi) + const didCreateResult = await dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString(secretKey) }, + }) + + const did = didCreateResult.didState.did as string + const didKey = DidKey.fromDid(did) + const kid = `${did}#${didKey.key.fingerprint}` + + const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + return { did, kid, verificationMethod } +} + +const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' +export type KDV = Awaited> + +describe('data anoncreds w3c data integrity e2e tests', () => { + let issuerAgent: AnonCredsTestsAgent + let holderAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let issuerHolderConnectionId: string + let holderIssuerConnectionId: string + let revocationRegistryDefinitionId: string | undefined + + let issuerReplay: EventReplaySubject + let holderReplay: EventReplaySubject + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + afterEach(async () => { + await issuerAgent.shutdown() + await issuerAgent.wallet.delete() + await holderAgent.shutdown() + await holderAgent.wallet.delete() + }) + + test('issuance and verification flow starting from proposal without negotiation and with revocation', async () => { + ;({ + issuerAgent, + issuerReplay, + holderAgent, + holderReplay, + credentialDefinitionId, + issuerHolderConnectionId, + revocationRegistryDefinitionId, + holderIssuerConnectionId, + } = await setupAnonCredsTests({ + issuerId: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL', + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['id', 'name', 'height', 'age'], + registries: [inMemoryRegistry], + supportRevocation: true, + })) + await anonCredsFlowTest({ + credentialDefinitionId, + issuerHolderConnectionId, + revocationRegistryDefinitionId, + holderIssuerConnectionId, + issuerReplay: issuerReplay, + holderReplay: holderReplay, + issuer: issuerAgent, + holder: holderAgent, + }) + }) + + test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { + ;({ + issuerAgent, + issuerReplay, + holderAgent, + holderReplay, + credentialDefinitionId, + issuerHolderConnectionId, + revocationRegistryDefinitionId, + holderIssuerConnectionId, + } = await setupAnonCredsTests({ + issuerId: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL', + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['id', 'name', 'height', 'age'], + registries: [inMemoryRegistry], + supportRevocation: false, + })) + await anonCredsFlowTest({ + credentialDefinitionId, + issuerHolderConnectionId, + holderIssuerConnectionId, + issuerReplay, + holderReplay, + revocationRegistryDefinitionId, + issuer: issuerAgent, + holder: holderAgent, + }) + }) +}) + +async function anonCredsFlowTest(options: { + issuer: AnonCredsTestsAgent + holder: AnonCredsTestsAgent + issuerHolderConnectionId: string + holderIssuerConnectionId: string + issuerReplay: EventReplaySubject + holderReplay: EventReplaySubject + revocationRegistryDefinitionId: string | undefined + credentialDefinitionId: string +}) { + const { + credentialDefinitionId, + issuerHolderConnectionId, + holderIssuerConnectionId, + issuer, + revocationRegistryDefinitionId, + holder, + issuerReplay, + holderReplay, + } = options + + const holderKdv = await createDidKidVerificationMethod(holder.context, '96213c3d7fc8d4d6754c7a0fd969598f') + const linkSecret = await holder.modules.anoncreds.createLinkSecret({ linkSecretId: 'linkSecretId' }) + expect(linkSecret).toBe('linkSecretId') + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerId, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ + id: holderKdv.did, + claims: { name: 'John', age: '25', height: 173 }, + }), + }) + + // issuer offers credential + let issuerRecord = await issuer.credentials.offerCredential({ + protocolVersion: 'v2', + autoAcceptCredential: AutoAcceptCredential.Never, + connectionId: issuerHolderConnectionId, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + anonCredsLinkSecretBindingMethodOptions: { + credentialDefinitionId, + revocationRegistryDefinitionId, + revocationRegistryIndex: revocationRegistryDefinitionId ? 1 : undefined, + }, + didCommSignedAttachmentBindingMethodOptions: {}, + }, + }, + }) + + // Holder processes and accepts offer + let holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.OfferReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holder.credentials.acceptOffer({ + credentialRecordId: holderRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: { + anonCredsLinkSecretCredentialRequestOptions: { + linkSecretId: 'linkSecretId', + }, + // didCommSignedAttachmentCredentialRequestOptions: { + // kid: holderKdv.kid, + // }, + }, + }, + }) + + // issuer receives request and accepts + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.RequestReceived, + threadId: holderRecord.threadId, + }) + issuerRecord = await issuer.credentials.acceptRequest({ + credentialRecordId: issuerRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: {}, + }, + }) + + holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.CredentialReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holder.credentials.acceptCredential({ + credentialRecordId: holderRecord.id, + }) + + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.Done, + threadId: holderRecord.threadId, + }) + + expect(holderRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_dataIntegrity/credential': { + linkSecretMetadata: { + credentialDefinitionId, + schemaId: expect.any(String), + }, + }, + '_dataIntegrity/credentialRequest': { + linkSecretRequestMetadata: { + link_secret_blinding_data: { + v_prime: expect.any(String), + vr_prime: revocationRegistryDefinitionId ? expect.any(String) : null, + }, + nonce: expect.any(String), + link_secret_name: 'linkSecretId', + }, + }, + }, + }, + state: CredentialState.Done, + }) + + const tags = holderRecord.getTags() + expect(tags.credentialIds).toHaveLength(1) + + await expect( + holder.dependencyManager + .resolve(W3cCredentialService) + .getCredentialRecordById(holder.context, tags.credentialIds[0]) + ).resolves + + let issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuer, { + state: ProofState.ProposalReceived, + }) + + const pdCopy: PresentationDefinitionV1 = JSON.parse(JSON.stringify(presentationDefinition)) + if (!revocationRegistryDefinitionId) + pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => delete ide.constraints?.statuses) + + let holderProofExchangeRecord = await holder.proofs.proposeProof({ + protocolVersion: 'v2', + connectionId: holderIssuerConnectionId, + proofFormats: { + presentationExchange: { + presentationDefinition: pdCopy, + }, + }, + }) + + let issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + + let holderProofExchangeRecordPromise = waitForProofExchangeRecord(holder, { + state: ProofState.RequestReceived, + }) + + issuerProofExchangeRecord = await issuer.proofs.acceptProposal({ + proofRecordId: issuerProofExchangeRecord.id, + }) + + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const requestedCredentials = await holder.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const selectedCredentials = requestedCredentials.proofFormats.presentationExchange?.credentials + if (!selectedCredentials) { + throw new Error('No credentials found for presentation exchange') + } + + issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuer, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holder.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { + presentationExchange: { + credentials: selectedCredentials, + }, + }, + }) + issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + + holderProofExchangeRecordPromise = waitForProofExchangeRecord(holder, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + await issuer.proofs.acceptPresentation({ proofRecordId: issuerProofExchangeRecord.id }) + + holderProofExchangeRecord = await holderProofExchangeRecordPromise +} diff --git a/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts b/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts new file mode 100644 index 0000000000..32f143b7b4 --- /dev/null +++ b/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts @@ -0,0 +1,54 @@ +export const presentationDefinition = { + id: '12345', + name: 'Age Verification', + purpose: 'We need to verify your age before entering a bar', + input_descriptors: [ + { + id: 'age-verification', + name: 'A specific type of VC + Issuer', + purpose: 'We want a VC of this type generated by this issuer', + schema: [ + { + uri: 'https://www.w3.org/2018/credentials/v1', + }, + ], + constraints: { + limit_disclosure: 'required' as const, + statuses: { + active: { + directive: 'required' as const, + }, + }, + fields: [ + { + path: ['$.issuer'], + filter: { + type: 'string', + const: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL', + }, + }, + { + path: ['$.credentialSubject.name'], + }, + { + path: ['$.credentialSubject.height'], + }, + { + path: ['$.credentialSubject.age'], + predicate: 'preferred' as const, + filter: { + type: 'number', + minimum: 18, + }, + }, + ], + }, + }, + ], + format: { + di_vc: { + proof_type: ['DataIntegrityProof'], + cryptosuite: ['anoncredspresvc-2023', 'eddsa-rdfc-2022'], + }, + }, +} diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index e1f87a4b06..4d6ed8eafd 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -29,17 +29,17 @@ "class-transformer": "0.5.1", "class-validator": "0.14.0", "reflect-metadata": "^0.1.13", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.7" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.8" }, "devDependencies": { "@aries-framework/node": "0.4.2", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.7", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.8", "indy-sdk": "^1.16.0-dev-1636", "rimraf": "^4.4.0", "rxjs": "^7.8.0", "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.7" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.8" } } diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index d510759f72..5974101c23 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -57,6 +57,7 @@ import { assertNoDuplicateGroupsNamesInProofRequest, getRevocationRegistriesForRequest, getRevocationRegistriesForProof, + fetchObjectsFromLedger, } from '../utils' import { dateToTimestamp } from '../utils/timestamp' @@ -461,19 +462,15 @@ export class AnonCredsProofFormatService implements ProofFormatService) { - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - const schemas: { [key: string]: AnonCredsSchema } = {} for (const schemaId of schemaIds) { - const schemaRegistry = registryService.getRegistryForIdentifier(agentContext, schemaId) - const schemaResult = await schemaRegistry.getSchema(agentContext, schemaId) - - if (!schemaResult.schema) { - throw new AriesFrameworkError(`Schema not found for id ${schemaId}: ${schemaResult.resolutionMetadata.message}`) + const { schemaReturn } = await fetchObjectsFromLedger(agentContext, { schemaId }) + if (!schemaReturn.schema) { + throw new AriesFrameworkError(`Schema not found for id ${schemaId}: ${schemaReturn.resolutionMetadata.message}`) } - schemas[schemaId] = schemaResult.schema + schemas[schemaId] = schemaReturn.schema } return schemas @@ -489,28 +486,17 @@ export class AnonCredsProofFormatService implements ProofFormatService) { - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - const credentialDefinitions: { [key: string]: AnonCredsCredentialDefinition } = {} for (const credentialDefinitionId of credentialDefinitionIds) { - const credentialDefinitionRegistry = registryService.getRegistryForIdentifier( - agentContext, - credentialDefinitionId - ) - - const credentialDefinitionResult = await credentialDefinitionRegistry.getCredentialDefinition( - agentContext, - credentialDefinitionId - ) - - if (!credentialDefinitionResult.credentialDefinition) { + const { credentialDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { credentialDefinitionId }) + if (!credentialDefinitionReturn.credentialDefinition) { throw new AriesFrameworkError( - `Credential definition not found for id ${credentialDefinitionId}: ${credentialDefinitionResult.resolutionMetadata.message}` + `Credential definition not found for id ${credentialDefinitionId}: ${credentialDefinitionReturn.resolutionMetadata.message}` ) } - credentialDefinitions[credentialDefinitionId] = credentialDefinitionResult.credentialDefinition + credentialDefinitions[credentialDefinitionId] = credentialDefinitionReturn.credentialDefinition } return credentialDefinitions diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index b40e39d641..25faf5d84b 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -451,7 +451,14 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ) } - if (credentialSubjectId && credentialAttributes.find((ca) => ca.name === 'id') === undefined) { + const credentialSubjectIdAttribute = credentialAttributes.find((ca) => ca.name === 'id') + if ( + credentialSubjectId && + credentialSubjectIdAttribute && + credentialSubjectIdAttribute.value !== credentialSubjectId + ) { + throw new AriesFrameworkError('Invalid credential subject id.') + } else if (!credentialSubjectIdAttribute && credentialSubjectId) { credentialAttributes.push(new CredentialPreviewAttribute({ name: 'id', value: credentialSubjectId })) } @@ -530,9 +537,11 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer if (issuerKid) { verificationMethod = didDocument.dereferenceKey(issuerKid, ['authentication', 'assertionMethod']) } else { - const vms = didDocument.authentication ?? didDocument.assertionMethod + const vms = didDocument.authentication ?? didDocument.assertionMethod ?? didDocument.verificationMethod if (!vms || vms.length === 0) { - throw new AriesFrameworkError('Missing authentication or assertionMethod in did document') + throw new AriesFrameworkError( + 'Missing authenticationMethod, assertionMethod, and verificationMethods in did document' + ) } if (typeof vms[0] === 'string') { @@ -656,16 +665,8 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credentialSubjectId: dataIntegrityFormat.credentialSubjectId, }) - if ( - dataIntegrityFormat.credentialSubjectId && - Array.isArray(signedCredential.credentialSubject) === false && - signedCredential.credentialSubject.id && - dataIntegrityFormat.credentialSubjectId !== signedCredential.credentialSubject.id - ) { - throw new AriesFrameworkError('Invalid credential subject id.') - } - // TODO: check if any non integrity protected fields were on the offered credential. If so throw + // TODO: assert issuer id is the same as the credential definition issuer id } if (credentialRequest.binding_proof?.didcomm_signed_attachment) { @@ -673,10 +674,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new AriesFrameworkError('Cannot issue credential with a binding method that was not offered.') } - if (!dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions) { - throw new AriesFrameworkError('Missing didCommSignedAttachmentAcceptRequestOptions') - } - const bindingProofAttachment = requestAppendAttachments?.find( (attachments) => attachments.id === credentialRequest.binding_proof?.didcomm_signed_attachment?.attachment_id ) @@ -687,8 +684,8 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new AriesFrameworkError('Invalid nonce in signed attachment') } - const issuerKid = dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions.kid - signedCredential = await this.signCredential(agentContext, assertedCredential, issuerKid) + const issuerKid = dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions?.kid + signedCredential = await this.signCredential(agentContext, signedCredential ?? assertedCredential, issuerKid) } if ( @@ -835,6 +832,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ) } else { w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) + // TODO: check if the credentials contains a data integrity proof } const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService.ts new file mode 100644 index 0000000000..df22bdda7a --- /dev/null +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '../../../../agent' +import type { JsonObject } from '../../../../types' +import type { W3cCredentialRecord, W3cJsonLdVerifiablePresentation } from '../../../vc' +import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' + +export interface AnonCredsVcSignatureOptions extends Record { + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 + presentationSubmission: PresentationSubmission + selectedCredentials: JsonObject[] + selectedCredentialRecords: W3cCredentialRecord[] +} + +export interface AnonCredsVcVerificationOptions extends Record { + presentation: W3cJsonLdVerifiablePresentation + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 + presentationSubmission: PresentationSubmission +} + +export const anonCredsVcDataIntegrityServiceSymbol = Symbol('AnonCredsVcDataIntegrityService') + +/** + * We keep this standalone and don't integrity it + * with for example the SignatureSuiteRegistry due + * to it's unique properties, in order to not pollute, + * the existing api's. + */ +export interface AnonCredsVcDataIntegrityService { + createPresentation(agentContext: AgentContext, options: AnonCredsVcSignatureOptions): Promise + + verifyPresentation(agentContext: AgentContext, options: AnonCredsVcVerificationOptions): Promise +} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts index 24724deb61..7474d533d7 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts @@ -1,3 +1,4 @@ export * from './DataIntegrityCredentialFormat' export * from './dataIntegrityExchange' export * from './dataIntegrityMetadata' +export * from './AnonCredsVcDataIntegrityService' diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index eab8642230..f8710e8d19 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -1,25 +1,32 @@ import type { - DifPexInputDescriptorToCredentials, DifPexCredentialsForRequest, + DifPexInputDescriptorToCredentials, DifPresentationExchangeDefinition, DifPresentationExchangeDefinitionV1, - DifPresentationExchangeSubmission, DifPresentationExchangeDefinitionV2, + DifPresentationExchangeSubmission, } from './models' import type { AgentContext } from '../../agent' import type { Query } from '../../storage/StorageService' +import type { JsonObject } from '../../types' +import type { AnonCredsVcDataIntegrityService } from '../credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService' import type { VerificationMethod } from '../dids' -import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresentation } from '../vc' +import type { W3cCredentialRecord, W3cVerifiablePresentation } from '../vc' import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@sphereon/pex' import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' -import type { OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types' +import type { + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, + OriginalVerifiableCredential, + OriginalVerifiablePresentation, +} from '@sphereon/ssi-types' -import { Status, PEVersion, PEX } from '@sphereon/pex' +import { PEVersion, PEX, Status } from '@sphereon/pex' import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../crypto' import { AriesFrameworkError } from '../../error' import { JsonTransformer } from '../../utils' +import { anonCredsVcDataIntegrityServiceSymbol } from '../credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService' import { DidsApi, getKeyFromVerificationMethod } from '../dids' import { ClaimFormat, @@ -38,7 +45,7 @@ import { getW3cVerifiablePresentationInstance, } from './utils' -export type ProofStructure = Record>> +export type ProofStructure = Record>> @injectable() export class DifPresentationExchangeService { @@ -80,7 +87,7 @@ export class DifPresentationExchangeService { } // We pick the first matching VC if we are auto-selecting - credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0].credential) + credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0]) } } @@ -109,7 +116,10 @@ export class DifPresentationExchangeService { ) { const { errors } = this.pex.evaluatePresentation( presentationDefinition, - presentation.encoded as OriginalVerifiablePresentation + presentation.encoded as OriginalVerifiablePresentation, + { + limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncredsvc-2023'], + } ) if (errors) { @@ -185,12 +195,12 @@ export class DifPresentationExchangeService { subjectsToInputDescriptors: ProofStructure, subjectId: string, inputDescriptorId: string, - credential: W3cVerifiableCredential + credentialRecord: W3cCredentialRecord ) { const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] - credentials.push(credential) + credentials.push(credentialRecord) inputDescriptorsToCredentials[inputDescriptorId] = credentials subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials } @@ -198,10 +208,7 @@ export class DifPresentationExchangeService { private getPresentationFormat( presentationDefinition: DifPresentationExchangeDefinition, credentials: Array - ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { - const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') - const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') - + ): ClaimFormat.JwtVp | ClaimFormat.LdpVp | ClaimFormat.DiVp { const inputDescriptorsNotSupportingJwtVc = ( presentationDefinition.input_descriptors as Array ).filter((d) => d.format && d.format.jwt_vc === undefined) @@ -210,18 +217,31 @@ export class DifPresentationExchangeService { presentationDefinition.input_descriptors as Array ).filter((d) => d.format && d.format.ldp_vc === undefined) + const inputDescriptorsNotSupportingDiVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.di_vc === undefined) + + const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') + const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') + if ( allCredentialsAreJwtVc && - (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && - inputDescriptorsNotSupportingJwtVc.length === 0 + inputDescriptorsNotSupportingJwtVc.length === 0 && + (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) ) { return ClaimFormat.JwtVp } else if ( allCredentialsAreLdpVc && - (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && - inputDescriptorsNotSupportingLdpVc.length === 0 + inputDescriptorsNotSupportingLdpVc.length === 0 && + (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) ) { return ClaimFormat.LdpVp + } else if ( + allCredentialsAreLdpVc && + inputDescriptorsNotSupportingDiVc.length === 0 && + (presentationDefinition.format === undefined || presentationDefinition.format.di_vc) + ) { + return ClaimFormat.DiVp } else { throw new DifPresentationExchangeError( 'No suitable presentation format found for the given presentation definition, and credentials' @@ -248,19 +268,19 @@ export class DifPresentationExchangeService { const proofStructure: ProofStructure = {} Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { - credentials.forEach((credential) => { - const subjectId = credential.credentialSubjectIds[0] + credentials.forEach((credentialRecord) => { + const subjectId = credentialRecord.credential.credentialSubjectIds[0] if (!subjectId) { throw new DifPresentationExchangeError('Missing required credential subject for creating the presentation.') } - this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) + this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credentialRecord) }) }) const verifiablePresentationResultsWithFormat: Array<{ verifiablePresentationResult: VerifiablePresentationResult - format: ClaimFormat.LdpVp | ClaimFormat.JwtVp + format: ClaimFormat.LdpVp | ClaimFormat.JwtVp | ClaimFormat.DiVp }> = [] const subjectToInputDescriptors = Object.entries(proofStructure) @@ -279,9 +299,10 @@ export class DifPresentationExchangeService { (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials ) + const credentialRecordsForSubject = Object.values(subjectInputDescriptorsToCredentials).flat() // Get all the credentials associated with the input descriptors - const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) - .flat() + const credentialsForSubject = credentialRecordsForSubject + .map((credentialRecord) => credentialRecord.credential) .map(getSphereonOriginalVerifiableCredential) const presentationDefinitionForSubject: DifPresentationExchangeDefinition = { @@ -299,10 +320,16 @@ export class DifPresentationExchangeService { const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( presentationDefinitionForSubject, credentialsForSubject, - this.getPresentationSignCallback(agentContext, verificationMethod, format), + this.getPresentationSignCallback(agentContext, verificationMethod, format, credentialRecordsForSubject), { holderDID: subjectId, - proofOptions: { challenge, domain, nonce }, + proofOptions: { + challenge, + domain, + nonce, + typeSupportsSelectiveDisclosure: format === ClaimFormat.DiVp ? true : undefined, + type: format === ClaimFormat.DiVp ? 'DataIntegrityProof.anoncredsvc-2023' : undefined, + }, signatureOptions: { verificationMethod: verificationMethod?.id }, presentationSubmissionLocation: presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, @@ -470,16 +497,44 @@ export class DifPresentationExchangeService { return supportedSignatureSuite.proofType } + private getDataIntegritySignatureSuite( + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2 + ) { + const cryptoSuitesSatisfyingDefinition = presentationDefinition.format?.di_vc?.cryptosuite ?? [] + + const inputDescriptorCryptoSuites: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.di_vc?.cryptosuite ?? []) + .filter((alg) => alg.length > 0) + + const suitableCryptosuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + cryptoSuitesSatisfyingDefinition, + inputDescriptorCryptoSuites + ) + + if (!suitableCryptosuites) + throw new AriesFrameworkError('Could not find a suitable crypto suite for signing the presentation') + return suitableCryptosuites[0] + + // TODO: check against the supported signatures suites of the verification method key + } + public getPresentationSignCallback( agentContext: AgentContext, verificationMethod: VerificationMethod, - vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp + vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp | ClaimFormat.DiVp, + credentialRecordsForSubject: W3cCredentialRecord[] ) { const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) return async (callBackParams: PresentationSignCallBackParams) => { // The created partial proof and presentation, as well as original supplied options - const { presentation: presentationJson, options, presentationDefinition } = callBackParams + const { + presentation: presentationJson, + options, + presentationDefinition, + selectedCredentials, + presentationSubmission, + } = callBackParams const { challenge, domain, nonce } = options.proofOptions ?? {} const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} @@ -489,9 +544,27 @@ export class DifPresentationExchangeService { ) } - let signedPresentation: W3cVerifiablePresentation - if (vpFormat === 'jwt_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + if (vpFormat === ClaimFormat.DiVp) { + const cryptosuite = await this.getDataIntegritySignatureSuite(presentationDefinition) + + if (cryptosuite === 'anoncredsvc-2023' || cryptosuite === 'anoncredspresvc-2023') { + const anonCredsVcDataIntegrityService = + agentContext.dependencyManager.resolve( + anonCredsVcDataIntegrityServiceSymbol + ) + const presentation = await anonCredsVcDataIntegrityService.createPresentation(agentContext, { + presentationDefinition, + presentationSubmission, + selectedCredentials: selectedCredentials as JsonObject[], + selectedCredentialRecords: credentialRecordsForSubject, + }) + presentation.presentation_submission = presentationSubmission as unknown as JsonObject + return presentation as unknown as SphereonW3cVerifiablePresentation + } else { + throw new DifPresentationExchangeError(`Unsupported cryptosuite ${cryptosuite} for Data Integrity Proof`) + } + } else if (vpFormat === ClaimFormat.JwtVp) { + const presentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.JwtVp, alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), verificationMethod: verificationMethod.id, @@ -499,8 +572,9 @@ export class DifPresentationExchangeService { challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) - } else if (vpFormat === 'ldp_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + return getSphereonW3cVerifiablePresentation(presentation) + } else if (vpFormat === ClaimFormat.LdpVp) { + const presentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), proofPurpose: 'authentication', @@ -509,13 +583,12 @@ export class DifPresentationExchangeService { challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) + return getSphereonW3cVerifiablePresentation(presentation) } else { throw new DifPresentationExchangeError( `Only JWT credentials or JSONLD credentials are supported for a single presentation` ) } - - return getSphereonW3cVerifiablePresentation(signedPresentation) } } diff --git a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts index ec2e83d17e..ca336fa827 100644 --- a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts +++ b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts @@ -116,4 +116,4 @@ export interface DifPexCredentialsForRequestSubmissionEntry { /** * Mapping of selected credentials for an input descriptor */ -export type DifPexInputDescriptorToCredentials = Record> +export type DifPexInputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index 2d9c173d8c..872384c753 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -31,7 +31,7 @@ export async function getCredentialsForRequest( const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { holderDIDs, - // limitDisclosureSignatureSuites: [], + limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncredsvc-2023'], // restrictToDIDMethods, // restrictToFormats }) diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 3260e806d2..49f6d3b0a8 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -6,6 +6,7 @@ import type { } from './DifPresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' import type { JsonValue } from '../../../../types' +import type { AnonCredsVcDataIntegrityService } from '../../../credentials' import type { DifPexInputDescriptorToCredentials } from '../../../dif-presentation-exchange' import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' @@ -28,6 +29,7 @@ import type { import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' import { deepEquality, JsonTransformer } from '../../../../utils' +import { anonCredsVcDataIntegrityServiceSymbol } from '../../../credentials' import { DifPresentationExchangeService } from '../../../dif-presentation-exchange' import { W3cCredentialService, @@ -198,7 +200,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic requirements.forEach((r) => { r.submissionEntry.forEach((r) => { - credentials[r.inputDescriptorId] = r.verifiableCredentials.map((c) => c.credential) + credentials[r.inputDescriptorId] = r.verifiableCredentials }) }) } @@ -264,21 +266,80 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic let verificationResult: W3cVerifyPresentationResult + const descriptorMap = jsonPresentation.presentation_submission.descriptor_map + const uniqueFormats = Array.from(new Set(descriptorMap.map((descriptor) => descriptor.format))) + + if (uniqueFormats.length == 0) { + agentContext.config.logger.error('Received an invalid presentation submission with no specified format.') + return false + } + + if (uniqueFormats.length > 1) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format with multiple formats. This is not supported.' + ) + return false + } + + const format = uniqueFormats[0] + + if (format === ClaimFormat.DiVp && parsedPresentation.claimFormat === ClaimFormat.LdpVp) { + const uniqueCryptosuites = Array.from(new Set(descriptorMap.map((descriptor) => descriptor.cryptosuite))) + + if (uniqueCryptosuites.length == 0) { + agentContext.config.logger.error('Received an invalid presentation submission with no specified format.') + return false + } + + if (uniqueCryptosuites.length > 1) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format with multiple formats. This is not supported.' + ) + return false + } + + if (uniqueCryptosuites.includes('anoncredsvc-2023') || uniqueCryptosuites.includes('anoncredspresvc-2023')) { + const dataIntegrityService = agentContext.dependencyManager.resolve( + anonCredsVcDataIntegrityServiceSymbol + ) + + const proofVerificationResult = await dataIntegrityService.verifyPresentation(agentContext, { + presentation: parsedPresentation as W3cJsonLdVerifiablePresentation, + presentationDefinition: request.presentation_definition, + presentationSubmission: jsonPresentation.presentation_submission, + }) + + verificationResult = { + isValid: proofVerificationResult, + validations: {}, + error: { + name: 'DataIntegrityError', + message: 'Verifying the Data Integrity Proof failed. An unknown error occurred.', + }, + } + } else { + agentContext.config.logger.error(`Unsupported cryptosuites '${uniqueCryptosuites.join(', ')}'.`) + return false + } + } // FIXME: for some reason it won't accept the input if it doesn't know // whether it's a JWT or JSON-LD VP even though the input is the same. // Not sure how to fix - if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { + else if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { presentation: parsedPresentation, challenge: request.options.challenge, domain: request.options.domain, }) - } else { + } else if (parsedPresentation.claimFormat === ClaimFormat.LdpVp) { verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { presentation: parsedPresentation, challenge: request.options.challenge, domain: request.options.domain, }) + } else { + agentContext.config.logger.error(`Received presentation in PEX proof format with unsupported format ${format}.`) + return false } if (!verificationResult.isValid) { diff --git a/packages/core/src/modules/vc/models/ClaimFormat.ts b/packages/core/src/modules/vc/models/ClaimFormat.ts index f6c8cc909d..8577e088f6 100644 --- a/packages/core/src/modules/vc/models/ClaimFormat.ts +++ b/packages/core/src/modules/vc/models/ClaimFormat.ts @@ -9,4 +9,7 @@ export enum ClaimFormat { Ldp = 'ldp', LdpVc = 'ldp_vc', LdpVp = 'ldp_vp', + Di = 'di', + DiVc = 'di_vc', + DiVp = 'di_vp', } diff --git a/yarn.lock b/yarn.lock index 8da0d282eb..6e443e76a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1212,22 +1212,22 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@hyperledger/anoncreds-nodejs@^0.2.0-dev.7": - version "0.2.0-dev.7" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.7.tgz#5a69f1915eb2e1b8e5dc6782d35f2c27b6034ddc" - integrity sha512-CfeVgwhhl4+W/9AVqgErXOi3k4xDt1sEOGU+WOcWGBHQs5ONLUhj6sOS7/wQHECajQIWtmmC66ktynbRK0FySg== +"@hyperledger/anoncreds-nodejs@^0.2.0-dev.8": + version "0.2.0-dev.8" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.8.tgz#e0b3da6f67c5e723af3862e30f6af36856963c8a" + integrity sha512-pS8ox8daEv/natXBETAMVXtU+XWMb4RZBhIz1miWVk9nOis+XVfouBni1TrHCfhwdqBkEZYFaJ/i5gi2vDR5Bw== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/anoncreds-shared" "0.2.0-dev.7" + "@hyperledger/anoncreds-shared" "0.2.0-dev.8" "@mapbox/node-pre-gyp" "^1.0.11" ref-array-di "1.2.2" ref-struct-di "1.1.1" -"@hyperledger/anoncreds-shared@0.2.0-dev.7", "@hyperledger/anoncreds-shared@^0.2.0-dev.7": - version "0.2.0-dev.7" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.7.tgz#bd9d21448ea41bb2071a149d17606d1c3745314e" - integrity sha512-d+V2lD8kUlWzkS4oXD9GprPbQBNSd3XD2nQD3GIvlbpkpJU5d2H7nKqBWUbXYe+wpHK0H9boWgOC74gsFwxxUg== +"@hyperledger/anoncreds-shared@0.2.0-dev.8", "@hyperledger/anoncreds-shared@^0.2.0-dev.8": + version "0.2.0-dev.8" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.8.tgz#ef99f745216f069b95a019d6ca3b5f917669f481" + integrity sha512-AYwXq7ciKyUoAnSrSqSJhqRRYvRTvy57EkuMY0b4yKW3IphCSWCoos9YHz0zG+r1ud7rwJmxPRUy69ElxJ5FNQ== "@hyperledger/aries-askar-nodejs@^0.2.0-dev.5": version "0.2.0-dev.5" From c5477540a15376ef8a8873b17827729a2f480232 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 30 Jan 2024 16:42:28 +0100 Subject: [PATCH 07/38] fix: allow random presentation definition id string --- packages/anoncreds-rs/package.json | 1 + .../anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts | 8 +++++++- .../tests/fixtures/presentation-definition.ts | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json index 0fc559dd14..be842c1f86 100644 --- a/packages/anoncreds-rs/package.json +++ b/packages/anoncreds-rs/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@astronautlabs/jsonpath": "^1.1.2", + "big-integer": "^1.6.51", "@aries-framework/anoncreds": "0.4.2", "@aries-framework/core": "0.4.2", "class-transformer": "^0.5.1", diff --git a/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts b/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts index c1567bb752..6bb50a00d3 100644 --- a/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts +++ b/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts @@ -36,6 +36,8 @@ import { JsonTransformer, deepEquality, injectable, + Hasher, + TypedArrayEncoder, } from '@aries-framework/core' import { JSONPath } from '@astronautlabs/jsonpath' import { @@ -45,6 +47,7 @@ import { RevocationRegistryDefinition, RevocationStatusList, } from '@hyperledger/anoncreds-shared' +import BigNumber from 'bn.js' export interface CredentialWithMetadata { credential: JsonObject @@ -395,10 +398,13 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg const linkSecretIds = new Set() const credentialsWithMetadata: CredentialWithMetadata[] = [] + const hash = Hasher.hash(TypedArrayEncoder.fromString(presentationDefinition.id), 'sha2-256') + const nonce = new BigNumber(hash).toString().slice(0, 32) + const anonCredsProofRequest: AnonCredsProofRequest = { version: '1.0', name: presentationDefinition.name ?? 'Proof request', - nonce: presentationDefinition.id, + nonce, requested_attributes: {}, requested_predicates: {}, } diff --git a/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts b/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts index 32f143b7b4..927e582058 100644 --- a/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts +++ b/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts @@ -1,5 +1,5 @@ export const presentationDefinition = { - id: '12345', + id: '5591656f-5b5d-40f8-ab5c-9041c8e3a6a0', name: 'Age Verification', purpose: 'We need to verify your age before entering a bar', input_descriptors: [ From ded0462ffffba50a574439114c441555db4e6c1a Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 31 Jan 2024 11:17:21 +0100 Subject: [PATCH 08/38] feat: implement autorespond --- .../DataIntegrityCredentialFormatService.ts | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index 25faf5d84b..ce0c57f085 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -893,18 +893,54 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer // eslint-disable-next-line @typescript-eslint/no-unused-vars agentContext: AgentContext, // eslint-disable-next-line @typescript-eslint/no-unused-vars - input: CredentialFormatAutoRespondOfferOptions + { offerAttachment }: CredentialFormatAutoRespondOfferOptions ) { + const credentialOffer = offerAttachment.getDataAsJson() + if (!credentialOffer.binding_required) return true return false } public async shouldAutoRespondToRequest( // eslint-disable-next-line @typescript-eslint/no-unused-vars agentContext: AgentContext, - // eslint-disable-next-line @typescript-eslint/no-unused-vars { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions ) { - return false + const credentialOffer = offerAttachment.getDataAsJson() + const credentialRequest = requestAttachment.getDataAsJson() + + if ( + !credentialOffer.binding_required && + !credentialRequest.binding_proof?.anoncreds_link_secret && + !credentialRequest.binding_proof?.didcomm_signed_attachment + ) { + return true + } + + if ( + credentialOffer.binding_required && + !credentialRequest.binding_proof?.anoncreds_link_secret && + !credentialRequest.binding_proof?.didcomm_signed_attachment + ) { + return false + } + + // cannot auto response credential subject id must be set manually + const w3cCredential = JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential) + const credentialHasSubjectId = Array.isArray(w3cCredential.credentialSubject) ? false : !!w3cCredential.id + if (credentialRequest.binding_proof?.anoncreds_link_secret && !credentialHasSubjectId) { + return false + } + + const validLinkSecretRequest = + !credentialRequest.binding_proof?.anoncreds_link_secret || + (credentialRequest.binding_proof?.anoncreds_link_secret && credentialOffer.binding_method?.anoncreds_link_secret) + + const validDidCommSignedAttachmetRequest = + !credentialRequest.binding_proof?.didcomm_signed_attachment || + (credentialRequest.binding_proof?.didcomm_signed_attachment && + credentialOffer.binding_method?.didcomm_signed_attachment) + + return !!(validLinkSecretRequest && validDidCommSignedAttachmetRequest) } public async shouldAutoRespondToCredential( From fd6fe4faa2bf510b7b996eefbe6333ad68db95c0 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 31 Jan 2024 11:17:36 +0100 Subject: [PATCH 09/38] fix: todos --- .../DataIntegrityCredentialFormatService.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index ce0c57f085..77486434d7 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -665,8 +665,21 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credentialSubjectId: dataIntegrityFormat.credentialSubjectId, }) + if (Array.isArray(signedCredential.proof)) { + throw new AriesFrameworkError('Credential cannot have multiple proofs at this point') + } + + if ( + signedCredential.issuerId !== offeredCredential.issuerId || + !signedCredential.issuerId.startsWith(signedCredential.proof.verificationMethod) + ) { + throw new AriesFrameworkError('Invalid issuer in credential') + } + + if (offeredCredential.type.length !== 1 || offeredCredential.type[0] !== 'VerifiableCredential') { + throw new AriesFrameworkError('Offered Invalid credential type') + } // TODO: check if any non integrity protected fields were on the offered credential. If so throw - // TODO: assert issuer id is the same as the credential definition issuer id } if (credentialRequest.binding_proof?.didcomm_signed_attachment) { @@ -805,7 +818,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new AriesFrameworkError('Missing credential attributes on credential record.') } - // TODO: validate credential structure const { credential: credentialJson } = attachment.getDataAsJson() let anonCredsCredentialRecordOptions: AnonCredsCredentialRecordOptions | undefined @@ -832,7 +844,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ) } else { w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) - // TODO: check if the credentials contains a data integrity proof } const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) From 86f8e6a427e787882e446fb4d59340dbc499989b Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 31 Jan 2024 12:16:04 +0100 Subject: [PATCH 10/38] fix: remove presentation submission cryptosuite --- ...fPresentationExchangeProofFormatService.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 49f6d3b0a8..d1d75924e4 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -284,21 +284,14 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const format = uniqueFormats[0] if (format === ClaimFormat.DiVp && parsedPresentation.claimFormat === ClaimFormat.LdpVp) { - const uniqueCryptosuites = Array.from(new Set(descriptorMap.map((descriptor) => descriptor.cryptosuite))) - - if (uniqueCryptosuites.length == 0) { - agentContext.config.logger.error('Received an invalid presentation submission with no specified format.') - return false - } - - if (uniqueCryptosuites.length > 1) { - agentContext.config.logger.error( - 'Received presentation in PEX proof format with multiple formats. This is not supported.' - ) - return false + if (Array.isArray(parsedPresentation.proof)) + throw new AriesFrameworkError('Cannot process presentations with multiple proofs') + const cryptosuite = parsedPresentation.proof.cryptosuite + if (!cryptosuite) { + throw new AriesFrameworkError('Cannot process data integrity presentations without cryptosuites') } - if (uniqueCryptosuites.includes('anoncredsvc-2023') || uniqueCryptosuites.includes('anoncredspresvc-2023')) { + if (cryptosuite === 'anoncredsvc-2023' || cryptosuite === 'anoncredspresvc-2023') { const dataIntegrityService = agentContext.dependencyManager.resolve( anonCredsVcDataIntegrityServiceSymbol ) @@ -318,7 +311,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic }, } } else { - agentContext.config.logger.error(`Unsupported cryptosuites '${uniqueCryptosuites.join(', ')}'.`) + agentContext.config.logger.error(`Unsupported cryptosuite '${cryptosuite}'.`) return false } } From d5c38660653a6c2c579701b9e8d90970f2ee2875 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 1 Feb 2024 18:02:10 +0100 Subject: [PATCH 11/38] complete merge, and fix part's of the legacy identifier issues --- demo/src/Faber.ts | 7 +- .../src/AnonCredsVcDataIntegrityService.ts | 55 +-- .../src/services/AnonCredsRsHolderService.ts | 206 +++++++--- .../AnonCredsRsHolderService.test.ts | 38 +- .../__tests__/AnonCredsRsServices.test.ts | 18 +- .../src/services/__tests__/helpers.ts | 3 +- .../data-integrity-flow-anoncreds.test.ts | 14 +- .../tests/data-integrity-flow-w3c.test.ts | 8 +- .../tests/data-integrity-flow.test.ts | 8 +- .../tests/data-integrity.e2e.test.ts | 12 +- .../tests/fixtures/presentation-definition.ts | 4 +- packages/anoncreds-rs/tests/indy-flow.test.ts | 6 +- .../formats/AnonCredsProofFormatService.ts | 21 +- .../DataIntegrityCredentialFormatService.ts | 67 ++-- .../formats/LegacyIndyProofFormatService.ts | 46 +-- .../legacy-indy-format-services.test.ts | 36 +- packages/anoncreds/src/index.ts | 20 +- packages/anoncreds/src/models/exchange.ts | 2 +- packages/anoncreds/src/models/internal.ts | 2 +- .../services/AnonCredsHolderServiceOptions.ts | 13 +- .../w3cCredentialRecordMigration.test.ts | 4 +- .../0.4-0.5/anonCredsCredentialRecord.ts | 28 +- .../anoncreds/src/updates/0.4-0.5/index.ts | 2 +- packages/anoncreds/src/utils/index.ts | 20 +- packages/anoncreds/src/utils/ledgerObjects.ts | 361 +++++++++++++----- packages/anoncreds/src/utils/w3cUtils.ts | 25 +- .../tests/InMemoryAnonCredsRegistry.ts | 60 ++- packages/anoncreds/tests/anoncreds.test.ts | 4 +- packages/core/src/index.ts | 1 + ...fPresentationExchangeProofFormatService.ts | 6 +- .../vc/repository/W3cCredentialRecord.ts | 39 +- 31 files changed, 750 insertions(+), 386 deletions(-) diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index 84644fda52..bbf11148f0 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -15,8 +15,11 @@ import { W3cCredentialSubject, ProofEventTypes, ProofState, -} from '@crede-ts/core' -import { ConnectionEventTypes, KeyType, TypedArrayEncoder, utils } from '@credo-ts/core' + ConnectionEventTypes, + KeyType, + TypedArrayEncoder, + utils, +} from '@credo-ts/core' import { randomInt } from 'crypto' import { ui } from 'inquirer' diff --git a/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts b/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts index 6bb50a00d3..c96464c541 100644 --- a/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts +++ b/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts @@ -4,42 +4,43 @@ import type { AnonCredsProofRequest, AnonCredsRequestedPredicate, AnonCredsSchema, -} from '@aries-framework/anoncreds' +} from '@credo-ts/anoncreds' import type { AgentContext, AnonCredsVcDataIntegrityService, AnonCredsVcVerificationOptions, JsonObject, W3cCredentialRecord, -} from '@aries-framework/core' -import type { W3cCredentialEntry, CredentialProve, NonRevokedIntervalOverride } from '@hyperledger/anoncreds-shared' +} from '@credo-ts/core' +import type { CredentialProve, NonRevokedIntervalOverride, W3cCredentialEntry } from '@hyperledger/anoncreds-shared' import type { Descriptor, FieldV2, + InputDescriptorV1, + InputDescriptorV2, PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission, - InputDescriptorV2, - InputDescriptorV1, } from '@sphereon/pex-models' +import { JSONPath } from '@astronautlabs/jsonpath' import { AnonCredsLinkSecretRepository, AnonCredsModuleConfig, AnonCredsRegistryService, assertBestPracticeRevocationInterval, - fetchObjectsFromLedger, -} from '@aries-framework/anoncreds' + fetchCredentialDefinition, + fetchSchema, +} from '@credo-ts/anoncreds' import { - W3cJsonLdVerifiableCredential, AriesFrameworkError, + Hasher, JsonTransformer, + TypedArrayEncoder, + W3cJsonLdVerifiableCredential, deepEquality, injectable, - Hasher, - TypedArrayEncoder, -} from '@aries-framework/core' -import { JSONPath } from '@astronautlabs/jsonpath' +} from '@credo-ts/core' import { W3cCredential as AnonCredsW3cCredential, W3cPresentation as AnonCredsW3cPresentation, @@ -239,11 +240,9 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg schemaIds: Set | undefined, credentialDefinitionIds: Set ) { - const schemaFetchPromises = [...(schemaIds ?? [])].map((schemaId) => - fetchObjectsFromLedger(agentContext, { schemaId }) - ) + const schemaFetchPromises = [...(schemaIds ?? [])].map((schemaId) => fetchSchema(agentContext, schemaId)) const credentialDefinitionFetchPromises = [...credentialDefinitionIds].map((credentialDefinitionId) => - fetchObjectsFromLedger(agentContext, { credentialDefinitionId }) + fetchCredentialDefinition(agentContext, credentialDefinitionId) ) const schemas: Record = {} @@ -255,13 +254,9 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg ]) const credentialDefinitionFetchResults = results[1] - for (const res of credentialDefinitionFetchResults) { - const credentialDefinitionId = res.credentialDefinitionReturn.credentialDefinitionId - const credentialDefinition = res.credentialDefinitionReturn.credentialDefinition - if (!credentialDefinition) { - throw new AriesFrameworkError('Credential definition not found') - } - + for (const credentialDefinitionFetchResult of credentialDefinitionFetchResults) { + const credentialDefinitionId = credentialDefinitionFetchResult.id + const credentialDefinition = credentialDefinitionFetchResult.credentialDefinition credentialDefinitions[credentialDefinitionId] = credentialDefinition } @@ -269,20 +264,12 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg schemaFetchPromises.length > 0 ? results[0] : await Promise.all( - credentialDefinitionFetchResults.map((res) => - fetchObjectsFromLedger(agentContext, { - schemaId: res.credentialDefinitionReturn.credentialDefinition?.schemaId as string, - }) - ) + credentialDefinitionFetchResults.map((res) => fetchSchema(agentContext, res.credentialDefinition.schemaId)) ) for (const schemaFetchResult of schemaFetchResults) { - const schemaId = schemaFetchResult.schemaReturn.schemaId - const schema = schemaFetchResult.schemaReturn.schema - if (!schema) { - throw new AriesFrameworkError('Credential definition not found') - } - + const schemaId = schemaFetchResult.id + const schema = schemaFetchResult.schema schemas[schemaId] = schema } diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts index 7f83636fc9..62f139de81 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts @@ -1,4 +1,6 @@ import type { + AnonCredsCredential, + AnonCredsCredentialDefinition, AnonCredsCredentialInfo, AnonCredsCredentialRequest, AnonCredsCredentialRequestMetadata, @@ -7,6 +9,8 @@ import type { AnonCredsProofRequestRestriction, AnonCredsRequestedAttributeMatch, AnonCredsRequestedPredicateMatch, + AnonCredsRevocationRegistryDefinition, + AnonCredsSchema, CreateCredentialRequestOptions, CreateCredentialRequestReturn, CreateLinkSecretOptions, @@ -17,7 +21,6 @@ import type { GetCredentialsForProofRequestReturn, GetCredentialsOptions, StoreCredentialOptions, - StoreW3cCredentialOptions, } from '@credo-ts/anoncreds' import type { AgentContext, AnonCredsClaimRecord, Query, SimpleQuery, W3cCredentialRecord } from '@credo-ts/core' import type { @@ -27,26 +30,36 @@ import type { JsonObject, } from '@hyperledger/anoncreds-shared' -import { - AriesFrameworkError, - JsonTransformer, - TypedArrayEncoder, - W3cCredentialRepository, - W3cCredentialService, - W3cJsonLdVerifiableCredential, - injectable, - utils, -} from '@aries-framework/core' import { AnonCredsLinkSecretRepository, AnonCredsRegistryService, AnonCredsRestrictionWrapper, - fetchQualifiedIds, + fetchCredentialDefinition, + isQualifiedCredentialDefinition, legacyCredentialToW3cCredential, storeLinkSecret, unqualifiedCredentialDefinitionIdRegex, w3cToLegacyCredential, + getQualifiedCredentialDefinition, + getIndyNamespace, + isQualifiedRevocationRegistryDefinition, + getQualifiedRevocationRegistryDefinition, + isQualifiedSchema, + getQualifiedSchema, + isIndyDid, + getNonQualifiedId, } from '@credo-ts/anoncreds' +import { + AriesFrameworkError, + JsonTransformer, + TypedArrayEncoder, + W3cCredentialRepository, + W3cCredentialService, + W3cJsonLdVerifiableCredential, + injectable, + isDid, + utils, +} from '@credo-ts/core' import { W3cCredential as AW3cCredential, CredentialRequest, @@ -266,8 +279,16 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } - public async legacyToW3cCredential(agentContext: AgentContext, options: StoreCredentialOptions) { - const { credential, credentialDefinition, credentialRequestMetadata, revocationRegistry } = options + public async legacyToW3cCredential( + agentContext: AgentContext, + options: { + credential: AnonCredsCredential + credentialDefinition: AnonCredsCredentialDefinition + credentialRequestMetadata: AnonCredsCredentialRequestMetadata + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | undefined + } + ) { + const { credential, credentialRequestMetadata, revocationRegistryDefinition, credentialDefinition } = options const linkSecretRecord = await agentContext.dependencyManager .resolve(AnonCredsLinkSecretRepository) @@ -277,29 +298,33 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { throw new AnonCredsRsError('Link Secret value not stored') } - const w3cJsonLdCredential = await legacyCredentialToW3cCredential(agentContext, credential, { + const w3cJsonLdCredential = await legacyCredentialToW3cCredential(credential, credentialDefinition, { credentialDefinition: credentialDefinition as unknown as JsonObject, credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject, linkSecret: linkSecretRecord.value, - revocationRegistryDefinition: revocationRegistry?.definition as unknown as JsonObject, + revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, }) return w3cJsonLdCredential } - public async storeW3cCredential(agentContext: AgentContext, options: StoreW3cCredentialOptions): Promise { - const { - credential, - credentialRequestMetadata, - schema, - revocationRegistry, - credentialDefinition, - credentialDefinitionId: credDefId, - } = options + public async storeW3cCredential( + agentContext: AgentContext, + options: { + credentialId?: string + credential: W3cJsonLdVerifiableCredential + credentialDefinitionId: string + schema: AnonCredsSchema + credentialDefinition: AnonCredsCredentialDefinition + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + credentialRequestMetadata: AnonCredsCredentialRequestMetadata + } + ): Promise { + const { credential, credentialRequestMetadata, schema, credentialDefinition, credentialDefinitionId } = options const methodName = agentContext.dependencyManager .resolve(AnonCredsRegistryService) - .getRegistryForIdentifier(agentContext, credDefId).methodName + .getRegistryForIdentifier(agentContext, credential.issuerId).methodName const linkSecretRecord = await agentContext.dependencyManager .resolve(AnonCredsLinkSecretRepository) @@ -309,20 +334,14 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { throw new AnonCredsRsError('Link Secret value not stored') } - const { schemaId, schemaIssuerId, revocationRegistryId, credentialDefinitionId } = await fetchQualifiedIds( - agentContext, - { - schemaId: credentialDefinition.schemaId, - schemaIssuerId: schema.issuerId, - revocationRegistryId: revocationRegistry?.id, - credentialDefinitionId: credDefId, - } + const { revocationRegistryId, revocationRegistryIndex } = AW3cCredential.fromJson( + JsonTransformer.toJSON(credential) ) - const credentialRevocationId = AW3cCredential.fromJson(JsonTransformer.toJSON(credential)).revocationRegistryIndex - const credentialId = options.credentialId ?? utils.uuid() + const indyDid = isIndyDid(credentialDefinitionId) + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) await w3cCredentialService.storeCredential(agentContext, { credential, @@ -330,13 +349,22 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { credentialId, linkSecretId: linkSecretRecord.linkSecretId, credentialDefinitionId, - schemaId, + schemaId: credentialDefinition.schemaId, schemaName: schema.name, - schemaIssuerId, + schemaIssuerId: schema.issuerId, schemaVersion: schema.version, methodName, - revocationRegistryId: revocationRegistryId, - credentialRevocationId: credentialRevocationId?.toString(), + revocationRegistryId, + credentialRevocationId: revocationRegistryIndex?.toString(), + unqualifiedTags: indyDid + ? { + issuerId: getNonQualifiedId(credential.issuerId), + credentialDefinitionId: getNonQualifiedId(credentialDefinitionId), + schemaId: getNonQualifiedId(credentialDefinition.schemaId), + schemaIssuerId: getNonQualifiedId(schema.issuerId), + revocationRegistryId: revocationRegistryId ? getNonQualifiedId(revocationRegistryId) : undefined, + } + : undefined, }, }) @@ -345,11 +373,59 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { // convert legacy to w3c and call store w3c public async storeCredential(agentContext: AgentContext, options: StoreCredentialOptions): Promise { - const w3cJsonLdCredential = await this.legacyToW3cCredential(agentContext, options) + const { + credentialId, + credential, + credentialDefinition, + credentialDefinitionId, + credentialRequestMetadata, + schema, + revocationRegistry, + } = options + + let qualifiedCredentialDefinitionId: string + if (isDid(credentialDefinitionId)) { + qualifiedCredentialDefinitionId = credentialDefinitionId + } else { + const result = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + qualifiedCredentialDefinitionId = result.qualifiedId + } + + const qualifiedSchema = isQualifiedSchema(schema) + ? schema + : getQualifiedSchema(schema, getIndyNamespace(qualifiedCredentialDefinitionId)) + + const qualifiedCredentialDefinition = isQualifiedCredentialDefinition(credentialDefinition) + ? credentialDefinition + : getQualifiedCredentialDefinition(credentialDefinition, getIndyNamespace(qualifiedCredentialDefinitionId)) + + const qualifiedRevocationRegistryDefinition = !revocationRegistry?.definition + ? undefined + : isQualifiedRevocationRegistryDefinition(revocationRegistry.definition) + ? revocationRegistry.definition + : getQualifiedRevocationRegistryDefinition( + revocationRegistry.definition, + getIndyNamespace(qualifiedCredentialDefinitionId) + ) + + const w3cJsonLdCredential = + credential instanceof W3cJsonLdVerifiableCredential + ? credential + : await this.legacyToW3cCredential(agentContext, { + credential, + credentialDefinition: qualifiedCredentialDefinition, + credentialRequestMetadata, + revocationRegistryDefinition: qualifiedRevocationRegistryDefinition, + }) return await this.storeW3cCredential(agentContext, { - ...options, + credentialId, + credentialRequestMetadata, credential: w3cJsonLdCredential, + credentialDefinitionId: qualifiedCredentialDefinitionId, + schema: qualifiedSchema, + credentialDefinition: qualifiedCredentialDefinition, + revocationRegistryDefinition: qualifiedRevocationRegistryDefinition, }) } @@ -389,16 +465,28 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { agentContext: AgentContext, options: GetCredentialsOptions ): Promise { + const getIfQualifiedId = (id: string | undefined) => { + return !id ? undefined : isDid(id) ? id : undefined + } + + const getIfUnqualifiedId = (id: string | undefined) => { + return !id ? undefined : isDid(id) ? undefined : id + } + const credentialRecords = await agentContext.dependencyManager .resolve(W3cCredentialRepository) .findByQuery(agentContext, { - credentialDefinitionId: options.credentialDefinitionId, - schemaId: options.schemaId, - issuerId: options.issuerId, + credentialDefinitionId: getIfQualifiedId(options.credentialDefinitionId), + schemaId: getIfQualifiedId(options.schemaId), + issuerId: getIfQualifiedId(options.issuerId), schemaName: options.schemaName, schemaVersion: options.schemaVersion, - schemaIssuerId: options.schemaIssuerId, + schemaIssuerId: getIfQualifiedId(options.schemaIssuerId), methodName: options.methodName, + unqualifiedSchemaId: getIfUnqualifiedId(options.schemaId), + unqualifiedIssuerId: getIfUnqualifiedId(options.issuerId), + unqualifiedSchemaIssuerId: getIfUnqualifiedId(options.schemaIssuerId), + unqualifiedCredentialDefinitionId: getIfUnqualifiedId(options.credentialDefinitionId), }) return credentialRecords.map((credentialRecord) => this.anoncredsMetadataFromRecord(credentialRecord)) @@ -470,19 +558,37 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const queryElements: SimpleQuery = {} if (restriction.credentialDefinitionId) { - queryElements.credentialDefinitionId = restriction.credentialDefinitionId + if (isDid(restriction.credentialDefinitionId)) { + queryElements.credentialDefinitionId = restriction.credentialDefinitionId + } else { + queryElements.unqualifiedCredentialDefinitionId = restriction.credentialDefinitionId + } } if (restriction.issuerId || restriction.issuerDid) { - queryElements.issuerId = restriction.issuerId ?? restriction.issuerDid + const issuerId = (restriction.issuerId ?? restriction.issuerDid) as string + if (isDid(issuerId)) { + queryElements.issuerId = issuerId + } else { + queryElements.unqualifiedIssuerId = issuerId + } } if (restriction.schemaId) { - queryElements.schemaId = restriction.schemaId + if (isDid(restriction.schemaId)) { + queryElements.schemaId = restriction.schemaId + } else { + queryElements.unqualifiedSchemaId = restriction.schemaId + } } if (restriction.schemaIssuerId || restriction.schemaIssuerDid) { - queryElements.schemaIssuerId = restriction.schemaIssuerId ?? restriction.issuerDid + const issuerId = (restriction.schemaIssuerId ?? restriction.schemaIssuerDid) as string + if (isDid(issuerId)) { + queryElements.schemaIssuerId = issuerId + } else { + queryElements.unqualifiedSchemaIssuerId = issuerId + } } if (restriction.schemaName) { diff --git a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts index 2ea5a77cc6..81593e2aeb 100644 --- a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts +++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsHolderService.test.ts @@ -261,7 +261,7 @@ describe('AnonCredsRsHolderService', () => { methodName: 'inMemory', schemaId: personCredentialInfo.schemaId, credentialDefinitionId: personCredentialInfo.credentialDefinitionId, - revocationRegistryId: personCredentialInfo.revocationRegistryId, + revocationRegistryId: personCredentialInfo.revocationRegistryId ?? undefined, }, }) ) @@ -278,7 +278,7 @@ describe('AnonCredsRsHolderService', () => { methodName: 'inMemory', schemaId: phoneCredentialInfo.schemaId, credentialDefinitionId: phoneCredentialInfo.credentialDefinitionId, - revocationRegistryId: phoneCredentialInfo.revocationRegistryId, + revocationRegistryId: phoneCredentialInfo.revocationRegistryId ?? undefined, }, }) ) @@ -381,7 +381,7 @@ describe('AnonCredsRsHolderService', () => { 'attr::name::marker': true, }, { - issuerId: 'issuer:uri', + unqualifiedIssuerId: 'issuer:uri', }, ], }) @@ -414,7 +414,7 @@ describe('AnonCredsRsHolderService', () => { 'attr::age::marker': true, }, { - $or: [{ schemaId: 'schemaid:uri', schemaName: 'schemaName' }, { schemaVersion: '1.0' }], + $or: [{ unqualifiedSchemaId: 'schemaid:uri', schemaName: 'schemaName' }, { schemaVersion: '1.0' }], }, ], }) @@ -433,8 +433,8 @@ describe('AnonCredsRsHolderService', () => { 'attr::height::marker': true, }, { - credentialDefinitionId: 'crededefid:uri', - issuerId: 'issuerid:uri', + unqualifiedCredentialDefinitionId: 'crededefid:uri', + unqualifiedIssuerId: 'issuerid:uri', }, ], }) @@ -593,12 +593,12 @@ describe('AnonCredsRsHolderService', () => { }) expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { - credentialDefinitionId: 'credDefId', - schemaId: 'schemaId', - schemaIssuerId: 'schemaIssuerDid', + unqualifiedCredentialDefinitionId: 'credDefId', + unqualifiedSchemaId: 'schemaId', + unqualifiedSchemaIssuerId: 'schemaIssuerDid', + unqualifiedIssuerId: 'issuerDid', schemaName: 'schemaName', schemaVersion: 'schemaVersion', - issuerId: 'issuerDid', methodName: 'inMemory', }) expect(credentialInfo).toMatchObject([ @@ -627,7 +627,7 @@ describe('AnonCredsRsHolderService', () => { const schema: AnonCredsSchema = { attrNames: ['name', 'sex', 'height', 'age'], - issuerId: 'did:indy:example:issuerId', + issuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', name: 'schemaName', version: '1', } @@ -640,29 +640,29 @@ describe('AnonCredsRsHolderService', () => { age: '19', }, credentialDefinition: credentialDefinition as unknown as JsonObject, - schemaId: 'personschema:uri', - credentialDefinitionId: 'personcreddef:uri', + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/credentialDefinition-name/1.0', + credentialDefinitionId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default', credentialDefinitionPrivate, keyCorrectnessProof, linkSecret, linkSecretId: 'linkSecretId', credentialId: 'personCredId', - revocationRegistryDefinitionId: 'personrevregid:uri', + revocationRegistryDefinitionId: 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag', }) const saveCredentialMock = jest.spyOn(w3cCredentialRepositoryMock, 'save') saveCredentialMock.mockResolvedValue() - const credentialId = await anonCredsHolderService.storeW3cCredential(agentContext, { + const credentialId = await anonCredsHolderService.storeCredential(agentContext, { credential, credentialDefinition, - schema, - credentialDefinitionId: 'personcreddefid:uri', + credentialDefinitionId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default', credentialRequestMetadata: credentialRequestMetadata.toJson() as unknown as AnonCredsCredentialRequestMetadata, credentialId: 'personCredId', + schema, revocationRegistry: { - id: 'personrevregid:uri', + id: 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag', definition: new RevocationRegistryDefinition( revocationRegistryDefinition.handle ).toJson() as unknown as AnonCredsRevocationRegistryDefinition, @@ -680,7 +680,7 @@ describe('AnonCredsRsHolderService', () => { // The stored credential is different from the one received originally _tags: expect.objectContaining({ schemaName: 'schemaName', - schemaIssuerId: 'did:indy:example:issuerId', + schemaIssuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', schemaVersion: '1', }), }) diff --git a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts index cd255c1bc4..c5af8947b4 100644 --- a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts +++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts @@ -15,6 +15,12 @@ import { AnonCredsSchemaRecord, AnonCredsSchemaRepository, AnonCredsVerifierServiceSymbol, + getUnqualifiedCredentialDefinitionId, + getUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, + parseIndySchemaId, +} from '@credo-ts/anoncreds' +import { ConsoleLogger, DidResolverService, DidsModuleConfig, @@ -26,11 +32,7 @@ import { VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialsModuleConfig, encodeCredentialValue, - getUnqualifiedCredentialDefinitionId, - getUnqualifiedSchemaId, - parseIndyCredentialDefinitionId, - parseIndySchemaId, -} from '@credo-ts/anoncreds' +} from '@credo-ts/core' import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { Subject } from 'rxjs' @@ -422,11 +424,11 @@ describe('AnonCredsRsServices', () => { expect(credentialInfo).toEqual({ credentialId, attributes: { - age: '25', + age: 25, name: 'John', }, - schemaId: unqualifiedSchemaId, - credentialDefinitionId: unqualifiedCredentialDefinitionId, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: null, credentialRevocationId: null, methodName: 'inMemory', diff --git a/packages/anoncreds-rs/src/services/__tests__/helpers.ts b/packages/anoncreds-rs/src/services/__tests__/helpers.ts index 009a3d150b..293df8f3a6 100644 --- a/packages/anoncreds-rs/src/services/__tests__/helpers.ts +++ b/packages/anoncreds-rs/src/services/__tests__/helpers.ts @@ -5,8 +5,7 @@ import type { } from '@credo-ts/anoncreds' import type { JsonObject } from '@hyperledger/anoncreds-shared' -import {} from '@aries-framework/anoncreds' -import { JsonTransformer, W3cJsonLdVerifiableCredential } from '@aries-framework/core' +import { JsonTransformer, W3cJsonLdVerifiableCredential } from '@credo-ts/core' import { CredentialDefinition, CredentialOffer, diff --git a/packages/anoncreds-rs/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds-rs/tests/data-integrity-flow-anoncreds.test.ts index 531c1e4d3d..49f6c4571c 100644 --- a/packages/anoncreds-rs/tests/data-integrity-flow-anoncreds.test.ts +++ b/packages/anoncreds-rs/tests/data-integrity-flow-anoncreds.test.ts @@ -1,4 +1,4 @@ -import type { DataIntegrityCredentialRequest } from '@aries-framework/core' +import type { DataIntegrityCredentialRequest } from '@credo-ts/core' import { AnonCredsCredentialDefinitionPrivateRecord, @@ -21,7 +21,7 @@ import { AnonCredsSchemaRecord, AnonCredsSchemaRepository, AnonCredsVerifierServiceSymbol, -} from '@aries-framework/anoncreds' +} from '@credo-ts/anoncreds' import { AgentContext, CredentialExchangeRecord, @@ -44,7 +44,7 @@ import { W3cCredentialService, W3cCredentialSubject, W3cCredentialsModuleConfig, -} from '@aries-framework/core' +} from '@credo-ts/core' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' @@ -134,7 +134,7 @@ describe('data integrity format service (anoncreds)', () => { }) afterEach(async () => { - inMemoryStorageService.records = {} + inMemoryStorageService.contextCorrelationIdToRecords = {} }) test('issuance and verification flow anoncreds starting from offer without negotiation and without revocation', async () => { @@ -281,7 +281,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean options: {}, }) - if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { + if (!revocationStatusListState.revocationStatusList) { throw new Error('Failed to create revocation status list') } } @@ -417,8 +417,8 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean }, schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: revocable ? revocationRegistryDefinitionId : undefined, - credentialRevocationId: revocable ? '1' : undefined, + revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, + credentialRevocationId: revocable ? '1' : null, methodName: 'inMemory', }) diff --git a/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts index f79e780fa5..a86ebd77f3 100644 --- a/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts +++ b/packages/anoncreds-rs/tests/data-integrity-flow-w3c.test.ts @@ -1,11 +1,11 @@ -import type { KeyDidCreateOptions } from '@aries-framework/core' +import type { KeyDidCreateOptions } from '@credo-ts/core' import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, AnonCredsModuleConfig, AnonCredsVerifierServiceSymbol, -} from '@aries-framework/anoncreds' +} from '@credo-ts/anoncreds' import { AgentContext, CredentialExchangeRecord, @@ -29,7 +29,7 @@ import { W3cCredentialService, W3cCredentialSubject, W3cCredentialsModuleConfig, -} from '@aries-framework/core' +} from '@credo-ts/core' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' @@ -122,7 +122,7 @@ describe('data integrity format service (w3c)', () => { }) afterEach(async () => { - inMemoryStorageService.records = {} + inMemoryStorageService.contextCorrelationIdToRecords = {} }) test('issuance and verification flow w3c starting from offer without negotiation and without revocation', async () => { diff --git a/packages/anoncreds-rs/tests/data-integrity-flow.test.ts b/packages/anoncreds-rs/tests/data-integrity-flow.test.ts index 856ceb9a19..5de87e07d9 100644 --- a/packages/anoncreds-rs/tests/data-integrity-flow.test.ts +++ b/packages/anoncreds-rs/tests/data-integrity-flow.test.ts @@ -1,11 +1,11 @@ -import type { KeyDidCreateOptions } from '@aries-framework/core' +import type { KeyDidCreateOptions } from '@credo-ts/core' import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, AnonCredsModuleConfig, AnonCredsVerifierServiceSymbol, -} from '@aries-framework/anoncreds' +} from '@credo-ts/anoncreds' import { AgentContext, CredentialExchangeRecord, @@ -29,7 +29,7 @@ import { W3cCredentialService, W3cCredentialSubject, W3cCredentialsModuleConfig, -} from '@aries-framework/core' +} from '@credo-ts/core' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' @@ -122,7 +122,7 @@ describe('data integrity format service (w3c)', () => { }) afterEach(async () => { - inMemoryStorageService.records = {} + inMemoryStorageService.contextCorrelationIdToRecords = {} }) test('issuance and verification flow w3c starting from offer without negotiation and without revocation', async () => { diff --git a/packages/anoncreds-rs/tests/data-integrity.e2e.test.ts b/packages/anoncreds-rs/tests/data-integrity.e2e.test.ts index b545961c2f..b0dd0eec71 100644 --- a/packages/anoncreds-rs/tests/data-integrity.e2e.test.ts +++ b/packages/anoncreds-rs/tests/data-integrity.e2e.test.ts @@ -1,6 +1,6 @@ import type { AnonCredsTestsAgent } from './anoncredsSetup' -import type { AgentContext, KeyDidCreateOptions } from '@aries-framework/core' -import type { EventReplaySubject } from '@aries-framework/core/tests' +import type { AgentContext, KeyDidCreateOptions } from '@credo-ts/core' +import type { EventReplaySubject } from '@credo-ts/core/tests' import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' import { @@ -15,7 +15,7 @@ import { W3cCredential, W3cCredentialService, W3cCredentialSubject, -} from '@aries-framework/core' +} from '@credo-ts/core' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { waitForCredentialRecordSubject, waitForProofExchangeRecord } from '../../core/tests/helpers' @@ -50,7 +50,7 @@ describe('data anoncreds w3c data integrity e2e tests', () => { let credentialDefinitionId: string let issuerHolderConnectionId: string let holderIssuerConnectionId: string - let revocationRegistryDefinitionId: string | undefined + let revocationRegistryDefinitionId: string | null let issuerReplay: EventReplaySubject let holderReplay: EventReplaySubject @@ -132,7 +132,7 @@ async function anonCredsFlowTest(options: { holderIssuerConnectionId: string issuerReplay: EventReplaySubject holderReplay: EventReplaySubject - revocationRegistryDefinitionId: string | undefined + revocationRegistryDefinitionId: string | null credentialDefinitionId: string }) { const { @@ -178,7 +178,7 @@ async function anonCredsFlowTest(options: { credential, anonCredsLinkSecretBindingMethodOptions: { credentialDefinitionId, - revocationRegistryDefinitionId, + revocationRegistryDefinitionId: revocationRegistryDefinitionId ?? undefined, revocationRegistryIndex: revocationRegistryDefinitionId ? 1 : undefined, }, didCommSignedAttachmentBindingMethodOptions: {}, diff --git a/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts b/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts index 927e582058..1a4b8fe674 100644 --- a/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts +++ b/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts @@ -1,4 +1,6 @@ -export const presentationDefinition = { +import type { PresentationDefinitionV1 } from '@sphereon/pex-models' + +export const presentationDefinition: PresentationDefinitionV1 = { id: '5591656f-5b5d-40f8-ab5c-9041c8e3a6a0', name: 'Age Verification', purpose: 'We need to verify your age before entering a bar', diff --git a/packages/anoncreds-rs/tests/indy-flow.test.ts b/packages/anoncreds-rs/tests/indy-flow.test.ts index ef9242a0a9..50ead543a7 100644 --- a/packages/anoncreds-rs/tests/indy-flow.test.ts +++ b/packages/anoncreds-rs/tests/indy-flow.test.ts @@ -304,11 +304,11 @@ describe('Legacy indy format services using anoncreds-rs', () => { expect(anonCredsCredential).toEqual({ credentialId, attributes: { - age: '25', + age: 25, name: 'John', }, - schemaId: unqualifiedSchemaId, - credentialDefinitionId: unqualifiedCredentialDefinitionId, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: null, credentialRevocationId: null, methodName: 'inMemory', diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index 2df5fe784b..e9d7449c7b 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -57,7 +57,8 @@ import { assertNoDuplicateGroupsNamesInProofRequest, getRevocationRegistriesForRequest, getRevocationRegistriesForProof, - fetchObjectsFromLedger, + fetchSchema, + fetchCredentialDefinition, } from '../utils' import { dateToTimestamp } from '../utils/timestamp' @@ -465,12 +466,8 @@ export class AnonCredsProofFormatService implements ProofFormatService(AnonCredsHolderServiceSymbol) const credentialDefinitionId = credentialOffer.binding_method.anoncreds_link_secret.cred_def_id - const { credentialDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { credentialDefinitionId }) - if (!credentialDefinitionReturn.credentialDefinition) { - throw new AnonCredsError(`Unable to retrieve credential definition with id ${credentialDefinitionId}`) - } + const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, credentialDefinitionId) const { credentialRequest: anonCredsCredentialRequest, @@ -526,7 +528,12 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer revocationStatusList, }) - return await legacyCredentialToW3cCredential(agentContext, credential) + const { credentialDefinition: anoncredsCredentialDefinition } = await fetchCredentialDefinition( + agentContext, + credential.cred_def_id + ) + + return await legacyCredentialToW3cCredential(credential, anoncredsCredentialDefinition) } private async getSignatureMetadata(agentContext: AgentContext, offeredCredential: W3cCredential, issuerKid?: string) { @@ -665,13 +672,14 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credentialSubjectId: dataIntegrityFormat.credentialSubjectId, }) - if (Array.isArray(signedCredential.proof)) { + const proofs = Array.isArray(signedCredential.proof) ? signedCredential.proof : [signedCredential.proof] + if (proofs.length > 1) { throw new AriesFrameworkError('Credential cannot have multiple proofs at this point') } if ( signedCredential.issuerId !== offeredCredential.issuerId || - !signedCredential.issuerId.startsWith(signedCredential.proof.verificationMethod) + !proofs[0].verificationMethod.startsWith(signedCredential.issuerId) ) { throw new AriesFrameworkError('Invalid issuer in credential') } @@ -732,24 +740,15 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const aCredential = AW3cCredential.fromJson(credentialJson) const { schemaId, credentialDefinitionId, revocationRegistryId, revocationRegistryIndex } = aCredential.toLegacy() - const { schemaReturn, credentialDefinitionReturn, revocationRegistryDefinitionReturn } = - await fetchObjectsFromLedger(agentContext, { - schemaId, - credentialDefinitionId, - revocationRegistryId: revocationRegistryId as string | undefined, - }) - if (!schemaReturn.schema) throw new AriesFrameworkError('Schema not found.') - if (!credentialDefinitionReturn.credentialDefinition) { - throw new AriesFrameworkError('Credential definition not found.') - } - - if (revocationRegistryId && !revocationRegistryDefinitionReturn?.revocationRegistryDefinition) { - throw new AriesFrameworkError('Revoaction Registry definition not found.') - } + const schemaReturn = await fetchSchema(agentContext, schemaId) + const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + const revocationRegistryDefinitionReturn = revocationRegistryId + ? await fetchRevocationRegistryDefinition(agentContext, revocationRegistryId) + : undefined const methodName = agentContext.dependencyManager .resolve(AnonCredsRegistryService) - .getRegistryForIdentifier(agentContext, credentialDefinitionReturn.credentialDefinitionId).methodName + .getRegistryForIdentifier(agentContext, credentialDefinitionReturn.id).methodName const linkSecretRecord = await agentContext.dependencyManager .resolve(AnonCredsLinkSecretRepository) @@ -768,13 +767,13 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const anonCredsCredentialRecordOptions = { credentialId: utils.uuid(), linkSecretId: linkSecretRecord.linkSecretId, - credentialDefinitionId: credentialDefinitionReturn.credentialDefinitionId, - schemaId: schemaReturn.schemaId, + credentialDefinitionId: credentialDefinitionReturn.id, + schemaId: schemaReturn.id, schemaName: schemaReturn.schema.name, schemaIssuerId: schemaReturn.schema.issuerId, schemaVersion: schemaReturn.schema.version, methodName, - revocationRegistryId: revocationRegistryDefinitionReturn?.revocationRegistryDefinitionId, + revocationRegistryId: revocationRegistryDefinitionReturn?.id, credentialRevocationId: revocationRegistryIndex?.toString(), } @@ -783,8 +782,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const metadata = credentialRecord.metadata.get(DataIntegrityMetadataKey) if (!metadata?.linkSecretMetadata) throw new AriesFrameworkError('Missing link secret metadata') - metadata.linkSecretMetadata.revocationRegistryId = - revocationRegistryDefinitionReturn?.revocationRegistryDefinitionId + metadata.linkSecretMetadata.revocationRegistryId = revocationRegistryDefinitionReturn?.id metadata.linkSecretMetadata.credentialRevocationId = revocationRegistryIndex?.toString() credentialRecord.metadata.set(DataIntegrityMetadataKey, metadata) } @@ -1096,12 +1094,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ) { const attributes = this.previewAttributesFromCredential(credential) - const { schemaReturn } = await fetchObjectsFromLedger(agentContext, { schemaId }) - if (!schemaReturn.schema) { - throw new AriesFrameworkError( - `Unable to resolve schema ${schemaId} from registry: ${schemaReturn.resolutionMetadata.error} ${schemaReturn.resolutionMetadata.message}` - ) - } + const schemaReturn = await fetchSchema(agentContext, schemaId) const enhancedAttributes = [...attributes] if ( diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts index 29070be6ab..418b358e05 100644 --- a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts @@ -57,8 +57,10 @@ import { assertNoDuplicateGroupsNamesInProofRequest, getRevocationRegistriesForRequest, getRevocationRegistriesForProof, + fetchSchema, + fetchCredentialDefinition, } from '../utils' -import { isUnqualifiedCredentialDefinitionId, isUnqualifiedSchemaId } from '../utils/indyIdentifiers' +import { getUnQualifiedId } from '../utils/ledgerObjects' import { dateToTimestamp } from '../utils/timestamp' const V2_INDY_PRESENTATION_PROPOSAL = 'hlindy/proof-req@v2.0' @@ -463,23 +465,11 @@ export class LegacyIndyProofFormatService implements ProofFormatService) { - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - const schemas: { [key: string]: AnonCredsSchema } = {} for (const schemaId of schemaIds) { - if (!isUnqualifiedSchemaId(schemaId)) { - throw new AriesFrameworkError(`${schemaId} is not a valid legacy indy schema id`) - } - - const schemaRegistry = registryService.getRegistryForIdentifier(agentContext, schemaId) - const schemaResult = await schemaRegistry.getSchema(agentContext, schemaId) - - if (!schemaResult.schema) { - throw new AriesFrameworkError(`Schema not found for id ${schemaId}: ${schemaResult.resolutionMetadata.message}`) - } - - schemas[schemaId] = schemaResult.schema + const schemaResult = await fetchSchema(agentContext, schemaId) + schemas[getUnQualifiedId(schemaId)] = schemaResult.unqualifiedSchema } return schemas @@ -495,32 +485,12 @@ export class LegacyIndyProofFormatService implements ProofFormatService) { - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - const credentialDefinitions: { [key: string]: AnonCredsCredentialDefinition } = {} for (const credentialDefinitionId of credentialDefinitionIds) { - if (!isUnqualifiedCredentialDefinitionId(credentialDefinitionId)) { - throw new AriesFrameworkError(`${credentialDefinitionId} is not a valid legacy indy credential definition id`) - } - - const credentialDefinitionRegistry = registryService.getRegistryForIdentifier( - agentContext, - credentialDefinitionId - ) - - const credentialDefinitionResult = await credentialDefinitionRegistry.getCredentialDefinition( - agentContext, - credentialDefinitionId - ) - - if (!credentialDefinitionResult.credentialDefinition) { - throw new AriesFrameworkError( - `Credential definition not found for id ${credentialDefinitionId}: ${credentialDefinitionResult.resolutionMetadata.message}` - ) - } - - credentialDefinitions[credentialDefinitionId] = credentialDefinitionResult.credentialDefinition + const credentialDefinitionResult = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + credentialDefinitions[getUnQualifiedId(credentialDefinitionId)] = + credentialDefinitionResult.unqualifiedCredentialDefinition } return credentialDefinitions diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts index ecda92dcb8..d6649db7e0 100644 --- a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts +++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts @@ -9,6 +9,14 @@ import { ProofState, EventEmitter, InjectionSymbols, + Ed25519Signature2018, + SignatureSuiteToken, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + W3cCredentialsModuleConfig, + ConsoleLogger, + DidResolverService, + DidsModuleConfig, } from '@credo-ts/core' import { Subject } from 'rxjs' @@ -73,13 +81,22 @@ const anonCredsCredentialDefinitionPrivateRepository = new AnonCredsCredentialDe storageService, eventEmitter ) + +const logger = new ConsoleLogger() +const inMemoryStorageService = new InMemoryStorageService() const anonCredsCredentialRepository = new AnonCredsCredentialRepository(storageService, eventEmitter) const anonCredsKeyCorrectnessProofRepository = new AnonCredsKeyCorrectnessProofRepository(storageService, eventEmitter) const agentContext = getAgentContext({ registerInstances: [ + [InjectionSymbols.Stop$, new Subject()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], + [InjectionSymbols.StorageService, inMemoryStorageService], + [InjectionSymbols.Logger, logger], [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], + [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [AnonCredsLinkSecretRepository, anonCredsLinkSecretRepository], @@ -87,6 +104,7 @@ const agentContext = getAgentContext({ [AnonCredsCredentialDefinitionPrivateRepository, anonCredsCredentialDefinitionPrivateRepository], [AnonCredsCredentialRepository, anonCredsCredentialRepository], [AnonCredsKeyCorrectnessProofRepository, anonCredsKeyCorrectnessProofRepository], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], [InjectionSymbols.StorageService, storageService], [ AnonCredsRsModuleConfig, @@ -95,6 +113,18 @@ const agentContext = getAgentContext({ autoCreateLinkSecret: false, }), ], + [ + SignatureSuiteToken, + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + verificationMethodTypes: [ + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + ], + keyTypes: [KeyType.Ed25519], + }, + ], ], agentConfig, wallet, @@ -295,11 +325,11 @@ describe('Legacy indy format services', () => { expect(anonCredsCredential).toEqual({ credentialId, attributes: { - age: '25', + age: 25, name: 'John', }, - schemaId: legacySchemaId, - credentialDefinitionId: legacyCredentialDefinitionId, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: null, credentialRevocationId: null, methodName: 'inMemory', diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index 8017142e61..0b44c85e2a 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -16,5 +16,23 @@ export * from './utils/indyIdentifiers' export { assertBestPracticeRevocationInterval } from './utils/revocationInterval' export { storeLinkSecret } from './utils/linkSecret' export { legacyCredentialToW3cCredential, w3cToLegacyCredential } from './utils/w3cUtils' -export { fetchObjectsFromLedger, fetchQualifiedIds } from './utils/ledgerObjects' + export { dateToTimestamp } from './utils' +export { + fetchCredentialDefinition, + fetchRevocationRegistryDefinition, + fetchSchema, + getIndyNamespace, + isIndyDid, + getUnQualifiedId as getNonQualifiedId, + getQualifiedCredentialDefinition, + getQualifiedId, + getQualifiedRevocationRegistryDefinition, + getQualifiedSchema, + getUnqualifiedCredentialDefinition, + getUnqualifiedRevocationRegistryDefinition, + getUnqualifiedSchema, + isQualifiedCredentialDefinition, + isQualifiedRevocationRegistryDefinition, + isQualifiedSchema, +} from './utils/ledgerObjects' diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index 3437d4cd16..cc7187be2a 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -1,4 +1,4 @@ -import type { AnonCredsCredentialValue } from '@aries-framework/core' +import type { AnonCredsCredentialValue } from '@credo-ts/core' export const anonCredsPredicateType = ['>=', '>', '<=', '<'] as const export type AnonCredsPredicateType = (typeof anonCredsPredicateType)[number] diff --git a/packages/anoncreds/src/models/internal.ts b/packages/anoncreds/src/models/internal.ts index ded8f830fa..53270b5d02 100644 --- a/packages/anoncreds/src/models/internal.ts +++ b/packages/anoncreds/src/models/internal.ts @@ -1,4 +1,4 @@ -import type { AnonCredsClaimRecord } from '@aries-framework/core' +import type { AnonCredsClaimRecord } from '@credo-ts/core' export interface AnonCredsCredentialInfo { credentialId: string diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index 8604405f1a..caba2a551c 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -16,7 +16,7 @@ import type { AnonCredsRevocationStatusList, AnonCredsSchema, } from '../models/registry' -import type { W3cJsonLdVerifiableCredential } from '@aries-framework/core' +import type { W3cJsonLdVerifiableCredential } from '@credo-ts/core' export interface AnonCredsAttributeInfo { name?: string @@ -44,7 +44,8 @@ export interface CreateProofOptions { } } -export interface BaseStoreCredentialOptions { +export interface StoreCredentialOptions { + credential: W3cJsonLdVerifiableCredential | AnonCredsCredential credentialRequestMetadata: AnonCredsCredentialRequestMetadata credentialDefinition: AnonCredsCredentialDefinition schema: AnonCredsSchema @@ -56,14 +57,6 @@ export interface BaseStoreCredentialOptions { } } -export interface StoreW3cCredentialOptions extends BaseStoreCredentialOptions { - credential: W3cJsonLdVerifiableCredential -} - -export interface StoreCredentialOptions extends BaseStoreCredentialOptions { - credential: AnonCredsCredential -} - export interface GetCredentialOptions { credentialId: string } diff --git a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts index bbadb71a1a..2941f43f27 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts @@ -1,4 +1,4 @@ -import type { Wallet } from '@aries-framework/core' +import type { Wallet } from '@credo-ts/core' import { Agent, @@ -14,7 +14,7 @@ import { VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialRepository, W3cCredentialsModuleConfig, -} from '@aries-framework/core' +} from '@credo-ts/core' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' diff --git a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts index 886f5d03fe..c6b497fe2c 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts @@ -1,25 +1,23 @@ import type { AnonCredsCredentialRecord } from '../../repository' -import type { AgentContext, BaseAgent } from '@aries-framework/core' +import type { AgentContext, BaseAgent } from '@credo-ts/core' -import { W3cCredentialService } from '@aries-framework/core' +import { W3cCredentialService } from '@credo-ts/core' import { AnonCredsCredentialRepository } from '../../repository' import { legacyCredentialToW3cCredential } from '../../utils' -import { fetchQualifiedIds } from '../../utils/ledgerObjects' +import { getQualifiedId, fetchCredentialDefinition, getIndyNamespace } from '../../utils/ledgerObjects' async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRecord: AnonCredsCredentialRecord) { const legacyCredential = legacyRecord.credential const legacyTags = legacyRecord.getTags() - const w3cJsonLdCredential = await legacyCredentialToW3cCredential(agentContext, legacyCredential) + // TODO: check if it is in cache + const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, legacyTags.credentialDefinitionId) + const namespace = getIndyNamespace(credentialDefinitionReturn.qualifiedId) - const { schemaId, schemaIssuerId, revocationRegistryId, credentialDefinitionId } = await fetchQualifiedIds( - agentContext, - { - schemaId: legacyTags.schemaId, - credentialDefinitionId: legacyTags.credentialDefinitionId, - revocationRegistryId: legacyTags.revocationRegistryId, - } + const w3cJsonLdCredential = await legacyCredentialToW3cCredential( + legacyCredential, + credentialDefinitionReturn.qualifiedCredentialDefinition ) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) @@ -28,13 +26,13 @@ async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRe anonCredsCredentialRecordOptions: { credentialId: legacyRecord.credentialId, linkSecretId: legacyRecord.linkSecretId, - credentialDefinitionId, - schemaId, + credentialDefinitionId: credentialDefinitionReturn.qualifiedId, + schemaId: getQualifiedId(legacyTags.schemaId, namespace), schemaName: legacyTags.schemaName, - schemaIssuerId, + schemaIssuerId: getQualifiedId(legacyTags.issuerId, namespace), schemaVersion: legacyTags.schemaVersion, methodName: legacyRecord.methodName, - revocationRegistryId: revocationRegistryId, + revocationRegistryId: getQualifiedId(legacyTags.credentialDefinitionId, namespace), credentialRevocationId: legacyTags.credentialRevocationId, }, }) diff --git a/packages/anoncreds/src/updates/0.4-0.5/index.ts b/packages/anoncreds/src/updates/0.4-0.5/index.ts index f3f2741ee8..29f16e6b91 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/index.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/index.ts @@ -1,4 +1,4 @@ -import type { BaseAgent } from '@aries-framework/core' +import type { BaseAgent } from '@credo-ts/core' import { storeAnonCredsInW3cFormatV0_5 } from './anonCredsCredentialRecord' diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index e66d902264..17f2f684e0 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -17,4 +17,22 @@ export { unqualifiedSchemaVersionRegex, } from './indyIdentifiers' export { legacyCredentialToW3cCredential, w3cToLegacyCredential } from './w3cUtils' -export { fetchObjectsFromLedger, fetchQualifiedIds } from './ledgerObjects' + +export { + fetchCredentialDefinition, + fetchRevocationRegistryDefinition, + fetchSchema, + getIndyNamespace, + isIndyDid, + getUnQualifiedId as getNonQualifiedId, + getQualifiedCredentialDefinition, + getQualifiedId, + getQualifiedRevocationRegistryDefinition, + getQualifiedSchema, + getUnqualifiedCredentialDefinition, + getUnqualifiedRevocationRegistryDefinition, + getUnqualifiedSchema, + isQualifiedCredentialDefinition, + isQualifiedRevocationRegistryDefinition, + isQualifiedSchema, +} from './ledgerObjects' diff --git a/packages/anoncreds/src/utils/ledgerObjects.ts b/packages/anoncreds/src/utils/ledgerObjects.ts index 76c4377f3b..e25796bc1f 100644 --- a/packages/anoncreds/src/utils/ledgerObjects.ts +++ b/packages/anoncreds/src/utils/ledgerObjects.ts @@ -1,115 +1,306 @@ -import type { GetCredentialDefinitionReturn, GetSchemaReturn, GetRevocationRegistryDefinitionReturn } from '../services' -import type { AgentContext } from '@aries-framework/core' +import type { AnonCredsCredentialDefinition, AnonCredsRevocationRegistryDefinition, AnonCredsSchema } from '../models' +import type { AgentContext } from '@credo-ts/core' -import { AriesFrameworkError } from '@aries-framework/core' +import { AriesFrameworkError, isDid } from '@credo-ts/core' import { AnonCredsRegistryService } from '../services' import { - isUnqualifiedSchemaId, + getUnqualifiedCredentialDefinitionId, + getUnqualifiedRevocationRegistryDefinitionId, + getUnqualifiedSchemaId, + isDidIndyCredentialDefinitionId, + isDidIndyRevocationRegistryId, + isDidIndySchemaId, isUnqualifiedCredentialDefinitionId, isUnqualifiedRevocationRegistryId, + isUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, + parseIndyDid, + parseIndyRevocationRegistryId, + parseIndySchemaId, } from './indyIdentifiers' -export type FetchLedgerObjectsInput = { - credentialDefinitionId?: string - schemaId?: string - revocationRegistryId?: string -} - -export type FetchLedgerObjectsReturn = { - credentialDefinitionReturn: T['credentialDefinitionId'] extends string - ? GetCredentialDefinitionReturn - : T['credentialDefinitionId'] extends string | undefined - ? GetCredentialDefinitionReturn | undefined - : undefined - schemaReturn: T['schemaId'] extends string - ? GetSchemaReturn - : T['schemaId'] extends string | undefined - ? GetSchemaReturn | undefined - : undefined - revocationRegistryDefinitionReturn: T['revocationRegistryId'] extends string - ? GetRevocationRegistryDefinitionReturn - : T['revocationRegistryId'] extends string | undefined - ? GetRevocationRegistryDefinitionReturn | undefined - : undefined -} - -export async function fetchObjectsFromLedger(agentContext: AgentContext, input: T) { - const { credentialDefinitionId, schemaId, revocationRegistryId } = input +type WithIds = input & { qualifiedId: string; id: string } - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) +type ReturnHelper = input extends string + ? WithIds + : input extends string | undefined + ? WithIds | undefined + : undefined - let schemaReturn: GetSchemaReturn | undefined = undefined - if (schemaId) { - const result = await registryService - .getRegistryForIdentifier(agentContext, schemaId) - .getSchema(agentContext, schemaId) +export function getIndyNamespace(identifier: string): string { + if (!isIndyDid(identifier)) throw new AriesFrameworkError(`Cannot get indy namespace of identifier '${identifier}'`) + if (isDidIndySchemaId(identifier)) { + const { namespace } = parseIndySchemaId(identifier) + if (!namespace) throw new AriesFrameworkError(`Cannot get indy namespace of identifier '${identifier}'`) + return namespace + } else if (isDidIndyCredentialDefinitionId(identifier)) { + const { namespace } = parseIndyCredentialDefinitionId(identifier) + if (!namespace) throw new AriesFrameworkError(`Cannot get indy namespace of identifier '${identifier}'`) + return namespace + } else if (isDidIndyRevocationRegistryId(identifier)) { + const { namespace } = parseIndyRevocationRegistryId(identifier) + if (!namespace) throw new AriesFrameworkError(`Cannot get indy namespace of identifier '${identifier}'`) + return namespace + } - if (!result) throw new AriesFrameworkError('Schema not found') - schemaReturn = result + const { namespace } = parseIndyDid(identifier) + return namespace +} + +export function getUnQualifiedId(identifier: string): string { + if (!isDid(identifier)) return identifier + if (!isIndyDid(identifier)) throw new AriesFrameworkError(`Cannot get unqualified id of identifier '${identifier}'`) + + if (isDidIndySchemaId(identifier)) { + const { schemaName, schemaVersion, namespaceIdentifier } = parseIndySchemaId(identifier) + return getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) + } else if (isDidIndyCredentialDefinitionId(identifier)) { + const { schemaSeqNo, tag, namespaceIdentifier } = parseIndyCredentialDefinitionId(identifier) + return getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, tag) + } else if (isDidIndyRevocationRegistryId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = + parseIndyRevocationRegistryId(identifier) + return getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) } - let credentialDefinitionReturn: GetCredentialDefinitionReturn | undefined = undefined - if (credentialDefinitionId) { - const result = await registryService - .getRegistryForIdentifier(agentContext, credentialDefinitionId) - .getCredentialDefinition(agentContext, credentialDefinitionId) - if (!result) throw new AriesFrameworkError('CredentialDefinition not found') - credentialDefinitionReturn = result + const { namespaceIdentifier } = parseIndyDid(identifier) + return namespaceIdentifier +} + +export function isIndyDid(identifier: string): boolean { + return identifier.startsWith('did:indy:') +} + +export function getQualifiedId(identifier: string, namespace: string) { + const isQualifiedDid = isDid(identifier) + if (isQualifiedDid) return identifier + + if (!namespace || typeof namespace !== 'string') { + throw new AriesFrameworkError('Missing required indy namespace') } - let revocationRegistryDefinitionReturn: GetRevocationRegistryDefinitionReturn | undefined = undefined - if (revocationRegistryId) { - const result = await registryService - .getRegistryForIdentifier(agentContext, revocationRegistryId) - .getRevocationRegistryDefinition(agentContext, revocationRegistryId) - if (!result) throw new AriesFrameworkError('RevocationRegistryDefinition not found') - revocationRegistryDefinitionReturn = result + if (isUnqualifiedSchemaId(identifier)) { + const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(identifier) + const schemaId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/SCHEMA/${schemaName}/${schemaVersion}` + //if (isDidIndySchemaId(schemaId)) throw new Error(`schemaid conversion error: ${schemaId}`) + return schemaId + } else if (isUnqualifiedCredentialDefinitionId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(identifier) + const credentialDefinitionId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` + //if (isDidIndyCredentialDefinitionId(credentialDefinitionId)) + // throw new Error(`credentialdefintiion id conversion error: ${credentialDefinitionId}`) + return credentialDefinitionId + } else if (isUnqualifiedRevocationRegistryId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, revocationRegistryTag } = parseIndyRevocationRegistryId(identifier) + const revocationRegistryId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${revocationRegistryTag}` + //if (isDidIndyRevocationRegistryId(revocationRegistryId)) + // throw new Error(`revocationregistry id conversion error: ${revocationRegistryId}`) + return revocationRegistryId } + return `did:indy:${namespace}:${identifier}` +} + +export function getUnqualifiedSchema(schema: AnonCredsSchema): AnonCredsSchema { + if (!isIndyDid(schema.issuerId)) return schema + const issuerId = getUnQualifiedId(schema.issuerId) + + return { ...schema, issuerId } +} + +export function isQualifiedSchema(schema: AnonCredsSchema) { + return isDid(schema.issuerId) +} + +export function getQualifiedSchema(schema: AnonCredsSchema, namespace: string): AnonCredsSchema { + if (isQualifiedSchema(schema)) return schema + return { - credentialDefinitionReturn, - schemaReturn, - revocationRegistryDefinitionReturn, - } as FetchLedgerObjectsReturn + ...schema, + issuerId: getQualifiedId(schema.issuerId, namespace), + } } -export async function fetchQualifiedIds( +export async function fetchSchema( agentContext: AgentContext, - input: T -): Promise { - const { schemaId, credentialDefinitionId, revocationRegistryId, schemaIssuerId } = input - - let qSchemaId = schemaId ?? undefined - let qSchemaIssuerId = schemaIssuerId ?? undefined - if (schemaIssuerId && !schemaId) throw new AriesFrameworkError('Cannot fetch schemaIssuerId without schemaId') - if (schemaId && (isUnqualifiedSchemaId(schemaId) || schemaId.startsWith('did:') === false)) { - const { schemaReturn } = await fetchObjectsFromLedger(agentContext, { schemaId }) - qSchemaId = schemaReturn.schemaId - - if (schemaIssuerId && schemaIssuerId.startsWith('did') === false) { - if (!schemaReturn.schema) throw new AriesFrameworkError('Schema not found') - qSchemaIssuerId = schemaReturn.schema.issuerId - } + schemaId: string +): Promise< + ReturnHelper< + string, + WithIds<{ schema: AnonCredsSchema; qualifiedSchema: AnonCredsSchema; unqualifiedSchema: AnonCredsSchema }> + > +> { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, schemaId) + .getSchema(agentContext, schemaId) + if (!result || !result.schema) { + throw new AriesFrameworkError(`Schema not found for id ${schemaId}: ${result.resolutionMetadata.message}`) } - let qCredentialDefinitionId = credentialDefinitionId ?? undefined - if (credentialDefinitionId && isUnqualifiedCredentialDefinitionId(credentialDefinitionId)) { - const { credentialDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { credentialDefinitionId }) - qCredentialDefinitionId = credentialDefinitionReturn.credentialDefinitionId + const indyNamespace = result.schemaMetadata.didIndyNamespace + + const schema = result.schema + const qualifiedSchema = getQualifiedSchema(schema, indyNamespace as string) + const unqualifiedSchema = getUnqualifiedSchema(schema) + + return { + schema, + id: schemaId, + qualifiedId: getQualifiedId(schemaId, indyNamespace as string), + qualifiedSchema, + unqualifiedSchema, } +} - let qRevocationRegistryId = revocationRegistryId ?? undefined - if (revocationRegistryId && isUnqualifiedRevocationRegistryId(revocationRegistryId)) { - const { revocationRegistryDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { revocationRegistryId }) - qRevocationRegistryId = revocationRegistryDefinitionReturn.revocationRegistryDefinitionId +export function getUnqualifiedCredentialDefinition( + anonCredsCredentialDefinition: AnonCredsCredentialDefinition +): AnonCredsCredentialDefinition { + if (!isIndyDid(anonCredsCredentialDefinition.issuerId) || !isIndyDid(anonCredsCredentialDefinition.schemaId)) { + return anonCredsCredentialDefinition } + const issuerId = getUnQualifiedId(anonCredsCredentialDefinition.issuerId) + const schemaId = getUnQualifiedId(anonCredsCredentialDefinition.schemaId) + + return { ...anonCredsCredentialDefinition, issuerId, schemaId } +} + +export function isQualifiedCredentialDefinition(anonCredsCredentialDefinition: AnonCredsCredentialDefinition) { + return isDid(anonCredsCredentialDefinition.issuerId) && isDid(anonCredsCredentialDefinition.schemaId) +} + +export function getQualifiedCredentialDefinition( + anonCredsCredentialDefinition: AnonCredsCredentialDefinition, + namespace: string +): AnonCredsCredentialDefinition { + if (isQualifiedCredentialDefinition(anonCredsCredentialDefinition)) return { ...anonCredsCredentialDefinition } return { - schemaId: qSchemaId, - credentialDefinitionId: qCredentialDefinitionId, - revocationRegistryId: qRevocationRegistryId, - schemaIssuerId: qSchemaIssuerId, - } as T & (T['schemaId'] extends string ? { schemaIssuerId: string } : { schemaIssuerId: never }) + ...anonCredsCredentialDefinition, + issuerId: getQualifiedId(anonCredsCredentialDefinition.issuerId, namespace), + schemaId: getQualifiedId(anonCredsCredentialDefinition.schemaId, namespace), + } +} + +export async function fetchCredentialDefinition( + agentContext: AgentContext, + credentialDefinitionId: string +): Promise< + ReturnHelper< + string, + WithIds<{ + credentialDefinition: AnonCredsCredentialDefinition + qualifiedCredentialDefinition: AnonCredsCredentialDefinition + unqualifiedCredentialDefinition: AnonCredsCredentialDefinition + }> + > +> { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, credentialDefinitionId) + .getCredentialDefinition(agentContext, credentialDefinitionId) + if (!result || !result.credentialDefinition) { + throw new AriesFrameworkError( + `Schema not found for id ${credentialDefinitionId}: ${result.resolutionMetadata.message}` + ) + } + + const indyNamespace = result.credentialDefinitionMetadata.didIndyNamespace + + const credentialDefinition = result.credentialDefinition + const qualifiedCredentialDefinition = getQualifiedCredentialDefinition(credentialDefinition, indyNamespace as string) + const unqualifiedCredentialDefinition = getUnqualifiedCredentialDefinition(credentialDefinition) + + return { + credentialDefinition, + id: credentialDefinitionId, + qualifiedId: getQualifiedId(credentialDefinitionId, indyNamespace as string), + qualifiedCredentialDefinition, + unqualifiedCredentialDefinition, + } +} + +export function getUnqualifiedRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +): AnonCredsRevocationRegistryDefinition { + if (!isIndyDid(revocationRegistryDefinition.issuerId) || !isIndyDid(revocationRegistryDefinition.credDefId)) { + return revocationRegistryDefinition + } + + const issuerId = getUnQualifiedId(revocationRegistryDefinition.issuerId) + const credDefId = getUnQualifiedId(revocationRegistryDefinition.credDefId) + + return { ...revocationRegistryDefinition, issuerId, credDefId } +} + +export function isQualifiedRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +) { + return isDid(revocationRegistryDefinition.issuerId) && isDid(revocationRegistryDefinition.credDefId) +} + +export function getQualifiedRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, + namespace: string +): AnonCredsRevocationRegistryDefinition { + if (isQualifiedRevocationRegistryDefinition(revocationRegistryDefinition)) return { ...revocationRegistryDefinition } + + return { + ...revocationRegistryDefinition, + issuerId: getQualifiedId(revocationRegistryDefinition.issuerId, namespace), + credDefId: getQualifiedId(revocationRegistryDefinition.credDefId, namespace), + } +} + +export async function fetchRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string +): Promise< + ReturnHelper< + string, + WithIds<{ + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + qualifiedRevocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + unqualifiedRevocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + }> + > +> { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) + .getRevocationRegistryDefinition(agentContext, revocationRegistryDefinitionId) + if (!result || !result.revocationRegistryDefinition) { + throw new AriesFrameworkError( + `RevocationRegistryDefinition not found for id ${revocationRegistryDefinitionId}: ${result.resolutionMetadata.message}` + ) + } + + const indyNamespace = result.revocationRegistryDefinitionMetadata.didIndyNamespace + + const revocationRegistryDefinition = result.revocationRegistryDefinition + const qualifiedRevocationRegistryDefinition = getQualifiedRevocationRegistryDefinition( + revocationRegistryDefinition, + indyNamespace as string + ) + const unqualifiedRevocationRegistryDefinition = getUnqualifiedRevocationRegistryDefinition( + qualifiedRevocationRegistryDefinition + ) + + return { + revocationRegistryDefinition, + id: revocationRegistryDefinitionId, + qualifiedId: getQualifiedId(revocationRegistryDefinitionId, indyNamespace as string), + qualifiedRevocationRegistryDefinition, + unqualifiedRevocationRegistryDefinition, + } } diff --git a/packages/anoncreds/src/utils/w3cUtils.ts b/packages/anoncreds/src/utils/w3cUtils.ts index df485e627b..e2ff49b20a 100644 --- a/packages/anoncreds/src/utils/w3cUtils.ts +++ b/packages/anoncreds/src/utils/w3cUtils.ts @@ -1,26 +1,14 @@ -import type { AnonCredsCredential } from '../models' +import type { AnonCredsCredential, AnonCredsCredentialDefinition } from '../models' import type { ProcessCredentialOptions } from '@hyperledger/anoncreds-shared' -import { - JsonTransformer, - W3cJsonLdVerifiableCredential, - type AgentContext, - type JsonObject, -} from '@aries-framework/core' +import { JsonTransformer, W3cJsonLdVerifiableCredential, type JsonObject } from '@credo-ts/core' import { Credential, W3cCredential } from '@hyperledger/anoncreds-shared' -import { fetchObjectsFromLedger } from './ledgerObjects' - export async function legacyCredentialToW3cCredential( - agentContext: AgentContext, legacyCredential: AnonCredsCredential, + credentialDefinition: AnonCredsCredentialDefinition, process?: ProcessCredentialOptions ) { - const { credentialDefinitionReturn } = await fetchObjectsFromLedger(agentContext, { - credentialDefinitionId: legacyCredential.cred_def_id, - }) - if (!credentialDefinitionReturn.credentialDefinition) throw new Error('Credential definition not found.') - let credential: W3cJsonLdVerifiableCredential let anonCredsCredential: Credential | undefined let w3cCredentialObj: W3cCredential | undefined @@ -29,11 +17,14 @@ export async function legacyCredentialToW3cCredential( try { anonCredsCredential = Credential.fromJson(legacyCredential as unknown as JsonObject) w3cCredentialObj = anonCredsCredential.toW3c({ - credentialDefinition: credentialDefinitionReturn.credentialDefinition as unknown as JsonObject, + credentialDefinition: credentialDefinition as unknown as JsonObject, w3cVersion: '1.1', }) - processed = process ? w3cCredentialObj.process(process) : w3cCredentialObj + processed = process + ? w3cCredentialObj.process({ ...process, credentialDefinition: credentialDefinition as unknown as JsonObject }) + : w3cCredentialObj + const jsonObject = processed.toJson() credential = JsonTransformer.fromJSON(jsonObject, W3cJsonLdVerifiableCredential) } finally { diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 382b1f68b0..d1ccf42843 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -19,7 +19,7 @@ import type { } from '../src' import type { AgentContext } from '@credo-ts/core' -import { Hasher, TypedArrayEncoder } from '@credo-ts/core' +import { Hasher, TypedArrayEncoder, isDid } from '@credo-ts/core' import BigNumber from 'bn.js' import { @@ -31,10 +31,16 @@ import { getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedCredentialDefinitionId, getUnqualifiedSchemaId, - parseIndyCredentialDefinitionId, parseIndyDid, - parseIndySchemaId, + getUnqualifiedSchema, } from '../src' +import { + parseIndyCredentialDefinitionId, + parseIndyRevocationRegistryId, + parseIndySchemaId, + parseIndySchemaId, +} from '../src/utils/indyIdentifiers' +import { getQualifiedId, getUnQualifiedId } from '../src/utils/ledgerObjects' import { dateToTimestamp } from '../src/utils/timestamp' /** @@ -71,7 +77,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { const schema = this.schemas[schemaId] const parsed = parseIndySchemaId(schemaId) - const legacySchemaId = getUnqualifiedSchemaId(parsed.namespaceIdentifier, parsed.schemaName, parsed.schemaVersion) const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) @@ -86,6 +91,16 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } + let didIndyNamespace + if (isDid(schemaId)) { + didIndyNamespace = parseIndySchemaId(schemaId).namespace + } else { + const qSchemaIdEnd = getQualifiedId(schemaId, 'mock').split('mock:')[1] + const qSchemaId = Object.keys(this.schemas).find((schemaId) => schemaId.endsWith(qSchemaIdEnd)) + if (!qSchemaId) didIndyNamespace = undefined + else didIndyNamespace = parseIndySchemaId(qSchemaId).namespace + } + return { resolutionMetadata: {}, schema, @@ -94,6 +109,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { // 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, + didIndyNamespace, }, } } @@ -103,7 +119,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { options: RegisterSchemaOptions ): Promise { const { namespace, namespaceIdentifier } = parseIndyDid(options.schema.issuerId) - const legacyIssuerId = namespaceIdentifier const didIndySchemaId = getDidIndySchemaId( namespace, namespaceIdentifier, @@ -112,13 +127,10 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { ) this.schemas[didIndySchemaId] = options.schema - const legacySchemaId = getUnqualifiedSchemaId(legacyIssuerId, options.schema.name, options.schema.version) + const legacySchemaId = getUnQualifiedId(didIndySchemaId) const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) - this.schemas[legacySchemaId] = { - ...options.schema, - issuerId: legacyIssuerId, - } + this.schemas[legacySchemaId] = getUnqualifiedSchema(options.schema) return { registrationMetadata: {}, @@ -152,11 +164,23 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } + let didIndyNamespace + if (isDid(credentialDefinitionId)) { + didIndyNamespace = parseIndyCredentialDefinitionId(credentialDefinitionId).namespace + } else { + const qCredDefEnd = getQualifiedId(credentialDefinitionId, 'mock').split('mock:')[1] + const qCredDefId = Object.keys(this.credentialDefinitions).find((credentialDefinitionid) => + credentialDefinitionid.endsWith(qCredDefEnd) + ) + if (!qCredDefId) didIndyNamespace = undefined + else didIndyNamespace = parseIndyCredentialDefinitionId(qCredDefId).namespace + } + return { resolutionMetadata: {}, credentialDefinition, credentialDefinitionId, - credentialDefinitionMetadata: {}, + credentialDefinitionMetadata: { didIndyNamespace }, } } @@ -223,11 +247,23 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } + let didIndyNamespace + if (isDid(revocationRegistryDefinitionId)) { + didIndyNamespace = parseIndyRevocationRegistryId(revocationRegistryDefinitionId).namespace + } else { + const qRevRegIdEnd = getQualifiedId(revocationRegistryDefinitionId, 'mock').split('mock:')[1] + const qRevRegId = Object.keys(this.revocationRegistryDefinitions).find((revRegId) => + revRegId.endsWith(qRevRegIdEnd) + ) + if (!qRevRegId) didIndyNamespace = undefined + else didIndyNamespace = parseIndyRevocationRegistryId(qRevRegId).namespace + } + return { resolutionMetadata: {}, revocationRegistryDefinition, revocationRegistryDefinitionId, - revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionMetadata: { didIndyNamespace }, } } diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index dc0ce53967..e3e108d85a 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -270,7 +270,9 @@ describe('AnonCreds API', () => { expect(credentialDefinitionResult).toEqual({ resolutionMetadata: {}, - credentialDefinitionMetadata: {}, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localhost', + }, credentialDefinition: existingCredentialDefinitions['VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG'], credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', }) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9ec5248ac4..d1cfea3010 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -69,6 +69,7 @@ export { TypedArrayEncoder, Buffer, deepEquality, + isDid, } from './utils' export * from './logger' export * from './error' diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 2e9e8c17c1..0542e92b14 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -285,7 +285,11 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic throw new AriesFrameworkError('Cannot process data integrity presentations without cryptosuites') } - if (cryptosuite === 'anoncredsvc-2023' || cryptosuite === 'anoncredspresvc-2023') { + if ( + cryptosuite === 'anoncredsvc-2023' || + cryptosuite === 'anoncredspresvc-2023' || + cryptosuite === 'anoncredspresvp-2023' + ) { const dataIntegrityService = agentContext.dependencyManager.resolve( anonCredsVcDataIntegrityServiceSymbol ) diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts index 43e4171db7..69d3e0f313 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts @@ -26,6 +26,14 @@ export interface AnonCredsCredentialRecordOptions { schemaId: string credentialDefinitionId: string revocationRegistryId?: string + + unqualifiedTags?: { + issuerId: string + schemaId: string + schemaIssuerId: string + credentialDefinitionId: string + revocationRegistryId?: string + } } export interface W3cCredentialRecordOptions { @@ -54,10 +62,7 @@ export type DefaultW3cCredentialTags = { export type DefaultAnonCredsCredentialTags = { credentialId: string linkSecretId: string - credentialDefinitionId: string credentialRevocationId?: string - revocationRegistryId?: string - schemaId: string methodName: string // the following keys can be used for every `attribute name` in credential. @@ -75,12 +80,18 @@ export type CustomW3cCredentialTags = TagsBase & { export type CustomAnonCredsCredentialTags = { schemaName: string schemaVersion: string - schemaIssuerId: string // TODO: derive from proof schemaId: string + schemaIssuerId: string credentialDefinitionId: string revocationRegistryId?: string + + unqualifiedIssuerId: string | undefined + unqualifiedSchemaId: string | undefined + unqualifiedSchemaIssuerId: string | undefined + unqualifiedCredentialDefinitionId: string | undefined + unqualifiedRevocationRegistryId: string | undefined } export class W3cCredentialRecord extends BaseRecord< @@ -121,6 +132,12 @@ export class W3cCredentialRecord extends BaseRecord< schemaId: props.anonCredsCredentialRecordOptions?.schemaId, credentialDefinitionId: props.anonCredsCredentialRecordOptions?.credentialDefinitionId, revocationRegistryId: props.anonCredsCredentialRecordOptions?.revocationRegistryId, + unqualifiedIssuerId: props.anonCredsCredentialRecordOptions?.unqualifiedTags?.issuerId, + unqualifiedSchemaIssuerId: props.anonCredsCredentialRecordOptions?.unqualifiedTags?.schemaIssuerId, + unqualifiedSchemaId: props.anonCredsCredentialRecordOptions?.unqualifiedTags?.schemaId, + unqualifiedCredentialDefinitionId: + props.anonCredsCredentialRecordOptions?.unqualifiedTags?.credentialDefinitionId, + unqualifiedRevocationRegistryId: props.anonCredsCredentialRecordOptions?.unqualifiedTags?.revocationRegistryId, }) } } @@ -162,14 +179,26 @@ export class W3cCredentialRecord extends BaseRecord< const { schemaId, schemaName, schemaVersion, schemaIssuerId, credentialDefinitionId } = this._tags if (!schemaId || !schemaName || !schemaVersion || !schemaIssuerId || !credentialDefinitionId) return undefined + const { + unqualifiedSchemaId, + unqualifiedSchemaIssuerId, + unqualifiedCredentialDefinitionId, + unqualifiedRevocationRegistryId, + unqualifiedIssuerId, + } = this._tags const anonCredsCredentialTags: Tags = { + schemaId, schemaIssuerId, schemaName, schemaVersion, - schemaId, credentialDefinitionId, revocationRegistryId: this._tags.revocationRegistryId, + unqualifiedIssuerId, + unqualifiedSchemaId, + unqualifiedSchemaIssuerId, + unqualifiedCredentialDefinitionId, + unqualifiedRevocationRegistryId, credentialId: this.anonCredsCredentialMetadata?.credentialId as string, credentialRevocationId: this.anonCredsCredentialMetadata?.credentialRevocationId as string, linkSecretId: this.anonCredsCredentialMetadata?.linkSecretId as string, From 866105a5fa24fcc99e5525eb5c4b1d5e588edf02 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 2 Feb 2024 09:17:20 +0100 Subject: [PATCH 12/38] fix: anoncreds-rs cryptosuite naming --- demo/package.json | 2 +- packages/anoncreds-rs/package.json | 6 +++--- .../anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts | 2 +- .../anoncreds-rs/tests/fixtures/presentation-definition.ts | 2 +- packages/anoncreds/package.json | 6 +++--- .../DifPresentationExchangeService.ts | 2 +- .../DifPresentationExchangeProofFormatService.ts | 6 +----- 7 files changed, 11 insertions(+), 15 deletions(-) diff --git a/demo/package.json b/demo/package.json index 581c54e94c..10f1f5c152 100644 --- a/demo/package.json +++ b/demo/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.8", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.10", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "inquirer": "^8.2.5" }, diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json index f8ae0234db..8664f8b7ae 100644 --- a/packages/anoncreds-rs/package.json +++ b/packages/anoncreds-rs/package.json @@ -35,8 +35,8 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.8", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.8", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.10", + "@hyperledger/anoncreds-shared": "^0.2.0-dev.10", "@sphereon/pex-models": "^2.1.2", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", @@ -45,6 +45,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.8" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.10" } } diff --git a/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts b/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts index c96464c541..ae9263ac0d 100644 --- a/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts +++ b/packages/anoncreds-rs/src/AnonCredsVcDataIntegrityService.ts @@ -587,7 +587,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg for (const verifiableCredential of verifiableCredentials) { if (verifiableCredential instanceof W3cJsonLdVerifiableCredential) { - const proof = this.getDataIntegrityProof(verifiableCredential, 'anoncredspresvc-2023') + const proof = this.getDataIntegrityProof(verifiableCredential, 'anoncredsvc-2023') credentialDefinitionIds.add(proof.verificationMethod) } else { throw new AriesFrameworkError('Unsupported credential type') diff --git a/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts b/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts index 1a4b8fe674..dd0dc486fb 100644 --- a/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts +++ b/packages/anoncreds-rs/tests/fixtures/presentation-definition.ts @@ -50,7 +50,7 @@ export const presentationDefinition: PresentationDefinitionV1 = { format: { di_vc: { proof_type: ['DataIntegrityProof'], - cryptosuite: ['anoncredspresvc-2023', 'eddsa-rdfc-2022'], + cryptosuite: ['anoncredsvc-2023', 'eddsa-rdfc-2022'], }, }, } diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index a65b54a9c1..9e11d77d65 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -29,16 +29,16 @@ "class-transformer": "0.5.1", "class-validator": "0.14.0", "reflect-metadata": "^0.1.13", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.8" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.10" }, "devDependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.8", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.10", "@credo-ts/node": "0.4.2", "rimraf": "^4.4.0", "rxjs": "^7.8.0", "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.8" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.10" } } diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index f8710e8d19..6a4c46915d 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -547,7 +547,7 @@ export class DifPresentationExchangeService { if (vpFormat === ClaimFormat.DiVp) { const cryptosuite = await this.getDataIntegritySignatureSuite(presentationDefinition) - if (cryptosuite === 'anoncredsvc-2023' || cryptosuite === 'anoncredspresvc-2023') { + if (cryptosuite === 'anoncredsvc-2023' || cryptosuite === 'anoncredsvc-2023') { const anonCredsVcDataIntegrityService = agentContext.dependencyManager.resolve( anonCredsVcDataIntegrityServiceSymbol diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 0542e92b14..40f3cff78c 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -285,11 +285,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic throw new AriesFrameworkError('Cannot process data integrity presentations without cryptosuites') } - if ( - cryptosuite === 'anoncredsvc-2023' || - cryptosuite === 'anoncredspresvc-2023' || - cryptosuite === 'anoncredspresvp-2023' - ) { + if (cryptosuite === 'anoncredsvc-2023') { const dataIntegrityService = agentContext.dependencyManager.resolve( anonCredsVcDataIntegrityServiceSymbol ) From 76dc9624011657e4bf990f54ad387181d6b9ea7c Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 2 Feb 2024 12:53:37 +0100 Subject: [PATCH 13/38] fix: rename anoncredsvc to anoncreds --- demo/src/Faber.ts | 2 +- packages/anoncreds/package.json | 4 ++-- .../src/anoncreds-rs/AnonCredsVcDataIntegrityService.ts | 2 +- .../anoncreds/tests/fixtures/presentation-definition.ts | 2 +- .../DifPresentationExchangeService.ts | 7 ++++--- .../dif-presentation-exchange/utils/credentialSelection.ts | 2 +- .../DifPresentationExchangeProofFormatService.ts | 2 +- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index bbf11148f0..8f0ac0327a 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -327,7 +327,7 @@ export class Faber extends BaseAgent { format: { di_vc: { proof_type: ['DataIntegrityProof'], - cryptosuite: ['anoncredsvc-2023', 'eddsa-rdfc-2022'], + cryptosuite: ['anoncreds-2023', 'eddsa-rdfc-2022'], }, }, }, diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 09b848a423..e785c34300 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -24,6 +24,7 @@ "test": "jest" }, "dependencies": { + "@astronautlabs/jsonpath": "^1.1.2", "@credo-ts/core": "0.4.2", "bn.js": "^5.2.1", "class-transformer": "0.5.1", @@ -31,10 +32,9 @@ "reflect-metadata": "^0.1.13" }, "devDependencies": { - "@astronautlabs/jsonpath": "^1.1.2", + "@credo-ts/node": "0.4.2", "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.10", "@hyperledger/anoncreds-shared": "^0.2.0-dev.10", - "@credo-ts/node": "0.4.2", "rimraf": "^4.4.0", "rxjs": "^7.8.0", "typescript": "~4.9.5" diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsVcDataIntegrityService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsVcDataIntegrityService.ts index a02d0aeabf..a1348c4fd7 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsVcDataIntegrityService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsVcDataIntegrityService.ts @@ -583,7 +583,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg for (const verifiableCredential of verifiableCredentials) { if (verifiableCredential instanceof W3cJsonLdVerifiableCredential) { - const proof = this.getDataIntegrityProof(verifiableCredential, 'anoncredsvc-2023') + const proof = this.getDataIntegrityProof(verifiableCredential, 'anoncreds-2023') credentialDefinitionIds.add(proof.verificationMethod) } else { throw new CredoError('Unsupported credential type') diff --git a/packages/anoncreds/tests/fixtures/presentation-definition.ts b/packages/anoncreds/tests/fixtures/presentation-definition.ts index dd0dc486fb..67d2330622 100644 --- a/packages/anoncreds/tests/fixtures/presentation-definition.ts +++ b/packages/anoncreds/tests/fixtures/presentation-definition.ts @@ -50,7 +50,7 @@ export const presentationDefinition: PresentationDefinitionV1 = { format: { di_vc: { proof_type: ['DataIntegrityProof'], - cryptosuite: ['anoncredsvc-2023', 'eddsa-rdfc-2022'], + cryptosuite: ['anoncreds-2023', 'eddsa-rdfc-2022'], }, }, } diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index fc8f4374ce..6d042ccd59 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -125,7 +125,7 @@ export class DifPresentationExchangeService { presentationDefinition, getSphereonOriginalVerifiablePresentation(presentation), { - limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncredsvc-2023'], + limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncreds-2023'], } ) @@ -441,7 +441,7 @@ export class DifPresentationExchangeService { (descriptor) => descriptor.id === verifiableCredentials.inputDescriptorId )?.format if ( - format === 'DiVp' && + format === 'di_vp' && verifiableCredentials.credential.credential instanceof W3cJsonLdVerifiableCredential ) { return verifiableCredentials.credential.credential.cryptoSuites @@ -452,7 +452,7 @@ export class DifPresentationExchangeService { const commonCryptoSuites = cryptosuites.reduce((a, b) => a.filter((c) => b.includes(c))) if (commonCryptoSuites.length > 0) { - if (commonCryptoSuites.includes('anoncredsvc-2023')) { + if (commonCryptoSuites.includes('anoncreds-2023')) { const anonCredsVcDataIntegrityService = agentContext.dependencyManager.resolve( anonCredsVcDataIntegrityServiceSymbol @@ -507,6 +507,7 @@ export class DifPresentationExchangeService { presentationFrame: true, verifierMetadata: { audience: domain, + nonce: challenge, // TODO: we should make this optional issuedAt: Math.floor(Date.now() / 1000), }, diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index 27b22af232..155660fbee 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -24,7 +24,7 @@ export async function getCredentialsForRequest( ): Promise { const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c)) const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { - limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncredsvc-2023'], + limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncreds-2023'], }) const selectResults = { diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 18e16a9348..6c783db2e3 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -293,7 +293,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic throw new CredoError('Cannot process data integrity presentations without cryptosuites') } - if (cryptosuite === 'anoncredsvc-2023') { + if (cryptosuite === 'anoncreds-2023') { const dataIntegrityService = agentContext.dependencyManager.resolve( anonCredsVcDataIntegrityServiceSymbol ) From 79d8eaf570b022fb643ca60d7dd877df164aebfe Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 10:17:29 +0100 Subject: [PATCH 14/38] review changes --- docker-compose.arm.yml | 6 + packages/anoncreds/package.json | 2 +- packages/anoncreds/src/AnonCredsModule.ts | 11 +- .../src/__tests__/AnonCredsModule.test.ts | 8 +- ...s => AnonCreds2023DataIntegrityService.ts} | 338 ++++++++---------- .../anoncreds-rs/AnonCredsRsHolderService.ts | 34 +- .../AnonCredsRsVerifierService.ts | 8 +- .../AnonCredsRsHolderService.test.ts | 7 +- .../AnonCredsCredentialFormatService.ts | 101 ++---- .../formats/AnonCredsProofFormatService.ts | 18 +- .../DataIntegrityCredentialFormatService.ts | 54 ++- .../src/formats/LegacyIndyCredentialFormat.ts | 3 +- .../LegacyIndyCredentialFormatService.ts | 80 +---- .../formats/LegacyIndyProofFormatService.ts | 18 +- .../legacy-indy-format-services.test.ts | 2 +- packages/anoncreds/src/index.ts | 1 + .../v1-connectionless-credentials.e2e.test.ts | 4 +- .../v1-credentials-auto-accept.e2e.test.ts | 6 +- .../w3cCredentialRecordMigration.test.ts | 3 + packages/anoncreds/src/utils/index.ts | 1 + packages/anoncreds/src/utils/ledgerObjects.ts | 44 ++- packages/anoncreds/src/utils/w3cUtils.ts | 11 +- .../tests/InMemoryAnonCredsRegistry.ts | 3 +- .../data-integrity-flow-anoncreds.test.ts | 2 +- .../tests/data-integrity-flow-w3c.test.ts | 2 +- .../tests/data-integrity.e2e.test.ts | 2 +- packages/anoncreds/tests/indy-flow.test.ts | 2 +- packages/core/package.json | 2 +- ...ce.ts => AnonCredsDataIntegrityService.ts} | 12 +- .../DataIntegrityCredentialFormat.ts | 4 +- .../formats/dataIntegrity/index.ts | 2 +- .../DifPresentationExchangeService.ts | 73 ++-- ...fPresentationExchangeProofFormatService.ts | 88 ++--- .../data-integrity/models/LinkedDataProof.ts | 4 +- .../models/credential/W3cCredentialSubject.ts | 3 +- .../vc/repository/W3cCredentialRepository.ts | 1 + .../__tests__/W3cCredentialRecord.test.ts | 6 + .../vc/repository/anonCredsCredentialValue.ts | 35 +- 38 files changed, 447 insertions(+), 554 deletions(-) rename packages/anoncreds/src/anoncreds-rs/{AnonCredsVcDataIntegrityService.ts => AnonCreds2023DataIntegrityService.ts} (68%) rename packages/core/src/modules/credentials/formats/dataIntegrity/{AnonCredsVcDataIntegrityService.ts => AnonCredsDataIntegrityService.ts} (62%) diff --git a/docker-compose.arm.yml b/docker-compose.arm.yml index 8eb4230c5e..263f77baf2 100644 --- a/docker-compose.arm.yml +++ b/docker-compose.arm.yml @@ -24,3 +24,9 @@ services: add-did-from-seed 000000000000000000000000Trustee9 TRUSTEE && /usr/bin/supervisord -n " + + cheqd-ledger: + image: ghcr.io/cheqd/cheqd-testnet:latest + platform: linux/amd64 + ports: + - '26657:26657' diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index e785c34300..ff4ee94c19 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -28,7 +28,7 @@ "@credo-ts/core": "0.4.2", "bn.js": "^5.2.1", "class-transformer": "0.5.1", - "class-validator": "0.14.0", + "class-validator": "0.14.1", "reflect-metadata": "^0.1.13" }, "devDependencies": { diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index 1362d01148..fcd957bbbc 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -1,11 +1,16 @@ import type { AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' -import { anonCredsVcDataIntegrityServiceSymbol, type DependencyManager, type Module, type Update } from '@credo-ts/core' +import { + anoncreds2023DataIntegrityServiceSymbol, + type DependencyManager, + type Module, + type Update, +} from '@credo-ts/core' import { AnonCredsApi } from './AnonCredsApi' import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from './anoncreds-rs' -import { AnonCredsVc2023DataIntegrityService } from './anoncreds-rs/AnonCredsVcDataIntegrityService' +import { AnonCreds2023DataIntegrityServiceImpl } from './anoncreds-rs/AnonCreds2023DataIntegrityService' import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, @@ -51,7 +56,7 @@ export class AnonCredsModule implements Module { dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, AnonCredsRsIssuerService) dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, AnonCredsRsVerifierService) - dependencyManager.registerSingleton(anonCredsVcDataIntegrityServiceSymbol, AnonCredsVc2023DataIntegrityService) + dependencyManager.registerSingleton(anoncreds2023DataIntegrityServiceSymbol, AnonCreds2023DataIntegrityServiceImpl) } public updates = [ diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts index cee54b05bb..dfe7eb0e1c 100644 --- a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -1,12 +1,12 @@ import type { AnonCredsRegistry } from '../services' -import { anonCredsVcDataIntegrityServiceSymbol, type DependencyManager } from '@credo-ts/core' +import { anoncreds2023DataIntegrityServiceSymbol, type DependencyManager } from '@credo-ts/core' import { anoncreds } from '../../tests/helpers' import { AnonCredsModule } from '../AnonCredsModule' import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../anoncreds-rs' -import { AnonCredsVc2023DataIntegrityService } from '../anoncreds-rs/AnonCredsVcDataIntegrityService' +import { AnonCreds2023DataIntegrityServiceImpl } from '../anoncreds-rs/AnonCreds2023DataIntegrityService' import { AnonCredsSchemaRepository, AnonCredsCredentialDefinitionRepository, @@ -60,8 +60,8 @@ describe('AnonCredsModule', () => { ) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( - anonCredsVcDataIntegrityServiceSymbol, - AnonCredsVc2023DataIntegrityService + anoncreds2023DataIntegrityServiceSymbol, + AnonCreds2023DataIntegrityServiceImpl ) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsVcDataIntegrityService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts similarity index 68% rename from packages/anoncreds/src/anoncreds-rs/AnonCredsVcDataIntegrityService.ts rename to packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts index a1348c4fd7..ddf5ec6d5c 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsVcDataIntegrityService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts @@ -7,8 +7,8 @@ import type { } from '../models' import type { AgentContext, - AnonCredsVcDataIntegrityService, - AnonCredsVcVerificationOptions, + Anoncreds2023DataIntegrityService, + Anoncreds2023VerificationOptions, JsonObject, W3cCredentialRecord, } from '@credo-ts/core' @@ -44,8 +44,13 @@ import BigNumber from 'bn.js' import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' import { AnonCredsLinkSecretRepository } from '../repository' -import { AnonCredsRegistryService } from '../services' -import { assertBestPracticeRevocationInterval, fetchCredentialDefinition, fetchSchema } from '../utils' +import { + assertBestPracticeRevocationInterval, + fetchCredentialDefinition, + fetchRevocationRegistryDefinition, + fetchRevocationStatusList, + fetchSchema, +} from '../utils' export interface CredentialWithMetadata { credential: JsonObject @@ -63,19 +68,20 @@ export interface RevocationRegistryFetchMetadata { export type PathComponent = string | number @injectable() -export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataIntegrityService { - private getDataIntegrityProof(credential: W3cJsonLdVerifiableCredential, cryptosuite: string) { +export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataIntegrityService { + private getDataIntegrityProof(credential: W3cJsonLdVerifiableCredential) { + const cryptosuite = 'anoncreds-2023' if (Array.isArray(credential.proof)) { const proof = credential.proof.find( (proof) => proof.type === 'DataIntegrityProof' && proof.cryptosuite === cryptosuite ) - if (!proof) throw new CredoError('Could not find anoncreds proof') + if (!proof) throw new CredoError('Could not find anoncreds-2023 proof') return proof } if (credential.proof.type !== 'DataIntegrityProof' || credential.proof.cryptosuite !== cryptosuite) { throw new CredoError( - `Unsupported proof type '${credential.proof.type}' or cryptosuite '${credential.proof.cryptosuite}'.` + `Unsupported proof type cryptosuite '${credential.proof.cryptosuite}', expected anoncreds-2023.` ) } @@ -92,48 +98,45 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg return result } - private getCredentialMetadata( - entryIndex: number, + private getCredentialMetadataForDescriptor( + descriptorMapObject: Descriptor, selectedCredentials: JsonObject[], - selectedCredentialRecords: W3cCredentialRecord[] + selectedCredentialRecords?: W3cCredentialRecord[] ) { - const credentialRecord = selectedCredentialRecords[entryIndex] - if (!deepEquality(JsonTransformer.toJSON(credentialRecord.credential), selectedCredentials[entryIndex])) { - throw new CredoError('selected credential does not match the selected credential record') - } - - const anonCredsTags = credentialRecord.getAnonCredsTags() - if (!anonCredsTags) throw new CredoError('No anoncreds tags found on credential record') - - return { - entryIndex, - credentialRecord, - anonCredsTags, - } - } - - private getCredential(descriptorMapObject: Descriptor, selectedCredentials: JsonObject[]) { - const presentationWrapper = { - verifiableCredential: selectedCredentials, - } + const credentialExtractionResult = this.extractPathNodes({ verifiableCredential: selectedCredentials }, [ + descriptorMapObject.path, + ]) - const credentialExtractionResult = this.extractPathNodes(presentationWrapper, [descriptorMapObject.path]) if (credentialExtractionResult.length === 0 || credentialExtractionResult.length > 1) { throw new Error('Could not extract credential from presentation submission') } - // only available on the holder side - const jsonLdVerifiableCredentialJson = credentialExtractionResult[0].value + const credentialJson = credentialExtractionResult[0].value as JsonObject - const entryIndex = selectedCredentials.findIndex((credential) => - deepEquality(credential, jsonLdVerifiableCredentialJson) - ) + // FIXME: Is this required? + const entryIndex = selectedCredentials.findIndex((credential) => deepEquality(credential, credentialJson)) if (entryIndex === -1) throw new CredoError('Could not find selected credential') + const credentialRecord = selectedCredentialRecords ? selectedCredentialRecords[entryIndex] : undefined + if ( + credentialRecord && + !deepEquality(JsonTransformer.toJSON(credentialRecord.credential), selectedCredentials[entryIndex]) + ) { + throw new CredoError('selected credential does not match the selected credential record') + } + + const anonCredsTags = credentialRecord?.getAnonCredsTags() + if (credentialRecord && !anonCredsTags) throw new CredoError('No anoncreds tags found on credential record') + + const { credentialDefinitionId, revocationRegistryId, schemaId } = AnonCredsW3cCredential.fromJson(credentialJson) + return { entryIndex, - credential: JsonTransformer.fromJSON(jsonLdVerifiableCredentialJson, W3cJsonLdVerifiableCredential), - credentialJson: jsonLdVerifiableCredentialJson, + credentialJson, + anonCredsTags, + credentialDefinitionId, + revocationRegistryId, + schemaId, } } @@ -153,40 +156,28 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg // Make sure the revocation interval follows best practices from Aries RFC 0441 assertBestPracticeRevocationInterval(nonRevokedInterval) - const registry = agentContext.dependencyManager - .resolve(AnonCredsRegistryService) - .getRegistryForIdentifier(agentContext, revocationRegistryId) - - const { revocationRegistryDefinition: _revocationRegistryDefinition, resolutionMetadata } = - await registry.getRevocationRegistryDefinition(agentContext, revocationRegistryId) - if (!_revocationRegistryDefinition) { - throw new CredoError( - `Could not retrieve revocation registry definition for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` - ) - } + const { qualifiedRevocationRegistryDefinition } = await fetchRevocationRegistryDefinition( + agentContext, + revocationRegistryId + ) const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService const { tailsFilePath } = await tailsFileService.getTailsFile(agentContext, { - revocationRegistryDefinition: _revocationRegistryDefinition, + revocationRegistryDefinition: qualifiedRevocationRegistryDefinition, }) const timestampToFetch = timestamp ?? nonRevokedInterval.to if (!timestampToFetch) throw new CredoError('Timestamp to fetch is required') - // Fetch revocation status list if we don't already have a revocation status list for the given timestamp - const { revocationStatusList: _revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = - await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestampToFetch) - - if (!_revocationStatusList) { - throw new CredoError( - `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` - ) - } - + const { revocationStatusList: _revocationStatusList } = await fetchRevocationStatusList( + agentContext, + revocationRegistryId, + timestampToFetch + ) const updatedTimestamp = timestamp ?? _revocationStatusList.timestamp const revocationRegistryDefinition = RevocationRegistryDefinition.fromJson( - _revocationRegistryDefinition as unknown as JsonObject + qualifiedRevocationRegistryDefinition as unknown as JsonObject ) const revocationStatusList = RevocationStatusList.fromJson(_revocationStatusList as unknown as JsonObject) const revocationState = revocationRegistryIndex @@ -200,7 +191,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg const requestedFrom = nonRevokedInterval.from if (requestedFrom && requestedFrom > timestampToFetch) { - const { revocationStatusList: overrideRevocationStatusList } = await registry.getRevocationStatusList( + const { revocationStatusList: overrideRevocationStatusList } = await fetchRevocationStatusList( agentContext, revocationRegistryId, requestedFrom @@ -222,58 +213,43 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg return { updatedTimestamp, - revocationRegistryDefinition: [revocationRegistryId, revocationRegistryDefinition] as [ - string, - RevocationRegistryDefinition - ], + revocationRegistryId, + revocationRegistryDefinition, revocationStatusList, revocationState, nonRevokedIntervalOverride, } } - private async getCredentialDefinitionsAndSchemas( - agentContext: AgentContext, - schemaIds: Set | undefined, - credentialDefinitionIds: Set - ) { - const schemaFetchPromises = [...(schemaIds ?? [])].map((schemaId) => fetchSchema(agentContext, schemaId)) + private async getSchemas(agentContext: AgentContext, schemaIds: Set) { + const schemaFetchPromises = [...schemaIds].map((schemaId) => fetchSchema(agentContext, schemaId)) + + const schemas: Record = {} + const schemaFetchResults = await Promise.all(schemaFetchPromises) + for (const schemaFetchResult of schemaFetchResults) { + const schemaId = schemaFetchResult.id + const schema = schemaFetchResult.schema + schemas[schemaId] = schema + } + + return schemas + } + + private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { const credentialDefinitionFetchPromises = [...credentialDefinitionIds].map((credentialDefinitionId) => fetchCredentialDefinition(agentContext, credentialDefinitionId) ) - const schemas: Record = {} const credentialDefinitions: Record = {} - const results = await Promise.all([ - Promise.all(schemaFetchPromises), - Promise.all(credentialDefinitionFetchPromises), - ]) - - const credentialDefinitionFetchResults = results[1] + const credentialDefinitionFetchResults = await Promise.all(credentialDefinitionFetchPromises) for (const credentialDefinitionFetchResult of credentialDefinitionFetchResults) { const credentialDefinitionId = credentialDefinitionFetchResult.id const credentialDefinition = credentialDefinitionFetchResult.credentialDefinition credentialDefinitions[credentialDefinitionId] = credentialDefinition } - const schemaFetchResults = - schemaFetchPromises.length > 0 - ? results[0] - : await Promise.all( - credentialDefinitionFetchResults.map((res) => fetchSchema(agentContext, res.credentialDefinition.schemaId)) - ) - - for (const schemaFetchResult of schemaFetchResults) { - const schemaId = schemaFetchResult.id - const schema = schemaFetchResult.schema - schemas[schemaId] = schema - } - - return { - schemas, - credentialDefinitions, - } + return credentialDefinitions } private getPresentationMetadata = async ( @@ -286,7 +262,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg credentialDefinitionIds: Set } ) => { - const { linkSecretIds, schemaIds, credentialDefinitionIds, credentialsWithMetadata, credentialsProve } = input + const { linkSecretIds, credentialDefinitionIds, schemaIds, credentialsWithMetadata, credentialsProve } = input const linkSecretIdArray = [...linkSecretIds] if (linkSecretIdArray.length > 1) { throw new CredoError('Multiple linksecret cannot be used to create a single presentation') @@ -298,17 +274,13 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg .resolve(AnonCredsLinkSecretRepository) .getByLinkSecretId(agentContext, linkSecretIdArray[0]) - if (!linkSecretRecord.value) { - throw new CredoError('Link Secret value not stored') - } + if (!linkSecretRecord.value) throw new CredoError('Link Secret value not stored') const credentials: W3cCredentialEntry[] = await Promise.all( credentialsWithMetadata.map(async ({ credential, nonRevoked }) => { const { revocationRegistryIndex, revocationRegistryId, timestamp } = AnonCredsW3cCredential.fromJson(credential) - if (!nonRevoked) { - return { credential: credential as unknown as JsonObject, revocationState: undefined, timestamp: undefined } - } + if (!nonRevoked) return { credential, revocationState: undefined, timestamp: undefined } if (!revocationRegistryId || !revocationRegistryIndex) throw new CredoError('Missing revocation metadata') @@ -319,15 +291,12 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg revocationRegistryId, }) - return { credential: credential as unknown as JsonObject, revocationState, timestamp: updatedTimestamp } + return { credential, revocationState, timestamp: updatedTimestamp } }) ) - const { schemas, credentialDefinitions } = await this.getCredentialDefinitionsAndSchemas( - agentContext, - schemaIds, - credentialDefinitionIds - ) + const schemas = await this.getSchemas(agentContext, schemaIds) + const credentialDefinitions = await this.getCredentialDefinitions(agentContext, credentialDefinitionIds) return { schemas, @@ -338,9 +307,20 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg } } - private getPredicateTypeAndValues(predicateFilter?: FieldV2['filter']) { - if (!predicateFilter) throw new CredoError('Predicate filter is required') + private descriptorRequiresRevocationStatus(descriptor: InputDescriptorV1 | InputDescriptorV2) { + const statuses = descriptor.constraints?.statuses + if (!statuses) return false + if ( + statuses?.active?.directive && + (statuses.active.directive === 'allowed' || statuses.active.directive === 'required') + ) { + return true + } else { + throw new CredoError('Unsupported status directive') + } + } + private getPredicateTypeAndValues(predicateFilter: NonNullable) { const predicates: { predicateType: AnonCredsRequestedPredicate['p_type'] predicateValue: AnonCredsRequestedPredicate['p_value'] @@ -355,6 +335,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg for (const [key, value] of Object.entries(predicateFilter)) { if (key === 'type') continue + const predicateType = supportedJsonSchemaNumericRangeProperties[key] if (!predicateType) throw new CredoError(`Unsupported predicate filter property '${key}'`) predicates.push({ @@ -366,6 +347,42 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg return predicates } + private getRevocationMetadataForCredentials = async ( + agentContext: AgentContext, + credentialsWithMetadata: CredentialWithMetadata[] + ) => { + const revocationMetadataFetchPromises = credentialsWithMetadata + .filter((cwm) => cwm.nonRevoked) + .map(async (credentialWithMetadata) => { + const { revocationRegistryIndex, revocationRegistryId, timestamp } = AnonCredsW3cCredential.fromJson( + credentialWithMetadata.credential + ) + return await this.getRevocationMetadata(agentContext, { + nonRevokedInterval: credentialWithMetadata.nonRevoked as AnonCredsNonRevokedInterval, + timestamp: timestamp, + revocationRegistryId, + revocationRegistryIndex, + }) + }) + + return await Promise.all(revocationMetadataFetchPromises) + } + + private getClaimNameForField(field: FieldV2) { + if (!field.path) throw new CredoError('Field path is required') + // fixme: could the path start otherwise? + const baseClaimPath = '$.credentialSubject.' + const claimPaths = field.path.filter((path) => path.startsWith(baseClaimPath)) + if (claimPaths.length === 0) return undefined + + // FIXME: we should iterate over all attributes of the schema here and check if the path is valid + // see https://identity.foundation/presentation-exchange/#presentation-definition + const claimNames = claimPaths.map((path) => path.slice(baseClaimPath.length)) + const propertyName = claimNames[0] + + return propertyName + } + public createAnonCredsProofRequestAndMetadata = async ( agentContext: AgentContext, presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, @@ -396,12 +413,13 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg const nonRevokedInterval = { from: nonRevoked, to: nonRevoked } for (const descriptorMapObject of presentationSubmission.descriptor_map) { - // PresentationDefinitionV2 is the common denominator const descriptor: InputDescriptorV1 | InputDescriptorV2 | undefined = ( presentationDefinition.input_descriptors as InputDescriptorV2[] ).find((descriptor) => descriptor.id === descriptorMapObject.id) - if (!descriptor) + + if (!descriptor) { throw new Error(`Descriptor with id ${descriptorMapObject.id} not found in presentation definition`) + } const referent = descriptorMapObject.id const attributeReferent = `${referent}_attribute` @@ -411,60 +429,32 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg const fields = descriptor.constraints?.fields if (!fields) throw new CredoError('Unclear mapping of constraint with no fields.') - const { credential, entryIndex } = this.getCredential(descriptorMapObject, credentials) - const credentialJson = JsonTransformer.toJSON(credential) - const { credentialDefinitionId, revocationRegistryId, schemaId } = AnonCredsW3cCredential.fromJson(credentialJson) + const { entryIndex, schemaId, credentialDefinitionId, revocationRegistryId, anonCredsTags, credentialJson } = + this.getCredentialMetadataForDescriptor(descriptorMapObject, credentials, holderOpts?.selectedCredentialRecords) if (holderOpts) { - const credentialMetadata = this.getCredentialMetadata( - entryIndex, - credentials, - holderOpts.selectedCredentialRecords - ) + if (!anonCredsTags) throw new CredoError('Anoncreds tags are required for holder') schemaIds.add(schemaId) credentialDefinitionIds.add(credentialDefinitionId) - linkSecretIds.add(credentialMetadata.anonCredsTags.linkSecretId) + linkSecretIds.add(anonCredsTags.linkSecretId) } - let useNonRevoked = false - - const statuses = descriptor.constraints?.statuses - if (statuses) { - if ( - statuses?.active?.directive && - (statuses.active.directive === 'allowed' || statuses.active.directive === 'required') - ) { - if (!revocationRegistryId) { - throw new CredoError('Selected credentials must be revocable but are not') - } - useNonRevoked = true - } else { - throw new CredoError('Unsupported status directive') - } + const requiresRevocationStatus = this.descriptorRequiresRevocationStatus(descriptor) + if (requiresRevocationStatus && !revocationRegistryId) { + throw new CredoError('Selected credentials must be revocable but are not') } credentialsWithMetadata.push({ credential: credentialJson, - nonRevoked: useNonRevoked ? nonRevokedInterval : undefined, + nonRevoked: requiresRevocationStatus ? nonRevokedInterval : undefined, }) for (const field of fields) { - if (!field.path) throw new CredoError('Field path is required') - // fixme: could the path start otherwise? - const claimPaths = field.path?.filter((path) => path.startsWith('$.credentialSubject.')) - if (!claimPaths) throw new CredoError('No claim paths found') - if (claimPaths.length === 0) continue - - const claimNames = claimPaths.map((path) => { - const parts = path.split('$.credentialSubject.') - if (parts.length !== 2) throw new CredoError('Invalid claim path') - if (parts[1] === '') throw new CredoError('Invalid empty claim name') - return parts[1] - }) - - const propertyName = claimNames[0] + const propertyName = this.getClaimNameForField(field) + if (!propertyName) continue if (field.predicate) { + if (!field.filter) throw new CredoError('Missing required predicate filter property.') const predicateTypeAndValues = this.getPredicateTypeAndValues(field.filter) for (const { predicateType, predicateValue } of predicateTypeAndValues) { const predicateReferent = `${predicateReferentBase}_${predicateReferentIndex++}` @@ -473,7 +463,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg p_type: predicateType, p_value: predicateValue, restrictions: [{ cred_def_id: credentialDefinitionId }], - non_revoked: useNonRevoked ? nonRevokedInterval : undefined, + non_revoked: requiresRevocationStatus ? nonRevokedInterval : undefined, } credentialsProve.push({ entryIndex, referent: predicateReferent, isPredicate: true, reveal: true }) @@ -484,7 +474,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg name: propertyName, names: [propertyName], restrictions: [{ cred_def_id: credentialDefinitionId }], - non_revoked: useNonRevoked ? nonRevokedInterval : undefined, + non_revoked: requiresRevocationStatus ? nonRevokedInterval : undefined, } } else { const name = anonCredsProofRequest.requested_attributes[attributeReferent].name @@ -501,7 +491,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg const presentationMetadata = holderOpts ? await this.getPresentationMetadata(agentContext, { - credentialsWithMetadata: credentialsWithMetadata, + credentialsWithMetadata, credentialsProve, linkSecretIds, schemaIds, @@ -510,21 +500,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg : undefined const revocationMetadata = !holderOpts - ? await Promise.all( - credentialsWithMetadata - .filter((cwm) => cwm.nonRevoked) - .map(async (credentialWithMetadata) => { - const { revocationRegistryIndex, revocationRegistryId, timestamp } = AnonCredsW3cCredential.fromJson( - credentialWithMetadata.credential - ) - return await this.getRevocationMetadata(agentContext, { - nonRevokedInterval: credentialWithMetadata.nonRevoked as AnonCredsNonRevokedInterval, - timestamp: timestamp, - revocationRegistryId, - revocationRegistryIndex, - }) - }, true) - ) + ? await this.getRevocationMetadataForCredentials(agentContext, credentialsWithMetadata) : undefined return { anonCredsProofRequest, presentationMetadata, revocationMetadata } @@ -545,9 +521,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg presentationDefinition, presentationSubmission, selectedCredentials, - { - selectedCredentialRecords, - } + { selectedCredentialRecords } ) if (!presentationMetadata) throw new CredoError('Presentation metadata not created') @@ -570,11 +544,12 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg } } - public async verifyPresentation(agentContext: AgentContext, options: AnonCredsVcVerificationOptions) { + public async verifyPresentation(agentContext: AgentContext, options: Anoncreds2023VerificationOptions) { const { presentation, presentationDefinition, presentationSubmission } = options let anonCredsW3cPresentation: AnonCredsW3cPresentation | undefined let result = false + const credentialDefinitionIds = new Set() try { const verifiableCredentials = Array.isArray(presentation.verifiableCredential) @@ -583,7 +558,7 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg for (const verifiableCredential of verifiableCredentials) { if (verifiableCredential instanceof W3cJsonLdVerifiableCredential) { - const proof = this.getDataIntegrityProof(verifiableCredential, 'anoncreds-2023') + const proof = this.getDataIntegrityProof(verifiableCredential) credentialDefinitionIds.add(proof.verificationMethod) } else { throw new CredoError('Unsupported credential type') @@ -599,23 +574,22 @@ export class AnonCredsVc2023DataIntegrityService implements AnonCredsVcDataInteg ) if (!revocationMetadata) throw new CredoError('Missing revocation metadata') - const { credentialDefinitions, schemas } = await this.getCredentialDefinitionsAndSchemas( - agentContext, - undefined, - credentialDefinitionIds - ) + const credentialDefinitions = await this.getCredentialDefinitions(agentContext, credentialDefinitionIds) + const schemaIds = new Set(Object.values(credentialDefinitions).map((cd) => cd.schemaId)) + const schemas = await this.getSchemas(agentContext, schemaIds) + const presentationJson = JsonTransformer.toJSON(presentation) anonCredsW3cPresentation = AnonCredsW3cPresentation.fromJson(presentationJson) const revocationRegistryDefinitions: Record = {} revocationMetadata.forEach( - (rm) => (revocationRegistryDefinitions[rm.revocationRegistryDefinition[0]] = rm.revocationRegistryDefinition[1]) + (rm) => (revocationRegistryDefinitions[rm.revocationRegistryId] = rm.revocationRegistryDefinition) ) result = anonCredsW3cPresentation.verify({ presentationRequest: anonCredsProofRequest as unknown as JsonObject, schemas: schemas as unknown as Record, credentialDefinitions: credentialDefinitions as unknown as Record, - revocationRegistryDefinitions: revocationRegistryDefinitions, + revocationRegistryDefinitions, revocationStatusLists: revocationMetadata.map((rm) => rm.revocationStatusList), nonRevokedIntervalOverrides: revocationMetadata .filter((rm) => rm.nonRevokedIntervalOverride) diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts index 25167393c7..4a7a77d192 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts @@ -61,16 +61,13 @@ import { AnonCredsLinkSecretRepository } from '../repository' import { AnonCredsRegistryService } from '../services' import { fetchCredentialDefinition, - isQualifiedCredentialDefinition, legacyCredentialToW3cCredential, storeLinkSecret, unqualifiedCredentialDefinitionIdRegex, w3cToLegacyCredential, getQualifiedCredentialDefinition, getIndyNamespace, - isQualifiedRevocationRegistryDefinition, getQualifiedRevocationRegistryDefinition, - isQualifiedSchema, getQualifiedSchema, isIndyDid, getNonQualifiedId, @@ -301,7 +298,6 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } const w3cJsonLdCredential = await legacyCredentialToW3cCredential(credential, credentialDefinition, { - credentialDefinition: credentialDefinition as unknown as JsonObject, credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject, linkSecret: linkSecretRecord.value, revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, @@ -342,7 +338,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const credentialId = options.credentialId ?? utils.uuid() - const indyDid = isIndyDid(credentialDefinitionId) + const indyDid = isIndyDid(credential.issuerId) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) await w3cCredentialService.storeCredential(agentContext, { @@ -385,26 +381,19 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { revocationRegistry, } = options - let qualifiedCredentialDefinitionId: string - if (isDid(credentialDefinitionId)) { - qualifiedCredentialDefinitionId = credentialDefinitionId - } else { - const result = await fetchCredentialDefinition(agentContext, credentialDefinitionId) - qualifiedCredentialDefinitionId = result.qualifiedId - } + const qualifiedCredentialDefinitionId = isDid(credentialDefinitionId) + ? credentialDefinitionId + : (await fetchCredentialDefinition(agentContext, credentialDefinitionId)).qualifiedId - const qualifiedSchema = isQualifiedSchema(schema) - ? schema - : getQualifiedSchema(schema, getIndyNamespace(qualifiedCredentialDefinitionId)) + const qualifiedSchema = getQualifiedSchema(schema, getIndyNamespace(qualifiedCredentialDefinitionId)) - const qualifiedCredentialDefinition = isQualifiedCredentialDefinition(credentialDefinition) - ? credentialDefinition - : getQualifiedCredentialDefinition(credentialDefinition, getIndyNamespace(qualifiedCredentialDefinitionId)) + const qualifiedCredentialDefinition = getQualifiedCredentialDefinition( + credentialDefinition, + getIndyNamespace(qualifiedCredentialDefinitionId) + ) const qualifiedRevocationRegistryDefinition = !revocationRegistry?.definition ? undefined - : isQualifiedRevocationRegistryDefinition(revocationRegistry.definition) - ? revocationRegistry.definition : getQualifiedRevocationRegistryDefinition( revocationRegistry.definition, getIndyNamespace(qualifiedCredentialDefinitionId) @@ -415,8 +404,8 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { ? credential : await this.legacyToW3cCredential(agentContext, { credential, - credentialDefinition: qualifiedCredentialDefinition, credentialRequestMetadata, + credentialDefinition: qualifiedCredentialDefinition, revocationRegistryDefinition: qualifiedRevocationRegistryDefinition, }) @@ -442,8 +431,9 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } private anoncredsMetadataFromRecord(w3cCredentialRecord: W3cCredentialRecord): AnonCredsCredentialInfo { - if (Array.isArray(w3cCredentialRecord.credential.credentialSubject)) + if (Array.isArray(w3cCredentialRecord.credential.credentialSubject)) { throw new CredoError('Credential subject must be an object, not an array.') + } const anonCredsTags = w3cCredentialRecord.getAnonCredsTags() if (!anonCredsTags) throw new CredoError('AnonCreds tags not found on credential record.') diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts index d47f54f8e6..f9f565e53e 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts @@ -6,7 +6,7 @@ import type { JsonObject, NonRevokedIntervalOverride } from '@hyperledger/anoncr import { injectable } from '@credo-ts/core' import { Presentation } from '@hyperledger/anoncreds-shared' -import { AnonCredsRegistryService } from '../services' +import { fetchRevocationStatusList } from '../utils' @injectable() export class AnonCredsRsVerifierService implements AnonCredsVerifierService { @@ -110,14 +110,12 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { if (requestedFrom && requestedFrom > identifier.timestamp) { // Check VDR if the active revocation status list at requestedFrom was the one from provided timestamp. // If it matches, add to the override list - const registry = agentContext.dependencyManager - .resolve(AnonCredsRegistryService) - .getRegistryForIdentifier(agentContext, identifier.rev_reg_id) - const { revocationStatusList } = await registry.getRevocationStatusList( + const { revocationStatusList } = await fetchRevocationStatusList( agentContext, identifier.rev_reg_id, requestedFrom ) + const vdrTimestamp = revocationStatusList?.timestamp if (vdrTimestamp && vdrTimestamp === identifier.timestamp) { nonRevokedIntervalOverrides.push({ diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts index a96ca64e60..b56a57026d 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts @@ -42,12 +42,7 @@ import { createLinkSecret, } from './helpers' -import { - AnonCredsModuleConfig, - AnonCredsHolderServiceSymbol, - AnonCredsLinkSecretRecord, - AnonCredsCredentialRecord, -} from '@credo-ts/anoncreds' +import { AnonCredsModuleConfig, AnonCredsHolderServiceSymbol, AnonCredsLinkSecretRecord } from '@credo-ts/anoncreds' const agentConfig = getAgentConfig('AnonCredsRsHolderServiceTest') const anonCredsHolderService = new AnonCredsRsHolderService() diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index c93e814cfd..77fd4c23aa 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -5,7 +5,7 @@ import type { AnonCredsCredentialRequest, AnonCredsCredentialRequestMetadata, } from '../models' -import type { AnonCredsIssuerService, AnonCredsHolderService, GetRevocationRegistryDefinitionReturn } from '../services' +import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' import type { AnonCredsCredentialMetadata } from '../utils/metadata' import type { CredentialFormatService, @@ -41,7 +41,6 @@ import { JsonTransformer, } from '@credo-ts/core' -import { AnonCredsError } from '../error' import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal' import { AnonCredsCredentialDefinitionRepository, @@ -49,8 +48,13 @@ import { AnonCredsRevocationRegistryState, } from '../repository' import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' -import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' -import { dateToTimestamp } from '../utils' +import { + dateToTimestamp, + fetchCredentialDefinition, + fetchRevocationRegistryDefinition, + fetchRevocationStatusList, + fetchSchema, +} from '../utils' import { convertAttributesToCredentialValues, assertCredentialValuesMatch, @@ -226,23 +230,12 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService credentialFormats, }: CredentialFormatAcceptOfferOptions ): 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 { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialOffer.cred_def_id) const { credentialRequest, credentialRequestMetadata } = await holderService.createCredentialRequest(agentContext, { credentialOffer, @@ -344,18 +337,11 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService ) } - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - const revocationStatusListResult = await registryService - .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) - .getRevocationStatusList(agentContext, revocationRegistryDefinitionId, dateToTimestamp(new Date())) - - if (!revocationStatusListResult.revocationStatusList) { - throw new CredoError( - `Unable to resolve revocation status list for ${revocationRegistryDefinitionId}: - ${revocationStatusListResult.resolutionMetadata.error} ${revocationStatusListResult.resolutionMetadata.message}` - ) - } - + const revocationStatusListResult = await fetchRevocationStatusList( + agentContext, + revocationRegistryDefinitionId, + dateToTimestamp(new Date()) + ) revocationStatusList = revocationStatusListResult.revocationStatusList } @@ -390,7 +376,6 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService AnonCredsCredentialRequestMetadataKey ) - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) const anonCredsHolderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) @@ -406,37 +391,16 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const anonCredsCredential = attachment.getDataAsJson() - const credentialDefinitionResult = await registryService - .getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id) - .getCredentialDefinition(agentContext, anonCredsCredential.cred_def_id) - if (!credentialDefinitionResult.credentialDefinition) { - throw new CredoError( - `Unable to resolve credential definition ${anonCredsCredential.cred_def_id}: ${credentialDefinitionResult.resolutionMetadata.error} ${credentialDefinitionResult.resolutionMetadata.message}` - ) - } - - const schemaResult = await registryService - .getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id) - .getSchema(agentContext, anonCredsCredential.schema_id) - if (!schemaResult.schema) { - throw new CredoError( - `Unable to resolve schema ${anonCredsCredential.schema_id}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` - ) - } + const { credentialDefinition, id: credentialDefinitionId } = await fetchCredentialDefinition( + agentContext, + anonCredsCredential.cred_def_id + ) + const { schema } = await fetchSchema(agentContext, anonCredsCredential.schema_id) // Resolve revocation registry if credential is revocable - let revocationRegistryResult: null | GetRevocationRegistryDefinitionReturn = null - if (anonCredsCredential.rev_reg_id) { - revocationRegistryResult = await registryService - .getRegistryForIdentifier(agentContext, anonCredsCredential.rev_reg_id) - .getRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id) - - if (!revocationRegistryResult.revocationRegistryDefinition) { - throw new CredoError( - `Unable to resolve revocation registry definition ${anonCredsCredential.rev_reg_id}: ${revocationRegistryResult.resolutionMetadata.error} ${revocationRegistryResult.resolutionMetadata.message}` - ) - } - } + const revocationRegistryResult = anonCredsCredential.rev_reg_id + ? await fetchRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id) + : undefined // assert the credential values match the offer values const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) @@ -446,13 +410,13 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService credentialId: utils.uuid(), credentialRequestMetadata, credential: anonCredsCredential, - credentialDefinitionId: credentialDefinitionResult.credentialDefinitionId, - credentialDefinition: credentialDefinitionResult.credentialDefinition, - schema: schemaResult.schema, + credentialDefinitionId, + credentialDefinition, + schema, revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition ? { definition: revocationRegistryResult.revocationRegistryDefinition, - id: revocationRegistryResult.revocationRegistryDefinitionId, + id: revocationRegistryResult.id, } : undefined, }) @@ -644,18 +608,9 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService offer: AnonCredsCredentialOffer, attributes: CredentialPreviewAttributeOptions[] ): 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 CredoError( - `Unable to resolve schema ${offer.schema_id} from registry: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` - ) - } + const { schema } = await fetchSchema(agentContext, offer.schema_id) - assertAttributesMatch(schemaResult.schema, attributes) + assertAttributesMatch(schema, attributes) } /** diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index 0db665303f..f7bc96cb42 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -47,7 +47,6 @@ import { import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest' import { AnonCredsVerifierServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' -import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' import { sortRequestedCredentialsMatches, createRequestFromPreview, @@ -59,6 +58,7 @@ import { getRevocationRegistriesForProof, fetchSchema, fetchCredentialDefinition, + fetchRevocationStatusList, } from '../utils' import { dateToTimestamp } from '../utils/timestamp' @@ -516,23 +516,13 @@ export class AnonCredsProofFormatService implements ProofFormatService() - // validate the credential + // TODO: validate the credential JsonTransformer.fromJSON(credential, W3cCredential) const missingBindingMethod = @@ -322,7 +323,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer let anonCredsLinkSecretDataIntegrityBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof | undefined = undefined - if (dataIntegrityFormat.anonCredsLinkSecretCredentialRequestOptions) { + if (dataIntegrityFormat.anonCredsLinkSecretAcceptOfferOptions) { if (!credentialOffer.binding_method?.anoncreds_link_secret) { throw new CredoError('Cannot request credential with a binding method that was not offered.') } @@ -342,7 +343,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer schema_id: credentialDefinitionReturn.credentialDefinition.schemaId, }, credentialDefinition: credentialDefinitionReturn.credentialDefinition, - linkSecretId: dataIntegrityFormat.anonCredsLinkSecretCredentialRequestOptions?.linkSecretId, + linkSecretId: dataIntegrityFormat.anonCredsLinkSecretAcceptOfferOptions?.linkSecretId, }) dataIntegrityRequestMetadata.linkSecretRequestMetadata = anonCredsCredentialRequestMetadata @@ -352,16 +353,14 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer schemaId: credentialDefinitionReturn.credentialDefinition.schemaId, } - if (!anonCredsCredentialRequest.entropy) { - throw new CredoError('Missing entropy for anonCredsCredentialRequest') - } + if (!anonCredsCredentialRequest.entropy) throw new CredoError('Missing entropy for anonCredsCredentialRequest') anonCredsLinkSecretDataIntegrityBindingProof = anonCredsCredentialRequest as AnonCredsLinkSecretDataIntegrityBindingProof } let didCommSignedAttachmentBindingProof: DidCommSignedAttachmentDataIntegrityBindingProof | undefined = undefined let didCommSignedAttachment: Attachment | undefined = undefined - if (dataIntegrityFormat.didCommSignedAttachmentCredentialRequestOptions) { + if (dataIntegrityFormat.didCommSignedAttachmentAcceptOfferOptions) { if (!credentialOffer.binding_method?.didcomm_signed_attachment) { throw new CredoError('Cannot request credential with a binding method that was not offered.') } @@ -369,7 +368,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer didCommSignedAttachment = await this.createSignedAttachment( agentContext, { nonce: credentialOffer.binding_method.didcomm_signed_attachment.nonce }, - dataIntegrityFormat.didCommSignedAttachmentCredentialRequestOptions, + dataIntegrityFormat.didCommSignedAttachmentAcceptOfferOptions, credentialOffer.binding_method.didcomm_signed_attachment.algs_supported ) @@ -384,9 +383,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer didcomm_signed_attachment: didCommSignedAttachmentBindingProof, } - if (credentialOffer.binding_required && !bindingProof) { - throw new CredoError('Missing required binding proof') - } + if (credentialOffer.binding_required && !bindingProof) throw new CredoError('Missing required binding proof') const dataModelVersion = dataIntegrityFormat.dataModelVersion ?? credentialOffer.data_model_versions_supported[0] if (!credentialOffer.data_model_versions_supported.includes(dataModelVersion)) { @@ -501,17 +498,11 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ) } - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - const revocationStatusListResult = await registryService - .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) - .getRevocationStatusList(agentContext, revocationRegistryDefinitionId, dateToTimestamp(new Date())) - - if (!revocationStatusListResult.revocationStatusList) { - throw new CredoError( - `Unable to resolve revocation status list for ${revocationRegistryDefinitionId}: - ${revocationStatusListResult.resolutionMetadata.error} ${revocationStatusListResult.resolutionMetadata.message}` - ) - } + const revocationStatusListResult = await fetchRevocationStatusList( + agentContext, + revocationRegistryDefinitionId, + dateToTimestamp(new Date()) + ) revocationStatusList = revocationStatusListResult.revocationStatusList } @@ -599,21 +590,23 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credentialToBeSigned = _credentialToBeSigned as W3cCredential } - const signed = (await w3cCredentialService.signCredential(agentContext, { + const w3cJsonLdVerifiableCredential = (await w3cCredentialService.signCredential(agentContext, { format: ClaimFormat.LdpVc, credential: credentialToBeSigned as W3cCredential, proofType: signatureSuite.proofType, verificationMethod: verificationMethod.id, })) as W3cJsonLdVerifiableCredential - if (Array.isArray(signed.proof)) throw new CredoError('A newly signed credential can not have multiple proofs') + if (Array.isArray(w3cJsonLdVerifiableCredential.proof)) { + throw new CredoError('A newly signed credential can not have multiple proofs') + } if (credential instanceof W3cJsonLdVerifiableCredential) { const combinedProofs = Array.isArray(credential.proof) ? credential.proof : [credential.proof] - combinedProofs.push(signed.proof) - signed.proof = combinedProofs + combinedProofs.push(w3cJsonLdVerifiableCredential.proof) + w3cJsonLdVerifiableCredential.proof = combinedProofs } - return signed + return w3cJsonLdVerifiableCredential } public async acceptRequest( @@ -827,6 +820,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer true ) } else { + // TODO: check the sturcture of the credential w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) } @@ -978,9 +972,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const anoncredsCredentialOffer = await agentContext.dependencyManager .resolve(AnonCredsIssuerServiceSymbol) - .createCredentialOffer(agentContext, { - credentialDefinitionId, - }) + .createCredentialOffer(agentContext, { credentialDefinitionId }) // We check locally for credential definition info. If it supports revocation, revocationRegistryIndex // and revocationRegistryDefinitionId are mandatory diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts index 7e81155445..5e7ec57ae3 100644 --- a/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormat.ts @@ -31,8 +31,7 @@ export interface LegacyIndyCredentialRequest extends AnonCredsCredentialRequest export interface LegacyIndyCredentialFormat extends CredentialFormat { formatKey: 'indy' - // The stored type is the same as the anoncreds credential service - credentialRecordType: 'anoncreds' + credentialRecordType: 'w3c' // credential formats are the same as the AnonCreds credential format credentialFormats: { diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts index 1ba4644430..545fd8db06 100644 --- a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -5,7 +5,7 @@ import type { AnonCredsCredentialRequest, AnonCredsCredentialRequestMetadata, } from '../models' -import type { AnonCredsIssuerService, AnonCredsHolderService, GetRevocationRegistryDefinitionReturn } from '../services' +import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' import type { AnonCredsCredentialMetadata } from '../utils/metadata' import type { CredentialFormatService, @@ -41,10 +41,9 @@ import { JsonTransformer, } from '@credo-ts/core' -import { AnonCredsError } from '../error' import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal' import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' -import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' +import { fetchCredentialDefinition, fetchRevocationRegistryDefinition, fetchSchema } from '../utils' import { convertAttributesToCredentialValues, assertCredentialValuesMatch, @@ -69,7 +68,7 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic * credentialRecordType is the type of record that stores the credential. It is stored in the credential * record binding in the credential exchange record. */ - public readonly credentialRecordType = 'anoncreds' as const + public readonly credentialRecordType = 'w3c' as const /** * Create a {@link AttachmentFormats} object dependent on the message type. @@ -224,7 +223,6 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic credentialFormats, }: CredentialFormatAcceptOfferOptions ): Promise { - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) const credentialOffer = offerAttachment.getDataAsJson() @@ -233,17 +231,7 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic throw new CredoError(`${credentialOffer.cred_def_id} is not a valid legacy indy credential definition id`) } // 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 { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialOffer.cred_def_id) const { credentialRequest, credentialRequestMetadata } = await holderService.createCredentialRequest(agentContext, { credentialOffer, @@ -356,7 +344,6 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic AnonCredsCredentialRequestMetadataKey ) - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) const anonCredsHolderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) @@ -372,37 +359,16 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic const anonCredsCredential = attachment.getDataAsJson() - const credentialDefinitionResult = await registryService - .getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id) - .getCredentialDefinition(agentContext, anonCredsCredential.cred_def_id) - if (!credentialDefinitionResult.credentialDefinition) { - throw new CredoError( - `Unable to resolve credential definition ${anonCredsCredential.cred_def_id}: ${credentialDefinitionResult.resolutionMetadata.error} ${credentialDefinitionResult.resolutionMetadata.message}` - ) - } - - const schemaResult = await registryService - .getRegistryForIdentifier(agentContext, anonCredsCredential.cred_def_id) - .getSchema(agentContext, anonCredsCredential.schema_id) - if (!schemaResult.schema) { - throw new CredoError( - `Unable to resolve schema ${anonCredsCredential.schema_id}: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` - ) - } + const { credentialDefinition, id: credentialDefinitionId } = await fetchCredentialDefinition( + agentContext, + anonCredsCredential.cred_def_id + ) + const { schema } = await fetchSchema(agentContext, anonCredsCredential.schema_id) // Resolve revocation registry if credential is revocable - let revocationRegistryResult: null | GetRevocationRegistryDefinitionReturn = null - if (anonCredsCredential.rev_reg_id) { - revocationRegistryResult = await registryService - .getRegistryForIdentifier(agentContext, anonCredsCredential.rev_reg_id) - .getRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id) - - if (!revocationRegistryResult.revocationRegistryDefinition) { - throw new CredoError( - `Unable to resolve revocation registry definition ${anonCredsCredential.rev_reg_id}: ${revocationRegistryResult.resolutionMetadata.error} ${revocationRegistryResult.resolutionMetadata.message}` - ) - } - } + const revocationRegistryResult = anonCredsCredential.rev_reg_id + ? await fetchRevocationRegistryDefinition(agentContext, anonCredsCredential.rev_reg_id) + : undefined // assert the credential values match the offer values const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) @@ -412,13 +378,13 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic credentialId: utils.uuid(), credentialRequestMetadata, credential: anonCredsCredential, - credentialDefinitionId: credentialDefinitionResult.credentialDefinitionId, - credentialDefinition: credentialDefinitionResult.credentialDefinition, - schema: schemaResult.schema, + credentialDefinitionId, + credentialDefinition, + schema, revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition ? { definition: revocationRegistryResult.revocationRegistryDefinition, - id: revocationRegistryResult.revocationRegistryDefinitionId, + id: revocationRegistryResult.id, } : undefined, }) @@ -576,18 +542,8 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic offer: AnonCredsCredentialOffer, attributes: CredentialPreviewAttributeOptions[] ): 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 CredoError( - `Unable to resolve schema ${offer.schema_id} from registry: ${schemaResult.resolutionMetadata.error} ${schemaResult.resolutionMetadata.message}` - ) - } - - assertAttributesMatch(schemaResult.schema, attributes) + const { schema } = await fetchSchema(agentContext, offer.schema_id) + assertAttributesMatch(schema, attributes) } /** diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts index 01b641b612..6b32b34dd9 100644 --- a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts @@ -47,7 +47,6 @@ import { import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest' import { AnonCredsVerifierServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' -import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' import { sortRequestedCredentialsMatches, createRequestFromPreview, @@ -60,7 +59,7 @@ import { fetchSchema, fetchCredentialDefinition, } from '../utils' -import { getUnQualifiedId } from '../utils/ledgerObjects' +import { fetchRevocationStatusList, getUnQualifiedId } from '../utils/ledgerObjects' import { dateToTimestamp } from '../utils/timestamp' const V2_INDY_PRESENTATION_PROPOSAL = 'hlindy/proof-req@v2.0' @@ -519,23 +518,14 @@ export class LegacyIndyProofFormatService implements ProofFormatService { }) expect(holderCredentialRecord.credentials).toEqual([ - { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, ]) const credentialId = holderCredentialRecord.credentials[0].credentialRecordId diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index 0b44c85e2a..39426f706e 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -35,4 +35,5 @@ export { isQualifiedCredentialDefinition, isQualifiedRevocationRegistryDefinition, isQualifiedSchema, + fetchRevocationStatusList, } from './utils/ledgerObjects' diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts index 1ed8450eba..3b4a66d16f 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts @@ -122,7 +122,7 @@ describe('V1 Connectionless Credentials', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], @@ -204,7 +204,7 @@ describe('V1 Connectionless Credentials', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts index 14b899cd2a..b4896b544b 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts @@ -137,7 +137,7 @@ describe('V1 Credentials Auto Accept', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], @@ -239,7 +239,7 @@ describe('V1 Credentials Auto Accept', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], @@ -327,7 +327,7 @@ describe('V1 Credentials Auto Accept', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], diff --git a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts index 2941f43f27..b815dc47dd 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts @@ -25,9 +25,12 @@ import { AnonCredsCredentialRecord } from '../../../repository' import { AnonCredsRegistryService } from '../../../services' import * as testModule from '../anonCredsCredentialRecord' +import { anoncreds } from './../../../../tests/helpers' + const agentConfig = getAgentConfig('Migration AnonCreds Credential Records 0.4-0.5') const registry = new InMemoryAnonCredsRegistry() const anonCredsModuleConfig = new AnonCredsModuleConfig({ + anoncreds, registries: [registry], }) diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index 17f2f684e0..4cd04a00be 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -35,4 +35,5 @@ export { isQualifiedCredentialDefinition, isQualifiedRevocationRegistryDefinition, isQualifiedSchema, + fetchRevocationStatusList, } from './ledgerObjects' diff --git a/packages/anoncreds/src/utils/ledgerObjects.ts b/packages/anoncreds/src/utils/ledgerObjects.ts index c7fd13d256..c791890ef6 100644 --- a/packages/anoncreds/src/utils/ledgerObjects.ts +++ b/packages/anoncreds/src/utils/ledgerObjects.ts @@ -1,4 +1,9 @@ -import type { AnonCredsCredentialDefinition, AnonCredsRevocationRegistryDefinition, AnonCredsSchema } from '../models' +import type { + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, + AnonCredsSchema, +} from '../models' import type { AgentContext } from '@credo-ts/core' import { isDid, CredoError } from '@credo-ts/core' @@ -89,19 +94,14 @@ export function getQualifiedId(identifier: string, namespace: string) { if (isUnqualifiedSchemaId(identifier)) { const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(identifier) const schemaId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/SCHEMA/${schemaName}/${schemaVersion}` - //if (isDidIndySchemaId(schemaId)) throw new Error(`schemaid conversion error: ${schemaId}`) return schemaId } else if (isUnqualifiedCredentialDefinitionId(identifier)) { const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(identifier) const credentialDefinitionId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` - //if (isDidIndyCredentialDefinitionId(credentialDefinitionId)) - // throw new Error(`credentialdefintiion id conversion error: ${credentialDefinitionId}`) return credentialDefinitionId } else if (isUnqualifiedRevocationRegistryId(identifier)) { const { namespaceIdentifier, schemaSeqNo, revocationRegistryTag } = parseIndyRevocationRegistryId(identifier) const revocationRegistryId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${revocationRegistryTag}` - //if (isDidIndyRevocationRegistryId(revocationRegistryId)) - // throw new Error(`revocationregistry id conversion error: ${revocationRegistryId}`) return revocationRegistryId } @@ -109,7 +109,7 @@ export function getQualifiedId(identifier: string, namespace: string) { } export function getUnqualifiedSchema(schema: AnonCredsSchema): AnonCredsSchema { - if (!isIndyDid(schema.issuerId)) return schema + if (!isIndyDid(schema.issuerId)) return { ...schema } const issuerId = getUnQualifiedId(schema.issuerId) return { ...schema, issuerId } @@ -120,7 +120,7 @@ export function isQualifiedSchema(schema: AnonCredsSchema) { } export function getQualifiedSchema(schema: AnonCredsSchema, namespace: string): AnonCredsSchema { - if (isQualifiedSchema(schema)) return schema + if (isQualifiedSchema(schema)) return { ...schema } return { ...schema, @@ -165,7 +165,7 @@ export function getUnqualifiedCredentialDefinition( anonCredsCredentialDefinition: AnonCredsCredentialDefinition ): AnonCredsCredentialDefinition { if (!isIndyDid(anonCredsCredentialDefinition.issuerId) || !isIndyDid(anonCredsCredentialDefinition.schemaId)) { - return anonCredsCredentialDefinition + return { ...anonCredsCredentialDefinition } } const issuerId = getUnQualifiedId(anonCredsCredentialDefinition.issuerId) const schemaId = getUnQualifiedId(anonCredsCredentialDefinition.schemaId) @@ -231,7 +231,7 @@ export function getUnqualifiedRevocationRegistryDefinition( revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition ): AnonCredsRevocationRegistryDefinition { if (!isIndyDid(revocationRegistryDefinition.issuerId) || !isIndyDid(revocationRegistryDefinition.credDefId)) { - return revocationRegistryDefinition + return { ...revocationRegistryDefinition } } const issuerId = getUnQualifiedId(revocationRegistryDefinition.issuerId) @@ -302,3 +302,27 @@ export async function fetchRevocationRegistryDefinition( unqualifiedRevocationRegistryDefinition, } } + +export async function fetchRevocationStatusList( + agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number +): Promise<{ revocationStatusList: AnonCredsRevocationStatusList }> { + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, revocationRegistryId) + + const { revocationStatusList, resolutionMetadata } = await registry.getRevocationStatusList( + agentContext, + revocationRegistryId, + timestamp + ) + + if (!revocationStatusList) { + throw new CredoError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` + ) + } + + return { revocationStatusList } +} diff --git a/packages/anoncreds/src/utils/w3cUtils.ts b/packages/anoncreds/src/utils/w3cUtils.ts index e2ff49b20a..f6f54c5bf3 100644 --- a/packages/anoncreds/src/utils/w3cUtils.ts +++ b/packages/anoncreds/src/utils/w3cUtils.ts @@ -7,7 +7,7 @@ import { Credential, W3cCredential } from '@hyperledger/anoncreds-shared' export async function legacyCredentialToW3cCredential( legacyCredential: AnonCredsCredential, credentialDefinition: AnonCredsCredentialDefinition, - process?: ProcessCredentialOptions + process?: Omit ) { let credential: W3cJsonLdVerifiableCredential let anonCredsCredential: Credential | undefined @@ -21,11 +21,12 @@ export async function legacyCredentialToW3cCredential( w3cVersion: '1.1', }) - processed = process - ? w3cCredentialObj.process({ ...process, credentialDefinition: credentialDefinition as unknown as JsonObject }) - : w3cCredentialObj + const jsonObject = process + ? w3cCredentialObj + .process({ ...process, credentialDefinition: credentialDefinition as unknown as JsonObject }) + .toJson() + : w3cCredentialObj.toJson() - const jsonObject = processed.toJson() credential = JsonTransformer.fromJSON(jsonObject, W3cJsonLdVerifiableCredential) } finally { anonCredsCredential?.handle?.clear() diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 7558cb4cdf..a156cd3dc8 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -19,8 +19,7 @@ import type { } from '../src' import type { AgentContext } from '@credo-ts/core' -import { isDid } from '@credo-ts/core' -import { Hasher } from '@credo-ts/core' +import { Hasher, isDid } from '@credo-ts/core' import BigNumber from 'bn.js' import { diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts index 222e8b251b..c0b39307d1 100644 --- a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts @@ -363,7 +363,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean credentialFormats: { dataIntegrity: { dataModelVersion: '1.1', - anonCredsLinkSecretCredentialRequestOptions: { + anonCredsLinkSecretAcceptOfferOptions: { linkSecretId: linkSecret.linkSecretId, }, }, diff --git a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts index 1d444345ed..3c750161c8 100644 --- a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts @@ -219,7 +219,7 @@ async function anonCredsFlowTest(options: { offerAttachment, credentialFormats: { dataIntegrity: { - didCommSignedAttachmentCredentialRequestOptions: { + didCommSignedAttachmentAcceptOfferOptions: { kid: holder.kid, }, }, diff --git a/packages/anoncreds/tests/data-integrity.e2e.test.ts b/packages/anoncreds/tests/data-integrity.e2e.test.ts index 5c03bf31c3..0137b1baa5 100644 --- a/packages/anoncreds/tests/data-integrity.e2e.test.ts +++ b/packages/anoncreds/tests/data-integrity.e2e.test.ts @@ -196,7 +196,7 @@ async function anonCredsFlowTest(options: { autoAcceptCredential: AutoAcceptCredential.Never, credentialFormats: { dataIntegrity: { - anonCredsLinkSecretCredentialRequestOptions: { + anonCredsLinkSecretAcceptOfferOptions: { linkSecretId: 'linkSecretId', }, }, diff --git a/packages/anoncreds/tests/indy-flow.test.ts b/packages/anoncreds/tests/indy-flow.test.ts index a39aeb1cca..b00fdaceaa 100644 --- a/packages/anoncreds/tests/indy-flow.test.ts +++ b/packages/anoncreds/tests/indy-flow.test.ts @@ -295,7 +295,7 @@ describe('Legacy indy format services using anoncreds-rs', () => { }) expect(holderCredentialRecord.credentials).toEqual([ - { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, ]) const credentialId = holderCredentialRecord.credentials[0].credentialRecordId diff --git a/packages/core/package.json b/packages/core/package.json index 14ac3d2cbe..545abc4713 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,7 +30,7 @@ "@sd-jwt/core": "^0.2.0", "@sd-jwt/decode": "^0.2.0", "@sphereon/pex": "^3.0.1", - "@sphereon/pex-models": "^2.1.5", + "@sphereon/pex-models": "^2.2.0", "@sphereon/ssi-types": "^0.18.1", "@stablelib/ed25519": "^1.0.2", "@stablelib/sha256": "^1.0.1", diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsDataIntegrityService.ts similarity index 62% rename from packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService.ts rename to packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsDataIntegrityService.ts index df22bdda7a..5b25c52d70 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsDataIntegrityService.ts @@ -3,20 +3,20 @@ import type { JsonObject } from '../../../../types' import type { W3cCredentialRecord, W3cJsonLdVerifiablePresentation } from '../../../vc' import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' -export interface AnonCredsVcSignatureOptions extends Record { +export interface Anoncreds2023SignatureOptions extends Record { presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 presentationSubmission: PresentationSubmission selectedCredentials: JsonObject[] selectedCredentialRecords: W3cCredentialRecord[] } -export interface AnonCredsVcVerificationOptions extends Record { +export interface Anoncreds2023VerificationOptions extends Record { presentation: W3cJsonLdVerifiablePresentation presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 presentationSubmission: PresentationSubmission } -export const anonCredsVcDataIntegrityServiceSymbol = Symbol('AnonCredsVcDataIntegrityService') +export const anoncreds2023DataIntegrityServiceSymbol = Symbol('AnonCredsVcDataIntegrityService') /** * We keep this standalone and don't integrity it @@ -24,8 +24,8 @@ export const anonCredsVcDataIntegrityServiceSymbol = Symbol('AnonCredsVcDataInte * to it's unique properties, in order to not pollute, * the existing api's. */ -export interface AnonCredsVcDataIntegrityService { - createPresentation(agentContext: AgentContext, options: AnonCredsVcSignatureOptions): Promise +export interface Anoncreds2023DataIntegrityService { + createPresentation(agentContext: AgentContext, options: Anoncreds2023SignatureOptions): Promise - verifyPresentation(agentContext: AgentContext, options: AnonCredsVcVerificationOptions): Promise + verifyPresentation(agentContext: AgentContext, options: Anoncreds2023VerificationOptions): Promise } diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts index 90a1071a26..73f5235b06 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts @@ -26,8 +26,8 @@ export interface DidCommSignedAttachmentBindingMethodOptions { */ export interface DataIntegrityAcceptOfferFormat { dataModelVersion?: W3C_VC_DATA_MODEL_VERSION - didCommSignedAttachmentCredentialRequestOptions?: DidCommSignedAttachmentCredentialRequestOptions - anonCredsLinkSecretCredentialRequestOptions?: AnonCredsLinkSecretCredentialRequestOptions + didCommSignedAttachmentAcceptOfferOptions?: DidCommSignedAttachmentCredentialRequestOptions + anonCredsLinkSecretAcceptOfferOptions?: AnonCredsLinkSecretCredentialRequestOptions } /** diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts index 7474d533d7..7ce262d5d5 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts @@ -1,4 +1,4 @@ export * from './DataIntegrityCredentialFormat' export * from './dataIntegrityExchange' export * from './dataIntegrityMetadata' -export * from './AnonCredsVcDataIntegrityService' +export * from './AnonCredsDataIntegrityService' diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 6d042ccd59..913c0c680f 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -11,7 +11,7 @@ import type { PresentationToCreate } from './utils' import type { AgentContext } from '../../agent' import type { Query } from '../../storage/StorageService' import type { JsonObject } from '../../types' -import type { AnonCredsVcDataIntegrityService } from '../credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService' +import type { Anoncreds2023DataIntegrityService } from '../credentials/formats/dataIntegrity/AnonCredsDataIntegrityService' import type { VerificationMethod } from '../dids' import type { SdJwtVcRecord } from '../sd-jwt-vc' import type { W3cCredentialRecord } from '../vc' @@ -21,7 +21,7 @@ import type { Validated, VerifiablePresentationResult, } from '@sphereon/pex' -import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' +import type { InputDescriptorV2, PresentationDefinitionV1, PresentationSubmission } from '@sphereon/pex-models' import type { W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, W3CVerifiablePresentation, @@ -33,7 +33,7 @@ import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../crypto' import { CredoError } from '../../error' import { Hasher, JsonTransformer } from '../../utils' -import { anonCredsVcDataIntegrityServiceSymbol } from '../credentials/formats/dataIntegrity/AnonCredsVcDataIntegrityService' +import { anoncreds2023DataIntegrityServiceSymbol } from '../credentials/formats/dataIntegrity/AnonCredsDataIntegrityService' import { DidsApi, getKeyFromVerificationMethod } from '../dids' import { SdJwtVcApi } from '../sd-jwt-vc' import { @@ -399,6 +399,28 @@ export class DifPresentationExchangeService { return supportedSignatureSuites[0].proofType } + private signUsingAnoncreds2023( + presentationToCreate: PresentationToCreate, + presentationSubmission: PresentationSubmission + ) { + if (presentationToCreate.claimFormat !== ClaimFormat.LdpVp) return undefined + + const cryptosuites = presentationToCreate.verifiableCredentials.map((verifiableCredentials) => { + const inputDescriptor = presentationSubmission.descriptor_map.find( + (descriptor) => descriptor.id === verifiableCredentials.inputDescriptorId + ) + + return inputDescriptor?.format === 'di_vp' && + verifiableCredentials.credential.credential instanceof W3cJsonLdVerifiableCredential + ? verifiableCredentials.credential.credential.cryptoSuites + : [] + }) + + const commonCryptoSuites = cryptosuites.reduce((a, b) => a.filter((c) => b.includes(c))) + if (commonCryptoSuites.length === 0 || !commonCryptoSuites.includes('anoncreds-2023')) return undefined + return true + } + private getPresentationSignCallback(agentContext: AgentContext, presentationToCreate: PresentationToCreate) { return async (callBackParams: PresentationSignCallBackParams) => { // The created partial proof and presentation, as well as original supplied options @@ -436,38 +458,19 @@ export class DifPresentationExchangeService { return signedPresentation.encoded as W3CVerifiablePresentation } else if (presentationToCreate.claimFormat === ClaimFormat.LdpVp) { - const cryptosuites = presentationToCreate.verifiableCredentials.map((verifiableCredentials) => { - const format = presentationSubmission.descriptor_map.find( - (descriptor) => descriptor.id === verifiableCredentials.inputDescriptorId - )?.format - if ( - format === 'di_vp' && - verifiableCredentials.credential.credential instanceof W3cJsonLdVerifiableCredential - ) { - return verifiableCredentials.credential.credential.cryptoSuites - } else { - return [] - } - }) - - const commonCryptoSuites = cryptosuites.reduce((a, b) => a.filter((c) => b.includes(c))) - if (commonCryptoSuites.length > 0) { - if (commonCryptoSuites.includes('anoncreds-2023')) { - const anonCredsVcDataIntegrityService = - agentContext.dependencyManager.resolve( - anonCredsVcDataIntegrityServiceSymbol - ) - const presentation = await anonCredsVcDataIntegrityService.createPresentation(agentContext, { - presentationDefinition, - presentationSubmission, - selectedCredentials: selectedCredentials as JsonObject[], - selectedCredentialRecords: presentationToCreate.verifiableCredentials.map((vc) => vc.credential), - }) - presentation.presentation_submission = presentationSubmission as unknown as JsonObject - return presentation as unknown as SphereonW3cVerifiablePresentation - } else { - throw new DifPresentationExchangeError(`Unsupported cryptosuites '${commonCryptoSuites.join(', ')}'`) - } + if (this.signUsingAnoncreds2023(presentationToCreate, presentationSubmission)) { + const anoncredsDataIntegrityService = + agentContext.dependencyManager.resolve( + anoncreds2023DataIntegrityServiceSymbol + ) + const presentation = await anoncredsDataIntegrityService.createPresentation(agentContext, { + presentationDefinition, + presentationSubmission, + selectedCredentials: selectedCredentials as JsonObject[], + selectedCredentialRecords: presentationToCreate.verifiableCredentials.map((vc) => vc.credential), + }) + presentation.presentation_submission = presentationSubmission as unknown as JsonObject + return presentation as unknown as SphereonW3cVerifiablePresentation } // Determine a suitable verification method for the presentation diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 6c783db2e3..ef29d413af 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -6,7 +6,7 @@ import type { } from './DifPresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' import type { JsonValue } from '../../../../types' -import type { AnonCredsVcDataIntegrityService } from '../../../credentials' +import type { Anoncreds2023DataIntegrityService } from '../../../credentials' import type { DifPexInputDescriptorToCredentials } from '../../../dif-presentation-exchange' import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' @@ -25,11 +25,12 @@ import type { ProofFormatAutoRespondRequestOptions, ProofFormatAutoRespondPresentationOptions, } from '../ProofFormatServiceOptions' +import type { PresentationSubmission } from '@sphereon/pex-models' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { CredoError } from '../../../../error' import { deepEquality, JsonTransformer } from '../../../../utils' -import { anonCredsVcDataIntegrityServiceSymbol } from '../../../credentials' +import { anoncreds2023DataIntegrityServiceSymbol } from '../../../credentials' import { DifPresentationExchangeService, DifPresentationExchangeSubmissionLocation, @@ -220,6 +221,24 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic return { attachment, format } } + private verifyUsingAnoncreds2023( + presentation: W3cVerifiablePresentation, + presentationSubmission: PresentationSubmission + ) { + if (presentation.claimFormat !== ClaimFormat.LdpVp) return false + + const descriptorMap = presentationSubmission.descriptor_map + + const verifyUsingDataIntegrity = descriptorMap.every((descriptor) => descriptor.format === ClaimFormat.DiVp) + if (!verifyUsingDataIntegrity) return false + + if (Array.isArray(presentation.proof)) { + return presentation.proof.some((proof) => proof.cryptosuite?.includes('anoncreds-2023')) + } else { + return presentation.proof.cryptosuite?.includes('anoncreds-2023') + } + } + public async processPresentation( agentContext: AgentContext, { requestAttachment, attachment }: ProofFormatProcessPresentationOptions @@ -268,36 +287,20 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic let verificationResult: W3cVerifyPresentationResult - const descriptorMap = jsonPresentation.presentation_submission.descriptor_map - const uniqueFormats = Array.from(new Set(descriptorMap.map((descriptor) => descriptor.format))) - - if (uniqueFormats.length == 0) { - agentContext.config.logger.error('Received an invalid presentation submission with no specified format.') - return false - } - - if (uniqueFormats.length > 1) { - agentContext.config.logger.error( - 'Received presentation in PEX proof format with multiple formats. This is not supported.' - ) - return false - } - - const format = uniqueFormats[0] - - if (format === ClaimFormat.DiVp && parsedPresentation.claimFormat === ClaimFormat.LdpVp) { - if (Array.isArray(parsedPresentation.proof)) - throw new CredoError('Cannot process presentations with multiple proofs') - const cryptosuite = parsedPresentation.proof.cryptosuite - if (!cryptosuite) { - throw new CredoError('Cannot process data integrity presentations without cryptosuites') - } - - if (cryptosuite === 'anoncreds-2023') { - const dataIntegrityService = agentContext.dependencyManager.resolve( - anonCredsVcDataIntegrityServiceSymbol + // FIXME: for some reason it won't accept the input if it doesn't know + // whether it's a JWT or JSON-LD VP even though the input is the same. + // Not sure how to fix + if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } else if (parsedPresentation.claimFormat === ClaimFormat.LdpVp) { + if (this.verifyUsingAnoncreds2023(parsedPresentation, jsonPresentation.presentation_submission)) { + const dataIntegrityService = agentContext.dependencyManager.resolve( + anoncreds2023DataIntegrityServiceSymbol ) - const proofVerificationResult = await dataIntegrityService.verifyPresentation(agentContext, { presentation: parsedPresentation as W3cJsonLdVerifiablePresentation, presentationDefinition: request.presentation_definition, @@ -313,25 +316,12 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic }, } } else { - agentContext.config.logger.error(`Unsupported cryptosuite '${cryptosuite}'.`) - return false + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) } - } - // FIXME: for some reason it won't accept the input if it doesn't know - // whether it's a JWT or JSON-LD VP even though the input is the same. - // Not sure how to fix - else if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { - verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { - presentation: parsedPresentation, - challenge: request.options.challenge, - domain: request.options.domain, - }) - } else if (parsedPresentation.claimFormat === ClaimFormat.LdpVp) { - verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { - presentation: parsedPresentation, - challenge: request.options.challenge, - domain: request.options.domain, - }) } else { agentContext.config.logger.error(`Received presentation in PEX proof format with unsupported format ${format}.`) return false diff --git a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts index c8fd56137c..4d18e66b05 100644 --- a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts +++ b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts @@ -7,11 +7,11 @@ import { IsUri } from '../../../../utils' export interface LinkedDataProofOptions { type: string - // FIXME: cryptosuite is not optional when migrating to the new data integrity specification + // FIXME: cryptosuite is not optional in Verifiable Credential Data Integrity 1.0 cryptosuite?: string proofPurpose: string verificationMethod: string - // FIXME: created is optional when migrating to the new data integrity specification + // FIXME: created is optional in Verifiable Credential Data Integrity 1.0 created: string domain?: string challenge?: string diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts index 722c1a3412..6281f0cec5 100644 --- a/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts @@ -54,11 +54,10 @@ export function W3cCredentialSubjectTransformer() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const vToJson = (v: any) => { if (isInstance(v, W3cCredentialSubject)) return v.id ? { ...v.claims, id: v.id } : { ...v.claims } - if (v.claims) throw new Error('Credential subject claims in plain json') return v } - return Array.isArray(value) ? value.every(vToJson) : vToJson(value) + return Array.isArray(value) ? value.map(vToJson) : vToJson(value) } // PLAIN_TO_PLAIN return value diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts b/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts index d53976e458..aec0b776f7 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts @@ -25,6 +25,7 @@ export class W3cCredentialRepository extends Repository { return this.findSingleByQuery(agentContext, { credentialDefinitionId }) } + // FIXME: maybe we should rename this, as it only with anoncreds public async getByCredentialId(agentContext: AgentContext, credentialId: string) { return this.getSingleByQuery(agentContext, { credentialId }) } diff --git a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts index 8e0e62bb95..73838c8735 100644 --- a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts +++ b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts @@ -85,6 +85,11 @@ describe('W3cCredentialRecord', () => { 'attr::degree::value': 'Bachelor of Science and Arts', revocationRegistryId: 'revocationRegistryId', credentialRevocationId: 'credentialRevocationId', + unqualifiedCredentialDefinitionId: undefined, + unqualifiedIssuerId: undefined, + unqualifiedRevocationRegistryId: undefined, + unqualifiedSchemaId: undefined, + unqualifiedSchemaIssuerId: undefined, } const anonCredsTags = w3cCredentialRecord.getAnonCredsTags() @@ -100,6 +105,7 @@ describe('W3cCredentialRecord', () => { contexts: credential.contexts, proofTypes: credential.proofTypes, givenId: credential.id, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], cryptosuites: [], expandedTypes: ['https://expanded.tag#1'], ...anoncredsCredentialTags, diff --git a/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts b/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts index bb2c738994..e8071f3f82 100644 --- a/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts +++ b/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts @@ -10,6 +10,9 @@ export interface AnonCredsCredentialValue { encoded: string // Raw value as number in string } +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) => { @@ -32,22 +35,34 @@ const isInt32 = (number: number) => { * @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 const encodeCredentialValue = (data: unknown): string => { - if (typeof data === 'boolean') return data ? '1' : '0' +export function encodeCredentialValue(value: unknown) { + const isEmpty = (value: unknown) => isString(value) && value === '' - // Keep any 32-bit integer as is - if (typeof data === 'number' && isInt32(data)) { - return String(data) + // If bool return bool as number string + if (isBoolean(value)) { + return Number(value).toString() } - // Convert any string integer (e.g. "1234") to be a 32-bit integer (e.g. 1234) - if (typeof data === 'string' && data !== '' && !isNaN(Number(data)) && isNumeric(data) && isInt32(Number(data))) { - return Number(data).toString() + // If value is int32 return as number string + if (isNumber(value) && isInt32(value)) { + return value.toString() } - data = data === undefined || data === null ? 'None' : data + // 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' + } - const buffer = TypedArrayEncoder.fromString(String(data)) + const buffer = TypedArrayEncoder.fromString(String(value)) const hash = Hasher.hash(buffer, 'sha-256') const hex = Buffer.from(hash).toString('hex') From cde35f850bbcbc69757ba95c11d22c7224630fb0 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 14:29:12 +0100 Subject: [PATCH 15/38] fix: todos --- .../DataIntegrityCredentialFormatService.ts | 17 +++++++++++++++-- ...DifPresentationExchangeProofFormatService.ts | 4 +++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index daeccc0091..a23ab4216e 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -185,8 +185,21 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { credential, data_model_versions_supported, binding_method, binding_required } = attachment.getDataAsJson() - // TODO: validate the credential - JsonTransformer.fromJSON(credential, W3cCredential) + const context = credential['@context'] + if (!context || !Array.isArray(context)) throw new CredoError('Invalid @context in credential offer') + + const isV1Credential = context.find((c) => c === 'https://www.w3.org/2018/credentials/v1') + const isV2Credential = context.find((c) => c === 'https://www.w3.org/ns/credentials/v2') + + if (!isV1Credential && !isV2Credential) throw new CredoError('Missing @context in credential offer') + + const credentialToBeValidated = { + ...credential, + issuer: credential.issuer ?? 'https://example.com', + ...(isV1Credential && { validFrom: credential.issuanceDate ?? new Date().toISOString() }), + } + + JsonTransformer.fromJSON(credentialToBeValidated, W3cCredential) const missingBindingMethod = binding_required && !binding_method?.anoncreds_link_secret && !binding_method?.didcomm_signed_attachment diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index ef29d413af..b40f5fb5c7 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -323,7 +323,9 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic }) } } else { - agentContext.config.logger.error(`Received presentation in PEX proof format with unsupported format ${format}.`) + agentContext.config.logger.error( + `Received presentation in PEX proof format with unsupported format ${parsedPresentation['claimFormat']}.` + ) return false } From 06ee6be73522859f621f989f4264eef8d9fcbfb5 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 14:35:19 +0100 Subject: [PATCH 16/38] fix: stuff --- demo/package.json | 4 +-- demo/src/Alice.ts | 2 +- .../AnonCredsRsHolderService.test.ts | 9 +++++ yarn.lock | 35 ++++++++++++------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/demo/package.json b/demo/package.json index 0d17235cd7..886d1402ff 100644 --- a/demo/package.json +++ b/demo/package.json @@ -9,8 +9,8 @@ }, "license": "Apache-2.0", "scripts": { - "alice": "ts-node src/AliceInquirer.ts", - "faber": "ts-node src/FaberInquirer.ts", + "alice": "ts-node -T src/AliceInquirer.ts", + "faber": "ts-node -T src/FaberInquirer.ts", "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts index 788b660570..3b64d404c2 100644 --- a/demo/src/Alice.ts +++ b/demo/src/Alice.ts @@ -83,7 +83,7 @@ export class Alice extends BaseAgent { credentialRecordId: credentialRecord.id, credentialFormats: { dataIntegrity: { - anonCredsLinkSecretCredentialRequestOptions: { + anonCredsLinkSecretAcceptOfferOptions: { linkSecretId: 'linkSecretId', }, }, diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts index b56a57026d..169161fa80 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts @@ -17,6 +17,7 @@ import { InjectionSymbols, KeyType, SignatureSuiteToken, + SigningProviderRegistry, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialRecord, @@ -32,6 +33,7 @@ import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageServ import { AnonCredsCredentialDefinitionRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsLinkSecretRepository } from '../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository' import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService' @@ -63,6 +65,12 @@ const w3cCredentialRepositoryMock = new W3cCredentialRepositoryMock() const inMemoryStorageService = new InMemoryStorageService() const logger = new ConsoleLogger() +const wallet = new RegisteredAskarTestWallet( + agentConfig.logger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([]) +) + const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.AgentDependencies, agentDependencies], @@ -96,6 +104,7 @@ const agentContext = getAgentContext({ ], ], agentConfig, + wallet, }) describe('AnonCredsRsHolderService', () => { diff --git a/yarn.lock b/yarn.lock index d2ebb263f2..5e2d6c415c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2618,6 +2618,26 @@ resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.5.tgz#ba4474a3783081392b72403c4c8ee6da3d2e5585" integrity sha512-7THexvdYUK/Dh8olBB46ErT9q/RnecnMdb5r2iwZ6be0Dt4vQLAUN7QU80H0HZBok4jRTb8ydt12x0raBSTHOg== +"@sphereon/pex-models@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.2.0.tgz#32013fff43d4f47df03e213792a9bcc6866a1f06" + integrity sha512-dGDRdoxJj+P0TRqu0R8R0/IdIzrCya1MsnxIFbcmSW3rjPsbwXbV0EojEfxXGD5LhqsUJiuAffMtyE2dtVI/XQ== + +"@sphereon/pex@../PEX/": + version "3.1.1-unstable.0" + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sd-jwt/decode" "^0.2.0" + "@sd-jwt/present" "^0.2.0" + "@sd-jwt/utils" "^0.2.0" + "@sphereon/pex-models" "^2.2.0" + "@sphereon/ssi-types" "0.18.1" + ajv "^8.12.0" + ajv-formats "^2.1.1" + jwt-decode "^3.1.2" + nanoid "^3.3.7" + string.prototype.matchall "^4.0.10" + "@sphereon/pex@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.0.1.tgz#e7d9d36c7c921ab97190a735c67e0a2632432e3b" @@ -3142,7 +3162,7 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== -"@types/validator@^13.11.8", "@types/validator@^13.7.10": +"@types/validator@^13.11.8": version "13.11.8" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.11.8.tgz#bb1162ec0fe6f87c95ca812f15b996fcc5e1e2dc" integrity sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ== @@ -4283,15 +4303,6 @@ class-transformer@0.5.1, class-transformer@^0.5.1: resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== -class-validator@0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.0.tgz#40ed0ecf3c83b2a8a6a320f4edb607be0f0df159" - integrity sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A== - dependencies: - "@types/validator" "^13.7.10" - libphonenumber-js "^1.10.14" - validator "^13.7.0" - class-validator@0.14.1: version "0.14.1" resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.1.tgz#ff2411ed8134e9d76acfeb14872884448be98110" @@ -7969,7 +7980,7 @@ libnpmpublish@7.1.4: sigstore "^1.4.0" ssri "^10.0.1" -libphonenumber-js@^1.10.14, libphonenumber-js@^1.10.53: +libphonenumber-js@^1.10.53: version "1.10.54" resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.54.tgz#8dfba112f49d1b9c2a160e55f9697f22e50f0841" integrity sha512-P+38dUgJsmh0gzoRDoM4F5jLbyfztkU6PY6eSK6S5HwTi/LPvnwXqVCQZlAy1FxZ5c48q25QhxGQ0pq+WQcSlQ== @@ -11813,7 +11824,7 @@ validate-npm-package-name@^5.0.0: dependencies: builtins "^5.0.0" -validator@^13.7.0, validator@^13.9.0: +validator@^13.9.0: version "13.11.0" resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== From 9856e2f525125020475fd4a098d9f7d890a1b221 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 14:35:27 +0100 Subject: [PATCH 17/38] feat: also query legacy --- .../anoncreds-rs/AnonCredsRsHolderService.ts | 234 ++++++++++++++++-- 1 file changed, 213 insertions(+), 21 deletions(-) diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts index 4a7a77d192..9ec99b59a2 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts @@ -11,7 +11,7 @@ import type { GetCredentialsOptions, StoreCredentialOptions, } from '../services' -import type { AgentContext, AnonCredsClaimRecord, Query, SimpleQuery, W3cCredentialRecord } from '@credo-ts/core' +import type { AgentContext, AnonCredsClaimRecord, Query, SimpleQuery } from '@credo-ts/core' import type { CredentialEntry, CredentialProve, @@ -22,6 +22,7 @@ import type { import { CredoError, JsonTransformer, + W3cCredentialRecord, TypedArrayEncoder, W3cCredentialRepository, W3cCredentialService, @@ -57,7 +58,7 @@ import { type AnonCredsProofRequestRestriction, AnonCredsRestrictionWrapper, } from '../models' -import { AnonCredsLinkSecretRepository } from '../repository' +import { AnonCredsCredentialRecord, AnonCredsCredentialRepository, AnonCredsLinkSecretRepository } from '../repository' import { AnonCredsRegistryService } from '../services' import { fetchCredentialDefinition, @@ -101,28 +102,53 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const legacyCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) // Cache retrieved credentials in order to minimize storage calls - const retrievedCredentials = new Map() + const retrievedCredentials = new Map() const credentialEntryFromAttribute = async ( attribute: AnonCredsRequestedAttributeMatch | AnonCredsRequestedPredicateMatch ): Promise<{ linkSecretId: string; credentialEntry: CredentialEntry }> => { let credentialRecord = retrievedCredentials.get(attribute.credentialId) if (!credentialRecord) { - credentialRecord = await credentialRepository.getByCredentialId(agentContext, attribute.credentialId) - retrievedCredentials.set(attribute.credentialId, credentialRecord) - } + try { + credentialRecord = await credentialRepository.getByCredentialId(agentContext, attribute.credentialId) + retrievedCredentials.set(attribute.credentialId, credentialRecord) + } catch { + // do nothing + } - if (!credentialRecord.anonCredsCredentialMetadata) { - throw new CredoError('AnonCreds metadata not found on credential record.') + if (!credentialRecord) { + credentialRecord = await legacyCredentialRepository.getByCredentialId(agentContext, attribute.credentialId) + } } - if (credentialRecord.credential instanceof W3cJsonLdVerifiableCredential === false) { - throw new CredoError('Credential must be a W3cJsonLdVerifiableCredential.') - } + let revocationRegistryId: string | null + let credentialRevocationId: string | null + let linkSecretId: string + + if (credentialRecord instanceof W3cCredentialRecord) { + if (!credentialRecord.anonCredsCredentialMetadata) { + throw new CredoError('AnonCreds metadata not found on credential record.') + } + + if (credentialRecord.credential instanceof W3cJsonLdVerifiableCredential === false) { + throw new CredoError('Credential must be a W3cJsonLdVerifiableCredential.') + } - const { revocationRegistryId, credentialRevocationId } = this.anoncredsMetadataFromRecord(credentialRecord) + linkSecretId = credentialRecord.anonCredsCredentialMetadata.linkSecretId + const metadata = this.anoncredsMetadataFromRecord(credentialRecord) + revocationRegistryId = metadata.revocationRegistryId + credentialRevocationId = metadata.credentialRevocationId + } else if (credentialRecord instanceof AnonCredsCredentialRecord) { + const metadata = this.anoncredsMetadataFromLegacyRecord(credentialRecord) + linkSecretId = credentialRecord.linkSecretId + revocationRegistryId = metadata.revocationRegistryId + credentialRevocationId = metadata.credentialRevocationId + } else { + throw new CredoError('Credential record must be either a W3cCredentialRecord or AnonCredsCredentialRecord.') + } // TODO: Check if credential has a revocation registry id (check response from anoncreds-rs API, as it is // sending back a mandatory string in Credential.revocationRegistryId) @@ -156,12 +182,15 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { }) } + const credential = + credentialRecord instanceof W3cCredentialRecord + ? w3cToLegacyCredential(credentialRecord.credential as W3cJsonLdVerifiableCredential) + : (credentialRecord.credential as AnonCredsCredential) + return { - linkSecretId: credentialRecord.anonCredsCredentialMetadata.linkSecretId, + linkSecretId, credentialEntry: { - credential: w3cToLegacyCredential( - credentialRecord.credential as W3cJsonLdVerifiableCredential - ) as unknown as JsonObject, + credential: credential as unknown as JsonObject, revocationState: revocationState?.toJson(), timestamp, }, @@ -425,9 +454,39 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { options: GetCredentialOptions ): Promise { const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const credentialRecord = await credentialRepository.getByCredentialId(agentContext, options.credentialId) + try { + const credentialRecord = await credentialRepository.getByCredentialId(agentContext, options.credentialId) + if (credentialRecord) return this.anoncredsMetadataFromRecord(credentialRecord) + } catch { + // do nothing + } + + const anonCredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + const anonCredsCredentialRecord = await anonCredsCredentialRepository.getByCredentialId( + agentContext, + options.credentialId + ) + + return this.anoncredsMetadataFromLegacyRecord(anonCredsCredentialRecord) + } + + private anoncredsMetadataFromLegacyRecord( + anonCredsCredentialRecord: AnonCredsCredentialRecord + ): AnonCredsCredentialInfo { + const attributes: { [key: string]: string } = {} + for (const attribute in anonCredsCredentialRecord.credential) { + attributes[attribute] = anonCredsCredentialRecord.credential.values[attribute].raw + } - return this.anoncredsMetadataFromRecord(credentialRecord) + return { + attributes, + credentialDefinitionId: anonCredsCredentialRecord.credential.cred_def_id, + credentialId: anonCredsCredentialRecord.credentialId, + schemaId: anonCredsCredentialRecord.credential.schema_id, + credentialRevocationId: anonCredsCredentialRecord.credentialRevocationId ?? null, + revocationRegistryId: anonCredsCredentialRecord.credential.rev_reg_id ?? null, + methodName: anonCredsCredentialRecord.methodName, + } } private anoncredsMetadataFromRecord(w3cCredentialRecord: W3cCredentialRecord): AnonCredsCredentialInfo { @@ -452,6 +511,25 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } + public async getLegacyCredentials( + agentContext: AgentContext, + options: GetCredentialsOptions + ): Promise { + const credentialRecords = await agentContext.dependencyManager + .resolve(AnonCredsCredentialRepository) + .findByQuery(agentContext, { + credentialDefinitionId: options.credentialDefinitionId, + schemaId: options.schemaId, + issuerId: options.issuerId, + schemaName: options.schemaName, + schemaVersion: options.schemaVersion, + schemaIssuerId: options.schemaIssuerId, + methodName: options.methodName, + }) + + return credentialRecords.map((credentialRecord) => this.anoncredsMetadataFromLegacyRecord(credentialRecord)) + } + public async getCredentials( agentContext: AgentContext, options: GetCredentialsOptions @@ -480,14 +558,76 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { unqualifiedCredentialDefinitionId: getIfUnqualifiedId(options.credentialDefinitionId), }) - return credentialRecords.map((credentialRecord) => this.anoncredsMetadataFromRecord(credentialRecord)) + const credentials = credentialRecords.map((credentialRecord) => this.anoncredsMetadataFromRecord(credentialRecord)) + const legacyCredentials = await this.getLegacyCredentials(agentContext, options) + + return [...legacyCredentials, ...credentials] } public async deleteCredential(agentContext: AgentContext, credentialId: string): Promise { - const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + try { + const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const credentialRecord = await credentialRepository.getByCredentialId(agentContext, credentialId) + await credentialRepository.delete(agentContext, credentialRecord) + return + } catch { + // do nothing + } + + const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) const credentialRecord = await credentialRepository.getByCredentialId(agentContext, credentialId) await credentialRepository.delete(agentContext, credentialRecord) } + public async getLegacyCredentialsForProofRequest( + agentContext: AgentContext, + options: GetCredentialsForProofRequestOptions + ): Promise { + const proofRequest = options.proofRequest + const referent = options.attributeReferent + + const requestedAttribute = + proofRequest.requested_attributes[referent] ?? proofRequest.requested_predicates[referent] + + if (!requestedAttribute) { + throw new AnonCredsRsError(`Referent not found in proof request`) + } + + const $and = [] + + // Make sure the attribute(s) that are requested are present using the marker tag + const attributes = requestedAttribute.names ?? [requestedAttribute.name] + const attributeQuery: SimpleQuery = {} + for (const attribute of attributes) { + attributeQuery[`attr::${attribute}::marker`] = true + } + $and.push(attributeQuery) + + // Add query for proof request restrictions + if (requestedAttribute.restrictions) { + const restrictionQuery = this.queryLegacyFromRestrictions(requestedAttribute.restrictions) + $and.push(restrictionQuery) + } + + // Add extra query + // TODO: we're not really typing the extraQuery, and it will work differently based on the anoncreds implmentation + // We should make the allowed properties more strict + if (options.extraQuery) { + $and.push(options.extraQuery) + } + + const credentials = await agentContext.dependencyManager + .resolve(AnonCredsCredentialRepository) + .findByQuery(agentContext, { + $and, + }) + + return credentials.map((credentialRecord) => { + return { + credentialInfo: this.anoncredsMetadataFromLegacyRecord(credentialRecord), + interval: proofRequest.non_revoked, + } + }) + } public async getCredentialsForProofRequest( agentContext: AgentContext, @@ -532,12 +672,16 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { $and, }) - return credentials.map((credentialRecord) => { + const legacyCredentialWithMetadata = await this.getLegacyCredentialsForProofRequest(agentContext, options) + + const credentialWithMetadata = credentials.map((credentialRecord) => { return { credentialInfo: this.anoncredsMetadataFromRecord(credentialRecord), interval: proofRequest.non_revoked, } }) + + return [...credentialWithMetadata, ...legacyCredentialWithMetadata] } private queryFromRestrictions(restrictions: AnonCredsProofRequestRestriction[]) { @@ -605,4 +749,52 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { return query.length === 1 ? query[0] : { $or: query } } + + private queryLegacyFromRestrictions(restrictions: AnonCredsProofRequestRestriction[]) { + const query: Query[] = [] + + const { restrictions: parsedRestrictions } = JsonTransformer.fromJSON({ restrictions }, AnonCredsRestrictionWrapper) + + for (const restriction of parsedRestrictions) { + const queryElements: SimpleQuery = {} + + if (restriction.credentialDefinitionId) { + queryElements.credentialDefinitionId = restriction.credentialDefinitionId + } + + if (restriction.issuerId || restriction.issuerDid) { + queryElements.issuerId = restriction.issuerId ?? restriction.issuerDid + } + + if (restriction.schemaId) { + queryElements.schemaId = restriction.schemaId + } + + if (restriction.schemaIssuerId || restriction.schemaIssuerDid) { + queryElements.schemaIssuerId = restriction.schemaIssuerId ?? restriction.issuerDid + } + + if (restriction.schemaName) { + queryElements.schemaName = restriction.schemaName + } + + if (restriction.schemaVersion) { + queryElements.schemaVersion = restriction.schemaVersion + } + + for (const [attributeName, attributeValue] of Object.entries(restriction.attributeValues)) { + queryElements[`attr::${attributeName}::value`] = attributeValue + } + + for (const [attributeName, isAvailable] of Object.entries(restriction.attributeMarkers)) { + if (isAvailable) { + queryElements[`attr::${attributeName}::marker`] = isAvailable + } + } + + query.push(queryElements) + } + + return query.length === 1 ? query[0] : { $or: query } + } } From 378e4aa6de2bb6adb4fb31ba16821dc0dce5c1a1 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 14:35:36 +0100 Subject: [PATCH 18/38] fix: packed pex --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 545abc4713..df6c854304 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -29,7 +29,7 @@ "@multiformats/base-x": "^4.0.1", "@sd-jwt/core": "^0.2.0", "@sd-jwt/decode": "^0.2.0", - "@sphereon/pex": "^3.0.1", + "@sphereon/pex": "../PEX/", "@sphereon/pex-models": "^2.2.0", "@sphereon/ssi-types": "^0.18.1", "@stablelib/ed25519": "^1.0.2", From 1584dd827ce4406f56cdbde13c6f4e33c9e22098 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 14:41:30 +0100 Subject: [PATCH 19/38] update readme --- demo/README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/demo/README.md b/demo/README.md index 1c65ce8988..b4fc1d19ac 100644 --- a/demo/README.md +++ b/demo/README.md @@ -21,14 +21,26 @@ In order to use Credo some platform specific dependencies and setup is required. ### Run the demo +#### PEX Setup + +This demo won't work out of the box until https://github.com/Sphereon-Opensource/PEX/pull/141 +is merged. For now you need to clone this repo https://github.com/2mau/PEX/tree/data-integrity, build it manually, +and add PEX as dependency in `packages/core/package.json`. + +There will be some type errors you can ignore them for now. + +#### Setup Demo + These are the steps for running the Credo demo: Clone the Credo git repository: + ```sh git clone https://github.com/openwallet-foundation/credo-ts.git ``` + Open two different terminals next to each other and in both, go to the demo folder: ```sh @@ -74,12 +86,6 @@ To request a proof: - Faber will create a new proof attribute and will then send a proof request to Alice! - Go to Alice to accept the incoming proof request -To send a basic message: - -- Select 'send message' in either one of the Agents -- Type your message and press enter -- Message sent! - Exit: - Select 'exit' to shutdown the agent. From 9acc6fa7c7978c83be391a43c53f7900fe720010 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 14:44:50 +0100 Subject: [PATCH 20/38] feat: roll back demo --- demo/README.md | 18 ++--- demo/package.json | 4 +- demo/src/Alice.ts | 62 +--------------- demo/src/BaseAgent.ts | 53 +++++++++----- demo/src/Faber.ts | 166 +++++++++++------------------------------- 5 files changed, 87 insertions(+), 216 deletions(-) diff --git a/demo/README.md b/demo/README.md index b4fc1d19ac..1c65ce8988 100644 --- a/demo/README.md +++ b/demo/README.md @@ -21,26 +21,14 @@ In order to use Credo some platform specific dependencies and setup is required. ### Run the demo -#### PEX Setup - -This demo won't work out of the box until https://github.com/Sphereon-Opensource/PEX/pull/141 -is merged. For now you need to clone this repo https://github.com/2mau/PEX/tree/data-integrity, build it manually, -and add PEX as dependency in `packages/core/package.json`. - -There will be some type errors you can ignore them for now. - -#### Setup Demo - These are the steps for running the Credo demo: Clone the Credo git repository: - ```sh git clone https://github.com/openwallet-foundation/credo-ts.git ``` - Open two different terminals next to each other and in both, go to the demo folder: ```sh @@ -86,6 +74,12 @@ To request a proof: - Faber will create a new proof attribute and will then send a proof request to Alice! - Go to Alice to accept the incoming proof request +To send a basic message: + +- Select 'send message' in either one of the Agents +- Type your message and press enter +- Message sent! + Exit: - Select 'exit' to shutdown the agent. diff --git a/demo/package.json b/demo/package.json index 886d1402ff..0d17235cd7 100644 --- a/demo/package.json +++ b/demo/package.json @@ -9,8 +9,8 @@ }, "license": "Apache-2.0", "scripts": { - "alice": "ts-node -T src/AliceInquirer.ts", - "faber": "ts-node -T src/FaberInquirer.ts", + "alice": "ts-node src/AliceInquirer.ts", + "faber": "ts-node src/FaberInquirer.ts", "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts index 3b64d404c2..ca50a0f50a 100644 --- a/demo/src/Alice.ts +++ b/demo/src/Alice.ts @@ -1,52 +1,20 @@ -import type { - KeyDidCreateOptions, - CredentialStateChangedEvent, - ConnectionRecord, - CredentialExchangeRecord, - ProofExchangeRecord, -} from '@credo-ts/core' -import type BottomBar from 'inquirer/lib/ui/bottom-bar' - -import { - AutoAcceptCredential, - CredentialEventTypes, - CredentialState, - DidsApi, - KeyType, - TypedArrayEncoder, -} from '@credo-ts/core' -import { randomInt } from 'crypto' -import { ui } from 'inquirer' +import type { ConnectionRecord, CredentialExchangeRecord, ProofExchangeRecord } from '@credo-ts/core' import { BaseAgent } from './BaseAgent' -import { Color, greenText, Output, redText } from './OutputClass' +import { greenText, Output, redText } from './OutputClass' export class Alice extends BaseAgent { public connected: boolean public connectionRecordFaberId?: string - public did?: string - public ui: BottomBar public constructor(port: number, name: string) { super({ port, name }) this.connected = false - this.ui = new ui.BottomBar() } public static async build(): Promise { - const alice = new Alice(9000, 'alice' + randomInt(100000)) + const alice = new Alice(9000, 'alice') await alice.initializeAgent() - - await alice.agent.modules.anoncreds.createLinkSecret({ linkSecretId: 'linkSecretId' }) - - const dids = alice.agent.context.dependencyManager.resolve(DidsApi) - const didCreateResult = await dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, - }) - - if (!didCreateResult.didState.did) throw new Error('failed to created did') return alice } @@ -79,26 +47,7 @@ export class Alice extends BaseAgent { public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { await this.agent.credentials.acceptOffer({ - autoAcceptCredential: AutoAcceptCredential.Never, credentialRecordId: credentialRecord.id, - credentialFormats: { - dataIntegrity: { - anonCredsLinkSecretAcceptOfferOptions: { - linkSecretId: 'linkSecretId', - }, - }, - }, - }) - - this.agent.events.on(CredentialEventTypes.CredentialStateChanged, async (afjEvent) => { - const credentialRecord = afjEvent.payload.credentialRecord - - if (afjEvent.payload.credentialRecord.state !== CredentialState.CredentialReceived) return - - console.log(`\nReceived Credential. Processing and storing it!\n\n${Color.Reset}`) - await this.agent.credentials.acceptCredential({ - credentialRecordId: credentialRecord.id, - }) }) } @@ -107,11 +56,6 @@ export class Alice extends BaseAgent { proofRecordId: proofRecord.id, }) - const selectedCredentials = requestedCredentials.proofFormats.presentationExchange?.credentials - if (!selectedCredentials) { - throw new Error('No credentials found for presentation exchange') - } - await this.agent.proofs.acceptRequest({ proofRecordId: proofRecord.id, proofFormats: requestedCredentials.proofFormats, diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts index 60e975857b..c0acab7e88 100644 --- a/demo/src/BaseAgent.ts +++ b/demo/src/BaseAgent.ts @@ -1,8 +1,15 @@ import type { InitConfig } from '@credo-ts/core' import type { IndyVdrPoolConfig } from '@credo-ts/indy-vdr' -import { AnonCredsModule, DataIntegrityCredentialFormatService } from '@credo-ts/anoncreds' - +import { + AnonCredsCredentialFormatService, + AnonCredsModule, + AnonCredsProofFormatService, + LegacyIndyCredentialFormatService, + LegacyIndyProofFormatService, + V1CredentialProtocol, + V1ProofProtocol, +} from '@credo-ts/anoncreds' import { AskarModule } from '@credo-ts/askar' import { CheqdAnonCredsRegistry, @@ -12,22 +19,19 @@ import { CheqdModuleConfig, } from '@credo-ts/cheqd' import { - Agent, - AutoAcceptCredential, - AutoAcceptProof, ConnectionsModule, - CredentialsModule, DidsModule, - HttpOutboundTransport, - KeyDidRegistrar, - KeyDidResolver, - PresentationExchangeProofFormatService, - ProofsModule, - V2CredentialProtocol, V2ProofProtocol, + V2CredentialProtocol, + ProofsModule, + AutoAcceptProof, + AutoAcceptCredential, + CredentialsModule, + Agent, + HttpOutboundTransport, } from '@credo-ts/core' -import { IndyVdrAnonCredsRegistry, IndyVdrIndyDidResolver, IndyVdrModule } from '@credo-ts/indy-vdr' -import { HttpInboundTransport, agentDependencies } from '@credo-ts/node' +import { IndyVdrIndyDidResolver, IndyVdrAnonCredsRegistry, IndyVdrModule } from '@credo-ts/indy-vdr' +import { agentDependencies, HttpInboundTransport } from '@credo-ts/node' import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' @@ -86,23 +90,32 @@ export class BaseAgent { } function getAskarAnonCredsIndyModules() { + const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() + const legacyIndyProofFormatService = new LegacyIndyProofFormatService() + return { connections: new ConnectionsModule({ autoAcceptConnections: true, }), credentials: new CredentialsModule({ - autoAcceptCredentials: AutoAcceptCredential.Never, + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, credentialProtocols: [ + new V1CredentialProtocol({ + indyCredentialFormat: legacyIndyCredentialFormatService, + }), new V2CredentialProtocol({ - credentialFormats: [new DataIntegrityCredentialFormatService()], + credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()], }), ], }), proofs: new ProofsModule({ - autoAcceptProofs: AutoAcceptProof.Never, + autoAcceptProofs: AutoAcceptProof.ContentApproved, proofProtocols: [ + new V1ProofProtocol({ + indyProofFormat: legacyIndyProofFormatService, + }), new V2ProofProtocol({ - proofFormats: [new PresentationExchangeProofFormatService()], + proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService()], }), ], }), @@ -126,8 +139,8 @@ function getAskarAnonCredsIndyModules() { }) ), dids: new DidsModule({ - resolvers: [new IndyVdrIndyDidResolver(), new CheqdDidResolver(), new KeyDidResolver()], - registrars: [new CheqdDidRegistrar(), new KeyDidRegistrar()], + resolvers: [new IndyVdrIndyDidResolver(), new CheqdDidResolver()], + registrars: [new CheqdDidRegistrar()], }), askar: new AskarModule({ ariesAskar, diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index 8f0ac0327a..5f3542b803 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -1,26 +1,9 @@ import type { RegisterCredentialDefinitionReturnStateFinished } from '@credo-ts/anoncreds' -import type { - ConnectionRecord, - ConnectionStateChangedEvent, - CredentialStateChangedEvent, - ProofStateChangedEvent, -} from '@credo-ts/core' +import type { ConnectionRecord, ConnectionStateChangedEvent } from '@credo-ts/core' import type { IndyVdrRegisterSchemaOptions, IndyVdrRegisterCredentialDefinitionOptions } from '@credo-ts/indy-vdr' import type BottomBar from 'inquirer/lib/ui/bottom-bar' -import { - CredentialState, - CredentialEventTypes, - W3cCredential, - W3cCredentialSubject, - ProofEventTypes, - ProofState, - ConnectionEventTypes, - KeyType, - TypedArrayEncoder, - utils, -} from '@credo-ts/core' -import { randomInt } from 'crypto' +import { ConnectionEventTypes, KeyType, TypedArrayEncoder, utils } from '@credo-ts/core' import { ui } from 'inquirer' import { BaseAgent, indyNetworkConfig } from './BaseAgent' @@ -34,7 +17,7 @@ export enum RegistryOptions { export class Faber extends BaseAgent { public outOfBandId?: string public credentialDefinition?: RegisterCredentialDefinitionReturnStateFinished - public anonCredsIssuerId!: string + public anonCredsIssuerId?: string public ui: BottomBar public constructor(port: number, name: string) { @@ -43,9 +26,8 @@ export class Faber extends BaseAgent { } public static async build(): Promise { - const faber = new Faber(9001, 'faber' + randomInt(10000)) + const faber = new Faber(9001, 'faber') await faber.initializeAgent() - return faber } @@ -145,9 +127,7 @@ export class Faber extends BaseAgent { console.log(`\n\nThe credential definition will look like this:\n`) console.log(purpleText(`Name: ${Color.Reset}${name}`)) console.log(purpleText(`Version: ${Color.Reset}${version}`)) - console.log( - purpleText(`Attributes: ${Color.Reset}${attributes[0]}, ${attributes[1]}, ${attributes[2]}, ${attributes[3]}\n`) - ) + console.log(purpleText(`Attributes: ${Color.Reset}${attributes[0]}, ${attributes[1]}, ${attributes[2]}\n`)) } private async registerSchema() { @@ -157,7 +137,7 @@ export class Faber extends BaseAgent { const schemaTemplate = { name: 'Faber College' + utils.uuid(), version: '1.0.0', - attrNames: ['id', 'name', 'height', 'age'], + attrNames: ['name', 'degree', 'date'], issuerId: this.anonCredsIssuerId, } this.printSchema(schemaTemplate.name, schemaTemplate.version, schemaTemplate.attrNames) @@ -224,48 +204,28 @@ export class Faber extends BaseAgent { connectionId: connectionRecord.id, protocolVersion: 'v2', credentialFormats: { - dataIntegrity: { - bindingRequired: true, - anonCredsLinkSecretBindingMethodOptions: { - credentialDefinitionId: credentialDefinition.credentialDefinitionId, - }, - credential: new W3cCredential({ - type: ['VerifiableCredential'], - issuanceDate: new Date().toISOString(), - issuer: this.anonCredsIssuerId as string, - credentialSubject: new W3cCredentialSubject({ - claims: { - name: 'Alice Smith', - age: 28, - height: 173, - }, - }), - }), + anoncreds: { + attributes: [ + { + name: 'name', + value: 'Alice Smith', + }, + { + name: 'degree', + value: 'Computer Science', + }, + { + name: 'date', + value: '01/01/2022', + }, + ], + credentialDefinitionId: credentialDefinition.credentialDefinitionId, }, }, }) this.ui.updateBottomBar( `\nCredential offer sent!\n\nGo to the Alice agent to accept the credential offer\n\n${Color.Reset}` ) - - this.agent.events.on(CredentialEventTypes.CredentialStateChanged, async (afjEvent) => { - const credentialRecord = afjEvent.payload.credentialRecord - if (afjEvent.payload.credentialRecord.state !== CredentialState.RequestReceived) return - - console.log(`\nAccepting Credential Request. Sending Credential!\n\n`) - - await this.agent.credentials.acceptRequest({ - credentialRecordId: credentialRecord.id, - credentialFormats: { - dataIntegrity: { - credentialSubjectId: 'did:key:z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9', - didCommSignedAttachmentAcceptRequestOptions: { - kid: 'did:key:z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9#z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9', - }, - }, - }, - }) - }) } private async printProofFlow(print: string) { @@ -273,81 +233,41 @@ export class Faber extends BaseAgent { await new Promise((f) => setTimeout(f, 2000)) } + private async newProofAttribute() { + await this.printProofFlow(greenText(`Creating new proof attribute for 'name' ...\n`)) + const proofAttribute = { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: this.credentialDefinition?.credentialDefinitionId, + }, + ], + }, + } + + return proofAttribute + } + public async sendProofRequest() { const connectionRecord = await this.getConnectionRecord() + const proofAttribute = await this.newProofAttribute() await this.printProofFlow(greenText('\nRequesting proof...\n', false)) await this.agent.proofs.requestProof({ protocolVersion: 'v2', connectionId: connectionRecord.id, proofFormats: { - presentationExchange: { - presentationDefinition: { - id: '1234567', - name: 'Age Verification', - purpose: 'We need to verify your age before entering a bar', - input_descriptors: [ - { - id: 'age-verification', - name: 'A specific type of VC + Issuer', - purpose: 'We want a VC of this type generated by this issuer', - schema: [ - { - uri: 'https://www.w3.org/2018/credentials/v1', - }, - ], - constraints: { - limit_disclosure: 'required', - fields: [ - { - path: ['$.issuer'], - filter: { - type: 'string', - const: 'did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675', - }, - }, - { - path: ['$.credentialSubject.name'], - }, - { - path: ['$.credentialSubject.height'], - }, - { - path: ['$.credentialSubject.age'], - predicate: 'preferred', - filter: { - type: 'number', - minimum: 18, - }, - }, - ], - }, - }, - ], - format: { - di_vc: { - proof_type: ['DataIntegrityProof'], - cryptosuite: ['anoncreds-2023', 'eddsa-rdfc-2022'], - }, - }, - }, + anoncreds: { + name: 'proof-request', + version: '1.0', + requested_attributes: proofAttribute, }, }, }) this.ui.updateBottomBar( `\nProof request sent!\n\nGo to the Alice agent to accept the proof request\n\n${Color.Reset}` ) - - this.agent.events.on(ProofEventTypes.ProofStateChanged, async (afjEvent) => { - if (afjEvent.payload.proofRecord.state !== ProofState.PresentationReceived) return - - const proofRecord = afjEvent.payload.proofRecord - - console.log(`\nAccepting Presentation!\n\n${Color.Reset}`) - await this.agent.proofs.acceptPresentation({ - proofRecordId: proofRecord.id, - }) - }) } public async sendMessage(message: string) { From fb470e340b7d82c1933ec421ba9463b53ed745f1 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 18:08:48 +0100 Subject: [PATCH 21/38] fix: warn when using legacy --- .../anoncreds-rs/AnonCredsRsHolderService.ts | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts index 9ec99b59a2..c31e7b77bd 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts @@ -121,6 +121,13 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { if (!credentialRecord) { credentialRecord = await legacyCredentialRepository.getByCredentialId(agentContext, attribute.credentialId) + + agentContext.config.logger.warn( + [ + `Creating proof with legacy credential ${attribute.credentialId}.`, + `Please run the migration script to migrate credentials to the new w3c format.`, + ].join('\n') + ) } } @@ -453,8 +460,8 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { agentContext: AgentContext, options: GetCredentialOptions ): Promise { - const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) try { + const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) const credentialRecord = await credentialRepository.getByCredentialId(agentContext, options.credentialId) if (credentialRecord) return this.anoncredsMetadataFromRecord(credentialRecord) } catch { @@ -467,6 +474,13 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { options.credentialId ) + agentContext.config.logger.warn( + [ + `Querying legacy credential repository for credential with id ${options.credentialId}.`, + `Please run the migration script to migrate credentials to the new w3c format.`, + ].join('\n') + ) + return this.anoncredsMetadataFromLegacyRecord(anonCredsCredentialRecord) } @@ -511,7 +525,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } - public async getLegacyCredentials( + private async getLegacyCredentials( agentContext: AgentContext, options: GetCredentialsOptions ): Promise { @@ -561,6 +575,14 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const credentials = credentialRecords.map((credentialRecord) => this.anoncredsMetadataFromRecord(credentialRecord)) const legacyCredentials = await this.getLegacyCredentials(agentContext, options) + if (legacyCredentials.length > 0) { + agentContext.config.logger.warn( + [ + `Queried credentials include legacy credentials.`, + `Please run the migration script to migrate credentials to the new w3c format.`, + ].join('\n') + ) + } return [...legacyCredentials, ...credentials] } @@ -578,7 +600,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const credentialRecord = await credentialRepository.getByCredentialId(agentContext, credentialId) await credentialRepository.delete(agentContext, credentialRecord) } - public async getLegacyCredentialsForProofRequest( + private async getLegacyCredentialsForProofRequest( agentContext: AgentContext, options: GetCredentialsForProofRequestOptions ): Promise { @@ -674,6 +696,15 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const legacyCredentialWithMetadata = await this.getLegacyCredentialsForProofRequest(agentContext, options) + if (legacyCredentialWithMetadata.length > 0) { + agentContext.config.logger.warn( + [ + `Including legacy credentials in proof request.`, + `Please run the migration script to migrate credentials to the new w3c format.`, + ].join('\n') + ) + } + const credentialWithMetadata = credentials.map((credentialRecord) => { return { credentialInfo: this.anoncredsMetadataFromRecord(credentialRecord), From 29a1715233aaca0fd68e1946725636da3dcf2e75 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 5 Feb 2024 19:43:45 +0100 Subject: [PATCH 22/38] fix: add additional checks --- .../DataIntegrityCredentialFormatService.ts | 77 ++++++++++++++++--- .../anoncreds/tests/anoncreds-flow.test.ts | 1 + .../data-integrity-flow-anoncreds.test.ts | 1 + .../tests/data-integrity-flow-w3c.test.ts | 3 +- .../tests/data-integrity-flow.test.ts | 1 + .../formats/CredentialFormatServiceOptions.ts | 2 +- .../v2/CredentialFormatCoordinator.ts | 12 +++ 7 files changed, 84 insertions(+), 13 deletions(-) diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index a23ab4216e..1d6c106a14 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -66,6 +66,7 @@ import { SignatureSuiteRegistry, CredentialPreviewAttribute, CredoError, + deepEquality, } from '@credo-ts/core' import { W3cCredential as AW3cCredential } from '@hyperledger/anoncreds-shared' @@ -174,6 +175,18 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer return { format, attachment, previewAttributes } } + private getCredentialVersion(credentialJson: JsonObject): W3C_VC_DATA_MODEL_VERSION { + const context = credentialJson['@context'] + if (!context || !Array.isArray(context)) throw new CredoError('Invalid @context in credential offer') + + const isV1Credential = context.find((c) => c === 'https://www.w3.org/2018/credentials/v1') + const isV2Credential = context.find((c) => c === 'https://www.w3.org/ns/credentials/v2') + + if (isV1Credential) return '1.1' + else if (isV2Credential) return '2.0' + else throw new CredoError('Missing @context in credential offer') + } + public async processOffer( agentContext: AgentContext, { attachment, credentialRecord }: CredentialFormatProcessOptions @@ -185,18 +198,12 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { credential, data_model_versions_supported, binding_method, binding_required } = attachment.getDataAsJson() - const context = credential['@context'] - if (!context || !Array.isArray(context)) throw new CredoError('Invalid @context in credential offer') - - const isV1Credential = context.find((c) => c === 'https://www.w3.org/2018/credentials/v1') - const isV2Credential = context.find((c) => c === 'https://www.w3.org/ns/credentials/v2') - - if (!isV1Credential && !isV2Credential) throw new CredoError('Missing @context in credential offer') + const credentialVersion = this.getCredentialVersion(credential) const credentialToBeValidated = { ...credential, issuer: credential.issuer ?? 'https://example.com', - ...(isV1Credential && { validFrom: credential.issuanceDate ?? new Date().toISOString() }), + ...(credentialVersion === '1.1' && { validFrom: credential.issuanceDate ?? new Date().toISOString() }), } JsonTransformer.fromJSON(credentialToBeValidated, W3cCredential) @@ -684,7 +691,15 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer if (offeredCredential.type.length !== 1 || offeredCredential.type[0] !== 'VerifiableCredential') { throw new CredoError('Offered Invalid credential type') } - // TODO: check if any non integrity protected fields were on the offered credential. If so throw + + const integrityProtectedFields = ['@context', 'issuer', 'type', 'credentialSubject', 'validFrom', 'issuanceDate'] + if ( + Object.keys(credentialOffer.credential).some( + (key) => !integrityProtectedFields.includes(key) && key !== 'proof' + ) + ) { + throw new CredoError('Invalid credential subject id') + } } if (credentialRequest.binding_proof?.didcomm_signed_attachment) { @@ -792,11 +807,14 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer */ public async processCredential( agentContext: AgentContext, - { credentialRecord, attachment, requestAttachment }: CredentialFormatProcessCredentialOptions + { credentialRecord, attachment, requestAttachment, offerAttachment }: CredentialFormatProcessCredentialOptions ): Promise { + const credentialOffer = offerAttachment.getDataAsJson() + const credentialRequestMetadata = credentialRecord.metadata.get( DataIntegrityRequestMetadataKey ) + if (!credentialRequestMetadata) { throw new CredoError(`Missing request metadata for credential exchange with thread id ${credentialRecord.id}`) } @@ -810,6 +828,44 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { credential: credentialJson } = attachment.getDataAsJson() + const offeredCredentialJson = credentialOffer.credential + + if (!Array.isArray(offeredCredentialJson.credentialSubject)) { + const credentialSubjectMatches = Object.entries(offeredCredentialJson.credentialSubject as JsonObject).every( + ([key, offeredValue]) => { + const receivedValue = (credentialJson.credentialSubject as JsonObject)[key] + if (!offeredValue || !receivedValue) return false + + if (typeof offeredValue === 'number' || typeof receivedValue === 'number') { + return offeredValue.toString() === receivedValue.toString() + } + + return deepEquality(offeredValue, receivedValue) + } + ) + + if (!credentialSubjectMatches) { + throw new CredoError( + 'Received invalid credential. Received credential subject does not match the offered credential subject.' + ) + } + } + + const credentialVersion = this.getCredentialVersion(credentialJson) + const expectedReceivedCredential = { + ...offeredCredentialJson, + issuer: offeredCredentialJson.issuer ?? credentialJson.issuer, + credentialSubject: credentialJson.credentialSubject, + ...(credentialVersion === '1.1' && { issuanceDate: credentialJson.issuanceDate }), + ...(credentialVersion === '2.0' && { validFrom: credentialJson.validFrom }), + ...(offeredCredentialJson.credentialStatus === '2.0' && { credentialStatus: credentialJson.credentialStatus }), + proof: credentialJson.proof, + } + + if (!deepEquality(credentialJson, expectedReceivedCredential)) { + throw new CredoError('Received invalid credential. Received credential does not match the offered credential') + } + let anonCredsCredentialRecordOptions: AnonCredsCredentialRecordOptions | undefined let w3cJsonLdVerifiableCredential: W3cJsonLdVerifiableCredential if (credentialRequest.binding_proof?.anoncreds_link_secret) { @@ -833,7 +889,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer true ) } else { - // TODO: check the sturcture of the credential w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) } diff --git a/packages/anoncreds/tests/anoncreds-flow.test.ts b/packages/anoncreds/tests/anoncreds-flow.test.ts index de56e2b297..a3fa629850 100644 --- a/packages/anoncreds/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds/tests/anoncreds-flow.test.ts @@ -360,6 +360,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean // Holder processes and accepts credential await anoncredsCredentialFormatService.processCredential(agentContext, { + offerAttachment, credentialRecord: holderCredentialRecord, attachment: credentialAttachment, requestAttachment, diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts index c0b39307d1..23c5fc3b97 100644 --- a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts @@ -391,6 +391,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean // Holder processes and accepts credential await dataIntegrityCredentialFormatService.processCredential(agentContext, { + offerAttachment, credentialRecord: holderCredentialRecord, attachment: credentialAttachment, requestAttachment, diff --git a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts index 3c750161c8..24a6b022de 100644 --- a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts @@ -194,7 +194,7 @@ async function anonCredsFlowTest(options: { type: ['VerifiableCredential'], issuer: issuer.did, issuanceDate: new Date().toISOString(), - credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: '25' } }), + credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: 25 } }), }) const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { @@ -248,6 +248,7 @@ async function anonCredsFlowTest(options: { // Holder processes and accepts credential await dataIntegrityCredentialFormatService.processCredential(agentContext, { + offerAttachment, credentialRecord: holderCredentialRecord, attachment: credentialAttachment, requestAttachment, diff --git a/packages/anoncreds/tests/data-integrity-flow.test.ts b/packages/anoncreds/tests/data-integrity-flow.test.ts index 35c0c93839..419ed04126 100644 --- a/packages/anoncreds/tests/data-integrity-flow.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow.test.ts @@ -240,6 +240,7 @@ async function anonCredsFlowTest(options: { // Holder processes and accepts credential await dataIntegrityCredentialFormatService.processCredential(agentContext, { + offerAttachment, credentialRecord: holderCredentialRecord, attachment: credentialAttachment, requestAttachment, diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts index 20262645e8..1a70479450 100644 --- a/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts +++ b/packages/core/src/modules/credentials/formats/CredentialFormatServiceOptions.ts @@ -55,6 +55,7 @@ export interface CredentialFormatProcessOptions { } export interface CredentialFormatProcessCredentialOptions extends CredentialFormatProcessOptions { + offerAttachment: Attachment requestAttachment: Attachment requestAppendAttachments?: Attachment[] } @@ -87,7 +88,6 @@ export interface CredentialFormatAcceptOfferOptions credentialRecord: CredentialExchangeRecord credentialFormats?: CredentialFormatPayload<[CF], 'acceptOffer'> attachmentId?: string - offerAttachment: Attachment } diff --git a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts index 809555004d..782db490dd 100644 --- a/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts +++ b/packages/core/src/modules/credentials/protocol/v2/CredentialFormatCoordinator.ts @@ -530,7 +530,18 @@ export class CredentialFormatCoordinator ) { const didCommMessageRepository = agentContext.dependencyManager.resolve(DidCommMessageRepository) + const offerMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + for (const formatService of formatServices) { + const offerAttachment = this.getAttachmentForService( + formatService, + offerMessage.formats, + offerMessage.offerAttachments + ) + const attachment = this.getAttachmentForService(formatService, message.formats, message.credentialAttachments) const requestAttachment = this.getAttachmentForService( formatService, @@ -540,6 +551,7 @@ export class CredentialFormatCoordinator await formatService.processCredential(agentContext, { attachment, + offerAttachment, requestAttachment, credentialRecord, requestAppendAttachments: requestMessage.appendedAttachments, From 443fe6ae177086924e82629b82c1e985848dfddb Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 6 Feb 2024 09:11:44 +0100 Subject: [PATCH 23/38] feat: update anoncreds-rs to 0.2.0-dev.11 --- demo-openid/package.json | 2 +- demo/package.json | 2 +- packages/anoncreds/package.json | 6 ++-- .../anoncreds-rs/AnonCredsRsHolderService.ts | 3 +- .../DataIntegrityCredentialFormatService.ts | 2 +- .../0.4-0.5/anonCredsCredentialRecord.ts | 2 +- packages/anoncreds/src/utils/w3cUtils.ts | 14 +++----- yarn.lock | 35 +++++-------------- 8 files changed, 23 insertions(+), 43 deletions(-) diff --git a/demo-openid/package.json b/demo-openid/package.json index e6fa775559..1e33aff1a3 100644 --- a/demo-openid/package.json +++ b/demo-openid/package.json @@ -15,7 +15,7 @@ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.9", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.11", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.6", "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", "express": "^4.18.1", diff --git a/demo/package.json b/demo/package.json index 0d17235cd7..8d698595a1 100644 --- a/demo/package.json +++ b/demo/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.10", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.11", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.6", "inquirer": "^8.2.5" }, diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index ff4ee94c19..7d7cce94ae 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -33,13 +33,13 @@ }, "devDependencies": { "@credo-ts/node": "0.4.2", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.10", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.10", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.11", + "@hyperledger/anoncreds-shared": "^0.2.0-dev.11", "rimraf": "^4.4.0", "rxjs": "^7.8.0", "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.10" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.11" } } diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts index c31e7b77bd..a8f3128e11 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts @@ -333,10 +333,11 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { throw new AnonCredsRsError('Link Secret value not stored') } - const w3cJsonLdCredential = await legacyCredentialToW3cCredential(credential, credentialDefinition, { + const w3cJsonLdCredential = await legacyCredentialToW3cCredential(credential, credentialDefinition.issuerId, { credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject, linkSecret: linkSecretRecord.value, revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, + credentialDefinition: credentialDefinition as unknown as JsonObject, }) return w3cJsonLdCredential diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index 1d6c106a14..9f4778b681 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -544,7 +544,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credential.cred_def_id ) - return await legacyCredentialToW3cCredential(credential, anoncredsCredentialDefinition) + return await legacyCredentialToW3cCredential(credential, anoncredsCredentialDefinition.issuerId) } private async getSignatureMetadata(agentContext: AgentContext, offeredCredential: W3cCredential, issuerKid?: string) { diff --git a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts index c6b497fe2c..14a9868d30 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts @@ -17,7 +17,7 @@ async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRe const w3cJsonLdCredential = await legacyCredentialToW3cCredential( legacyCredential, - credentialDefinitionReturn.qualifiedCredentialDefinition + credentialDefinitionReturn.qualifiedCredentialDefinition.issuerId ) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) diff --git a/packages/anoncreds/src/utils/w3cUtils.ts b/packages/anoncreds/src/utils/w3cUtils.ts index f6f54c5bf3..689d0f849f 100644 --- a/packages/anoncreds/src/utils/w3cUtils.ts +++ b/packages/anoncreds/src/utils/w3cUtils.ts @@ -1,4 +1,4 @@ -import type { AnonCredsCredential, AnonCredsCredentialDefinition } from '../models' +import type { AnonCredsCredential } from '../models' import type { ProcessCredentialOptions } from '@hyperledger/anoncreds-shared' import { JsonTransformer, W3cJsonLdVerifiableCredential, type JsonObject } from '@credo-ts/core' @@ -6,8 +6,8 @@ import { Credential, W3cCredential } from '@hyperledger/anoncreds-shared' export async function legacyCredentialToW3cCredential( legacyCredential: AnonCredsCredential, - credentialDefinition: AnonCredsCredentialDefinition, - process?: Omit + qualifiedIssuerId: string, + process?: ProcessCredentialOptions ) { let credential: W3cJsonLdVerifiableCredential let anonCredsCredential: Credential | undefined @@ -17,15 +17,11 @@ export async function legacyCredentialToW3cCredential( try { anonCredsCredential = Credential.fromJson(legacyCredential as unknown as JsonObject) w3cCredentialObj = anonCredsCredential.toW3c({ - credentialDefinition: credentialDefinition as unknown as JsonObject, + issuerId: qualifiedIssuerId, w3cVersion: '1.1', }) - const jsonObject = process - ? w3cCredentialObj - .process({ ...process, credentialDefinition: credentialDefinition as unknown as JsonObject }) - .toJson() - : w3cCredentialObj.toJson() + const jsonObject = process ? w3cCredentialObj.process(process).toJson() : w3cCredentialObj.toJson() credential = JsonTransformer.fromJSON(jsonObject, W3cJsonLdVerifiableCredential) } finally { diff --git a/yarn.lock b/yarn.lock index 5e2d6c415c..7657cd9682 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1263,39 +1263,22 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@hyperledger/anoncreds-nodejs@^0.2.0-dev.10": - version "0.2.0-dev.10" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.10.tgz#18f0304d8a710e930f4abcb0242827b2187b37e9" - integrity sha512-Dj5f1CFsWOQWcQCWEralMC/YvEFhIy+ZSlUBN8ZUTg81UW6Ik438BmHnqrbymdCjy9WMXPPVjSmvrubYI7KFew== +"@hyperledger/anoncreds-nodejs@^0.2.0-dev.11": + version "0.2.0-dev.11" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.11.tgz#de89612c64a5531790680e0f92ac9060321202d0" + integrity sha512-gBKWAc6ViyAbntLGJKwMN1tC6S8VxGDRgJiZxw6KJb/1H7wAB62kpS0oT80SwCEhbirgmOhLZQUlwmYoUqHddg== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/anoncreds-shared" "0.2.0-dev.10" + "@hyperledger/anoncreds-shared" "0.2.0-dev.11" "@mapbox/node-pre-gyp" "^1.0.11" ref-array-di "1.2.2" ref-struct-di "1.1.1" -"@hyperledger/anoncreds-nodejs@^0.2.0-dev.9": - version "0.2.0-dev.9" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.9.tgz#f33385780485f97bb3122d90611cc584157d2be9" - integrity sha512-XrpaYNDJTpxzGMKJP7icePKnu0jhkCKP8U7LAS7cNxt5fgkJzW4zb4TPINLNKs28RFYwxm9fOss8R3mfCVEiuA== - dependencies: - "@2060.io/ffi-napi" "4.0.8" - "@2060.io/ref-napi" "3.0.6" - "@hyperledger/anoncreds-shared" "0.2.0-dev.9" - "@mapbox/node-pre-gyp" "^1.0.11" - ref-array-di "1.2.2" - ref-struct-di "1.1.1" - -"@hyperledger/anoncreds-shared@0.2.0-dev.10", "@hyperledger/anoncreds-shared@^0.2.0-dev.10": - version "0.2.0-dev.10" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.10.tgz#49208950a0aff96742fda7f7a7ecd8b1784ecff4" - integrity sha512-jbbsgiYjjEceOgD9iVXCLMILgpyDZrhXfs/ZXOnfr8xk9ZPeB3Xp7ZqpyNSTPJkNaORM9eC/ni56A1dLbGeBIg== - -"@hyperledger/anoncreds-shared@0.2.0-dev.9": - version "0.2.0-dev.9" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.9.tgz#da6cbab72324b1185f97e3edaf8fef752117795b" - integrity sha512-2cK6x2jq98JjKJQRYGmhyPWLB0aYBYrUDM1J/kSQP2RCRoHj1hHV6Ok/DlUmxk+wO1o+71gvb8CYvoGPMI6C4Q== +"@hyperledger/anoncreds-shared@0.2.0-dev.11", "@hyperledger/anoncreds-shared@^0.2.0-dev.11": + version "0.2.0-dev.11" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.11.tgz#2ee81aad40be00a9cf420f43ddf0249088f60368" + integrity sha512-8Pspg14+/q7ayGGXk9y1wbrgZtyeEON0z6nxmK2KkcKv0EG2GL/590+xlroRWo8NDjCDVkvHIMfH+qeXGcJO0Q== "@hyperledger/aries-askar-nodejs@^0.2.0-dev.6": version "0.2.0-dev.6" From 8a278d00e496879ebf93be4c25e4344e031b381c Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 6 Feb 2024 09:12:28 +0100 Subject: [PATCH 24/38] feat: update pex to official release --- .../tests/data-integrity.e2e.test.ts | 4 ++-- packages/core/package.json | 2 +- yarn.lock | 21 ++++++++----------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/anoncreds/tests/data-integrity.e2e.test.ts b/packages/anoncreds/tests/data-integrity.e2e.test.ts index 0137b1baa5..eb5f5a0641 100644 --- a/packages/anoncreds/tests/data-integrity.e2e.test.ts +++ b/packages/anoncreds/tests/data-integrity.e2e.test.ts @@ -1,7 +1,7 @@ import type { AnonCredsTestsAgent } from './anoncredsSetup' import type { AgentContext, KeyDidCreateOptions } from '@credo-ts/core' import type { EventReplaySubject } from '@credo-ts/core/tests' -import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' +import type { InputDescriptorV2 } from '@sphereon/pex-models' import { AutoAcceptCredential, @@ -269,7 +269,7 @@ async function anonCredsFlowTest(options: { state: ProofState.ProposalReceived, }) - const pdCopy: PresentationDefinitionV1 = JSON.parse(JSON.stringify(presentationDefinition)) + const pdCopy = JSON.parse(JSON.stringify(presentationDefinition)) if (!revocationRegistryDefinitionId) pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => delete ide.constraints?.statuses) diff --git a/packages/core/package.json b/packages/core/package.json index df6c854304..b24be9e9bd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -29,7 +29,7 @@ "@multiformats/base-x": "^4.0.1", "@sd-jwt/core": "^0.2.0", "@sd-jwt/decode": "^0.2.0", - "@sphereon/pex": "../PEX/", + "@sphereon/pex": "3.2.1-unstable.7", "@sphereon/pex-models": "^2.2.0", "@sphereon/ssi-types": "^0.18.1", "@stablelib/ed25519": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index 7657cd9682..25252c94fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2596,18 +2596,15 @@ "@sphereon/ssi-types" "^0.18.1" uuid "^9.0.0" -"@sphereon/pex-models@^2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.5.tgz#ba4474a3783081392b72403c4c8ee6da3d2e5585" - integrity sha512-7THexvdYUK/Dh8olBB46ErT9q/RnecnMdb5r2iwZ6be0Dt4vQLAUN7QU80H0HZBok4jRTb8ydt12x0raBSTHOg== - -"@sphereon/pex-models@^2.2.0": +"@sphereon/pex-models@^2.1.5", "@sphereon/pex-models@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.2.0.tgz#32013fff43d4f47df03e213792a9bcc6866a1f06" integrity sha512-dGDRdoxJj+P0TRqu0R8R0/IdIzrCya1MsnxIFbcmSW3rjPsbwXbV0EojEfxXGD5LhqsUJiuAffMtyE2dtVI/XQ== -"@sphereon/pex@../PEX/": - version "3.1.1-unstable.0" +"@sphereon/pex@3.2.1-unstable.7": + version "3.2.1-unstable.7" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.2.1-unstable.7.tgz#218d39c2311e5d542258607883185cacc3e6e862" + integrity sha512-X55PUfZL5gZ/mJinNS+eQ/iUKuFmNA6PP8NU14p4SemZbt/8kn67XYM6Nl/hYSFDysx64daPMRfPTkopKAfT+Q== dependencies: "@astronautlabs/jsonpath" "^1.1.2" "@sd-jwt/decode" "^0.2.0" @@ -2622,15 +2619,15 @@ string.prototype.matchall "^4.0.10" "@sphereon/pex@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.0.1.tgz#e7d9d36c7c921ab97190a735c67e0a2632432e3b" - integrity sha512-rj+GhFfV5JLyo7dTIA3htWlrT+f6tayF9JRAGxdsIYBfYictLi9BirQ++JRBXsiq7T5zMnfermz4RGi3cvt13Q== + version "3.2.0" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.2.0.tgz#2b8cd5e9094c88c2cbf822b1b70584ca4a08293a" + integrity sha512-6qk4L7PaxFsHSVjG0w5SbffwuwI0sbnwyoaNBNku17u2WOThBcnH22sgCdNRRbzacXs0e4iAw7Cb1cd730LQaQ== dependencies: "@astronautlabs/jsonpath" "^1.1.2" "@sd-jwt/decode" "^0.2.0" "@sd-jwt/present" "^0.2.0" "@sd-jwt/utils" "^0.2.0" - "@sphereon/pex-models" "^2.1.5" + "@sphereon/pex-models" "^2.2.0" "@sphereon/ssi-types" "0.18.1" ajv "^8.12.0" ajv-formats "^2.1.1" From 218cf191233618d3e6d0a5a1312a5873f88d402c Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 6 Feb 2024 09:13:04 +0100 Subject: [PATCH 25/38] fix: missing offerattachment format --- .../formats/__tests__/legacy-indy-format-services.test.ts | 1 + .../src/protocols/credentials/v1/V1CredentialProtocol.ts | 6 ++++++ packages/anoncreds/tests/indy-flow.test.ts | 1 + .../jsonld/__tests__/JsonLdCredentialFormatService.test.ts | 6 ++++++ 4 files changed, 14 insertions(+) diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts index 549e4085a3..4ddf5f5c86 100644 --- a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts +++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts @@ -298,6 +298,7 @@ describe('Legacy indy format services', () => { // Holder processes and accepts credential await indyCredentialFormatService.processCredential(agentContext, { + offerAttachment, credentialRecord: holderCredentialRecord, attachment: credentialAttachment, requestAttachment, diff --git a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts index b4fe0b2857..0ff157b6f2 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts @@ -903,7 +903,13 @@ export class V1CredentialProtocol throw new CredoError('Missing indy credential request attachment in processCredential') } + const offerAttachment = requestCredentialMessage?.getRequestAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + if (!offerAttachment) { + throw new CredoError('Missing indy credential request attachment in processCredential') + } + await this.indyCredentialFormat.processCredential(messageContext.agentContext, { + offerAttachment, attachment: issueAttachment, credentialRecord, requestAttachment, diff --git a/packages/anoncreds/tests/indy-flow.test.ts b/packages/anoncreds/tests/indy-flow.test.ts index b00fdaceaa..32af103d18 100644 --- a/packages/anoncreds/tests/indy-flow.test.ts +++ b/packages/anoncreds/tests/indy-flow.test.ts @@ -289,6 +289,7 @@ describe('Legacy indy format services using anoncreds-rs', () => { // Holder processes and accepts credential await legacyIndyCredentialFormatService.processCredential(agentContext, { + offerAttachment, credentialRecord: holderCredentialRecord, attachment: credentialAttachment, requestAttachment, diff --git a/packages/core/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts b/packages/core/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts index 6e131f21a0..91f3f23dc0 100644 --- a/packages/core/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts +++ b/packages/core/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts @@ -382,6 +382,7 @@ describe('JsonLd CredentialFormatService', () => { // when await jsonLdFormatService.processCredential(agentContext, { + offerAttachment, attachment: credentialAttachment, requestAttachment: requestAttachment, credentialRecord, @@ -416,6 +417,7 @@ describe('JsonLd CredentialFormatService', () => { // when/then await expect( jsonLdFormatService.processCredential(agentContext, { + offerAttachment: offerAttachment, attachment: credentialAttachment, requestAttachment: requestAttachment, credentialRecord, @@ -440,6 +442,7 @@ describe('JsonLd CredentialFormatService', () => { // when/then await expect( jsonLdFormatService.processCredential(agentContext, { + offerAttachment, attachment: credentialAttachment, requestAttachment: requestAttachmentWithDomain, credentialRecord, @@ -466,6 +469,7 @@ describe('JsonLd CredentialFormatService', () => { // when/then await expect( jsonLdFormatService.processCredential(agentContext, { + offerAttachment, attachment: credentialAttachment, requestAttachment: requestAttachmentWithChallenge, credentialRecord, @@ -488,6 +492,7 @@ describe('JsonLd CredentialFormatService', () => { // when/then await expect( jsonLdFormatService.processCredential(agentContext, { + offerAttachment, attachment: credentialAttachment, requestAttachment: requestAttachmentWithProofType, credentialRecord, @@ -510,6 +515,7 @@ describe('JsonLd CredentialFormatService', () => { // when/then await expect( jsonLdFormatService.processCredential(agentContext, { + offerAttachment, attachment: credentialAttachment, requestAttachment: requestAttachmentWithProofPurpose, credentialRecord, From 1ea69dedad62ad7af7d8d97e7dbf37161073e265 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 6 Feb 2024 09:33:48 +0100 Subject: [PATCH 26/38] fix: offerattachment issues --- .../src/protocols/credentials/v1/V1CredentialProtocol.ts | 2 +- .../credentials/v1/__tests__/V1CredentialProtocolCred.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts index 0ff157b6f2..e993de22fc 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts @@ -903,7 +903,7 @@ export class V1CredentialProtocol throw new CredoError('Missing indy credential request attachment in processCredential') } - const offerAttachment = requestCredentialMessage?.getRequestAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + const offerAttachment = offerCredentialMessage?.getOfferAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) if (!offerAttachment) { throw new CredoError('Missing indy credential request attachment in processCredential') } diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts index 4c9b999d35..c918c438e2 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts @@ -524,6 +524,7 @@ describe('V1CredentialProtocol', () => { attachment: credentialAttachment, credentialRecord, requestAttachment: expect.any(Attachment), + offerAttachment: expect.any(Attachment), }) }) }) From 046b633bb74ee4a4f2616db72c5a350e0fb3cc28 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 6 Feb 2024 09:33:53 +0100 Subject: [PATCH 27/38] fix: simplify linksecret handling for the anoncreds2023 cryptosuite --- .../AnonCreds2023DataIntegrityService.ts | 106 +++++++----------- 1 file changed, 41 insertions(+), 65 deletions(-) diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts index ddf5ec6d5c..37778aa074 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts @@ -98,11 +98,7 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI return result } - private getCredentialMetadataForDescriptor( - descriptorMapObject: Descriptor, - selectedCredentials: JsonObject[], - selectedCredentialRecords?: W3cCredentialRecord[] - ) { + private getCredentialMetadataForDescriptor(descriptorMapObject: Descriptor, selectedCredentials: JsonObject[]) { const credentialExtractionResult = this.extractPathNodes({ verifiableCredential: selectedCredentials }, [ descriptorMapObject.path, ]) @@ -117,23 +113,11 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI const entryIndex = selectedCredentials.findIndex((credential) => deepEquality(credential, credentialJson)) if (entryIndex === -1) throw new CredoError('Could not find selected credential') - const credentialRecord = selectedCredentialRecords ? selectedCredentialRecords[entryIndex] : undefined - if ( - credentialRecord && - !deepEquality(JsonTransformer.toJSON(credentialRecord.credential), selectedCredentials[entryIndex]) - ) { - throw new CredoError('selected credential does not match the selected credential record') - } - - const anonCredsTags = credentialRecord?.getAnonCredsTags() - if (credentialRecord && !anonCredsTags) throw new CredoError('No anoncreds tags found on credential record') - const { credentialDefinitionId, revocationRegistryId, schemaId } = AnonCredsW3cCredential.fromJson(credentialJson) return { entryIndex, credentialJson, - anonCredsTags, credentialDefinitionId, revocationRegistryId, schemaId, @@ -252,18 +236,14 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI return credentialDefinitions } - private getPresentationMetadata = async ( - agentContext: AgentContext, - input: { - credentialsWithMetadata: CredentialWithMetadata[] - credentialsProve: CredentialProve[] - linkSecretIds: Set - schemaIds: Set - credentialDefinitionIds: Set - } - ) => { - const { linkSecretIds, credentialDefinitionIds, schemaIds, credentialsWithMetadata, credentialsProve } = input - const linkSecretIdArray = [...linkSecretIds] + private async getLinkSecret(agentContext: AgentContext, credentialRecord: W3cCredentialRecord[]) { + const linkSecrets = new Set( + credentialRecord + .map((record) => record.getAnonCredsTags()?.linkSecretId) + .filter((linkSecretId): linkSecretId is string => linkSecretId !== undefined) + ) + const linkSecretIdArray = [...linkSecrets] + if (linkSecretIdArray.length > 1) { throw new CredoError('Multiple linksecret cannot be used to create a single presentation') } else if (linkSecretIdArray.length === 0) { @@ -275,6 +255,19 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI .getByLinkSecretId(agentContext, linkSecretIdArray[0]) if (!linkSecretRecord.value) throw new CredoError('Link Secret value not stored') + return linkSecretRecord.value + } + + private getPresentationMetadata = async ( + agentContext: AgentContext, + input: { + credentialsWithMetadata: CredentialWithMetadata[] + credentialsProve: CredentialProve[] + schemaIds: Set + credentialDefinitionIds: Set + } + ) => { + const { credentialDefinitionIds, schemaIds, credentialsWithMetadata, credentialsProve } = input const credentials: W3cCredentialEntry[] = await Promise.all( credentialsWithMetadata.map(async ({ credential, nonRevoked }) => { @@ -301,7 +294,6 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI return { schemas, credentialDefinitions, - linkSecret: linkSecretRecord.value, credentialsProve, credentials, } @@ -387,15 +379,11 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI agentContext: AgentContext, presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, presentationSubmission: PresentationSubmission, - credentials: JsonObject[], - holderOpts?: { - selectedCredentialRecords: W3cCredentialRecord[] - } + credentials: JsonObject[] ) => { const credentialsProve: CredentialProve[] = [] const schemaIds = new Set() const credentialDefinitionIds = new Set() - const linkSecretIds = new Set() const credentialsWithMetadata: CredentialWithMetadata[] = [] const hash = Hasher.hash(TypedArrayEncoder.fromString(presentationDefinition.id), 'sha-256') @@ -429,15 +417,11 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI const fields = descriptor.constraints?.fields if (!fields) throw new CredoError('Unclear mapping of constraint with no fields.') - const { entryIndex, schemaId, credentialDefinitionId, revocationRegistryId, anonCredsTags, credentialJson } = - this.getCredentialMetadataForDescriptor(descriptorMapObject, credentials, holderOpts?.selectedCredentialRecords) + const { entryIndex, schemaId, credentialDefinitionId, revocationRegistryId, credentialJson } = + this.getCredentialMetadataForDescriptor(descriptorMapObject, credentials) - if (holderOpts) { - if (!anonCredsTags) throw new CredoError('Anoncreds tags are required for holder') - schemaIds.add(schemaId) - credentialDefinitionIds.add(credentialDefinitionId) - linkSecretIds.add(anonCredsTags.linkSecretId) - } + schemaIds.add(schemaId) + credentialDefinitionIds.add(credentialDefinitionId) const requiresRevocationStatus = this.descriptorRequiresRevocationStatus(descriptor) if (requiresRevocationStatus && !revocationRegistryId) { @@ -489,21 +473,7 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI } } - const presentationMetadata = holderOpts - ? await this.getPresentationMetadata(agentContext, { - credentialsWithMetadata, - credentialsProve, - linkSecretIds, - schemaIds, - credentialDefinitionIds, - }) - : undefined - - const revocationMetadata = !holderOpts - ? await this.getRevocationMetadataForCredentials(agentContext, credentialsWithMetadata) - : undefined - - return { anonCredsProofRequest, presentationMetadata, revocationMetadata } + return { anonCredsProofRequest, credentialsWithMetadata, credentialsProve, schemaIds, credentialDefinitionIds } } public async createPresentation( @@ -516,16 +486,19 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI } ) { const { presentationDefinition, presentationSubmission, selectedCredentialRecords, selectedCredentials } = options - const { anonCredsProofRequest, presentationMetadata } = await this.createAnonCredsProofRequestAndMetadata( + + const linkSecret = await this.getLinkSecret(agentContext, selectedCredentialRecords) + + const { anonCredsProofRequest, ...metadata } = await this.createAnonCredsProofRequestAndMetadata( agentContext, presentationDefinition, presentationSubmission, - selectedCredentials, - { selectedCredentialRecords } + selectedCredentials ) - if (!presentationMetadata) throw new CredoError('Presentation metadata not created') - const { schemas, credentialDefinitions, linkSecret, credentialsProve, credentials } = presentationMetadata + const presentationMetadata = await this.getPresentationMetadata(agentContext, metadata) + + const { schemas, credentialDefinitions, credentialsProve, credentials } = presentationMetadata let presentation: AnonCredsW3cPresentation | undefined try { @@ -566,13 +539,16 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI } const verifiableCredentialsJson = verifiableCredentials.map((credential) => JsonTransformer.toJSON(credential)) - const { anonCredsProofRequest, revocationMetadata } = await this.createAnonCredsProofRequestAndMetadata( + const { anonCredsProofRequest, ...metadata } = await this.createAnonCredsProofRequestAndMetadata( agentContext, presentationDefinition, presentationSubmission, verifiableCredentialsJson ) - if (!revocationMetadata) throw new CredoError('Missing revocation metadata') + const revocationMetadata = await this.getRevocationMetadataForCredentials( + agentContext, + metadata.credentialsWithMetadata + ) const credentialDefinitions = await this.getCredentialDefinitions(agentContext, credentialDefinitionIds) const schemaIds = new Set(Object.values(credentialDefinitions).map((cd) => cd.schemaId)) From 9c2d57459f2dc92203988ea5c4822995f8d7e855 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 6 Feb 2024 11:02:37 +0100 Subject: [PATCH 28/38] fix: tests --- .../V1CredentialProtocolCred.test.ts | 4 +- .../V1CredentialProtocolProposeOffer.test.ts | 2 +- .../data-integrity-flow-anoncreds.test.ts | 2 +- .../v2-connectionless-credentials.e2e.test.ts | 4 +- .../v2-credentials-auto-accept.e2e.test.ts | 6 +- .../v2/__tests__/v2-indy-proofs.e2e.test.ts | 6 +- .../libraries/contexts/dataIntegrity_v2.ts | 81 +++++++++++++++++++ .../libraries/contexts/defaultContexts.ts | 2 + .../__tests__/__snapshots__/0.3.test.ts.snap | 1 + .../__tests__/__snapshots__/0.4.test.ts.snap | 1 + 10 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 packages/core/src/modules/vc/data-integrity/libraries/contexts/dataIntegrity_v2.ts diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts index c918c438e2..0f7c493300 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts @@ -67,7 +67,7 @@ const connectionService = new ConnectionServiceMock() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -legacyIndyCredentialFormatService.credentialRecordType = 'anoncreds' +legacyIndyCredentialFormatService.credentialRecordType = 'w3c' const connection = getMockConnection({ id: '123', @@ -174,7 +174,7 @@ const mockCredentialRecord = ({ connectionId: connectionId ?? '123', credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: '123456', }, ], diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts index 59db789343..fb5f973652 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolProposeOffer.test.ts @@ -55,7 +55,7 @@ const agentContext = getAgentContext({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -indyCredentialFormatService.credentialRecordType = 'anoncreds' +indyCredentialFormatService.credentialRecordType = 'w3c' const connectionRecord = getMockConnection({ id: '123', diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts index 23c5fc3b97..a7aed5d2ba 100644 --- a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts @@ -374,7 +374,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean expect( (requestAttachment.getDataAsJson() as DataIntegrityCredentialRequest).binding_proof?.anoncreds_link_secret?.entropy ).toBeDefined() - expect((requestAttachment.getDataAsJson() as any).prover_did).toBeUndefined() + expect((requestAttachment.getDataAsJson() as Record).prover_did).toBeUndefined() // Issuer processes and accepts request await dataIntegrityCredentialFormatService.processRequest(agentContext, { diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts index 2a90d4ec88..a722bdbbba 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts @@ -166,7 +166,7 @@ describe('V2 Connectionless Credentials', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], @@ -247,7 +247,7 @@ describe('V2 Connectionless Credentials', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.e2e.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.e2e.test.ts index 5e909c1457..5f97cd5d6b 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.e2e.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.e2e.test.ts @@ -140,7 +140,7 @@ describe('V2 Credentials Auto Accept', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], @@ -246,7 +246,7 @@ describe('V2 Credentials Auto Accept', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], @@ -325,7 +325,7 @@ describe('V2 Credentials Auto Accept', () => { }, credentials: [ { - credentialRecordType: 'anoncreds', + credentialRecordType: 'w3c', credentialRecordId: expect.any(String), }, ], diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts index a942e4744f..e8ebf2fc16 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts @@ -602,7 +602,7 @@ describe('Present Proof', () => { image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', name: 'John', - age: '99', + age: 99, }, schemaId: expect.any(String), credentialDefinitionId: expect.any(String), @@ -618,7 +618,7 @@ describe('Present Proof', () => { credentialInfo: { credentialId: expect.any(String), attributes: { - age: '99', + age: 99, image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', name: 'John', @@ -641,7 +641,7 @@ describe('Present Proof', () => { image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', name: 'John', - age: '99', + age: 99, }, schemaId: expect.any(String), credentialDefinitionId: expect.any(String), diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/dataIntegrity_v2.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/dataIntegrity_v2.ts new file mode 100644 index 0000000000..d7aa6dff6d --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/dataIntegrity_v2.ts @@ -0,0 +1,81 @@ +export const DATA_INTEGRITY_V2 = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + DataIntegrityProof: { + '@id': 'https://w3id.org/security#DataIntegrityProof', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + previousProof: { + '@id': 'https://w3id.org/security#previousProof', + '@type': '@id', + }, + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + cryptosuite: { + '@id': 'https://w3id.org/security#cryptosuite', + '@type': 'https://w3id.org/security#cryptosuiteString', + }, + proofValue: { + '@id': 'https://w3id.org/security#proofValue', + '@type': 'https://w3id.org/security#multibase', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts index 0a3c8ecf5f..210a67a04e 100644 --- a/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts @@ -1,6 +1,7 @@ import { X25519_V1 } from './X25519_v1' import { BBS_V1 } from './bbs_v1' import { CREDENTIALS_V1 } from './credentials_v1' +import { DATA_INTEGRITY_V2 } from './dataIntegrity_v2' import { DID_V1 } from './did_v1' import { ED25519_V1 } from './ed25519_v1' import { ODRL } from './odrl' @@ -29,4 +30,5 @@ export const DEFAULT_CONTEXTS = { 'https://identity.foundation/presentation-exchange/submission/v1': PRESENTATION_SUBMISSION, 'https://purl.imsglobal.org/spec/ob/v3p0/context.json': PURL_OB_V3P0, 'https://w3c-ccg.github.io/vc-status-rl-2020/contexts/vc-revocation-list-2020/v1.jsonld': VC_REVOCATION_LIST_2020, + 'https://w3id.org/security/data-integrity/v2': DATA_INTEGRITY_V2, } diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap index fdbb18199d..d1267f8270 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.3.test.ts.snap @@ -10,6 +10,7 @@ exports[`UpdateAssistant | v0.3.1 - v0.4 should correctly update 'claimFormat' t "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1", ], + "cryptosuites": [], "expandedTypes": [ "https", "https", diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.4.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.4.test.ts.snap index 8758d79c27..9163e70cfe 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.4.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.4.test.ts.snap @@ -10,6 +10,7 @@ exports[`UpdateAssistant | v0.4 - v0.5 should correctly add 'type' tag to w3c re "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1", ], + "cryptosuites": [], "expandedTypes": [ "https://www.w3.org/2018/credentials#VerifiableCredential", "https://example.org/examples#UniversityDegreeCredential", From cacc3c074d6a09ac8b8c187c7919aabefdf4f813 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 6 Feb 2024 11:25:06 +0100 Subject: [PATCH 29/38] fix: migration --- .../w3cCredentialRecordMigration.test.ts | 6 +++ .../0.4-0.5/anonCredsCredentialRecord.ts | 54 ++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts index b815dc47dd..cb9eb9eee4 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts @@ -2,11 +2,13 @@ import type { Wallet } from '@credo-ts/core' import { Agent, + CacheModuleConfig, ConsoleLogger, DidResolverService, DidsModuleConfig, Ed25519Signature2018, EventEmitter, + InMemoryLruCache, InjectionSymbols, KeyType, SignatureSuiteToken, @@ -43,6 +45,8 @@ const w3cRepo = { save: jest.fn(), } +const cacheModuleConfig = new InMemoryLruCache({ limit: 500 }) + const inMemoryStorageService = new InMemoryStorageService() const logger = new ConsoleLogger() const agentContext = getAgentContext({ @@ -57,6 +61,7 @@ const agentContext = getAgentContext({ [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], [InjectionSymbols.Logger, logger], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + [CacheModuleConfig, cacheModuleConfig], [AnonCredsModuleConfig, anonCredsModuleConfig], [ SignatureSuiteToken, @@ -86,6 +91,7 @@ jest.mock('../../../../../core/src/agent/Agent', () => { config: agentConfig, context: agentContext, dependencyManager: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any resolve: jest.fn((repo: any) => { if (repo.prototype.constructor.name === 'AnonCredsCredentialRepository') { return anonCredsRepo diff --git a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts index 14a9868d30..a04e8cdf19 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts @@ -1,9 +1,8 @@ -import type { AnonCredsCredentialRecord } from '../../repository' import type { AgentContext, BaseAgent } from '@credo-ts/core' -import { W3cCredentialService } from '@credo-ts/core' +import { CacheModuleConfig, W3cCredentialService } from '@credo-ts/core' -import { AnonCredsCredentialRepository } from '../../repository' +import { AnonCredsCredentialRepository, type AnonCredsCredentialRecord } from '../../repository' import { legacyCredentialToW3cCredential } from '../../utils' import { getQualifiedId, fetchCredentialDefinition, getIndyNamespace } from '../../utils/ledgerObjects' @@ -11,14 +10,45 @@ async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRe const legacyCredential = legacyRecord.credential const legacyTags = legacyRecord.getTags() - // TODO: check if it is in cache - const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, legacyTags.credentialDefinitionId) - const namespace = getIndyNamespace(credentialDefinitionReturn.qualifiedId) + let qualifiedCredentialDefinitionId: string | undefined + let qualifiedIssuerId: string | undefined + let namespace: string | undefined - const w3cJsonLdCredential = await legacyCredentialToW3cCredential( - legacyCredential, - credentialDefinitionReturn.qualifiedCredentialDefinition.issuerId - ) + const cacheModuleConfig = agentContext.dependencyManager.resolve(CacheModuleConfig) + const cache = cacheModuleConfig?.cache + const indyCacheKey = `IndyVdrPoolService:${legacyTags.credentialDefinitionId}` + const sovCacheKey = `IndySdkPoolService:${legacyTags.credentialDefinitionId}` + + const cachedNymResponse: Record | null = + (await cache.get(agentContext, indyCacheKey)) ?? (await cache.get(agentContext, sovCacheKey)) + + namespace = cachedNymResponse?.indyNamespace + + if (!namespace) { + try { + const credentialDefinitionReturn = await fetchCredentialDefinition( + agentContext, + legacyTags.credentialDefinitionId + ) + qualifiedCredentialDefinitionId = credentialDefinitionReturn.qualifiedId + qualifiedIssuerId = credentialDefinitionReturn.qualifiedCredentialDefinition.issuerId + namespace = getIndyNamespace(credentialDefinitionReturn.qualifiedId) + } catch (e) { + agentContext.config.logger.warn( + [ + `Failed to fetch credential definition for credentialId ${legacyTags.credentialDefinitionId}.`, + `Not updating credential with id ${legacyRecord.credentialId} to W3C format.`, + ].join('\n') + ) + } + } else { + qualifiedCredentialDefinitionId = getQualifiedId(legacyTags.credentialDefinitionId, namespace) + qualifiedIssuerId = getQualifiedId(legacyTags.issuerId, namespace) + } + + if (!qualifiedCredentialDefinitionId || !qualifiedIssuerId || !namespace) return + + const w3cJsonLdCredential = await legacyCredentialToW3cCredential(legacyCredential, qualifiedIssuerId) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) await w3cCredentialService.storeCredential(agentContext, { @@ -26,7 +56,7 @@ async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRe anonCredsCredentialRecordOptions: { credentialId: legacyRecord.credentialId, linkSecretId: legacyRecord.linkSecretId, - credentialDefinitionId: credentialDefinitionReturn.qualifiedId, + credentialDefinitionId: qualifiedCredentialDefinitionId, schemaId: getQualifiedId(legacyTags.schemaId, namespace), schemaName: legacyTags.schemaName, schemaIssuerId: getQualifiedId(legacyTags.issuerId, namespace), @@ -36,8 +66,6 @@ async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRe credentialRevocationId: legacyTags.credentialRevocationId, }, }) - - return w3cJsonLdCredential } /** From f456e1ba867df234a89dd55712d1e9a5211ce73f Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 6 Feb 2024 12:32:39 +0100 Subject: [PATCH 30/38] fix: credential validation issues --- .../DataIntegrityCredentialFormatService.ts | 94 ++++++++----------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index 9f4778b681..0c63132c68 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -184,7 +184,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer if (isV1Credential) return '1.1' else if (isV2Credential) return '2.0' - else throw new CredoError('Missing @context in credential offer') + else throw new CredoError('Cannot determine credential version from @context') } public async processOffer( @@ -203,7 +203,9 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const credentialToBeValidated = { ...credential, issuer: credential.issuer ?? 'https://example.com', - ...(credentialVersion === '1.1' && { validFrom: credential.issuanceDate ?? new Date().toISOString() }), + ...(credentialVersion === '1.1' + ? { issuanceDate: new Date().toISOString() } + : { validFrom: new Date().toISOString() }), } JsonTransformer.fromJSON(credentialToBeValidated, W3cCredential) @@ -366,16 +368,16 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer linkSecretId: dataIntegrityFormat.anonCredsLinkSecretAcceptOfferOptions?.linkSecretId, }) + if (!anonCredsCredentialRequest.entropy) throw new CredoError('Missing entropy for anonCredsCredentialRequest') + anonCredsLinkSecretDataIntegrityBindingProof = + anonCredsCredentialRequest as AnonCredsLinkSecretDataIntegrityBindingProof + dataIntegrityRequestMetadata.linkSecretRequestMetadata = anonCredsCredentialRequestMetadata dataIntegrityMetadata.linkSecretMetadata = { credentialDefinitionId: credentialOffer.binding_method.anoncreds_link_secret.cred_def_id, schemaId: credentialDefinitionReturn.credentialDefinition.schemaId, } - - if (!anonCredsCredentialRequest.entropy) throw new CredoError('Missing entropy for anonCredsCredentialRequest') - anonCredsLinkSecretDataIntegrityBindingProof = - anonCredsCredentialRequest as AnonCredsLinkSecretDataIntegrityBindingProof } let didCommSignedAttachmentBindingProof: DidCommSignedAttachmentDataIntegrityBindingProof | undefined = undefined @@ -661,12 +663,10 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer let signedCredential: W3cJsonLdVerifiableCredential | undefined if (credentialRequest.binding_proof?.anoncreds_link_secret) { if (!credentialOffer.binding_method?.anoncreds_link_secret) { - throw new CredoError('Cannot issue credential with a binding method that was not offered.') + throw new CredoError('Cannot issue credential with a binding method that was not offered') } - if (!dataIntegrityMetadata.linkSecretMetadata) { - throw new CredoError('Missing anoncreds link secret metadata') - } + if (!dataIntegrityMetadata.linkSecretMetadata) throw new CredoError('Missing anoncreds link secret metadata') signedCredential = await this.createCredentialWithAnonCredsDataIntegrityProof(agentContext, { credentialRecord, @@ -675,36 +675,11 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer anonCredsLinkSecretBindingProof: credentialRequest.binding_proof.anoncreds_link_secret, credentialSubjectId: dataIntegrityFormat.credentialSubjectId, }) - - const proofs = Array.isArray(signedCredential.proof) ? signedCredential.proof : [signedCredential.proof] - if (proofs.length > 1) { - throw new CredoError('Credential cannot have multiple proofs at this point') - } - - if ( - signedCredential.issuerId !== offeredCredential.issuerId || - !proofs[0].verificationMethod.startsWith(signedCredential.issuerId) - ) { - throw new CredoError('Invalid issuer in credential') - } - - if (offeredCredential.type.length !== 1 || offeredCredential.type[0] !== 'VerifiableCredential') { - throw new CredoError('Offered Invalid credential type') - } - - const integrityProtectedFields = ['@context', 'issuer', 'type', 'credentialSubject', 'validFrom', 'issuanceDate'] - if ( - Object.keys(credentialOffer.credential).some( - (key) => !integrityProtectedFields.includes(key) && key !== 'proof' - ) - ) { - throw new CredoError('Invalid credential subject id') - } } if (credentialRequest.binding_proof?.didcomm_signed_attachment) { if (!credentialOffer.binding_method?.didcomm_signed_attachment) { - throw new CredoError('Cannot issue credential with a binding method that was not offered.') + throw new CredoError('Cannot issue credential with a binding method that was not offered') } const bindingProofAttachment = requestAppendAttachments?.find( @@ -810,11 +785,11 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer { credentialRecord, attachment, requestAttachment, offerAttachment }: CredentialFormatProcessCredentialOptions ): Promise { const credentialOffer = offerAttachment.getDataAsJson() + const offeredCredentialJson = credentialOffer.credential const credentialRequestMetadata = credentialRecord.metadata.get( DataIntegrityRequestMetadataKey ) - if (!credentialRequestMetadata) { throw new CredoError(`Missing request metadata for credential exchange with thread id ${credentialRecord.id}`) } @@ -828,27 +803,27 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { credential: credentialJson } = attachment.getDataAsJson() - const offeredCredentialJson = credentialOffer.credential + if (Array.isArray(offeredCredentialJson.credentialSubject)) { + throw new CredoError('Invalid credential subject. Only single credential subject object are supported') + } - if (!Array.isArray(offeredCredentialJson.credentialSubject)) { - const credentialSubjectMatches = Object.entries(offeredCredentialJson.credentialSubject as JsonObject).every( - ([key, offeredValue]) => { - const receivedValue = (credentialJson.credentialSubject as JsonObject)[key] - if (!offeredValue || !receivedValue) return false + const credentialSubjectMatches = Object.entries(offeredCredentialJson.credentialSubject as JsonObject).every( + ([key, offeredValue]) => { + const receivedValue = (credentialJson.credentialSubject as JsonObject)[key] + if (!offeredValue || !receivedValue) return false - if (typeof offeredValue === 'number' || typeof receivedValue === 'number') { - return offeredValue.toString() === receivedValue.toString() - } - - return deepEquality(offeredValue, receivedValue) + if (typeof offeredValue === 'number' || typeof receivedValue === 'number') { + return offeredValue.toString() === receivedValue.toString() } - ) - if (!credentialSubjectMatches) { - throw new CredoError( - 'Received invalid credential. Received credential subject does not match the offered credential subject.' - ) + return deepEquality(offeredValue, receivedValue) } + ) + + if (!credentialSubjectMatches) { + throw new CredoError( + 'Received invalid credential. Received credential subject does not match the offered credential subject.' + ) } const credentialVersion = this.getCredentialVersion(credentialJson) @@ -858,7 +833,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credentialSubject: credentialJson.credentialSubject, ...(credentialVersion === '1.1' && { issuanceDate: credentialJson.issuanceDate }), ...(credentialVersion === '2.0' && { validFrom: credentialJson.validFrom }), - ...(offeredCredentialJson.credentialStatus === '2.0' && { credentialStatus: credentialJson.credentialStatus }), + ...(offeredCredentialJson.credentialStatus && { credentialStatus: credentialJson.credentialStatus }), proof: credentialJson.proof, } @@ -888,6 +863,17 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer anonCredsCredentialRecordOptions.schemaId, true ) + + const integrityProtectedFields = ['@context', 'issuer', 'type', 'credentialSubject', 'validFrom', 'issuanceDate'] + if ( + Object.keys(offeredCredentialJson).some((key) => !integrityProtectedFields.includes(key) && key !== 'proof') + ) { + throw new CredoError('Credential offer contains non anoncreds integrity protected fields.') + } + + if (w3cJsonLdVerifiableCredential.type.length !== 1) { + throw new CredoError(`Invalid credential type. Only single credential type 'VerifiableCredential' is supported`) + } } else { w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) } From 1c36fa2b94dbb4751ea575f5340a8599d43a2761 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 13 Feb 2024 11:19:37 +0100 Subject: [PATCH 31/38] fix: incoroporate feedback --- packages/anoncreds/package.json | 1 + packages/anoncreds/src/AnonCredsModule.ts | 18 +- .../src/__tests__/AnonCredsModule.test.ts | 9 +- ...ce.ts => AnonCredsDataIntegrityService.ts} | 191 +++---- .../anoncreds-rs/AnonCredsRsHolderService.ts | 517 +++++++++--------- .../AnonCredsRsVerifierService.ts | 30 +- .../AnonCredsRsHolderService.test.ts | 399 +++++++------- .../__tests__/AnonCredsRsServices.test.ts | 33 +- .../src/anoncreds-rs/__tests__/helpers.ts | 67 ++- .../AnonCredsCredentialFormatService.ts | 47 +- .../formats/AnonCredsProofFormatService.ts | 11 +- .../DataIntegrityCredentialFormatService.ts | 245 ++++----- .../LegacyIndyCredentialFormatService.ts | 48 +- .../formats/LegacyIndyProofFormatService.ts | 34 +- .../legacy-indy-format-services.test.ts | 8 +- packages/anoncreds/src/index.ts | 18 +- .../src/models/W3cAnonCredsMetadata.ts | 0 packages/anoncreds/src/models/exchange.ts | 2 +- packages/anoncreds/src/models/internal.ts | 9 +- .../services/AnonCredsHolderServiceOptions.ts | 7 +- .../w3cCredentialRecordMigration.test.ts | 289 ++++++---- .../0.4-0.5/anonCredsCredentialRecord.ts | 166 ++++-- .../anoncreds/src/updates/0.4-0.5/index.ts | 2 +- .../anonCredsCredentialValue.test.ts | 2 +- .../anoncreds/src/utils/anonCredsObjects.ts | 95 ++++ packages/anoncreds/src/utils/credential.ts | 87 ++- packages/anoncreds/src/utils/index.ts | 19 +- .../anoncreds/src/utils/indyIdentifiers.ts | 190 +++++++ packages/anoncreds/src/utils/ledgerObjects.ts | 328 ----------- packages/anoncreds/src/utils/metadata.ts | 34 +- .../anoncreds/src/utils/w3cAnonCredsUtils.ts | 201 +++++++ packages/anoncreds/src/utils/w3cUtils.ts | 41 -- .../tests/InMemoryAnonCredsRegistry.ts | 138 +++-- .../anoncreds/tests/anoncreds-flow.test.ts | 10 +- .../data-integrity-flow-anoncreds.test.ts | 58 +- .../tests/data-integrity-flow-w3c.test.ts | 291 +++++----- .../tests/data-integrity-flow.test.ts | 78 +-- .../tests/data-integrity.e2e.test.ts | 57 +- packages/anoncreds/tests/indy-flow.test.ts | 9 +- .../DataIntegrityCredentialFormat.ts | 26 +- .../dataIntegrity/dataIntegrityExchange.ts | 2 +- .../dataIntegrity/dataIntegrityMetadata.ts | 45 -- .../formats/dataIntegrity/index.ts | 2 - .../RevocationNotificationService.test.ts | 7 +- .../DifPresentationExchangeService.ts | 27 +- .../utils/credentialSelection.ts | 4 +- ...fPresentationExchangeProofFormatService.ts | 27 +- .../src/modules/vc/W3cCredentialService.ts | 1 - .../modules/vc/W3cCredentialServiceOptions.ts | 2 - .../models/DataIntegrityProof.ts | 81 +++ .../models/IAnonCredsDataIntegrityService.ts} | 19 +- .../data-integrity/models/LinkedDataProof.ts | 29 +- .../data-integrity/models/ProofTransformer.ts | 38 ++ .../models/W3cJsonLdVerifiableCredential.ts | 22 +- .../models/W3cJsonLdVerifiablePresentation.ts | 23 +- .../modules/vc/data-integrity/models/index.ts | 2 + .../models/credential/W3cCredentialSubject.ts | 12 +- .../W3CAnoncredsCredentialMetadata.ts | 36 -- .../vc/repository/W3cCredentialRecord.ts | 183 ++----- .../vc/repository/W3cCredentialRepository.ts | 15 - .../__tests__/W3cCredentialRecord.test.ts | 72 +-- .../vc/repository/anonCredsCredentialValue.ts | 88 --- .../core/src/modules/vc/repository/index.ts | 6 - packages/core/tests/helpers.ts | 21 + .../src/IndySdkToAskarMigrationUpdater.ts | 3 +- .../OpenId4vcSiopHolderService.ts | 2 +- .../OpenId4VcSiopVerifierService.ts | 2 +- packages/openid4vc/tests/utils.ts | 30 +- 68 files changed, 2342 insertions(+), 2274 deletions(-) rename packages/anoncreds/src/anoncreds-rs/{AnonCreds2023DataIntegrityService.ts => AnonCredsDataIntegrityService.ts} (77%) create mode 100644 packages/anoncreds/src/models/W3cAnonCredsMetadata.ts rename packages/{core/src/modules/vc/repository => anoncreds/src/utils}/__tests__/anonCredsCredentialValue.test.ts (98%) create mode 100644 packages/anoncreds/src/utils/anonCredsObjects.ts delete mode 100644 packages/anoncreds/src/utils/ledgerObjects.ts create mode 100644 packages/anoncreds/src/utils/w3cAnonCredsUtils.ts delete mode 100644 packages/anoncreds/src/utils/w3cUtils.ts delete mode 100644 packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityMetadata.ts create mode 100644 packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts rename packages/core/src/modules/{credentials/formats/dataIntegrity/AnonCredsDataIntegrityService.ts => vc/data-integrity/models/IAnonCredsDataIntegrityService.ts} (59%) create mode 100644 packages/core/src/modules/vc/data-integrity/models/ProofTransformer.ts delete mode 100644 packages/core/src/modules/vc/repository/W3CAnoncredsCredentialMetadata.ts delete mode 100644 packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 7d7cce94ae..af8fc72520 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -26,6 +26,7 @@ "dependencies": { "@astronautlabs/jsonpath": "^1.1.2", "@credo-ts/core": "0.4.2", + "big-integer": "^1.6.51", "bn.js": "^5.2.1", "class-transformer": "0.5.1", "class-validator": "0.14.1", diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index fcd957bbbc..daa5b927e0 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -1,16 +1,12 @@ import type { AnonCredsModuleConfigOptions } from './AnonCredsModuleConfig' +import type { DependencyManager, Module, Update } from '@credo-ts/core' -import { - anoncreds2023DataIntegrityServiceSymbol, - type DependencyManager, - type Module, - type Update, -} from '@credo-ts/core' +import { AnonCredsDataIntegrityServiceSymbol } from '@credo-ts/core' import { AnonCredsApi } from './AnonCredsApi' import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from './anoncreds-rs' -import { AnonCreds2023DataIntegrityServiceImpl } from './anoncreds-rs/AnonCreds2023DataIntegrityService' +import { AnonCredsDataIntegrityService } from './anoncreds-rs/AnonCredsDataIntegrityService' import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, @@ -23,7 +19,7 @@ import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepositor import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, AnonCredsVerifierServiceSymbol } from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' import { updateAnonCredsModuleV0_3_1ToV0_4 } from './updates/0.3.1-0.4' -import { updateAnonCredsModuleV0_4_1ToV0_5 } from './updates/0.4-0.5' +import { updateAnonCredsModuleV0_4ToV0_5 } from './updates/0.4-0.5' /** * @public @@ -56,7 +52,7 @@ export class AnonCredsModule implements Module { dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, AnonCredsRsIssuerService) dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, AnonCredsRsVerifierService) - dependencyManager.registerSingleton(anoncreds2023DataIntegrityServiceSymbol, AnonCreds2023DataIntegrityServiceImpl) + dependencyManager.registerSingleton(AnonCredsDataIntegrityServiceSymbol, AnonCredsDataIntegrityService) } public updates = [ @@ -66,9 +62,9 @@ export class AnonCredsModule implements Module { doUpdate: updateAnonCredsModuleV0_3_1ToV0_4, }, { - fromVersion: '0.4.1', + fromVersion: '0.4', toVersion: '0.5', - doUpdate: updateAnonCredsModuleV0_4_1ToV0_5, + doUpdate: updateAnonCredsModuleV0_4ToV0_5, }, ] satisfies Update[] } diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts index dfe7eb0e1c..c0be4691a5 100644 --- a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -1,12 +1,13 @@ import type { AnonCredsRegistry } from '../services' +import type { DependencyManager } from '@credo-ts/core' -import { anoncreds2023DataIntegrityServiceSymbol, type DependencyManager } from '@credo-ts/core' +import { AnonCredsDataIntegrityServiceSymbol } from '@credo-ts/core' import { anoncreds } from '../../tests/helpers' import { AnonCredsModule } from '../AnonCredsModule' import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../anoncreds-rs' -import { AnonCreds2023DataIntegrityServiceImpl } from '../anoncreds-rs/AnonCreds2023DataIntegrityService' +import { AnonCredsDataIntegrityService } from '../anoncreds-rs/AnonCredsDataIntegrityService' import { AnonCredsSchemaRepository, AnonCredsCredentialDefinitionRepository, @@ -60,8 +61,8 @@ describe('AnonCredsModule', () => { ) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( - anoncreds2023DataIntegrityServiceSymbol, - AnonCreds2023DataIntegrityServiceImpl + AnonCredsDataIntegrityServiceSymbol, + AnonCredsDataIntegrityService ) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts similarity index 77% rename from packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts rename to packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts index 37778aa074..eaa1e5246e 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCreds2023DataIntegrityService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts @@ -7,21 +7,22 @@ import type { } from '../models' import type { AgentContext, - Anoncreds2023DataIntegrityService, - Anoncreds2023VerificationOptions, + IAnoncredsDataIntegrityService, + AnoncredsDataIntegrityVerifyPresentation, + DifPresentationExchangeDefinition, + DifPresentationExchangeSubmission, JsonObject, W3cCredentialRecord, + W3cJsonLdVerifiableCredential, } from '@credo-ts/core' -import type { CredentialProve, NonRevokedIntervalOverride, W3cCredentialEntry } from '@hyperledger/anoncreds-shared' import type { - Descriptor, - FieldV2, - InputDescriptorV1, - InputDescriptorV2, - PresentationDefinitionV1, - PresentationDefinitionV2, - PresentationSubmission, -} from '@sphereon/pex-models' + CreateW3cPresentationOptions, + CredentialProve, + NonRevokedIntervalOverride, + VerifyW3cPresentationOptions, + W3cCredentialEntry, +} from '@hyperledger/anoncreds-shared' +import type { Descriptor, FieldV2, InputDescriptorV1, InputDescriptorV2 } from '@sphereon/pex-models' import { JSONPath } from '@astronautlabs/jsonpath' import { @@ -29,9 +30,10 @@ import { Hasher, JsonTransformer, TypedArrayEncoder, - W3cJsonLdVerifiableCredential, + AnoncredsDataIntegrityCryptosuite, deepEquality, injectable, + ClaimFormat, } from '@credo-ts/core' import { W3cCredential as AnonCredsW3cCredential, @@ -43,7 +45,6 @@ import { import BigNumber from 'bn.js' import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' -import { AnonCredsLinkSecretRepository } from '../repository' import { assertBestPracticeRevocationInterval, fetchCredentialDefinition, @@ -51,6 +52,10 @@ import { fetchRevocationStatusList, fetchSchema, } from '../utils' +import { getAnonCredsTagsFromRecord } from '../utils/w3cAnonCredsUtils' + +import { AnonCredsRsHolderService } from './AnonCredsRsHolderService' +import { AnonCredsRsVerifierService } from './AnonCredsRsVerifierService' export interface CredentialWithMetadata { credential: JsonObject @@ -68,21 +73,22 @@ export interface RevocationRegistryFetchMetadata { export type PathComponent = string | number @injectable() -export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataIntegrityService { +export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegrityService { private getDataIntegrityProof(credential: W3cJsonLdVerifiableCredential) { - const cryptosuite = 'anoncreds-2023' + const cryptosuite = AnoncredsDataIntegrityCryptosuite if (Array.isArray(credential.proof)) { const proof = credential.proof.find( - (proof) => proof.type === 'DataIntegrityProof' && proof.cryptosuite === cryptosuite + (proof) => proof.type === 'DataIntegrityProof' && 'cryptosuite' in proof && proof.cryptosuite === cryptosuite ) - if (!proof) throw new CredoError('Could not find anoncreds-2023 proof') + if (!proof) throw new CredoError(`Could not find ${AnoncredsDataIntegrityCryptosuite} proof`) return proof } - if (credential.proof.type !== 'DataIntegrityProof' || credential.proof.cryptosuite !== cryptosuite) { - throw new CredoError( - `Unsupported proof type cryptosuite '${credential.proof.cryptosuite}', expected anoncreds-2023.` - ) + if ( + credential.proof.type !== 'DataIntegrityProof' || + !('cryptosuite' in credential.proof && credential.proof.cryptosuite === cryptosuite) + ) { + throw new CredoError(`Could not find ${AnoncredsDataIntegrityCryptosuite} proof`) } return credential.proof @@ -140,14 +146,14 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI // Make sure the revocation interval follows best practices from Aries RFC 0441 assertBestPracticeRevocationInterval(nonRevokedInterval) - const { qualifiedRevocationRegistryDefinition } = await fetchRevocationRegistryDefinition( + const revocaitonRegistryDefinitionResult = await fetchRevocationRegistryDefinition( agentContext, revocationRegistryId ) const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService const { tailsFilePath } = await tailsFileService.getTailsFile(agentContext, { - revocationRegistryDefinition: qualifiedRevocationRegistryDefinition, + revocationRegistryDefinition: revocaitonRegistryDefinitionResult.revocationRegistryDefinition, }) const timestampToFetch = timestamp ?? nonRevokedInterval.to @@ -161,7 +167,7 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI const updatedTimestamp = timestamp ?? _revocationStatusList.timestamp const revocationRegistryDefinition = RevocationRegistryDefinition.fromJson( - qualifiedRevocationRegistryDefinition as unknown as JsonObject + revocaitonRegistryDefinitionResult.revocationRegistryDefinition as unknown as JsonObject ) const revocationStatusList = RevocationStatusList.fromJson(_revocationStatusList as unknown as JsonObject) const revocationState = revocationRegistryIndex @@ -206,68 +212,46 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI } private async getSchemas(agentContext: AgentContext, schemaIds: Set) { - const schemaFetchPromises = [...schemaIds].map((schemaId) => fetchSchema(agentContext, schemaId)) - - const schemas: Record = {} - const schemaFetchResults = await Promise.all(schemaFetchPromises) - for (const schemaFetchResult of schemaFetchResults) { - const schemaId = schemaFetchResult.id - const schema = schemaFetchResult.schema - schemas[schemaId] = schema - } + const schemaFetchPromises = [...schemaIds].map(async (schemaId): Promise<[string, AnonCredsSchema]> => { + const { schema } = await fetchSchema(agentContext, schemaId) + return [schemaId, schema] + }) + const schemas = Object.fromEntries(await Promise.all(schemaFetchPromises)) return schemas } private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { - const credentialDefinitionFetchPromises = [...credentialDefinitionIds].map((credentialDefinitionId) => - fetchCredentialDefinition(agentContext, credentialDefinitionId) + const credentialDefinitionEntries = [...credentialDefinitionIds].map( + async (credentialDefinitionId): Promise<[string, AnonCredsCredentialDefinition]> => { + const { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + return [credentialDefinitionId, credentialDefinition] + } ) - const credentialDefinitions: Record = {} - - const credentialDefinitionFetchResults = await Promise.all(credentialDefinitionFetchPromises) - for (const credentialDefinitionFetchResult of credentialDefinitionFetchResults) { - const credentialDefinitionId = credentialDefinitionFetchResult.id - const credentialDefinition = credentialDefinitionFetchResult.credentialDefinition - credentialDefinitions[credentialDefinitionId] = credentialDefinition - } - + const credentialDefinitions = Object.fromEntries(await Promise.all(credentialDefinitionEntries)) return credentialDefinitions } private async getLinkSecret(agentContext: AgentContext, credentialRecord: W3cCredentialRecord[]) { - const linkSecrets = new Set( - credentialRecord - .map((record) => record.getAnonCredsTags()?.linkSecretId) - .filter((linkSecretId): linkSecretId is string => linkSecretId !== undefined) - ) - const linkSecretIdArray = [...linkSecrets] - - if (linkSecretIdArray.length > 1) { - throw new CredoError('Multiple linksecret cannot be used to create a single presentation') - } else if (linkSecretIdArray.length === 0) { - throw new CredoError('Cannot create a presentation without a linksecret') - } - - const linkSecretRecord = await agentContext.dependencyManager - .resolve(AnonCredsLinkSecretRepository) - .getByLinkSecretId(agentContext, linkSecretIdArray[0]) + const linkSecrets = credentialRecord + .map((record) => getAnonCredsTagsFromRecord(record)?.anonCredsLinkSecretId) + .filter((linkSecretId): linkSecretId is string => linkSecretId !== undefined) - if (!linkSecretRecord.value) throw new CredoError('Link Secret value not stored') - return linkSecretRecord.value + const anoncredsHolderService = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) + return anoncredsHolderService.getLinkSecret(agentContext, linkSecrets) } private getPresentationMetadata = async ( agentContext: AgentContext, - input: { + options: { credentialsWithMetadata: CredentialWithMetadata[] credentialsProve: CredentialProve[] schemaIds: Set credentialDefinitionIds: Set } ) => { - const { credentialDefinitionIds, schemaIds, credentialsWithMetadata, credentialsProve } = input + const { credentialDefinitionIds, schemaIds, credentialsWithMetadata, credentialsProve } = options const credentials: W3cCredentialEntry[] = await Promise.all( credentialsWithMetadata.map(async ({ credential, nonRevoked }) => { @@ -376,18 +360,18 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI } public createAnonCredsProofRequestAndMetadata = async ( - agentContext: AgentContext, - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, - presentationSubmission: PresentationSubmission, - credentials: JsonObject[] + presentationDefinition: DifPresentationExchangeDefinition, + presentationSubmission: DifPresentationExchangeSubmission, + credentials: JsonObject[], + challenge: string ) => { const credentialsProve: CredentialProve[] = [] const schemaIds = new Set() const credentialDefinitionIds = new Set() const credentialsWithMetadata: CredentialWithMetadata[] = [] - const hash = Hasher.hash(TypedArrayEncoder.fromString(presentationDefinition.id), 'sha-256') - const nonce = new BigNumber(hash).toString().slice(0, 32) + const hash = Hasher.hash(TypedArrayEncoder.fromString(challenge), 'sha-256') + const nonce = new BigNumber(hash).toString().slice(0, 20) const anonCredsProofRequest: AnonCredsProofRequest = { version: '1.0', @@ -455,16 +439,12 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI } else { if (!anonCredsProofRequest.requested_attributes[attributeReferent]) { anonCredsProofRequest.requested_attributes[attributeReferent] = { - name: propertyName, names: [propertyName], restrictions: [{ cred_def_id: credentialDefinitionId }], non_revoked: requiresRevocationStatus ? nonRevokedInterval : undefined, } } else { - const name = anonCredsProofRequest.requested_attributes[attributeReferent].name - const names = anonCredsProofRequest.requested_attributes[attributeReferent].names ?? [name ?? 'name'] - - anonCredsProofRequest.requested_attributes[attributeReferent].name = undefined + const names = anonCredsProofRequest.requested_attributes[attributeReferent].names ?? [] anonCredsProofRequest.requested_attributes[attributeReferent].names = [...names, propertyName] } @@ -479,48 +459,54 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI public async createPresentation( agentContext: AgentContext, options: { - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 - presentationSubmission: PresentationSubmission + presentationDefinition: DifPresentationExchangeDefinition + presentationSubmission: DifPresentationExchangeSubmission selectedCredentials: JsonObject[] selectedCredentialRecords: W3cCredentialRecord[] + challenge: string } ) { - const { presentationDefinition, presentationSubmission, selectedCredentialRecords, selectedCredentials } = options + const { + presentationDefinition, + presentationSubmission, + selectedCredentialRecords, + selectedCredentials, + challenge, + } = options const linkSecret = await this.getLinkSecret(agentContext, selectedCredentialRecords) const { anonCredsProofRequest, ...metadata } = await this.createAnonCredsProofRequestAndMetadata( - agentContext, presentationDefinition, presentationSubmission, - selectedCredentials + selectedCredentials, + challenge ) const presentationMetadata = await this.getPresentationMetadata(agentContext, metadata) const { schemas, credentialDefinitions, credentialsProve, credentials } = presentationMetadata - let presentation: AnonCredsW3cPresentation | undefined - try { - presentation = AnonCredsW3cPresentation.create({ - credentials, - schemas: schemas as unknown as Record, - credentialDefinitions: credentialDefinitions as unknown as Record, - linkSecret, - credentialsProve, - presentationRequest: anonCredsProofRequest as unknown as JsonObject, - }) - const presentationJson = presentation.toJson() as unknown as JsonObject - return presentationJson - } finally { - presentation?.handle.clear() + const anonCredsRsHolderService = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) + + const createPresentationOptions: CreateW3cPresentationOptions = { + credentials, + schemas: schemas as unknown as Record, + credentialDefinitions: credentialDefinitions as unknown as Record, + linkSecret, + credentialsProve, + presentationRequest: anonCredsProofRequest as unknown as JsonObject, } + + const w3cPresentation = await anonCredsRsHolderService.createW3cPresentation(createPresentationOptions) + return w3cPresentation as JsonObject } - public async verifyPresentation(agentContext: AgentContext, options: Anoncreds2023VerificationOptions) { - const { presentation, presentationDefinition, presentationSubmission } = options + public async verifyPresentation(agentContext: AgentContext, options: AnoncredsDataIntegrityVerifyPresentation) { + const { presentation, presentationDefinition, presentationSubmission, challenge } = options let anonCredsW3cPresentation: AnonCredsW3cPresentation | undefined + let result = false const credentialDefinitionIds = new Set() @@ -530,7 +516,7 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI : [presentation.verifiableCredential] for (const verifiableCredential of verifiableCredentials) { - if (verifiableCredential instanceof W3cJsonLdVerifiableCredential) { + if (verifiableCredential.claimFormat === ClaimFormat.LdpVc) { const proof = this.getDataIntegrityProof(verifiableCredential) credentialDefinitionIds.add(proof.verificationMethod) } else { @@ -540,10 +526,10 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI const verifiableCredentialsJson = verifiableCredentials.map((credential) => JsonTransformer.toJSON(credential)) const { anonCredsProofRequest, ...metadata } = await this.createAnonCredsProofRequestAndMetadata( - agentContext, presentationDefinition, presentationSubmission, - verifiableCredentialsJson + verifiableCredentialsJson, + challenge ) const revocationMetadata = await this.getRevocationMetadataForCredentials( agentContext, @@ -561,7 +547,8 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI revocationMetadata.forEach( (rm) => (revocationRegistryDefinitions[rm.revocationRegistryId] = rm.revocationRegistryDefinition) ) - result = anonCredsW3cPresentation.verify({ + + const verificationOptions: VerifyW3cPresentationOptions = { presentationRequest: anonCredsProofRequest as unknown as JsonObject, schemas: schemas as unknown as Record, credentialDefinitions: credentialDefinitions as unknown as Record, @@ -570,7 +557,9 @@ export class AnonCreds2023DataIntegrityServiceImpl implements Anoncreds2023DataI nonRevokedIntervalOverrides: revocationMetadata .filter((rm) => rm.nonRevokedIntervalOverride) .map((rm) => rm.nonRevokedIntervalOverride as NonRevokedIntervalOverride), - }) + } + const anonCredsRsVerifierService = agentContext.dependencyManager.resolve(AnonCredsRsVerifierService) + result = anonCredsRsVerifierService.verifyW3cPresentation(presentation, verificationOptions) } finally { anonCredsW3cPresentation?.handle.clear() } diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts index a8f3128e11..336892eee3 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts @@ -1,3 +1,16 @@ +import type { + AnonCredsCredentialDefinition, + AnonCredsProof, + AnonCredsRequestedAttributeMatch, + AnonCredsRequestedPredicateMatch, + AnonCredsRevocationRegistryDefinition, + AnonCredsSchema, + AnonCredsCredentialRequest, + AnonCredsCredential, + AnonCredsCredentialInfo, + AnonCredsProofRequestRestriction, +} from '../models' +import type { AnonCredsCredentialRecord } from '../repository' import type { GetCredentialsForProofRequestOptions, GetCredentialsForProofRequestReturn, @@ -11,8 +24,10 @@ import type { GetCredentialsOptions, StoreCredentialOptions, } from '../services' -import type { AgentContext, AnonCredsClaimRecord, Query, SimpleQuery } from '@credo-ts/core' +import type { AnonCredsCredentialRequestMetadata, W3cAnoncredsCredentialMetadata } from '../utils/metadata' +import type { AgentContext, Query, SimpleQuery } from '@credo-ts/core' import type { + CreateW3cPresentationOptions, CredentialEntry, CredentialProve, CredentialRequestMetadata, @@ -28,10 +43,11 @@ import { W3cCredentialService, W3cJsonLdVerifiableCredential, injectable, - isDid, utils, } from '@credo-ts/core' import { + Credential, + W3cPresentation, W3cCredential as AW3cCredential, CredentialRequest, CredentialRevocationState, @@ -40,39 +56,22 @@ import { RevocationRegistryDefinition, RevocationStatusList, anoncreds, + W3cCredential, } from '@hyperledger/anoncreds-shared' import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' import { AnonCredsRsError } from '../error' -import { - type AnonCredsCredentialDefinition, - type AnonCredsProof, - type AnonCredsRequestedAttributeMatch, - type AnonCredsRequestedPredicateMatch, - type AnonCredsRevocationRegistryDefinition, - type AnonCredsSchema, - type AnonCredsCredentialRequest, - type AnonCredsCredentialRequestMetadata, - type AnonCredsCredential, - type AnonCredsCredentialInfo, - type AnonCredsProofRequestRestriction, - AnonCredsRestrictionWrapper, -} from '../models' -import { AnonCredsCredentialRecord, AnonCredsCredentialRepository, AnonCredsLinkSecretRepository } from '../repository' +import { AnonCredsRestrictionWrapper } from '../models' +import { AnonCredsCredentialRepository, AnonCredsLinkSecretRepository } from '../repository' import { AnonCredsRegistryService } from '../services' +import { storeLinkSecret, unqualifiedCredentialDefinitionIdRegex } from '../utils' import { - fetchCredentialDefinition, - legacyCredentialToW3cCredential, - storeLinkSecret, - unqualifiedCredentialDefinitionIdRegex, - w3cToLegacyCredential, - getQualifiedCredentialDefinition, - getIndyNamespace, - getQualifiedRevocationRegistryDefinition, - getQualifiedSchema, - isIndyDid, - getNonQualifiedId, -} from '../utils' + isUnqualifiedCredentialDefinitionId, + isUnqualifiedIndyDid, + isUnqualifiedSchemaId, +} from '../utils/indyIdentifiers' +import { W3cAnonCredsCredentialMetadataKey } from '../utils/metadata' +import { getAnoncredsCredentialInfoFromRecord, getW3cRecordAnonCredsTags } from '../utils/w3cAnonCredsUtils' @injectable() export class AnonCredsRsHolderService implements AnonCredsHolderService { @@ -86,6 +85,24 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } + public async getLinkSecret(agentContext: AgentContext, linkSecretIds: string[]): Promise { + // Get all requested credentials and take linkSecret. If it's not the same for every credential, throw error + const linkSecretsMatch = linkSecretIds.every((linkSecretId) => linkSecretId === linkSecretIds[0]) + if (!linkSecretsMatch) { + throw new AnonCredsRsError('All credentials in a Proof should have been issued using the same Link Secret') + } + + const linkSecretRecord = await agentContext.dependencyManager + .resolve(AnonCredsLinkSecretRepository) + .getByLinkSecretId(agentContext, linkSecretIds[0]) + + if (!linkSecretRecord.value) { + throw new AnonCredsRsError('Link Secret value not stored') + } + + return linkSecretRecord.value + } + public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise { const { credentialDefinitions, proofRequest, selectedCredentials, schemas } = options @@ -101,8 +118,8 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { rsSchemas[schemaId] = schemas[schemaId] as unknown as JsonObject } - const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const legacyCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const anoncredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) // Cache retrieved credentials in order to minimize storage calls const retrievedCredentials = new Map() @@ -111,51 +128,32 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { attribute: AnonCredsRequestedAttributeMatch | AnonCredsRequestedPredicateMatch ): Promise<{ linkSecretId: string; credentialEntry: CredentialEntry }> => { let credentialRecord = retrievedCredentials.get(attribute.credentialId) + if (!credentialRecord) { - try { - credentialRecord = await credentialRepository.getByCredentialId(agentContext, attribute.credentialId) - retrievedCredentials.set(attribute.credentialId, credentialRecord) - } catch { - // do nothing - } + const w3cCredentialRecord = await w3cCredentialRepository.findSingleByQuery(agentContext, { + anonCredsCredentialId: attribute.credentialId, + }) - if (!credentialRecord) { - credentialRecord = await legacyCredentialRepository.getByCredentialId(agentContext, attribute.credentialId) + if (w3cCredentialRecord) { + credentialRecord = w3cCredentialRecord + retrievedCredentials.set(attribute.credentialId, w3cCredentialRecord) + } else { + credentialRecord = await anoncredsCredentialRepository.getByCredentialId( + agentContext, + attribute.credentialId + ) agentContext.config.logger.warn( [ - `Creating proof with legacy credential ${attribute.credentialId}.`, - `Please run the migration script to migrate credentials to the new w3c format.`, + `Creating AnonCreds proof with legacy credential ${attribute.credentialId}.`, + `Please run the migration script to migrate credentials to the new w3c format. See https://credo.js.org/guides/updating/versions/0.4-to-0.5 for information on how to migrate.`, ].join('\n') ) } } - let revocationRegistryId: string | null - let credentialRevocationId: string | null - let linkSecretId: string - - if (credentialRecord instanceof W3cCredentialRecord) { - if (!credentialRecord.anonCredsCredentialMetadata) { - throw new CredoError('AnonCreds metadata not found on credential record.') - } - - if (credentialRecord.credential instanceof W3cJsonLdVerifiableCredential === false) { - throw new CredoError('Credential must be a W3cJsonLdVerifiableCredential.') - } - - linkSecretId = credentialRecord.anonCredsCredentialMetadata.linkSecretId - const metadata = this.anoncredsMetadataFromRecord(credentialRecord) - revocationRegistryId = metadata.revocationRegistryId - credentialRevocationId = metadata.credentialRevocationId - } else if (credentialRecord instanceof AnonCredsCredentialRecord) { - const metadata = this.anoncredsMetadataFromLegacyRecord(credentialRecord) - linkSecretId = credentialRecord.linkSecretId - revocationRegistryId = metadata.revocationRegistryId - credentialRevocationId = metadata.credentialRevocationId - } else { - throw new CredoError('Credential record must be either a W3cCredentialRecord or AnonCredsCredentialRecord.') - } + const { linkSecretId, revocationRegistryId, credentialRevocationId } = + getAnoncredsCredentialInfoFromRecord(credentialRecord) // TODO: Check if credential has a revocation registry id (check response from anoncreds-rs API, as it is // sending back a mandatory string in Credential.revocationRegistryId) @@ -191,7 +189,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const credential = credentialRecord instanceof W3cCredentialRecord - ? w3cToLegacyCredential(credentialRecord.credential as W3cJsonLdVerifiableCredential) + ? this.w3cToLegacyCredential(agentContext, credentialRecord.credential as W3cJsonLdVerifiableCredential) : (credentialRecord.credential as AnonCredsCredential) return { @@ -247,7 +245,10 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { credentials: credentials.map((entry) => entry.credentialEntry), credentialsProve, selfAttest: selectedCredentials.selfAttestedAttributes, - linkSecret: linkSecretRecord.value, + linkSecret: await this.getLinkSecret( + agentContext, + credentials.map((entry) => entry.linkSecretId) + ), }) return presentation.toJson() as unknown as AnonCredsProof @@ -314,39 +315,72 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } - public async legacyToW3cCredential( + public w3cToLegacyCredential(agentContext: AgentContext, credential: W3cJsonLdVerifiableCredential) { + const credentialJson = JsonTransformer.toJSON(credential) + const w3cCredentialObj = W3cCredential.fromJson(credentialJson) + const legacyCredential = w3cCredentialObj.toLegacy().toJson() as unknown as AnonCredsCredential + return legacyCredential + } + + public async processW3cCredential( agentContext: AgentContext, - options: { - credential: AnonCredsCredential + credential: W3cCredential, + process: { credentialDefinition: AnonCredsCredentialDefinition credentialRequestMetadata: AnonCredsCredentialRequestMetadata revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | undefined } ) { - const { credential, credentialRequestMetadata, revocationRegistryDefinition, credentialDefinition } = options + const { credentialRequestMetadata, revocationRegistryDefinition, credentialDefinition } = process - const linkSecretRecord = await agentContext.dependencyManager - .resolve(AnonCredsLinkSecretRepository) - .getByLinkSecretId(agentContext, credentialRequestMetadata.link_secret_name) - - if (!linkSecretRecord.value) { - throw new AnonCredsRsError('Link Secret value not stored') - } - - const w3cJsonLdCredential = await legacyCredentialToW3cCredential(credential, credentialDefinition.issuerId, { + const processCredentialOptions = { credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject, - linkSecret: linkSecretRecord.value, + linkSecret: await this.getLinkSecret(agentContext, [credentialRequestMetadata.link_secret_name]), revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, credentialDefinition: credentialDefinition as unknown as JsonObject, - }) + } - return w3cJsonLdCredential + const processedW3cCredential = credential.process(processCredentialOptions) + return processedW3cCredential + } + + public async legacyToW3cCredential( + agentContext: AgentContext, + credential: AnonCredsCredential, + issuerId: string, + options?: { + credentialDefinition: AnonCredsCredentialDefinition + credentialRequestMetadata: AnonCredsCredentialRequestMetadata + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | undefined + } + ) { + let w3cJsonLdVerifiableCredential: W3cJsonLdVerifiableCredential + let anonCredsCredential: Credential | undefined + let w3cCredentialObj: W3cCredential | undefined + + try { + anonCredsCredential = Credential.fromJson(credential as unknown as JsonObject) + w3cCredentialObj = anonCredsCredential.toW3c({ + issuerId: issuerId, + w3cVersion: '1.1', + }) + + const jsonObject = options + ? (await this.processW3cCredential(agentContext, w3cCredentialObj, options)).toJson() + : w3cCredentialObj.toJson() + + w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(jsonObject, W3cJsonLdVerifiableCredential) + } finally { + anonCredsCredential?.handle?.clear() + w3cCredentialObj?.handle?.clear() + } + + return w3cJsonLdVerifiableCredential } public async storeW3cCredential( agentContext: AgentContext, options: { - credentialId?: string credential: W3cJsonLdVerifiableCredential credentialDefinitionId: string schema: AnonCredsSchema @@ -354,62 +388,52 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition credentialRequestMetadata: AnonCredsCredentialRequestMetadata } - ): Promise { + ) { const { credential, credentialRequestMetadata, schema, credentialDefinition, credentialDefinitionId } = options const methodName = agentContext.dependencyManager .resolve(AnonCredsRegistryService) .getRegistryForIdentifier(agentContext, credential.issuerId).methodName - const linkSecretRecord = await agentContext.dependencyManager - .resolve(AnonCredsLinkSecretRepository) - .getByLinkSecretId(agentContext, credentialRequestMetadata.link_secret_name) - - if (!linkSecretRecord.value) { - throw new AnonCredsRsError('Link Secret value not stored') - } + // this thows an error if the link secret is not found + await this.getLinkSecret(agentContext, [credentialRequestMetadata.link_secret_name]) const { revocationRegistryId, revocationRegistryIndex } = AW3cCredential.fromJson( JsonTransformer.toJSON(credential) ) - const credentialId = options.credentialId ?? utils.uuid() - - const indyDid = isIndyDid(credential.issuerId) - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - await w3cCredentialService.storeCredential(agentContext, { - credential, - anonCredsCredentialRecordOptions: { - credentialId, - linkSecretId: linkSecretRecord.linkSecretId, - credentialDefinitionId, - schemaId: credentialDefinition.schemaId, - schemaName: schema.name, - schemaIssuerId: schema.issuerId, - schemaVersion: schema.version, - methodName, - revocationRegistryId, - credentialRevocationId: revocationRegistryIndex?.toString(), - unqualifiedTags: indyDid - ? { - issuerId: getNonQualifiedId(credential.issuerId), - credentialDefinitionId: getNonQualifiedId(credentialDefinitionId), - schemaId: getNonQualifiedId(credentialDefinition.schemaId), - schemaIssuerId: getNonQualifiedId(schema.issuerId), - revocationRegistryId: revocationRegistryId ? getNonQualifiedId(revocationRegistryId) : undefined, - } - : undefined, - }, + const w3cCredentialRecord = await w3cCredentialService.storeCredential(agentContext, { credential }) + + const anonCredsTags = getW3cRecordAnonCredsTags({ + w3cCredentialRecord, + schema, + schemaId: credentialDefinition.schemaId, + credentialDefinitionId, + revocationRegistryId, + credentialRevocationId: revocationRegistryIndex?.toString(), + linkSecretId: credentialRequestMetadata.link_secret_name, + methodName, }) - return credentialId + const anonCredsCredentialMetadata: W3cAnoncredsCredentialMetadata = { + credentialId: w3cCredentialRecord.id, + credentialRevocationId: anonCredsTags.anonCredsCredentialRevocationId, + linkSecretId: anonCredsTags.anonCredsLinkSecretId, + methodName: anonCredsTags.anonCredsMethodName, + } + + w3cCredentialRecord.setTags(anonCredsTags) + w3cCredentialRecord.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + await w3cCredentialRepository.update(agentContext, w3cCredentialRecord) + + return w3cCredentialRecord } - // convert legacy to w3c and call store w3c public async storeCredential(agentContext: AgentContext, options: StoreCredentialOptions): Promise { const { - credentialId, credential, credentialDefinition, credentialDefinitionId, @@ -418,56 +442,36 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { revocationRegistry, } = options - const qualifiedCredentialDefinitionId = isDid(credentialDefinitionId) - ? credentialDefinitionId - : (await fetchCredentialDefinition(agentContext, credentialDefinitionId)).qualifiedId - - const qualifiedSchema = getQualifiedSchema(schema, getIndyNamespace(qualifiedCredentialDefinitionId)) - - const qualifiedCredentialDefinition = getQualifiedCredentialDefinition( - credentialDefinition, - getIndyNamespace(qualifiedCredentialDefinitionId) - ) - - const qualifiedRevocationRegistryDefinition = !revocationRegistry?.definition - ? undefined - : getQualifiedRevocationRegistryDefinition( - revocationRegistry.definition, - getIndyNamespace(qualifiedCredentialDefinitionId) - ) - const w3cJsonLdCredential = credential instanceof W3cJsonLdVerifiableCredential ? credential - : await this.legacyToW3cCredential(agentContext, { - credential, + : await this.legacyToW3cCredential(agentContext, credential, credentialDefinition.issuerId, { credentialRequestMetadata, - credentialDefinition: qualifiedCredentialDefinition, - revocationRegistryDefinition: qualifiedRevocationRegistryDefinition, + credentialDefinition, + revocationRegistryDefinition: revocationRegistry?.definition, }) - return await this.storeW3cCredential(agentContext, { - credentialId, + const w3cCredentialRecord = await this.storeW3cCredential(agentContext, { credentialRequestMetadata, credential: w3cJsonLdCredential, - credentialDefinitionId: qualifiedCredentialDefinitionId, - schema: qualifiedSchema, - credentialDefinition: qualifiedCredentialDefinition, - revocationRegistryDefinition: qualifiedRevocationRegistryDefinition, + credentialDefinitionId, + schema, + credentialDefinition, + revocationRegistryDefinition: revocationRegistry?.definition, }) + + return w3cCredentialRecord.id } public async getCredential( agentContext: AgentContext, options: GetCredentialOptions ): Promise { - try { - const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const credentialRecord = await credentialRepository.getByCredentialId(agentContext, options.credentialId) - if (credentialRecord) return this.anoncredsMetadataFromRecord(credentialRecord) - } catch { - // do nothing - } + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const w3cCredentialRecord = await w3cCredentialRepository.findSingleByQuery(agentContext, { + anonCredsCredentialId: options.credentialId, + }) + if (w3cCredentialRecord) return getAnoncredsCredentialInfoFromRecord(w3cCredentialRecord) const anonCredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) const anonCredsCredentialRecord = await anonCredsCredentialRepository.getByCredentialId( @@ -482,48 +486,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { ].join('\n') ) - return this.anoncredsMetadataFromLegacyRecord(anonCredsCredentialRecord) - } - - private anoncredsMetadataFromLegacyRecord( - anonCredsCredentialRecord: AnonCredsCredentialRecord - ): AnonCredsCredentialInfo { - const attributes: { [key: string]: string } = {} - for (const attribute in anonCredsCredentialRecord.credential) { - attributes[attribute] = anonCredsCredentialRecord.credential.values[attribute].raw - } - - return { - attributes, - credentialDefinitionId: anonCredsCredentialRecord.credential.cred_def_id, - credentialId: anonCredsCredentialRecord.credentialId, - schemaId: anonCredsCredentialRecord.credential.schema_id, - credentialRevocationId: anonCredsCredentialRecord.credentialRevocationId ?? null, - revocationRegistryId: anonCredsCredentialRecord.credential.rev_reg_id ?? null, - methodName: anonCredsCredentialRecord.methodName, - } - } - - private anoncredsMetadataFromRecord(w3cCredentialRecord: W3cCredentialRecord): AnonCredsCredentialInfo { - if (Array.isArray(w3cCredentialRecord.credential.credentialSubject)) { - throw new CredoError('Credential subject must be an object, not an array.') - } - - const anonCredsTags = w3cCredentialRecord.getAnonCredsTags() - if (!anonCredsTags) throw new CredoError('AnonCreds tags not found on credential record.') - - const anoncredsCredentialMetadata = w3cCredentialRecord.anonCredsCredentialMetadata - if (!anoncredsCredentialMetadata) throw new CredoError('AnonCreds metadata not found on credential record.') - - return { - attributes: (w3cCredentialRecord.credential.credentialSubject.claims as AnonCredsClaimRecord) ?? {}, - credentialId: anoncredsCredentialMetadata.credentialId, - credentialDefinitionId: anonCredsTags.credentialDefinitionId, - schemaId: anonCredsTags.schemaId, - credentialRevocationId: anoncredsCredentialMetadata.credentialRevocationId ?? null, - revocationRegistryId: anonCredsTags.revocationRegistryId ?? null, - methodName: anoncredsCredentialMetadata.methodName, - } + return getAnoncredsCredentialInfoFromRecord(anonCredsCredentialRecord) } private async getLegacyCredentials( @@ -542,64 +505,67 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { methodName: options.methodName, }) - return credentialRecords.map((credentialRecord) => this.anoncredsMetadataFromLegacyRecord(credentialRecord)) + return credentialRecords.map((credentialRecord) => getAnoncredsCredentialInfoFromRecord(credentialRecord)) } public async getCredentials( agentContext: AgentContext, options: GetCredentialsOptions ): Promise { - const getIfQualifiedId = (id: string | undefined) => { - return !id ? undefined : isDid(id) ? id : undefined - } - - const getIfUnqualifiedId = (id: string | undefined) => { - return !id ? undefined : isDid(id) ? undefined : id - } - const credentialRecords = await agentContext.dependencyManager .resolve(W3cCredentialRepository) .findByQuery(agentContext, { - credentialDefinitionId: getIfQualifiedId(options.credentialDefinitionId), - schemaId: getIfQualifiedId(options.schemaId), - issuerId: getIfQualifiedId(options.issuerId), - schemaName: options.schemaName, - schemaVersion: options.schemaVersion, - schemaIssuerId: getIfQualifiedId(options.schemaIssuerId), - methodName: options.methodName, - unqualifiedSchemaId: getIfUnqualifiedId(options.schemaId), - unqualifiedIssuerId: getIfUnqualifiedId(options.issuerId), - unqualifiedSchemaIssuerId: getIfUnqualifiedId(options.schemaIssuerId), - unqualifiedCredentialDefinitionId: getIfUnqualifiedId(options.credentialDefinitionId), + anonCredsCredentialDefinitionId: + !options.credentialDefinitionId || isUnqualifiedCredentialDefinitionId(options.credentialDefinitionId) + ? undefined + : options.credentialDefinitionId, + anonCredsSchemaId: !options.schemaId || isUnqualifiedSchemaId(options.schemaId) ? undefined : options.schemaId, + anonCredsIssuerId: !options.issuerId || isUnqualifiedIndyDid(options.issuerId) ? undefined : options.issuerId, + anonCredsSchemaName: options.schemaName, + anonCredsSchemaVersion: options.schemaVersion, + anonCredsSchemaIssuerId: + !options.schemaIssuerId || isUnqualifiedIndyDid(options.schemaIssuerId) ? undefined : options.schemaIssuerId, + + anonCredsMethodName: options.methodName, + anonCredsUnqualifiedSchemaId: + options.schemaId && isUnqualifiedSchemaId(options.schemaId) ? options.schemaId : undefined, + anonCredsUnqualifiedIssuerId: + options.issuerId && isUnqualifiedIndyDid(options.issuerId) ? options.issuerId : undefined, + anonCredsUnqualifiedSchemaIssuerId: + options.schemaIssuerId && isUnqualifiedIndyDid(options.schemaIssuerId) ? options.schemaIssuerId : undefined, + anonCredsUnqualifiedCredentialDefinitionId: + options.credentialDefinitionId && isUnqualifiedCredentialDefinitionId(options.credentialDefinitionId) + ? options.credentialDefinitionId + : undefined, }) - const credentials = credentialRecords.map((credentialRecord) => this.anoncredsMetadataFromRecord(credentialRecord)) + const credentials = credentialRecords.map((credentialRecord) => + getAnoncredsCredentialInfoFromRecord(credentialRecord) + ) const legacyCredentials = await this.getLegacyCredentials(agentContext, options) if (legacyCredentials.length > 0) { agentContext.config.logger.warn( - [ - `Queried credentials include legacy credentials.`, - `Please run the migration script to migrate credentials to the new w3c format.`, - ].join('\n') + `Queried credentials include legacy credentials. Please run the migration script to migrate credentials to the new w3c format.` ) } return [...legacyCredentials, ...credentials] } public async deleteCredential(agentContext: AgentContext, credentialId: string): Promise { - try { - const credentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const credentialRecord = await credentialRepository.getByCredentialId(agentContext, credentialId) - await credentialRepository.delete(agentContext, credentialRecord) + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const w3cCredentialRecord = await w3cCredentialRepository.findSingleByQuery(agentContext, { + anonCredsCredentialId: credentialId, + }) + + if (w3cCredentialRecord) { + await w3cCredentialRepository.delete(agentContext, w3cCredentialRecord) return - } catch { - // do nothing } - const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) - const credentialRecord = await credentialRepository.getByCredentialId(agentContext, credentialId) - await credentialRepository.delete(agentContext, credentialRecord) + const anoncredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) + const anoncredsCredentialRecord = await anoncredsCredentialRepository.getByCredentialId(agentContext, credentialId) + await anoncredsCredentialRepository.delete(agentContext, anoncredsCredentialRecord) } private async getLegacyCredentialsForProofRequest( agentContext: AgentContext, @@ -621,7 +587,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const attributes = requestedAttribute.names ?? [requestedAttribute.name] const attributeQuery: SimpleQuery = {} for (const attribute of attributes) { - attributeQuery[`attr::${attribute}::marker`] = true + attributeQuery[`anonCredsAttr::${attribute}::marker`] = true } $and.push(attributeQuery) @@ -646,7 +612,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { return credentials.map((credentialRecord) => { return { - credentialInfo: this.anoncredsMetadataFromLegacyRecord(credentialRecord), + credentialInfo: getAnoncredsCredentialInfoFromRecord(credentialRecord), interval: proofRequest.non_revoked, } }) @@ -672,7 +638,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const attributes = requestedAttribute.names ?? [requestedAttribute.name] const attributeQuery: SimpleQuery = {} for (const attribute of attributes) { - attributeQuery[`attr::${attribute}::marker`] = true + attributeQuery[`anonCredsAttr::${attribute}::marker`] = true } $and.push(attributeQuery) @@ -689,12 +655,8 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { $and.push(options.extraQuery) } - const credentials = await agentContext.dependencyManager - .resolve(W3cCredentialRepository) - .findByQuery(agentContext, { - $and, - }) - + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const credentials = await w3cCredentialRepository.findByQuery(agentContext, { $and }) const legacyCredentialWithMetadata = await this.getLegacyCredentialsForProofRequest(agentContext, options) if (legacyCredentialWithMetadata.length > 0) { @@ -708,7 +670,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const credentialWithMetadata = credentials.map((credentialRecord) => { return { - credentialInfo: this.anoncredsMetadataFromRecord(credentialRecord), + credentialInfo: getAnoncredsCredentialInfoFromRecord(credentialRecord), interval: proofRequest.non_revoked, } }) @@ -725,54 +687,54 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const queryElements: SimpleQuery = {} if (restriction.credentialDefinitionId) { - if (isDid(restriction.credentialDefinitionId)) { - queryElements.credentialDefinitionId = restriction.credentialDefinitionId + if (isUnqualifiedCredentialDefinitionId(restriction.credentialDefinitionId)) { + queryElements.anonCredsUnqualifiedCredentialDefinitionId = restriction.credentialDefinitionId } else { - queryElements.unqualifiedCredentialDefinitionId = restriction.credentialDefinitionId + queryElements.anonCredsCredentialDefinitionId = restriction.credentialDefinitionId } } if (restriction.issuerId || restriction.issuerDid) { const issuerId = (restriction.issuerId ?? restriction.issuerDid) as string - if (isDid(issuerId)) { - queryElements.issuerId = issuerId + if (isUnqualifiedIndyDid(issuerId)) { + queryElements.anonCredsUnqualifiedIssuerId = issuerId } else { - queryElements.unqualifiedIssuerId = issuerId + queryElements.anonCredsIssuerId = issuerId } } if (restriction.schemaId) { - if (isDid(restriction.schemaId)) { - queryElements.schemaId = restriction.schemaId + if (isUnqualifiedSchemaId(restriction.schemaId)) { + queryElements.anonCredsUnqualifiedSchemaId = restriction.schemaId } else { - queryElements.unqualifiedSchemaId = restriction.schemaId + queryElements.anonCredsSchemaId = restriction.schemaId } } if (restriction.schemaIssuerId || restriction.schemaIssuerDid) { - const issuerId = (restriction.schemaIssuerId ?? restriction.schemaIssuerDid) as string - if (isDid(issuerId)) { - queryElements.schemaIssuerId = issuerId + const schemaIssuerId = (restriction.schemaIssuerId ?? restriction.schemaIssuerDid) as string + if (isUnqualifiedIndyDid(schemaIssuerId)) { + queryElements.anonCredsUnqualifiedSchemaIssuerId = schemaIssuerId } else { - queryElements.unqualifiedSchemaIssuerId = issuerId + queryElements.anonCredsSchemaIssuerId = schemaIssuerId } } if (restriction.schemaName) { - queryElements.schemaName = restriction.schemaName + queryElements.anonCredsSchemaName = restriction.schemaName } if (restriction.schemaVersion) { - queryElements.schemaVersion = restriction.schemaVersion + queryElements.anonCredsSchemaVersion = restriction.schemaVersion } for (const [attributeName, attributeValue] of Object.entries(restriction.attributeValues)) { - queryElements[`attr::${attributeName}::value`] = attributeValue + queryElements[`anonCredsAttr::${attributeName}::value`] = attributeValue } for (const [attributeName, isAvailable] of Object.entries(restriction.attributeMarkers)) { if (isAvailable) { - queryElements[`attr::${attributeName}::marker`] = isAvailable + queryElements[`anonCredsAttr::${attributeName}::marker`] = isAvailable } } @@ -789,21 +751,36 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { for (const restriction of parsedRestrictions) { const queryElements: SimpleQuery = {} + const additionalQueryElements: SimpleQuery = {} if (restriction.credentialDefinitionId) { queryElements.credentialDefinitionId = restriction.credentialDefinitionId + if (isUnqualifiedCredentialDefinitionId(restriction.credentialDefinitionId)) { + additionalQueryElements.credentialDefinitionId = restriction.credentialDefinitionId + } } if (restriction.issuerId || restriction.issuerDid) { - queryElements.issuerId = restriction.issuerId ?? restriction.issuerDid + const issuerId = (restriction.issuerId ?? restriction.issuerDid) as string + queryElements.issuerId = issuerId + if (isUnqualifiedIndyDid(issuerId)) { + additionalQueryElements.issuerId = issuerId + } } if (restriction.schemaId) { queryElements.schemaId = restriction.schemaId + if (isUnqualifiedSchemaId(restriction.schemaId)) { + additionalQueryElements.schemaId = restriction.schemaId + } } if (restriction.schemaIssuerId || restriction.schemaIssuerDid) { - queryElements.schemaIssuerId = restriction.schemaIssuerId ?? restriction.issuerDid + const issuerId = (restriction.schemaIssuerId ?? restriction.schemaIssuerDid) as string + queryElements.schemaIssuerId = issuerId + if (isUnqualifiedIndyDid(issuerId)) { + additionalQueryElements.schemaIssuerId = issuerId + } } if (restriction.schemaName) { @@ -825,8 +802,22 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } query.push(queryElements) + if (Object.keys(additionalQueryElements).length > 0) { + query.push(additionalQueryElements) + } } return query.length === 1 ? query[0] : { $or: query } } + + public async createW3cPresentation(options: CreateW3cPresentationOptions) { + let presentation: W3cPresentation | undefined + try { + presentation = W3cPresentation.create(options) + const presentationJson = presentation.toJson() as unknown as JsonObject + return presentationJson + } finally { + presentation?.handle.clear() + } + } } diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts index f9f565e53e..3916cc0fa0 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts @@ -1,10 +1,14 @@ import type { AnonCredsProof, AnonCredsProofRequest, AnonCredsNonRevokedInterval } from '../models' import type { AnonCredsVerifierService, VerifyProofOptions } from '../services' -import type { AgentContext } from '@credo-ts/core' -import type { JsonObject, NonRevokedIntervalOverride } from '@hyperledger/anoncreds-shared' +import type { AgentContext, W3cJsonLdVerifiablePresentation } from '@credo-ts/core' +import type { + JsonObject, + NonRevokedIntervalOverride, + VerifyW3cPresentationOptions, +} from '@hyperledger/anoncreds-shared' -import { injectable } from '@credo-ts/core' -import { Presentation } from '@hyperledger/anoncreds-shared' +import { JsonTransformer, injectable } from '@credo-ts/core' +import { Presentation, W3cPresentation } from '@hyperledger/anoncreds-shared' import { fetchRevocationStatusList } from '../utils' @@ -137,4 +141,22 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { nonRevokedIntervalOverrides: nonRevokedIntervalOverrides.length ? nonRevokedIntervalOverrides : undefined, } } + + public verifyW3cPresentation( + presentation: W3cJsonLdVerifiablePresentation, + options: VerifyW3cPresentationOptions + ): boolean { + const presentationJson = JsonTransformer.toJSON(presentation) + const w3cPresentation = W3cPresentation.fromJson(presentationJson) + + let result = false + + try { + result = w3cPresentation.verify(options) + } finally { + w3cPresentation?.handle.clear() + } + + return result + } } diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts index 169161fa80..89c58ec206 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts @@ -1,23 +1,21 @@ +import type { W3cAnoncredsCredentialMetadata } from '../../utils/metadata' import type { AnonCredsCredentialDefinition, - AnonCredsCredentialRequestMetadata, AnonCredsProofRequest, - AnonCredsRevocationRegistryDefinition, AnonCredsRevocationStatusList, AnonCredsSchema, AnonCredsSelectedCredentials, } from '@credo-ts/anoncreds' +import type { AnonCredsCredentialTags } from '@credo-ts/core' import type { JsonObject } from '@hyperledger/anoncreds-shared' import { - ConsoleLogger, DidResolverService, DidsModuleConfig, Ed25519Signature2018, InjectionSymbols, KeyType, SignatureSuiteToken, - SigningProviderRegistry, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialRecord, @@ -26,25 +24,33 @@ import { W3cCredentialsModuleConfig, W3cJsonLdVerifiableCredential, } from '@credo-ts/core' -import { RevocationRegistryDefinition, anoncreds } from '@hyperledger/anoncreds-nodejs' +import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' import { AnonCredsCredentialDefinitionRepository } from '../../../../anoncreds/src/repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsLinkSecretRepository } from '../../../../anoncreds/src/repository/AnonCredsLinkSecretRepository' import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' +import { testLogger } from '../../../../core/tests' import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { W3cAnonCredsCredentialMetadataKey } from '../../utils/metadata' import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService' +import { InMemoryWallet } from './../../../../../tests/InMemoryWallet' import { createCredentialDefinition, createCredentialForHolder, createCredentialOffer, createLinkSecret, + storeCredential, } from './helpers' -import { AnonCredsModuleConfig, AnonCredsHolderServiceSymbol, AnonCredsLinkSecretRecord } from '@credo-ts/anoncreds' +import { + AnonCredsCredentialRepository, + AnonCredsModuleConfig, + AnonCredsHolderServiceSymbol, + AnonCredsLinkSecretRecord, +} from '@credo-ts/anoncreds' const agentConfig = getAgentConfig('AnonCredsRsHolderServiceTest') const anonCredsHolderService = new AnonCredsRsHolderService() @@ -62,14 +68,13 @@ jest.mock('../../../../core/src/modules/vc/repository/W3cCredentialRepository') const W3cCredentialRepositoryMock = W3cCredentialRepository as jest.Mock const w3cCredentialRepositoryMock = new W3cCredentialRepositoryMock() +jest.mock('../../../../anoncreds/src/repository/AnonCredsCredentialRepository') +const AnonCredsCredentialRepositoryMock = AnonCredsCredentialRepository as jest.Mock +const anoncredsCredentialRepositoryMock = new AnonCredsCredentialRepositoryMock() + const inMemoryStorageService = new InMemoryStorageService() -const logger = new ConsoleLogger() -const wallet = new RegisteredAskarTestWallet( - agentConfig.logger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([]) -) +const wallet = new InMemoryWallet() const agentContext = getAgentContext({ registerInstances: [ @@ -79,6 +84,7 @@ const agentContext = getAgentContext({ [AnonCredsCredentialDefinitionRepository, credentialDefinitionRepositoryMock], [AnonCredsLinkSecretRepository, anoncredsLinkSecretRepositoryMock], [W3cCredentialRepository, w3cCredentialRepositoryMock], + [AnonCredsCredentialRepository, anoncredsCredentialRepositoryMock], [AnonCredsHolderServiceSymbol, anonCredsHolderService], [ AnonCredsModuleConfig, @@ -87,8 +93,8 @@ const agentContext = getAgentContext({ anoncreds, }), ], - [InjectionSymbols.Logger, logger], - [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], + [InjectionSymbols.Logger, testLogger], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig())], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], [ SignatureSuiteToken, @@ -108,11 +114,14 @@ const agentContext = getAgentContext({ }) describe('AnonCredsRsHolderService', () => { - const getByCredentialIdMock = jest.spyOn(w3cCredentialRepositoryMock, 'getByCredentialId') + const getByCredentialIdMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'getByCredentialId') + const findSingleByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findSingleByQuery') const findByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findByQuery') beforeEach(() => { + findSingleByQueryMock.mockClear() getByCredentialIdMock.mockClear() + findByQueryMock.mockClear() }) test('createCredentialRequest', async () => { @@ -180,6 +189,7 @@ describe('AnonCredsRsHolderService', () => { } const { + schema: personSchema, credentialDefinition: personCredentialDefinition, credentialDefinitionPrivate: personCredentialDefinitionPrivate, keyCorrectnessProof: personKeyCorrectnessProof, @@ -189,6 +199,7 @@ describe('AnonCredsRsHolderService', () => { }) const { + schema: phoneSchema, credentialDefinition: phoneCredentialDefinition, credentialDefinitionPrivate: phoneCredentialDefinitionPrivate, keyCorrectnessProof: phoneKeyCorrectnessProof, @@ -208,7 +219,8 @@ describe('AnonCredsRsHolderService', () => { credentialInfo: personCredentialInfo, revocationRegistryDefinition: personRevRegDef, tailsPath: personTailsPath, - } = createCredentialForHolder({ + } = await createCredentialForHolder({ + agentContext, attributes: { name: 'John', sex: 'M', @@ -222,16 +234,22 @@ describe('AnonCredsRsHolderService', () => { keyCorrectnessProof: personKeyCorrectnessProof, linkSecret, linkSecretId: 'linkSecretId', - credentialId: 'personCredId', revocationRegistryDefinitionId: 'personrevregid:uri', }) + const personRecord = await storeCredential(agentContext, personCredential, { + credentialDefinitionId: 'personcreddef:uri', + schemaId: 'personschema:uri', + schema: personSchema as unknown as AnonCredsSchema, + linkSecretId: 'linkSecretId', + }) const { credential: phoneCredential, credentialInfo: phoneCredentialInfo, revocationRegistryDefinition: phoneRevRegDef, tailsPath: phoneTailsPath, - } = createCredentialForHolder({ + } = await createCredentialForHolder({ + agentContext, attributes: { phoneNumber: 'linkSecretId56', }, @@ -242,57 +260,50 @@ describe('AnonCredsRsHolderService', () => { keyCorrectnessProof: phoneKeyCorrectnessProof, linkSecret, linkSecretId: 'linkSecretId', - credentialId: 'phoneCredId', revocationRegistryDefinitionId: 'phonerevregid:uri', }) + const phoneRecord = await storeCredential(agentContext, phoneCredential, { + credentialDefinitionId: 'phonecreddef:uri', + schemaId: 'phoneschema:uri', + schema: phoneSchema as unknown as AnonCredsSchema, + linkSecretId: 'linkSecretId', + }) + const selectedCredentials: AnonCredsSelectedCredentials = { selfAttestedAttributes: { attr5_referent: 'football' }, attributes: { - attr1_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo, revealed: true }, - attr2_referent: { credentialId: 'phoneCredId', credentialInfo: phoneCredentialInfo, revealed: true }, - attr3_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo, revealed: true }, - attr4_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo, revealed: true }, + attr1_referent: { + credentialId: personRecord.id, + credentialInfo: { ...personCredentialInfo, credentialId: personRecord.id }, + revealed: true, + }, + attr2_referent: { + credentialId: phoneRecord.id, + credentialInfo: { ...phoneCredentialInfo, credentialId: phoneRecord.id }, + revealed: true, + }, + attr3_referent: { + credentialId: personRecord.id, + credentialInfo: { ...personCredentialInfo, credentialId: personRecord.id }, + revealed: true, + }, + attr4_referent: { + credentialId: personRecord.id, + credentialInfo: { ...personCredentialInfo, credentialId: personRecord.id }, + revealed: true, + }, }, predicates: { - predicate1_referent: { credentialId: 'personCredId', credentialInfo: personCredentialInfo }, + predicate1_referent: { + credentialId: personRecord.id, + credentialInfo: { ...personCredentialInfo, credentialId: personRecord.id }, + }, }, } - getByCredentialIdMock.mockResolvedValueOnce( - new W3cCredentialRecord({ - credential: personCredential, - tags: {}, - anonCredsCredentialRecordOptions: { - credentialId: 'personCredId', - linkSecretId: 'linkSecretId', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', - schemaId: personCredentialInfo.schemaId, - credentialDefinitionId: personCredentialInfo.credentialDefinitionId, - revocationRegistryId: personCredentialInfo.revocationRegistryId ?? undefined, - }, - }) - ) - getByCredentialIdMock.mockResolvedValueOnce( - new W3cCredentialRecord({ - credential: phoneCredential, - tags: {}, - anonCredsCredentialRecordOptions: { - credentialId: 'phoneCredId', - linkSecretId: 'linkSecretId', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', - schemaId: phoneCredentialInfo.schemaId, - credentialDefinitionId: phoneCredentialInfo.credentialDefinitionId, - revocationRegistryId: phoneCredentialInfo.revocationRegistryId ?? undefined, - }, - }) - ) + findSingleByQueryMock.mockResolvedValueOnce(personRecord) + findSingleByQueryMock.mockResolvedValueOnce(phoneRecord) const revocationRegistries = { 'personrevregid:uri': { @@ -326,12 +337,13 @@ describe('AnonCredsRsHolderService', () => { revocationRegistries, }) - expect(getByCredentialIdMock).toHaveBeenCalledTimes(2) + expect(findSingleByQueryMock).toHaveBeenCalledTimes(2) // TODO: check proof object }) describe('getCredentialsForProofRequest', () => { const findByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findByQuery') + const anonCredsFindByQueryMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'findByQuery') const proofRequest: AnonCredsProofRequest = { nonce: anoncreds.generateNonce(), @@ -365,10 +377,12 @@ describe('AnonCredsRsHolderService', () => { beforeEach(() => { findByQueryMock.mockResolvedValue([]) + anonCredsFindByQueryMock.mockResolvedValue([]) }) afterEach(() => { findByQueryMock.mockClear() + anonCredsFindByQueryMock.mockClear() }) test('invalid referent', async () => { @@ -389,10 +403,10 @@ describe('AnonCredsRsHolderService', () => { expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { $and: [ { - 'attr::name::marker': true, + 'anonCredsAttr::name::marker': true, }, { - unqualifiedIssuerId: 'issuer:uri', + anonCredsIssuerId: 'issuer:uri', }, ], }) @@ -407,7 +421,7 @@ describe('AnonCredsRsHolderService', () => { expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { $and: [ { - 'attr::phoneNumber::marker': true, + 'anonCredsAttr::phoneNumber::marker': true, }, ], }) @@ -422,10 +436,13 @@ describe('AnonCredsRsHolderService', () => { expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { $and: [ { - 'attr::age::marker': true, + 'anonCredsAttr::age::marker': true, }, { - $or: [{ unqualifiedSchemaId: 'schemaid:uri', schemaName: 'schemaName' }, { schemaVersion: '1.0' }], + $or: [ + { anonCredsSchemaId: 'schemaid:uri', anonCredsSchemaName: 'schemaName' }, + { anonCredsSchemaVersion: '1.0' }, + ], }, ], }) @@ -440,12 +457,12 @@ describe('AnonCredsRsHolderService', () => { expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { $and: [ { - 'attr::name::marker': true, - 'attr::height::marker': true, + 'anonCredsAttr::name::marker': true, + 'anonCredsAttr::height::marker': true, }, { - unqualifiedCredentialDefinitionId: 'crededefid:uri', - unqualifiedIssuerId: 'issuerid:uri', + anonCredsCredentialDefinitionId: 'crededefid:uri', + anonCredsIssuerId: 'issuerid:uri', }, ], }) @@ -460,11 +477,11 @@ describe('AnonCredsRsHolderService', () => { expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { $and: [ { - 'attr::name::marker': true, + 'anonCredsAttr::name::marker': true, }, { - 'attr::name::value': 'Alice', - 'attr::name::marker': true, + 'anonCredsAttr::name::value': 'Alice', + 'anonCredsAttr::name::marker': true, }, ], }) @@ -479,7 +496,7 @@ describe('AnonCredsRsHolderService', () => { expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { $and: [ { - 'attr::age::marker': true, + 'anonCredsAttr::age::marker': true, }, ], }) @@ -487,66 +504,62 @@ describe('AnonCredsRsHolderService', () => { }) test('deleteCredential', async () => { + const record = new W3cCredentialRecord({ + credential: {} as W3cJsonLdVerifiableCredential, + tags: {}, + }) + findSingleByQueryMock.mockResolvedValueOnce(null).mockResolvedValueOnce(record) getByCredentialIdMock.mockRejectedValueOnce(new Error()) - getByCredentialIdMock.mockResolvedValueOnce( - new W3cCredentialRecord({ - credential: {} as W3cJsonLdVerifiableCredential, - tags: {}, - anonCredsCredentialRecordOptions: { - credentialId: 'personCredId', - linkSecretId: 'linkSecretId', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', - schemaId: 'schemaId', - credentialDefinitionId: 'credDefId', - revocationRegistryId: 'revRegId', - }, - }) - ) - - expect(anonCredsHolderService.deleteCredential(agentContext, 'credentialId')).rejects.toThrowError() + await expect(anonCredsHolderService.deleteCredential(agentContext, 'credentialId')).rejects.toThrow() await anonCredsHolderService.deleteCredential(agentContext, 'credentialId') - - expect(getByCredentialIdMock).toHaveBeenCalledWith(agentContext, 'credentialId') + expect(findSingleByQueryMock).toHaveBeenCalledWith(agentContext, { anonCredsCredentialId: 'credentialId' }) }) test('get single Credential', async () => { - getByCredentialIdMock.mockRejectedValueOnce(new Error()) - - getByCredentialIdMock.mockResolvedValueOnce( - new W3cCredentialRecord({ - credential: new W3cJsonLdVerifiableCredential({ - credentialSubject: new W3cCredentialSubject({ claims: { attr1: 'value1', attr2: 'value2' } }), - issuer: 'test', - issuanceDate: Date.now().toString(), - type: ['VerifiableCredential'], - proof: { - created: Date.now().toString(), - type: 'test', - proofPurpose: 'test', - verificationMethod: 'test', - }, - }), - tags: {}, - anonCredsCredentialRecordOptions: { - credentialId: 'myCredentialId', - credentialRevocationId: 'credentialRevocationId', - linkSecretId: 'linkSecretId', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', - schemaId: 'schemaId', - credentialDefinitionId: 'credDefId', - revocationRegistryId: 'revRegId', + const record = new W3cCredentialRecord({ + credential: new W3cJsonLdVerifiableCredential({ + credentialSubject: new W3cCredentialSubject({ claims: { attr1: 'value1', attr2: 'value2' } }), + issuer: 'test', + issuanceDate: Date.now().toString(), + type: ['VerifiableCredential'], + proof: { + created: Date.now().toString(), + type: 'test', + proofPurpose: 'test', + verificationMethod: 'test', }, - }) - ) + }), + tags: {}, + }) + + const tags: AnonCredsCredentialTags = { + anonCredsCredentialId: record.id, + anonCredsLinkSecretId: 'linkSecretId', + anonCredsCredentialDefinitionId: 'credDefId', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsMethodName: 'methodName', + anonCredsCredentialRevocationId: 'credentialRevocationId', + anonCredsRevocationRegistryId: 'revRegId', + } + + const anonCredsCredentialMetadata: W3cAnoncredsCredentialMetadata = { + credentialId: record.id, + credentialRevocationId: tags.anonCredsCredentialRevocationId, + linkSecretId: tags.anonCredsLinkSecretId, + methodName: tags.anonCredsMethodName, + } - expect( + record.setTags(tags) + record.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + findSingleByQueryMock.mockResolvedValueOnce(null).mockResolvedValueOnce(record) + getByCredentialIdMock.mockRejectedValueOnce(new Error()) + + await expect( anonCredsHolderService.getCredential(agentContext, { credentialId: 'myCredentialId' }) ).rejects.toThrowError() @@ -555,7 +568,7 @@ describe('AnonCredsRsHolderService', () => { expect(credentialInfo).toMatchObject({ attributes: { attr1: 'value1', attr2: 'value2' }, credentialDefinitionId: 'credDefId', - credentialId: 'myCredentialId', + credentialId: record.id, revocationRegistryId: 'revRegId', schemaId: 'schemaId', credentialRevocationId: 'credentialRevocationId', @@ -563,35 +576,47 @@ describe('AnonCredsRsHolderService', () => { }) test('getCredentials', async () => { - findByQueryMock.mockResolvedValueOnce([ - new W3cCredentialRecord({ - credential: new W3cJsonLdVerifiableCredential({ - credentialSubject: new W3cCredentialSubject({ claims: { attr1: 'value1', attr2: 'value2' } }), - issuer: 'test', - issuanceDate: Date.now().toString(), - type: ['VerifiableCredential'], - proof: { - created: Date.now().toString(), - type: 'test', - proofPurpose: 'test', - verificationMethod: 'test', - }, - }), - tags: {}, - anonCredsCredentialRecordOptions: { - credentialId: 'myCredentialId', - credentialRevocationId: 'credentialRevocationId', - linkSecretId: 'linkSecretId', - schemaIssuerId: 'schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', - schemaId: 'schemaId', - credentialDefinitionId: 'credDefId', - revocationRegistryId: 'revRegId', + const record = new W3cCredentialRecord({ + credential: new W3cJsonLdVerifiableCredential({ + credentialSubject: new W3cCredentialSubject({ claims: { attr1: 'value1', attr2: 'value2' } }), + issuer: 'test', + issuanceDate: Date.now().toString(), + type: ['VerifiableCredential'], + proof: { + created: Date.now().toString(), + type: 'test', + proofPurpose: 'test', + verificationMethod: 'test', }, }), - ]) + tags: {}, + }) + const records = [record] + + const tags: AnonCredsCredentialTags = { + anonCredsCredentialId: record.id, + anonCredsLinkSecretId: 'linkSecretId', + anonCredsCredentialDefinitionId: 'credDefId', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsMethodName: 'methodName', + anonCredsCredentialRevocationId: 'credentialRevocationId', + anonCredsRevocationRegistryId: 'revRegId', + } + + const anonCredsCredentialMetadata: W3cAnoncredsCredentialMetadata = { + credentialId: record.id, + credentialRevocationId: tags.anonCredsCredentialRevocationId, + linkSecretId: tags.anonCredsLinkSecretId, + methodName: tags.anonCredsMethodName, + } + + record.setTags(tags) + record.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + findByQueryMock.mockResolvedValueOnce(records) const credentialInfo = await anonCredsHolderService.getCredentials(agentContext, { credentialDefinitionId: 'credDefId', @@ -604,19 +629,19 @@ describe('AnonCredsRsHolderService', () => { }) expect(findByQueryMock).toHaveBeenCalledWith(agentContext, { - unqualifiedCredentialDefinitionId: 'credDefId', - unqualifiedSchemaId: 'schemaId', - unqualifiedSchemaIssuerId: 'schemaIssuerDid', - unqualifiedIssuerId: 'issuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'inMemory', + anonCredsCredentialDefinitionId: 'credDefId', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaIssuerId: 'schemaIssuerDid', + anonCredsIssuerId: 'issuerDid', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsMethodName: 'inMemory', }) expect(credentialInfo).toMatchObject([ { attributes: { attr1: 'value1', attr2: 'value2' }, credentialDefinitionId: 'credDefId', - credentialId: 'myCredentialId', + credentialId: record.id, revocationRegistryId: 'revRegId', schemaId: 'schemaId', credentialRevocationId: 'credentialRevocationId', @@ -636,14 +661,10 @@ describe('AnonCredsRsHolderService', () => { new AnonCredsLinkSecretRecord({ linkSecretId: 'linkSecretId', value: linkSecret }) ) - const schema: AnonCredsSchema = { - attrNames: ['name', 'sex', 'height', 'age'], - issuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', - name: 'schemaName', - version: '1', - } + const saveCredentialMock = jest.spyOn(w3cCredentialRepositoryMock, 'save') - const { credential, revocationRegistryDefinition, credentialRequestMetadata } = createCredentialForHolder({ + const { credential } = await createCredentialForHolder({ + agentContext, attributes: { name: 'John', sex: 'M', @@ -657,44 +678,22 @@ describe('AnonCredsRsHolderService', () => { keyCorrectnessProof, linkSecret, linkSecretId: 'linkSecretId', - credentialId: 'personCredId', revocationRegistryDefinitionId: 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag', }) - const saveCredentialMock = jest.spyOn(w3cCredentialRepositoryMock, 'save') - - saveCredentialMock.mockResolvedValue() + await storeCredential(agentContext, credential, { + schema: { + name: 'schemaname', + attrNames: ['name', 'age', 'height', 'sex'], + issuerId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + version: '1.0', + }, - const credentialId = await anonCredsHolderService.storeCredential(agentContext, { - credential, - credentialDefinition, + linkSecretId: 'linkSecretId', + schemaId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/credentialDefinition-name/1.0', credentialDefinitionId: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/CLAIM_DEF/104/default', - credentialRequestMetadata: credentialRequestMetadata.toJson() as unknown as AnonCredsCredentialRequestMetadata, - credentialId: 'personCredId', - schema, - revocationRegistry: { - id: 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag', - definition: new RevocationRegistryDefinition( - revocationRegistryDefinition.handle - ).toJson() as unknown as AnonCredsRevocationRegistryDefinition, - }, }) - expect(credentialId).toBe('personCredId') - expect(saveCredentialMock).toHaveBeenCalledWith( - agentContext, - expect.objectContaining({ - anonCredsCredentialMetadata: expect.objectContaining({ - credentialId: 'personCredId', - linkSecretId: 'linkSecretId', - }), - // The stored credential is different from the one received originally - _tags: expect.objectContaining({ - schemaName: 'schemaName', - schemaIssuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', - schemaVersion: '1', - }), - }) - ) + expect(saveCredentialMock).toHaveBeenCalledWith(agentContext, expect.objectContaining({ credential })) }) }) diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts index 3d3d198ff6..4fe802209d 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts @@ -1,7 +1,6 @@ import type { AnonCredsProofRequest } from '@credo-ts/anoncreds' import { - ConsoleLogger, DidResolverService, DidsModuleConfig, Ed25519Signature2018, @@ -11,7 +10,6 @@ import { VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialsModuleConfig, - encodeCredentialValue, } from '@credo-ts/core' import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { Subject } from 'rxjs' @@ -19,7 +17,9 @@ import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { testLogger } from '../../../../core/tests' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { encodeCredentialValue } from '../../utils/credential' import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService' import { AnonCredsRsIssuerService } from '../AnonCredsRsIssuerService' import { AnonCredsRsVerifierService } from '../AnonCredsRsVerifierService' @@ -53,8 +53,6 @@ const storageService = new InMemoryStorageService() const wallet = new InMemoryWallet() const registry = new InMemoryAnonCredsRegistry() -const logger = new ConsoleLogger() - const agentContext = getAgentContext({ wallet, registerInstances: [ @@ -72,8 +70,8 @@ const agentContext = getAgentContext({ }), ], - [InjectionSymbols.Logger, logger], - [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], + [InjectionSymbols.Logger, testLogger], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig())], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], [ SignatureSuiteToken, @@ -198,19 +196,14 @@ describe('AnonCredsRsServices', () => { }, }) - const credentialId = 'holderCredentialId' - - const storedId = await anonCredsHolderService.storeCredential(agentContext, { + const credentialId = await anonCredsHolderService.storeCredential(agentContext, { credential, credentialDefinition, schema, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, credentialRequestMetadata: credentialRequestState.credentialRequestMetadata, - credentialId, }) - expect(storedId).toEqual(credentialId) - const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { credentialId, }) @@ -221,6 +214,7 @@ describe('AnonCredsRsServices', () => { age: 25, name: 'John', }, + linkSecretId: 'linkSecretId', schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: null, @@ -406,19 +400,15 @@ describe('AnonCredsRsServices', () => { }, }) - const credentialId = 'holderCredentialId2' - - const storedId = await anonCredsHolderService.storeCredential(agentContext, { + // store credential now requires qualified identifiers + const credentialId = await anonCredsHolderService.storeCredential(agentContext, { credential, - credentialDefinition: unqualifiedCredentialDefinition.credentialDefinition, - schema: unqualifiedSchema.schema, - credentialDefinitionId: credentialOffer.cred_def_id, + credentialDefinition, + schema, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, credentialRequestMetadata: credentialRequestState.credentialRequestMetadata, - credentialId, }) - expect(storedId).toEqual(credentialId) - const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { credentialId, }) @@ -429,6 +419,7 @@ describe('AnonCredsRsServices', () => { age: 25, name: 'John', }, + linkSecretId: 'someLinkSecretId', schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: null, diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts index 293df8f3a6..286a61250d 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts @@ -1,11 +1,19 @@ +import type { W3cAnoncredsCredentialMetadata } from '../../utils/metadata' import type { AnonCredsCredentialDefinition, AnonCredsCredentialInfo, AnonCredsCredentialOffer, + AnonCredsSchema, } from '@credo-ts/anoncreds' +import type { AgentContext, AnonCredsCredentialTags } from '@credo-ts/core' import type { JsonObject } from '@hyperledger/anoncreds-shared' -import { JsonTransformer, W3cJsonLdVerifiableCredential } from '@credo-ts/core' +import { + JsonTransformer, + W3cCredentialRepository, + W3cCredentialService, + W3cJsonLdVerifiableCredential, +} from '@credo-ts/core' import { CredentialDefinition, CredentialOffer, @@ -20,6 +28,8 @@ import { anoncreds, } from '@hyperledger/anoncreds-shared' +import { W3cAnonCredsCredentialMetadataKey } from '../../utils/metadata' + /** * Creates a valid credential definition and returns its public and * private part, including its key correctness proof @@ -37,7 +47,7 @@ export function createCredentialDefinition(options: { attributeNames: string[]; const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = CredentialDefinition.create({ issuerId, schema, - schemaId: 'did:indy:sovrin:F72i3Y3Q4i466efjYJYCHM/anoncreds/v0/SCHEMA/npdb/4.3.4', + schemaId: 'schema:uri', signatureType: 'CL', supportRevocation: true, // FIXME: Revocation should not be mandatory but current anoncreds-rs is requiring it tag: 'TAG', @@ -65,7 +75,7 @@ export function createCredentialOffer(keyCorrectnessProof: Record linkSecret: string linkSecretId: string - credentialId: string revocationRegistryDefinitionId: string }) { const { @@ -101,7 +111,6 @@ export function createCredentialForHolder(options: { attributes, linkSecret, linkSecretId, - credentialId, revocationRegistryDefinitionId, } = options @@ -156,10 +165,10 @@ export function createCredentialForHolder(options: { const w3cJsonLdCredential = JsonTransformer.fromJSON(credentialObj.toJson(), W3cJsonLdVerifiableCredential) - const credentialInfo: AnonCredsCredentialInfo = { + const credentialInfo: Omit = { attributes, credentialDefinitionId, - credentialId, + linkSecretId, schemaId, methodName: 'inMemory', credentialRevocationId: null, @@ -204,3 +213,45 @@ export function createRevocationRegistryDefinition(options: { return { revocationRegistryDefinition, revocationRegistryDefinitionPrivate, tailsPath } } + +export async function storeCredential( + agentContext: AgentContext, + w3cJsonLdCredential: W3cJsonLdVerifiableCredential, + options: { + linkSecretId: string + credentialDefinitionId: string + schemaId: string + schema: AnonCredsSchema + } +) { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const record = await w3cCredentialService.storeCredential(agentContext, { + credential: w3cJsonLdCredential, + }) + + const anonCredsCredentialRecordTags: AnonCredsCredentialTags = { + anonCredsCredentialId: record.id, + anonCredsLinkSecretId: options.linkSecretId, + anonCredsCredentialDefinitionId: options.credentialDefinitionId, + anonCredsSchemaId: options.schemaId, + anonCredsSchemaName: options.schema.name, + anonCredsSchemaIssuerId: options.schema.issuerId, + anonCredsSchemaVersion: options.schema.version, + anonCredsMethodName: 'method', + } + + const anonCredsCredentialMetadata: W3cAnoncredsCredentialMetadata = { + credentialId: record.id, + credentialRevocationId: anonCredsCredentialRecordTags.anonCredsCredentialRevocationId, + linkSecretId: anonCredsCredentialRecordTags.anonCredsLinkSecretId, + methodName: anonCredsCredentialRecordTags.anonCredsMethodName, + } + + record.setTags(anonCredsCredentialRecordTags) + record.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + await w3cCredentialRepository.update(agentContext, record) + + return record +} diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index d8f81b7b19..73f45b770e 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -1,12 +1,7 @@ import type { AnonCredsCredentialFormat, AnonCredsCredentialProposalFormat } from './AnonCredsCredentialFormat' -import type { - AnonCredsCredential, - AnonCredsCredentialOffer, - AnonCredsCredentialRequest, - AnonCredsCredentialRequestMetadata, -} from '../models' +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' -import type { AnonCredsCredentialMetadata } from '../utils/metadata' +import type { AnonCredsCredentialMetadata, AnonCredsCredentialRequestMetadata } from '../utils/metadata' import type { CredentialFormatService, AgentContext, @@ -63,6 +58,7 @@ import { createAndLinkAttachmentsToPreview, } from '../utils/credential' import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' +import { getStoreCredentialOptions } from '../utils/w3cAnonCredsUtils' const ANONCREDS_CREDENTIAL_OFFER = 'anoncreds/credential-offer@v1.0' const ANONCREDS_CREDENTIAL_REQUEST = 'anoncreds/credential-request@v1.0' @@ -391,11 +387,11 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const anonCredsCredential = attachment.getDataAsJson() - const { credentialDefinition, id: credentialDefinitionId } = await fetchCredentialDefinition( + const { credentialDefinition, credentialDefinitionId } = await fetchCredentialDefinition( agentContext, anonCredsCredential.cred_def_id ) - const { schema } = await fetchSchema(agentContext, anonCredsCredential.schema_id) + const { schema, indyNamespace } = await fetchSchema(agentContext, anonCredsCredential.schema_id) // Resolve revocation registry if credential is revocable const revocationRegistryResult = anonCredsCredential.rev_reg_id @@ -406,20 +402,25 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) assertCredentialValuesMatch(anonCredsCredential.values, recordCredentialValues) - const credentialId = await anonCredsHolderService.storeCredential(agentContext, { - credentialId: utils.uuid(), - credentialRequestMetadata, - credential: anonCredsCredential, - credentialDefinitionId, - credentialDefinition, - schema, - revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition - ? { - definition: revocationRegistryResult.revocationRegistryDefinition, - id: revocationRegistryResult.id, - } - : undefined, - }) + const storeCredentialOptions = getStoreCredentialOptions( + { + credentialId: utils.uuid(), + credentialRequestMetadata, + credential: anonCredsCredential, + credentialDefinitionId, + credentialDefinition, + schema, + revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition + ? { + definition: revocationRegistryResult.revocationRegistryDefinition, + id: revocationRegistryResult.revocationRegistryDefinitionId, + } + : undefined, + }, + indyNamespace + ) + + const credentialId = await anonCredsHolderService.storeCredential(agentContext, storeCredentialOptions) // If the credential is revocable, store the revocation identifiers in the credential record if (anonCredsCredential.rev_reg_id) { diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index e25fccc0ba..f79e9517f6 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -35,15 +35,7 @@ import type { ProofFormatAutoRespondPresentationOptions, } from '@credo-ts/core' -import { - encodeCredentialValue, - CredoError, - Attachment, - AttachmentData, - JsonEncoder, - ProofFormatSpec, - JsonTransformer, -} from '@credo-ts/core' +import { CredoError, Attachment, AttachmentData, JsonEncoder, ProofFormatSpec, JsonTransformer } from '@credo-ts/core' import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest' import { AnonCredsVerifierServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' @@ -60,6 +52,7 @@ import { fetchCredentialDefinition, fetchRevocationStatusList, } from '../utils' +import { encodeCredentialValue } from '../utils/credential' import { dateToTimestamp } from '../utils/timestamp' const ANONCREDS_PRESENTATION_PROPOSAL = 'anoncreds/proof-request@v1.0' diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index 0c63132c68..0c95ae1ac2 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -1,5 +1,7 @@ import type { AnonCredsRevocationStatusList } from '../models' import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' +import type { AnonCredsClaimRecord } from '../utils/credential' +import type { AnonCredsCredentialMetadata, AnonCredsCredentialRequestMetadata } from '../utils/metadata' import type { DataIntegrityCredentialRequest, DataIntegrityCredentialOffer, @@ -12,8 +14,6 @@ import type { DidCommSignedAttachmentDataIntegrityBindingProof, DataIntegrityOfferCredentialFormat, DataIntegrityCredentialFormat, - DataIntegrityRequestMetadata, - DataIntegrityMetadata, CredentialFormatService, AgentContext, CredentialFormatCreateProposalOptions, @@ -33,13 +33,10 @@ import type { CredentialExchangeRecord, CredentialPreviewAttributeOptions, JsonObject, - AnonCredsClaimRecord, JwaSignatureAlgorithm, JwsDetachedFormat, - AnonCredsCredentialRecordOptions, - DataIntegrityLinkSecretRequestMetadata, - DataIntegrityLinkSecretMetadata, VerificationMethod, + W3cCredentialRecord, } from '@credo-ts/core' import { @@ -47,7 +44,6 @@ import { CredentialFormatSpec, Attachment, JsonEncoder, - utils, CredentialProblemReportReason, JsonTransformer, W3cCredential, @@ -59,8 +55,6 @@ import { JwsService, getKeyFromVerificationMethod, getJwkFromKey, - DataIntegrityRequestMetadataKey, - DataIntegrityMetadataKey, ClaimFormat, JwtPayload, SignatureSuiteRegistry, @@ -70,26 +64,26 @@ import { } from '@credo-ts/core' import { W3cCredential as AW3cCredential } from '@hyperledger/anoncreds-shared' +import { AnonCredsRsHolderService } from '../anoncreds-rs' import { AnonCredsCredentialDefinitionRepository, - AnonCredsLinkSecretRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, AnonCredsRevocationRegistryState, } from '../repository' import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' -import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' import { dateToTimestamp, fetchCredentialDefinition, fetchRevocationRegistryDefinition, fetchRevocationStatusList, fetchSchema, - legacyCredentialToW3cCredential, } from '../utils' import { convertAttributesToCredentialValues, assertAttributesMatch as assertAttributesMatchSchema, } from '../utils/credential' +import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' +import { getAnonCredsTagsFromRecord } from '../utils/w3cAnonCredsUtils' const W3C_DATA_INTEGRITY_CREDENTIAL_OFFER = 'didcomm/w3c-di-vc-offer@v0.1' const W3C_DATA_INTEGRITY_CREDENTIAL_REQUEST = 'didcomm/w3c-di-vc-request@v0.1' @@ -183,7 +177,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const isV2Credential = context.find((c) => c === 'https://www.w3.org/ns/credentials/v2') if (isV1Credential) return '1.1' - else if (isV2Credential) return '2.0' + else if (isV2Credential) throw new CredoError('Received w3c credential with unsupported version 2.0.') else throw new CredoError('Cannot determine credential version from @context') } @@ -282,7 +276,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer }) const signedAttach = new Attachment({ - mimeType: typeof data === 'string' ? undefined : 'application/json', + mimeType: 'application/json', data: new AttachmentData({ base64: jws.payload }), }) @@ -340,12 +334,9 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const credentialOffer = offerAttachment.getDataAsJson() - const dataIntegrityMetadata: DataIntegrityMetadata = {} - const dataIntegrityRequestMetadata: DataIntegrityRequestMetadata = {} - let anonCredsLinkSecretDataIntegrityBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof | undefined = undefined - if (dataIntegrityFormat.anonCredsLinkSecretAcceptOfferOptions) { + if (dataIntegrityFormat.anonCredsLinkSecret) { if (!credentialOffer.binding_method?.anoncreds_link_secret) { throw new CredoError('Cannot request credential with a binding method that was not offered.') } @@ -365,24 +356,26 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer schema_id: credentialDefinitionReturn.credentialDefinition.schemaId, }, credentialDefinition: credentialDefinitionReturn.credentialDefinition, - linkSecretId: dataIntegrityFormat.anonCredsLinkSecretAcceptOfferOptions?.linkSecretId, + linkSecretId: dataIntegrityFormat.anonCredsLinkSecret?.linkSecretId, }) if (!anonCredsCredentialRequest.entropy) throw new CredoError('Missing entropy for anonCredsCredentialRequest') anonCredsLinkSecretDataIntegrityBindingProof = anonCredsCredentialRequest as AnonCredsLinkSecretDataIntegrityBindingProof - dataIntegrityRequestMetadata.linkSecretRequestMetadata = anonCredsCredentialRequestMetadata - - dataIntegrityMetadata.linkSecretMetadata = { + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { credentialDefinitionId: credentialOffer.binding_method.anoncreds_link_secret.cred_def_id, schemaId: credentialDefinitionReturn.credentialDefinition.schemaId, - } + }) + credentialRecord.metadata.set( + AnonCredsCredentialRequestMetadataKey, + anonCredsCredentialRequestMetadata + ) } let didCommSignedAttachmentBindingProof: DidCommSignedAttachmentDataIntegrityBindingProof | undefined = undefined let didCommSignedAttachment: Attachment | undefined = undefined - if (dataIntegrityFormat.didCommSignedAttachmentAcceptOfferOptions) { + if (dataIntegrityFormat.didCommSignedAttachment) { if (!credentialOffer.binding_method?.didcomm_signed_attachment) { throw new CredoError('Cannot request credential with a binding method that was not offered.') } @@ -390,7 +383,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer didCommSignedAttachment = await this.createSignedAttachment( agentContext, { nonce: credentialOffer.binding_method.didcomm_signed_attachment.nonce }, - dataIntegrityFormat.didCommSignedAttachmentAcceptOfferOptions, + dataIntegrityFormat.didCommSignedAttachment, credentialOffer.binding_method.didcomm_signed_attachment.algs_supported ) @@ -412,12 +405,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new CredoError('Cannot request credential with a data model version that was not offered.') } - credentialRecord.metadata.set(DataIntegrityMetadataKey, dataIntegrityMetadata) - credentialRecord.metadata.set( - DataIntegrityRequestMetadataKey, - dataIntegrityRequestMetadata - ) - const credentialRequest: DataIntegrityCredentialRequest = { data_model_version: dataModelVersion, binding_proof: bindingProof, @@ -453,7 +440,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credentialRecord: CredentialExchangeRecord anonCredsLinkSecretBindingMethod: AnonCredsLinkSecretBindingMethod anonCredsLinkSecretBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof - linkSecretMetadata: DataIntegrityLinkSecretMetadata + linkSecretMetadata: AnonCredsCredentialMetadata credentialSubjectId?: string } ): Promise { @@ -489,7 +476,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const credentialDefinition = ( await agentContext.dependencyManager .resolve(AnonCredsCredentialDefinitionRepository) - .getByCredentialDefinitionId(agentContext, linkSecretMetadata.credentialDefinitionId) + .getByCredentialDefinitionId(agentContext, linkSecretMetadata.credentialDefinitionId as string) ).credentialDefinition.value // We check locally for credential definition info. If it supports revocation, we need to search locally for @@ -532,7 +519,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { credential } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer: { ...anonCredsLinkSecretBindingMethod, - schema_id: linkSecretMetadata.schemaId, + schema_id: linkSecretMetadata.schemaId as string, }, credentialRequest: anonCredsLinkSecretBindingProof, credentialValues: convertAttributesToCredentialValues(credentialAttributes), @@ -546,16 +533,25 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credential.cred_def_id ) - return await legacyCredentialToW3cCredential(credential, anoncredsCredentialDefinition.issuerId) + const anoncredsRsHolderServive = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) + return await anoncredsRsHolderServive.legacyToW3cCredential( + agentContext, + credential, + anoncredsCredentialDefinition.issuerId + ) } - private async getSignatureMetadata(agentContext: AgentContext, offeredCredential: W3cCredential, issuerKid?: string) { + private async getSignatureMetadata( + agentContext: AgentContext, + offeredCredential: W3cCredential, + issuerVerificationMethod?: string + ) { const didsApi = agentContext.dependencyManager.resolve(DidsApi) const didDocument = await didsApi.resolveDidDocument(offeredCredential.issuerId) let verificationMethod: VerificationMethod - if (issuerKid) { - verificationMethod = didDocument.dereferenceKey(issuerKid, ['authentication', 'assertionMethod']) + if (issuerVerificationMethod) { + verificationMethod = didDocument.dereferenceKey(issuerVerificationMethod, ['authentication', 'assertionMethod']) } else { const vms = didDocument.authentication ?? didDocument.assertionMethod ?? didDocument.verificationMethod if (!vms || vms.length === 0) { @@ -600,9 +596,13 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer private async signCredential( agentContext: AgentContext, credential: W3cCredential | W3cJsonLdVerifiableCredential, - issuerKid?: string + issuerVerificationMethod?: string ) { - const { signatureSuite, verificationMethod } = await this.getSignatureMetadata(agentContext, credential, issuerKid) + const { signatureSuite, verificationMethod } = await this.getSignatureMetadata( + agentContext, + credential, + issuerVerificationMethod + ) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) let credentialToBeSigned = credential @@ -657,21 +657,20 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const credentialRequest = requestAttachment.getDataAsJson() if (!credentialRequest) throw new CredoError('Missing data integrity credential request in createCredential') - const dataIntegrityMetadata = credentialRecord.metadata.get(DataIntegrityMetadataKey) - if (!dataIntegrityMetadata) throw new CredoError('Missing data integrity credential metadata in createCredential') - let signedCredential: W3cJsonLdVerifiableCredential | undefined if (credentialRequest.binding_proof?.anoncreds_link_secret) { if (!credentialOffer.binding_method?.anoncreds_link_secret) { throw new CredoError('Cannot issue credential with a binding method that was not offered') } - if (!dataIntegrityMetadata.linkSecretMetadata) throw new CredoError('Missing anoncreds link secret metadata') + const linkSecretMetadata = + credentialRecord.metadata.get(AnonCredsCredentialMetadataKey) + if (!linkSecretMetadata) throw new CredoError('Missing anoncreds link secret metadata') signedCredential = await this.createCredentialWithAnonCredsDataIntegrityProof(agentContext, { credentialRecord, anonCredsLinkSecretBindingMethod: credentialOffer.binding_method.anoncreds_link_secret, - linkSecretMetadata: dataIntegrityMetadata.linkSecretMetadata, + linkSecretMetadata, anonCredsLinkSecretBindingProof: credentialRequest.binding_proof.anoncreds_link_secret, credentialSubjectId: dataIntegrityFormat.credentialSubjectId, }) @@ -692,8 +691,11 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new CredoError('Invalid nonce in signed attachment') } - const issuerKid = dataIntegrityFormat.didCommSignedAttachmentAcceptRequestOptions?.kid - signedCredential = await this.signCredential(agentContext, signedCredential ?? assertedCredential, issuerKid) + signedCredential = await this.signCredential( + agentContext, + signedCredential ?? assertedCredential, + dataIntegrityFormat.issuerVerificationMethod + ) } if ( @@ -712,11 +714,11 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer return { format, attachment } } - private async processLinkSecretBoundCredential( + private async storeAnonCredsCredential( agentContext: AgentContext, credentialJson: JsonObject, credentialRecord: CredentialExchangeRecord, - linkSecretRequestMetadata: DataIntegrityLinkSecretRequestMetadata + linkSecretRequestMetadata: AnonCredsCredentialRequestMetadata ) { if (!credentialRecord.credentialAttributes) { throw new CredoError('Missing credential attributes on credential record. Unable to check credential attributes') @@ -731,48 +733,34 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ? await fetchRevocationRegistryDefinition(agentContext, revocationRegistryId) : undefined - const methodName = agentContext.dependencyManager - .resolve(AnonCredsRegistryService) - .getRegistryForIdentifier(agentContext, credentialDefinitionReturn.id).methodName - - const linkSecretRecord = await agentContext.dependencyManager - .resolve(AnonCredsLinkSecretRepository) - .getByLinkSecretId(agentContext, linkSecretRequestMetadata.link_secret_name) - - if (!linkSecretRecord.value) throw new CredoError('Link Secret value not stored') - - const processed = aCredential.process({ - credentialRequestMetadata: linkSecretRequestMetadata as unknown as JsonObject, - credentialDefinition: credentialDefinitionReturn.credentialDefinition as unknown as JsonObject, - linkSecret: linkSecretRecord.value, - revocationRegistryDefinition: - revocationRegistryDefinitionReturn?.revocationRegistryDefinition as unknown as JsonObject, + const anonCredsRsHolderService = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) + const processed = await anonCredsRsHolderService.processW3cCredential(agentContext, aCredential, { + credentialRequestMetadata: linkSecretRequestMetadata, + credentialDefinition: credentialDefinitionReturn.credentialDefinition, + revocationRegistryDefinition: revocationRegistryDefinitionReturn?.revocationRegistryDefinition, }) - const anonCredsCredentialRecordOptions = { - credentialId: utils.uuid(), - linkSecretId: linkSecretRecord.linkSecretId, - credentialDefinitionId: credentialDefinitionReturn.id, - schemaId: schemaReturn.id, - schemaName: schemaReturn.schema.name, - schemaIssuerId: schemaReturn.schema.issuerId, - schemaVersion: schemaReturn.schema.version, - methodName, - revocationRegistryId: revocationRegistryDefinitionReturn?.id, - credentialRevocationId: revocationRegistryIndex?.toString(), - } + const w3cCredentialRecord = await anonCredsRsHolderService.storeW3cCredential(agentContext, { + credential: W3cJsonLdVerifiableCredential.fromJson(processed.toJson()), + schema: schemaReturn.schema, + credentialDefinitionId, + credentialDefinition: credentialDefinitionReturn.credentialDefinition, + credentialRequestMetadata: linkSecretRequestMetadata, + revocationRegistryDefinition: revocationRegistryDefinitionReturn?.revocationRegistryDefinition, + }) // If the credential is revocable, store the revocation identifiers in the credential record if (revocationRegistryId) { - const metadata = credentialRecord.metadata.get(DataIntegrityMetadataKey) - if (!metadata?.linkSecretMetadata) throw new CredoError('Missing link secret metadata') + const linkSecretMetadata = + credentialRecord.metadata.get(AnonCredsCredentialMetadataKey) + if (!linkSecretMetadata) throw new CredoError('Missing link secret metadata') - metadata.linkSecretMetadata.revocationRegistryId = revocationRegistryDefinitionReturn?.id - metadata.linkSecretMetadata.credentialRevocationId = revocationRegistryIndex?.toString() - credentialRecord.metadata.set(DataIntegrityMetadataKey, metadata) + linkSecretMetadata.revocationRegistryId = revocationRegistryDefinitionReturn?.revocationRegistryDefinitionId + linkSecretMetadata.credentialRevocationId = revocationRegistryIndex?.toString() + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, linkSecretMetadata) } - return { processed: processed.toJson(), anonCredsCredentialRecordOptions } + return w3cCredentialRecord } /** @@ -787,13 +775,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const credentialOffer = offerAttachment.getDataAsJson() const offeredCredentialJson = credentialOffer.credential - const credentialRequestMetadata = credentialRecord.metadata.get( - DataIntegrityRequestMetadataKey - ) - if (!credentialRequestMetadata) { - throw new CredoError(`Missing request metadata for credential exchange with thread id ${credentialRecord.id}`) - } - const credentialRequest = requestAttachment.getDataAsJson() if (!credentialRequest) throw new CredoError('Missing data integrity credential request in createCredential') @@ -841,29 +822,15 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new CredoError('Received invalid credential. Received credential does not match the offered credential') } - let anonCredsCredentialRecordOptions: AnonCredsCredentialRecordOptions | undefined - let w3cJsonLdVerifiableCredential: W3cJsonLdVerifiableCredential + let w3cCredentialRecord: W3cCredentialRecord if (credentialRequest.binding_proof?.anoncreds_link_secret) { - if (!credentialRequestMetadata.linkSecretRequestMetadata) { + const linkSecretRequestMetadata = credentialRecord.metadata.get( + AnonCredsCredentialRequestMetadataKey + ) + if (!linkSecretRequestMetadata) { throw new CredoError('Missing link secret request metadata') } - const { anonCredsCredentialRecordOptions: options, processed } = await this.processLinkSecretBoundCredential( - agentContext, - credentialJson, - credentialRecord, - credentialRequestMetadata.linkSecretRequestMetadata - ) - anonCredsCredentialRecordOptions = options - - w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(processed, W3cJsonLdVerifiableCredential) - await this.assertCredentialAttributesMatchSchemaAttributes( - agentContext, - w3cJsonLdVerifiableCredential, - anonCredsCredentialRecordOptions.schemaId, - true - ) - const integrityProtectedFields = ['@context', 'issuer', 'type', 'credentialSubject', 'validFrom', 'issuanceDate'] if ( Object.keys(offeredCredentialJson).some((key) => !integrityProtectedFields.includes(key) && key !== 'proof') @@ -871,22 +838,34 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new CredoError('Credential offer contains non anoncreds integrity protected fields.') } - if (w3cJsonLdVerifiableCredential.type.length !== 1) { + if (!Array.isArray(offeredCredentialJson.type) || offeredCredentialJson?.type.length !== 1) { throw new CredoError(`Invalid credential type. Only single credential type 'VerifiableCredential' is supported`) } + + w3cCredentialRecord = await this.storeAnonCredsCredential( + agentContext, + credentialJson, + credentialRecord, + linkSecretRequestMetadata + ) + + await this.assertCredentialAttributesMatchSchemaAttributes( + agentContext, + w3cCredentialRecord.credential, + getAnonCredsTagsFromRecord(w3cCredentialRecord)?.anonCredsSchemaId as string, + true + ) } else { - w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) + w3cCredentialRecord = await w3cCredentialService.storeCredential(agentContext, { + credential: w3cJsonLdVerifiableCredential, + }) } - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - const record = await w3cCredentialService.storeCredential(agentContext, { - credential: w3cJsonLdVerifiableCredential, - anonCredsCredentialRecordOptions, - }) - credentialRecord.credentials.push({ credentialRecordType: this.credentialRecordType, - credentialRecordId: record.id, + credentialRecordId: w3cCredentialRecord.id, }) } @@ -983,7 +962,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer (credentialRequest.binding_proof?.didcomm_signed_attachment && credentialOffer.binding_method?.didcomm_signed_attachment) - return !!(validLinkSecretRequest && validDidCommSignedAttachmetRequest) + return Boolean(validLinkSecretRequest && validDidCommSignedAttachmetRequest) } public async shouldAutoRespondToCredential( @@ -992,7 +971,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer // eslint-disable-next-line @typescript-eslint/no-unused-vars { credentialRecord, requestAttachment, credentialAttachment }: CredentialFormatAutoRespondCredentialOptions ) { - return false + return true } private async createDataIntegrityCredentialOffer( @@ -1006,8 +985,8 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { bindingRequired, credential, - anonCredsLinkSecretBindingMethodOptions, - didCommSignedAttachmentBindingMethodOptions, + anonCredsLinkSecretBinding: anonCredsLinkSecretBindingMethodOptions, + didCommSignedAttachmentBinding: didCommSignedAttachmentBindingMethodOptions, } = options const dataModelVersionsSupported: W3C_VC_DATA_MODEL_VERSION[] = ['1.1'] @@ -1017,8 +996,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const validW3cCredential = JsonTransformer.fromJSON(credentialJson, W3cCredential) const previewAttributes = this.previewAttributesFromCredential(validW3cCredential) - const dataIntegrityMetadata: DataIntegrityMetadata = {} - let anonCredsLinkSecretBindingMethod: AnonCredsLinkSecretBindingMethod | undefined = undefined if (anonCredsLinkSecretBindingMethodOptions) { const { credentialDefinitionId, revocationRegistryDefinitionId, revocationRegistryIndex } = @@ -1058,19 +1035,20 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { schema_id, ..._anonCredsLinkSecretBindingMethod } = anoncredsCredentialOffer anonCredsLinkSecretBindingMethod = _anonCredsLinkSecretBindingMethod - dataIntegrityMetadata.linkSecretMetadata = { + credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { schemaId: schema_id, credentialDefinitionId: credentialDefinitionId, credentialRevocationId: revocationRegistryIndex?.toString(), revocationRegistryId: revocationRegistryDefinitionId, - } + }) } let didCommSignedAttachmentBindingMethod: DidCommSignedAttachmentBindingMethod | undefined = undefined if (didCommSignedAttachmentBindingMethodOptions) { const { didMethodsSupported, algsSupported } = didCommSignedAttachmentBindingMethodOptions didCommSignedAttachmentBindingMethod = { - did_methods_supported: didMethodsSupported ?? this.getSupportedDidMethods(agentContext), + did_methods_supported: + didMethodsSupported ?? agentContext.dependencyManager.resolve(DidsApi).supportedResolverMethods, algs_supported: algsSupported ?? this.getSupportedJwaSignatureAlgorithms(agentContext), nonce: await agentContext.wallet.generateNonce(), } @@ -1098,8 +1076,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credential: credentialJson, } - credentialRecord.metadata.set(DataIntegrityMetadataKey, dataIntegrityMetadata) - return { dataIntegrityCredentialOffer, previewAttributes } } @@ -1159,17 +1135,6 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer return attachment } - private getSupportedDidMethods(agentContext: AgentContext) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const supportedDidMethods: Set = new Set() - - for (const resolver of didsApi.config.resolvers) { - resolver.supportedMethods.forEach((method) => supportedDidMethods.add(method)) - } - - return Array.from(supportedDidMethods) - } - /** * Returns the JWA Signature Algorithms that are supported by the wallet. * diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts index 545fd8db06..76f14aa498 100644 --- a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -1,12 +1,7 @@ import type { LegacyIndyCredentialFormat, LegacyIndyCredentialProposalFormat } from './LegacyIndyCredentialFormat' -import type { - AnonCredsCredential, - AnonCredsCredentialOffer, - AnonCredsCredentialRequest, - AnonCredsCredentialRequestMetadata, -} from '../models' +import type { AnonCredsCredential, AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '../models' import type { AnonCredsIssuerService, AnonCredsHolderService } from '../services' -import type { AnonCredsCredentialMetadata } from '../utils/metadata' +import type { AnonCredsCredentialMetadata, AnonCredsCredentialRequestMetadata } from '../utils/metadata' import type { CredentialFormatService, AgentContext, @@ -36,7 +31,6 @@ import { CredoError, Attachment, JsonEncoder, - utils, CredentialProblemReportReason, JsonTransformer, } from '@credo-ts/core' @@ -54,6 +48,7 @@ import { import { isUnqualifiedCredentialDefinitionId, isUnqualifiedSchemaId } from '../utils/indyIdentifiers' import { AnonCredsCredentialMetadataKey, AnonCredsCredentialRequestMetadataKey } from '../utils/metadata' import { generateLegacyProverDidLikeString } from '../utils/proverDid' +import { getStoreCredentialOptions } from '../utils/w3cAnonCredsUtils' const INDY_CRED_ABSTRACT = 'hlindy/cred-abstract@v2.0' const INDY_CRED_REQUEST = 'hlindy/cred-req@v2.0' @@ -359,11 +354,12 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic const anonCredsCredential = attachment.getDataAsJson() - const { credentialDefinition, id: credentialDefinitionId } = await fetchCredentialDefinition( + const { credentialDefinition, credentialDefinitionId } = await fetchCredentialDefinition( agentContext, anonCredsCredential.cred_def_id ) - const { schema } = await fetchSchema(agentContext, anonCredsCredential.schema_id) + + const { schema, indyNamespace } = await fetchSchema(agentContext, anonCredsCredential.schema_id) // Resolve revocation registry if credential is revocable const revocationRegistryResult = anonCredsCredential.rev_reg_id @@ -374,20 +370,24 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic const recordCredentialValues = convertAttributesToCredentialValues(credentialRecord.credentialAttributes) assertCredentialValuesMatch(anonCredsCredential.values, recordCredentialValues) - const credentialId = await anonCredsHolderService.storeCredential(agentContext, { - credentialId: utils.uuid(), - credentialRequestMetadata, - credential: anonCredsCredential, - credentialDefinitionId, - credentialDefinition, - schema, - revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition - ? { - definition: revocationRegistryResult.revocationRegistryDefinition, - id: revocationRegistryResult.id, - } - : undefined, - }) + const storeCredentialOptions = getStoreCredentialOptions( + { + credential: anonCredsCredential, + credentialRequestMetadata, + credentialDefinition, + schema, + credentialDefinitionId, + revocationRegistry: revocationRegistryResult?.revocationRegistryDefinition + ? { + id: revocationRegistryResult.revocationRegistryDefinitionId, + definition: revocationRegistryResult.revocationRegistryDefinition, + } + : undefined, + }, + indyNamespace + ) + + const credentialId = await anonCredsHolderService.storeCredential(agentContext, storeCredentialOptions) // If the credential is revocable, store the revocation identifiers in the credential record if (anonCredsCredential.rev_reg_id) { diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts index c734dd2938..a0bd459b48 100644 --- a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts @@ -35,15 +35,7 @@ import type { ProofFormatAutoRespondPresentationOptions, } from '@credo-ts/core' -import { - encodeCredentialValue, - CredoError, - Attachment, - AttachmentData, - JsonEncoder, - ProofFormatSpec, - JsonTransformer, -} from '@credo-ts/core' +import { CredoError, Attachment, AttachmentData, JsonEncoder, ProofFormatSpec, JsonTransformer } from '@credo-ts/core' import { AnonCredsProofRequest as AnonCredsProofRequestClass } from '../models/AnonCredsProofRequest' import { AnonCredsVerifierServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' @@ -58,8 +50,16 @@ import { getRevocationRegistriesForProof, fetchSchema, fetchCredentialDefinition, + fetchRevocationStatusList, } from '../utils' -import { fetchRevocationStatusList, getUnQualifiedId } from '../utils/ledgerObjects' +import { encodeCredentialValue } from '../utils/credential' +import { + getUnQualifiedDidIndyDid, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedSchemaId, + getUnqualifiedDidIndySchema, + getUnqualifiedDidIndyCredentialDefinition, +} from '../utils/indyIdentifiers' import { dateToTimestamp } from '../utils/timestamp' const V2_INDY_PRESENTATION_PROPOSAL = 'hlindy/proof-req@v2.0' @@ -468,7 +468,11 @@ export class LegacyIndyProofFormatService implements ProofFormatService { name: 'John', }, schemaId: schemaState.schemaId, + linkSecretId: 'link-secret-id', credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: null, credentialRevocationId: null, diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index 39426f706e..56914eca83 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -15,25 +15,11 @@ export { generateLegacyProverDidLikeString } from './utils/proverDid' export * from './utils/indyIdentifiers' export { assertBestPracticeRevocationInterval } from './utils/revocationInterval' export { storeLinkSecret } from './utils/linkSecret' -export { legacyCredentialToW3cCredential, w3cToLegacyCredential } from './utils/w3cUtils' -export { dateToTimestamp } from './utils' +export { dateToTimestamp, AnonCredsCredentialValue, AnonCredsCredentialMetadata } from './utils' export { fetchCredentialDefinition, fetchRevocationRegistryDefinition, fetchSchema, - getIndyNamespace, - isIndyDid, - getUnQualifiedId as getNonQualifiedId, - getQualifiedCredentialDefinition, - getQualifiedId, - getQualifiedRevocationRegistryDefinition, - getQualifiedSchema, - getUnqualifiedCredentialDefinition, - getUnqualifiedRevocationRegistryDefinition, - getUnqualifiedSchema, - isQualifiedCredentialDefinition, - isQualifiedRevocationRegistryDefinition, - isQualifiedSchema, fetchRevocationStatusList, -} from './utils/ledgerObjects' +} from './utils/anonCredsObjects' diff --git a/packages/anoncreds/src/models/W3cAnonCredsMetadata.ts b/packages/anoncreds/src/models/W3cAnonCredsMetadata.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index cc7187be2a..b6726b2979 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -1,4 +1,4 @@ -import type { AnonCredsCredentialValue } from '@credo-ts/core' +import type { AnonCredsCredentialValue } from '../utils/credential' export const anonCredsPredicateType = ['>=', '>', '<=', '<'] as const export type AnonCredsPredicateType = (typeof anonCredsPredicateType)[number] diff --git a/packages/anoncreds/src/models/internal.ts b/packages/anoncreds/src/models/internal.ts index 53270b5d02..ef815cbe9b 100644 --- a/packages/anoncreds/src/models/internal.ts +++ b/packages/anoncreds/src/models/internal.ts @@ -1,4 +1,4 @@ -import type { AnonCredsClaimRecord } from '@credo-ts/core' +import type { AnonCredsClaimRecord } from '../utils/credential' export interface AnonCredsCredentialInfo { credentialId: string @@ -8,6 +8,7 @@ export interface AnonCredsCredentialInfo { revocationRegistryId: string | null credentialRevocationId: string | null methodName: string + linkSecretId: string } export interface AnonCredsRequestedAttributeMatch { @@ -35,9 +36,3 @@ export interface AnonCredsLinkSecretBlindingData { v_prime: string vr_prime: string | null } - -export interface AnonCredsCredentialRequestMetadata { - link_secret_blinding_data: AnonCredsLinkSecretBlindingData - link_secret_name: string - nonce: string -} diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index caba2a551c..9a8ce1e3c8 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -1,8 +1,4 @@ -import type { - AnonCredsCredentialInfo, - AnonCredsCredentialRequestMetadata, - AnonCredsSelectedCredentials, -} from '../models' +import type { AnonCredsCredentialInfo, AnonCredsSelectedCredentials } from '../models' import type { AnonCredsCredential, AnonCredsCredentialOffer, @@ -16,6 +12,7 @@ import type { AnonCredsRevocationStatusList, AnonCredsSchema, } from '../models/registry' +import type { AnonCredsCredentialRequestMetadata } from '../utils/metadata' import type { W3cJsonLdVerifiableCredential } from '@credo-ts/core' export interface AnonCredsAttributeInfo { diff --git a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts index cb9eb9eee4..1ca0a2706e 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts @@ -3,12 +3,11 @@ import type { Wallet } from '@credo-ts/core' import { Agent, CacheModuleConfig, - ConsoleLogger, + CredoError, DidResolverService, DidsModuleConfig, Ed25519Signature2018, EventEmitter, - InMemoryLruCache, InjectionSymbols, KeyType, SignatureSuiteToken, @@ -20,11 +19,12 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from '../../../../../core/tests' +import { agentDependencies, getAgentConfig, getAgentContext, mockFunction, testLogger } from '../../../../../core/tests' import { InMemoryAnonCredsRegistry } from '../../../../tests/InMemoryAnonCredsRegistry' import { AnonCredsModuleConfig } from '../../../AnonCredsModuleConfig' import { AnonCredsCredentialRecord } from '../../../repository' import { AnonCredsRegistryService } from '../../../services' +import { getUnQualifiedDidIndyDid, getQualifiedDidIndyDid, isUnqualifiedIndyDid } from '../../../utils/indyIdentifiers' import * as testModule from '../anonCredsCredentialRecord' import { anoncreds } from './../../../../tests/helpers' @@ -43,14 +43,25 @@ const eventEmitter = new EventEmitter(agentDependencies, stop) const w3cRepo = { save: jest.fn(), + update: jest.fn(), } -const cacheModuleConfig = new InMemoryLruCache({ limit: 500 }) +const inMemoryLruCache = { + get: jest.fn(), + set: jest.fn(), + clear: jest.fn(), + remove: jest.fn(), +} + +const cacheModuleConfig = new CacheModuleConfig({ + cache: inMemoryLruCache, +}) const inMemoryStorageService = new InMemoryStorageService() -const logger = new ConsoleLogger() + const agentContext = getAgentContext({ registerInstances: [ + [CacheModuleConfig, cacheModuleConfig], [EventEmitter, eventEmitter], [W3cCredentialRepository, w3cRepo], [InjectionSymbols.Stop$, new Subject()], @@ -58,10 +69,9 @@ const agentContext = getAgentContext({ [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], [InjectionSymbols.StorageService, inMemoryStorageService], [AnonCredsRegistryService, new AnonCredsRegistryService()], - [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], - [InjectionSymbols.Logger, logger], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig())], + [InjectionSymbols.Logger, testLogger], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], - [CacheModuleConfig, cacheModuleConfig], [AnonCredsModuleConfig, anonCredsModuleConfig], [ SignatureSuiteToken, @@ -109,95 +119,188 @@ const AgentMock = Agent as jest.Mock describe('0.4-0.5 | AnonCredsRecord', () => { let agent: Agent - beforeEach(() => { - agent = new AgentMock() - }) - describe('migrateW3cCredentialRecordToV0_5()', () => { - it('should fetch all w3c credential records and re-save them', async () => { - const records = [ - new AnonCredsCredentialRecord({ - credential: { - schema_id: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', - cred_def_id: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/CLAIM_DEF/34815/Employee Credential', - - values: { - name: { - raw: 'John', - encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', - }, - age: { - raw: '25', - encoded: '25', - }, - }, - signature: { - p_credential: { - m_2: '96181142928573619139692730181044468294945970900261235940698944149443005219418', - a: '95552886901127172841432400616361951122825637102065915900211722444153579891548765880931308692457984326066263506661706967742637168349111737200116541217341739027256190535822337883555402874901690699603230292607481206740216276736875319709356355255797288879451730329296366840213920367976178079664448005608079197649139477441385127107355597906058676699377491628047651331689288017597714832563994968230904723400034478518535411493372596211553797813567090114739752408151368926090849149021350138796163980103411453098000223493524437564062789271302371287568506870484060911412715559140166845310368136412863128732929561146328431066870', - e: '259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742929837794489002147266183999965799605813', - v: '8070312275110314663750247899433202850238560575163878956819342967827136399370879736823043902982634515009588016797203155246614708232573921376646871743359587732590693401587607271972304303322060390310307460889523961550612965021232979808509508502354241838342542729225461467834597352210800168107201638861601487760961526713355932504366874557170337152964069325172574449356691055377568302458374147949937789910094307449082152173580675507028369533914480926873196435808261915052547630680304620062203647948590064800546491641963412948122135194369131128319694594446518925913583118382698018169919523769679141724867515604189334120099773703979769794325694804992635522127820413717601811493634024617930397944903746555691677663850240187799372670069559074549528342288602574968520156320273386872799429362106185458798531573424651644586691950218', - }, - r_credential: null, - }, - signature_correctness_proof: { - se: '22707379000451320101568757017184696744124237924783723059712360528872398590682272715197914336834321599243107036831239336605987281577690130807752876870302232265860540101807563741012022740942625464987934377354684266599492895835685698819662114798915664525092894122648542269399563759087759048742378622062870244156257780544523627249100818371255142174054148531811440128609220992508274170196108004985441276737673328642493312249112077836369109453214857237693701603680205115444482751700483514317558743227403858290707747986550689265796031162549838465391957776237071049436590886476581821857234951536091662216488995258175202055258', - c: '86499530658088050169174214946559930902913340880816576251403968391737698128027', - }, - //witness: null, - rev_reg_id: undefined, - //rev_reg: undefined, - }, - credentialId: 'myCredentialId', - credentialRevocationId: undefined, - linkSecretId: 'linkSecretId', - issuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', - schemaIssuerId: 'did:example:schemaIssuerDid', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - methodName: 'methodName', - }), - ] - - const registry = await agentContext.dependencyManager - .resolve(AnonCredsRegistryService) - .getRegistryForIdentifier( - agentContext, - 'did:indy:local:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/CLAIM_DEF/34815/Employee Credential' - ) - - await registry.registerCredentialDefinition(agentContext, { - credentialDefinition: { - schemaId: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', - type: 'CL', - tag: 'Employee Credential', - value: { - primary: { - n: '96580316873365712442732878101936646890604119889300256012760004147648019614357085076364923021085826868139621573684543249964678348356482485140527957732786530916400278400000660594438781319168272211306232441102713960203075436899295821371799038925693667322779688360706410505540407867607819490853610928774850151039047069357657140257065718659230885391255982730600838743036039711140083284918623906117435892506848479452322000479436955298502839148769930281251929368562720371560260726440893569655811165804238971700685368149522154328822673070750192788830837447670660152195003043802510899143110060139772708073728514051890251226573', - s: '75501169085950126423249157998833414929129208062284812993616444532525695129548804062583842133218092574263501104948737639625833940700883624316320978432322582288936701621781896861131284952998380826417162040016550587340823832731945229065884469806723217100370126833740077464404509861175397581089717495779179489233739975691055780558708056569691296866880514640011052194662545371451908889892210433975411453987754134291774476185207289195701174795140189362641644917865101153841235103322243375241496141786303488408131721122704625842138002478498178520263715598899259097315781832554764315008915688899555385079843761690822607379111', - r: { - age: '77540670431411230038763922593314057361920691860149780021247345546110594816960144474334769978922558437548167814211078474008950463908860798685487527066465227411414311215109347438752200023045503271169383262727401013107872116564443896905324906462332380026785798806953280066387451803949226448584225962096665020244191229063723249351163778395354282347357165322007286709571349618598645876371030907856017571738360851407364231328550357981247798517795822722356010859380461592920151980368953491924564759581591539937752386114770938355831372517555540534219652937595339962248857890418611836415170566769174263185424389504546847791061', - name: '56742811203198572257254422595806148480437594543198516349563027967943211653217799525148065500107783030709376059668814822301811566517601408461597171188532787265942263962719966788682945248064629136273708677025304469521003291988851716171767936997105137959854045442533627185824896706311588434426708666794422548240008058413804660062414897767172901561637004230184962449104905433874433106461860673266368007446282814453132977549811373164579634926487398703746240854572636222768903661936542049761028833196194927339141225860442129881312421875004614067598828714629143133815560576383442835845338263420621113398541139833020926358483', - master_secret: - '47747144545528691003767568337472105276331891233385663931584274593369979405459771996932889017746007711684586508906823242148854224004122637231405489854166589517019033322603946444431305440324935310636815918200611202700765046091022859325187263050783813756813224792976045471735525150004048149843525973339369133943560241544453714388862237336971069786113757093274533177228170822141225802024684552058049687105759446916872700318309370449824235232087307054291066123530983268176971897233515383614938649406180978604188604030816485303101208443369021847829704196934707372786773595567687934642471997496883786836109942269282274646821', - }, - rctxt: - '56624145913410031711009467194049739028044689257231550726399481216874451927585543568732728200991667356553765568186221627220562697315384161695993324560029249334601709666000269987161110370944904361123034293076300325831500797294972192392858769494862446579930065658123775287266632055490150224877768031718759385137678458946705469525103921298013633970637295409365635673547258006414068589487568446936418629870049873056708576696589883095398217681918429160130132727488662842876963800048249179530353781028982129766362865351617486193454223628637074575525915653459208863652607756131262546529918749753409703149380392151341320092701', - z: '48207831484908089113913456529606728278875173243133137568203149862235480864817131176165695429997836542014395411854617371967345903846590322848315574430219622375108777832406077167357765312048126429295008846417923207098159790545077579480434122704652997388986707634157186643373176212809933891460515705299787583898608744041271224726626894030124816906292858431898018633343059228110335652476641836263281987023563730093708908265403781917908475102010080313484277539579578010231066258146934633395220956275733173978548481848026533424513200278825491847270318469226963088243667105115637069262564294713288882078385391140385504192475', - }, - }, - issuerId: 'did:indy:local:LjgpST2rjsoxYegQDRm7EL', - }, - options: {}, + beforeEach(() => { + anonCredsRepo.delete.mockClear() + anonCredsRepo.getAll.mockClear() + w3cRepo.save.mockClear() + w3cRepo.update.mockClear() + inMemoryLruCache.clear.mockClear() + inMemoryLruCache.get.mockClear() + inMemoryLruCache.set.mockClear() + inMemoryLruCache.remove.mockClear() + + agent = new AgentMock() + }) + + it('credential with cheqd identifier', async () => { + await testMigration(agent, { + issuerId: 'did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8L', + schemaIssuerId: 'did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K', + schemaId: 'did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K/resources/6259d357-eeb1-4b98-8bee-12a8390d3497', }) - mockFunction(anonCredsRepo.getAll).mockResolvedValue(records) + }) - await testModule.storeAnonCredsInW3cFormatV0_5(agent) + it('credential with did:indy (sovrin) identifier', async () => { + await testMigration(agent, { + issuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgg', + schemaIssuerId: 'did:indy:sovrin:7Tqg6BwSSWapxgUDm9KKgf', + schemaId: 'did:indy:sovrin:LjgpST2rjsoxYegQDRm7EL/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + indyNamespace: 'sovrin', + }) + }) - expect(anonCredsRepo.getAll).toHaveBeenCalledTimes(1) - expect(anonCredsRepo.getAll).toHaveBeenCalledWith(agent.context) - expect(w3cRepo.save).toHaveBeenCalledTimes(1) - expect(anonCredsRepo.delete).toHaveBeenCalledTimes(1) + it('credential with unqualified did:indy (bcovrin:test) identifiers', async () => { + await testMigration(agent, { + issuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH'), + schemaIssuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG'), + schemaId: getUnQualifiedDidIndyDid( + 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG/anoncreds/v0/SCHEMA/Employee Credential/1.0.0' + ), + indyNamespace: 'bcovrin:test', + }) + }) + + it('credential with cached unqualified did:indy (bcovrin:test) identifiers', async () => { + inMemoryLruCache.get.mockReturnValueOnce({ indyNamespace: 'bcovrin:test' }) + + await testMigration(agent, { + issuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH'), + schemaIssuerId: getUnQualifiedDidIndyDid('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG'), + schemaId: getUnQualifiedDidIndyDid( + 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjG/anoncreds/v0/SCHEMA/Employee Credential/1.0.0' + ), + indyNamespace: 'bcovrin:test', + shouldBeInCache: 'indy', + }) + }) + + it('credential with cached unqualified did:sov identifiers', async () => { + inMemoryLruCache.get.mockReturnValueOnce(null).mockReturnValueOnce({ indyNamespace: 'sov' }) + + await testMigration(agent, { + issuerId: 'SDqTzbVuCowusqGBNbNDjH', + schemaIssuerId: 'SDqTzbVuCowusqGBNbNDjG', + schemaId: 'SDqTzbVuCowusqGBNbNDjG:2:Employee Credential:1.0.0', + indyNamespace: 'sov', + shouldBeInCache: 'sov', + }) }) }) }) + +async function testMigration( + agent: Agent, + options: { + issuerId: string + schemaIssuerId: string + schemaId: string + indyNamespace?: string + shouldBeInCache?: 'indy' | 'sov' + } +) { + const { issuerId, schemaIssuerId, schemaId, indyNamespace } = options + + const registry = await agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, issuerId) + + const registerCredentialDefinitionReturn = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition: { + schemaId: indyNamespace ? getQualifiedDidIndyDid(schemaId, indyNamespace) : schemaId, + type: 'CL', + tag: 'Employee Credential', + value: { + primary: { + n: '96580316873365712442732878101936646890604119889300256012760004147648019614357085076364923021085826868139621573684543249964678348356482485140527957732786530916400278400000660594438781319168272211306232441102713960203075436899295821371799038925693667322779688360706410505540407867607819490853610928774850151039047069357657140257065718659230885391255982730600838743036039711140083284918623906117435892506848479452322000479436955298502839148769930281251929368562720371560260726440893569655811165804238971700685368149522154328822673070750192788830837447670660152195003043802510899143110060139772708073728514051890251226573', + s: '75501169085950126423249157998833414929129208062284812993616444532525695129548804062583842133218092574263501104948737639625833940700883624316320978432322582288936701621781896861131284952998380826417162040016550587340823832731945229065884469806723217100370126833740077464404509861175397581089717495779179489233739975691055780558708056569691296866880514640011052194662545371451908889892210433975411453987754134291774476185207289195701174795140189362641644917865101153841235103322243375241496141786303488408131721122704625842138002478498178520263715598899259097315781832554764315008915688899555385079843761690822607379111', + r: { + age: '77540670431411230038763922593314057361920691860149780021247345546110594816960144474334769978922558437548167814211078474008950463908860798685487527066465227411414311215109347438752200023045503271169383262727401013107872116564443896905324906462332380026785798806953280066387451803949226448584225962096665020244191229063723249351163778395354282347357165322007286709571349618598645876371030907856017571738360851407364231328550357981247798517795822722356010859380461592920151980368953491924564759581591539937752386114770938355831372517555540534219652937595339962248857890418611836415170566769174263185424389504546847791061', + name: '56742811203198572257254422595806148480437594543198516349563027967943211653217799525148065500107783030709376059668814822301811566517601408461597171188532787265942263962719966788682945248064629136273708677025304469521003291988851716171767936997105137959854045442533627185824896706311588434426708666794422548240008058413804660062414897767172901561637004230184962449104905433874433106461860673266368007446282814453132977549811373164579634926487398703746240854572636222768903661936542049761028833196194927339141225860442129881312421875004614067598828714629143133815560576383442835845338263420621113398541139833020926358483', + master_secret: + '47747144545528691003767568337472105276331891233385663931584274593369979405459771996932889017746007711684586508906823242148854224004122637231405489854166589517019033322603946444431305440324935310636815918200611202700765046091022859325187263050783813756813224792976045471735525150004048149843525973339369133943560241544453714388862237336971069786113757093274533177228170822141225802024684552058049687105759446916872700318309370449824235232087307054291066123530983268176971897233515383614938649406180978604188604030816485303101208443369021847829704196934707372786773595567687934642471997496883786836109942269282274646821', + }, + rctxt: + '56624145913410031711009467194049739028044689257231550726399481216874451927585543568732728200991667356553765568186221627220562697315384161695993324560029249334601709666000269987161110370944904361123034293076300325831500797294972192392858769494862446579930065658123775287266632055490150224877768031718759385137678458946705469525103921298013633970637295409365635673547258006414068589487568446936418629870049873056708576696589883095398217681918429160130132727488662842876963800048249179530353781028982129766362865351617486193454223628637074575525915653459208863652607756131262546529918749753409703149380392151341320092701', + z: '48207831484908089113913456529606728278875173243133137568203149862235480864817131176165695429997836542014395411854617371967345903846590322848315574430219622375108777832406077167357765312048126429295008846417923207098159790545077579480434122704652997388986707634157186643373176212809933891460515705299787583898608744041271224726626894030124816906292858431898018633343059228110335652476641836263281987023563730093708908265403781917908475102010080313484277539579578010231066258146934633395220956275733173978548481848026533424513200278825491847270318469226963088243667105115637069262564294713288882078385391140385504192475', + }, + }, + issuerId: indyNamespace ? getQualifiedDidIndyDid(issuerId, indyNamespace) : issuerId, + }, + options: {}, + }) + + if (!registerCredentialDefinitionReturn.credentialDefinitionState.credentialDefinitionId) + throw new CredoError('Registering Credential Definition Failed') + + const records = [ + new AnonCredsCredentialRecord({ + credential: { + schema_id: schemaId, + cred_def_id: registerCredentialDefinitionReturn.credentialDefinitionState.credentialDefinitionId, + values: { + name: { + raw: 'John', + encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', + }, + age: { raw: '25', encoded: '25' }, + }, + signature: { + p_credential: { + m_2: '96181142928573619139692730181044468294945970900261235940698944149443005219418', + a: '95552886901127172841432400616361951122825637102065915900211722444153579891548765880931308692457984326066263506661706967742637168349111737200116541217341739027256190535822337883555402874901690699603230292607481206740216276736875319709356355255797288879451730329296366840213920367976178079664448005608079197649139477441385127107355597906058676699377491628047651331689288017597714832563994968230904723400034478518535411493372596211553797813567090114739752408151368926090849149021350138796163980103411453098000223493524437564062789271302371287568506870484060911412715559140166845310368136412863128732929561146328431066870', + e: '259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742929837794489002147266183999965799605813', + v: '8070312275110314663750247899433202850238560575163878956819342967827136399370879736823043902982634515009588016797203155246614708232573921376646871743359587732590693401587607271972304303322060390310307460889523961550612965021232979808509508502354241838342542729225461467834597352210800168107201638861601487760961526713355932504366874557170337152964069325172574449356691055377568302458374147949937789910094307449082152173580675507028369533914480926873196435808261915052547630680304620062203647948590064800546491641963412948122135194369131128319694594446518925913583118382698018169919523769679141724867515604189334120099773703979769794325694804992635522127820413717601811493634024617930397944903746555691677663850240187799372670069559074549528342288602574968520156320273386872799429362106185458798531573424651644586691950218', + }, + r_credential: null, + }, + signature_correctness_proof: { + se: '22707379000451320101568757017184696744124237924783723059712360528872398590682272715197914336834321599243107036831239336605987281577690130807752876870302232265860540101807563741012022740942625464987934377354684266599492895835685698819662114798915664525092894122648542269399563759087759048742378622062870244156257780544523627249100818371255142174054148531811440128609220992508274170196108004985441276737673328642493312249112077836369109453214857237693701603680205115444482751700483514317558743227403858290707747986550689265796031162549838465391957776237071049436590886476581821857234951536091662216488995258175202055258', + c: '86499530658088050169174214946559930902913340880816576251403968391737698128027', + }, + rev_reg_id: undefined, + }, + credentialId: 'myCredentialId', + credentialRevocationId: undefined, + linkSecretId: 'linkSecretId', + issuerId, + schemaIssuerId, + schemaName: 'schemaName', + schemaVersion: 'schemaVersion', + methodName: 'methodName', + }), + ] + + mockFunction(anonCredsRepo.getAll).mockResolvedValue(records) + + await testModule.storeAnonCredsInW3cFormatV0_5(agent) + + const unqualifiedDidIndyDid = isUnqualifiedIndyDid(issuerId) + if (unqualifiedDidIndyDid) { + expect(inMemoryLruCache.get).toHaveBeenCalledTimes( + options.shouldBeInCache === 'sov' || !options.shouldBeInCache ? 2 : 1 + ) + expect(inMemoryLruCache.get).toHaveBeenCalledWith( + agent.context, + options.shouldBeInCache === 'sov' || !options.shouldBeInCache + ? 'IndySdkPoolService:' + issuerId + : 'IndyVdrPoolService:' + issuerId + ) + } else { + expect(inMemoryLruCache.get).toHaveBeenCalledTimes(0) + } + + expect(anonCredsRepo.getAll).toHaveBeenCalledTimes(1) + expect(anonCredsRepo.getAll).toHaveBeenCalledWith(agent.context) + expect(w3cRepo.save).toHaveBeenCalledTimes(1) + expect(w3cRepo.update).toHaveBeenCalledTimes(1) + expect(anonCredsRepo.delete).toHaveBeenCalledTimes(1) + + if (unqualifiedDidIndyDid && options.shouldBeInCache) { + expect(inMemoryLruCache.get).toHaveReturnedWith({ indyNamespace }) + } else if (unqualifiedDidIndyDid && !options.shouldBeInCache) { + expect(inMemoryLruCache.get).toHaveBeenCalledTimes(2) + } else { + expect(inMemoryLruCache.get).toHaveBeenCalledTimes(0) + } +} diff --git a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts index a04e8cdf19..ec9ebe8e88 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts @@ -1,71 +1,134 @@ +import type { W3cAnoncredsCredentialMetadata } from '../../utils/metadata' import type { AgentContext, BaseAgent } from '@credo-ts/core' -import { CacheModuleConfig, W3cCredentialService } from '@credo-ts/core' +import { CacheModuleConfig, CredoError, W3cCredentialRepository, W3cCredentialService } from '@credo-ts/core' +import { AnonCredsRsHolderService } from '../../anoncreds-rs/AnonCredsRsHolderService' import { AnonCredsCredentialRepository, type AnonCredsCredentialRecord } from '../../repository' -import { legacyCredentialToW3cCredential } from '../../utils' -import { getQualifiedId, fetchCredentialDefinition, getIndyNamespace } from '../../utils/ledgerObjects' - -async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRecord: AnonCredsCredentialRecord) { - const legacyCredential = legacyRecord.credential - const legacyTags = legacyRecord.getTags() - - let qualifiedCredentialDefinitionId: string | undefined - let qualifiedIssuerId: string | undefined - let namespace: string | undefined - +import { fetchCredentialDefinition } from '../../utils/anonCredsObjects' +import { + getIndyNamespaceFromIndyDid, + getQualifiedDidIndyDid, + isIndyDid, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedIndyDid, +} from '../../utils/indyIdentifiers' +import { W3cAnonCredsCredentialMetadataKey } from '../../utils/metadata' +import { getW3cRecordAnonCredsTags } from '../../utils/w3cAnonCredsUtils' + +async function getIndyNamespace( + agentContext: AgentContext, + legacyCredentialDefinitionId: string, + legacyIssuerId: string +) { const cacheModuleConfig = agentContext.dependencyManager.resolve(CacheModuleConfig) - const cache = cacheModuleConfig?.cache - const indyCacheKey = `IndyVdrPoolService:${legacyTags.credentialDefinitionId}` - const sovCacheKey = `IndySdkPoolService:${legacyTags.credentialDefinitionId}` + const cache = cacheModuleConfig.cache + + const indyCacheKey = `IndyVdrPoolService:${legacyIssuerId}` + const sovCacheKey = `IndySdkPoolService:${legacyIssuerId}` const cachedNymResponse: Record | null = (await cache.get(agentContext, indyCacheKey)) ?? (await cache.get(agentContext, sovCacheKey)) - namespace = cachedNymResponse?.indyNamespace - - if (!namespace) { + if (!cachedNymResponse?.indyNamespace || typeof cachedNymResponse?.indyNamespace !== 'string') { try { - const credentialDefinitionReturn = await fetchCredentialDefinition( - agentContext, - legacyTags.credentialDefinitionId - ) - qualifiedCredentialDefinitionId = credentialDefinitionReturn.qualifiedId - qualifiedIssuerId = credentialDefinitionReturn.qualifiedCredentialDefinition.issuerId - namespace = getIndyNamespace(credentialDefinitionReturn.qualifiedId) - } catch (e) { + const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, legacyCredentialDefinitionId) + const namespace = credentialDefinitionReturn.indyNamespace + + if (!namespace) { + throw new CredoError( + 'Could not determine the indyNamespace required for storing anoncreds in the new w3c format.' + ) + } + + return namespace + } catch (error) { agentContext.config.logger.warn( - [ - `Failed to fetch credential definition for credentialId ${legacyTags.credentialDefinitionId}.`, - `Not updating credential with id ${legacyRecord.credentialId} to W3C format.`, - ].join('\n') + `Failed to fetch credential definition for credentialId ${legacyCredentialDefinitionId}`, + error ) } } else { - qualifiedCredentialDefinitionId = getQualifiedId(legacyTags.credentialDefinitionId, namespace) - qualifiedIssuerId = getQualifiedId(legacyTags.issuerId, namespace) + return cachedNymResponse.indyNamespace } +} - if (!qualifiedCredentialDefinitionId || !qualifiedIssuerId || !namespace) return +async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRecord: AnonCredsCredentialRecord) { + const legacyTags = legacyRecord.getTags() - const w3cJsonLdCredential = await legacyCredentialToW3cCredential(legacyCredential, qualifiedIssuerId) + let indyNamespace: string | undefined + let qualifiedSchemaId: string + let qualifiedSchemaIssuerId: string + let qualifiedCredentialDefinitionId: string + let qualifiedIssuerId: string + let qualifiedRevocationRegistryId: string | undefined + + if ( + !isUnqualifiedCredentialDefinitionId(legacyTags.credentialDefinitionId) && + !isUnqualifiedIndyDid(legacyTags.issuerId) + ) { + if (isIndyDid(legacyTags.issuerId)) { + indyNamespace = getIndyNamespaceFromIndyDid(legacyTags.issuerId) + } + } else { + indyNamespace = await getIndyNamespace(agentContext, legacyTags.credentialDefinitionId, legacyTags.issuerId) + } + + if (indyNamespace) { + qualifiedCredentialDefinitionId = getQualifiedDidIndyDid(legacyTags.credentialDefinitionId, indyNamespace) + qualifiedIssuerId = getQualifiedDidIndyDid(legacyTags.issuerId, indyNamespace) + qualifiedRevocationRegistryId = legacyTags.revocationRegistryId + ? getQualifiedDidIndyDid(legacyTags.revocationRegistryId, indyNamespace) + : undefined + qualifiedSchemaId = getQualifiedDidIndyDid(legacyTags.schemaId, indyNamespace) + qualifiedSchemaIssuerId = getQualifiedDidIndyDid(legacyTags.schemaIssuerId, indyNamespace) + } else { + qualifiedCredentialDefinitionId = legacyTags.credentialDefinitionId + qualifiedIssuerId = legacyTags.issuerId + qualifiedRevocationRegistryId = legacyTags.revocationRegistryId + qualifiedSchemaId = legacyTags.schemaId + qualifiedSchemaIssuerId = legacyTags.schemaIssuerId + } + + const anonCredsRsHolderService = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) + const w3cJsonLdCredential = await anonCredsRsHolderService.legacyToW3cCredential( + agentContext, + legacyRecord.credential, + qualifiedIssuerId + ) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - await w3cCredentialService.storeCredential(agentContext, { + const w3cCredentialRecord = await w3cCredentialService.storeCredential(agentContext, { credential: w3cJsonLdCredential, - anonCredsCredentialRecordOptions: { - credentialId: legacyRecord.credentialId, - linkSecretId: legacyRecord.linkSecretId, - credentialDefinitionId: qualifiedCredentialDefinitionId, - schemaId: getQualifiedId(legacyTags.schemaId, namespace), - schemaName: legacyTags.schemaName, - schemaIssuerId: getQualifiedId(legacyTags.issuerId, namespace), - schemaVersion: legacyTags.schemaVersion, - methodName: legacyRecord.methodName, - revocationRegistryId: getQualifiedId(legacyTags.credentialDefinitionId, namespace), - credentialRevocationId: legacyTags.credentialRevocationId, + }) + + const anonCredsTags = getW3cRecordAnonCredsTags({ + w3cCredentialRecord, + schemaId: qualifiedSchemaId, + schema: { + issuerId: qualifiedSchemaIssuerId, + name: legacyTags.schemaName, + version: legacyTags.schemaVersion, }, + credentialRevocationId: legacyTags.credentialRevocationId, + revocationRegistryId: qualifiedRevocationRegistryId, + credentialDefinitionId: qualifiedCredentialDefinitionId, + linkSecretId: legacyTags.linkSecretId, + methodName: legacyTags.methodName, }) + + const anonCredsCredentialMetadata: W3cAnoncredsCredentialMetadata = { + credentialId: w3cCredentialRecord.id, + credentialRevocationId: anonCredsTags.anonCredsCredentialRevocationId, + linkSecretId: anonCredsTags.anonCredsLinkSecretId, + methodName: anonCredsTags.anonCredsMethodName, + } + + w3cCredentialRecord.setTags(anonCredsTags) + w3cCredentialRecord.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + await w3cCredentialRepository.update(agentContext, w3cCredentialRecord) } /** @@ -85,8 +148,15 @@ export async function storeAnonCredsInW3cFormatV0_5(age agent.config.logger.debug( `Re-saving anonCreds credential record with id ${record.id} in the new w3c format, and deleting the legacy record` ) - await migrateLegacyToW3cCredential(agent.context, record) - await anoncredsRepository.delete(agent.context, record) + try { + await migrateLegacyToW3cCredential(agent.context, record) + await anoncredsRepository.delete(agent.context, record) + } catch (error) { + agent.config.logger.error( + `Failed to migrate w3c credential record with id ${record.id} to storage version 0.5`, + error + ) + } agent.config.logger.debug(`Successfully migrated w3c credential record with id ${record.id} to storage version 0.5`) } diff --git a/packages/anoncreds/src/updates/0.4-0.5/index.ts b/packages/anoncreds/src/updates/0.4-0.5/index.ts index 29f16e6b91..9f846c7a09 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/index.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/index.ts @@ -2,6 +2,6 @@ import type { BaseAgent } from '@credo-ts/core' import { storeAnonCredsInW3cFormatV0_5 } from './anonCredsCredentialRecord' -export async function updateAnonCredsModuleV0_4_1ToV0_5(agent: Agent): Promise { +export async function updateAnonCredsModuleV0_4ToV0_5(agent: Agent): Promise { await storeAnonCredsInW3cFormatV0_5(agent) } diff --git a/packages/core/src/modules/vc/repository/__tests__/anonCredsCredentialValue.test.ts b/packages/anoncreds/src/utils/__tests__/anonCredsCredentialValue.test.ts similarity index 98% rename from packages/core/src/modules/vc/repository/__tests__/anonCredsCredentialValue.test.ts rename to packages/anoncreds/src/utils/__tests__/anonCredsCredentialValue.test.ts index 74494a1984..81ee9ff846 100644 --- a/packages/core/src/modules/vc/repository/__tests__/anonCredsCredentialValue.test.ts +++ b/packages/anoncreds/src/utils/__tests__/anonCredsCredentialValue.test.ts @@ -1,4 +1,4 @@ -import { encodeCredentialValue, mapAttributeRawValuesToAnonCredsCredentialValues } from '../anonCredsCredentialValue' +import { encodeCredentialValue, mapAttributeRawValuesToAnonCredsCredentialValues } from '../credential' const testVectors = { 'str 0.0': { diff --git a/packages/anoncreds/src/utils/anonCredsObjects.ts b/packages/anoncreds/src/utils/anonCredsObjects.ts new file mode 100644 index 0000000000..45df4984da --- /dev/null +++ b/packages/anoncreds/src/utils/anonCredsObjects.ts @@ -0,0 +1,95 @@ +import type { AnonCredsRevocationStatusList } from '../models' +import type { AgentContext } from '@credo-ts/core' + +import { CredoError } from '@credo-ts/core' + +import { AnonCredsRegistryService } from '../services' + +export async function fetchSchema(agentContext: AgentContext, schemaId: string) { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, schemaId) + .getSchema(agentContext, schemaId) + + if (!result || !result.schema) { + throw new CredoError(`Schema not found for id ${schemaId}: ${result.resolutionMetadata.message}`) + } + + const indyNamespace = result.schemaMetadata.didIndyNamespace + + return { + schema: result.schema, + schemaId: result.schemaId, + indyNamespace: indyNamespace && typeof indyNamespace === 'string' ? indyNamespace : undefined, + } +} + +export async function fetchCredentialDefinition(agentContext: AgentContext, credentialDefinitionId: string) { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, credentialDefinitionId) + .getCredentialDefinition(agentContext, credentialDefinitionId) + + if (!result || !result.credentialDefinition) { + throw new CredoError(`Schema not found for id ${credentialDefinitionId}: ${result.resolutionMetadata.message}`) + } + + const indyNamespace = result.credentialDefinitionMetadata.didIndyNamespace + + return { + credentialDefinition: result.credentialDefinition, + credentialDefinitionId, + indyNamespace: indyNamespace && typeof indyNamespace === 'string' ? indyNamespace : undefined, + } +} + +export async function fetchRevocationRegistryDefinition( + agentContext: AgentContext, + revocationRegistryDefinitionId: string +) { + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + + const result = await registryService + .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) + .getRevocationRegistryDefinition(agentContext, revocationRegistryDefinitionId) + + if (!result || !result.revocationRegistryDefinition) { + throw new CredoError( + `RevocationRegistryDefinition not found for id ${revocationRegistryDefinitionId}: ${result.resolutionMetadata.message}` + ) + } + + const indyNamespace = result.revocationRegistryDefinitionMetadata.didIndyNamespace + + return { + revocationRegistryDefinition: result.revocationRegistryDefinition, + revocationRegistryDefinitionId, + indyNamespace: indyNamespace && typeof indyNamespace === 'string' ? indyNamespace : undefined, + } +} + +export async function fetchRevocationStatusList( + agentContext: AgentContext, + revocationRegistryId: string, + timestamp: number +): Promise<{ revocationStatusList: AnonCredsRevocationStatusList }> { + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, revocationRegistryId) + + const { revocationStatusList, resolutionMetadata } = await registry.getRevocationStatusList( + agentContext, + revocationRegistryId, + timestamp + ) + + if (!revocationStatusList) { + throw new CredoError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` + ) + } + + return { revocationStatusList } +} diff --git a/packages/anoncreds/src/utils/credential.ts b/packages/anoncreds/src/utils/credential.ts index 8aa927b520..e8623f6583 100644 --- a/packages/anoncreds/src/utils/credential.ts +++ b/packages/anoncreds/src/utils/credential.ts @@ -1,7 +1,92 @@ import type { AnonCredsSchema, AnonCredsCredentialValues } from '../models' import type { CredentialPreviewAttributeOptions, LinkedAttachment } from '@credo-ts/core' -import { CredoError, encodeAttachment, encodeCredentialValue } from '@credo-ts/core' +import { CredoError, Hasher, TypedArrayEncoder, encodeAttachment } from '@credo-ts/core' +import bigInt from 'big-integer' + +export type AnonCredsClaimRecord = Record + +export interface AnonCredsCredentialValue { + raw: string + encoded: string // Raw value as number in string +} + +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 +} + +// TODO: this function can only encode strings +// If encoding numbers we run into problems with 0.0 representing the same value as 0 and is implicitly converted to 0 +/** + * 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 encodeCredentialValue(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' + } + + const buffer = TypedArrayEncoder.fromString(String(value)) + const hash = Hasher.hash(buffer, 'sha-256') + const hex = Buffer.from(hash).toString('hex') + + return bigInt(hex, 16).toString() +} + +export const mapAttributeRawValuesToAnonCredsCredentialValues = ( + record: AnonCredsClaimRecord +): Record => { + const credentialValues: Record = {} + + for (const [key, value] of Object.entries(record)) { + if (typeof value === 'object') { + throw new CredoError(`Unsupported value type: object for W3cAnonCreds Credential`) + } + credentialValues[key] = { + raw: value.toString(), + encoded: encodeCredentialValue(value), + } + } + + return credentialValues +} /** * Converts int value to string diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index 4cd04a00be..6b98e17358 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -4,7 +4,8 @@ export { assertNoDuplicateGroupsNamesInProofRequest } from './hasDuplicateGroupN export { areAnonCredsProofRequestsEqual } from './areRequestsEqual' export { assertBestPracticeRevocationInterval } from './revocationInterval' export { getRevocationRegistriesForRequest, getRevocationRegistriesForProof } from './getRevocationRegistries' -export { checkValidCredentialValueEncoding } from './credential' +export { checkValidCredentialValueEncoding, AnonCredsCredentialValue } from './credential' +export { AnonCredsCredentialMetadata } from './metadata' export { IsMap } from './isMap' export { composeCredentialAutoAccept, composeProofAutoAccept } from './composeAutoAccept' export { areCredentialPreviewAttributesEqual } from './credentialPreviewAttributes' @@ -16,24 +17,10 @@ export { unqualifiedSchemaIdRegex, unqualifiedSchemaVersionRegex, } from './indyIdentifiers' -export { legacyCredentialToW3cCredential, w3cToLegacyCredential } from './w3cUtils' export { fetchCredentialDefinition, fetchRevocationRegistryDefinition, fetchSchema, - getIndyNamespace, - isIndyDid, - getUnQualifiedId as getNonQualifiedId, - getQualifiedCredentialDefinition, - getQualifiedId, - getQualifiedRevocationRegistryDefinition, - getQualifiedSchema, - getUnqualifiedCredentialDefinition, - getUnqualifiedRevocationRegistryDefinition, - getUnqualifiedSchema, - isQualifiedCredentialDefinition, - isQualifiedRevocationRegistryDefinition, - isQualifiedSchema, fetchRevocationStatusList, -} from './ledgerObjects' +} from './anonCredsObjects' diff --git a/packages/anoncreds/src/utils/indyIdentifiers.ts b/packages/anoncreds/src/utils/indyIdentifiers.ts index f18e558447..b1951f2f03 100644 --- a/packages/anoncreds/src/utils/indyIdentifiers.ts +++ b/packages/anoncreds/src/utils/indyIdentifiers.ts @@ -1,3 +1,5 @@ +import type { AnonCredsCredentialDefinition, AnonCredsRevocationRegistryDefinition, AnonCredsSchema } from '../models' + import { CredoError } from '@credo-ts/core' const didIndyAnonCredsBase = @@ -50,6 +52,10 @@ export function getUnqualifiedRevocationRegistryDefinitionId( return `${unqualifiedDid}:4:${unqualifiedDid}:3:CL:${schemaSeqNo}:${credentialDefinitionTag}:CL_ACCUM:${revocationRegistryTag}` } +export function isUnqualifiedIndyDid(did: string) { + return unqualifiedIndyDidRegex.test(did) +} + export function isUnqualifiedCredentialDefinitionId(credentialDefinitionId: string) { return unqualifiedCredentialDefinitionIdRegex.test(credentialDefinitionId) } @@ -198,3 +204,187 @@ export function parseIndyRevocationRegistryId(revocationRegistryId: string): Par throw new Error(`Invalid revocation registry id: ${revocationRegistryId}`) } + +export function getIndyNamespaceFromIndyDid(identifier: string): string { + let namespace: string | undefined + if (isDidIndySchemaId(identifier)) { + namespace = parseIndySchemaId(identifier).namespace + } else if (isDidIndyCredentialDefinitionId(identifier)) { + namespace = parseIndyCredentialDefinitionId(identifier).namespace + } else if (isDidIndyRevocationRegistryId(identifier)) { + namespace = parseIndyRevocationRegistryId(identifier).namespace + } else { + namespace = parseIndyDid(identifier).namespace + } + if (!namespace) throw new CredoError(`Cannot get indy namespace of identifier '${identifier}'`) + return namespace +} + +export function getUnQualifiedDidIndyDid(identifier: string): string { + if (isDidIndySchemaId(identifier)) { + const { schemaName, schemaVersion, namespaceIdentifier } = parseIndySchemaId(identifier) + return getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) + } else if (isDidIndyCredentialDefinitionId(identifier)) { + const { schemaSeqNo, tag, namespaceIdentifier } = parseIndyCredentialDefinitionId(identifier) + return getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, tag) + } else if (isDidIndyRevocationRegistryId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = + parseIndyRevocationRegistryId(identifier) + return getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + } + + const { namespaceIdentifier } = parseIndyDid(identifier) + return namespaceIdentifier +} + +export function isIndyDid(identifier: string): boolean { + return identifier.startsWith('did:indy:') +} + +export function getQualifiedDidIndyDid(identifier: string, namespace: string) { + if (isIndyDid(identifier)) return identifier + + if (!namespace || typeof namespace !== 'string') { + throw new CredoError('Missing required indy namespace') + } + + if (isUnqualifiedSchemaId(identifier)) { + const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(identifier) + const schemaId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/SCHEMA/${schemaName}/${schemaVersion}` + return schemaId + } else if (isUnqualifiedCredentialDefinitionId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(identifier) + const credentialDefinitionId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` + return credentialDefinitionId + } else if (isUnqualifiedRevocationRegistryId(identifier)) { + const { namespaceIdentifier, schemaSeqNo, revocationRegistryTag } = parseIndyRevocationRegistryId(identifier) + const revocationRegistryId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${revocationRegistryTag}` + return revocationRegistryId + } else if (isUnqualifiedIndyDid(identifier)) { + return `did:indy:${namespace}:${identifier}` + } else { + throw new CredoError(`Cannot created qualified indy identifier for '${identifier}' with namespace '${namespace}'`) + } +} + +// -- schema -- // + +export function isUnqualifiedDidIndySchema(schema: AnonCredsSchema) { + return isUnqualifiedIndyDid(schema.issuerId) +} + +export function getUnqualifiedDidIndySchema(schema: AnonCredsSchema): AnonCredsSchema { + if (isUnqualifiedDidIndySchema(schema)) return { ...schema } + if (!isIndyDid(schema.issuerId)) { + throw new CredoError(`IssuerId '${schema.issuerId}' is not a valid qualified did-indy did.`) + } + + const issuerId = getUnQualifiedDidIndyDid(schema.issuerId) + return { ...schema, issuerId } +} + +export function isQualifiedDidIndySchema(schema: AnonCredsSchema) { + return !isUnqualifiedIndyDid(schema.issuerId) +} + +export function getQualifiedDidIndySchema(schema: AnonCredsSchema, namespace: string): AnonCredsSchema { + if (isQualifiedDidIndySchema(schema)) return { ...schema } + + return { + ...schema, + issuerId: getQualifiedDidIndyDid(schema.issuerId, namespace), + } +} + +// -- credential definition -- // + +export function isUnqualifiedDidIndyCredentialDefinition(anonCredsCredentialDefinition: AnonCredsCredentialDefinition) { + return ( + isUnqualifiedIndyDid(anonCredsCredentialDefinition.issuerId) && + isUnqualifiedSchemaId(anonCredsCredentialDefinition.schemaId) + ) +} + +export function getUnqualifiedDidIndyCredentialDefinition( + anonCredsCredentialDefinition: AnonCredsCredentialDefinition +): AnonCredsCredentialDefinition { + if (isUnqualifiedDidIndyCredentialDefinition(anonCredsCredentialDefinition)) { + return { ...anonCredsCredentialDefinition } + } + + const issuerId = getUnQualifiedDidIndyDid(anonCredsCredentialDefinition.issuerId) + const schemaId = getUnQualifiedDidIndyDid(anonCredsCredentialDefinition.schemaId) + + return { ...anonCredsCredentialDefinition, issuerId, schemaId } +} + +export function isQualifiedDidIndyCredentialDefinition(anonCredsCredentialDefinition: AnonCredsCredentialDefinition) { + return ( + !isUnqualifiedIndyDid(anonCredsCredentialDefinition.issuerId) && + !isUnqualifiedSchemaId(anonCredsCredentialDefinition.schemaId) + ) +} + +export function getQualifiedDidIndyCredentialDefinition( + anonCredsCredentialDefinition: AnonCredsCredentialDefinition, + namespace: string +): AnonCredsCredentialDefinition { + if (isQualifiedDidIndyCredentialDefinition(anonCredsCredentialDefinition)) return { ...anonCredsCredentialDefinition } + + return { + ...anonCredsCredentialDefinition, + issuerId: getQualifiedDidIndyDid(anonCredsCredentialDefinition.issuerId, namespace), + schemaId: getQualifiedDidIndyDid(anonCredsCredentialDefinition.schemaId, namespace), + } +} + +// -- revocation registry definition -- // + +export function isUnqualifiedDidIndyRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +) { + return ( + isUnqualifiedIndyDid(revocationRegistryDefinition.issuerId) && + isUnqualifiedCredentialDefinitionId(revocationRegistryDefinition.credDefId) + ) +} + +export function getUnqualifiedDidIndyRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +): AnonCredsRevocationRegistryDefinition { + if (isUnqualifiedDidIndyRevocationRegistryDefinition(revocationRegistryDefinition)) { + return { ...revocationRegistryDefinition } + } + + const issuerId = getUnQualifiedDidIndyDid(revocationRegistryDefinition.issuerId) + const credDefId = getUnQualifiedDidIndyDid(revocationRegistryDefinition.credDefId) + + return { ...revocationRegistryDefinition, issuerId, credDefId } +} + +export function isQualifiedRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +) { + return ( + !isUnqualifiedIndyDid(revocationRegistryDefinition.issuerId) && + !isUnqualifiedCredentialDefinitionId(revocationRegistryDefinition.credDefId) + ) +} + +export function getQualifiedDidIndyRevocationRegistryDefinition( + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, + namespace: string +): AnonCredsRevocationRegistryDefinition { + if (isQualifiedRevocationRegistryDefinition(revocationRegistryDefinition)) return { ...revocationRegistryDefinition } + + return { + ...revocationRegistryDefinition, + issuerId: getQualifiedDidIndyDid(revocationRegistryDefinition.issuerId, namespace), + credDefId: getQualifiedDidIndyDid(revocationRegistryDefinition.credDefId, namespace), + } +} diff --git a/packages/anoncreds/src/utils/ledgerObjects.ts b/packages/anoncreds/src/utils/ledgerObjects.ts deleted file mode 100644 index c791890ef6..0000000000 --- a/packages/anoncreds/src/utils/ledgerObjects.ts +++ /dev/null @@ -1,328 +0,0 @@ -import type { - AnonCredsCredentialDefinition, - AnonCredsRevocationRegistryDefinition, - AnonCredsRevocationStatusList, - AnonCredsSchema, -} from '../models' -import type { AgentContext } from '@credo-ts/core' - -import { isDid, CredoError } from '@credo-ts/core' - -import { AnonCredsRegistryService } from '../services' - -import { - getUnqualifiedCredentialDefinitionId, - getUnqualifiedRevocationRegistryDefinitionId, - getUnqualifiedSchemaId, - isDidIndyCredentialDefinitionId, - isDidIndyRevocationRegistryId, - isDidIndySchemaId, - isUnqualifiedCredentialDefinitionId, - isUnqualifiedRevocationRegistryId, - isUnqualifiedSchemaId, - parseIndyCredentialDefinitionId, - parseIndyDid, - parseIndyRevocationRegistryId, - parseIndySchemaId, -} from './indyIdentifiers' - -type WithIds = input & { qualifiedId: string; id: string } - -type ReturnHelper = input extends string - ? WithIds - : input extends string | undefined - ? WithIds | undefined - : undefined - -export function getIndyNamespace(identifier: string): string { - if (!isIndyDid(identifier)) throw new CredoError(`Cannot get indy namespace of identifier '${identifier}'`) - if (isDidIndySchemaId(identifier)) { - const { namespace } = parseIndySchemaId(identifier) - if (!namespace) throw new CredoError(`Cannot get indy namespace of identifier '${identifier}'`) - return namespace - } else if (isDidIndyCredentialDefinitionId(identifier)) { - const { namespace } = parseIndyCredentialDefinitionId(identifier) - if (!namespace) throw new CredoError(`Cannot get indy namespace of identifier '${identifier}'`) - return namespace - } else if (isDidIndyRevocationRegistryId(identifier)) { - const { namespace } = parseIndyRevocationRegistryId(identifier) - if (!namespace) throw new CredoError(`Cannot get indy namespace of identifier '${identifier}'`) - return namespace - } - - const { namespace } = parseIndyDid(identifier) - return namespace -} - -export function getUnQualifiedId(identifier: string): string { - if (!isDid(identifier)) return identifier - if (!isIndyDid(identifier)) throw new CredoError(`Cannot get unqualified id of identifier '${identifier}'`) - - if (isDidIndySchemaId(identifier)) { - const { schemaName, schemaVersion, namespaceIdentifier } = parseIndySchemaId(identifier) - return getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) - } else if (isDidIndyCredentialDefinitionId(identifier)) { - const { schemaSeqNo, tag, namespaceIdentifier } = parseIndyCredentialDefinitionId(identifier) - return getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, tag) - } else if (isDidIndyRevocationRegistryId(identifier)) { - const { namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = - parseIndyRevocationRegistryId(identifier) - return getUnqualifiedRevocationRegistryDefinitionId( - namespaceIdentifier, - schemaSeqNo, - credentialDefinitionTag, - revocationRegistryTag - ) - } - - const { namespaceIdentifier } = parseIndyDid(identifier) - return namespaceIdentifier -} - -export function isIndyDid(identifier: string): boolean { - return identifier.startsWith('did:indy:') -} - -export function getQualifiedId(identifier: string, namespace: string) { - const isQualifiedDid = isDid(identifier) - if (isQualifiedDid) return identifier - - if (!namespace || typeof namespace !== 'string') { - throw new CredoError('Missing required indy namespace') - } - - if (isUnqualifiedSchemaId(identifier)) { - const { namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(identifier) - const schemaId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/SCHEMA/${schemaName}/${schemaVersion}` - return schemaId - } else if (isUnqualifiedCredentialDefinitionId(identifier)) { - const { namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(identifier) - const credentialDefinitionId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` - return credentialDefinitionId - } else if (isUnqualifiedRevocationRegistryId(identifier)) { - const { namespaceIdentifier, schemaSeqNo, revocationRegistryTag } = parseIndyRevocationRegistryId(identifier) - const revocationRegistryId = `did:indy:${namespace}:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${revocationRegistryTag}` - return revocationRegistryId - } - - return `did:indy:${namespace}:${identifier}` -} - -export function getUnqualifiedSchema(schema: AnonCredsSchema): AnonCredsSchema { - if (!isIndyDid(schema.issuerId)) return { ...schema } - const issuerId = getUnQualifiedId(schema.issuerId) - - return { ...schema, issuerId } -} - -export function isQualifiedSchema(schema: AnonCredsSchema) { - return isDid(schema.issuerId) -} - -export function getQualifiedSchema(schema: AnonCredsSchema, namespace: string): AnonCredsSchema { - if (isQualifiedSchema(schema)) return { ...schema } - - return { - ...schema, - issuerId: getQualifiedId(schema.issuerId, namespace), - } -} - -export async function fetchSchema( - agentContext: AgentContext, - schemaId: string -): Promise< - ReturnHelper< - string, - WithIds<{ schema: AnonCredsSchema; qualifiedSchema: AnonCredsSchema; unqualifiedSchema: AnonCredsSchema }> - > -> { - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - - const result = await registryService - .getRegistryForIdentifier(agentContext, schemaId) - .getSchema(agentContext, schemaId) - if (!result || !result.schema) { - throw new CredoError(`Schema not found for id ${schemaId}: ${result.resolutionMetadata.message}`) - } - - const indyNamespace = result.schemaMetadata.didIndyNamespace - - const schema = result.schema - const qualifiedSchema = getQualifiedSchema(schema, indyNamespace as string) - const unqualifiedSchema = getUnqualifiedSchema(schema) - - return { - schema, - id: schemaId, - qualifiedId: getQualifiedId(schemaId, indyNamespace as string), - qualifiedSchema, - unqualifiedSchema, - } -} - -export function getUnqualifiedCredentialDefinition( - anonCredsCredentialDefinition: AnonCredsCredentialDefinition -): AnonCredsCredentialDefinition { - if (!isIndyDid(anonCredsCredentialDefinition.issuerId) || !isIndyDid(anonCredsCredentialDefinition.schemaId)) { - return { ...anonCredsCredentialDefinition } - } - const issuerId = getUnQualifiedId(anonCredsCredentialDefinition.issuerId) - const schemaId = getUnQualifiedId(anonCredsCredentialDefinition.schemaId) - - return { ...anonCredsCredentialDefinition, issuerId, schemaId } -} - -export function isQualifiedCredentialDefinition(anonCredsCredentialDefinition: AnonCredsCredentialDefinition) { - return isDid(anonCredsCredentialDefinition.issuerId) && isDid(anonCredsCredentialDefinition.schemaId) -} - -export function getQualifiedCredentialDefinition( - anonCredsCredentialDefinition: AnonCredsCredentialDefinition, - namespace: string -): AnonCredsCredentialDefinition { - if (isQualifiedCredentialDefinition(anonCredsCredentialDefinition)) return { ...anonCredsCredentialDefinition } - - return { - ...anonCredsCredentialDefinition, - issuerId: getQualifiedId(anonCredsCredentialDefinition.issuerId, namespace), - schemaId: getQualifiedId(anonCredsCredentialDefinition.schemaId, namespace), - } -} - -export async function fetchCredentialDefinition( - agentContext: AgentContext, - credentialDefinitionId: string -): Promise< - ReturnHelper< - string, - WithIds<{ - credentialDefinition: AnonCredsCredentialDefinition - qualifiedCredentialDefinition: AnonCredsCredentialDefinition - unqualifiedCredentialDefinition: AnonCredsCredentialDefinition - }> - > -> { - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - - const result = await registryService - .getRegistryForIdentifier(agentContext, credentialDefinitionId) - .getCredentialDefinition(agentContext, credentialDefinitionId) - if (!result || !result.credentialDefinition) { - throw new CredoError(`Schema not found for id ${credentialDefinitionId}: ${result.resolutionMetadata.message}`) - } - - const indyNamespace = result.credentialDefinitionMetadata.didIndyNamespace - - const credentialDefinition = result.credentialDefinition - const qualifiedCredentialDefinition = getQualifiedCredentialDefinition(credentialDefinition, indyNamespace as string) - const unqualifiedCredentialDefinition = getUnqualifiedCredentialDefinition(credentialDefinition) - - return { - credentialDefinition, - id: credentialDefinitionId, - qualifiedId: getQualifiedId(credentialDefinitionId, indyNamespace as string), - qualifiedCredentialDefinition, - unqualifiedCredentialDefinition, - } -} - -export function getUnqualifiedRevocationRegistryDefinition( - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition -): AnonCredsRevocationRegistryDefinition { - if (!isIndyDid(revocationRegistryDefinition.issuerId) || !isIndyDid(revocationRegistryDefinition.credDefId)) { - return { ...revocationRegistryDefinition } - } - - const issuerId = getUnQualifiedId(revocationRegistryDefinition.issuerId) - const credDefId = getUnQualifiedId(revocationRegistryDefinition.credDefId) - - return { ...revocationRegistryDefinition, issuerId, credDefId } -} - -export function isQualifiedRevocationRegistryDefinition( - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition -) { - return isDid(revocationRegistryDefinition.issuerId) && isDid(revocationRegistryDefinition.credDefId) -} - -export function getQualifiedRevocationRegistryDefinition( - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, - namespace: string -): AnonCredsRevocationRegistryDefinition { - if (isQualifiedRevocationRegistryDefinition(revocationRegistryDefinition)) return { ...revocationRegistryDefinition } - - return { - ...revocationRegistryDefinition, - issuerId: getQualifiedId(revocationRegistryDefinition.issuerId, namespace), - credDefId: getQualifiedId(revocationRegistryDefinition.credDefId, namespace), - } -} - -export async function fetchRevocationRegistryDefinition( - agentContext: AgentContext, - revocationRegistryDefinitionId: string -): Promise< - ReturnHelper< - string, - WithIds<{ - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition - qualifiedRevocationRegistryDefinition: AnonCredsRevocationRegistryDefinition - unqualifiedRevocationRegistryDefinition: AnonCredsRevocationRegistryDefinition - }> - > -> { - const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) - - const result = await registryService - .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) - .getRevocationRegistryDefinition(agentContext, revocationRegistryDefinitionId) - if (!result || !result.revocationRegistryDefinition) { - throw new CredoError( - `RevocationRegistryDefinition not found for id ${revocationRegistryDefinitionId}: ${result.resolutionMetadata.message}` - ) - } - - const indyNamespace = result.revocationRegistryDefinitionMetadata.didIndyNamespace - - const revocationRegistryDefinition = result.revocationRegistryDefinition - const qualifiedRevocationRegistryDefinition = getQualifiedRevocationRegistryDefinition( - revocationRegistryDefinition, - indyNamespace as string - ) - const unqualifiedRevocationRegistryDefinition = getUnqualifiedRevocationRegistryDefinition( - qualifiedRevocationRegistryDefinition - ) - - return { - revocationRegistryDefinition, - id: revocationRegistryDefinitionId, - qualifiedId: getQualifiedId(revocationRegistryDefinitionId, indyNamespace as string), - qualifiedRevocationRegistryDefinition, - unqualifiedRevocationRegistryDefinition, - } -} - -export async function fetchRevocationStatusList( - agentContext: AgentContext, - revocationRegistryId: string, - timestamp: number -): Promise<{ revocationStatusList: AnonCredsRevocationStatusList }> { - const registry = agentContext.dependencyManager - .resolve(AnonCredsRegistryService) - .getRegistryForIdentifier(agentContext, revocationRegistryId) - - const { revocationStatusList, resolutionMetadata } = await registry.getRevocationStatusList( - agentContext, - revocationRegistryId, - timestamp - ) - - if (!revocationStatusList) { - throw new CredoError( - `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` - ) - } - - return { revocationStatusList } -} diff --git a/packages/anoncreds/src/utils/metadata.ts b/packages/anoncreds/src/utils/metadata.ts index c8c1c245fc..56a24593da 100644 --- a/packages/anoncreds/src/utils/metadata.ts +++ b/packages/anoncreds/src/utils/metadata.ts @@ -1,3 +1,25 @@ +import type { AnonCredsLinkSecretBlindingData } from '../models' + +export interface AnonCredsCredentialMetadata { + schemaId?: string + credentialDefinitionId?: string + revocationRegistryId?: string + credentialRevocationId?: string +} + +export interface AnonCredsCredentialRequestMetadata { + link_secret_blinding_data: AnonCredsLinkSecretBlindingData + link_secret_name: string + nonce: string +} + +export interface W3cAnoncredsCredentialMetadata { + credentialId: string + methodName: string + credentialRevocationId?: string + linkSecretId: string +} + // 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 @@ -16,14 +38,8 @@ export const AnonCredsCredentialMetadataKey = '_anoncreds/credential' export const AnonCredsCredentialRequestMetadataKey = '_anoncreds/credentialRequest' /** - * Metadata for an AnonCreds credential that will be stored - * in the credential record. + * Metadata key for storing the W3C AnonCreds credential metadata. * - * MUST be used with {@link AnonCredsCredentialMetadataKey} + * MUST be used with {@link W3cAnoncredsCredentialMetadata} */ -export interface AnonCredsCredentialMetadata { - schemaId?: string - credentialDefinitionId?: string - revocationRegistryId?: string - credentialRevocationId?: string -} +export const W3cAnonCredsCredentialMetadataKey = '_w3c/AnonCredsMetadata' diff --git a/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts b/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts new file mode 100644 index 0000000000..076e96b6cc --- /dev/null +++ b/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts @@ -0,0 +1,201 @@ +import type { AnonCredsClaimRecord } from './credential' +import type { W3cAnoncredsCredentialMetadata } from './metadata' +import type { AnonCredsCredentialInfo, AnonCredsSchema } from '../models' +import type { AnonCredsCredentialRecord } from '../repository' +import type { StoreCredentialOptions } from '../services' +import type { AnonCredsCredentialTags } from '@credo-ts/core' + +import { CredoError, W3cCredentialRecord, utils } from '@credo-ts/core' + +import { mapAttributeRawValuesToAnonCredsCredentialValues } from './credential' +import { + getQualifiedDidIndyCredentialDefinition, + getQualifiedDidIndyDid, + getQualifiedDidIndyRevocationRegistryDefinition, + getQualifiedDidIndySchema, + isUnqualifiedDidIndyCredentialDefinition, + isUnqualifiedDidIndyRevocationRegistryDefinition, + isUnqualifiedDidIndySchema, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedRevocationRegistryId, + isIndyDid, + getUnQualifiedDidIndyDid, +} from './indyIdentifiers' +import { W3cAnonCredsCredentialMetadataKey } from './metadata' + +function anoncredsCredentialInfoFromW3cRecord(w3cCredentialRecord: W3cCredentialRecord): AnonCredsCredentialInfo { + if (Array.isArray(w3cCredentialRecord.credential.credentialSubject)) { + throw new CredoError('Credential subject must be an object, not an array.') + } + + const anonCredsTags = getAnonCredsTagsFromRecord(w3cCredentialRecord) + if (!anonCredsTags) throw new CredoError('AnonCreds tags not found on credential record.') + + const anoncredsCredentialMetadata = w3cCredentialRecord.metadata.get( + W3cAnonCredsCredentialMetadataKey + ) + if (!anoncredsCredentialMetadata) throw new CredoError('AnonCreds metadata not found on credential record.') + + return { + attributes: (w3cCredentialRecord.credential.credentialSubject.claims as AnonCredsClaimRecord) ?? {}, + credentialId: anoncredsCredentialMetadata.credentialId, + credentialDefinitionId: anonCredsTags.anonCredsCredentialDefinitionId, + schemaId: anonCredsTags.anonCredsSchemaId, + credentialRevocationId: anoncredsCredentialMetadata.credentialRevocationId ?? null, + revocationRegistryId: anonCredsTags.anonCredsRevocationRegistryId ?? null, + methodName: anoncredsCredentialMetadata.methodName, + linkSecretId: anoncredsCredentialMetadata.linkSecretId, + } +} + +function anoncredsCredentialInfoFromAnoncredsRecord( + anonCredsCredentialRecord: AnonCredsCredentialRecord +): AnonCredsCredentialInfo { + const attributes: { [key: string]: string } = {} + for (const attribute in anonCredsCredentialRecord.credential) { + attributes[attribute] = anonCredsCredentialRecord.credential.values[attribute].raw + } + + return { + attributes, + credentialDefinitionId: anonCredsCredentialRecord.credential.cred_def_id, + credentialId: anonCredsCredentialRecord.credentialId, + schemaId: anonCredsCredentialRecord.credential.schema_id, + credentialRevocationId: anonCredsCredentialRecord.credentialRevocationId ?? null, + revocationRegistryId: anonCredsCredentialRecord.credential.rev_reg_id ?? null, + methodName: anonCredsCredentialRecord.methodName, + linkSecretId: anonCredsCredentialRecord.linkSecretId, + } +} + +export function getAnoncredsCredentialInfoFromRecord( + credentialRecord: W3cCredentialRecord | AnonCredsCredentialRecord +): AnonCredsCredentialInfo { + if (credentialRecord instanceof W3cCredentialRecord) { + return anoncredsCredentialInfoFromW3cRecord(credentialRecord) + } else { + return anoncredsCredentialInfoFromAnoncredsRecord(credentialRecord) + } +} +export function getAnonCredsTagsFromRecord(record: W3cCredentialRecord) { + const anoncredsMetadata = record.metadata.get(W3cAnonCredsCredentialMetadataKey) + if (!anoncredsMetadata) return undefined + + const tags = record.getTags() + if ( + !tags.anonCredsCredentialId || + !tags.anonCredsLinkSecretId || + !tags.anonCredsMethodName || + !tags.anonCredsSchemaId || + !tags.anonCredsSchemaName || + !tags.anonCredsSchemaVersion || + !tags.anonCredsSchemaIssuerId || + !tags.anonCredsCredentialDefinitionId + ) { + return undefined + } + + return Object.fromEntries( + Object.entries(tags).filter(([key]) => key.startsWith('anonCreds')) + ) as AnonCredsCredentialTags +} + +export function getStoreCredentialOptions( + options: StoreCredentialOptions, + indyNamespace?: string +): StoreCredentialOptions { + const { + credentialRequestMetadata, + credentialDefinitionId, + schema, + credential, + credentialDefinition, + revocationRegistry, + } = options + + const storeCredentialOptions = { + credentialId: utils.uuid(), + credentialRequestMetadata, + credential, + credentialDefinitionId: isUnqualifiedCredentialDefinitionId(credentialDefinitionId) + ? getQualifiedDidIndyDid(credentialDefinitionId, indyNamespace as string) + : credentialDefinitionId, + credentialDefinition: isUnqualifiedDidIndyCredentialDefinition(credentialDefinition) + ? getQualifiedDidIndyCredentialDefinition(credentialDefinition, indyNamespace as string) + : credentialDefinition, + schema: isUnqualifiedDidIndySchema(schema) ? getQualifiedDidIndySchema(schema, indyNamespace as string) : schema, + revocationRegistry: revocationRegistry?.definition + ? { + definition: isUnqualifiedDidIndyRevocationRegistryDefinition(revocationRegistry.definition) + ? getQualifiedDidIndyRevocationRegistryDefinition(revocationRegistry.definition, indyNamespace as string) + : revocationRegistry.definition, + id: isUnqualifiedRevocationRegistryId(revocationRegistry.id) + ? getQualifiedDidIndyDid(revocationRegistry.id, indyNamespace as string) + : revocationRegistry.id, + } + : undefined, + } + + return storeCredentialOptions +} + +export function getW3cRecordAnonCredsTags(options: { + w3cCredentialRecord: W3cCredentialRecord + schemaId: string + schema: Omit + credentialDefinitionId: string + revocationRegistryId?: string + credentialRevocationId?: string + linkSecretId: string + methodName: string +}) { + const { + w3cCredentialRecord, + schema, + schemaId, + credentialDefinitionId, + revocationRegistryId, + credentialRevocationId, + linkSecretId, + methodName, + } = options + + const issuerId = w3cCredentialRecord.credential.issuerId + + const anonCredsCredentialRecordTags: AnonCredsCredentialTags = { + anonCredsCredentialId: w3cCredentialRecord.id, + anonCredsLinkSecretId: linkSecretId, + anonCredsCredentialDefinitionId: credentialDefinitionId, + anonCredsSchemaId: schemaId, + anonCredsSchemaName: schema.name, + anonCredsSchemaIssuerId: schema.issuerId, + anonCredsSchemaVersion: schema.version, + anonCredsMethodName: methodName, + anonCredsRevocationRegistryId: revocationRegistryId, + anonCredsCredentialRevocationId: credentialRevocationId, + ...(isIndyDid(issuerId) && { + anonCredsUnqualifiedIssuerId: getUnQualifiedDidIndyDid(issuerId), + anonCredsUnqualifiedCredentialDefinitionId: getUnQualifiedDidIndyDid(credentialDefinitionId), + anonCredsUnqualifiedSchemaId: getUnQualifiedDidIndyDid(schemaId), + anonCredsUnqualifiedSchemaIssuerId: getUnQualifiedDidIndyDid(schema.issuerId), + anonCredsUnqualifiedRevocationRegistryId: revocationRegistryId + ? getUnQualifiedDidIndyDid(revocationRegistryId) + : undefined, + }), + } + + if (Array.isArray(w3cCredentialRecord.credential.credentialSubject)) { + throw new CredoError('Credential subject must be an object, not an array.') + } + + const values = mapAttributeRawValuesToAnonCredsCredentialValues( + (w3cCredentialRecord.credential.credentialSubject.claims as AnonCredsClaimRecord) ?? {} + ) + + for (const [key, value] of Object.entries(values)) { + anonCredsCredentialRecordTags[`anonCredsAttr::${key}::value`] = value.raw + anonCredsCredentialRecordTags[`anonCredsAttr::${key}::marker`] = true + } + + return anonCredsCredentialRecordTags +} diff --git a/packages/anoncreds/src/utils/w3cUtils.ts b/packages/anoncreds/src/utils/w3cUtils.ts deleted file mode 100644 index 689d0f849f..0000000000 --- a/packages/anoncreds/src/utils/w3cUtils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { AnonCredsCredential } from '../models' -import type { ProcessCredentialOptions } from '@hyperledger/anoncreds-shared' - -import { JsonTransformer, W3cJsonLdVerifiableCredential, type JsonObject } from '@credo-ts/core' -import { Credential, W3cCredential } from '@hyperledger/anoncreds-shared' - -export async function legacyCredentialToW3cCredential( - legacyCredential: AnonCredsCredential, - qualifiedIssuerId: string, - process?: ProcessCredentialOptions -) { - let credential: W3cJsonLdVerifiableCredential - let anonCredsCredential: Credential | undefined - let w3cCredentialObj: W3cCredential | undefined - let processed: W3cCredential | undefined - - try { - anonCredsCredential = Credential.fromJson(legacyCredential as unknown as JsonObject) - w3cCredentialObj = anonCredsCredential.toW3c({ - issuerId: qualifiedIssuerId, - w3cVersion: '1.1', - }) - - const jsonObject = process ? w3cCredentialObj.process(process).toJson() : w3cCredentialObj.toJson() - - credential = JsonTransformer.fromJSON(jsonObject, W3cJsonLdVerifiableCredential) - } finally { - anonCredsCredential?.handle?.clear() - w3cCredentialObj?.handle?.clear() - processed?.handle?.clear() - } - - return credential -} - -export function w3cToLegacyCredential(credential: W3cJsonLdVerifiableCredential) { - const credentialJson = JsonTransformer.toJSON(credential) - const w3cCredentialObj = W3cCredential.fromJson(credentialJson) - const legacyCredential = w3cCredentialObj.toLegacy().toJson() as unknown as AnonCredsCredential - return legacyCredential -} diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index a156cd3dc8..77e40ad3fa 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -19,7 +19,7 @@ import type { } from '../src' import type { AgentContext } from '@credo-ts/core' -import { Hasher, isDid } from '@credo-ts/core' +import { Hasher, utils } from '@credo-ts/core' import BigNumber from 'bn.js' import { @@ -27,19 +27,24 @@ import { getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, } from '../../indy-vdr/src/anoncreds/utils/identifiers' +import {} from '../src' import { + getQualifiedDidIndyDid, + getUnQualifiedDidIndyDid, getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedCredentialDefinitionId, getUnqualifiedSchemaId, parseIndyDid, - getUnqualifiedSchema, -} from '../src' -import { + getUnqualifiedDidIndySchema, parseIndyCredentialDefinitionId, parseIndyRevocationRegistryId, parseIndySchemaId, + isIndyDid, + isUnqualifiedCredentialDefinitionId, + isUnqualifiedSchemaId, + isUnqualifiedRevocationRegistryId, + isUnqualifiedIndyDid, } from '../src/utils/indyIdentifiers' -import { getQualifiedId, getUnQualifiedId } from '../src/utils/ledgerObjects' import { dateToTimestamp } from '../src/utils/timestamp' /** @@ -90,14 +95,14 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } - let didIndyNamespace - if (isDid(schemaId)) { - didIndyNamespace = parseIndySchemaId(schemaId).namespace - } else { - const qSchemaIdEnd = getQualifiedId(schemaId, 'mock').split('mock:')[1] + let didIndyNamespace: string | undefined = undefined + if (isUnqualifiedSchemaId(schemaId)) { + const qSchemaIdEnd = getQualifiedDidIndyDid(schemaId, 'mock').split('mock:')[1] const qSchemaId = Object.keys(this.schemas).find((schemaId) => schemaId.endsWith(qSchemaIdEnd)) if (!qSchemaId) didIndyNamespace = undefined else didIndyNamespace = parseIndySchemaId(qSchemaId).namespace + } else if (isIndyDid(schemaId)) { + didIndyNamespace = parseIndySchemaId(schemaId).namespace } return { @@ -117,19 +122,25 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { _agentContext: AgentContext, options: RegisterSchemaOptions ): Promise { - const { namespace, namespaceIdentifier } = parseIndyDid(options.schema.issuerId) - const didIndySchemaId = getDidIndySchemaId( - namespace, - namespaceIdentifier, - options.schema.name, - options.schema.version - ) - this.schemas[didIndySchemaId] = options.schema + const issuerId = options.schema.issuerId - const legacySchemaId = getUnQualifiedId(didIndySchemaId) - const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) + let schemaId: string + let indyLedgerSeqNo: number | undefined + if (isIndyDid(issuerId) || isUnqualifiedIndyDid(issuerId)) { + const { namespace, namespaceIdentifier } = parseIndyDid(issuerId) + schemaId = getDidIndySchemaId(namespace, namespaceIdentifier, options.schema.name, options.schema.version) + + const legacySchemaId = getUnQualifiedDidIndyDid(schemaId) + this.schemas[legacySchemaId] = getUnqualifiedDidIndySchema(options.schema) - this.schemas[legacySchemaId] = getUnqualifiedSchema(options.schema) + indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) + } else if (issuerId.startsWith('did:cheqd:')) { + schemaId = issuerId + '/resources/' + utils.uuid() + } else { + throw new Error(`Cannot register Schema. Unsupported issuerId '${issuerId}'`) + } + + this.schemas[schemaId] = options.schema return { registrationMetadata: {}, @@ -141,7 +152,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { schemaState: { state: 'finished', schema: options.schema, - schemaId: didIndySchemaId, + schemaId: schemaId, }, } } @@ -163,16 +174,16 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } - let didIndyNamespace - if (isDid(credentialDefinitionId)) { - didIndyNamespace = parseIndyCredentialDefinitionId(credentialDefinitionId).namespace - } else { - const qCredDefEnd = getQualifiedId(credentialDefinitionId, 'mock').split('mock:')[1] + let didIndyNamespace: string | undefined = undefined + if (isUnqualifiedCredentialDefinitionId(credentialDefinitionId)) { + const qCredDefEnd = getQualifiedDidIndyDid(credentialDefinitionId, 'mock').split('mock:')[1] const qCredDefId = Object.keys(this.credentialDefinitions).find((credentialDefinitionid) => credentialDefinitionid.endsWith(qCredDefEnd) ) if (!qCredDefId) didIndyNamespace = undefined else didIndyNamespace = parseIndyCredentialDefinitionId(qCredDefId).namespace + } else if (isIndyDid(credentialDefinitionId)) { + didIndyNamespace = parseIndyCredentialDefinitionId(credentialDefinitionId).namespace } return { @@ -187,44 +198,53 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { _agentContext: AgentContext, options: RegisterCredentialDefinitionOptions ): Promise { - const parsedSchema = parseIndySchemaId(options.credentialDefinition.schemaId) - const legacySchemaId = getUnqualifiedSchemaId( - parsedSchema.namespaceIdentifier, - parsedSchema.schemaName, - parsedSchema.schemaVersion - ) - const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) - - const { namespace, namespaceIdentifier } = parseIndyDid(options.credentialDefinition.issuerId) - const legacyIssuerId = namespaceIdentifier - const didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( - namespace, - namespaceIdentifier, - indyLedgerSeqNo, - options.credentialDefinition.tag - ) + const schemaId = options.credentialDefinition.schemaId + + let credentialDefinitionId: string + if (isIndyDid(schemaId) || isUnqualifiedSchemaId(schemaId)) { + const parsedSchema = parseIndySchemaId(options.credentialDefinition.schemaId) + const legacySchemaId = getUnqualifiedSchemaId( + parsedSchema.namespaceIdentifier, + parsedSchema.schemaName, + parsedSchema.schemaVersion + ) + const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) - this.credentialDefinitions[didIndyCredentialDefinitionId] = options.credentialDefinition + const { namespace, namespaceIdentifier } = parseIndyDid(options.credentialDefinition.issuerId) + const legacyIssuerId = namespaceIdentifier + const didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + namespaceIdentifier, + indyLedgerSeqNo, + options.credentialDefinition.tag + ) - const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( - legacyIssuerId, - indyLedgerSeqNo, - options.credentialDefinition.tag - ) + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + legacyIssuerId, + indyLedgerSeqNo, + options.credentialDefinition.tag + ) - this.credentialDefinitions[legacyCredentialDefinitionId] = { - ...options.credentialDefinition, - issuerId: legacyIssuerId, - schemaId: legacySchemaId, + this.credentialDefinitions[legacyCredentialDefinitionId] = { + ...options.credentialDefinition, + issuerId: legacyIssuerId, + schemaId: legacySchemaId, + } + credentialDefinitionId = didIndyCredentialDefinitionId + } else if (schemaId.startsWith('did:cheqd:')) { + credentialDefinitionId = options.credentialDefinition.issuerId + '/resources/' + utils.uuid() + } else { + throw new Error(`Cannot register Credential Definition. Unsupported schemaId '${schemaId}'`) } + this.credentialDefinitions[credentialDefinitionId] = options.credentialDefinition return { registrationMetadata: {}, credentialDefinitionMetadata: {}, credentialDefinitionState: { state: 'finished', credentialDefinition: options.credentialDefinition, - credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinitionId, }, } } @@ -246,16 +266,16 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } - let didIndyNamespace - if (isDid(revocationRegistryDefinitionId)) { - didIndyNamespace = parseIndyRevocationRegistryId(revocationRegistryDefinitionId).namespace - } else { - const qRevRegIdEnd = getQualifiedId(revocationRegistryDefinitionId, 'mock').split('mock:')[1] + let didIndyNamespace: string | undefined + if (isUnqualifiedRevocationRegistryId(revocationRegistryDefinitionId)) { + const qRevRegIdEnd = getQualifiedDidIndyDid(revocationRegistryDefinitionId, 'mock').split('mock:')[1] const qRevRegId = Object.keys(this.revocationRegistryDefinitions).find((revRegId) => revRegId.endsWith(qRevRegIdEnd) ) if (!qRevRegId) didIndyNamespace = undefined else didIndyNamespace = parseIndyRevocationRegistryId(qRevRegId).namespace + } else if (isIndyDid(revocationRegistryDefinitionId)) { + didIndyNamespace = parseIndyRevocationRegistryId(revocationRegistryDefinitionId).namespace } return { diff --git a/packages/anoncreds/tests/anoncreds-flow.test.ts b/packages/anoncreds/tests/anoncreds-flow.test.ts index a3fa629850..fa3ed98bc9 100644 --- a/packages/anoncreds/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds/tests/anoncreds-flow.test.ts @@ -5,7 +5,6 @@ import { InjectionSymbols, ProofState, ProofExchangeRecord, - ConsoleLogger, CredentialExchangeRecord, CredentialPreviewAttribute, CredentialState, @@ -24,7 +23,7 @@ import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { agentDependencies, getAgentConfig, getAgentContext, testLogger } from '../../core/tests' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../src/anoncreds-rs' import { InMemoryTailsFileService } from './InMemoryTailsFileService' @@ -71,8 +70,6 @@ const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123' const inMemoryStorageService = new InMemoryStorageService() -const logger = new ConsoleLogger() - const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], @@ -82,8 +79,8 @@ const agentContext = getAgentContext({ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], - [InjectionSymbols.Logger, logger], - [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], + [InjectionSymbols.Logger, testLogger], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig())], [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], @@ -381,6 +378,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean age: 25, name: 'John', }, + linkSecretId: 'linkSecretId', schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts index a7aed5d2ba..6cdc60ea0d 100644 --- a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts @@ -15,7 +15,6 @@ import { ProofExchangeRecord, ProofState, SignatureSuiteToken, - SigningProviderRegistry, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredential, @@ -26,13 +25,14 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../tests/InMemoryWallet' import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { agentDependencies, getAgentConfig, getAgentContext, testLogger } from '../../core/tests' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../src/anoncreds-rs' +import { getAnonCredsTagsFromRecord } from '../src/utils/w3cAnonCredsUtils' import { InMemoryTailsFileService } from './InMemoryTailsFileService' import { anoncreds } from './helpers' @@ -75,19 +75,13 @@ const anonCredsIssuerService = new AnonCredsRsIssuerService() const inMemoryStorageService = new InMemoryStorageService() -const logger = agentConfig.logger - const didsModuleConfig = new DidsModuleConfig({ registrars: [new KeyDidRegistrar()], resolvers: [new KeyDidResolver()], }) const fileSystem = new agentDependencies.FileSystem() -const wallet = new RegisteredAskarTestWallet( - agentConfig.logger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([]) -) +const wallet = new InMemoryWallet() const agentContext = getAgentContext({ registerInstances: [ @@ -98,10 +92,9 @@ const agentContext = getAgentContext({ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], - [InjectionSymbols.Logger, logger], - [InjectionSymbols.Logger, logger], + [InjectionSymbols.Logger, testLogger], [DidsModuleConfig, didsModuleConfig], - [DidResolverService, new DidResolverService(logger, didsModuleConfig)], + [DidResolverService, new DidResolverService(testLogger, didsModuleConfig)], [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], @@ -341,12 +334,12 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean dataIntegrity: { bindingRequired: true, credential, - anonCredsLinkSecretBindingMethodOptions: { + anonCredsLinkSecretBinding: { credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryDefinitionId, revocationRegistryIndex: revocable ? 1 : undefined, }, - didCommSignedAttachmentBindingMethodOptions: {}, + didCommSignedAttachmentBinding: {}, }, }, }) @@ -363,7 +356,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean credentialFormats: { dataIntegrity: { dataModelVersion: '1.1', - anonCredsLinkSecretAcceptOfferOptions: { + anonCredsLinkSecret: { linkSecretId: linkSecret.linkSecretId, }, }, @@ -404,7 +397,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) - const credentialId = credentialRecord.getAnonCredsTags()?.credentialId + const credentialId = getAnonCredsTagsFromRecord(credentialRecord)?.anonCredsCredentialId if (!credentialId) throw new Error('Credential ID not found') const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { @@ -417,6 +410,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean age: 25, name: 'John', }, + linkSecretId: 'linkSecretId', schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, @@ -426,32 +420,26 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean const expectedCredentialMetadata = revocable ? { - linkSecretMetadata: { - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: revocationRegistryDefinitionId, - credentialRevocationId: '1', - }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocationRegistryDefinitionId, + credentialRevocationId: '1', } : { - linkSecretMetadata: { - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, } expect(holderCredentialRecord.metadata.data).toEqual({ - '_dataIntegrity/credential': expectedCredentialMetadata, - '_dataIntegrity/credentialRequest': { - linkSecretRequestMetadata: { - link_secret_blinding_data: expect.any(Object), - link_secret_name: expect.any(String), - nonce: expect.any(String), - }, + '_anoncreds/credential': expectedCredentialMetadata, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), }, }) expect(issuerCredentialRecord.metadata.data).toEqual({ - '_dataIntegrity/credential': expectedCredentialMetadata, + '_anoncreds/credential': expectedCredentialMetadata, }) const holderProofRecord = new ProofExchangeRecord({ diff --git a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts index 24a6b022de..f182c89fde 100644 --- a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts @@ -1,13 +1,11 @@ -import type { KeyDidCreateOptions } from '@credo-ts/core' +import type { CreateDidKidVerificationMethodReturn } from '../../core/tests' import { AgentContext, CredentialExchangeRecord, CredentialPreviewAttribute, CredentialState, - DidKey, DidResolverService, - DidsApi, DidsModuleConfig, Ed25519Signature2018, InjectionSymbols, @@ -15,8 +13,6 @@ import { KeyDidResolver, KeyType, SignatureSuiteToken, - SigningProviderRegistry, - TypedArrayEncoder, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredential, @@ -27,11 +23,17 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../tests/InMemoryWallet' import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { + agentDependencies, + getAgentConfig, + getAgentContext, + testLogger, + createDidKidVerificationMethod, +} from '../../core/tests' import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, @@ -58,19 +60,13 @@ const anonCredsIssuerService = new AnonCredsRsIssuerService() const inMemoryStorageService = new InMemoryStorageService() -const logger = agentConfig.logger - const didsModuleConfig = new DidsModuleConfig({ registrars: [new KeyDidRegistrar()], resolvers: [new KeyDidResolver()], }) const fileSystem = new agentDependencies.FileSystem() -const wallet = new RegisteredAskarTestWallet( - agentConfig.logger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([]) -) +const wallet = new InMemoryWallet() const agentContext = getAgentContext({ registerInstances: [ @@ -81,9 +77,9 @@ const agentContext = getAgentContext({ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], - [InjectionSymbols.Logger, logger], + [InjectionSymbols.Logger, testLogger], [DidsModuleConfig, didsModuleConfig], - [DidResolverService, new DidResolverService(logger, didsModuleConfig)], + [DidResolverService, new DidResolverService(testLogger, didsModuleConfig)], [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], @@ -108,17 +104,15 @@ agentContext.dependencyManager.registerInstance(AgentContext, agentContext) const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() -const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' - describe('data integrity format service (w3c)', () => { - let issuer: Awaited> - let holder: Awaited> + let issuerKdv: CreateDidKidVerificationMethodReturn + let holderKdv: CreateDidKidVerificationMethodReturn beforeAll(async () => { await wallet.createAndOpen(agentConfig.walletConfig) - issuer = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') - holder = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') + issuerKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') + holderKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') }) afterEach(async () => { @@ -126,168 +120,127 @@ describe('data integrity format service (w3c)', () => { }) test('issuance and verification flow w3c starting from offer without negotiation and without revocation', async () => { - await anonCredsFlowTest({ issuerId: indyDid, revocable: false, issuer, holder }) - }) -}) - -export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey: string) { - const dids = agentContext.dependencyManager.resolve(DidsApi) - const didCreateResult = await dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString(secretKey) }, - }) - - const did = didCreateResult.didState.did as string - const didKey = DidKey.fromDid(did) - const kid = `${did}#${didKey.key.fingerprint}` - - const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - return { - did, - kid, - verificationMethod, - } -} - -async function anonCredsFlowTest(options: { - issuerId: string - revocable: boolean - issuer: Awaited> - holder: Awaited> -}) { - const { issuer, holder } = options - - const holderCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalSent, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', - }) + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) - const issuerCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalReceived, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', - }) + const 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' }), - ] + const credentialAttributes = [ + new CredentialPreviewAttribute({ name: 'name', value: 'John' }), + new CredentialPreviewAttribute({ name: 'age', value: '25' }), + ] - // Set attributes on the credential record, this is normally done by the protocol service - holderCredentialRecord.credentialAttributes = credentialAttributes - issuerCredentialRecord.credentialAttributes = credentialAttributes + // Set attributes on the credential record, this is normally done by the protocol service + holderCredentialRecord.credentialAttributes = credentialAttributes + issuerCredentialRecord.credentialAttributes = credentialAttributes - // -------------------------------------------------------------------------------------------------------- + // -------------------------------------------------------------------------------------------------------- - const credential = new W3cCredential({ - context: [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/security/data-integrity/v2', - { - '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', - }, - ], - type: ['VerifiableCredential'], - issuer: issuer.did, - issuanceDate: new Date().toISOString(), - credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: 25 } }), - }) - - const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { - credentialRecord: issuerCredentialRecord, - credentialFormats: { - dataIntegrity: { - bindingRequired: true, - credential, - didCommSignedAttachmentBindingMethodOptions: {}, - }, - }, - }) + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerKdv.did, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ claims: { name: 'John', age: 25 } }), + }) - // Holder processes and accepts offer - await dataIntegrityCredentialFormatService.processOffer(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: offerAttachment, - }) - const { attachment: requestAttachment, appendAttachments: requestAppendAttachments } = - await dataIntegrityCredentialFormatService.acceptOffer(agentContext, { - credentialRecord: holderCredentialRecord, - offerAttachment, + const { attachment: offerAttachment } = await dataIntegrityCredentialFormatService.createOffer(agentContext, { + credentialRecord: issuerCredentialRecord, credentialFormats: { dataIntegrity: { - didCommSignedAttachmentAcceptOfferOptions: { - kid: holder.kid, - }, + bindingRequired: true, + credential, + didCommSignedAttachmentBinding: {}, }, }, }) - // Issuer processes and accepts request - await dataIntegrityCredentialFormatService.processRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: requestAttachment, - }) - const { attachment: credentialAttachment } = await dataIntegrityCredentialFormatService.acceptRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - requestAttachment, - offerAttachment, - requestAppendAttachments, - credentialFormats: { - dataIntegrity: { - credentialSubjectId: issuer.did, - didCommSignedAttachmentAcceptRequestOptions: { - kid: issuer.kid, + // Holder processes and accepts offer + await dataIntegrityCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment, appendAttachments: requestAppendAttachments } = + await dataIntegrityCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + dataIntegrity: { + didCommSignedAttachment: { + kid: holderKdv.kid, + }, + }, }, - }, - }, - }) - - // Holder processes and accepts credential - await dataIntegrityCredentialFormatService.processCredential(agentContext, { - offerAttachment, - credentialRecord: holderCredentialRecord, - attachment: credentialAttachment, - requestAttachment, - }) - - expect(holderCredentialRecord.credentials).toEqual([ - { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, - ]) + }) - await expect( - anonCredsHolderService.getCredential(agentContext, { - credentialId: holderCredentialRecord.id, + // Issuer processes and accepts request + await dataIntegrityCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, }) - ).rejects.toThrow() - - const expectedCredentialMetadata = {} - expect(holderCredentialRecord.metadata.data).toEqual({ - '_dataIntegrity/credential': expectedCredentialMetadata, - '_dataIntegrity/credentialRequest': {}, - }) - - expect(issuerCredentialRecord.metadata.data).toEqual({ - '_dataIntegrity/credential': expectedCredentialMetadata, - }) + const { attachment: credentialAttachment } = await dataIntegrityCredentialFormatService.acceptRequest( + agentContext, + { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + requestAppendAttachments, + credentialFormats: { + dataIntegrity: { + credentialSubjectId: issuerKdv.did, + issuerVerificationMethod: issuerKdv.kid, + }, + }, + } + ) - const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) - const credentialId = credentialRecord.getAnonCredsTags()?.credentialId - expect(credentialId).toBeUndefined() + // Holder processes and accepts credential + await dataIntegrityCredentialFormatService.processCredential(agentContext, { + offerAttachment, + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) - expect(credentialRecord.credential).toEqual({ - ...{ - ...credential, - credentialSubject: new W3cCredentialSubject({ - id: issuer.did, - claims: (credential.credentialSubject as W3cCredentialSubject).claims, - }), - }, - proof: expect.any(Object), + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'w3c', credentialRecordId: expect.any(String) }, + ]) + + await expect( + anonCredsHolderService.getCredential(agentContext, { + credentialId: holderCredentialRecord.id, + }) + ).rejects.toThrow() + + expect(holderCredentialRecord.metadata.data).toEqual({}) + expect(issuerCredentialRecord.metadata.data).toEqual({}) + + const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) + + expect(credentialRecord.credential).toEqual({ + ...{ + ...credential, + credentialSubject: new W3cCredentialSubject({ + id: issuerKdv.did, + claims: (credential.credentialSubject as W3cCredentialSubject).claims, + }), + }, + proof: expect.any(Object), + }) }) -} +}) diff --git a/packages/anoncreds/tests/data-integrity-flow.test.ts b/packages/anoncreds/tests/data-integrity-flow.test.ts index 419ed04126..eec52b74e5 100644 --- a/packages/anoncreds/tests/data-integrity-flow.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow.test.ts @@ -1,13 +1,11 @@ -import type { KeyDidCreateOptions } from '@credo-ts/core' +import type { CreateDidKidVerificationMethodReturn } from '../../core/tests' import { AgentContext, CredentialExchangeRecord, CredentialPreviewAttribute, CredentialState, - DidKey, DidResolverService, - DidsApi, DidsModuleConfig, Ed25519Signature2018, InjectionSymbols, @@ -15,8 +13,6 @@ import { KeyDidResolver, KeyType, SignatureSuiteToken, - SigningProviderRegistry, - TypedArrayEncoder, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredential, @@ -27,11 +23,17 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../tests/InMemoryWallet' import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { + agentDependencies, + createDidKidVerificationMethod, + getAgentConfig, + getAgentContext, + testLogger, +} from '../../core/tests' import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, @@ -58,19 +60,13 @@ const anonCredsIssuerService = new AnonCredsRsIssuerService() const inMemoryStorageService = new InMemoryStorageService() -const logger = agentConfig.logger - const didsModuleConfig = new DidsModuleConfig({ registrars: [new KeyDidRegistrar()], resolvers: [new KeyDidResolver()], }) const fileSystem = new agentDependencies.FileSystem() -const wallet = new RegisteredAskarTestWallet( - agentConfig.logger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([]) -) +const wallet = new InMemoryWallet() const agentContext = getAgentContext({ registerInstances: [ @@ -81,9 +77,9 @@ const agentContext = getAgentContext({ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], - [InjectionSymbols.Logger, logger], + [InjectionSymbols.Logger, testLogger], [DidsModuleConfig, didsModuleConfig], - [DidResolverService, new DidResolverService(logger, didsModuleConfig)], + [DidResolverService, new DidResolverService(testLogger, didsModuleConfig)], [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], @@ -111,14 +107,14 @@ const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatSe const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' describe('data integrity format service (w3c)', () => { - let issuer: Awaited> - let holder: Awaited> + let issuerKdv: CreateDidKidVerificationMethodReturn + let holderKdv: CreateDidKidVerificationMethodReturn beforeAll(async () => { await wallet.createAndOpen(agentConfig.walletConfig) - issuer = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') - holder = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') + issuerKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') + holderKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') }) afterEach(async () => { @@ -126,39 +122,17 @@ describe('data integrity format service (w3c)', () => { }) test('issuance and verification flow w3c starting from offer without negotiation and without revocation', async () => { - await anonCredsFlowTest({ issuerId: indyDid, revocable: false, issuer, holder }) + await anonCredsFlowTest({ issuerId: indyDid, revocable: false, issuerKdv: issuerKdv, holderKdv: holderKdv }) }) }) -export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey: string) { - const dids = agentContext.dependencyManager.resolve(DidsApi) - const didCreateResult = await dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString(secretKey) }, - }) - - const did = didCreateResult.didState.did as string - const didKey = DidKey.fromDid(did) - const kid = `${did}#${didKey.key.fingerprint}` - - const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - return { - did, - kid, - verificationMethod, - } -} - async function anonCredsFlowTest(options: { issuerId: string revocable: boolean - issuer: Awaited> - holder: Awaited> + issuerKdv: CreateDidKidVerificationMethodReturn + holderKdv: CreateDidKidVerificationMethodReturn }) { - const { issuer } = options + const { issuerKdv: issuer } = options const holderCredentialRecord = new CredentialExchangeRecord({ protocolVersion: 'v1', @@ -256,21 +230,13 @@ async function anonCredsFlowTest(options: { }) ).rejects.toThrow() - const expectedCredentialMetadata = {} - expect(holderCredentialRecord.metadata.data).toEqual({ - '_dataIntegrity/credential': expectedCredentialMetadata, - '_dataIntegrity/credentialRequest': {}, - }) + expect(holderCredentialRecord.metadata.data).toEqual({}) - expect(issuerCredentialRecord.metadata.data).toEqual({ - '_dataIntegrity/credential': expectedCredentialMetadata, - }) + expect(issuerCredentialRecord.metadata.data).toEqual({}) const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) - const credentialId = credentialRecord.getAnonCredsTags()?.credentialId - expect(credentialId).toBeUndefined() expect(credentialRecord.credential).toEqual({ ...{ diff --git a/packages/anoncreds/tests/data-integrity.e2e.test.ts b/packages/anoncreds/tests/data-integrity.e2e.test.ts index eb5f5a0641..a4b7e18ad7 100644 --- a/packages/anoncreds/tests/data-integrity.e2e.test.ts +++ b/packages/anoncreds/tests/data-integrity.e2e.test.ts @@ -1,5 +1,4 @@ import type { AnonCredsTestsAgent } from './anoncredsSetup' -import type { AgentContext, KeyDidCreateOptions } from '@credo-ts/core' import type { EventReplaySubject } from '@credo-ts/core/tests' import type { InputDescriptorV2 } from '@sphereon/pex-models' @@ -7,42 +6,20 @@ import { AutoAcceptCredential, CredentialExchangeRecord, CredentialState, - DidKey, - DidsApi, - KeyType, ProofState, - TypedArrayEncoder, W3cCredential, W3cCredentialService, W3cCredentialSubject, } from '@credo-ts/core' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { createDidKidVerificationMethod } from '../../core/tests' import { waitForCredentialRecordSubject, waitForProofExchangeRecord } from '../../core/tests/helpers' import { setupAnonCredsTests } from './anoncredsSetup' import { presentationDefinition } from './fixtures/presentation-definition' -export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey: string) { - const dids = agentContext.dependencyManager.resolve(DidsApi) - const didCreateResult = await dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString(secretKey) }, - }) - - const did = didCreateResult.didState.did as string - const didKey = DidKey.fromDid(did) - const kid = `${did}#${didKey.key.fingerprint}` - - const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - return { did, kid, verificationMethod } -} - const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' -export type KDV = Awaited> describe('data anoncreds w3c data integrity e2e tests', () => { let issuerAgent: AnonCredsTestsAgent @@ -64,7 +41,7 @@ describe('data anoncreds w3c data integrity e2e tests', () => { await holderAgent.wallet.delete() }) - test('issuance and verification flow starting from proposal without negotiation and with revocation', async () => { + test('issuance and verification flow starting from offer with revocation', async () => { ;({ issuerAgent, issuerReplay, @@ -94,7 +71,7 @@ describe('data anoncreds w3c data integrity e2e tests', () => { }) }) - test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { + test('issuance and verification flow starting from offer without revocation', async () => { ;({ issuerAgent, issuerReplay, @@ -176,12 +153,12 @@ async function anonCredsFlowTest(options: { dataIntegrity: { bindingRequired: true, credential, - anonCredsLinkSecretBindingMethodOptions: { + anonCredsLinkSecretBinding: { credentialDefinitionId, revocationRegistryDefinitionId: revocationRegistryDefinitionId ?? undefined, revocationRegistryIndex: revocationRegistryDefinitionId ? 1 : undefined, }, - didCommSignedAttachmentBindingMethodOptions: {}, + didCommSignedAttachmentBinding: {}, }, }, }) @@ -196,7 +173,7 @@ async function anonCredsFlowTest(options: { autoAcceptCredential: AutoAcceptCredential.Never, credentialFormats: { dataIntegrity: { - anonCredsLinkSecretAcceptOfferOptions: { + anonCredsLinkSecret: { linkSecretId: 'linkSecretId', }, }, @@ -235,21 +212,17 @@ async function anonCredsFlowTest(options: { createdAt: expect.any(Date), metadata: { data: { - '_dataIntegrity/credential': { - linkSecretMetadata: { - credentialDefinitionId, - schemaId: expect.any(String), - }, + '_anoncreds/credential': { + credentialDefinitionId, + schemaId: expect.any(String), }, - '_dataIntegrity/credentialRequest': { - linkSecretRequestMetadata: { - link_secret_blinding_data: { - v_prime: expect.any(String), - vr_prime: revocationRegistryDefinitionId ? expect.any(String) : null, - }, - nonce: expect.any(String), - link_secret_name: 'linkSecretId', + '_anoncreds/credentialRequest': { + link_secret_blinding_data: { + v_prime: expect.any(String), + vr_prime: revocationRegistryDefinitionId ? expect.any(String) : null, }, + nonce: expect.any(String), + link_secret_name: 'linkSecretId', }, }, }, diff --git a/packages/anoncreds/tests/indy-flow.test.ts b/packages/anoncreds/tests/indy-flow.test.ts index 32af103d18..719afdcbe6 100644 --- a/packages/anoncreds/tests/indy-flow.test.ts +++ b/packages/anoncreds/tests/indy-flow.test.ts @@ -14,7 +14,6 @@ import { VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialsModuleConfig, - ConsoleLogger, DidResolverService, DidsModuleConfig, } from '@credo-ts/core' @@ -23,7 +22,7 @@ import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' +import { agentDependencies, getAgentConfig, getAgentContext, testLogger } from '../../core/tests' import { AnonCredsRsVerifierService, AnonCredsRsIssuerService, AnonCredsRsHolderService } from '../src/anoncreds-rs' import { anoncreds } from './helpers' @@ -64,7 +63,6 @@ const anonCredsIssuerService = new AnonCredsRsIssuerService() const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet -const logger = new ConsoleLogger() const inMemoryStorageService = new InMemoryStorageService() const agentContext = getAgentContext({ registerInstances: [ @@ -75,8 +73,8 @@ const agentContext = getAgentContext({ [AnonCredsHolderServiceSymbol, anonCredsHolderService], [AnonCredsVerifierServiceSymbol, anonCredsVerifierService], [AnonCredsRegistryService, new AnonCredsRegistryService()], - [DidResolverService, new DidResolverService(logger, new DidsModuleConfig())], - [InjectionSymbols.Logger, logger], + [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig())], + [InjectionSymbols.Logger, testLogger], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], [AnonCredsModuleConfig, anonCredsModuleConfig], [ @@ -315,6 +313,7 @@ describe('Legacy indy format services using anoncreds-rs', () => { revocationRegistryId: null, credentialRevocationId: null, methodName: 'inMemory', + linkSecretId: 'linkSecretId', }) expect(holderCredentialRecord.metadata.data).toEqual({ diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts index 73f5235b06..ead9140de4 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts @@ -1,33 +1,29 @@ import type { - AnonCredsLinkSecretCredentialRequestOptions, + AnonCredsLinkSecretCredentialRequestOptions as AnonCredsLinkSecretAcceptOfferOptions, DataIntegrityCredential, DataIntegrityCredentialOffer, DataIntegrityCredentialRequest, - DidCommSignedAttachmentCredentialRequestOptions, + DidCommSignedAttachmentCredentialRequestOptions as DidCommSignedAttachmentAcceptOfferOptions, W3C_VC_DATA_MODEL_VERSION, } from './dataIntegrityExchange' import type { CredentialFormat, JsonObject } from '../../../..' import type { W3cCredential } from '../../../vc' -export interface AnonCredsLinkSecretBindingMethodOptions { +export interface AnonCredsLinkSecretCreateOfferOptions { credentialDefinitionId: string revocationRegistryDefinitionId?: string revocationRegistryIndex?: number } -export interface DidCommSignedAttachmentBindingMethodOptions { +export interface DidCommSignedAttachmentCreateOfferOptions { didMethodsSupported?: string[] algsSupported?: string[] } -/** - * This defines the module payload for calling CredentialsApi.acceptOffer. No options are available for this - * method, so it's an empty object - */ export interface DataIntegrityAcceptOfferFormat { dataModelVersion?: W3C_VC_DATA_MODEL_VERSION - didCommSignedAttachmentAcceptOfferOptions?: DidCommSignedAttachmentCredentialRequestOptions - anonCredsLinkSecretAcceptOfferOptions?: AnonCredsLinkSecretCredentialRequestOptions + didCommSignedAttachment?: DidCommSignedAttachmentAcceptOfferOptions + anonCredsLinkSecret?: AnonCredsLinkSecretAcceptOfferOptions } /** @@ -37,19 +33,17 @@ export interface DataIntegrityAcceptOfferFormat { export interface DataIntegrityOfferCredentialFormat { credential: W3cCredential | JsonObject bindingRequired: boolean - anonCredsLinkSecretBindingMethodOptions?: AnonCredsLinkSecretBindingMethodOptions - didCommSignedAttachmentBindingMethodOptions?: DidCommSignedAttachmentBindingMethodOptions + anonCredsLinkSecretBinding?: AnonCredsLinkSecretCreateOfferOptions + didCommSignedAttachmentBinding?: DidCommSignedAttachmentCreateOfferOptions } /** * This defines the module payload for calling CredentialsApi.acceptRequest. No options are available for this * method, so it's an empty object */ -export type DataIntegrityAcceptRequestFormat = { +export interface DataIntegrityAcceptRequestFormat { credentialSubjectId?: string - didCommSignedAttachmentAcceptRequestOptions?: { - kid: string - } + issuerVerificationMethod?: string } export interface DataIntegrityCredentialFormat extends CredentialFormat { diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts index 874b6d5b22..bc3de26b49 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts @@ -1,4 +1,4 @@ -import type { JsonObject } from '../../../..' +import type { JsonObject } from '../../../../types' export type W3C_VC_DATA_MODEL_VERSION = '1.1' | '2.0' diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityMetadata.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityMetadata.ts deleted file mode 100644 index 08d7ecf11d..0000000000 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityMetadata.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Metadata key for storing metadata. - * - * MUST be used with {@link DataIntegrityMetadata} - */ -export const DataIntegrityMetadataKey = '_dataIntegrity/credential' - -export interface DataIntegrityLinkSecretMetadata { - schemaId: string - credentialDefinitionId: string - revocationRegistryId?: string - credentialRevocationId?: string -} - -/** - * Metadata for an data integrity credential offers / requests that will be stored - * in the credential record. - * - * MUST be used with {@link DataIntegrityMetadataKey} - */ -export interface DataIntegrityMetadata { - linkSecretMetadata?: DataIntegrityLinkSecretMetadata -} - -/** - * Metadata key for storing metadata on an anonCreds link secret credential request. - * - * MUST be used with {@link AnonCredsCredentialRequestMetadata} - */ -export const DataIntegrityRequestMetadataKey = '_dataIntegrity/credentialRequest' - -export interface DataIntegrityLinkSecretRequestMetadata { - link_secret_blinding_data: AnonCredsLinkSecretBlindingData - link_secret_name: string - nonce: string -} - -export interface DataIntegrityRequestMetadata { - linkSecretRequestMetadata?: DataIntegrityLinkSecretRequestMetadata -} - -export interface AnonCredsLinkSecretBlindingData { - v_prime: string - vr_prime: string | null -} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts index 7ce262d5d5..7d053b9f0f 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/index.ts @@ -1,4 +1,2 @@ export * from './DataIntegrityCredentialFormat' export * from './dataIntegrityExchange' -export * from './dataIntegrityMetadata' -export * from './AnonCredsDataIntegrityService' diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts index 90d07d81ae..6b56ab692c 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts @@ -1,10 +1,9 @@ -import type { AnonCredsCredentialMetadata } from '../../../../../../../../anoncreds/src/utils/metadata' import type { AgentContext } from '../../../../../../agent' import type { RevocationNotificationReceivedEvent } from '../../../../CredentialEvents' +import type { AnonCredsCredentialMetadata } from '@credo-ts/anoncreds' import { Subject } from 'rxjs' -import { CredentialExchangeRecord, CredentialState, InboundMessageContext } from '../../../../../..' import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../../tests/helpers' import { EventEmitter } from '../../../../../../agent/EventEmitter' import { MessageHandlerRegistry } from '../../../../../../agent/MessageHandlerRegistry' @@ -14,6 +13,8 @@ import { CredentialRepository } from '../../../../repository/CredentialRepositor import { V1RevocationNotificationMessage, V2RevocationNotificationMessage } from '../../messages' import { RevocationNotificationService } from '../RevocationNotificationService' +import { CredentialExchangeRecord, CredentialState, InboundMessageContext } from '@credo-ts/core' + jest.mock('../../../../repository/CredentialRepository') const CredentialRepositoryMock = CredentialRepository as jest.Mock const credentialRepository = new CredentialRepositoryMock() @@ -187,7 +188,7 @@ describe('RevocationNotificationService', () => { revocationRegistryId: 'AsB27X6KRrJFsqZ3unNAH6:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:default:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9', credentialRevocationId: '1', - } satisfies AnonCredsCredentialMetadata + } mockFunction(credentialRepository.getSingleByQuery).mockResolvedValueOnce(credentialRecord) const revocationNotificationCredentialId = `${metadata.revocationRegistryId}::${metadata.credentialRevocationId}` diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 913c0c680f..518496254c 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -11,10 +11,10 @@ import type { PresentationToCreate } from './utils' import type { AgentContext } from '../../agent' import type { Query } from '../../storage/StorageService' import type { JsonObject } from '../../types' -import type { Anoncreds2023DataIntegrityService } from '../credentials/formats/dataIntegrity/AnonCredsDataIntegrityService' import type { VerificationMethod } from '../dids' import type { SdJwtVcRecord } from '../sd-jwt-vc' import type { W3cCredentialRecord } from '../vc' +import type { IAnoncredsDataIntegrityService } from '../vc/data-integrity/models/IAnonCredsDataIntegrityService' import type { PresentationSignCallBackParams, SdJwtDecodedVerifiableCredentialWithKbJwtInput, @@ -33,7 +33,6 @@ import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../crypto' import { CredoError } from '../../error' import { Hasher, JsonTransformer } from '../../utils' -import { anoncreds2023DataIntegrityServiceSymbol } from '../credentials/formats/dataIntegrity/AnonCredsDataIntegrityService' import { DidsApi, getKeyFromVerificationMethod } from '../dids' import { SdJwtVcApi } from '../sd-jwt-vc' import { @@ -44,6 +43,10 @@ import { W3cCredentialService, W3cPresentation, } from '../vc' +import { + AnonCredsDataIntegrityServiceSymbol, + AnoncredsDataIntegrityCryptosuite, +} from '../vc/data-integrity/models/IAnonCredsDataIntegrityService' import { DifPresentationExchangeError } from './DifPresentationExchangeError' import { DifPresentationExchangeSubmissionLocation } from './models' @@ -55,8 +58,6 @@ import { getSphereonOriginalVerifiableCredential, } from './utils' -// export type ProofStructure = Record>> - /** * @todo create a public api for using dif presentation exchange */ @@ -399,7 +400,7 @@ export class DifPresentationExchangeService { return supportedSignatureSuites[0].proofType } - private signUsingAnoncreds2023( + private shouldSignUsingAnoncredsDataIntegrity( presentationToCreate: PresentationToCreate, presentationSubmission: PresentationSubmission ) { @@ -412,12 +413,12 @@ export class DifPresentationExchangeService { return inputDescriptor?.format === 'di_vp' && verifiableCredentials.credential.credential instanceof W3cJsonLdVerifiableCredential - ? verifiableCredentials.credential.credential.cryptoSuites + ? verifiableCredentials.credential.credential.dataIntegrityCryptosuites : [] }) - const commonCryptoSuites = cryptosuites.reduce((a, b) => a.filter((c) => b.includes(c))) - if (commonCryptoSuites.length === 0 || !commonCryptoSuites.includes('anoncreds-2023')) return undefined + const commonCryptosuites = cryptosuites.reduce((a, b) => a.filter((c) => b.includes(c))) + if (commonCryptosuites.length === 0 || !commonCryptosuites.includes(AnoncredsDataIntegrityCryptosuite)) return false return true } @@ -458,16 +459,16 @@ export class DifPresentationExchangeService { return signedPresentation.encoded as W3CVerifiablePresentation } else if (presentationToCreate.claimFormat === ClaimFormat.LdpVp) { - if (this.signUsingAnoncreds2023(presentationToCreate, presentationSubmission)) { - const anoncredsDataIntegrityService = - agentContext.dependencyManager.resolve( - anoncreds2023DataIntegrityServiceSymbol - ) + if (this.shouldSignUsingAnoncredsDataIntegrity(presentationToCreate, presentationSubmission)) { + const anoncredsDataIntegrityService = agentContext.dependencyManager.resolve( + AnonCredsDataIntegrityServiceSymbol + ) const presentation = await anoncredsDataIntegrityService.createPresentation(agentContext, { presentationDefinition, presentationSubmission, selectedCredentials: selectedCredentials as JsonObject[], selectedCredentialRecords: presentationToCreate.verifiableCredentials.map((vc) => vc.credential), + challenge, }) presentation.presentation_submission = presentationSubmission as unknown as JsonObject return presentation as unknown as SphereonW3cVerifiablePresentation diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index 155660fbee..c1ef770b36 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -23,9 +23,7 @@ export async function getCredentialsForRequest( credentialRecords: Array ): Promise { const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c)) - const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { - limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncreds-2023'], - }) + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials) const selectResults = { ...selectResultsRaw, diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index b40f5fb5c7..7a68e03dc5 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -6,9 +6,12 @@ import type { } from './DifPresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' import type { JsonValue } from '../../../../types' -import type { Anoncreds2023DataIntegrityService } from '../../../credentials' import type { DifPexInputDescriptorToCredentials } from '../../../dif-presentation-exchange' -import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' +import type { + IAnoncredsDataIntegrityService, + W3cVerifiablePresentation, + W3cVerifyPresentationResult, +} from '../../../vc' import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' import type { ProofFormatService } from '../ProofFormatService' import type { @@ -30,12 +33,13 @@ import type { PresentationSubmission } from '@sphereon/pex-models' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { CredoError } from '../../../../error' import { deepEquality, JsonTransformer } from '../../../../utils' -import { anoncreds2023DataIntegrityServiceSymbol } from '../../../credentials' import { DifPresentationExchangeService, DifPresentationExchangeSubmissionLocation, } from '../../../dif-presentation-exchange' import { + AnoncredsDataIntegrityCryptosuite, + AnonCredsDataIntegrityServiceSymbol, W3cCredentialService, ClaimFormat, W3cJsonLdVerifiablePresentation, @@ -221,7 +225,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic return { attachment, format } } - private verifyUsingAnoncreds2023( + private shouldVerifyUsingAnoncredsDataIntegrity( presentation: W3cVerifiablePresentation, presentationSubmission: PresentationSubmission ) { @@ -232,11 +236,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const verifyUsingDataIntegrity = descriptorMap.every((descriptor) => descriptor.format === ClaimFormat.DiVp) if (!verifyUsingDataIntegrity) return false - if (Array.isArray(presentation.proof)) { - return presentation.proof.some((proof) => proof.cryptosuite?.includes('anoncreds-2023')) - } else { - return presentation.proof.cryptosuite?.includes('anoncreds-2023') - } + return presentation.dataIntegrityCryptosuites.includes(AnoncredsDataIntegrityCryptosuite) } public async processPresentation( @@ -297,14 +297,17 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic domain: request.options.domain, }) } else if (parsedPresentation.claimFormat === ClaimFormat.LdpVp) { - if (this.verifyUsingAnoncreds2023(parsedPresentation, jsonPresentation.presentation_submission)) { - const dataIntegrityService = agentContext.dependencyManager.resolve( - anoncreds2023DataIntegrityServiceSymbol + if ( + this.shouldVerifyUsingAnoncredsDataIntegrity(parsedPresentation, jsonPresentation.presentation_submission) + ) { + const dataIntegrityService = agentContext.dependencyManager.resolve( + AnonCredsDataIntegrityServiceSymbol ) const proofVerificationResult = await dataIntegrityService.verifyPresentation(agentContext, { presentation: parsedPresentation as W3cJsonLdVerifiablePresentation, presentationDefinition: request.presentation_definition, presentationSubmission: jsonPresentation.presentation_submission, + challenge: request.options.challenge, }) verificationResult = { diff --git a/packages/core/src/modules/vc/W3cCredentialService.ts b/packages/core/src/modules/vc/W3cCredentialService.ts index 2af59cf39f..5743629cb8 100644 --- a/packages/core/src/modules/vc/W3cCredentialService.ts +++ b/packages/core/src/modules/vc/W3cCredentialService.ts @@ -178,7 +178,6 @@ export class W3cCredentialService { const w3cCredentialRecord = new W3cCredentialRecord({ tags: { expandedTypes }, credential: options.credential, - anonCredsCredentialRecordOptions: options.anonCredsCredentialRecordOptions, }) // Store the w3c credential record diff --git a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts index e8f1d546f9..3a9b892e89 100644 --- a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts +++ b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts @@ -5,7 +5,6 @@ import type { W3cJwtVerifiablePresentation } from './jwt-vc/W3cJwtVerifiablePres import type { ClaimFormat, W3cVerifiableCredential } from './models' import type { W3cCredential } from './models/credential/W3cCredential' import type { W3cPresentation } from './models/presentation/W3cPresentation' -import type { AnonCredsCredentialRecordOptions } from './repository' import type { JwaSignatureAlgorithm } from '../../crypto/jose/jwa' import type { SingleOrArray } from '../../utils/type' @@ -191,5 +190,4 @@ export interface W3cJsonLdVerifyPresentationOptions extends W3cVerifyPresentatio export interface StoreCredentialOptions { credential: W3cVerifiableCredential - anonCredsCredentialRecordOptions?: AnonCredsCredentialRecordOptions } diff --git a/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts b/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts new file mode 100644 index 0000000000..1fcfbe8181 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts @@ -0,0 +1,81 @@ +import { IsOptional, IsString } from 'class-validator' + +import { IsUri } from '../../../../utils' + +export interface DataIntegrityProofOptions { + type: string + cryptosuite: string + verificationMethod: string + proofPurpose: string + domain?: string + challenge?: string + nonce?: string + created?: string + expires?: string + proofValue?: string + previousProof?: string +} + +/** + * Linked Data Proof + * @see https://w3c.github.io/vc-data-model/#proofs-signatures + * + * @class LinkedDataProof + */ +export class DataIntegrityProof { + public constructor(options: DataIntegrityProofOptions) { + if (options) { + this.type = options.type + this.cryptosuite = options.cryptosuite + this.verificationMethod = options.verificationMethod + this.proofPurpose = options.proofPurpose + this.domain = options.domain + this.challenge = options.challenge + this.nonce = options.nonce + this.created = options.created + this.expires = options.expires + this.proofValue = options.proofValue + this.previousProof = options.previousProof + } + } + + @IsString() + public type!: string + + @IsString() + public cryptosuite!: string + + @IsString() + public proofPurpose!: string + + @IsString() + public verificationMethod!: string + + @IsUri() + @IsOptional() + public domain?: string + + @IsString() + @IsOptional() + public challenge?: string + + @IsString() + @IsOptional() + public nonce?: string + + @IsString() + @IsOptional() + public created?: string + + @IsString() + @IsOptional() + public expires?: string + + @IsString() + @IsOptional() + public proofValue?: string + + @IsString() + @IsOptional() + public previousProof?: string +} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsDataIntegrityService.ts b/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts similarity index 59% rename from packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsDataIntegrityService.ts rename to packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts index 5b25c52d70..9e6b839b26 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/AnonCredsDataIntegrityService.ts +++ b/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts @@ -1,22 +1,27 @@ +import type { W3cJsonLdVerifiablePresentation } from './W3cJsonLdVerifiablePresentation' import type { AgentContext } from '../../../../agent' import type { JsonObject } from '../../../../types' -import type { W3cCredentialRecord, W3cJsonLdVerifiablePresentation } from '../../../vc' +import type { W3cCredentialRecord } from '../../repository' import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' -export interface Anoncreds2023SignatureOptions extends Record { +export const AnoncredsDataIntegrityCryptosuite = 'anoncreds-2023' as const + +export interface AnoncredsDataIntegrityCreatePresentation { presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 presentationSubmission: PresentationSubmission selectedCredentials: JsonObject[] selectedCredentialRecords: W3cCredentialRecord[] + challenge: string } -export interface Anoncreds2023VerificationOptions extends Record { +export interface AnoncredsDataIntegrityVerifyPresentation { presentation: W3cJsonLdVerifiablePresentation presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 presentationSubmission: PresentationSubmission + challenge: string } -export const anoncreds2023DataIntegrityServiceSymbol = Symbol('AnonCredsVcDataIntegrityService') +export const AnonCredsDataIntegrityServiceSymbol = Symbol('AnonCredsDataIntegrityService') /** * We keep this standalone and don't integrity it @@ -24,8 +29,8 @@ export const anoncreds2023DataIntegrityServiceSymbol = Symbol('AnonCredsVcDataIn * to it's unique properties, in order to not pollute, * the existing api's. */ -export interface Anoncreds2023DataIntegrityService { - createPresentation(agentContext: AgentContext, options: Anoncreds2023SignatureOptions): Promise +export interface IAnoncredsDataIntegrityService { + createPresentation(agentContext: AgentContext, options: AnoncredsDataIntegrityCreatePresentation): Promise - verifyPresentation(agentContext: AgentContext, options: Anoncreds2023VerificationOptions): Promise + verifyPresentation(agentContext: AgentContext, options: AnoncredsDataIntegrityVerifyPresentation): Promise } diff --git a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts index 4d18e66b05..3cbf05a485 100644 --- a/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts +++ b/packages/core/src/modules/vc/data-integrity/models/LinkedDataProof.ts @@ -1,23 +1,18 @@ -import type { SingleOrArray } from '../../../../utils/type' - -import { Transform, TransformationType, plainToInstance, instanceToPlain } from 'class-transformer' import { IsOptional, IsString } from 'class-validator' import { IsUri } from '../../../../utils' export interface LinkedDataProofOptions { type: string - // FIXME: cryptosuite is not optional in Verifiable Credential Data Integrity 1.0 - cryptosuite?: string proofPurpose: string verificationMethod: string - // FIXME: created is optional in Verifiable Credential Data Integrity 1.0 created: string domain?: string challenge?: string jws?: string proofValue?: string nonce?: string + cryptosuite?: never } /** @@ -30,7 +25,6 @@ export class LinkedDataProof { public constructor(options: LinkedDataProofOptions) { if (options) { this.type = options.type - this.cryptosuite = options.cryptosuite this.proofPurpose = options.proofPurpose this.verificationMethod = options.verificationMethod this.created = options.created @@ -45,10 +39,6 @@ export class LinkedDataProof { @IsString() public type!: string - @IsString() - @IsOptional() - public cryptosuite: string | undefined - @IsString() public proofPurpose!: string @@ -56,7 +46,6 @@ export class LinkedDataProof { public verificationMethod!: string @IsString() - @IsOptional() public created!: string @IsUri() @@ -79,19 +68,3 @@ export class LinkedDataProof { @IsOptional() public nonce?: string } - -// Custom transformers - -export function LinkedDataProofTransformer() { - return Transform(({ value, type }: { value: SingleOrArray; type: TransformationType }) => { - if (type === TransformationType.PLAIN_TO_CLASS) { - if (Array.isArray(value)) return value.map((v) => plainToInstance(LinkedDataProof, v)) - return plainToInstance(LinkedDataProof, value) - } else if (type === TransformationType.CLASS_TO_PLAIN) { - if (Array.isArray(value)) return value.map((v) => instanceToPlain(v)) - return instanceToPlain(value) - } - // PLAIN_TO_PLAIN - return value - }) -} diff --git a/packages/core/src/modules/vc/data-integrity/models/ProofTransformer.ts b/packages/core/src/modules/vc/data-integrity/models/ProofTransformer.ts new file mode 100644 index 0000000000..d89f04f4d8 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/models/ProofTransformer.ts @@ -0,0 +1,38 @@ +import type { DataIntegrityProofOptions } from './DataIntegrityProof' +import type { LinkedDataProofOptions } from './LinkedDataProof' +import type { SingleOrArray } from '../../../../utils' + +import { Transform, TransformationType, instanceToPlain, plainToInstance } from 'class-transformer' + +import { DataIntegrityProof } from './DataIntegrityProof' +import { LinkedDataProof } from './LinkedDataProof' + +export function ProofTransformer() { + return Transform( + ({ + value, + type, + }: { + value: SingleOrArray + type: TransformationType + }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + const plainOptionsToClass = (v: LinkedDataProofOptions | DataIntegrityProofOptions) => { + if ('cryptosuite' in v) { + return plainToInstance(DataIntegrityProof, v) + } else { + return plainToInstance(LinkedDataProof, v) + } + } + + if (Array.isArray(value)) return value.map(plainOptionsToClass) + return plainOptionsToClass(value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (Array.isArray(value)) return value.map((v) => instanceToPlain(v)) + return instanceToPlain(value) + } + // PLAIN_TO_PLAIN + return value + } + ) +} diff --git a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts index ed6eaa0591..c78d43a9d1 100644 --- a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts +++ b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts @@ -1,3 +1,4 @@ +import type { DataIntegrityProofOptions } from './DataIntegrityProof' import type { LinkedDataProofOptions } from './LinkedDataProof' import type { W3cCredentialOptions } from '../../models/credential/W3cCredential' import type { W3cJsonCredential } from '../../models/credential/W3cJsonCredential' @@ -14,35 +15,40 @@ import { import { ClaimFormat } from '../../models/ClaimFormat' import { W3cCredential } from '../../models/credential/W3cCredential' -import { LinkedDataProof, LinkedDataProofTransformer } from './LinkedDataProof' +import { DataIntegrityProof } from './DataIntegrityProof' +import { LinkedDataProof } from './LinkedDataProof' +import { ProofTransformer } from './ProofTransformer' export interface W3cJsonLdVerifiableCredentialOptions extends W3cCredentialOptions { - proof: SingleOrArray + proof: SingleOrArray } export class W3cJsonLdVerifiableCredential extends W3cCredential { public constructor(options: W3cJsonLdVerifiableCredentialOptions) { super(options) if (options) { - this.proof = mapSingleOrArray(options.proof, (proof) => new LinkedDataProof(proof)) + this.proof = mapSingleOrArray(options.proof, (proof) => { + if (proof.cryptosuite) return new DataIntegrityProof(proof) + else return new LinkedDataProof(proof as LinkedDataProofOptions) + }) } } - @LinkedDataProofTransformer() - @IsInstanceOrArrayOfInstances({ classType: LinkedDataProof }) + @ProofTransformer() + @IsInstanceOrArrayOfInstances({ classType: [LinkedDataProof, DataIntegrityProof] }) @ValidateNested() - public proof!: SingleOrArray + public proof!: SingleOrArray public get proofTypes(): Array { const proofArray = asArray(this.proof) ?? [] return proofArray.map((proof) => proof.type) } - public get cryptoSuites(): Array { + public get dataIntegrityCryptosuites(): Array { const proofArray = asArray(this.proof) ?? [] return proofArray + .filter((proof): proof is DataIntegrityProof => proof.type === 'DataIntegrityProof' && 'cryptosuite' in proof) .map((proof) => proof.cryptosuite) - .filter((cryptosuite): cryptosuite is string => typeof cryptosuite === 'string') } public toJson() { diff --git a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiablePresentation.ts b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiablePresentation.ts index b13ef2cc84..f559cde534 100644 --- a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiablePresentation.ts +++ b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiablePresentation.ts @@ -1,3 +1,4 @@ +import type { DataIntegrityProofOptions } from './DataIntegrityProof' import type { LinkedDataProofOptions } from './LinkedDataProof' import type { W3cPresentationOptions } from '../../models/presentation/W3cPresentation' @@ -5,29 +6,39 @@ import { SingleOrArray, IsInstanceOrArrayOfInstances, JsonTransformer, asArray } import { ClaimFormat } from '../../models' import { W3cPresentation } from '../../models/presentation/W3cPresentation' -import { LinkedDataProof, LinkedDataProofTransformer } from './LinkedDataProof' +import { DataIntegrityProof } from './DataIntegrityProof' +import { LinkedDataProof } from './LinkedDataProof' +import { ProofTransformer } from './ProofTransformer' export interface W3cJsonLdVerifiablePresentationOptions extends W3cPresentationOptions { - proof: LinkedDataProofOptions + proof: LinkedDataProofOptions | DataIntegrityProofOptions } export class W3cJsonLdVerifiablePresentation extends W3cPresentation { public constructor(options: W3cJsonLdVerifiablePresentationOptions) { super(options) if (options) { - this.proof = new LinkedDataProof(options.proof) + if (options.proof.cryptosuite) this.proof = new DataIntegrityProof(options.proof) + else this.proof = new LinkedDataProof(options.proof as LinkedDataProofOptions) } } - @LinkedDataProofTransformer() - @IsInstanceOrArrayOfInstances({ classType: LinkedDataProof }) - public proof!: SingleOrArray + @ProofTransformer() + @IsInstanceOrArrayOfInstances({ classType: [LinkedDataProof, DataIntegrityProof] }) + public proof!: SingleOrArray public get proofTypes(): Array { const proofArray = asArray(this.proof) ?? [] return proofArray.map((proof) => proof.type) } + public get dataIntegrityCryptosuites(): Array { + const proofArray = asArray(this.proof) ?? [] + return proofArray + .filter((proof): proof is DataIntegrityProof => proof.type === 'DataIntegrityProof' && 'cryptosuite' in proof) + .map((proof) => proof.cryptosuite) + } + public toJson() { return JsonTransformer.toJSON(this) } diff --git a/packages/core/src/modules/vc/data-integrity/models/index.ts b/packages/core/src/modules/vc/data-integrity/models/index.ts index eee41acbde..d4d4c76909 100644 --- a/packages/core/src/modules/vc/data-integrity/models/index.ts +++ b/packages/core/src/modules/vc/data-integrity/models/index.ts @@ -1,3 +1,5 @@ export * from './W3cJsonLdVerifiableCredential' export * from './W3cJsonLdVerifiablePresentation' export * from './LdKeyPair' +export * from './IAnonCredsDataIntegrityService' +export * from './DataIntegrityProof' diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts index 6281f0cec5..0674dd8273 100644 --- a/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialSubject.ts @@ -38,10 +38,11 @@ export class W3cCredentialSubject { export function W3cCredentialSubjectTransformer() { return Transform(({ value, type }: { value: W3cCredentialSubjectOptions; type: TransformationType }) => { if (type === TransformationType.PLAIN_TO_CLASS) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const vToClass = (v: any) => { + const vToClass = (v: unknown) => { + if (!v || typeof v !== 'object') throw new CredoError('Invalid credential subject') if (isInstance(v, W3cCredentialSubject)) return v - const { id, ...claims } = v + const { id, ...claims } = v as Record + if (id !== undefined && typeof id !== 'string') throw new CredoError('Invalid credential subject id') return new W3cCredentialSubject({ id, claims }) } @@ -51,9 +52,8 @@ export function W3cCredentialSubjectTransformer() { return Array.isArray(value) ? value.map(vToClass) : vToClass(value) } else if (type === TransformationType.CLASS_TO_PLAIN) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const vToJson = (v: any) => { - if (isInstance(v, W3cCredentialSubject)) return v.id ? { ...v.claims, id: v.id } : { ...v.claims } + const vToJson = (v: unknown) => { + if (v instanceof W3cCredentialSubject) return v.id ? { ...v.claims, id: v.id } : { ...v.claims } return v } diff --git a/packages/core/src/modules/vc/repository/W3CAnoncredsCredentialMetadata.ts b/packages/core/src/modules/vc/repository/W3CAnoncredsCredentialMetadata.ts deleted file mode 100644 index c1bb8ebda4..0000000000 --- a/packages/core/src/modules/vc/repository/W3CAnoncredsCredentialMetadata.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { IsOptional, IsString } from 'class-validator' - -export interface W3CAnonCredsCredentialMetadataOptions { - credentialId: string - methodName: string - credentialRevocationId?: string - linkSecretId: string -} - -export class W3cAnonCredsCredentialMetadata { - public constructor(options: W3CAnonCredsCredentialMetadataOptions) { - if (options) { - this.credentialId = options.credentialId - this.methodName = options.methodName - this.credentialRevocationId = options.credentialRevocationId - this.linkSecretId = options.linkSecretId - } - } - - @IsString() - public credentialId!: string - - @IsString() - @IsOptional() - public credentialRevocationId?: string - - @IsString() - public linkSecretId!: string - - /** - * AnonCreds method name. We don't use names explicitly from the registry (there's no identifier for a registry) - * @see https://hyperledger.github.io/anoncreds-methods-registry/ - */ - @IsString() - public methodName!: string -} diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts index cb515685bc..6286714ca0 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts @@ -1,47 +1,23 @@ -import type { AnonCredsClaimRecord } from './anonCredsCredentialValue' -import type { Tags, TagsBase } from '../../../storage/BaseRecord' +import type { TagsBase } from '../../../storage/BaseRecord' import type { Constructable } from '../../../utils/mixins' -import { Type } from 'class-transformer' -import { IsOptional } from 'class-validator' - -import { CredoError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' import { JsonTransformer } from '../../../utils' import { uuid } from '../../../utils/uuid' import { ClaimFormat, W3cVerifiableCredential, W3cVerifiableCredentialTransformer } from '../models' -import { W3cAnonCredsCredentialMetadata } from './W3CAnoncredsCredentialMetadata' -import { mapAttributeRawValuesToAnonCredsCredentialValues } from './anonCredsCredentialValue' - -export interface AnonCredsCredentialRecordOptions { - credentialId: string - credentialRevocationId?: string - linkSecretId: string - schemaName: string - schemaVersion: string - schemaIssuerId: string - methodName: string - // TODO: derive from proof - schemaId: string - credentialDefinitionId: string - revocationRegistryId?: string - - unqualifiedTags?: { - issuerId: string - schemaId: string - schemaIssuerId: string - credentialDefinitionId: string - revocationRegistryId?: string - } -} - export interface W3cCredentialRecordOptions { id?: string createdAt?: Date credential: W3cVerifiableCredential tags: CustomW3cCredentialTags - anonCredsCredentialRecordOptions?: AnonCredsCredentialRecordOptions +} + +export type CustomW3cCredentialTags = TagsBase & { + /** + * Expanded types are used for JSON-LD credentials to allow for filtering on the expanded type. + */ + expandedTypes?: Array } export type DefaultW3cCredentialTags = { @@ -59,45 +35,34 @@ export type DefaultW3cCredentialTags = { types: Array algs?: Array } - -export type DefaultAnonCredsCredentialTags = { - credentialId: string - linkSecretId: string - credentialRevocationId?: string - methodName: string +export type AnonCredsCredentialTags = { + anonCredsCredentialId: string + anonCredsLinkSecretId: string + anonCredsCredentialRevocationId?: string + anonCredsMethodName: string // the following keys can be used for every `attribute name` in credential. - [key: `attr::${string}::marker`]: true | undefined - [key: `attr::${string}::value`]: string | undefined -} - -export type CustomW3cCredentialTags = TagsBase & { - /** - * Expanded types are used for JSON-LD credentials to allow for filtering on the expanded type. - */ - expandedTypes?: Array -} - -export type CustomAnonCredsCredentialTags = { - schemaName: string - schemaVersion: string - - // TODO: derive from proof - schemaId: string - schemaIssuerId: string - credentialDefinitionId: string - revocationRegistryId?: string - - unqualifiedIssuerId: string | undefined - unqualifiedSchemaId: string | undefined - unqualifiedSchemaIssuerId: string | undefined - unqualifiedCredentialDefinitionId: string | undefined - unqualifiedRevocationRegistryId: string | undefined + [key: `anonCredsAttr::${string}::marker`]: true | undefined + [key: `anonCredsAttr::${string}::value`]: string | undefined + + anonCredsSchemaName: string + anonCredsSchemaVersion: string + + anonCredsSchemaId: string + anonCredsSchemaIssuerId: string + anonCredsCredentialDefinitionId: string + anonCredsRevocationRegistryId?: string + + anonCredsUnqualifiedIssuerId?: string + anonCredsUnqualifiedSchemaId?: string + anonCredsUnqualifiedSchemaIssuerId?: string + anonCredsUnqualifiedCredentialDefinitionId?: string + anonCredsUnqualifiedRevocationRegistryId?: string } export class W3cCredentialRecord extends BaseRecord< - DefaultW3cCredentialTags & Partial, - CustomW3cCredentialTags & Partial + DefaultW3cCredentialTags & Partial, + CustomW3cCredentialTags > { public static readonly type = 'W3cCredentialRecord' public readonly type = W3cCredentialRecord.type @@ -105,45 +70,17 @@ export class W3cCredentialRecord extends BaseRecord< @W3cVerifiableCredentialTransformer() public credential!: W3cVerifiableCredential - @IsOptional() - @Type(() => W3cAnonCredsCredentialMetadata) - public readonly anonCredsCredentialMetadata?: W3cAnonCredsCredentialMetadata - public constructor(props: W3cCredentialRecordOptions) { super() if (props) { this.id = props.id ?? uuid() this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags this.credential = props.credential - - if (props.anonCredsCredentialRecordOptions) { - this.anonCredsCredentialMetadata = new W3cAnonCredsCredentialMetadata({ - credentialId: props.anonCredsCredentialRecordOptions.credentialId, - credentialRevocationId: props.anonCredsCredentialRecordOptions.credentialRevocationId, - linkSecretId: props.anonCredsCredentialRecordOptions.linkSecretId, - methodName: props.anonCredsCredentialRecordOptions.methodName, - }) - } - - this.setTags({ - ...props.tags, - schemaIssuerId: props.anonCredsCredentialRecordOptions?.schemaIssuerId, - schemaName: props.anonCredsCredentialRecordOptions?.schemaName, - schemaVersion: props.anonCredsCredentialRecordOptions?.schemaVersion, - schemaId: props.anonCredsCredentialRecordOptions?.schemaId, - credentialDefinitionId: props.anonCredsCredentialRecordOptions?.credentialDefinitionId, - revocationRegistryId: props.anonCredsCredentialRecordOptions?.revocationRegistryId, - unqualifiedIssuerId: props.anonCredsCredentialRecordOptions?.unqualifiedTags?.issuerId, - unqualifiedSchemaIssuerId: props.anonCredsCredentialRecordOptions?.unqualifiedTags?.schemaIssuerId, - unqualifiedSchemaId: props.anonCredsCredentialRecordOptions?.unqualifiedTags?.schemaId, - unqualifiedCredentialDefinitionId: - props.anonCredsCredentialRecordOptions?.unqualifiedTags?.credentialDefinitionId, - unqualifiedRevocationRegistryId: props.anonCredsCredentialRecordOptions?.unqualifiedTags?.revocationRegistryId, - }) } } - public getTags() { + public getTags(): DefaultW3cCredentialTags & Partial { // Contexts are usually strings, but can sometimes be objects. We're unable to use objects as tags, // so we filter out the objects before setting the tags. const stringContexts = this.credential.contexts.filter((ctx): ctx is string => typeof ctx === 'string') @@ -162,7 +99,7 @@ export class W3cCredentialRecord extends BaseRecord< // Proof types is used for ldp_vc credentials if (this.credential.claimFormat === ClaimFormat.LdpVc) { tags.proofTypes = this.credential.proofTypes - tags.cryptosuites = this.credential.cryptoSuites + tags.cryptosuites = this.credential.dataIntegrityCryptosuites } // Algs is used for jwt_vc credentials @@ -170,57 +107,7 @@ export class W3cCredentialRecord extends BaseRecord< tags.algs = [this.credential.jwt.header.alg] } - return { - ...tags, - ...this.getAnonCredsTags(), - } - } - - public getAnonCredsTags(): Tags | undefined { - if (!this.anonCredsCredentialMetadata) return undefined - - const { schemaId, schemaName, schemaVersion, schemaIssuerId, credentialDefinitionId } = this._tags - if (!schemaId || !schemaName || !schemaVersion || !schemaIssuerId || !credentialDefinitionId) return undefined - const { - unqualifiedSchemaId, - unqualifiedSchemaIssuerId, - unqualifiedCredentialDefinitionId, - unqualifiedRevocationRegistryId, - unqualifiedIssuerId, - } = this._tags - - const anonCredsCredentialTags: Tags = { - schemaId, - schemaIssuerId, - schemaName, - schemaVersion, - credentialDefinitionId, - revocationRegistryId: this._tags.revocationRegistryId, - unqualifiedIssuerId, - unqualifiedSchemaId, - unqualifiedSchemaIssuerId, - unqualifiedCredentialDefinitionId, - unqualifiedRevocationRegistryId, - credentialId: this.anonCredsCredentialMetadata?.credentialId as string, - credentialRevocationId: this.anonCredsCredentialMetadata?.credentialRevocationId as string, - linkSecretId: this.anonCredsCredentialMetadata?.linkSecretId as string, - methodName: this.anonCredsCredentialMetadata?.methodName as string, - } - - if (Array.isArray(this.credential.credentialSubject)) { - throw new CredoError('Credential subject must be an object, not an array.') - } - - const values = mapAttributeRawValuesToAnonCredsCredentialValues( - (this.credential.credentialSubject.claims as AnonCredsClaimRecord) ?? {} - ) - - for (const [key, value] of Object.entries(values)) { - anonCredsCredentialTags[`attr::${key}::value`] = value.raw - anonCredsCredentialTags[`attr::${key}::marker`] = true - } - - return anonCredsCredentialTags + return tags } /** diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts b/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts index aec0b776f7..6f711e6ed2 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRepository.ts @@ -1,5 +1,3 @@ -import type { AgentContext } from '../../../agent' - import { EventEmitter } from '../../../agent/EventEmitter' import { InjectionSymbols } from '../../../constants' import { inject, injectable } from '../../../plugins' @@ -16,17 +14,4 @@ export class W3cCredentialRepository extends Repository { ) { super(W3cCredentialRecord, 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 }) - } - - // FIXME: maybe we should rename this, as it only with anoncreds - public async getByCredentialId(agentContext: AgentContext, credentialId: string) { - return this.getSingleByQuery(agentContext, { credentialId }) - } } diff --git a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts index 73838c8735..60c2d2ebd1 100644 --- a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts +++ b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts @@ -1,9 +1,9 @@ -import type { AnonCredsCredentialRecordOptions } from '../W3cCredentialRecord' +import type { AnonCredsCredentialTags } from '../W3cCredentialRecord' +import { getAnonCredsTagsFromRecord } from '../../../../../../anoncreds/src/utils/w3cAnonCredsUtils' import { JsonTransformer } from '../../../../utils' import { Ed25519Signature2018Fixtures } from '../../data-integrity/__tests__/fixtures' import { W3cJsonLdVerifiableCredential } from '../../data-integrity/models' -import { W3cCredentialSubject } from '../../models' import { W3cCredentialRecord } from '../W3cCredentialRecord' describe('W3cCredentialRecord', () => { @@ -40,7 +40,7 @@ describe('W3cCredentialRecord', () => { types: ['VerifiableCredential', 'UniversityDegreeCredential'], }) - expect(w3cCredentialRecord.getAnonCredsTags()).toBeUndefined() + expect(getAnonCredsTagsFromRecord(w3cCredentialRecord)).toBeUndefined() }) it('should return default tags (w3cAnoncredsCredential)', () => { @@ -49,54 +49,56 @@ describe('W3cCredentialRecord', () => { W3cJsonLdVerifiableCredential ) - const anonCredsCredentialRecordOptions: AnonCredsCredentialRecordOptions = { - schemaIssuerId: 'schemaIssuerId', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - schemaId: 'schemaId', - credentialDefinitionId: 'credentialDefinitionId', - credentialId: 'credentialId', - credentialRevocationId: 'credentialRevocationId', - linkSecretId: 'linkSecretId', - methodName: 'methodName', - revocationRegistryId: 'revocationRegistryId', + const anoncredsCredentialRecordTags: AnonCredsCredentialTags = { + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsSchemaId: 'schemaId', + anonCredsCredentialDefinitionId: 'credentialDefinitionId', + anonCredsCredentialId: 'credentialId', + anonCredsCredentialRevocationId: 'credentialRevocationId', + anonCredsLinkSecretId: 'linkSecretId', + anonCredsMethodName: 'methodName', + anonCredsRevocationRegistryId: 'revocationRegistryId', } - if (Array.isArray(credential.credentialSubject)) throw new Error('Invalid credentialSubject') - credential.credentialSubject = new W3cCredentialSubject({ claims: { degree: 'Bachelor of Science and Arts' } }) const w3cCredentialRecord = new W3cCredentialRecord({ credential, tags: { expandedTypes: ['https://expanded.tag#1'], }, - anonCredsCredentialRecordOptions, }) + const anonCredsCredentialMetadata = { + credentialId: anoncredsCredentialRecordTags.anonCredsCredentialId, + credentialRevocationId: anoncredsCredentialRecordTags.anonCredsCredentialRevocationId, + linkSecretId: anoncredsCredentialRecordTags.anonCredsLinkSecretId, + methodName: anoncredsCredentialRecordTags.anonCredsMethodName, + } + + w3cCredentialRecord.setTags(anoncredsCredentialRecordTags) + w3cCredentialRecord.metadata.set('_w3c/AnonCredsMetadata', anonCredsCredentialMetadata) + const anoncredsCredentialTags = { - linkSecretId: 'linkSecretId', - methodName: 'methodName', - schemaId: 'schemaId', - schemaIssuerId: 'schemaIssuerId', - schemaName: 'schemaName', - schemaVersion: 'schemaVersion', - credentialDefinitionId: 'credentialDefinitionId', - credentialId: 'credentialId', - 'attr::degree::marker': true, - 'attr::degree::value': 'Bachelor of Science and Arts', - revocationRegistryId: 'revocationRegistryId', - credentialRevocationId: 'credentialRevocationId', - unqualifiedCredentialDefinitionId: undefined, - unqualifiedIssuerId: undefined, - unqualifiedRevocationRegistryId: undefined, - unqualifiedSchemaId: undefined, - unqualifiedSchemaIssuerId: undefined, + anonCredsLinkSecretId: 'linkSecretId', + anonCredsMethodName: 'methodName', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsCredentialDefinitionId: 'credentialDefinitionId', + anonCredsCredentialId: 'credentialId', + anonCredsRevocationRegistryId: 'revocationRegistryId', + anonCredsCredentialRevocationId: 'credentialRevocationId', } - const anonCredsTags = w3cCredentialRecord.getAnonCredsTags() + const anonCredsTags = getAnonCredsTagsFromRecord(w3cCredentialRecord) expect(anonCredsTags).toEqual({ ...anoncredsCredentialTags, }) + expect(w3cCredentialRecord.metadata.get('_w3c/AnonCredsMetadata')).toEqual(anonCredsCredentialMetadata) + expect(w3cCredentialRecord.getTags()).toEqual({ claimFormat: 'ldp_vc', issuerId: credential.issuerId, diff --git a/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts b/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts deleted file mode 100644 index e8071f3f82..0000000000 --- a/packages/core/src/modules/vc/repository/anonCredsCredentialValue.ts +++ /dev/null @@ -1,88 +0,0 @@ -import bigInt from 'big-integer' - -import { CredoError } from '../../../error' -import { Buffer, Hasher, TypedArrayEncoder } from '../../../utils' - -export type AnonCredsClaimRecord = Record - -export interface AnonCredsCredentialValue { - raw: string - encoded: string // Raw value as number in string -} - -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 -} - -// TODO: this function can only encode strings -// If encoding numbers we run into problems with 0.0 representing the same value as 0 and is implicitly converted to 0 -/** - * 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 encodeCredentialValue(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' - } - - const buffer = TypedArrayEncoder.fromString(String(value)) - const hash = Hasher.hash(buffer, 'sha-256') - const hex = Buffer.from(hash).toString('hex') - - return bigInt(hex, 16).toString() -} - -export const mapAttributeRawValuesToAnonCredsCredentialValues = ( - record: AnonCredsClaimRecord -): Record => { - const credentialValues: Record = {} - - for (const [key, value] of Object.entries(record)) { - if (typeof value === 'object') { - throw new CredoError(`Unsupported value type: object for W3cAnonCreds Credential`) - } - credentialValues[key] = { - raw: value.toString(), - encoded: encodeCredentialValue(value), - } - } - - return credentialValues -} diff --git a/packages/core/src/modules/vc/repository/index.ts b/packages/core/src/modules/vc/repository/index.ts index 3f906c4edb..64aae1fdcb 100644 --- a/packages/core/src/modules/vc/repository/index.ts +++ b/packages/core/src/modules/vc/repository/index.ts @@ -1,8 +1,2 @@ export * from './W3cCredentialRecord' export * from './W3cCredentialRepository' -export { - encodeCredentialValue, - AnonCredsCredentialValue, - mapAttributeRawValuesToAnonCredsCredentialValues, - AnonCredsClaimRecord, -} from './anonCredsCredentialValue' diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 260bb9fc27..f26aa51f24 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -17,6 +17,7 @@ import type { Buffer, AgentMessageProcessedEvent, RevocationNotificationReceivedEvent, + KeyDidCreateOptions, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../src/modules/connections/TrustPingEvents' @@ -49,6 +50,7 @@ import { InjectionSymbols, ProofEventTypes, TrustPingEventTypes, + DidsApi, } from '../src' import { Key, KeyType } from '../src/crypto' import { DidKey } from '../src/modules/dids/methods/key' @@ -693,3 +695,22 @@ export async function retryUntilResult Promise>( throw new Error(`Unable to get result from method in ${maxAttempts} attempts`) } + +export type CreateDidKidVerificationMethodReturn = Awaited> +export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey?: string) { + const dids = agentContext.dependencyManager.resolve(DidsApi) + const didCreateResult = await dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: secretKey ? TypedArrayEncoder.fromString(secretKey) : undefined }, + }) + + const did = didCreateResult.didState.did as string + const didKey = DidKey.fromDid(did) + const kid = `${did}#${didKey.key.fingerprint}` + + const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + return { did, kid, verificationMethod } +} diff --git a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts index ba862df405..3d9f51aced 100644 --- a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts +++ b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts @@ -1,4 +1,5 @@ -import type { Agent, FileSystem, WalletConfig, AnonCredsCredentialValue } from '@credo-ts/core' +import type { AnonCredsCredentialValue } from '@credo-ts/anoncreds' +import type { Agent, FileSystem, WalletConfig } from '@credo-ts/core' import type { EntryObject } from '@hyperledger/aries-askar-shared' import { AnonCredsCredentialRecord, AnonCredsLinkSecretRecord } from '@credo-ts/anoncreds' diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index a415aad3b0..28d6511449 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -11,12 +11,12 @@ import { W3cJwtVerifiablePresentation, parseDid, CredoError, - DidsApi, injectable, W3cJsonLdVerifiablePresentation, asArray, DifPresentationExchangeService, DifPresentationExchangeSubmissionLocation, + DidsApi, } from '@credo-ts/core' import { CheckLinkedDomain, diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index a25921cf1d..b5b6ac8b88 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -11,7 +11,6 @@ import type { PresentationVerificationCallback, SigningAlgo } from '@sphereon/di import { CredoError, - DidsApi, inject, injectable, InjectionSymbols, @@ -24,6 +23,7 @@ import { W3cCredentialService, W3cJsonLdVerifiablePresentation, Hasher, + DidsApi, } from '@credo-ts/core' import { AuthorizationResponse, diff --git a/packages/openid4vc/tests/utils.ts b/packages/openid4vc/tests/utils.ts index 88fcfe06a7..14feff0500 100644 --- a/packages/openid4vc/tests/utils.ts +++ b/packages/openid4vc/tests/utils.ts @@ -1,31 +1,9 @@ -import type { TenantAgent } from '../../tenants/src/TenantAgent' -import type { KeyDidCreateOptions, ModulesMap } from '@credo-ts/core' +import type { ModulesMap } from '@credo-ts/core' import type { TenantsModule } from '@credo-ts/tenants' -import { LogLevel, Agent, DidKey, KeyType, TypedArrayEncoder, utils } from '@credo-ts/core' +import { Agent, LogLevel, utils } from '@credo-ts/core' -import { agentDependencies, TestLogger } from '../../core/tests' - -export async function createDidKidVerificationMethod(agent: Agent | TenantAgent, secretKey?: string) { - const didCreateResult = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: secretKey ? TypedArrayEncoder.fromString(secretKey) : undefined }, - }) - - const did = didCreateResult.didState.did as string - const didKey = DidKey.fromDid(did) - const kid = `${did}#${didKey.key.fingerprint}` - - const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - return { - did, - kid, - verificationMethod, - } -} +import { TestLogger, agentDependencies, createDidKidVerificationMethod } from '../../core/tests' export async function createAgentFromModules(label: string, modulesMap: MM, secretKey: string) { const agent = new Agent({ @@ -35,7 +13,7 @@ export async function createAgentFromModules(label: strin }) await agent.initialize() - const data = await createDidKidVerificationMethod(agent, secretKey) + const data = await createDidKidVerificationMethod(agent.context, secretKey) return { ...data, From 9cefe8a4dd5bcf7b0d39fc4e5e52ae5ca489d1fe Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 13 Feb 2024 12:54:10 +0100 Subject: [PATCH 32/38] fix: add cheqd test --- .../tests/InMemoryAnonCredsRegistry.ts | 12 +- .../tests/data-integrity.e2e.test.ts | 2 +- .../tests/cheqd-data-integrity.e2e.test.ts | 273 ++++++++++++++ .../tests/helpers/cheqdAnonCredsSetup.ts | 356 ++++++++++++++++++ 4 files changed, 638 insertions(+), 5 deletions(-) create mode 100644 packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts create mode 100644 packages/cheqd/tests/helpers/cheqdAnonCredsSetup.ts diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 77e40ad3fa..3c97042d5e 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -80,10 +80,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { public async getSchema(_agentContext: AgentContext, schemaId: string): Promise { const schema = this.schemas[schemaId] - const parsed = parseIndySchemaId(schemaId) - const legacySchemaId = getUnqualifiedSchemaId(parsed.namespaceIdentifier, parsed.schemaName, parsed.schemaVersion) - const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) - if (!schema) { return { resolutionMetadata: { @@ -96,6 +92,8 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } let didIndyNamespace: string | undefined = undefined + let indyLedgerSeqNo: string | undefined = undefined + if (isUnqualifiedSchemaId(schemaId)) { const qSchemaIdEnd = getQualifiedDidIndyDid(schemaId, 'mock').split('mock:')[1] const qSchemaId = Object.keys(this.schemas).find((schemaId) => schemaId.endsWith(qSchemaIdEnd)) @@ -105,6 +103,12 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { didIndyNamespace = parseIndySchemaId(schemaId).namespace } + if (didIndyNamespace) { + const parsed = parseIndySchemaId(schemaId) + const legacySchemaId = getUnqualifiedSchemaId(parsed.namespaceIdentifier, parsed.schemaName, parsed.schemaVersion) + indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId).toString() + } + return { resolutionMetadata: {}, schema, diff --git a/packages/anoncreds/tests/data-integrity.e2e.test.ts b/packages/anoncreds/tests/data-integrity.e2e.test.ts index a4b7e18ad7..0d1a2a0c78 100644 --- a/packages/anoncreds/tests/data-integrity.e2e.test.ts +++ b/packages/anoncreds/tests/data-integrity.e2e.test.ts @@ -21,7 +21,7 @@ import { presentationDefinition } from './fixtures/presentation-definition' const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' -describe('data anoncreds w3c data integrity e2e tests', () => { +describe('anoncreds w3c data integrity e2e tests', () => { let issuerAgent: AnonCredsTestsAgent let holderAgent: AnonCredsTestsAgent let credentialDefinitionId: string diff --git a/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts b/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts new file mode 100644 index 0000000000..f5d4cfa3d6 --- /dev/null +++ b/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts @@ -0,0 +1,273 @@ +import type { AnonCredsTestsAgent } from './helpers/cheqdAnonCredsSetup' +import type { EventReplaySubject } from '../../core/tests' +import type { InputDescriptorV2 } from '@sphereon/pex-models' + +import { + AutoAcceptCredential, + CredentialExchangeRecord, + CredentialState, + ProofState, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, +} from '@credo-ts/core' + +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { presentationDefinition } from '../../anoncreds/tests/fixtures/presentation-definition' +import { createDidKidVerificationMethod } from '../../core/tests' +import { waitForCredentialRecordSubject, waitForProofExchangeRecord } from '../../core/tests/helpers' + +import { setupAnonCredsTests } from './helpers/cheqdAnonCredsSetup' + +describe('anoncreds w3c data integrity e2e tests', () => { + let issuerId: string + let issuerAgent: AnonCredsTestsAgent + let holderAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let issuerHolderConnectionId: string + let holderIssuerConnectionId: string + + let issuerReplay: EventReplaySubject + let holderReplay: EventReplaySubject + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + afterEach(async () => { + await issuerAgent.shutdown() + await issuerAgent.wallet.delete() + await holderAgent.shutdown() + await holderAgent.wallet.delete() + }) + + test('cheqd issuance and verification flow starting from offer without revocation', async () => { + ;({ + issuerAgent, + issuerReplay, + holderAgent, + holderReplay, + credentialDefinitionId, + issuerHolderConnectionId, + holderIssuerConnectionId, + issuerId, + } = await setupAnonCredsTests({ + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['id', 'name', 'height', 'age'], + registries: [inMemoryRegistry], + })) + await anonCredsFlowTest({ + issuerId, + credentialDefinitionId, + issuerHolderConnectionId, + holderIssuerConnectionId, + issuerReplay, + holderReplay, + issuer: issuerAgent, + holder: holderAgent, + }) + }) +}) + +async function anonCredsFlowTest(options: { + issuer: AnonCredsTestsAgent + issuerId: string + holder: AnonCredsTestsAgent + issuerHolderConnectionId: string + holderIssuerConnectionId: string + issuerReplay: EventReplaySubject + holderReplay: EventReplaySubject + credentialDefinitionId: string +}) { + const { + credentialDefinitionId, + issuerHolderConnectionId, + holderIssuerConnectionId, + issuer, + issuerId, + holder, + issuerReplay, + holderReplay, + } = options + + const holderKdv = await createDidKidVerificationMethod(holder.context, '96213c3d7fc8d4d6754c7a0fd969598f') + const linkSecret = await holder.modules.anoncreds.createLinkSecret({ linkSecretId: 'linkSecretId' }) + expect(linkSecret).toBe('linkSecretId') + + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerId, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ + id: holderKdv.did, + claims: { name: 'John', age: '25', height: 173 }, + }), + }) + + // issuer offers credential + let issuerRecord = await issuer.credentials.offerCredential({ + protocolVersion: 'v2', + autoAcceptCredential: AutoAcceptCredential.Never, + connectionId: issuerHolderConnectionId, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + anonCredsLinkSecretBinding: { + credentialDefinitionId, + revocationRegistryDefinitionId: undefined, + revocationRegistryIndex: undefined, + }, + didCommSignedAttachmentBinding: {}, + }, + }, + }) + + // Holder processes and accepts offer + let holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.OfferReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holder.credentials.acceptOffer({ + credentialRecordId: holderRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: { + anonCredsLinkSecret: { + linkSecretId: 'linkSecretId', + }, + }, + }, + }) + + // issuer receives request and accepts + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.RequestReceived, + threadId: holderRecord.threadId, + }) + issuerRecord = await issuer.credentials.acceptRequest({ + credentialRecordId: issuerRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: {}, + }, + }) + + holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.CredentialReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holder.credentials.acceptCredential({ + credentialRecordId: holderRecord.id, + }) + + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.Done, + threadId: holderRecord.threadId, + }) + + expect(holderRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + credentialDefinitionId, + schemaId: expect.any(String), + }, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: { + v_prime: expect.any(String), + vr_prime: null, + }, + nonce: expect.any(String), + link_secret_name: 'linkSecretId', + }, + }, + }, + state: CredentialState.Done, + }) + + const tags = holderRecord.getTags() + expect(tags.credentialIds).toHaveLength(1) + + await expect( + holder.dependencyManager + .resolve(W3cCredentialService) + .getCredentialRecordById(holder.context, tags.credentialIds[0]) + ).resolves + + let issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuer, { + state: ProofState.ProposalReceived, + }) + + const pdCopy = JSON.parse(JSON.stringify(presentationDefinition)) + pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => delete ide.constraints?.statuses) + pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => { + if (ide.constraints.fields && ide.constraints.fields[0].filter?.const) { + ide.constraints.fields[0].filter.const = issuerId + } + }) + + let holderProofExchangeRecord = await holder.proofs.proposeProof({ + protocolVersion: 'v2', + connectionId: holderIssuerConnectionId, + proofFormats: { + presentationExchange: { + presentationDefinition: pdCopy, + }, + }, + }) + + let issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + + let holderProofExchangeRecordPromise = waitForProofExchangeRecord(holder, { + state: ProofState.RequestReceived, + }) + + issuerProofExchangeRecord = await issuer.proofs.acceptProposal({ + proofRecordId: issuerProofExchangeRecord.id, + }) + + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const requestedCredentials = await holder.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const selectedCredentials = requestedCredentials.proofFormats.presentationExchange?.credentials + if (!selectedCredentials) { + throw new Error('No credentials found for presentation exchange') + } + + issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuer, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holder.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { + presentationExchange: { + credentials: selectedCredentials, + }, + }, + }) + issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + + holderProofExchangeRecordPromise = waitForProofExchangeRecord(holder, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + await issuer.proofs.acceptPresentation({ proofRecordId: issuerProofExchangeRecord.id }) + + holderProofExchangeRecord = await holderProofExchangeRecordPromise +} diff --git a/packages/cheqd/tests/helpers/cheqdAnonCredsSetup.ts b/packages/cheqd/tests/helpers/cheqdAnonCredsSetup.ts new file mode 100644 index 0000000000..5832feb809 --- /dev/null +++ b/packages/cheqd/tests/helpers/cheqdAnonCredsSetup.ts @@ -0,0 +1,356 @@ +import type { EventReplaySubject } from '../../../core/tests' +import type { CheqdDidCreateOptions } from '../../src' +import type { + AnonCredsRegisterCredentialDefinitionOptions, + AnonCredsSchema, + RegisterCredentialDefinitionReturnStateFinished, + RegisterSchemaReturnStateFinished, + AnonCredsRegistry, +} from '@credo-ts/anoncreds' +import type { AutoAcceptProof, ConnectionRecord, AutoAcceptCredential } from '@credo-ts/core' + +import { + AnonCredsCredentialFormatService, + AnonCredsProofFormatService, + AnonCredsModule, + DataIntegrityCredentialFormatService, +} from '@credo-ts/anoncreds' +import { + CacheModule, + InMemoryLruCache, + Agent, + CredoError, + CredentialEventTypes, + CredentialsModule, + ProofEventTypes, + ProofsModule, + V2CredentialProtocol, + V2ProofProtocol, + DidsModule, + PresentationExchangeProofFormatService, + TypedArrayEncoder, +} from '@credo-ts/core' +import { randomUUID } from 'crypto' + +import { InMemoryAnonCredsRegistry } from '../../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { anoncreds } from '../../../anoncreds/tests/helpers' +import { sleep } from '../../../core/src/utils/sleep' +import { + setupSubjectTransports, + setupEventReplaySubjects, + testLogger, + getInMemoryAgentOptions, + makeConnection, +} from '../../../core/tests' +import { CheqdDidRegistrar, CheqdDidResolver, CheqdModule } from '../../src' +import { getCheqdModuleConfig } from '../setupCheqdModule' + +// Helper type to get the type of the agents (with the custom modules) for the credential tests +export type AnonCredsTestsAgent = Agent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ReturnType & { mediationRecipient?: any; mediator?: any } +> + +export const getAnonCredsModules = ({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + seed, + rpcUrl, +}: { + seed?: string + rpcUrl?: string + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] +} = {}) => { + const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() + const anonCredsCredentialFormatService = new AnonCredsCredentialFormatService() + const anonCredsProofFormatService = new AnonCredsProofFormatService() + const presentationExchangeProofFormatService = new PresentationExchangeProofFormatService() + + const modules = { + cheqdSdk: new CheqdModule(getCheqdModuleConfig(seed, rpcUrl)), + credentials: new CredentialsModule({ + autoAcceptCredentials, + credentialProtocols: [ + new V2CredentialProtocol({ + credentialFormats: [dataIntegrityCredentialFormatService, anonCredsCredentialFormatService], + }), + ], + }), + proofs: new ProofsModule({ + autoAcceptProofs, + proofProtocols: [ + new V2ProofProtocol({ + proofFormats: [anonCredsProofFormatService, presentationExchangeProofFormatService], + }), + ], + }), + anoncreds: new AnonCredsModule({ + registries: registries ?? [new InMemoryAnonCredsRegistry()], + anoncreds, + }), + dids: new DidsModule({ + registrars: [new CheqdDidRegistrar()], + resolvers: [new CheqdDidResolver()], + }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 100 }), + }), + } as const + + return modules +} + +interface SetupAnonCredsTestsReturn { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: CreateConnections extends true ? string : undefined + holderIssuerConnectionId: CreateConnections extends true ? string : undefined + + issuerId: string + + verifierHolderConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + holderVerifierConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + + verifierAgent: VerifierName extends string ? AnonCredsTestsAgent : undefined + verifierReplay: VerifierName extends string ? EventReplaySubject : undefined + + schemaId: string + credentialDefinitionId: string +} + +async function createDid(agent: AnonCredsTestsAgent) { + const privateKey = TypedArrayEncoder.fromString('000000000000000000000000000cheqd') + const did = await agent.dids.create({ + method: 'cheqd', + secret: { + verificationMethod: { + id: 'key-10', + type: 'Ed25519VerificationKey2020', + privateKey, + }, + }, + options: { + network: 'testnet', + methodSpecificIdAlgo: 'uuid', + }, + }) + expect(did.didState).toMatchObject({ state: 'finished' }) + return did.didState.did as string +} + +export async function setupAnonCredsTests< + VerifierName extends string | undefined = undefined, + CreateConnections extends boolean = true +>({ + issuerName, + holderName, + verifierName, + autoAcceptCredentials, + autoAcceptProofs, + attributeNames, + createConnections, + registries, +}: { + issuerName: string + holderName: string + verifierName?: VerifierName + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + attributeNames: string[] + createConnections?: CreateConnections + registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] +}): Promise> { + const issuerAgent = new Agent( + getInMemoryAgentOptions( + issuerName, + { + endpoints: ['rxjs:issuer'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + + const holderAgent = new Agent( + getInMemoryAgentOptions( + holderName, + { + endpoints: ['rxjs:holder'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + + const verifierAgent = verifierName + ? new Agent( + getInMemoryAgentOptions( + verifierName, + { + endpoints: ['rxjs:verifier'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + : undefined + + setupSubjectTransports(verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent]) + const [issuerReplay, holderReplay, verifierReplay] = setupEventReplaySubjects( + verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent], + [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + ) + + await issuerAgent.initialize() + await holderAgent.initialize() + if (verifierAgent) await verifierAgent.initialize() + + const issuerId = await createDid(issuerAgent) + + // Create default link secret for holder + await holderAgent.modules.anoncreds.createLinkSecret({ + linkSecretId: 'default', + setAsDefault: true, + }) + + const { credentialDefinition, schema } = await prepareForAnonCredsIssuance(issuerAgent, { + attributeNames, + issuerId, + }) + + let issuerHolderConnection: ConnectionRecord | undefined + let holderIssuerConnection: ConnectionRecord | undefined + let verifierHolderConnection: ConnectionRecord | undefined + let holderVerifierConnection: ConnectionRecord | undefined + + if (createConnections ?? true) { + ;[issuerHolderConnection, holderIssuerConnection] = await makeConnection(issuerAgent, holderAgent) + + if (verifierAgent) { + ;[holderVerifierConnection, verifierHolderConnection] = await makeConnection(holderAgent, verifierAgent) + } + } + + return { + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + issuerId, + + verifierAgent: verifierName ? verifierAgent : undefined, + verifierReplay: verifierName ? verifierReplay : undefined, + + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + schemaId: schema.schemaId, + + issuerHolderConnectionId: issuerHolderConnection?.id, + holderIssuerConnectionId: holderIssuerConnection?.id, + holderVerifierConnectionId: holderVerifierConnection?.id, + verifierHolderConnectionId: verifierHolderConnection?.id, + } as unknown as SetupAnonCredsTestsReturn +} + +export async function prepareForAnonCredsIssuance( + agent: Agent, + { attributeNames, issuerId }: { attributeNames: string[]; issuerId: string } +) { + //const key = await agent.wallet.createKey({ keyType: KeyType.Ed25519 }) + + const schema = await registerSchema(agent, { + // TODO: update attrNames to attributeNames + attrNames: attributeNames, + name: `Schema ${randomUUID()}`, + version: '1.0', + issuerId, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + const credentialDefinition = await registerCredentialDefinition(agent, { + schemaId: schema.schemaId, + issuerId, + tag: 'default', + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + return { + schema: { + ...schema, + schemaId: schema.schemaId, + }, + credentialDefinition: { + ...credentialDefinition, + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + }, + } +} + +async function registerSchema( + agent: AnonCredsTestsAgent, + schema: AnonCredsSchema +): Promise { + const { schemaState } = await agent.modules.anoncreds.registerSchema({ + schema, + options: {}, + }) + + testLogger.test(`created schema with id ${schemaState.schemaId}`, schema) + + if (schemaState.state !== 'finished') { + throw new CredoError(`Schema not created: ${schemaState.state === 'failed' ? schemaState.reason : 'Not finished'}`) + } + + return schemaState +} + +async function registerCredentialDefinition( + agent: AnonCredsTestsAgent, + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions +): Promise { + const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition, + options: { + supportRevocation: false, + }, + }) + + if (credentialDefinitionState.state !== 'finished') { + throw new CredoError( + `Credential definition not created: ${ + credentialDefinitionState.state === 'failed' ? credentialDefinitionState.reason : 'Not finished' + }` + ) + } + + return credentialDefinitionState +} From 26739a57d4f520c66b854a8ec602d14595a74a42 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 18 Feb 2024 09:28:16 +0100 Subject: [PATCH 33/38] fix: incorporate feedback --- packages/anoncreds/src/AnonCredsApi.ts | 4 +- .../AnonCredsDataIntegrityService.ts | 402 ++++-------------- .../anoncreds-rs/AnonCredsRsHolderService.ts | 224 +++++----- .../AnonCredsRsVerifierService.ts | 68 ++- .../AnonCredsRsHolderService.test.ts | 43 +- .../__tests__/AnonCredsRsServices.test.ts | 21 +- .../src/anoncreds-rs/__tests__/helpers.ts | 4 +- packages/anoncreds/src/anoncreds-rs/utils.ts | 118 +++++ .../AnonCredsCredentialFormatService.ts | 2 +- .../formats/AnonCredsProofFormatService.ts | 2 +- .../DataIntegrityCredentialFormatService.ts | 210 ++++----- .../LegacyIndyCredentialFormatService.ts | 2 +- .../formats/LegacyIndyProofFormatService.ts | 2 +- .../legacy-indy-format-services.test.ts | 18 +- .../src/models/W3cAnonCredsMetadata.ts | 0 packages/anoncreds/src/models/utils.ts | 32 ++ .../src/services/AnonCredsHolderService.ts | 19 +- .../services/AnonCredsHolderServiceOptions.ts | 58 ++- .../src/services/AnonCredsVerifierService.ts | 4 +- .../AnonCredsVerifierServiceOptions.ts | 27 +- .../w3cCredentialRecordMigration.test.ts | 21 +- .../0.4-0.5/anonCredsCredentialRecord.ts | 36 +- .../W3cAnonCredsCredentialRecord.test.ts | 75 ++++ .../anoncreds/src/utils/anonCredsObjects.ts | 28 +- packages/anoncreds/src/utils/linkSecret.ts | 23 + packages/anoncreds/src/utils/metadata.ts | 2 +- .../anoncreds/src/utils/w3cAnonCredsUtils.ts | 30 +- .../tests/InMemoryAnonCredsRegistry.ts | 5 +- .../anoncreds/tests/anoncreds-flow.test.ts | 19 +- packages/anoncreds/tests/anoncredsSetup.ts | 155 +++---- .../data-integrity-flow-anoncreds.test.ts | 23 +- .../tests/data-integrity-flow-w3c.test.ts | 2 +- .../tests/data-integrity-flow.test.ts | 2 +- .../tests/fixtures/presentation-definition.ts | 4 +- packages/anoncreds/tests/indy-flow.test.ts | 19 +- .../tests/cheqd-data-integrity.e2e.test.ts | 348 +++++++-------- .../tests/helpers/cheqdAnonCredsSetup.ts | 356 ---------------- .../DataIntegrityCredentialFormat.ts | 4 +- .../dataIntegrity/dataIntegrityExchange.ts | 133 +++++- .../DifPresentationExchangeService.ts | 22 +- ...fPresentationExchangeProofFormatService.ts | 12 +- .../data-integrity/SignatureSuiteRegistry.ts | 4 +- .../models/DataIntegrityProof.ts | 3 +- .../models/IAnonCredsDataIntegrityService.ts | 23 +- .../vc/repository/W3cCredentialRecord.ts | 31 +- .../__tests__/W3cCredentialRecord.test.ts | 79 ---- 46 files changed, 1176 insertions(+), 1543 deletions(-) create mode 100644 packages/anoncreds/src/anoncreds-rs/utils.ts delete mode 100644 packages/anoncreds/src/models/W3cAnonCredsMetadata.ts create mode 100644 packages/anoncreds/src/models/utils.ts create mode 100644 packages/anoncreds/src/utils/__tests__/W3cAnonCredsCredentialRecord.test.ts delete mode 100644 packages/cheqd/tests/helpers/cheqdAnonCredsSetup.ts diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 13ccc3b27a..d4ddbf9073 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -601,8 +601,8 @@ export class AnonCredsApi { } } - public async getCredential(credentialId: string) { - return this.anonCredsHolderService.getCredential(this.agentContext, { credentialId }) + public async getCredential(id: string) { + return this.anonCredsHolderService.getCredential(this.agentContext, { id }) } public async getCredentials(options: GetCredentialsOptions) { diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts index eaa1e5246e..b7128d2dfa 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsDataIntegrityService.ts @@ -1,27 +1,16 @@ -import type { - AnonCredsSchema, - AnonCredsCredentialDefinition, - AnonCredsNonRevokedInterval, - AnonCredsProofRequest, - AnonCredsRequestedPredicate, -} from '../models' +import type { AnonCredsRsVerifierService } from './AnonCredsRsVerifierService' +import type { AnonCredsProofRequest, AnonCredsRequestedPredicate } from '../models' +import type { CredentialWithRevocationMetadata } from '../models/utils' +import type { AnonCredsCredentialProve, CreateW3cPresentationOptions, AnonCredsHolderService } from '../services' import type { AgentContext, IAnoncredsDataIntegrityService, AnoncredsDataIntegrityVerifyPresentation, DifPresentationExchangeDefinition, DifPresentationExchangeSubmission, - JsonObject, W3cCredentialRecord, W3cJsonLdVerifiableCredential, } from '@credo-ts/core' -import type { - CreateW3cPresentationOptions, - CredentialProve, - NonRevokedIntervalOverride, - VerifyW3cPresentationOptions, - W3cCredentialEntry, -} from '@hyperledger/anoncreds-shared' import type { Descriptor, FieldV2, InputDescriptorV1, InputDescriptorV2 } from '@sphereon/pex-models' import { JSONPath } from '@astronautlabs/jsonpath' @@ -30,57 +19,31 @@ import { Hasher, JsonTransformer, TypedArrayEncoder, - AnoncredsDataIntegrityCryptosuite, + ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE, deepEquality, injectable, ClaimFormat, } from '@credo-ts/core' -import { - W3cCredential as AnonCredsW3cCredential, - W3cPresentation as AnonCredsW3cPresentation, - CredentialRevocationState, - RevocationRegistryDefinition, - RevocationStatusList, -} from '@hyperledger/anoncreds-shared' import BigNumber from 'bn.js' -import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' -import { - assertBestPracticeRevocationInterval, - fetchCredentialDefinition, - fetchRevocationRegistryDefinition, - fetchRevocationStatusList, - fetchSchema, -} from '../utils' +import { AnonCredsHolderServiceSymbol, AnonCredsVerifierServiceSymbol } from '../services' +import { fetchCredentialDefinitions, fetchSchemas } from '../utils/anonCredsObjects' +import { assertLinkSecretsMatch } from '../utils/linkSecret' import { getAnonCredsTagsFromRecord } from '../utils/w3cAnonCredsUtils' -import { AnonCredsRsHolderService } from './AnonCredsRsHolderService' -import { AnonCredsRsVerifierService } from './AnonCredsRsVerifierService' - -export interface CredentialWithMetadata { - credential: JsonObject - nonRevoked?: AnonCredsNonRevokedInterval - timestamp?: number -} - -export interface RevocationRegistryFetchMetadata { - timestamp?: number - revocationRegistryId: string - revocationRegistryIndex?: number - nonRevokedInterval: AnonCredsNonRevokedInterval -} +import { getW3cAnonCredsCredentialMetadata } from './utils' export type PathComponent = string | number @injectable() export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegrityService { private getDataIntegrityProof(credential: W3cJsonLdVerifiableCredential) { - const cryptosuite = AnoncredsDataIntegrityCryptosuite + const cryptosuite = ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE if (Array.isArray(credential.proof)) { const proof = credential.proof.find( (proof) => proof.type === 'DataIntegrityProof' && 'cryptosuite' in proof && proof.cryptosuite === cryptosuite ) - if (!proof) throw new CredoError(`Could not find ${AnoncredsDataIntegrityCryptosuite} proof`) + if (!proof) throw new CredoError(`Could not find ${ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE} proof`) return proof } @@ -88,7 +51,7 @@ export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegritySer credential.proof.type !== 'DataIntegrityProof' || !('cryptosuite' in credential.proof && credential.proof.cryptosuite === cryptosuite) ) { - throw new CredoError(`Could not find ${AnoncredsDataIntegrityCryptosuite} proof`) + throw new CredoError(`Could not find ${ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE} proof`) } return credential.proof @@ -104,7 +67,11 @@ export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegritySer return result } - private getCredentialMetadataForDescriptor(descriptorMapObject: Descriptor, selectedCredentials: JsonObject[]) { + private async getCredentialMetadataForDescriptor( + agentContext: AgentContext, + descriptorMapObject: Descriptor, + selectedCredentials: W3cJsonLdVerifiableCredential[] + ) { const credentialExtractionResult = this.extractPathNodes({ verifiableCredential: selectedCredentials }, [ descriptorMapObject.path, ]) @@ -113,173 +80,18 @@ export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegritySer throw new Error('Could not extract credential from presentation submission') } - const credentialJson = credentialExtractionResult[0].value as JsonObject + const w3cJsonLdVerifiableCredential = credentialExtractionResult[0].value as W3cJsonLdVerifiableCredential + const w3cJsonLdVerifiableCredentialJson = JsonTransformer.toJSON(w3cJsonLdVerifiableCredential) - // FIXME: Is this required? - const entryIndex = selectedCredentials.findIndex((credential) => deepEquality(credential, credentialJson)) + const entryIndex = selectedCredentials.findIndex((credential) => + deepEquality(JsonTransformer.toJSON(credential), w3cJsonLdVerifiableCredentialJson) + ) if (entryIndex === -1) throw new CredoError('Could not find selected credential') - const { credentialDefinitionId, revocationRegistryId, schemaId } = AnonCredsW3cCredential.fromJson(credentialJson) - return { entryIndex, - credentialJson, - credentialDefinitionId, - revocationRegistryId, - schemaId, - } - } - - private async getRevocationMetadata( - agentContext: AgentContext, - revocationRegistryFetchMetadata: RevocationRegistryFetchMetadata, - mustHaveTimeStamp = false - ) { - let nonRevokedIntervalOverride: NonRevokedIntervalOverride | undefined - - const { revocationRegistryId, revocationRegistryIndex, nonRevokedInterval, timestamp } = - revocationRegistryFetchMetadata - if (!revocationRegistryId || !nonRevokedInterval || (mustHaveTimeStamp && !timestamp)) { - throw new CredoError('Invalid revocation metadata') - } - - // Make sure the revocation interval follows best practices from Aries RFC 0441 - assertBestPracticeRevocationInterval(nonRevokedInterval) - - const revocaitonRegistryDefinitionResult = await fetchRevocationRegistryDefinition( - agentContext, - revocationRegistryId - ) - - const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const { tailsFilePath } = await tailsFileService.getTailsFile(agentContext, { - revocationRegistryDefinition: revocaitonRegistryDefinitionResult.revocationRegistryDefinition, - }) - - const timestampToFetch = timestamp ?? nonRevokedInterval.to - if (!timestampToFetch) throw new CredoError('Timestamp to fetch is required') - - const { revocationStatusList: _revocationStatusList } = await fetchRevocationStatusList( - agentContext, - revocationRegistryId, - timestampToFetch - ) - const updatedTimestamp = timestamp ?? _revocationStatusList.timestamp - - const revocationRegistryDefinition = RevocationRegistryDefinition.fromJson( - revocaitonRegistryDefinitionResult.revocationRegistryDefinition as unknown as JsonObject - ) - const revocationStatusList = RevocationStatusList.fromJson(_revocationStatusList as unknown as JsonObject) - const revocationState = revocationRegistryIndex - ? CredentialRevocationState.create({ - revocationRegistryIndex: Number(revocationRegistryIndex), - revocationRegistryDefinition: revocationRegistryDefinition, - tailsPath: tailsFilePath, - revocationStatusList, - }) - : undefined - - const requestedFrom = nonRevokedInterval.from - if (requestedFrom && requestedFrom > timestampToFetch) { - const { revocationStatusList: overrideRevocationStatusList } = await fetchRevocationStatusList( - agentContext, - revocationRegistryId, - requestedFrom - ) - - const vdrTimestamp = overrideRevocationStatusList?.timestamp - if (vdrTimestamp && vdrTimestamp === timestampToFetch) { - nonRevokedIntervalOverride = { - overrideRevocationStatusListTimestamp: timestampToFetch, - requestedFromTimestamp: requestedFrom, - revocationRegistryDefinitionId: revocationRegistryId, - } - } else { - throw new CredoError( - `VDR timestamp for ${requestedFrom} does not correspond to the one provided in proof identifiers. Expected: ${updatedTimestamp} and received ${vdrTimestamp}` - ) - } - } - - return { - updatedTimestamp, - revocationRegistryId, - revocationRegistryDefinition, - revocationStatusList, - revocationState, - nonRevokedIntervalOverride, - } - } - - private async getSchemas(agentContext: AgentContext, schemaIds: Set) { - const schemaFetchPromises = [...schemaIds].map(async (schemaId): Promise<[string, AnonCredsSchema]> => { - const { schema } = await fetchSchema(agentContext, schemaId) - return [schemaId, schema] - }) - - const schemas = Object.fromEntries(await Promise.all(schemaFetchPromises)) - return schemas - } - - private async getCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { - const credentialDefinitionEntries = [...credentialDefinitionIds].map( - async (credentialDefinitionId): Promise<[string, AnonCredsCredentialDefinition]> => { - const { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialDefinitionId) - return [credentialDefinitionId, credentialDefinition] - } - ) - - const credentialDefinitions = Object.fromEntries(await Promise.all(credentialDefinitionEntries)) - return credentialDefinitions - } - - private async getLinkSecret(agentContext: AgentContext, credentialRecord: W3cCredentialRecord[]) { - const linkSecrets = credentialRecord - .map((record) => getAnonCredsTagsFromRecord(record)?.anonCredsLinkSecretId) - .filter((linkSecretId): linkSecretId is string => linkSecretId !== undefined) - - const anoncredsHolderService = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) - return anoncredsHolderService.getLinkSecret(agentContext, linkSecrets) - } - - private getPresentationMetadata = async ( - agentContext: AgentContext, - options: { - credentialsWithMetadata: CredentialWithMetadata[] - credentialsProve: CredentialProve[] - schemaIds: Set - credentialDefinitionIds: Set - } - ) => { - const { credentialDefinitionIds, schemaIds, credentialsWithMetadata, credentialsProve } = options - - const credentials: W3cCredentialEntry[] = await Promise.all( - credentialsWithMetadata.map(async ({ credential, nonRevoked }) => { - const { revocationRegistryIndex, revocationRegistryId, timestamp } = AnonCredsW3cCredential.fromJson(credential) - - if (!nonRevoked) return { credential, revocationState: undefined, timestamp: undefined } - - if (!revocationRegistryId || !revocationRegistryIndex) throw new CredoError('Missing revocation metadata') - - const { revocationState, updatedTimestamp } = await this.getRevocationMetadata(agentContext, { - nonRevokedInterval: nonRevoked, - timestamp, - revocationRegistryIndex, - revocationRegistryId, - }) - - return { credential, revocationState, timestamp: updatedTimestamp } - }) - ) - - const schemas = await this.getSchemas(agentContext, schemaIds) - const credentialDefinitions = await this.getCredentialDefinitions(agentContext, credentialDefinitionIds) - - return { - schemas, - credentialDefinitions, - credentialsProve, - credentials, + credential: selectedCredentials[entryIndex], + ...getW3cAnonCredsCredentialMetadata(w3cJsonLdVerifiableCredential), } } @@ -323,27 +135,6 @@ export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegritySer return predicates } - private getRevocationMetadataForCredentials = async ( - agentContext: AgentContext, - credentialsWithMetadata: CredentialWithMetadata[] - ) => { - const revocationMetadataFetchPromises = credentialsWithMetadata - .filter((cwm) => cwm.nonRevoked) - .map(async (credentialWithMetadata) => { - const { revocationRegistryIndex, revocationRegistryId, timestamp } = AnonCredsW3cCredential.fromJson( - credentialWithMetadata.credential - ) - return await this.getRevocationMetadata(agentContext, { - nonRevokedInterval: credentialWithMetadata.nonRevoked as AnonCredsNonRevokedInterval, - timestamp: timestamp, - revocationRegistryId, - revocationRegistryIndex, - }) - }) - - return await Promise.all(revocationMetadataFetchPromises) - } - private getClaimNameForField(field: FieldV2) { if (!field.path) throw new CredoError('Field path is required') // fixme: could the path start otherwise? @@ -360,15 +151,16 @@ export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegritySer } public createAnonCredsProofRequestAndMetadata = async ( + agentContext: AgentContext, presentationDefinition: DifPresentationExchangeDefinition, presentationSubmission: DifPresentationExchangeSubmission, - credentials: JsonObject[], + credentials: W3cJsonLdVerifiableCredential[], challenge: string ) => { - const credentialsProve: CredentialProve[] = [] + const credentialsProve: AnonCredsCredentialProve[] = [] const schemaIds = new Set() const credentialDefinitionIds = new Set() - const credentialsWithMetadata: CredentialWithMetadata[] = [] + const credentialsWithMetadata: CredentialWithRevocationMetadata[] = [] const hash = Hasher.hash(TypedArrayEncoder.fromString(challenge), 'sha-256') const nonce = new BigNumber(hash).toString().slice(0, 20) @@ -401,8 +193,8 @@ export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegritySer const fields = descriptor.constraints?.fields if (!fields) throw new CredoError('Unclear mapping of constraint with no fields.') - const { entryIndex, schemaId, credentialDefinitionId, revocationRegistryId, credentialJson } = - this.getCredentialMetadataForDescriptor(descriptorMapObject, credentials) + const { entryIndex, schemaId, credentialDefinitionId, revocationRegistryId, credential } = + await this.getCredentialMetadataForDescriptor(agentContext, descriptorMapObject, credentials) schemaIds.add(schemaId) credentialDefinitionIds.add(credentialDefinitionId) @@ -413,7 +205,7 @@ export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegritySer } credentialsWithMetadata.push({ - credential: credentialJson, + credential, nonRevoked: requiresRevocationStatus ? nonRevokedInterval : undefined, }) @@ -461,109 +253,81 @@ export class AnonCredsDataIntegrityService implements IAnoncredsDataIntegritySer options: { presentationDefinition: DifPresentationExchangeDefinition presentationSubmission: DifPresentationExchangeSubmission - selectedCredentials: JsonObject[] selectedCredentialRecords: W3cCredentialRecord[] challenge: string } ) { - const { - presentationDefinition, - presentationSubmission, - selectedCredentialRecords, - selectedCredentials, - challenge, - } = options + const { presentationDefinition, presentationSubmission, selectedCredentialRecords, challenge } = options - const linkSecret = await this.getLinkSecret(agentContext, selectedCredentialRecords) - - const { anonCredsProofRequest, ...metadata } = await this.createAnonCredsProofRequestAndMetadata( - presentationDefinition, - presentationSubmission, - selectedCredentials, - challenge - ) - - const presentationMetadata = await this.getPresentationMetadata(agentContext, metadata) + const linkSecrets = selectedCredentialRecords + .map((record) => getAnonCredsTagsFromRecord(record)?.anonCredsLinkSecretId) + .filter((linkSecretId): linkSecretId is string => linkSecretId !== undefined) - const { schemas, credentialDefinitions, credentialsProve, credentials } = presentationMetadata + const linkSecretId = assertLinkSecretsMatch(agentContext, linkSecrets) - const anonCredsRsHolderService = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) + const { anonCredsProofRequest, credentialDefinitionIds, schemaIds, credentialsProve, credentialsWithMetadata } = + await this.createAnonCredsProofRequestAndMetadata( + agentContext, + presentationDefinition, + presentationSubmission, + selectedCredentialRecords.map((record) => record.credential) as W3cJsonLdVerifiableCredential[], + challenge + ) const createPresentationOptions: CreateW3cPresentationOptions = { - credentials, - schemas: schemas as unknown as Record, - credentialDefinitions: credentialDefinitions as unknown as Record, - linkSecret, + linkSecretId, + proofRequest: anonCredsProofRequest, credentialsProve, - presentationRequest: anonCredsProofRequest as unknown as JsonObject, + credentialsWithRevocationMetadata: credentialsWithMetadata, + schemas: await fetchSchemas(agentContext, schemaIds), + credentialDefinitions: await fetchCredentialDefinitions(agentContext, credentialDefinitionIds), } - const w3cPresentation = await anonCredsRsHolderService.createW3cPresentation(createPresentationOptions) - return w3cPresentation as JsonObject + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + const w3cPresentation = await anonCredsHolderService.createW3cPresentation(agentContext, createPresentationOptions) + return w3cPresentation } public async verifyPresentation(agentContext: AgentContext, options: AnoncredsDataIntegrityVerifyPresentation) { const { presentation, presentationDefinition, presentationSubmission, challenge } = options - let anonCredsW3cPresentation: AnonCredsW3cPresentation | undefined - - let result = false - const credentialDefinitionIds = new Set() - try { - const verifiableCredentials = Array.isArray(presentation.verifiableCredential) - ? presentation.verifiableCredential - : [presentation.verifiableCredential] - - for (const verifiableCredential of verifiableCredentials) { - if (verifiableCredential.claimFormat === ClaimFormat.LdpVc) { - const proof = this.getDataIntegrityProof(verifiableCredential) - credentialDefinitionIds.add(proof.verificationMethod) - } else { - throw new CredoError('Unsupported credential type') - } - } - const verifiableCredentialsJson = verifiableCredentials.map((credential) => JsonTransformer.toJSON(credential)) - const { anonCredsProofRequest, ...metadata } = await this.createAnonCredsProofRequestAndMetadata( - presentationDefinition, - presentationSubmission, - verifiableCredentialsJson, - challenge - ) - const revocationMetadata = await this.getRevocationMetadataForCredentials( - agentContext, - metadata.credentialsWithMetadata - ) + const verifiableCredentials = Array.isArray(presentation.verifiableCredential) + ? presentation.verifiableCredential + : [presentation.verifiableCredential] - const credentialDefinitions = await this.getCredentialDefinitions(agentContext, credentialDefinitionIds) - const schemaIds = new Set(Object.values(credentialDefinitions).map((cd) => cd.schemaId)) - const schemas = await this.getSchemas(agentContext, schemaIds) + for (const verifiableCredential of verifiableCredentials) { + if (verifiableCredential.claimFormat === ClaimFormat.LdpVc) { + const proof = this.getDataIntegrityProof(verifiableCredential) + credentialDefinitionIds.add(proof.verificationMethod) + } else { + throw new CredoError('Unsupported credential type') + } + } - const presentationJson = JsonTransformer.toJSON(presentation) - anonCredsW3cPresentation = AnonCredsW3cPresentation.fromJson(presentationJson) + const { anonCredsProofRequest, credentialsWithMetadata } = await this.createAnonCredsProofRequestAndMetadata( + agentContext, + presentationDefinition, + presentationSubmission, + verifiableCredentials as W3cJsonLdVerifiableCredential[], + challenge + ) - const revocationRegistryDefinitions: Record = {} - revocationMetadata.forEach( - (rm) => (revocationRegistryDefinitions[rm.revocationRegistryId] = rm.revocationRegistryDefinition) - ) + const credentialDefinitions = await fetchCredentialDefinitions(agentContext, credentialDefinitionIds) + const schemaIds = new Set(Object.values(credentialDefinitions).map((cd) => cd.schemaId)) + const schemas = await fetchSchemas(agentContext, schemaIds) - const verificationOptions: VerifyW3cPresentationOptions = { - presentationRequest: anonCredsProofRequest as unknown as JsonObject, - schemas: schemas as unknown as Record, - credentialDefinitions: credentialDefinitions as unknown as Record, - revocationRegistryDefinitions, - revocationStatusLists: revocationMetadata.map((rm) => rm.revocationStatusList), - nonRevokedIntervalOverrides: revocationMetadata - .filter((rm) => rm.nonRevokedIntervalOverride) - .map((rm) => rm.nonRevokedIntervalOverride as NonRevokedIntervalOverride), - } - const anonCredsRsVerifierService = agentContext.dependencyManager.resolve(AnonCredsRsVerifierService) - result = anonCredsRsVerifierService.verifyW3cPresentation(presentation, verificationOptions) - } finally { - anonCredsW3cPresentation?.handle.clear() - } + const anonCredsVerifierService = + agentContext.dependencyManager.resolve(AnonCredsVerifierServiceSymbol) - return result + return await anonCredsVerifierService.verifyW3cPresentation(agentContext, { + credentialsWithRevocationMetadata: credentialsWithMetadata, + presentation, + proofRequest: anonCredsProofRequest, + schemas, + credentialDefinitions, + }) } } diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts index 336892eee3..90ae114c8a 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts @@ -10,6 +10,7 @@ import type { AnonCredsCredentialInfo, AnonCredsProofRequestRestriction, } from '../models' +import type { CredentialWithRevocationMetadata } from '../models/utils' import type { AnonCredsCredentialRecord } from '../repository' import type { GetCredentialsForProofRequestOptions, @@ -24,14 +25,20 @@ import type { GetCredentialsOptions, StoreCredentialOptions, } from '../services' +import type { + AnonCredsCredentialProve, + CreateW3cPresentationOptions, + LegacyToW3cCredentialOptions, + W3cToLegacyCredentialOptions, +} from '../services/AnonCredsHolderServiceOptions' import type { AnonCredsCredentialRequestMetadata, W3cAnoncredsCredentialMetadata } from '../utils/metadata' import type { AgentContext, Query, SimpleQuery } from '@credo-ts/core' import type { - CreateW3cPresentationOptions, CredentialEntry, CredentialProve, CredentialRequestMetadata, JsonObject, + W3cCredentialEntry, } from '@hyperledger/anoncreds-shared' import { @@ -44,11 +51,12 @@ import { W3cJsonLdVerifiableCredential, injectable, utils, + W3cJsonLdVerifiablePresentation, } from '@credo-ts/core' import { Credential, - W3cPresentation, - W3cCredential as AW3cCredential, + W3cPresentation as W3cAnonCredsPresentation, + W3cCredential as W3cAnonCredsCredential, CredentialRequest, CredentialRevocationState, LinkSecret, @@ -56,7 +64,6 @@ import { RevocationRegistryDefinition, RevocationStatusList, anoncreds, - W3cCredential, } from '@hyperledger/anoncreds-shared' import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' @@ -70,9 +77,12 @@ import { isUnqualifiedIndyDid, isUnqualifiedSchemaId, } from '../utils/indyIdentifiers' +import { assertLinkSecretsMatch, getLinkSecret } from '../utils/linkSecret' import { W3cAnonCredsCredentialMetadataKey } from '../utils/metadata' import { getAnoncredsCredentialInfoFromRecord, getW3cRecordAnonCredsTags } from '../utils/w3cAnonCredsUtils' +import { getRevocationMetadata } from './utils' + @injectable() export class AnonCredsRsHolderService implements AnonCredsHolderService { public async createLinkSecret( @@ -85,24 +95,6 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } - public async getLinkSecret(agentContext: AgentContext, linkSecretIds: string[]): Promise { - // Get all requested credentials and take linkSecret. If it's not the same for every credential, throw error - const linkSecretsMatch = linkSecretIds.every((linkSecretId) => linkSecretId === linkSecretIds[0]) - if (!linkSecretsMatch) { - throw new AnonCredsRsError('All credentials in a Proof should have been issued using the same Link Secret') - } - - const linkSecretRecord = await agentContext.dependencyManager - .resolve(AnonCredsLinkSecretRepository) - .getByLinkSecretId(agentContext, linkSecretIds[0]) - - if (!linkSecretRecord.value) { - throw new AnonCredsRsError('Link Secret value not stored') - } - - return linkSecretRecord.value - } - public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise { const { credentialDefinitions, proofRequest, selectedCredentials, schemas } = options @@ -130,9 +122,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { let credentialRecord = retrievedCredentials.get(attribute.credentialId) if (!credentialRecord) { - const w3cCredentialRecord = await w3cCredentialRepository.findSingleByQuery(agentContext, { - anonCredsCredentialId: attribute.credentialId, - }) + const w3cCredentialRecord = await w3cCredentialRepository.findById(agentContext, attribute.credentialId) if (w3cCredentialRecord) { credentialRecord = w3cCredentialRecord @@ -189,7 +179,9 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const credential = credentialRecord instanceof W3cCredentialRecord - ? this.w3cToLegacyCredential(agentContext, credentialRecord.credential as W3cJsonLdVerifiableCredential) + ? await this.w3cToLegacyCredential(agentContext, { + credential: credentialRecord.credential as W3cJsonLdVerifiableCredential, + }) : (credentialRecord.credential as AnonCredsCredential) return { @@ -224,19 +216,9 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { entryIndex = entryIndex + 1 } - // Get all requested credentials and take linkSecret. If it's not the same for every credential, throw error - const linkSecretsMatch = credentials.every((item) => item.linkSecretId === credentials[0].linkSecretId) - if (!linkSecretsMatch) { - throw new AnonCredsRsError('All credentials in a Proof should have been issued using the same Link Secret') - } - - const linkSecretRecord = await agentContext.dependencyManager - .resolve(AnonCredsLinkSecretRepository) - .getByLinkSecretId(agentContext, credentials[0].linkSecretId) - - if (!linkSecretRecord.value) { - throw new AnonCredsRsError('Link Secret value not stored') - } + const linkSecretIds = credentials.map((item) => item.linkSecretId) + const linkSecretId = assertLinkSecretsMatch(agentContext, linkSecretIds) + const linkSecret = await getLinkSecret(agentContext, linkSecretId) presentation = Presentation.create({ credentialDefinitions: rsCredentialDefinitions, @@ -245,10 +227,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { credentials: credentials.map((entry) => entry.credentialEntry), credentialsProve, selfAttest: selectedCredentials.selfAttestedAttributes, - linkSecret: await this.getLinkSecret( - agentContext, - credentials.map((entry) => entry.linkSecretId) - ), + linkSecret, }) return presentation.toJson() as unknown as AnonCredsProof @@ -315,67 +294,67 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } - public w3cToLegacyCredential(agentContext: AgentContext, credential: W3cJsonLdVerifiableCredential) { - const credentialJson = JsonTransformer.toJSON(credential) - const w3cCredentialObj = W3cCredential.fromJson(credentialJson) - const legacyCredential = w3cCredentialObj.toLegacy().toJson() as unknown as AnonCredsCredential + public async w3cToLegacyCredential(agentContext: AgentContext, options: W3cToLegacyCredentialOptions) { + const credentialJson = JsonTransformer.toJSON(options.credential) + const w3cAnonCredsCredentialObj = W3cAnonCredsCredential.fromJson(credentialJson) + const w3cCredentialObj = w3cAnonCredsCredentialObj.toLegacy() + const legacyCredential = w3cCredentialObj.toJson() as unknown as AnonCredsCredential return legacyCredential } public async processW3cCredential( agentContext: AgentContext, - credential: W3cCredential, - process: { + credential: W3cJsonLdVerifiableCredential, + processOptions: { credentialDefinition: AnonCredsCredentialDefinition credentialRequestMetadata: AnonCredsCredentialRequestMetadata revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | undefined } ) { - const { credentialRequestMetadata, revocationRegistryDefinition, credentialDefinition } = process + const { credentialRequestMetadata, revocationRegistryDefinition, credentialDefinition } = processOptions const processCredentialOptions = { credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject, - linkSecret: await this.getLinkSecret(agentContext, [credentialRequestMetadata.link_secret_name]), + linkSecret: await getLinkSecret(agentContext, credentialRequestMetadata.link_secret_name), revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, credentialDefinition: credentialDefinition as unknown as JsonObject, } - const processedW3cCredential = credential.process(processCredentialOptions) - return processedW3cCredential + const credentialJson = JsonTransformer.toJSON(credential) + const w3cAnonCredsCredential = W3cAnonCredsCredential.fromJson(credentialJson) + const processedW3cAnonCredsCredential = w3cAnonCredsCredential.process(processCredentialOptions) + + const processedW3cJsonLdVerifiableCredential = JsonTransformer.fromJSON( + processedW3cAnonCredsCredential.toJson(), + W3cJsonLdVerifiableCredential + ) + return processedW3cJsonLdVerifiableCredential } - public async legacyToW3cCredential( - agentContext: AgentContext, - credential: AnonCredsCredential, - issuerId: string, - options?: { - credentialDefinition: AnonCredsCredentialDefinition - credentialRequestMetadata: AnonCredsCredentialRequestMetadata - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | undefined - } - ) { - let w3cJsonLdVerifiableCredential: W3cJsonLdVerifiableCredential - let anonCredsCredential: Credential | undefined - let w3cCredentialObj: W3cCredential | undefined + public async legacyToW3cCredential(agentContext: AgentContext, options: LegacyToW3cCredentialOptions) { + const { credential, issuerId, processOptions } = options + let w3cCredential: W3cJsonLdVerifiableCredential + let anonCredsCredential: Credential | undefined + let w3cCredentialObj: W3cAnonCredsCredential | undefined try { anonCredsCredential = Credential.fromJson(credential as unknown as JsonObject) - w3cCredentialObj = anonCredsCredential.toW3c({ - issuerId: issuerId, - w3cVersion: '1.1', - }) + w3cCredentialObj = anonCredsCredential.toW3c({ issuerId, w3cVersion: '1.1' }) - const jsonObject = options - ? (await this.processW3cCredential(agentContext, w3cCredentialObj, options)).toJson() - : w3cCredentialObj.toJson() + const w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON( + w3cCredentialObj.toJson(), + W3cJsonLdVerifiableCredential + ) - w3cJsonLdVerifiableCredential = JsonTransformer.fromJSON(jsonObject, W3cJsonLdVerifiableCredential) + w3cCredential = processOptions + ? await this.processW3cCredential(agentContext, w3cJsonLdVerifiableCredential, processOptions) + : w3cJsonLdVerifiableCredential } finally { anonCredsCredential?.handle?.clear() w3cCredentialObj?.handle?.clear() } - return w3cJsonLdVerifiableCredential + return w3cCredential } public async storeW3cCredential( @@ -396,9 +375,9 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { .getRegistryForIdentifier(agentContext, credential.issuerId).methodName // this thows an error if the link secret is not found - await this.getLinkSecret(agentContext, [credentialRequestMetadata.link_secret_name]) + await getLinkSecret(agentContext, credentialRequestMetadata.link_secret_name) - const { revocationRegistryId, revocationRegistryIndex } = AW3cCredential.fromJson( + const { revocationRegistryId, revocationRegistryIndex } = W3cAnonCredsCredential.fromJson( JsonTransformer.toJSON(credential) ) @@ -445,10 +424,14 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const w3cJsonLdCredential = credential instanceof W3cJsonLdVerifiableCredential ? credential - : await this.legacyToW3cCredential(agentContext, credential, credentialDefinition.issuerId, { - credentialRequestMetadata, - credentialDefinition, - revocationRegistryDefinition: revocationRegistry?.definition, + : await this.legacyToW3cCredential(agentContext, { + credential, + issuerId: credentialDefinition.issuerId, + processOptions: { + credentialRequestMetadata, + credentialDefinition, + revocationRegistryDefinition: revocationRegistry?.definition, + }, }) const w3cCredentialRecord = await this.storeW3cCredential(agentContext, { @@ -468,20 +451,15 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { options: GetCredentialOptions ): Promise { const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const w3cCredentialRecord = await w3cCredentialRepository.findSingleByQuery(agentContext, { - anonCredsCredentialId: options.credentialId, - }) + const w3cCredentialRecord = await w3cCredentialRepository.findById(agentContext, options.id) if (w3cCredentialRecord) return getAnoncredsCredentialInfoFromRecord(w3cCredentialRecord) const anonCredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) - const anonCredsCredentialRecord = await anonCredsCredentialRepository.getByCredentialId( - agentContext, - options.credentialId - ) + const anonCredsCredentialRecord = await anonCredsCredentialRepository.getByCredentialId(agentContext, options.id) agentContext.config.logger.warn( [ - `Querying legacy credential repository for credential with id ${options.credentialId}.`, + `Querying legacy credential repository for credential with id ${options.id}.`, `Please run the migration script to migrate credentials to the new w3c format.`, ].join('\n') ) @@ -552,11 +530,9 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { return [...legacyCredentials, ...credentials] } - public async deleteCredential(agentContext: AgentContext, credentialId: string): Promise { + public async deleteCredential(agentContext: AgentContext, id: string): Promise { const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const w3cCredentialRecord = await w3cCredentialRepository.findSingleByQuery(agentContext, { - anonCredsCredentialId: credentialId, - }) + const w3cCredentialRecord = await w3cCredentialRepository.findById(agentContext, id) if (w3cCredentialRecord) { await w3cCredentialRepository.delete(agentContext, w3cCredentialRecord) @@ -564,7 +540,7 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } const anoncredsCredentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository) - const anoncredsCredentialRecord = await anoncredsCredentialRepository.getByCredentialId(agentContext, credentialId) + const anoncredsCredentialRecord = await anoncredsCredentialRepository.getByCredentialId(agentContext, id) await anoncredsCredentialRepository.delete(agentContext, anoncredsCredentialRecord) } private async getLegacyCredentialsForProofRequest( @@ -810,14 +786,62 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { return query.length === 1 ? query[0] : { $or: query } } - public async createW3cPresentation(options: CreateW3cPresentationOptions) { - let presentation: W3cPresentation | undefined + private getPresentationMetadata = async ( + agentContext: AgentContext, + options: { + credentialsWithMetadata: CredentialWithRevocationMetadata[] + credentialsProve: AnonCredsCredentialProve[] + } + ) => { + const { credentialsWithMetadata, credentialsProve } = options + + const credentials: W3cCredentialEntry[] = await Promise.all( + credentialsWithMetadata.map(async ({ credential, nonRevoked }) => { + const credentialJson = JsonTransformer.toJSON(credential) + const { revocationRegistryIndex, revocationRegistryId, timestamp } = + W3cAnonCredsCredential.fromJson(credentialJson) + + if (!nonRevoked) return { credential: credentialJson, revocationState: undefined, timestamp: undefined } + + if (!revocationRegistryId || !revocationRegistryIndex) throw new CredoError('Missing revocation metadata') + + const { revocationState, updatedTimestamp } = await getRevocationMetadata(agentContext, { + nonRevokedInterval: nonRevoked, + timestamp, + revocationRegistryIndex, + revocationRegistryId, + }) + + return { credential: credentialJson, revocationState, timestamp: updatedTimestamp } + }) + ) + + return { credentialsProve, credentials } + } + + public async createW3cPresentation(agentContext: AgentContext, options: CreateW3cPresentationOptions) { + const { credentialsProve, credentials } = await this.getPresentationMetadata(agentContext, { + credentialsWithMetadata: options.credentialsWithRevocationMetadata, + credentialsProve: options.credentialsProve, + }) + + let w3cAnonCredsPresentation: W3cAnonCredsPresentation | undefined + let w3cPresentation: W3cJsonLdVerifiablePresentation try { - presentation = W3cPresentation.create(options) - const presentationJson = presentation.toJson() as unknown as JsonObject - return presentationJson + w3cAnonCredsPresentation = W3cAnonCredsPresentation.create({ + credentials, + credentialsProve, + schemas: options.schemas as unknown as Record, + credentialDefinitions: options.credentialDefinitions as unknown as Record, + presentationRequest: options.proofRequest as unknown as JsonObject, + linkSecret: await getLinkSecret(agentContext, options.linkSecretId), + }) + const presentationJson = w3cAnonCredsPresentation.toJson() as unknown as JsonObject + w3cPresentation = JsonTransformer.fromJSON(presentationJson, W3cJsonLdVerifiablePresentation) } finally { - presentation?.handle.clear() + w3cAnonCredsPresentation?.handle.clear() } + + return w3cPresentation } } diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts index 3916cc0fa0..25aa2005a3 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts @@ -1,17 +1,21 @@ import type { AnonCredsProof, AnonCredsProofRequest, AnonCredsNonRevokedInterval } from '../models' -import type { AnonCredsVerifierService, VerifyProofOptions } from '../services' -import type { AgentContext, W3cJsonLdVerifiablePresentation } from '@credo-ts/core' +import type { CredentialWithRevocationMetadata } from '../models/utils' +import type { AnonCredsVerifierService, VerifyProofOptions, VerifyW3cPresentationOptions } from '../services' +import type { AgentContext } from '@credo-ts/core' import type { JsonObject, NonRevokedIntervalOverride, - VerifyW3cPresentationOptions, + RevocationRegistryDefinition, + VerifyW3cPresentationOptions as VerifyAnonCredsW3cPresentationOptions, } from '@hyperledger/anoncreds-shared' import { JsonTransformer, injectable } from '@credo-ts/core' -import { Presentation, W3cPresentation } from '@hyperledger/anoncreds-shared' +import { Presentation, W3cPresentation, W3cCredential as AnonCredsW3cCredential } from '@hyperledger/anoncreds-shared' import { fetchRevocationStatusList } from '../utils' +import { getRevocationMetadata } from './utils' + @injectable() export class AnonCredsRsVerifierService implements AnonCredsVerifierService { public async verifyProof(agentContext: AgentContext, options: VerifyProofOptions): Promise { @@ -142,21 +146,59 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { } } - public verifyW3cPresentation( - presentation: W3cJsonLdVerifiablePresentation, - options: VerifyW3cPresentationOptions - ): boolean { - const presentationJson = JsonTransformer.toJSON(presentation) - const w3cPresentation = W3cPresentation.fromJson(presentationJson) + private getRevocationMetadataForCredentials = async ( + agentContext: AgentContext, + credentialsWithMetadata: CredentialWithRevocationMetadata[] + ) => { + const revocationMetadataFetchPromises = credentialsWithMetadata + .filter((cwm) => cwm.nonRevoked) + .map(async (credentialWithMetadata) => { + const w3cJsonLdVerifiableCredential = JsonTransformer.toJSON(credentialWithMetadata.credential) + const { revocationRegistryIndex, revocationRegistryId, timestamp } = + AnonCredsW3cCredential.fromJson(w3cJsonLdVerifiableCredential) + + return await getRevocationMetadata(agentContext, { + nonRevokedInterval: credentialWithMetadata.nonRevoked as AnonCredsNonRevokedInterval, + timestamp: timestamp, + revocationRegistryId, + revocationRegistryIndex, + }) + }) - let result = false + return await Promise.all(revocationMetadataFetchPromises) + } + public async verifyW3cPresentation(agentContext: AgentContext, options: VerifyW3cPresentationOptions) { + const revocationMetadata = await this.getRevocationMetadataForCredentials( + agentContext, + options.credentialsWithRevocationMetadata + ) + + const revocationRegistryDefinitions: Record = {} + revocationMetadata.forEach( + (rm) => (revocationRegistryDefinitions[rm.revocationRegistryId] = rm.revocationRegistryDefinition) + ) + + const verificationOptions: VerifyAnonCredsW3cPresentationOptions = { + presentationRequest: options.proofRequest as unknown as JsonObject, + schemas: options.schemas as unknown as Record, + credentialDefinitions: options.credentialDefinitions as unknown as Record, + revocationRegistryDefinitions, + revocationStatusLists: revocationMetadata.map((rm) => rm.revocationStatusList), + nonRevokedIntervalOverrides: revocationMetadata + .filter((rm) => rm.nonRevokedIntervalOverride) + .map((rm) => rm.nonRevokedIntervalOverride as NonRevokedIntervalOverride), + } + + let result = false + const presentationJson = JsonTransformer.toJSON(options.presentation) + let w3cPresentation: W3cPresentation | undefined try { - result = w3cPresentation.verify(options) + w3cPresentation = W3cPresentation.fromJson(presentationJson) + result = w3cPresentation.verify(verificationOptions) } finally { w3cPresentation?.handle.clear() } - return result } } diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts index 89c58ec206..55832cf998 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts @@ -1,4 +1,5 @@ import type { W3cAnoncredsCredentialMetadata } from '../../utils/metadata' +import type { AnonCredsCredentialTags } from '../../utils/w3cAnonCredsUtils' import type { AnonCredsCredentialDefinition, AnonCredsProofRequest, @@ -6,18 +7,13 @@ import type { AnonCredsSchema, AnonCredsSelectedCredentials, } from '@credo-ts/anoncreds' -import type { AnonCredsCredentialTags } from '@credo-ts/core' import type { JsonObject } from '@hyperledger/anoncreds-shared' import { DidResolverService, DidsModuleConfig, - Ed25519Signature2018, InjectionSymbols, - KeyType, SignatureSuiteToken, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialRecord, W3cCredentialRepository, W3cCredentialSubject, @@ -96,18 +92,7 @@ const agentContext = getAgentContext({ [InjectionSymbols.Logger, testLogger], [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig())], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], - [ - SignatureSuiteToken, - { - suiteClass: Ed25519Signature2018, - proofType: 'Ed25519Signature2018', - verificationMethodTypes: [ - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, - ], - keyTypes: [KeyType.Ed25519], - }, - ], + [SignatureSuiteToken, 'default'], ], agentConfig, wallet, @@ -115,11 +100,11 @@ const agentContext = getAgentContext({ describe('AnonCredsRsHolderService', () => { const getByCredentialIdMock = jest.spyOn(anoncredsCredentialRepositoryMock, 'getByCredentialId') - const findSingleByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findSingleByQuery') + const findByIdMock = jest.spyOn(w3cCredentialRepositoryMock, 'findById') const findByQueryMock = jest.spyOn(w3cCredentialRepositoryMock, 'findByQuery') beforeEach(() => { - findSingleByQueryMock.mockClear() + findByIdMock.mockClear() getByCredentialIdMock.mockClear() findByQueryMock.mockClear() }) @@ -302,8 +287,8 @@ describe('AnonCredsRsHolderService', () => { }, } - findSingleByQueryMock.mockResolvedValueOnce(personRecord) - findSingleByQueryMock.mockResolvedValueOnce(phoneRecord) + findByIdMock.mockResolvedValueOnce(personRecord) + findByIdMock.mockResolvedValueOnce(phoneRecord) const revocationRegistries = { 'personrevregid:uri': { @@ -337,7 +322,7 @@ describe('AnonCredsRsHolderService', () => { revocationRegistries, }) - expect(findSingleByQueryMock).toHaveBeenCalledTimes(2) + expect(findByIdMock).toHaveBeenCalledTimes(2) // TODO: check proof object }) @@ -508,12 +493,12 @@ describe('AnonCredsRsHolderService', () => { credential: {} as W3cJsonLdVerifiableCredential, tags: {}, }) - findSingleByQueryMock.mockResolvedValueOnce(null).mockResolvedValueOnce(record) + findByIdMock.mockResolvedValueOnce(null).mockResolvedValueOnce(record) getByCredentialIdMock.mockRejectedValueOnce(new Error()) await expect(anonCredsHolderService.deleteCredential(agentContext, 'credentialId')).rejects.toThrow() await anonCredsHolderService.deleteCredential(agentContext, 'credentialId') - expect(findSingleByQueryMock).toHaveBeenCalledWith(agentContext, { anonCredsCredentialId: 'credentialId' }) + expect(findByIdMock).toHaveBeenCalledWith(agentContext, 'credentialId') }) test('get single Credential', async () => { @@ -534,7 +519,6 @@ describe('AnonCredsRsHolderService', () => { }) const tags: AnonCredsCredentialTags = { - anonCredsCredentialId: record.id, anonCredsLinkSecretId: 'linkSecretId', anonCredsCredentialDefinitionId: 'credDefId', anonCredsSchemaId: 'schemaId', @@ -556,14 +540,12 @@ describe('AnonCredsRsHolderService', () => { record.setTags(tags) record.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) - findSingleByQueryMock.mockResolvedValueOnce(null).mockResolvedValueOnce(record) + findByIdMock.mockResolvedValueOnce(null).mockResolvedValueOnce(record) getByCredentialIdMock.mockRejectedValueOnce(new Error()) - await expect( - anonCredsHolderService.getCredential(agentContext, { credentialId: 'myCredentialId' }) - ).rejects.toThrowError() + await expect(anonCredsHolderService.getCredential(agentContext, { id: 'myCredentialId' })).rejects.toThrowError() - const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { credentialId: 'myCredentialId' }) + const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { id: 'myCredentialId' }) expect(credentialInfo).toMatchObject({ attributes: { attr1: 'value1', attr2: 'value2' }, @@ -594,7 +576,6 @@ describe('AnonCredsRsHolderService', () => { const records = [record] const tags: AnonCredsCredentialTags = { - anonCredsCredentialId: record.id, anonCredsLinkSecretId: 'linkSecretId', anonCredsCredentialDefinitionId: 'credDefId', anonCredsSchemaId: 'schemaId', diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts index 4fe802209d..a371e7189d 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts @@ -3,12 +3,8 @@ import type { AnonCredsProofRequest } from '@credo-ts/anoncreds' import { DidResolverService, DidsModuleConfig, - Ed25519Signature2018, InjectionSymbols, - KeyType, SignatureSuiteToken, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialsModuleConfig, } from '@credo-ts/core' import { anoncreds } from '@hyperledger/anoncreds-nodejs' @@ -73,18 +69,7 @@ const agentContext = getAgentContext({ [InjectionSymbols.Logger, testLogger], [DidResolverService, new DidResolverService(testLogger, new DidsModuleConfig())], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], - [ - SignatureSuiteToken, - { - suiteClass: Ed25519Signature2018, - proofType: 'Ed25519Signature2018', - verificationMethodTypes: [ - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, - ], - keyTypes: [KeyType.Ed25519], - }, - ], + [SignatureSuiteToken, 'default'], ], agentConfig, }) @@ -205,7 +190,7 @@ describe('AnonCredsRsServices', () => { }) const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { - credentialId, + id: credentialId, }) expect(credentialInfo).toEqual({ @@ -410,7 +395,7 @@ describe('AnonCredsRsServices', () => { }) const credentialInfo = await anonCredsHolderService.getCredential(agentContext, { - credentialId, + id: credentialId, }) expect(credentialInfo).toEqual({ diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts index 286a61250d..43ffc5b3d3 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/helpers.ts @@ -1,11 +1,12 @@ import type { W3cAnoncredsCredentialMetadata } from '../../utils/metadata' +import type { AnonCredsCredentialTags } from '../../utils/w3cAnonCredsUtils' import type { AnonCredsCredentialDefinition, AnonCredsCredentialInfo, AnonCredsCredentialOffer, AnonCredsSchema, } from '@credo-ts/anoncreds' -import type { AgentContext, AnonCredsCredentialTags } from '@credo-ts/core' +import type { AgentContext } from '@credo-ts/core' import type { JsonObject } from '@hyperledger/anoncreds-shared' import { @@ -230,7 +231,6 @@ export async function storeCredential( }) const anonCredsCredentialRecordTags: AnonCredsCredentialTags = { - anonCredsCredentialId: record.id, anonCredsLinkSecretId: options.linkSecretId, anonCredsCredentialDefinitionId: options.credentialDefinitionId, anonCredsSchemaId: options.schemaId, diff --git a/packages/anoncreds/src/anoncreds-rs/utils.ts b/packages/anoncreds/src/anoncreds-rs/utils.ts new file mode 100644 index 0000000000..94d05a247f --- /dev/null +++ b/packages/anoncreds/src/anoncreds-rs/utils.ts @@ -0,0 +1,118 @@ +import type { AnonCredsNonRevokedInterval } from '../models' +import type { AgentContext, JsonObject, W3cJsonLdVerifiableCredential } from '@credo-ts/core' +import type { NonRevokedIntervalOverride } from '@hyperledger/anoncreds-shared' + +import { CredoError, JsonTransformer } from '@credo-ts/core' +import { + W3cCredential as AnonCredsW3cCredential, + RevocationRegistryDefinition, + RevocationStatusList, + CredentialRevocationState, +} from '@hyperledger/anoncreds-shared' + +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' +import { + assertBestPracticeRevocationInterval, + fetchRevocationRegistryDefinition, + fetchRevocationStatusList, +} from '../utils' + +export interface CredentialRevocationMetadata { + timestamp?: number + revocationRegistryId: string + revocationRegistryIndex?: number + nonRevokedInterval: AnonCredsNonRevokedInterval +} + +export async function getRevocationMetadata( + agentContext: AgentContext, + credentialRevocationMetadata: CredentialRevocationMetadata, + mustHaveTimeStamp = false +) { + let nonRevokedIntervalOverride: NonRevokedIntervalOverride | undefined + + const { revocationRegistryId, revocationRegistryIndex, nonRevokedInterval, timestamp } = credentialRevocationMetadata + if (!revocationRegistryId || !nonRevokedInterval || (mustHaveTimeStamp && !timestamp)) { + throw new CredoError('Invalid revocation metadata') + } + + // Make sure the revocation interval follows best practices from Aries RFC 0441 + assertBestPracticeRevocationInterval(nonRevokedInterval) + + const { revocationRegistryDefinition: anonCredsRevocationRegistryDefinition } = + await fetchRevocationRegistryDefinition(agentContext, revocationRegistryId) + + const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const { tailsFilePath } = await tailsFileService.getTailsFile(agentContext, { + revocationRegistryDefinition: anonCredsRevocationRegistryDefinition, + }) + + const timestampToFetch = timestamp ?? nonRevokedInterval.to + if (!timestampToFetch) throw new CredoError('Timestamp to fetch is required') + + const { revocationStatusList: _revocationStatusList } = await fetchRevocationStatusList( + agentContext, + revocationRegistryId, + timestampToFetch + ) + const updatedTimestamp = timestamp ?? _revocationStatusList.timestamp + + const revocationRegistryDefinition = RevocationRegistryDefinition.fromJson( + anonCredsRevocationRegistryDefinition as unknown as JsonObject + ) + + const revocationStatusList = RevocationStatusList.fromJson(_revocationStatusList as unknown as JsonObject) + const revocationState = revocationRegistryIndex + ? CredentialRevocationState.create({ + revocationRegistryIndex: Number(revocationRegistryIndex), + revocationRegistryDefinition: revocationRegistryDefinition, + tailsPath: tailsFilePath, + revocationStatusList, + }) + : undefined + + const requestedFrom = nonRevokedInterval.from + if (requestedFrom && requestedFrom > timestampToFetch) { + const { revocationStatusList: overrideRevocationStatusList } = await fetchRevocationStatusList( + agentContext, + revocationRegistryId, + requestedFrom + ) + + const vdrTimestamp = overrideRevocationStatusList?.timestamp + if (vdrTimestamp && vdrTimestamp === timestampToFetch) { + nonRevokedIntervalOverride = { + overrideRevocationStatusListTimestamp: timestampToFetch, + requestedFromTimestamp: requestedFrom, + revocationRegistryDefinitionId: revocationRegistryId, + } + } else { + throw new CredoError( + `VDR timestamp for ${requestedFrom} does not correspond to the one provided in proof identifiers. Expected: ${updatedTimestamp} and received ${vdrTimestamp}` + ) + } + } + + return { + updatedTimestamp, + revocationRegistryId, + revocationRegistryDefinition, + revocationStatusList, + nonRevokedIntervalOverride, + revocationState, + } +} + +export const getW3cAnonCredsCredentialMetadata = (w3cJsonLdVerifiableCredential: W3cJsonLdVerifiableCredential) => { + const w3cJsonLdVerifiableCredentialJson = JsonTransformer.toJSON(w3cJsonLdVerifiableCredential) + + const { schemaId, credentialDefinitionId, revocationRegistryId } = AnonCredsW3cCredential.fromJson( + w3cJsonLdVerifiableCredentialJson + ) + + return { + schemaId, + credentialDefinitionId, + revocationRegistryId, + } +} diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 73f45b770e..efebabc26e 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -424,7 +424,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService // 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 }) + const credential = await anonCredsHolderService.getCredential(agentContext, { id: credentialId }) credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { credentialRevocationId: credential.credentialRevocationId ?? undefined, diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index f79e9517f6..1ead79cc9d 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -545,7 +545,7 @@ export class AnonCredsProofFormatService implements ProofFormatService c.credentialInfo ?? holderService.getCredential(agentContext, { credentialId: c.credentialId }) + async (c) => c.credentialInfo ?? holderService.getCredential(agentContext, { id: c.credentialId }) ) ) diff --git a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts index 0c95ae1ac2..301c84354b 100644 --- a/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/DataIntegrityCredentialFormatService.ts @@ -4,7 +4,6 @@ import type { AnonCredsClaimRecord } from '../utils/credential' import type { AnonCredsCredentialMetadata, AnonCredsCredentialRequestMetadata } from '../utils/metadata' import type { DataIntegrityCredentialRequest, - DataIntegrityCredentialOffer, AnonCredsLinkSecretBindingMethod, DidCommSignedAttachmentBindingMethod, DataIntegrityCredentialRequestBindingProof, @@ -61,10 +60,10 @@ import { CredentialPreviewAttribute, CredoError, deepEquality, + DataIntegrityCredentialOffer, + W3cCredentialSubject, } from '@credo-ts/core' -import { W3cCredential as AW3cCredential } from '@hyperledger/anoncreds-shared' -import { AnonCredsRsHolderService } from '../anoncreds-rs' import { AnonCredsCredentialDefinitionRepository, AnonCredsRevocationRegistryDefinitionPrivateRepository, @@ -165,7 +164,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer dataIntegrityFormat ) - const attachment = this.getFormatData(dataIntegrityCredentialOffer, format.attachmentId) + const attachment = this.getFormatData(JsonTransformer.toJSON(dataIntegrityCredentialOffer), format.attachmentId) return { format, attachment, previewAttributes } } @@ -189,14 +188,17 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer `Processing data integrity credential offer for credential record ${credentialRecord.id}` ) - const { credential, data_model_versions_supported, binding_method, binding_required } = - attachment.getDataAsJson() + const dataIntegrityCredentialOffer = JsonTransformer.fromJSON( + attachment.getDataAsJson(), + DataIntegrityCredentialOffer + ) - const credentialVersion = this.getCredentialVersion(credential) + const credentialJson = dataIntegrityCredentialOffer.credential + const credentialVersion = this.getCredentialVersion(credentialJson) const credentialToBeValidated = { - ...credential, - issuer: credential.issuer ?? 'https://example.com', + ...credentialJson, + issuer: credentialJson.issuer ?? 'https://example.com', ...(credentialVersion === '1.1' ? { issuanceDate: new Date().toISOString() } : { validFrom: new Date().toISOString() }), @@ -205,32 +207,12 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer JsonTransformer.fromJSON(credentialToBeValidated, W3cCredential) const missingBindingMethod = - binding_required && !binding_method?.anoncreds_link_secret && !binding_method?.didcomm_signed_attachment - - const invalidDataModelVersions = - !data_model_versions_supported || - data_model_versions_supported.length === 0 || - data_model_versions_supported.some((v) => v !== '1.1' && v !== '2.0') + dataIntegrityCredentialOffer.bindingRequired && + !dataIntegrityCredentialOffer.bindingMethod?.anoncredsLinkSecret && + !dataIntegrityCredentialOffer.bindingMethod?.didcommSignedAttachment - const invalidLinkSecretBindingMethod = - binding_method?.anoncreds_link_secret && - (!binding_method.anoncreds_link_secret.cred_def_id || - !binding_method.anoncreds_link_secret.key_correctness_proof || - !binding_method.anoncreds_link_secret.nonce) - - const invalidDidCommSignedAttachmentBindingMethod = - binding_method?.didcomm_signed_attachment && - (!binding_method.didcomm_signed_attachment.algs_supported || - !binding_method.didcomm_signed_attachment.did_methods_supported || - !binding_method.didcomm_signed_attachment.nonce) - - if ( - missingBindingMethod || - invalidDataModelVersions || - invalidLinkSecretBindingMethod || - invalidDidCommSignedAttachmentBindingMethod - ) { - throw new ProblemReportError('Invalid credential offer', { + if (missingBindingMethod) { + throw new ProblemReportError('Invalid credential offer. Missing binding method.', { problemCode: CredentialProblemReportReason.IssuanceAbandoned, }) } @@ -332,19 +314,19 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const dataIntegrityFormat = credentialFormats?.dataIntegrity if (!dataIntegrityFormat) throw new CredoError('Missing data integrity credential format data') - const credentialOffer = offerAttachment.getDataAsJson() + const credentialOffer = JsonTransformer.fromJSON(offerAttachment.getDataAsJson(), DataIntegrityCredentialOffer) let anonCredsLinkSecretDataIntegrityBindingProof: AnonCredsLinkSecretDataIntegrityBindingProof | undefined = undefined if (dataIntegrityFormat.anonCredsLinkSecret) { - if (!credentialOffer.binding_method?.anoncreds_link_secret) { + if (!credentialOffer.bindingMethod?.anoncredsLinkSecret) { throw new CredoError('Cannot request credential with a binding method that was not offered.') } const anonCredsHolderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) - const credentialDefinitionId = credentialOffer.binding_method.anoncreds_link_secret.cred_def_id + const credentialDefinitionId = credentialOffer.bindingMethod.anoncredsLinkSecret.credentialDefinitionId const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, credentialDefinitionId) const { @@ -352,8 +334,10 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credentialRequestMetadata: anonCredsCredentialRequestMetadata, } = await anonCredsHolderService.createCredentialRequest(agentContext, { credentialOffer: { - ...credentialOffer.binding_method.anoncreds_link_secret, schema_id: credentialDefinitionReturn.credentialDefinition.schemaId, + cred_def_id: credentialOffer.bindingMethod.anoncredsLinkSecret.credentialDefinitionId, + key_correctness_proof: credentialOffer.bindingMethod.anoncredsLinkSecret.keyCorrectnessProof, + nonce: credentialOffer.bindingMethod.anoncredsLinkSecret.nonce, }, credentialDefinition: credentialDefinitionReturn.credentialDefinition, linkSecretId: dataIntegrityFormat.anonCredsLinkSecret?.linkSecretId, @@ -364,7 +348,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer anonCredsCredentialRequest as AnonCredsLinkSecretDataIntegrityBindingProof credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { - credentialDefinitionId: credentialOffer.binding_method.anoncreds_link_secret.cred_def_id, + credentialDefinitionId: credentialOffer.bindingMethod.anoncredsLinkSecret.credentialDefinitionId, schemaId: credentialDefinitionReturn.credentialDefinition.schemaId, }) credentialRecord.metadata.set( @@ -376,15 +360,15 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer let didCommSignedAttachmentBindingProof: DidCommSignedAttachmentDataIntegrityBindingProof | undefined = undefined let didCommSignedAttachment: Attachment | undefined = undefined if (dataIntegrityFormat.didCommSignedAttachment) { - if (!credentialOffer.binding_method?.didcomm_signed_attachment) { + if (!credentialOffer.bindingMethod?.didcommSignedAttachment) { throw new CredoError('Cannot request credential with a binding method that was not offered.') } didCommSignedAttachment = await this.createSignedAttachment( agentContext, - { nonce: credentialOffer.binding_method.didcomm_signed_attachment.nonce }, + { nonce: credentialOffer.bindingMethod.didcommSignedAttachment.nonce }, dataIntegrityFormat.didCommSignedAttachment, - credentialOffer.binding_method.didcomm_signed_attachment.algs_supported + credentialOffer.bindingMethod.didcommSignedAttachment.algsSupported ) didCommSignedAttachmentBindingProof = { attachment_id: didCommSignedAttachment.id } @@ -398,10 +382,10 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer didcomm_signed_attachment: didCommSignedAttachmentBindingProof, } - if (credentialOffer.binding_required && !bindingProof) throw new CredoError('Missing required binding proof') + if (credentialOffer.bindingRequired && !bindingProof) throw new CredoError('Missing required binding proof') - const dataModelVersion = dataIntegrityFormat.dataModelVersion ?? credentialOffer.data_model_versions_supported[0] - if (!credentialOffer.data_model_versions_supported.includes(dataModelVersion)) { + const dataModelVersion = dataIntegrityFormat.dataModelVersion ?? credentialOffer.dataModelVersionsSupported[0] + if (!credentialOffer.dataModelVersionsSupported.includes(dataModelVersion)) { throw new CredoError('Cannot request credential with a data model version that was not offered.') } @@ -518,8 +502,10 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { credential } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer: { - ...anonCredsLinkSecretBindingMethod, schema_id: linkSecretMetadata.schemaId as string, + cred_def_id: anonCredsLinkSecretBindingMethod.credentialDefinitionId, + key_correctness_proof: anonCredsLinkSecretBindingMethod.keyCorrectnessProof, + nonce: anonCredsLinkSecretBindingMethod.nonce, }, credentialRequest: anonCredsLinkSecretBindingProof, credentialValues: convertAttributesToCredentialValues(credentialAttributes), @@ -533,12 +519,12 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer credential.cred_def_id ) - const anoncredsRsHolderServive = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) - return await anoncredsRsHolderServive.legacyToW3cCredential( - agentContext, + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + return await anonCredsHolderService.legacyToW3cCredential(agentContext, { credential, - anoncredsCredentialDefinition.issuerId - ) + issuerId: anoncredsCredentialDefinition.issuerId, + }) } private async getSignatureMetadata( @@ -645,12 +631,10 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const dataIntegrityFormat = credentialFormats?.dataIntegrity if (!dataIntegrityFormat) throw new CredoError('Missing data integrity credential format data') - const credentialOffer = offerAttachment?.getDataAsJson() - if (!credentialOffer) throw new CredoError('Missing data integrity credential offer in createCredential') + const credentialOffer = JsonTransformer.fromJSON(offerAttachment?.getDataAsJson(), DataIntegrityCredentialOffer) - const offeredCredential = JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential) const assertedCredential = await this.assertAndSetCredentialSubjectId( - offeredCredential, + JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential), dataIntegrityFormat.credentialSubjectId ) @@ -659,7 +643,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer let signedCredential: W3cJsonLdVerifiableCredential | undefined if (credentialRequest.binding_proof?.anoncreds_link_secret) { - if (!credentialOffer.binding_method?.anoncreds_link_secret) { + if (!credentialOffer.bindingMethod?.anoncredsLinkSecret) { throw new CredoError('Cannot issue credential with a binding method that was not offered') } @@ -669,7 +653,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer signedCredential = await this.createCredentialWithAnonCredsDataIntegrityProof(agentContext, { credentialRecord, - anonCredsLinkSecretBindingMethod: credentialOffer.binding_method.anoncreds_link_secret, + anonCredsLinkSecretBindingMethod: credentialOffer.bindingMethod.anoncredsLinkSecret, linkSecretMetadata, anonCredsLinkSecretBindingProof: credentialRequest.binding_proof.anoncreds_link_secret, credentialSubjectId: dataIntegrityFormat.credentialSubjectId, @@ -677,7 +661,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer } if (credentialRequest.binding_proof?.didcomm_signed_attachment) { - if (!credentialOffer.binding_method?.didcomm_signed_attachment) { + if (!credentialOffer.bindingMethod?.didcommSignedAttachment) { throw new CredoError('Cannot issue credential with a binding method that was not offered') } @@ -687,7 +671,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer if (!bindingProofAttachment) throw new CredoError('Missing binding proof attachment') const { nonce } = await this.getSignedAttachmentPayload(agentContext, bindingProofAttachment) - if (nonce !== credentialOffer.binding_method.didcomm_signed_attachment.nonce) { + if (nonce !== credentialOffer.bindingMethod.didcommSignedAttachment.nonce) { throw new CredoError('Invalid nonce in signed attachment') } @@ -724,8 +708,18 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new CredoError('Missing credential attributes on credential record. Unable to check credential attributes') } - const aCredential = AW3cCredential.fromJson(credentialJson) - const { schemaId, credentialDefinitionId, revocationRegistryId, revocationRegistryIndex } = aCredential.toLegacy() + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const legacyAnonCredsCredential = await anonCredsHolderService.w3cToLegacyCredential(agentContext, { + credential: JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential), + }) + + const { + schema_id: schemaId, + cred_def_id: credentialDefinitionId, + rev_reg_id: revocationRegistryId, + } = legacyAnonCredsCredential const schemaReturn = await fetchSchema(agentContext, schemaId) const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, credentialDefinitionId) @@ -733,30 +727,45 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer ? await fetchRevocationRegistryDefinition(agentContext, revocationRegistryId) : undefined - const anonCredsRsHolderService = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) - const processed = await anonCredsRsHolderService.processW3cCredential(agentContext, aCredential, { - credentialRequestMetadata: linkSecretRequestMetadata, - credentialDefinition: credentialDefinitionReturn.credentialDefinition, - revocationRegistryDefinition: revocationRegistryDefinitionReturn?.revocationRegistryDefinition, + // This is required to process the credential + const w3cJsonLdVerifiableCredential = await anonCredsHolderService.legacyToW3cCredential(agentContext, { + credential: legacyAnonCredsCredential, + issuerId: credentialJson.issuer as string, + processOptions: { + credentialRequestMetadata: linkSecretRequestMetadata, + credentialDefinition: credentialDefinitionReturn.credentialDefinition, + revocationRegistryDefinition: revocationRegistryDefinitionReturn?.revocationRegistryDefinition, + }, }) - const w3cCredentialRecord = await anonCredsRsHolderService.storeW3cCredential(agentContext, { - credential: W3cJsonLdVerifiableCredential.fromJson(processed.toJson()), + const w3cCredentialRecordId = await anonCredsHolderService.storeCredential(agentContext, { + credential: w3cJsonLdVerifiableCredential, schema: schemaReturn.schema, credentialDefinitionId, credentialDefinition: credentialDefinitionReturn.credentialDefinition, credentialRequestMetadata: linkSecretRequestMetadata, - revocationRegistryDefinition: revocationRegistryDefinitionReturn?.revocationRegistryDefinition, + revocationRegistry: revocationRegistryDefinitionReturn + ? { + id: revocationRegistryId as string, + definition: revocationRegistryDefinitionReturn?.revocationRegistryDefinition, + } + : undefined, }) + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + const w3cCredentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, w3cCredentialRecordId) + // If the credential is revocable, store the revocation identifiers in the credential record if (revocationRegistryId) { const linkSecretMetadata = credentialRecord.metadata.get(AnonCredsCredentialMetadataKey) if (!linkSecretMetadata) throw new CredoError('Missing link secret metadata') + const anonCredsTags = await getAnonCredsTagsFromRecord(w3cCredentialRecord) + if (!anonCredsTags) throw new CredoError('Missing anoncreds tags on credential record.') + linkSecretMetadata.revocationRegistryId = revocationRegistryDefinitionReturn?.revocationRegistryDefinitionId - linkSecretMetadata.credentialRevocationId = revocationRegistryIndex?.toString() + linkSecretMetadata.credentialRevocationId = anonCredsTags.anonCredsCredentialRevocationId?.toString() credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, linkSecretMetadata) } @@ -772,7 +781,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer agentContext: AgentContext, { credentialRecord, attachment, requestAttachment, offerAttachment }: CredentialFormatProcessCredentialOptions ): Promise { - const credentialOffer = offerAttachment.getDataAsJson() + const credentialOffer = JsonTransformer.fromJSON(offerAttachment.getDataAsJson(), DataIntegrityCredentialOffer) const offeredCredentialJson = credentialOffer.credential const credentialRequest = requestAttachment.getDataAsJson() @@ -785,7 +794,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const { credential: credentialJson } = attachment.getDataAsJson() if (Array.isArray(offeredCredentialJson.credentialSubject)) { - throw new CredoError('Invalid credential subject. Only single credential subject object are supported') + throw new CredoError('Invalid credential subject. Multiple credential subjects are not yet supported.') } const credentialSubjectMatches = Object.entries(offeredCredentialJson.credentialSubject as JsonObject).every( @@ -917,8 +926,8 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer // eslint-disable-next-line @typescript-eslint/no-unused-vars { offerAttachment }: CredentialFormatAutoRespondOfferOptions ) { - const credentialOffer = offerAttachment.getDataAsJson() - if (!credentialOffer.binding_required) return true + const credentialOffer = JsonTransformer.fromJSON(offerAttachment.getDataAsJson(), DataIntegrityCredentialOffer) + if (!credentialOffer.bindingRequired) return true return false } @@ -927,11 +936,11 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer agentContext: AgentContext, { offerAttachment, requestAttachment }: CredentialFormatAutoRespondRequestOptions ) { - const credentialOffer = offerAttachment.getDataAsJson() + const credentialOffer = JsonTransformer.fromJSON(offerAttachment?.getDataAsJson(), DataIntegrityCredentialOffer) const credentialRequest = requestAttachment.getDataAsJson() if ( - !credentialOffer.binding_required && + !credentialOffer.bindingRequired && !credentialRequest.binding_proof?.anoncreds_link_secret && !credentialRequest.binding_proof?.didcomm_signed_attachment ) { @@ -939,7 +948,7 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer } if ( - credentialOffer.binding_required && + credentialOffer.bindingRequired && !credentialRequest.binding_proof?.anoncreds_link_secret && !credentialRequest.binding_proof?.didcomm_signed_attachment ) { @@ -947,20 +956,26 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer } // cannot auto response credential subject id must be set manually - const w3cCredential = JsonTransformer.fromJSON(credentialOffer.credential, W3cCredential) - const credentialHasSubjectId = Array.isArray(w3cCredential.credentialSubject) ? false : !!w3cCredential.id - if (credentialRequest.binding_proof?.anoncreds_link_secret && !credentialHasSubjectId) { + if (credentialRequest.binding_proof?.anoncreds_link_secret) { + try { + const subjectJson = credentialOffer.credential.credentialSubject + const credentialSubject = JsonTransformer.fromJSON(subjectJson, W3cCredentialSubject) + if (credentialSubject.id === undefined) return false + } catch (e) { + return false + } + return false } const validLinkSecretRequest = !credentialRequest.binding_proof?.anoncreds_link_secret || - (credentialRequest.binding_proof?.anoncreds_link_secret && credentialOffer.binding_method?.anoncreds_link_secret) + (credentialRequest.binding_proof?.anoncreds_link_secret && credentialOffer.bindingMethod?.anoncredsLinkSecret) const validDidCommSignedAttachmetRequest = !credentialRequest.binding_proof?.didcomm_signed_attachment || (credentialRequest.binding_proof?.didcomm_signed_attachment && - credentialOffer.binding_method?.didcomm_signed_attachment) + credentialOffer.bindingMethod?.didcommSignedAttachment) return Boolean(validLinkSecretRequest && validDidCommSignedAttachmetRequest) } @@ -1032,11 +1047,14 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer false ) - const { schema_id, ..._anonCredsLinkSecretBindingMethod } = anoncredsCredentialOffer - anonCredsLinkSecretBindingMethod = _anonCredsLinkSecretBindingMethod + anonCredsLinkSecretBindingMethod = { + credentialDefinitionId: anoncredsCredentialOffer.cred_def_id, + keyCorrectnessProof: anoncredsCredentialOffer.key_correctness_proof, + nonce: anoncredsCredentialOffer.nonce, + } credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { - schemaId: schema_id, + schemaId: anoncredsCredentialOffer.schema_id, credentialDefinitionId: credentialDefinitionId, credentialRevocationId: revocationRegistryIndex?.toString(), revocationRegistryId: revocationRegistryDefinitionId, @@ -1047,17 +1065,17 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer if (didCommSignedAttachmentBindingMethodOptions) { const { didMethodsSupported, algsSupported } = didCommSignedAttachmentBindingMethodOptions didCommSignedAttachmentBindingMethod = { - did_methods_supported: + didMethodsSupported: didMethodsSupported ?? agentContext.dependencyManager.resolve(DidsApi).supportedResolverMethods, - algs_supported: algsSupported ?? this.getSupportedJwaSignatureAlgorithms(agentContext), + algsSupported: algsSupported ?? this.getSupportedJwaSignatureAlgorithms(agentContext), nonce: await agentContext.wallet.generateNonce(), } - if (didCommSignedAttachmentBindingMethod.algs_supported.length === 0) { + if (didCommSignedAttachmentBindingMethod.algsSupported.length === 0) { throw new CredoError('No supported JWA signature algorithms found.') } - if (didCommSignedAttachmentBindingMethod.did_methods_supported.length === 0) { + if (didCommSignedAttachmentBindingMethod.didMethodsSupported.length === 0) { throw new CredoError('No supported DID methods found.') } } @@ -1066,15 +1084,15 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer throw new CredoError('Missing required binding method.') } - const dataIntegrityCredentialOffer: DataIntegrityCredentialOffer = { - data_model_versions_supported: dataModelVersionsSupported, - binding_required: bindingRequired, - binding_method: { - anoncreds_link_secret: anonCredsLinkSecretBindingMethod, - didcomm_signed_attachment: didCommSignedAttachmentBindingMethod, + const dataIntegrityCredentialOffer = new DataIntegrityCredentialOffer({ + dataModelVersionsSupported, + bindingRequired: bindingRequired, + bindingMethod: { + anoncredsLinkSecret: anonCredsLinkSecretBindingMethod, + didcommSignedAttachment: didCommSignedAttachmentBindingMethod, }, credential: credentialJson, - } + }) return { dataIntegrityCredentialOffer, previewAttributes } } diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts index 76f14aa498..5a2d7b9592 100644 --- a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -391,7 +391,7 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic // 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 }) + const credential = await anonCredsHolderService.getCredential(agentContext, { id: credentialId }) credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { credentialRevocationId: credential.credentialRevocationId ?? undefined, diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts index a0bd459b48..e9fc51f452 100644 --- a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts @@ -563,7 +563,7 @@ export class LegacyIndyProofFormatService implements ProofFormatService c.credentialInfo ?? holderService.getCredential(agentContext, { credentialId: c.credentialId }) + async (c) => c.credentialInfo ?? holderService.getCredential(agentContext, { id: c.credentialId }) ) ) diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts index 5fb40e9c5f..fba415b05a 100644 --- a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts +++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts @@ -9,10 +9,7 @@ import { ProofState, EventEmitter, InjectionSymbols, - Ed25519Signature2018, SignatureSuiteToken, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialsModuleConfig, DidResolverService, DidsModuleConfig, @@ -102,18 +99,7 @@ const agentContext = getAgentContext({ [AnonCredsKeyCorrectnessProofRepository, anonCredsKeyCorrectnessProofRepository], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], [InjectionSymbols.StorageService, storageService], - [ - SignatureSuiteToken, - { - suiteClass: Ed25519Signature2018, - proofType: 'Ed25519Signature2018', - verificationMethodTypes: [ - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, - ], - keyTypes: [KeyType.Ed25519], - }, - ], + [SignatureSuiteToken, 'default'], ], agentConfig, wallet, @@ -309,7 +295,7 @@ describe('Legacy indy format services', () => { const credentialId = holderCredentialRecord.credentials[0].credentialRecordId const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { - credentialId, + id: credentialId, }) expect(anonCredsCredential).toEqual({ diff --git a/packages/anoncreds/src/models/W3cAnonCredsMetadata.ts b/packages/anoncreds/src/models/W3cAnonCredsMetadata.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/anoncreds/src/models/utils.ts b/packages/anoncreds/src/models/utils.ts new file mode 100644 index 0000000000..beb1c97451 --- /dev/null +++ b/packages/anoncreds/src/models/utils.ts @@ -0,0 +1,32 @@ +import type { AnonCredsNonRevokedInterval } from './exchange' +import type { + AnonCredsSchema, + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, +} from './registry' +import type { W3cJsonLdVerifiableCredential } from '@credo-ts/core' + +export interface AnonCredsSchemas { + [schemaId: string]: AnonCredsSchema +} + +export interface AnonCredsCredentialDefinitions { + [credentialDefinitionId: string]: AnonCredsCredentialDefinition +} + +export interface AnonCredsRevocationRegistries { + [revocationRegistryDefinitionId: string]: { + // tails file MUST already be downloaded on a higher level and stored + tailsFilePath: string + definition: AnonCredsRevocationRegistryDefinition + revocationStatusLists: { + [timestamp: number]: AnonCredsRevocationStatusList + } + } +} + +export interface CredentialWithRevocationMetadata { + credential: W3cJsonLdVerifiableCredential + nonRevoked?: AnonCredsNonRevokedInterval +} diff --git a/packages/anoncreds/src/services/AnonCredsHolderService.ts b/packages/anoncreds/src/services/AnonCredsHolderService.ts index 698e2d1871..a40291274f 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderService.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderService.ts @@ -9,10 +9,13 @@ import type { CreateLinkSecretReturn, CreateLinkSecretOptions, GetCredentialsOptions, + CreateW3cPresentationOptions, + LegacyToW3cCredentialOptions, + W3cToLegacyCredentialOptions, } from './AnonCredsHolderServiceOptions' import type { AnonCredsCredentialInfo } from '../models' -import type { AnonCredsProof } from '../models/exchange' -import type { AgentContext } from '@credo-ts/core' +import type { AnonCredsCredential, AnonCredsProof } from '../models/exchange' +import type { AgentContext, W3cJsonLdVerifiableCredential, W3cJsonLdVerifiablePresentation } from '@credo-ts/core' export const AnonCredsHolderServiceSymbol = Symbol('AnonCredsHolderService') @@ -42,4 +45,16 @@ export interface AnonCredsHolderService { agentContext: AgentContext, options: GetCredentialsForProofRequestOptions ): Promise + + createW3cPresentation( + agentContext: AgentContext, + options: CreateW3cPresentationOptions + ): Promise + + w3cToLegacyCredential(agentContext: AgentContext, options: W3cToLegacyCredentialOptions): Promise + + legacyToW3cCredential( + agentContext: AgentContext, + options: LegacyToW3cCredentialOptions + ): Promise } diff --git a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts index 9a8ce1e3c8..56287d66f3 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderServiceOptions.ts @@ -9,9 +9,14 @@ import type { import type { AnonCredsCredentialDefinition, AnonCredsRevocationRegistryDefinition, - AnonCredsRevocationStatusList, AnonCredsSchema, } from '../models/registry' +import type { + AnonCredsSchemas, + AnonCredsCredentialDefinitions, + AnonCredsRevocationRegistries, + CredentialWithRevocationMetadata, +} from '../models/utils' import type { AnonCredsCredentialRequestMetadata } from '../utils/metadata' import type { W3cJsonLdVerifiableCredential } from '@credo-ts/core' @@ -23,22 +28,9 @@ export interface AnonCredsAttributeInfo { export interface CreateProofOptions { proofRequest: AnonCredsProofRequest selectedCredentials: AnonCredsSelectedCredentials - schemas: { - [schemaId: string]: AnonCredsSchema - } - credentialDefinitions: { - [credentialDefinitionId: string]: AnonCredsCredentialDefinition - } - revocationRegistries: { - [revocationRegistryDefinitionId: string]: { - // tails file MUST already be downloaded on a higher level and stored - tailsFilePath: string - definition: AnonCredsRevocationRegistryDefinition - revocationStatusLists: { - [timestamp: number]: AnonCredsRevocationStatusList - } - } - } + schemas: AnonCredsSchemas + credentialDefinitions: AnonCredsCredentialDefinitions + revocationRegistries: AnonCredsRevocationRegistries } export interface StoreCredentialOptions { @@ -55,7 +47,7 @@ export interface StoreCredentialOptions { } export interface GetCredentialOptions { - credentialId: string + id: string } export interface GetCredentialsOptions { @@ -107,3 +99,33 @@ export interface CreateLinkSecretReturn { linkSecretId: string linkSecretValue?: string } + +export interface AnonCredsCredentialProve { + entryIndex: number + referent: string + isPredicate: boolean + reveal: boolean +} + +export interface CreateW3cPresentationOptions { + proofRequest: AnonCredsProofRequest + linkSecretId: string + schemas: AnonCredsSchemas + credentialDefinitions: AnonCredsCredentialDefinitions + credentialsProve: AnonCredsCredentialProve[] + credentialsWithRevocationMetadata: CredentialWithRevocationMetadata[] +} + +export interface LegacyToW3cCredentialOptions { + credential: AnonCredsCredential + issuerId: string + processOptions?: { + credentialDefinition: AnonCredsCredentialDefinition + credentialRequestMetadata: AnonCredsCredentialRequestMetadata + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition | undefined + } +} + +export interface W3cToLegacyCredentialOptions { + credential: W3cJsonLdVerifiableCredential +} diff --git a/packages/anoncreds/src/services/AnonCredsVerifierService.ts b/packages/anoncreds/src/services/AnonCredsVerifierService.ts index 2eabf727dc..cec7b0b237 100644 --- a/packages/anoncreds/src/services/AnonCredsVerifierService.ts +++ b/packages/anoncreds/src/services/AnonCredsVerifierService.ts @@ -1,4 +1,4 @@ -import type { VerifyProofOptions } from './AnonCredsVerifierServiceOptions' +import type { VerifyProofOptions, VerifyW3cPresentationOptions } from './AnonCredsVerifierServiceOptions' import type { AgentContext } from '@credo-ts/core' export const AnonCredsVerifierServiceSymbol = Symbol('AnonCredsVerifierService') @@ -7,4 +7,6 @@ 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 verifyProof(agentContext: AgentContext, options: VerifyProofOptions): Promise + + verifyW3cPresentation(agentContext: AgentContext, options: VerifyW3cPresentationOptions): Promise } diff --git a/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts index 1bdd959f15..6b13e93a5e 100644 --- a/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsVerifierServiceOptions.ts @@ -1,20 +1,17 @@ import type { AnonCredsProof, AnonCredsProofRequest } from '../models/exchange' +import type { AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition } from '../models/registry' import type { - AnonCredsCredentialDefinition, - AnonCredsRevocationStatusList, - AnonCredsRevocationRegistryDefinition, - AnonCredsSchema, -} from '../models/registry' + AnonCredsSchemas, + AnonCredsCredentialDefinitions, + CredentialWithRevocationMetadata, +} from '../models/utils' +import type { W3cJsonLdVerifiablePresentation } from '@credo-ts/core' export interface VerifyProofOptions { proofRequest: AnonCredsProofRequest proof: AnonCredsProof - schemas: { - [schemaId: string]: AnonCredsSchema - } - credentialDefinitions: { - [credentialDefinitionId: string]: AnonCredsCredentialDefinition - } + schemas: AnonCredsSchemas + credentialDefinitions: AnonCredsCredentialDefinitions revocationRegistries: { [revocationRegistryDefinitionId: string]: { definition: AnonCredsRevocationRegistryDefinition @@ -29,3 +26,11 @@ export interface VerifyProofOptions { } } } + +export interface VerifyW3cPresentationOptions { + proofRequest: AnonCredsProofRequest + presentation: W3cJsonLdVerifiablePresentation + schemas: AnonCredsSchemas + credentialDefinitions: AnonCredsCredentialDefinitions + credentialsWithRevocationMetadata: CredentialWithRevocationMetadata[] +} diff --git a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts index 1ca0a2706e..226e868edd 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts @@ -6,13 +6,9 @@ import { CredoError, DidResolverService, DidsModuleConfig, - Ed25519Signature2018, EventEmitter, InjectionSymbols, - KeyType, SignatureSuiteToken, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialRepository, W3cCredentialsModuleConfig, } from '@credo-ts/core' @@ -22,8 +18,9 @@ import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageS import { agentDependencies, getAgentConfig, getAgentContext, mockFunction, testLogger } from '../../../../../core/tests' import { InMemoryAnonCredsRegistry } from '../../../../tests/InMemoryAnonCredsRegistry' import { AnonCredsModuleConfig } from '../../../AnonCredsModuleConfig' +import { AnonCredsRsHolderService } from '../../../anoncreds-rs' import { AnonCredsCredentialRecord } from '../../../repository' -import { AnonCredsRegistryService } from '../../../services' +import { AnonCredsHolderServiceSymbol, AnonCredsRegistryService } from '../../../services' import { getUnQualifiedDidIndyDid, getQualifiedDidIndyDid, isUnqualifiedIndyDid } from '../../../utils/indyIdentifiers' import * as testModule from '../anonCredsCredentialRecord' @@ -73,18 +70,8 @@ const agentContext = getAgentContext({ [InjectionSymbols.Logger, testLogger], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], [AnonCredsModuleConfig, anonCredsModuleConfig], - [ - SignatureSuiteToken, - { - suiteClass: Ed25519Signature2018, - proofType: 'Ed25519Signature2018', - verificationMethodTypes: [ - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, - ], - keyTypes: [KeyType.Ed25519], - }, - ], + [AnonCredsHolderServiceSymbol, new AnonCredsRsHolderService()], + [SignatureSuiteToken, 'default'], ], agentConfig, wallet, diff --git a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts index ec9ebe8e88..73a1e70165 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts @@ -1,10 +1,11 @@ +import type { AnonCredsHolderService } from '../../services' import type { W3cAnoncredsCredentialMetadata } from '../../utils/metadata' import type { AgentContext, BaseAgent } from '@credo-ts/core' import { CacheModuleConfig, CredoError, W3cCredentialRepository, W3cCredentialService } from '@credo-ts/core' -import { AnonCredsRsHolderService } from '../../anoncreds-rs/AnonCredsRsHolderService' import { AnonCredsCredentialRepository, type AnonCredsCredentialRecord } from '../../repository' +import { AnonCredsHolderServiceSymbol } from '../../services' import { fetchCredentialDefinition } from '../../utils/anonCredsObjects' import { getIndyNamespaceFromIndyDid, @@ -31,23 +32,16 @@ async function getIndyNamespace( (await cache.get(agentContext, indyCacheKey)) ?? (await cache.get(agentContext, sovCacheKey)) if (!cachedNymResponse?.indyNamespace || typeof cachedNymResponse?.indyNamespace !== 'string') { - try { - const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, legacyCredentialDefinitionId) - const namespace = credentialDefinitionReturn.indyNamespace - - if (!namespace) { - throw new CredoError( - 'Could not determine the indyNamespace required for storing anoncreds in the new w3c format.' - ) - } + const credentialDefinitionReturn = await fetchCredentialDefinition(agentContext, legacyCredentialDefinitionId) + const namespace = credentialDefinitionReturn.indyNamespace - return namespace - } catch (error) { - agentContext.config.logger.warn( - `Failed to fetch credential definition for credentialId ${legacyCredentialDefinitionId}`, - error + if (!namespace) { + throw new CredoError( + 'Could not determine the indyNamespace required for storing anoncreds in the new w3c format.' ) } + + return namespace } else { return cachedNymResponse.indyNamespace } @@ -90,12 +84,12 @@ async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRe qualifiedSchemaIssuerId = legacyTags.schemaIssuerId } - const anonCredsRsHolderService = agentContext.dependencyManager.resolve(AnonCredsRsHolderService) - const w3cJsonLdCredential = await anonCredsRsHolderService.legacyToW3cCredential( - agentContext, - legacyRecord.credential, - qualifiedIssuerId - ) + const anonCredsHolderService = + agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + const w3cJsonLdCredential = await anonCredsHolderService.legacyToW3cCredential(agentContext, { + credential: legacyRecord.credential, + issuerId: qualifiedIssuerId, + }) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) const w3cCredentialRecord = await w3cCredentialService.storeCredential(agentContext, { diff --git a/packages/anoncreds/src/utils/__tests__/W3cAnonCredsCredentialRecord.test.ts b/packages/anoncreds/src/utils/__tests__/W3cAnonCredsCredentialRecord.test.ts new file mode 100644 index 0000000000..19bff68335 --- /dev/null +++ b/packages/anoncreds/src/utils/__tests__/W3cAnonCredsCredentialRecord.test.ts @@ -0,0 +1,75 @@ +import { JsonTransformer, W3cCredentialRecord, W3cJsonLdVerifiableCredential } from '@credo-ts/core' + +import { Ed25519Signature2018Fixtures } from '../../../../core/src/modules/vc/data-integrity/__tests__/fixtures' +import { W3cAnonCredsCredentialMetadataKey } from '../metadata' +import { getAnonCredsTagsFromRecord, type AnonCredsCredentialTags } from '../w3cAnonCredsUtils' + +describe('AnoncredsW3cCredentialRecord', () => { + it('should return default tags (w3cAnoncredsCredential)', () => { + const credential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ) + + const anoncredsCredentialRecordTags: AnonCredsCredentialTags = { + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsSchemaId: 'schemaId', + anonCredsCredentialDefinitionId: 'credentialDefinitionId', + anonCredsCredentialRevocationId: 'credentialRevocationId', + anonCredsLinkSecretId: 'linkSecretId', + anonCredsMethodName: 'methodName', + anonCredsRevocationRegistryId: 'revocationRegistryId', + } + + const w3cCredentialRecord = new W3cCredentialRecord({ + credential, + tags: { + expandedTypes: ['https://expanded.tag#1'], + }, + }) + + const anonCredsCredentialMetadata = { + credentialRevocationId: anoncredsCredentialRecordTags.anonCredsCredentialRevocationId, + linkSecretId: anoncredsCredentialRecordTags.anonCredsLinkSecretId, + methodName: anoncredsCredentialRecordTags.anonCredsMethodName, + } + + w3cCredentialRecord.setTags(anoncredsCredentialRecordTags) + w3cCredentialRecord.metadata.set(W3cAnonCredsCredentialMetadataKey, anonCredsCredentialMetadata) + + const anoncredsCredentialTags = { + anonCredsLinkSecretId: 'linkSecretId', + anonCredsMethodName: 'methodName', + anonCredsSchemaId: 'schemaId', + anonCredsSchemaIssuerId: 'schemaIssuerId', + anonCredsSchemaName: 'schemaName', + anonCredsSchemaVersion: 'schemaVersion', + anonCredsCredentialDefinitionId: 'credentialDefinitionId', + anonCredsRevocationRegistryId: 'revocationRegistryId', + anonCredsCredentialRevocationId: 'credentialRevocationId', + } + + const anonCredsTags = getAnonCredsTagsFromRecord(w3cCredentialRecord) + expect(anonCredsTags).toEqual({ + ...anoncredsCredentialTags, + }) + + expect(w3cCredentialRecord.metadata.get(W3cAnonCredsCredentialMetadataKey)).toEqual(anonCredsCredentialMetadata) + + expect(w3cCredentialRecord.getTags()).toEqual({ + claimFormat: 'ldp_vc', + issuerId: credential.issuerId, + subjectIds: credential.credentialSubjectIds, + schemaIds: credential.credentialSchemaIds, + contexts: credential.contexts, + proofTypes: credential.proofTypes, + givenId: credential.id, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + cryptosuites: [], + expandedTypes: ['https://expanded.tag#1'], + ...anoncredsCredentialTags, + }) + }) +}) diff --git a/packages/anoncreds/src/utils/anonCredsObjects.ts b/packages/anoncreds/src/utils/anonCredsObjects.ts index 45df4984da..4ad2c95c95 100644 --- a/packages/anoncreds/src/utils/anonCredsObjects.ts +++ b/packages/anoncreds/src/utils/anonCredsObjects.ts @@ -1,4 +1,4 @@ -import type { AnonCredsRevocationStatusList } from '../models' +import type { AnonCredsCredentialDefinition, AnonCredsRevocationStatusList, AnonCredsSchema } from '../models' import type { AgentContext } from '@credo-ts/core' import { CredoError } from '@credo-ts/core' @@ -16,12 +16,10 @@ export async function fetchSchema(agentContext: AgentContext, schemaId: string) throw new CredoError(`Schema not found for id ${schemaId}: ${result.resolutionMetadata.message}`) } - const indyNamespace = result.schemaMetadata.didIndyNamespace - return { schema: result.schema, schemaId: result.schemaId, - indyNamespace: indyNamespace && typeof indyNamespace === 'string' ? indyNamespace : undefined, + indyNamespace: result.schemaMetadata.didIndyNamespace as string | undefined, } } @@ -93,3 +91,25 @@ export async function fetchRevocationStatusList( return { revocationStatusList } } + +export async function fetchSchemas(agentContext: AgentContext, schemaIds: Set) { + const schemaFetchPromises = [...schemaIds].map(async (schemaId): Promise<[string, AnonCredsSchema]> => { + const { schema } = await fetchSchema(agentContext, schemaId) + return [schemaId, schema] + }) + + const schemas = Object.fromEntries(await Promise.all(schemaFetchPromises)) + return schemas +} + +export async function fetchCredentialDefinitions(agentContext: AgentContext, credentialDefinitionIds: Set) { + const credentialDefinitionEntries = [...credentialDefinitionIds].map( + async (credentialDefinitionId): Promise<[string, AnonCredsCredentialDefinition]> => { + const { credentialDefinition } = await fetchCredentialDefinition(agentContext, credentialDefinitionId) + return [credentialDefinitionId, credentialDefinition] + } + ) + + const credentialDefinitions = Object.fromEntries(await Promise.all(credentialDefinitionEntries)) + return credentialDefinitions +} diff --git a/packages/anoncreds/src/utils/linkSecret.ts b/packages/anoncreds/src/utils/linkSecret.ts index 431ebbfef0..8a4d87ad63 100644 --- a/packages/anoncreds/src/utils/linkSecret.ts +++ b/packages/anoncreds/src/utils/linkSecret.ts @@ -1,5 +1,6 @@ import type { AgentContext } from '@credo-ts/core' +import { AnonCredsRsError } from '../error/AnonCredsRsError' import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../repository' export async function storeLinkSecret( @@ -28,3 +29,25 @@ export async function storeLinkSecret( return linkSecretRecord } + +export function assertLinkSecretsMatch(agentContext: AgentContext, linkSecretIds: string[]) { + // Get all requested credentials and take linkSecret. If it's not the same for every credential, throw error + const linkSecretsMatch = linkSecretIds.every((linkSecretId) => linkSecretId === linkSecretIds[0]) + if (!linkSecretsMatch) { + throw new AnonCredsRsError('All credentials in a Proof should have been issued using the same Link Secret') + } + + return linkSecretIds[0] +} + +export async function getLinkSecret(agentContext: AgentContext, linkSecretId: string): Promise { + const linkSecretRecord = await agentContext.dependencyManager + .resolve(AnonCredsLinkSecretRepository) + .getByLinkSecretId(agentContext, linkSecretId) + + if (!linkSecretRecord.value) { + throw new AnonCredsRsError('Link Secret value not stored') + } + + return linkSecretRecord.value +} diff --git a/packages/anoncreds/src/utils/metadata.ts b/packages/anoncreds/src/utils/metadata.ts index 56a24593da..e8e618d5d2 100644 --- a/packages/anoncreds/src/utils/metadata.ts +++ b/packages/anoncreds/src/utils/metadata.ts @@ -42,4 +42,4 @@ export const AnonCredsCredentialRequestMetadataKey = '_anoncreds/credentialReque * * MUST be used with {@link W3cAnoncredsCredentialMetadata} */ -export const W3cAnonCredsCredentialMetadataKey = '_w3c/AnonCredsMetadata' +export const W3cAnonCredsCredentialMetadataKey = '_w3c/anonCredsMetadata' diff --git a/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts b/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts index 076e96b6cc..03fcb97f29 100644 --- a/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts +++ b/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts @@ -3,7 +3,7 @@ import type { W3cAnoncredsCredentialMetadata } from './metadata' import type { AnonCredsCredentialInfo, AnonCredsSchema } from '../models' import type { AnonCredsCredentialRecord } from '../repository' import type { StoreCredentialOptions } from '../services' -import type { AnonCredsCredentialTags } from '@credo-ts/core' +import type { DefaultW3cCredentialTags } from '@credo-ts/core' import { CredoError, W3cCredentialRecord, utils } from '@credo-ts/core' @@ -23,6 +23,30 @@ import { } from './indyIdentifiers' import { W3cAnonCredsCredentialMetadataKey } from './metadata' +export type AnonCredsCredentialTags = { + anonCredsLinkSecretId: string + anonCredsCredentialRevocationId?: string + anonCredsMethodName: string + + // the following keys can be used for every `attribute name` in credential. + [key: `anonCredsAttr::${string}::marker`]: true | undefined + [key: `anonCredsAttr::${string}::value`]: string | undefined + + anonCredsSchemaName: string + anonCredsSchemaVersion: string + + anonCredsSchemaId: string + anonCredsSchemaIssuerId: string + anonCredsCredentialDefinitionId: string + anonCredsRevocationRegistryId?: string + + anonCredsUnqualifiedIssuerId?: string + anonCredsUnqualifiedSchemaId?: string + anonCredsUnqualifiedSchemaIssuerId?: string + anonCredsUnqualifiedCredentialDefinitionId?: string + anonCredsUnqualifiedRevocationRegistryId?: string +} + function anoncredsCredentialInfoFromW3cRecord(w3cCredentialRecord: W3cCredentialRecord): AnonCredsCredentialInfo { if (Array.isArray(w3cCredentialRecord.credential.credentialSubject)) { throw new CredoError('Credential subject must be an object, not an array.') @@ -81,9 +105,8 @@ export function getAnonCredsTagsFromRecord(record: W3cCredentialRecord) { const anoncredsMetadata = record.metadata.get(W3cAnonCredsCredentialMetadataKey) if (!anoncredsMetadata) return undefined - const tags = record.getTags() + const tags = record.getTags() as DefaultW3cCredentialTags & Partial if ( - !tags.anonCredsCredentialId || !tags.anonCredsLinkSecretId || !tags.anonCredsMethodName || !tags.anonCredsSchemaId || @@ -163,7 +186,6 @@ export function getW3cRecordAnonCredsTags(options: { const issuerId = w3cCredentialRecord.credential.issuerId const anonCredsCredentialRecordTags: AnonCredsCredentialTags = { - anonCredsCredentialId: w3cCredentialRecord.id, anonCredsLinkSecretId: linkSecretId, anonCredsCredentialDefinitionId: credentialDefinitionId, anonCredsSchemaId: schemaId, diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 636e105701..5dd178a86c 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -27,7 +27,6 @@ import { getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, } from '../../indy-vdr/src/anoncreds/utils/identifiers' -import {} from '../src' import { getUnQualifiedDidIndyDid, getUnqualifiedRevocationRegistryDefinitionId, @@ -114,7 +113,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { const issuerId = options.schema.issuerId let schemaId: string - if (isIndyDid(issuerId) || isUnqualifiedIndyDid(issuerId)) { + if (isIndyDid(issuerId)) { const { namespace, namespaceIdentifier } = parseIndyDid(issuerId) schemaId = getDidIndySchemaId(namespace, namespaceIdentifier, options.schema.name, options.schema.version) this.schemas[getUnQualifiedDidIndyDid(schemaId)] = getUnqualifiedDidIndySchema(options.schema) @@ -183,7 +182,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { const schemaId = options.credentialDefinition.schemaId let credentialDefinitionId: string - if (isIndyDid(schemaId) || isUnqualifiedSchemaId(schemaId)) { + if (isIndyDid(options.credentialDefinition.issuerId)) { const parsedSchema = parseIndySchemaId(options.credentialDefinition.schemaId) const legacySchemaId = getUnqualifiedSchemaId( parsedSchema.namespaceIdentifier, diff --git a/packages/anoncreds/tests/anoncreds-flow.test.ts b/packages/anoncreds/tests/anoncreds-flow.test.ts index fa3ed98bc9..90954d3a53 100644 --- a/packages/anoncreds/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds/tests/anoncreds-flow.test.ts @@ -10,11 +10,7 @@ import { CredentialState, DidResolverService, DidsModuleConfig, - Ed25519Signature2018, - KeyType, SignatureSuiteToken, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialsModuleConfig, } from '@credo-ts/core' import { Subject } from 'rxjs' @@ -84,18 +80,7 @@ const agentContext = getAgentContext({ [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], - [ - SignatureSuiteToken, - { - suiteClass: Ed25519Signature2018, - proofType: 'Ed25519Signature2018', - verificationMethodTypes: [ - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, - ], - keyTypes: [KeyType.Ed25519], - }, - ], + [SignatureSuiteToken, 'default'], ], agentConfig, wallet, @@ -369,7 +354,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean const credentialId = holderCredentialRecord.credentials[0].credentialRecordId const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { - credentialId, + id: credentialId, }) expect(anonCredsCredential).toEqual({ diff --git a/packages/anoncreds/tests/anoncredsSetup.ts b/packages/anoncreds/tests/anoncredsSetup.ts index 05eed47bb3..8d10b2a471 100644 --- a/packages/anoncreds/tests/anoncredsSetup.ts +++ b/packages/anoncreds/tests/anoncredsSetup.ts @@ -1,7 +1,6 @@ +import type { EventReplaySubject } from '../../core/tests' import type { AnonCredsRegisterCredentialDefinitionOptions, - AnonCredsRequestedAttribute, - AnonCredsRequestedPredicate, AnonCredsOfferCredentialFormat, AnonCredsSchema, RegisterCredentialDefinitionReturnStateFinished, @@ -11,8 +10,8 @@ import type { RegisterRevocationRegistryDefinitionReturnStateFinished, AnonCredsRegisterRevocationStatusListOptions, RegisterRevocationStatusListReturnStateFinished, -} from '../../anoncreds/src' -import type { EventReplaySubject } from '../../core/tests' +} from '../src' +import type { CheqdDidCreateOptions } from '@credo-ts/cheqd' import type { AutoAcceptProof, ConnectionRecord } from '@credo-ts/core' import { @@ -27,26 +26,23 @@ import { CredentialState, ProofEventTypes, ProofsModule, - ProofState, V2CredentialProtocol, V2ProofProtocol, DidsModule, PresentationExchangeProofFormatService, + TypedArrayEncoder, } from '@credo-ts/core' import { randomUUID } from 'crypto' -import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../../anoncreds/src' -import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' -import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { CheqdDidRegistrar, CheqdDidResolver, CheqdModule } from '../../cheqd' +import { getCheqdModuleConfig } from '../../cheqd/tests/setupCheqdModule' import { sleep } from '../../core/src/utils/sleep' import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' -import { - getInMemoryAgentOptions, - makeConnection, - waitForCredentialRecordSubject, - waitForProofExchangeRecordSubject, -} from '../../core/tests/helpers' +import { getInMemoryAgentOptions, makeConnection, waitForCredentialRecordSubject } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' +import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../src' +import { DataIntegrityCredentialFormatService } from '../src/formats/DataIntegrityCredentialFormatService' +import { InMemoryAnonCredsRegistry } from '../tests/InMemoryAnonCredsRegistry' import { InMemoryTailsFileService } from './InMemoryTailsFileService' import { LocalDidResolver } from './LocalDidResolver' @@ -63,10 +59,15 @@ export const getAnonCredsModules = ({ autoAcceptCredentials, autoAcceptProofs, registries, + cheqd, }: { autoAcceptCredentials?: AutoAcceptCredential autoAcceptProofs?: AutoAcceptProof registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] + cheqd?: { + rpcUrl?: string + seed?: string + } } = {}) => { const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() // Add support for resolving pre-created credential definitions and schemas @@ -84,7 +85,9 @@ export const getAnonCredsModules = ({ const anonCredsProofFormatService = new AnonCredsProofFormatService() const presentationExchangeProofFormatService = new PresentationExchangeProofFormatService() + const cheqdSdk = cheqd ? new CheqdModule(getCheqdModuleConfig(cheqd.seed, cheqd.rpcUrl)) : undefined const modules = { + ...(cheqdSdk && { cheqdSdk }), credentials: new CredentialsModule({ autoAcceptCredentials, credentialProtocols: [ @@ -107,7 +110,8 @@ export const getAnonCredsModules = ({ anoncreds, }), dids: new DidsModule({ - resolvers: [new LocalDidResolver()], + resolvers: cheqd ? [new CheqdDidResolver()] : [new LocalDidResolver()], + registrars: cheqd ? [new CheqdDidRegistrar()] : undefined, }), cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), @@ -117,83 +121,6 @@ export const getAnonCredsModules = ({ return modules } -export async function presentAnonCredsProof({ - verifierAgent, - verifierReplay, - - holderAgent, - holderReplay, - - verifierHolderConnectionId, - - request: { attributes, predicates }, -}: { - holderAgent: AnonCredsTestsAgent - holderReplay: EventReplaySubject - - verifierAgent: AnonCredsTestsAgent - verifierReplay: EventReplaySubject - - verifierHolderConnectionId: string - request: { - attributes?: Record - predicates?: Record - } -}) { - let holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { - state: ProofState.RequestReceived, - }) - - let verifierProofExchangeRecord = await verifierAgent.proofs.requestProof({ - connectionId: verifierHolderConnectionId, - proofFormats: { - anoncreds: { - name: 'Test Proof Request', - requested_attributes: attributes, - requested_predicates: predicates, - version: '1.0', - }, - }, - protocolVersion: 'v2', - }) - - let holderProofExchangeRecord = await holderProofExchangeRecordPromise - - const selectedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ - proofRecordId: holderProofExchangeRecord.id, - }) - - const verifierProofExchangeRecordPromise = waitForProofExchangeRecordSubject(verifierReplay, { - threadId: holderProofExchangeRecord.threadId, - state: ProofState.PresentationReceived, - }) - - await holderAgent.proofs.acceptRequest({ - proofRecordId: holderProofExchangeRecord.id, - proofFormats: { anoncreds: selectedCredentials.proofFormats.anoncreds }, - }) - - verifierProofExchangeRecord = await verifierProofExchangeRecordPromise - - // assert presentation is valid - expect(verifierProofExchangeRecord.isVerified).toBe(true) - - holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { - threadId: holderProofExchangeRecord.threadId, - state: ProofState.Done, - }) - - verifierProofExchangeRecord = await verifierAgent.proofs.acceptPresentation({ - proofRecordId: verifierProofExchangeRecord.id, - }) - holderProofExchangeRecord = await holderProofExchangeRecordPromise - - return { - verifierProofExchangeRecord, - holderProofExchangeRecord, - } -} - export async function issueAnonCredsCredential({ issuerAgent, issuerReplay, @@ -260,6 +187,8 @@ interface SetupAnonCredsTestsReturn({ + method: 'cheqd', + secret: { + verificationMethod: { + id: 'key-10', + type: 'Ed25519VerificationKey2020', + privateKey, + }, + }, + options: { + network: 'testnet', + methodSpecificIdAlgo: 'uuid', + }, + }) + issuerId = did.didState.did as string + } else { + throw new CredoError('issuerId is required if cheqd is not used') + } + const { credentialDefinition, revocationRegistryDefinition, revocationStatusList, schema } = await prepareForAnonCredsIssuance(issuerAgent, { issuerId, @@ -399,6 +360,8 @@ export async function setupAnonCredsTests< holderAgent, holderReplay, + issuerId, + verifierAgent: verifierName ? verifierAgent : undefined, verifierReplay: verifierName ? verifierReplay : undefined, @@ -422,12 +385,6 @@ export async function prepareForAnonCredsIssuance( issuerId, }: { attributeNames: string[]; supportRevocation?: boolean; issuerId: string } ) { - //const key = await agent.wallet.createKey({ keyType: KeyType.Ed25519 }) - - const didDocument = new DidDocumentBuilder(issuerId).build() - - await agent.dids.import({ did: issuerId, didDocument }) - const schema = await registerSchema(agent, { // TODO: update attrNames to attributeNames attrNames: attributeNames, diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts index 6cdc60ea0d..300fd35be7 100644 --- a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts @@ -7,16 +7,12 @@ import { CredentialState, DidResolverService, DidsModuleConfig, - Ed25519Signature2018, InjectionSymbols, KeyDidRegistrar, KeyDidResolver, - KeyType, ProofExchangeRecord, ProofState, SignatureSuiteToken, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredential, W3cCredentialService, W3cCredentialSubject, @@ -32,7 +28,6 @@ import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { agentDependencies, getAgentConfig, getAgentContext, testLogger } from '../../core/tests' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../src/anoncreds-rs' -import { getAnonCredsTagsFromRecord } from '../src/utils/w3cAnonCredsUtils' import { InMemoryTailsFileService } from './InMemoryTailsFileService' import { anoncreds } from './helpers' @@ -98,18 +93,7 @@ const agentContext = getAgentContext({ [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], - [ - SignatureSuiteToken, - { - suiteClass: Ed25519Signature2018, - proofType: 'Ed25519Signature2018', - verificationMethodTypes: [ - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, - ], - keyTypes: [KeyType.Ed25519], - }, - ], + [SignatureSuiteToken, 'default'], ], agentConfig, wallet, @@ -397,11 +381,10 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean const credentialRecordId = holderCredentialRecord.credentials[0].credentialRecordId const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) const credentialRecord = await w3cCredentialService.getCredentialRecordById(agentContext, credentialRecordId) - const credentialId = getAnonCredsTagsFromRecord(credentialRecord)?.anonCredsCredentialId - if (!credentialId) throw new Error('Credential ID not found') + const credentialId = credentialRecord.id const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { - credentialId, + id: credentialId, }) expect(anonCredsCredential).toEqual({ diff --git a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts index f182c89fde..da9df670b1 100644 --- a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts @@ -221,7 +221,7 @@ describe('data integrity format service (w3c)', () => { await expect( anonCredsHolderService.getCredential(agentContext, { - credentialId: holderCredentialRecord.id, + id: holderCredentialRecord.id, }) ).rejects.toThrow() diff --git a/packages/anoncreds/tests/data-integrity-flow.test.ts b/packages/anoncreds/tests/data-integrity-flow.test.ts index eec52b74e5..155e618368 100644 --- a/packages/anoncreds/tests/data-integrity-flow.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow.test.ts @@ -226,7 +226,7 @@ async function anonCredsFlowTest(options: { await expect( anonCredsHolderService.getCredential(agentContext, { - credentialId: holderCredentialRecord.id, + id: holderCredentialRecord.id, }) ).rejects.toThrow() diff --git a/packages/anoncreds/tests/fixtures/presentation-definition.ts b/packages/anoncreds/tests/fixtures/presentation-definition.ts index 67d2330622..34076b1c2a 100644 --- a/packages/anoncreds/tests/fixtures/presentation-definition.ts +++ b/packages/anoncreds/tests/fixtures/presentation-definition.ts @@ -1,6 +1,6 @@ -import type { PresentationDefinitionV1 } from '@sphereon/pex-models' +import type { DifPresentationExchangeDefinitionV1 } from '@credo-ts/core' -export const presentationDefinition: PresentationDefinitionV1 = { +export const presentationDefinition: DifPresentationExchangeDefinitionV1 = { id: '5591656f-5b5d-40f8-ab5c-9041c8e3a6a0', name: 'Age Verification', purpose: 'We need to verify your age before entering a bar', diff --git a/packages/anoncreds/tests/indy-flow.test.ts b/packages/anoncreds/tests/indy-flow.test.ts index 719afdcbe6..a40ccf1da7 100644 --- a/packages/anoncreds/tests/indy-flow.test.ts +++ b/packages/anoncreds/tests/indy-flow.test.ts @@ -8,11 +8,7 @@ import { InjectionSymbols, ProofState, ProofExchangeRecord, - Ed25519Signature2018, - KeyType, SignatureSuiteToken, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, W3cCredentialsModuleConfig, DidResolverService, DidsModuleConfig, @@ -77,18 +73,7 @@ const agentContext = getAgentContext({ [InjectionSymbols.Logger, testLogger], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], [AnonCredsModuleConfig, anonCredsModuleConfig], - [ - SignatureSuiteToken, - { - suiteClass: Ed25519Signature2018, - proofType: 'Ed25519Signature2018', - verificationMethodTypes: [ - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, - ], - keyTypes: [KeyType.Ed25519], - }, - ], + [SignatureSuiteToken, 'default'], ], agentConfig, wallet, @@ -299,7 +284,7 @@ describe('Legacy indy format services using anoncreds-rs', () => { const credentialId = holderCredentialRecord.credentials[0].credentialRecordId const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { - credentialId, + id: credentialId, }) expect(anonCredsCredential).toEqual({ diff --git a/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts b/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts index f5d4cfa3d6..647f3f76cd 100644 --- a/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts +++ b/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts @@ -1,4 +1,4 @@ -import type { AnonCredsTestsAgent } from './helpers/cheqdAnonCredsSetup' +import type { AnonCredsTestsAgent } from '../../anoncreds/tests/anoncredsSetup' import type { EventReplaySubject } from '../../core/tests' import type { InputDescriptorV2 } from '@sphereon/pex-models' @@ -13,12 +13,11 @@ import { } from '@credo-ts/core' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { setupAnonCredsTests } from '../../anoncreds/tests/anoncredsSetup' import { presentationDefinition } from '../../anoncreds/tests/fixtures/presentation-definition' import { createDidKidVerificationMethod } from '../../core/tests' import { waitForCredentialRecordSubject, waitForProofExchangeRecord } from '../../core/tests/helpers' -import { setupAnonCredsTests } from './helpers/cheqdAnonCredsSetup' - describe('anoncreds w3c data integrity e2e tests', () => { let issuerId: string let issuerAgent: AnonCredsTestsAgent @@ -30,8 +29,6 @@ describe('anoncreds w3c data integrity e2e tests', () => { let issuerReplay: EventReplaySubject let holderReplay: EventReplaySubject - const inMemoryRegistry = new InMemoryAnonCredsRegistry() - afterEach(async () => { await issuerAgent.shutdown() await issuerAgent.wallet.delete() @@ -50,224 +47,193 @@ describe('anoncreds w3c data integrity e2e tests', () => { holderIssuerConnectionId, issuerId, } = await setupAnonCredsTests({ - issuerName: 'Faber Agent Credentials v2', - holderName: 'Alice Agent Credentials v2', + issuerName: 'Issuer Agent Credentials v2', + holderName: 'Holder Agent Credentials v2', attributeNames: ['id', 'name', 'height', 'age'], - registries: [inMemoryRegistry], + registries: [new InMemoryAnonCredsRegistry()], + cheqd: {}, })) - await anonCredsFlowTest({ - issuerId, - credentialDefinitionId, - issuerHolderConnectionId, - holderIssuerConnectionId, - issuerReplay, - holderReplay, - issuer: issuerAgent, - holder: holderAgent, - }) - }) -}) - -async function anonCredsFlowTest(options: { - issuer: AnonCredsTestsAgent - issuerId: string - holder: AnonCredsTestsAgent - issuerHolderConnectionId: string - holderIssuerConnectionId: string - issuerReplay: EventReplaySubject - holderReplay: EventReplaySubject - credentialDefinitionId: string -}) { - const { - credentialDefinitionId, - issuerHolderConnectionId, - holderIssuerConnectionId, - issuer, - issuerId, - holder, - issuerReplay, - holderReplay, - } = options - const holderKdv = await createDidKidVerificationMethod(holder.context, '96213c3d7fc8d4d6754c7a0fd969598f') - const linkSecret = await holder.modules.anoncreds.createLinkSecret({ linkSecretId: 'linkSecretId' }) - expect(linkSecret).toBe('linkSecretId') + const holderKdv = await createDidKidVerificationMethod(holderAgent.context, '96213c3d7fc8d4d6754c7a0fd969598f') + const linkSecret = await holderAgent.modules.anoncreds.createLinkSecret({ linkSecretId: 'linkSecretId' }) + expect(linkSecret).toBe('linkSecretId') - const credential = new W3cCredential({ - context: [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/security/data-integrity/v2', - { - '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', - }, - ], - type: ['VerifiableCredential'], - issuer: issuerId, - issuanceDate: new Date().toISOString(), - credentialSubject: new W3cCredentialSubject({ - id: holderKdv.did, - claims: { name: 'John', age: '25', height: 173 }, - }), - }) + const credential = new W3cCredential({ + context: [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + { + '@vocab': 'https://www.w3.org/ns/credentials/issuer-dependent#', + }, + ], + type: ['VerifiableCredential'], + issuer: issuerId, + issuanceDate: new Date().toISOString(), + credentialSubject: new W3cCredentialSubject({ + id: holderKdv.did, + claims: { name: 'John', age: '25', height: 173 }, + }), + }) - // issuer offers credential - let issuerRecord = await issuer.credentials.offerCredential({ - protocolVersion: 'v2', - autoAcceptCredential: AutoAcceptCredential.Never, - connectionId: issuerHolderConnectionId, - credentialFormats: { - dataIntegrity: { - bindingRequired: true, - credential, - anonCredsLinkSecretBinding: { - credentialDefinitionId, - revocationRegistryDefinitionId: undefined, - revocationRegistryIndex: undefined, + // issuer offers credential + let issuerRecord = await issuerAgent.credentials.offerCredential({ + protocolVersion: 'v2', + autoAcceptCredential: AutoAcceptCredential.Never, + connectionId: issuerHolderConnectionId, + credentialFormats: { + dataIntegrity: { + bindingRequired: true, + credential, + anonCredsLinkSecretBinding: { + credentialDefinitionId, + revocationRegistryDefinitionId: undefined, + revocationRegistryIndex: undefined, + }, + didCommSignedAttachmentBinding: {}, }, - didCommSignedAttachmentBinding: {}, }, - }, - }) + }) - // Holder processes and accepts offer - let holderRecord = await waitForCredentialRecordSubject(holderReplay, { - state: CredentialState.OfferReceived, - threadId: issuerRecord.threadId, - }) - holderRecord = await holder.credentials.acceptOffer({ - credentialRecordId: holderRecord.id, - autoAcceptCredential: AutoAcceptCredential.Never, - credentialFormats: { - dataIntegrity: { - anonCredsLinkSecret: { - linkSecretId: 'linkSecretId', + // Holder processes and accepts offer + let holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.OfferReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holderAgent.credentials.acceptOffer({ + credentialRecordId: holderRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: { + anonCredsLinkSecret: { + linkSecretId: 'linkSecretId', + }, }, }, - }, - }) + }) - // issuer receives request and accepts - issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { - state: CredentialState.RequestReceived, - threadId: holderRecord.threadId, - }) - issuerRecord = await issuer.credentials.acceptRequest({ - credentialRecordId: issuerRecord.id, - autoAcceptCredential: AutoAcceptCredential.Never, - credentialFormats: { - dataIntegrity: {}, - }, - }) + // issuer receives request and accepts + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.RequestReceived, + threadId: holderRecord.threadId, + }) + issuerRecord = await issuerAgent.credentials.acceptRequest({ + credentialRecordId: issuerRecord.id, + autoAcceptCredential: AutoAcceptCredential.Never, + credentialFormats: { + dataIntegrity: {}, + }, + }) - holderRecord = await waitForCredentialRecordSubject(holderReplay, { - state: CredentialState.CredentialReceived, - threadId: issuerRecord.threadId, - }) - holderRecord = await holder.credentials.acceptCredential({ - credentialRecordId: holderRecord.id, - }) + holderRecord = await waitForCredentialRecordSubject(holderReplay, { + state: CredentialState.CredentialReceived, + threadId: issuerRecord.threadId, + }) + holderRecord = await holderAgent.credentials.acceptCredential({ + credentialRecordId: holderRecord.id, + }) - issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { - state: CredentialState.Done, - threadId: holderRecord.threadId, - }) + issuerRecord = await waitForCredentialRecordSubject(issuerReplay, { + state: CredentialState.Done, + threadId: holderRecord.threadId, + }) - expect(holderRecord).toMatchObject({ - type: CredentialExchangeRecord.type, - id: expect.any(String), - createdAt: expect.any(Date), - metadata: { - data: { - '_anoncreds/credential': { - credentialDefinitionId, - schemaId: expect.any(String), - }, - '_anoncreds/credentialRequest': { - link_secret_blinding_data: { - v_prime: expect.any(String), - vr_prime: null, + expect(holderRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_anoncreds/credential': { + credentialDefinitionId, + schemaId: expect.any(String), + }, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: { + v_prime: expect.any(String), + vr_prime: null, + }, + nonce: expect.any(String), + link_secret_name: 'linkSecretId', }, - nonce: expect.any(String), - link_secret_name: 'linkSecretId', }, }, - }, - state: CredentialState.Done, - }) + state: CredentialState.Done, + }) - const tags = holderRecord.getTags() - expect(tags.credentialIds).toHaveLength(1) + const tags = holderRecord.getTags() + expect(tags.credentialIds).toHaveLength(1) - await expect( - holder.dependencyManager - .resolve(W3cCredentialService) - .getCredentialRecordById(holder.context, tags.credentialIds[0]) - ).resolves + await expect( + holderAgent.dependencyManager + .resolve(W3cCredentialService) + .getCredentialRecordById(holderAgent.context, tags.credentialIds[0]) + ).resolves - let issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuer, { - state: ProofState.ProposalReceived, - }) + let issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuerAgent, { + state: ProofState.ProposalReceived, + }) - const pdCopy = JSON.parse(JSON.stringify(presentationDefinition)) - pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => delete ide.constraints?.statuses) - pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => { - if (ide.constraints.fields && ide.constraints.fields[0].filter?.const) { - ide.constraints.fields[0].filter.const = issuerId - } - }) + const pdCopy = JSON.parse(JSON.stringify(presentationDefinition)) + pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => delete ide.constraints?.statuses) + pdCopy.input_descriptors.forEach((ide: InputDescriptorV2) => { + if (ide.constraints.fields && ide.constraints.fields[0].filter?.const) { + ide.constraints.fields[0].filter.const = issuerId + } + }) - let holderProofExchangeRecord = await holder.proofs.proposeProof({ - protocolVersion: 'v2', - connectionId: holderIssuerConnectionId, - proofFormats: { - presentationExchange: { - presentationDefinition: pdCopy, + let holderProofExchangeRecord = await holderAgent.proofs.proposeProof({ + protocolVersion: 'v2', + connectionId: holderIssuerConnectionId, + proofFormats: { + presentationExchange: { + presentationDefinition: pdCopy, + }, }, - }, - }) + }) - let issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + let issuerProofExchangeRecord = await issuerProofExchangeRecordPromise - let holderProofExchangeRecordPromise = waitForProofExchangeRecord(holder, { - state: ProofState.RequestReceived, - }) + let holderProofExchangeRecordPromise = waitForProofExchangeRecord(holderAgent, { + state: ProofState.RequestReceived, + }) - issuerProofExchangeRecord = await issuer.proofs.acceptProposal({ - proofRecordId: issuerProofExchangeRecord.id, - }) + issuerProofExchangeRecord = await issuerAgent.proofs.acceptProposal({ + proofRecordId: issuerProofExchangeRecord.id, + }) - holderProofExchangeRecord = await holderProofExchangeRecordPromise + holderProofExchangeRecord = await holderProofExchangeRecordPromise - const requestedCredentials = await holder.proofs.selectCredentialsForRequest({ - proofRecordId: holderProofExchangeRecord.id, - }) + const requestedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) - const selectedCredentials = requestedCredentials.proofFormats.presentationExchange?.credentials - if (!selectedCredentials) { - throw new Error('No credentials found for presentation exchange') - } + const selectedCredentials = requestedCredentials.proofFormats.presentationExchange?.credentials + if (!selectedCredentials) { + throw new Error('No credentials found for presentation exchange') + } - issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuer, { - threadId: holderProofExchangeRecord.threadId, - state: ProofState.PresentationReceived, - }) + issuerProofExchangeRecordPromise = waitForProofExchangeRecord(issuerAgent, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) - await holder.proofs.acceptRequest({ - proofRecordId: holderProofExchangeRecord.id, - proofFormats: { - presentationExchange: { - credentials: selectedCredentials, + await holderAgent.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { + presentationExchange: { + credentials: selectedCredentials, + }, }, - }, - }) - issuerProofExchangeRecord = await issuerProofExchangeRecordPromise + }) + issuerProofExchangeRecord = await issuerProofExchangeRecordPromise - holderProofExchangeRecordPromise = waitForProofExchangeRecord(holder, { - threadId: holderProofExchangeRecord.threadId, - state: ProofState.Done, - }) + holderProofExchangeRecordPromise = waitForProofExchangeRecord(holderAgent, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) - await issuer.proofs.acceptPresentation({ proofRecordId: issuerProofExchangeRecord.id }) + await issuerAgent.proofs.acceptPresentation({ proofRecordId: issuerProofExchangeRecord.id }) - holderProofExchangeRecord = await holderProofExchangeRecordPromise -} + holderProofExchangeRecord = await holderProofExchangeRecordPromise + }) +}) diff --git a/packages/cheqd/tests/helpers/cheqdAnonCredsSetup.ts b/packages/cheqd/tests/helpers/cheqdAnonCredsSetup.ts deleted file mode 100644 index 5832feb809..0000000000 --- a/packages/cheqd/tests/helpers/cheqdAnonCredsSetup.ts +++ /dev/null @@ -1,356 +0,0 @@ -import type { EventReplaySubject } from '../../../core/tests' -import type { CheqdDidCreateOptions } from '../../src' -import type { - AnonCredsRegisterCredentialDefinitionOptions, - AnonCredsSchema, - RegisterCredentialDefinitionReturnStateFinished, - RegisterSchemaReturnStateFinished, - AnonCredsRegistry, -} from '@credo-ts/anoncreds' -import type { AutoAcceptProof, ConnectionRecord, AutoAcceptCredential } from '@credo-ts/core' - -import { - AnonCredsCredentialFormatService, - AnonCredsProofFormatService, - AnonCredsModule, - DataIntegrityCredentialFormatService, -} from '@credo-ts/anoncreds' -import { - CacheModule, - InMemoryLruCache, - Agent, - CredoError, - CredentialEventTypes, - CredentialsModule, - ProofEventTypes, - ProofsModule, - V2CredentialProtocol, - V2ProofProtocol, - DidsModule, - PresentationExchangeProofFormatService, - TypedArrayEncoder, -} from '@credo-ts/core' -import { randomUUID } from 'crypto' - -import { InMemoryAnonCredsRegistry } from '../../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { anoncreds } from '../../../anoncreds/tests/helpers' -import { sleep } from '../../../core/src/utils/sleep' -import { - setupSubjectTransports, - setupEventReplaySubjects, - testLogger, - getInMemoryAgentOptions, - makeConnection, -} from '../../../core/tests' -import { CheqdDidRegistrar, CheqdDidResolver, CheqdModule } from '../../src' -import { getCheqdModuleConfig } from '../setupCheqdModule' - -// Helper type to get the type of the agents (with the custom modules) for the credential tests -export type AnonCredsTestsAgent = Agent< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ReturnType & { mediationRecipient?: any; mediator?: any } -> - -export const getAnonCredsModules = ({ - autoAcceptCredentials, - autoAcceptProofs, - registries, - seed, - rpcUrl, -}: { - seed?: string - rpcUrl?: string - autoAcceptCredentials?: AutoAcceptCredential - autoAcceptProofs?: AutoAcceptProof - registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] -} = {}) => { - const dataIntegrityCredentialFormatService = new DataIntegrityCredentialFormatService() - const anonCredsCredentialFormatService = new AnonCredsCredentialFormatService() - const anonCredsProofFormatService = new AnonCredsProofFormatService() - const presentationExchangeProofFormatService = new PresentationExchangeProofFormatService() - - const modules = { - cheqdSdk: new CheqdModule(getCheqdModuleConfig(seed, rpcUrl)), - credentials: new CredentialsModule({ - autoAcceptCredentials, - credentialProtocols: [ - new V2CredentialProtocol({ - credentialFormats: [dataIntegrityCredentialFormatService, anonCredsCredentialFormatService], - }), - ], - }), - proofs: new ProofsModule({ - autoAcceptProofs, - proofProtocols: [ - new V2ProofProtocol({ - proofFormats: [anonCredsProofFormatService, presentationExchangeProofFormatService], - }), - ], - }), - anoncreds: new AnonCredsModule({ - registries: registries ?? [new InMemoryAnonCredsRegistry()], - anoncreds, - }), - dids: new DidsModule({ - registrars: [new CheqdDidRegistrar()], - resolvers: [new CheqdDidResolver()], - }), - cache: new CacheModule({ - cache: new InMemoryLruCache({ limit: 100 }), - }), - } as const - - return modules -} - -interface SetupAnonCredsTestsReturn { - issuerAgent: AnonCredsTestsAgent - issuerReplay: EventReplaySubject - - holderAgent: AnonCredsTestsAgent - holderReplay: EventReplaySubject - - issuerHolderConnectionId: CreateConnections extends true ? string : undefined - holderIssuerConnectionId: CreateConnections extends true ? string : undefined - - issuerId: string - - verifierHolderConnectionId: CreateConnections extends true - ? VerifierName extends string - ? string - : undefined - : undefined - holderVerifierConnectionId: CreateConnections extends true - ? VerifierName extends string - ? string - : undefined - : undefined - - verifierAgent: VerifierName extends string ? AnonCredsTestsAgent : undefined - verifierReplay: VerifierName extends string ? EventReplaySubject : undefined - - schemaId: string - credentialDefinitionId: string -} - -async function createDid(agent: AnonCredsTestsAgent) { - const privateKey = TypedArrayEncoder.fromString('000000000000000000000000000cheqd') - const did = await agent.dids.create({ - method: 'cheqd', - secret: { - verificationMethod: { - id: 'key-10', - type: 'Ed25519VerificationKey2020', - privateKey, - }, - }, - options: { - network: 'testnet', - methodSpecificIdAlgo: 'uuid', - }, - }) - expect(did.didState).toMatchObject({ state: 'finished' }) - return did.didState.did as string -} - -export async function setupAnonCredsTests< - VerifierName extends string | undefined = undefined, - CreateConnections extends boolean = true ->({ - issuerName, - holderName, - verifierName, - autoAcceptCredentials, - autoAcceptProofs, - attributeNames, - createConnections, - registries, -}: { - issuerName: string - holderName: string - verifierName?: VerifierName - autoAcceptCredentials?: AutoAcceptCredential - autoAcceptProofs?: AutoAcceptProof - attributeNames: string[] - createConnections?: CreateConnections - registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] -}): Promise> { - const issuerAgent = new Agent( - getInMemoryAgentOptions( - issuerName, - { - endpoints: ['rxjs:issuer'], - }, - getAnonCredsModules({ - autoAcceptCredentials, - autoAcceptProofs, - registries, - }) - ) - ) - - const holderAgent = new Agent( - getInMemoryAgentOptions( - holderName, - { - endpoints: ['rxjs:holder'], - }, - getAnonCredsModules({ - autoAcceptCredentials, - autoAcceptProofs, - registries, - }) - ) - ) - - const verifierAgent = verifierName - ? new Agent( - getInMemoryAgentOptions( - verifierName, - { - endpoints: ['rxjs:verifier'], - }, - getAnonCredsModules({ - autoAcceptCredentials, - autoAcceptProofs, - registries, - }) - ) - ) - : undefined - - setupSubjectTransports(verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent]) - const [issuerReplay, holderReplay, verifierReplay] = setupEventReplaySubjects( - verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent], - [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] - ) - - await issuerAgent.initialize() - await holderAgent.initialize() - if (verifierAgent) await verifierAgent.initialize() - - const issuerId = await createDid(issuerAgent) - - // Create default link secret for holder - await holderAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - - const { credentialDefinition, schema } = await prepareForAnonCredsIssuance(issuerAgent, { - attributeNames, - issuerId, - }) - - let issuerHolderConnection: ConnectionRecord | undefined - let holderIssuerConnection: ConnectionRecord | undefined - let verifierHolderConnection: ConnectionRecord | undefined - let holderVerifierConnection: ConnectionRecord | undefined - - if (createConnections ?? true) { - ;[issuerHolderConnection, holderIssuerConnection] = await makeConnection(issuerAgent, holderAgent) - - if (verifierAgent) { - ;[holderVerifierConnection, verifierHolderConnection] = await makeConnection(holderAgent, verifierAgent) - } - } - - return { - issuerAgent, - issuerReplay, - - holderAgent, - holderReplay, - - issuerId, - - verifierAgent: verifierName ? verifierAgent : undefined, - verifierReplay: verifierName ? verifierReplay : undefined, - - credentialDefinitionId: credentialDefinition.credentialDefinitionId, - schemaId: schema.schemaId, - - issuerHolderConnectionId: issuerHolderConnection?.id, - holderIssuerConnectionId: holderIssuerConnection?.id, - holderVerifierConnectionId: holderVerifierConnection?.id, - verifierHolderConnectionId: verifierHolderConnection?.id, - } as unknown as SetupAnonCredsTestsReturn -} - -export async function prepareForAnonCredsIssuance( - agent: Agent, - { attributeNames, issuerId }: { attributeNames: string[]; issuerId: string } -) { - //const key = await agent.wallet.createKey({ keyType: KeyType.Ed25519 }) - - const schema = await registerSchema(agent, { - // TODO: update attrNames to attributeNames - attrNames: attributeNames, - name: `Schema ${randomUUID()}`, - version: '1.0', - issuerId, - }) - - // Wait some time pass to let ledger settle the object - await sleep(1000) - - const credentialDefinition = await registerCredentialDefinition(agent, { - schemaId: schema.schemaId, - issuerId, - tag: 'default', - }) - - // Wait some time pass to let ledger settle the object - await sleep(1000) - - return { - schema: { - ...schema, - schemaId: schema.schemaId, - }, - credentialDefinition: { - ...credentialDefinition, - credentialDefinitionId: credentialDefinition.credentialDefinitionId, - }, - } -} - -async function registerSchema( - agent: AnonCredsTestsAgent, - schema: AnonCredsSchema -): Promise { - const { schemaState } = await agent.modules.anoncreds.registerSchema({ - schema, - options: {}, - }) - - testLogger.test(`created schema with id ${schemaState.schemaId}`, schema) - - if (schemaState.state !== 'finished') { - throw new CredoError(`Schema not created: ${schemaState.state === 'failed' ? schemaState.reason : 'Not finished'}`) - } - - return schemaState -} - -async function registerCredentialDefinition( - agent: AnonCredsTestsAgent, - credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions -): Promise { - const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ - credentialDefinition, - options: { - supportRevocation: false, - }, - }) - - if (credentialDefinitionState.state !== 'finished') { - throw new CredoError( - `Credential definition not created: ${ - credentialDefinitionState.state === 'failed' ? credentialDefinitionState.reason : 'Not finished' - }` - ) - } - - return credentialDefinitionState -} diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts index ead9140de4..d8be4784b7 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/DataIntegrityCredentialFormat.ts @@ -28,7 +28,6 @@ export interface DataIntegrityAcceptOfferFormat { /** * This defines the module payload for calling CredentialsApi.offerCredential - * or CredentialsApi.negotiateProposal */ export interface DataIntegrityOfferCredentialFormat { credential: W3cCredential | JsonObject @@ -38,8 +37,7 @@ export interface DataIntegrityOfferCredentialFormat { } /** - * This defines the module payload for calling CredentialsApi.acceptRequest. No options are available for this - * method, so it's an empty object + * This defines the module payload for calling CredentialsApi.acceptRequest */ export interface DataIntegrityAcceptRequestFormat { credentialSubjectId?: string diff --git a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts index bc3de26b49..c57124b270 100644 --- a/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts +++ b/packages/core/src/modules/credentials/formats/dataIntegrity/dataIntegrityExchange.ts @@ -1,41 +1,140 @@ -import type { JsonObject } from '../../../../types' +import { Expose, Type } from 'class-transformer' +import { ArrayNotEmpty, IsBoolean, IsEnum, IsOptional, IsString, ValidateNested } from 'class-validator' -export type W3C_VC_DATA_MODEL_VERSION = '1.1' | '2.0' +import { JsonObject } from '../../../../types' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { W3cCredential } from '../../../vc' -// This binding method is intended to be used in combination with a credential containing an AnonCreds proof. -export interface AnonCredsLinkSecretBindingMethod { - cred_def_id: string +const SUPPORTED_W3C_VC_DATA_MODEL_VERSIONS = ['1.1', '2.0'] as const +export type W3C_VC_DATA_MODEL_VERSION = (typeof SUPPORTED_W3C_VC_DATA_MODEL_VERSIONS)[number] + +export interface AnonCredsLinkSecretBindingMethodOptions { + credentialDefinitionId: string nonce: string - key_correctness_proof: Record + keyCorrectnessProof: Record } -export interface DidCommSignedAttachmentBindingMethod { - algs_supported: string[] - did_methods_supported: string[] +// This binding method is intended to be used in combination with a credential containing an AnonCreds proof. +export class AnonCredsLinkSecretBindingMethod { + public constructor(options: AnonCredsLinkSecretBindingMethodOptions) { + if (options) { + this.credentialDefinitionId = options.credentialDefinitionId + this.nonce = options.nonce + this.keyCorrectnessProof = options.keyCorrectnessProof + } + } + + @IsString() + @Expose({ name: 'cred_def_id' }) + public credentialDefinitionId!: string + + @IsString() + public nonce!: string + + @Expose({ name: 'key_correctness_proof' }) + public keyCorrectnessProof!: Record +} + +export interface DidCommSignedAttachmentBindingMethodOptions { + algSupported: string[] + didMethodsSupported: string[] nonce: string } -export interface DataIntegrityBindingMethods { - anoncreds_link_secret?: AnonCredsLinkSecretBindingMethod - didcomm_signed_attachment?: DidCommSignedAttachmentBindingMethod +export class DidCommSignedAttachmentBindingMethod { + public constructor(options: DidCommSignedAttachmentBindingMethodOptions) { + if (options) { + this.algsSupported = options.algSupported + this.didMethodsSupported = options.didMethodsSupported + this.nonce = options.nonce + } + } + + @IsString({ each: true }) + @Expose({ name: 'algs_supported' }) + public algsSupported!: string[] + + @IsString({ each: true }) + @Expose({ name: 'did_methods_supported' }) + public didMethodsSupported!: string[] + + @IsString() + public nonce!: string } -export interface DataIntegrityCredentialOffer { +export interface DataIntegrityBindingMethodsOptions { + anonCredsLinkSecret?: AnonCredsLinkSecretBindingMethod + didcommSignedAttachment?: DidCommSignedAttachmentBindingMethod +} + +export class DataIntegrityBindingMethods { + public constructor(options: DataIntegrityBindingMethodsOptions) { + if (options) { + this.anoncredsLinkSecret = options.anonCredsLinkSecret + this.didcommSignedAttachment = options.didcommSignedAttachment + } + } + + @IsOptional() + @ValidateNested() + @Type(() => AnonCredsLinkSecretBindingMethod) + @Expose({ name: 'anoncreds_link_secret' }) + public anoncredsLinkSecret?: AnonCredsLinkSecretBindingMethod + + @IsOptional() + @ValidateNested() + @Type(() => DidCommSignedAttachmentBindingMethod) + @Expose({ name: 'didcomm_signed_attachment' }) + public didcommSignedAttachment?: DidCommSignedAttachmentBindingMethod +} + +export interface DataIntegrityCredentialOfferOptions { + dataModelVersionsSupported: W3C_VC_DATA_MODEL_VERSION[] + bindingRequired?: boolean + bindingMethod?: DataIntegrityBindingMethods + credential: W3cCredential | JsonObject +} + +export class DataIntegrityCredentialOffer { + public constructor(options: DataIntegrityCredentialOfferOptions) { + if (options) { + this.credential = + options.credential instanceof W3cCredential ? JsonTransformer.toJSON(options.credential) : options.credential + this.bindingRequired = options.bindingRequired + this.bindingMethod = options.bindingMethod + this.dataModelVersionsSupported = options.dataModelVersionsSupported + } + } + // List of strings indicating the supported VC Data Model versions. // The list MUST contain at least one value. The values MUST be a valid data model version. Current supported values include 1.1 and 2.0. - data_model_versions_supported: W3C_VC_DATA_MODEL_VERSION[] + @ArrayNotEmpty() + @IsEnum(SUPPORTED_W3C_VC_DATA_MODEL_VERSIONS, { each: true }) + @Expose({ name: 'data_model_versions_supported' }) + public dataModelVersionsSupported!: W3C_VC_DATA_MODEL_VERSION[] + // Boolean indicating whether the credential MUST be bound to the holder. If omitted, the credential is not required to be bound to the holder. // If set to true, the credential MUST be bound to the holder using at least one of the binding methods defined in binding_method. - binding_required?: boolean + @IsOptional() + @IsBoolean() + @Expose({ name: 'binding_required' }) + public bindingRequired?: boolean + // Required if binding_required is true. // Object containing key-value pairs of binding methods supported by the issuer to bind the credential to a holder. // If the value is omitted, this indicates the issuer does not support any binding methods for issuance of the credential. - binding_method?: DataIntegrityBindingMethods + @IsOptional() + @ValidateNested() + @Type(() => DataIntegrityBindingMethods) + @Expose({ name: 'binding_method' }) + public bindingMethod?: DataIntegrityBindingMethods + // The credential should be compliant with the VC Data Model. // The credential MUST NOT contain any proofs. // Some properties MAY be omitted if they will only be available at time of issuance, such as issuanceDate, issuer, credentialSubject.id, credentialStatus, credentialStatus.id. // The credential MUST be conformant with one of the data model versions indicated in data_model_versions_supported. - credential: JsonObject + @Expose({ name: 'credential' }) + public credential!: JsonObject } export interface AnonCredsLinkSecretDataIntegrityBindingProof { diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 518496254c..4332b3a473 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -10,7 +10,6 @@ import type { import type { PresentationToCreate } from './utils' import type { AgentContext } from '../../agent' import type { Query } from '../../storage/StorageService' -import type { JsonObject } from '../../types' import type { VerificationMethod } from '../dids' import type { SdJwtVcRecord } from '../sd-jwt-vc' import type { W3cCredentialRecord } from '../vc' @@ -21,7 +20,7 @@ import type { Validated, VerifiablePresentationResult, } from '@sphereon/pex' -import type { InputDescriptorV2, PresentationDefinitionV1, PresentationSubmission } from '@sphereon/pex-models' +import type { InputDescriptorV2 } from '@sphereon/pex-models' import type { W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, W3CVerifiablePresentation, @@ -45,7 +44,7 @@ import { } from '../vc' import { AnonCredsDataIntegrityServiceSymbol, - AnoncredsDataIntegrityCryptosuite, + ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE, } from '../vc/data-integrity/models/IAnonCredsDataIntegrityService' import { DifPresentationExchangeError } from './DifPresentationExchangeError' @@ -175,7 +174,7 @@ export class DifPresentationExchangeService { // FIXME: cast to V1, as tsc errors for strange reasons if not const inputDescriptorIds = presentationToCreate.verifiableCredentials.map((c) => c.inputDescriptorId) const inputDescriptorsForPresentation = ( - presentationDefinition as PresentationDefinitionV1 + presentationDefinition as DifPresentationExchangeDefinitionV1 ).input_descriptors.filter((inputDescriptor) => inputDescriptorIds.includes(inputDescriptor.id)) // Get all the credentials for the presentation @@ -402,7 +401,7 @@ export class DifPresentationExchangeService { private shouldSignUsingAnoncredsDataIntegrity( presentationToCreate: PresentationToCreate, - presentationSubmission: PresentationSubmission + presentationSubmission: DifPresentationExchangeSubmission ) { if (presentationToCreate.claimFormat !== ClaimFormat.LdpVp) return undefined @@ -418,7 +417,8 @@ export class DifPresentationExchangeService { }) const commonCryptosuites = cryptosuites.reduce((a, b) => a.filter((c) => b.includes(c))) - if (commonCryptosuites.length === 0 || !commonCryptosuites.includes(AnoncredsDataIntegrityCryptosuite)) return false + if (commonCryptosuites.length === 0 || !commonCryptosuites.includes(ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE)) + return false return true } @@ -430,7 +430,6 @@ export class DifPresentationExchangeService { options, presentationDefinition, presentationSubmission, - selectedCredentials, } = callBackParams const { challenge, domain } = options.proofOptions ?? {} @@ -466,12 +465,13 @@ export class DifPresentationExchangeService { const presentation = await anoncredsDataIntegrityService.createPresentation(agentContext, { presentationDefinition, presentationSubmission, - selectedCredentials: selectedCredentials as JsonObject[], selectedCredentialRecords: presentationToCreate.verifiableCredentials.map((vc) => vc.credential), challenge, }) - presentation.presentation_submission = presentationSubmission as unknown as JsonObject - return presentation as unknown as SphereonW3cVerifiablePresentation + return { + ...presentation.toJSON(), + presentation_submission: presentationSubmission, + } as unknown as SphereonW3cVerifiablePresentation } // Determine a suitable verification method for the presentation @@ -577,7 +577,7 @@ export class DifPresentationExchangeService { // this could help enormously in the amount of credentials we have to retrieve from storage. // NOTE: for now we don't support SD-JWT for v1, as I don't know what the schema.uri should be? if (presentationDefinitionVersion.version === PEVersion.v1) { - const pd = presentationDefinition as PresentationDefinitionV1 + const pd = presentationDefinition as DifPresentationExchangeDefinitionV1 // The schema.uri can contain either an expanded type, or a context uri for (const inputDescriptor of pd.input_descriptors) { diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 7a68e03dc5..540b02e5b1 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -6,7 +6,10 @@ import type { } from './DifPresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' import type { JsonValue } from '../../../../types' -import type { DifPexInputDescriptorToCredentials } from '../../../dif-presentation-exchange' +import type { + DifPexInputDescriptorToCredentials, + DifPresentationExchangeSubmission, +} from '../../../dif-presentation-exchange' import type { IAnoncredsDataIntegrityService, W3cVerifiablePresentation, @@ -28,7 +31,6 @@ import type { ProofFormatAutoRespondRequestOptions, ProofFormatAutoRespondPresentationOptions, } from '../ProofFormatServiceOptions' -import type { PresentationSubmission } from '@sphereon/pex-models' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { CredoError } from '../../../../error' @@ -38,7 +40,7 @@ import { DifPresentationExchangeSubmissionLocation, } from '../../../dif-presentation-exchange' import { - AnoncredsDataIntegrityCryptosuite, + ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE, AnonCredsDataIntegrityServiceSymbol, W3cCredentialService, ClaimFormat, @@ -227,7 +229,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic private shouldVerifyUsingAnoncredsDataIntegrity( presentation: W3cVerifiablePresentation, - presentationSubmission: PresentationSubmission + presentationSubmission: DifPresentationExchangeSubmission ) { if (presentation.claimFormat !== ClaimFormat.LdpVp) return false @@ -236,7 +238,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const verifyUsingDataIntegrity = descriptorMap.every((descriptor) => descriptor.format === ClaimFormat.DiVp) if (!verifyUsingDataIntegrity) return false - return presentation.dataIntegrityCryptosuites.includes(AnoncredsDataIntegrityCryptosuite) + return presentation.dataIntegrityCryptosuites.includes(ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE) } public async processPresentation( diff --git a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts index b59540d718..24c2089567 100644 --- a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts +++ b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts @@ -19,8 +19,8 @@ export interface SuiteInfo { export class SignatureSuiteRegistry { private suiteMapping: SuiteInfo[] - public constructor(@injectAll(SignatureSuiteToken) suites: SuiteInfo[]) { - this.suiteMapping = suites + public constructor(@injectAll(SignatureSuiteToken) suites: Array) { + this.suiteMapping = suites.filter((suite): suite is SuiteInfo => suite !== 'default') } public get supportedProofTypes(): string[] { diff --git a/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts b/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts index 1fcfbe8181..c4c3a5fd36 100644 --- a/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts +++ b/packages/core/src/modules/vc/data-integrity/models/DataIntegrityProof.ts @@ -1,4 +1,4 @@ -import { IsOptional, IsString } from 'class-validator' +import { IsEnum, IsOptional, IsString } from 'class-validator' import { IsUri } from '../../../../utils' @@ -40,6 +40,7 @@ export class DataIntegrityProof { } @IsString() + @IsEnum(['DataIntegrityProof']) public type!: string @IsString() diff --git a/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts b/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts index 9e6b839b26..d0b1e3e2ae 100644 --- a/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts +++ b/packages/core/src/modules/vc/data-integrity/models/IAnonCredsDataIntegrityService.ts @@ -1,23 +1,25 @@ import type { W3cJsonLdVerifiablePresentation } from './W3cJsonLdVerifiablePresentation' import type { AgentContext } from '../../../../agent' -import type { JsonObject } from '../../../../types' +import type { + DifPresentationExchangeDefinition, + DifPresentationExchangeSubmission, +} from '../../../dif-presentation-exchange' +import type { W3cPresentation } from '../../models' import type { W3cCredentialRecord } from '../../repository' -import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' -export const AnoncredsDataIntegrityCryptosuite = 'anoncreds-2023' as const +export const ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE = 'anoncreds-2023' as const export interface AnoncredsDataIntegrityCreatePresentation { - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 - presentationSubmission: PresentationSubmission - selectedCredentials: JsonObject[] selectedCredentialRecords: W3cCredentialRecord[] + presentationDefinition: DifPresentationExchangeDefinition + presentationSubmission: DifPresentationExchangeSubmission challenge: string } export interface AnoncredsDataIntegrityVerifyPresentation { presentation: W3cJsonLdVerifiablePresentation - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 - presentationSubmission: PresentationSubmission + presentationDefinition: DifPresentationExchangeDefinition + presentationSubmission: DifPresentationExchangeSubmission challenge: string } @@ -30,7 +32,10 @@ export const AnonCredsDataIntegrityServiceSymbol = Symbol('AnonCredsDataIntegrit * the existing api's. */ export interface IAnoncredsDataIntegrityService { - createPresentation(agentContext: AgentContext, options: AnoncredsDataIntegrityCreatePresentation): Promise + createPresentation( + agentContext: AgentContext, + options: AnoncredsDataIntegrityCreatePresentation + ): Promise verifyPresentation(agentContext: AgentContext, options: AnoncredsDataIntegrityVerifyPresentation): Promise } diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts index 6286714ca0..3f0b485fec 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts @@ -35,35 +35,8 @@ export type DefaultW3cCredentialTags = { types: Array algs?: Array } -export type AnonCredsCredentialTags = { - anonCredsCredentialId: string - anonCredsLinkSecretId: string - anonCredsCredentialRevocationId?: string - anonCredsMethodName: string - // the following keys can be used for every `attribute name` in credential. - [key: `anonCredsAttr::${string}::marker`]: true | undefined - [key: `anonCredsAttr::${string}::value`]: string | undefined - - anonCredsSchemaName: string - anonCredsSchemaVersion: string - - anonCredsSchemaId: string - anonCredsSchemaIssuerId: string - anonCredsCredentialDefinitionId: string - anonCredsRevocationRegistryId?: string - - anonCredsUnqualifiedIssuerId?: string - anonCredsUnqualifiedSchemaId?: string - anonCredsUnqualifiedSchemaIssuerId?: string - anonCredsUnqualifiedCredentialDefinitionId?: string - anonCredsUnqualifiedRevocationRegistryId?: string -} - -export class W3cCredentialRecord extends BaseRecord< - DefaultW3cCredentialTags & Partial, - CustomW3cCredentialTags -> { +export class W3cCredentialRecord extends BaseRecord { public static readonly type = 'W3cCredentialRecord' public readonly type = W3cCredentialRecord.type @@ -80,7 +53,7 @@ export class W3cCredentialRecord extends BaseRecord< } } - public getTags(): DefaultW3cCredentialTags & Partial { + public getTags() { // Contexts are usually strings, but can sometimes be objects. We're unable to use objects as tags, // so we filter out the objects before setting the tags. const stringContexts = this.credential.contexts.filter((ctx): ctx is string => typeof ctx === 'string') diff --git a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts index 60c2d2ebd1..89b7fb89d9 100644 --- a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts +++ b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts @@ -1,5 +1,3 @@ -import type { AnonCredsCredentialTags } from '../W3cCredentialRecord' - import { getAnonCredsTagsFromRecord } from '../../../../../../anoncreds/src/utils/w3cAnonCredsUtils' import { JsonTransformer } from '../../../../utils' import { Ed25519Signature2018Fixtures } from '../../data-integrity/__tests__/fixtures' @@ -30,88 +28,11 @@ describe('W3cCredentialRecord', () => { contexts: credential.contexts, proofTypes: credential.proofTypes, givenId: credential.id, - credentialDefinitionId: undefined, - revocationRegistryId: undefined, - schemaId: undefined, - schemaIssuerId: undefined, - schemaName: undefined, - schemaVersion: undefined, expandedTypes: ['https://expanded.tag#1'], types: ['VerifiableCredential', 'UniversityDegreeCredential'], }) expect(getAnonCredsTagsFromRecord(w3cCredentialRecord)).toBeUndefined() }) - - it('should return default tags (w3cAnoncredsCredential)', () => { - const credential = JsonTransformer.fromJSON( - Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, - W3cJsonLdVerifiableCredential - ) - - const anoncredsCredentialRecordTags: AnonCredsCredentialTags = { - anonCredsSchemaIssuerId: 'schemaIssuerId', - anonCredsSchemaName: 'schemaName', - anonCredsSchemaVersion: 'schemaVersion', - anonCredsSchemaId: 'schemaId', - anonCredsCredentialDefinitionId: 'credentialDefinitionId', - anonCredsCredentialId: 'credentialId', - anonCredsCredentialRevocationId: 'credentialRevocationId', - anonCredsLinkSecretId: 'linkSecretId', - anonCredsMethodName: 'methodName', - anonCredsRevocationRegistryId: 'revocationRegistryId', - } - - const w3cCredentialRecord = new W3cCredentialRecord({ - credential, - tags: { - expandedTypes: ['https://expanded.tag#1'], - }, - }) - - const anonCredsCredentialMetadata = { - credentialId: anoncredsCredentialRecordTags.anonCredsCredentialId, - credentialRevocationId: anoncredsCredentialRecordTags.anonCredsCredentialRevocationId, - linkSecretId: anoncredsCredentialRecordTags.anonCredsLinkSecretId, - methodName: anoncredsCredentialRecordTags.anonCredsMethodName, - } - - w3cCredentialRecord.setTags(anoncredsCredentialRecordTags) - w3cCredentialRecord.metadata.set('_w3c/AnonCredsMetadata', anonCredsCredentialMetadata) - - const anoncredsCredentialTags = { - anonCredsLinkSecretId: 'linkSecretId', - anonCredsMethodName: 'methodName', - anonCredsSchemaId: 'schemaId', - anonCredsSchemaIssuerId: 'schemaIssuerId', - anonCredsSchemaName: 'schemaName', - anonCredsSchemaVersion: 'schemaVersion', - anonCredsCredentialDefinitionId: 'credentialDefinitionId', - anonCredsCredentialId: 'credentialId', - anonCredsRevocationRegistryId: 'revocationRegistryId', - anonCredsCredentialRevocationId: 'credentialRevocationId', - } - - const anonCredsTags = getAnonCredsTagsFromRecord(w3cCredentialRecord) - expect(anonCredsTags).toEqual({ - ...anoncredsCredentialTags, - }) - - expect(w3cCredentialRecord.metadata.get('_w3c/AnonCredsMetadata')).toEqual(anonCredsCredentialMetadata) - - expect(w3cCredentialRecord.getTags()).toEqual({ - claimFormat: 'ldp_vc', - issuerId: credential.issuerId, - subjectIds: credential.credentialSubjectIds, - schemaIds: credential.credentialSchemaIds, - contexts: credential.contexts, - proofTypes: credential.proofTypes, - givenId: credential.id, - types: ['VerifiableCredential', 'UniversityDegreeCredential'], - cryptosuites: [], - expandedTypes: ['https://expanded.tag#1'], - ...anoncredsCredentialTags, - }) - }) }) }) From a74ffcbdee447710af69e0850ea5c2596f6a6bd4 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 18 Feb 2024 09:53:47 +0100 Subject: [PATCH 34/38] fix: merge issue --- .../tests/InMemoryAnonCredsRegistry.ts | 1 - packages/anoncreds/tests/anoncredsSetup.ts | 87 ++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 5dd178a86c..fa397be680 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -40,7 +40,6 @@ import { isIndyDid, isUnqualifiedCredentialDefinitionId, isUnqualifiedSchemaId, - isUnqualifiedIndyDid, } from '../src/utils/indyIdentifiers' import { dateToTimestamp } from '../src/utils/timestamp' diff --git a/packages/anoncreds/tests/anoncredsSetup.ts b/packages/anoncreds/tests/anoncredsSetup.ts index 8d10b2a471..9836ce7454 100644 --- a/packages/anoncreds/tests/anoncredsSetup.ts +++ b/packages/anoncreds/tests/anoncredsSetup.ts @@ -10,6 +10,8 @@ import type { RegisterRevocationRegistryDefinitionReturnStateFinished, AnonCredsRegisterRevocationStatusListOptions, RegisterRevocationStatusListReturnStateFinished, + AnonCredsRequestedAttribute, + AnonCredsRequestedPredicate, } from '../src' import type { CheqdDidCreateOptions } from '@credo-ts/cheqd' import type { AutoAcceptProof, ConnectionRecord } from '@credo-ts/core' @@ -31,6 +33,7 @@ import { DidsModule, PresentationExchangeProofFormatService, TypedArrayEncoder, + ProofState, } from '@credo-ts/core' import { randomUUID } from 'crypto' @@ -38,7 +41,12 @@ import { CheqdDidRegistrar, CheqdDidResolver, CheqdModule } from '../../cheqd' import { getCheqdModuleConfig } from '../../cheqd/tests/setupCheqdModule' import { sleep } from '../../core/src/utils/sleep' import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' -import { getInMemoryAgentOptions, makeConnection, waitForCredentialRecordSubject } from '../../core/tests/helpers' +import { + getInMemoryAgentOptions, + makeConnection, + waitForCredentialRecordSubject, + waitForProofExchangeRecordSubject, +} from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../src' import { DataIntegrityCredentialFormatService } from '../src/formats/DataIntegrityCredentialFormatService' @@ -215,6 +223,83 @@ interface SetupAnonCredsTestsReturn + predicates?: Record + } +}) { + let holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + state: ProofState.RequestReceived, + }) + + let verifierProofExchangeRecord = await verifierAgent.proofs.requestProof({ + connectionId: verifierHolderConnectionId, + proofFormats: { + anoncreds: { + name: 'Test Proof Request', + requested_attributes: attributes, + requested_predicates: predicates, + version: '1.0', + }, + }, + protocolVersion: 'v2', + }) + + let holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const selectedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const verifierProofExchangeRecordPromise = waitForProofExchangeRecordSubject(verifierReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holderAgent.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { anoncreds: selectedCredentials.proofFormats.anoncreds }, + }) + + verifierProofExchangeRecord = await verifierProofExchangeRecordPromise + + // assert presentation is valid + expect(verifierProofExchangeRecord.isVerified).toBe(true) + + holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + verifierProofExchangeRecord = await verifierAgent.proofs.acceptPresentation({ + proofRecordId: verifierProofExchangeRecord.id, + }) + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + return { + verifierProofExchangeRecord, + holderProofExchangeRecord, + } +} + export async function setupAnonCredsTests< VerifierName extends string | undefined = undefined, CreateConnections extends boolean = true From e4a26b67a78e6b47b996149e3c0e36bbd4bc9d69 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 18 Feb 2024 10:00:59 +0100 Subject: [PATCH 35/38] fix: change import path --- packages/anoncreds/tests/anoncredsSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/anoncreds/tests/anoncredsSetup.ts b/packages/anoncreds/tests/anoncredsSetup.ts index 9836ce7454..193a9ecfd2 100644 --- a/packages/anoncreds/tests/anoncredsSetup.ts +++ b/packages/anoncreds/tests/anoncredsSetup.ts @@ -37,7 +37,7 @@ import { } from '@credo-ts/core' import { randomUUID } from 'crypto' -import { CheqdDidRegistrar, CheqdDidResolver, CheqdModule } from '../../cheqd' +import { CheqdDidRegistrar, CheqdDidResolver, CheqdModule } from '../../cheqd/src/index' import { getCheqdModuleConfig } from '../../cheqd/tests/setupCheqdModule' import { sleep } from '../../core/src/utils/sleep' import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' From 519e863aba445c0064c6875ba9a5d3554cee96b3 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 22 Feb 2024 12:17:11 +0100 Subject: [PATCH 36/38] fix: update anoncreds to stable release --- demo-openid/package.json | 2 +- demo/package.json | 2 +- packages/anoncreds/package.json | 6 +++--- .../anoncreds-rs/AnonCredsRsVerifierService.ts | 2 ++ .../data-integrity-flow-anoncreds-pex.test.ts | 2 +- yarn.lock | 18 +++++++++--------- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/demo-openid/package.json b/demo-openid/package.json index 1e33aff1a3..af02d322f2 100644 --- a/demo-openid/package.json +++ b/demo-openid/package.json @@ -15,7 +15,7 @@ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.11", + "@hyperledger/anoncreds-nodejs": "^0.2.0", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.6", "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", "express": "^4.18.1", diff --git a/demo/package.json b/demo/package.json index 8d698595a1..77f3161cc3 100644 --- a/demo/package.json +++ b/demo/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.11", + "@hyperledger/anoncreds-nodejs": "^0.2.0", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.6", "inquirer": "^8.2.5" }, diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index af8fc72520..9cbcffd22a 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -34,13 +34,13 @@ }, "devDependencies": { "@credo-ts/node": "0.4.2", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.11", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.11", + "@hyperledger/anoncreds-nodejs": "^0.2.0", + "@hyperledger/anoncreds-shared": "^0.2.0", "rimraf": "^4.4.0", "rxjs": "^7.8.0", "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.11" + "@hyperledger/anoncreds-shared": "^0.2.0" } } diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts index 25aa2005a3..56c2293ea5 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsVerifierService.ts @@ -192,6 +192,8 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { let result = false const presentationJson = JsonTransformer.toJSON(options.presentation) + if ('presentation_submission' in presentationJson) delete presentationJson.presentation_submission + let w3cPresentation: W3cPresentation | undefined try { w3cPresentation = W3cPresentation.fromJson(presentationJson) diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts index a1b2435b39..7e70b0de5d 100644 --- a/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts @@ -24,7 +24,7 @@ import { presentationDefinition } from './fixtures/presentation-definition' const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' -describe('anoncreds w3c data integrity e2e tests', () => { +describe('anoncreds w3c data integrity tests', () => { let issuerAgent: AnonCredsTestsAgent let holderAgent: AnonCredsTestsAgent let credentialDefinitionId: string diff --git a/yarn.lock b/yarn.lock index 017d09fc35..095e8d388a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1263,22 +1263,22 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@hyperledger/anoncreds-nodejs@^0.2.0-dev.11": - version "0.2.0-dev.11" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.11.tgz#de89612c64a5531790680e0f92ac9060321202d0" - integrity sha512-gBKWAc6ViyAbntLGJKwMN1tC6S8VxGDRgJiZxw6KJb/1H7wAB62kpS0oT80SwCEhbirgmOhLZQUlwmYoUqHddg== +"@hyperledger/anoncreds-nodejs@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0.tgz#cca538c51a637fb9cd2c231b778c27f838a1ed30" + integrity sha512-OAjzdAZv+nzTGfDyQi/pR3ztfYzbvCbALx8RbibAOe2y2Zja7kWcIpwmnDc/PyYI/B3xrgl5jiLslOPrZo35hA== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/anoncreds-shared" "0.2.0-dev.11" + "@hyperledger/anoncreds-shared" "0.2.0" "@mapbox/node-pre-gyp" "^1.0.11" ref-array-di "1.2.2" ref-struct-di "1.1.1" -"@hyperledger/anoncreds-shared@0.2.0-dev.11", "@hyperledger/anoncreds-shared@^0.2.0-dev.11": - version "0.2.0-dev.11" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.11.tgz#2ee81aad40be00a9cf420f43ddf0249088f60368" - integrity sha512-8Pspg14+/q7ayGGXk9y1wbrgZtyeEON0z6nxmK2KkcKv0EG2GL/590+xlroRWo8NDjCDVkvHIMfH+qeXGcJO0Q== +"@hyperledger/anoncreds-shared@0.2.0", "@hyperledger/anoncreds-shared@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0.tgz#923feb22ae06a265e8f9013bb546535eaf4bcaf2" + integrity sha512-ZVSivQgCisao/5vsuSb0KmvwJ227pGm3Wpb6KjPgFlea+F7e7cKAxwtrDBIReKe6E14OqysGte8TMozHUFldAA== "@hyperledger/aries-askar-nodejs@^0.2.0", "@hyperledger/aries-askar-nodejs@^0.2.0-dev.6": version "0.2.0" From 7d24ab0104af6ed01852b5f47e5278f7836479e7 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 22 Feb 2024 13:54:07 +0100 Subject: [PATCH 37/38] fix: AnonCredsClaimRecord value should only be string | number for now --- packages/anoncreds/src/utils/credential.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/anoncreds/src/utils/credential.ts b/packages/anoncreds/src/utils/credential.ts index e8623f6583..2b4f41c8f7 100644 --- a/packages/anoncreds/src/utils/credential.ts +++ b/packages/anoncreds/src/utils/credential.ts @@ -4,7 +4,7 @@ import type { CredentialPreviewAttributeOptions, LinkedAttachment } from '@credo import { CredoError, Hasher, TypedArrayEncoder, encodeAttachment } from '@credo-ts/core' import bigInt from 'big-integer' -export type AnonCredsClaimRecord = Record +export type AnonCredsClaimRecord = Record export interface AnonCredsCredentialValue { raw: string From 8d6e5344876f999627a41b2b295ce73227cd20c5 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 23 Feb 2024 13:09:30 +0100 Subject: [PATCH 38/38] refactor: find and replace npm scope (#1712) --- demo-openid/package.json | 6 +++--- demo/package.json | 6 +++--- packages/anoncreds/package.json | 6 +++--- yarn.lock | 22 +++++++++++----------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/demo-openid/package.json b/demo-openid/package.json index af02d322f2..731e6cccba 100644 --- a/demo-openid/package.json +++ b/demo-openid/package.json @@ -15,9 +15,9 @@ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.6", - "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", + "@hyperledger/anoncreds-nodejs": "^0.2.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0", + "@hyperledger/indy-vdr-nodejs": "^0.2.0", "express": "^4.18.1", "inquirer": "^8.2.5" }, diff --git a/demo/package.json b/demo/package.json index 77f3161cc3..5ac6f7fa53 100644 --- a/demo/package.json +++ b/demo/package.json @@ -14,9 +14,9 @@ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { - "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", - "@hyperledger/anoncreds-nodejs": "^0.2.0", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.6", + "@hyperledger/indy-vdr-nodejs": "^0.2.0", + "@hyperledger/anoncreds-nodejs": "^0.2.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0", "inquirer": "^8.2.5" }, "devDependencies": { diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index 9cbcffd22a..42bff03d20 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -33,14 +33,14 @@ "reflect-metadata": "^0.1.13" }, "devDependencies": { + "@hyperledger/anoncreds-nodejs": "^0.2.1", + "@hyperledger/anoncreds-shared": "^0.2.1", "@credo-ts/node": "0.4.2", - "@hyperledger/anoncreds-nodejs": "^0.2.0", - "@hyperledger/anoncreds-shared": "^0.2.0", "rimraf": "^4.4.0", "rxjs": "^7.8.0", "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0" + "@hyperledger/anoncreds-shared": "^0.2.1" } } diff --git a/yarn.lock b/yarn.lock index ba65f354fe..22677833db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1263,24 +1263,24 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@hyperledger/anoncreds-nodejs@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0.tgz#cca538c51a637fb9cd2c231b778c27f838a1ed30" - integrity sha512-OAjzdAZv+nzTGfDyQi/pR3ztfYzbvCbALx8RbibAOe2y2Zja7kWcIpwmnDc/PyYI/B3xrgl5jiLslOPrZo35hA== +"@hyperledger/anoncreds-nodejs@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.1.tgz#7dbde3e878758371e4d44542daa7f54ecf48f38e" + integrity sha512-wfQEVSqYHq6mQFTLRMVayyi8kbHlz3RGEIe10JOQSHCw4ZCTifQ1XuVajSwOj8ykNYwxuckcfNikJtJScs7l+w== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/anoncreds-shared" "0.2.0" + "@hyperledger/anoncreds-shared" "0.2.1" "@mapbox/node-pre-gyp" "^1.0.11" ref-array-di "1.2.2" ref-struct-di "1.1.1" -"@hyperledger/anoncreds-shared@0.2.0", "@hyperledger/anoncreds-shared@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0.tgz#923feb22ae06a265e8f9013bb546535eaf4bcaf2" - integrity sha512-ZVSivQgCisao/5vsuSb0KmvwJ227pGm3Wpb6KjPgFlea+F7e7cKAxwtrDBIReKe6E14OqysGte8TMozHUFldAA== +"@hyperledger/anoncreds-shared@0.2.1", "@hyperledger/anoncreds-shared@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.1.tgz#7a8be78473e8cdd33b73ccdf2e9b838226aef0f9" + integrity sha512-QpkmsiDBto4B3MS7+tJKn8DHCuhaZuzPKy+SoSAIH8wrjBmQ4NQqzMBZXs0z0JnNr1egkIFR3HIFsIu9ayK20g== -"@hyperledger/aries-askar-nodejs@^0.2.0", "@hyperledger/aries-askar-nodejs@^0.2.0-dev.6": +"@hyperledger/aries-askar-nodejs@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.2.0.tgz#7a0b469184f0682d0e31955e29d091956f662273" integrity sha512-d73D2zK1f1cM5y8MFp4BK+NvkquujDlRr91THpxkuRwmLf407gibOY3G4OdGIkL1kQtorGM5c5U0/qMzW+8E1Q== @@ -1300,7 +1300,7 @@ dependencies: buffer "^6.0.3" -"@hyperledger/indy-vdr-nodejs@^0.2.0", "@hyperledger/indy-vdr-nodejs@^0.2.0-dev.6": +"@hyperledger/indy-vdr-nodejs@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.2.0.tgz#c5fd2c211d5a2b2a0637efa6b9636b208d919c06" integrity sha512-yv+p0mU9NBUgmUDJijNgxtLonhzhDP54wRl4Mfn/s/ZyzLbEQakswmqa2sX0mYQDTLG14iq5uEN6d0eRzUtDeg==